Merge "Revert "Remove dead code #11: Remove more unused classes""
diff --git a/api/current.txt b/api/current.txt
index 120025d..7ea866a 100644
--- a/api/current.txt
+++ b/api/current.txt
@@ -482,7 +482,7 @@
     field public static final int dialogTitle = 16843250; // 0x10101f2
     field public static final int digits = 16843110; // 0x1010166
     field public static final int direction = 16843217; // 0x10101d1
-    field public static final deprecated int directionDescriptions = 16843681; // 0x10103a1
+    field public static final int directionDescriptions = 16843681; // 0x10103a1
     field public static final int directionPriority = 16843218; // 0x10101d2
     field public static final int disableDependentsState = 16843249; // 0x10101f1
     field public static final int disabledAlpha = 16842803; // 0x1010033
@@ -1200,7 +1200,7 @@
     field public static final int tag = 16842961; // 0x10100d1
     field public static final int targetActivity = 16843266; // 0x1010202
     field public static final int targetClass = 16842799; // 0x101002f
-    field public static final deprecated int targetDescriptions = 16843680; // 0x10103a0
+    field public static final int targetDescriptions = 16843680; // 0x10103a0
     field public static final int targetId = 16843740; // 0x10103dc
     field public static final int targetName = 16843853; // 0x101044d
     field public static final int targetPackage = 16842785; // 0x1010021
diff --git a/core/java/com/android/internal/widget/FaceUnlockView.java b/core/java/com/android/internal/widget/FaceUnlockView.java
new file mode 100644
index 0000000..121e601
--- /dev/null
+++ b/core/java/com/android/internal/widget/FaceUnlockView.java
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.widget.RelativeLayout;
+
+public class FaceUnlockView extends RelativeLayout {
+    private static final String TAG = "FaceUnlockView";
+
+    public FaceUnlockView(Context context) {
+        this(context, null);
+    }
+
+    public FaceUnlockView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    private int resolveMeasured(int measureSpec, int desired)
+    {
+        int result = 0;
+        int specSize = MeasureSpec.getSize(measureSpec);
+        switch (MeasureSpec.getMode(measureSpec)) {
+            case MeasureSpec.UNSPECIFIED:
+                result = desired;
+                break;
+            case MeasureSpec.AT_MOST:
+                result = Math.max(specSize, desired);
+                break;
+            case MeasureSpec.EXACTLY:
+            default:
+                result = specSize;
+        }
+        return result;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int minimumWidth = getSuggestedMinimumWidth();
+        final int minimumHeight = getSuggestedMinimumHeight();
+        int viewWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
+        int viewHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
+
+        final int chosenSize = Math.min(viewWidth, viewHeight);
+        final int newWidthMeasureSpec =
+                MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST);
+        final int newHeightMeasureSpec =
+                MeasureSpec.makeMeasureSpec(chosenSize, MeasureSpec.AT_MOST);
+
+        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
+    }
+}
diff --git a/core/java/com/android/internal/widget/SizeAdaptiveLayout.java b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java
new file mode 100644
index 0000000..5f3c5f9
--- /dev/null
+++ b/core/java/com/android/internal/widget/SizeAdaptiveLayout.java
@@ -0,0 +1,442 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import java.lang.Math;
+
+import com.android.internal.R;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.StateSet;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * A layout that switches between its children based on the requested layout height.
+ * Each child specifies its minimum and maximum valid height.  Results are undefined
+ * if children specify overlapping ranges.  A child may specify the maximum height
+ * as 'unbounded' to indicate that it is willing to be displayed arbitrarily tall.
+ *
+ * <p>
+ * See {@link SizeAdaptiveLayout.LayoutParams} for a full description of the
+ * layout parameters used by SizeAdaptiveLayout.
+ */
+@RemoteView
+public class SizeAdaptiveLayout extends ViewGroup {
+
+    private static final String TAG = "SizeAdaptiveLayout";
+    private static final boolean DEBUG = false;
+    private static final boolean REPORT_BAD_BOUNDS = true;
+    private static final long CROSSFADE_TIME = 250;
+
+    // TypedArray indices
+    private static final int MIN_VALID_HEIGHT =
+            R.styleable.SizeAdaptiveLayout_Layout_layout_minHeight;
+    private static final int MAX_VALID_HEIGHT =
+            R.styleable.SizeAdaptiveLayout_Layout_layout_maxHeight;
+
+    // view state
+    private View mActiveChild;
+    private View mLastActive;
+
+    // animation state
+    private AnimatorSet mTransitionAnimation;
+    private AnimatorListener mAnimatorListener;
+    private ObjectAnimator mFadePanel;
+    private ObjectAnimator mFadeView;
+    private int mCanceledAnimationCount;
+    private View mEnteringView;
+    private View mLeavingView;
+    // View used to hide larger views under smaller ones to create a uniform crossfade
+    private View mModestyPanel;
+    private int mModestyPanelTop;
+
+    public SizeAdaptiveLayout(Context context) {
+        this(context, null);
+    }
+
+    public SizeAdaptiveLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public SizeAdaptiveLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SizeAdaptiveLayout(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initialize();
+    }
+
+    private void initialize() {
+        mModestyPanel = new View(getContext());
+        // If the SizeAdaptiveLayout has a solid background, use it as a transition hint.
+        Drawable background = getBackground();
+        if (background instanceof StateListDrawable) {
+            StateListDrawable sld = (StateListDrawable) background;
+            sld.setState(StateSet.WILD_CARD);
+            background = sld.getCurrent();
+        }
+        if (background instanceof ColorDrawable) {
+            mModestyPanel.setBackgroundDrawable(background);
+        }
+        SizeAdaptiveLayout.LayoutParams layout =
+                new SizeAdaptiveLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                                                    ViewGroup.LayoutParams.MATCH_PARENT);
+        mModestyPanel.setLayoutParams(layout);
+        addView(mModestyPanel);
+        mFadePanel = ObjectAnimator.ofFloat(mModestyPanel, "alpha", 0f);
+        mFadeView = ObjectAnimator.ofFloat(null, "alpha", 0f);
+        mAnimatorListener = new BringToFrontOnEnd();
+        mTransitionAnimation = new AnimatorSet();
+        mTransitionAnimation.play(mFadeView).with(mFadePanel);
+        mTransitionAnimation.setDuration(CROSSFADE_TIME);
+        mTransitionAnimation.addListener(mAnimatorListener);
+    }
+
+    /**
+     * Visible for testing
+     * @hide
+     */
+    public Animator getTransitionAnimation() {
+        return mTransitionAnimation;
+    }
+
+    /**
+     * Visible for testing
+     * @hide
+     */
+    public View getModestyPanel() {
+        return mModestyPanel;
+    }
+
+    @Override
+    public void onAttachedToWindow() {
+        mLastActive = null;
+        // make sure all views start off invisible.
+        for (int i = 0; i < getChildCount(); i++) {
+            getChildAt(i).setVisibility(View.GONE);
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (DEBUG) Log.d(TAG, this + " measure spec: " +
+                         MeasureSpec.toString(heightMeasureSpec));
+        View model = selectActiveChild(heightMeasureSpec);
+        if (model == null) {
+            setMeasuredDimension(0, 0);
+            return;
+        }
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) model.getLayoutParams();
+        if (DEBUG) Log.d(TAG, "active min: " + lp.minHeight + " max: " + lp.maxHeight);
+        measureChild(model, widthMeasureSpec, heightMeasureSpec);
+        int childHeight = model.getMeasuredHeight();
+        int childWidth = model.getMeasuredHeight();
+        int childState = combineMeasuredStates(0, model.getMeasuredState());
+        if (DEBUG) Log.d(TAG, "measured child at: " + childHeight);
+        int resolvedWidth = resolveSizeAndState(childWidth, widthMeasureSpec, childState);
+        int resolvedHeight = resolveSizeAndState(childHeight, heightMeasureSpec, childState);
+        if (DEBUG) Log.d(TAG, "resolved to: " + resolvedHeight);
+        int boundedHeight = clampSizeToBounds(resolvedHeight, model);
+        if (DEBUG) Log.d(TAG, "bounded to: " + boundedHeight);
+        setMeasuredDimension(resolvedWidth, boundedHeight);
+    }
+
+    private int clampSizeToBounds(int measuredHeight, View child) {
+        SizeAdaptiveLayout.LayoutParams lp =
+                (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams();
+        int heightIn = View.MEASURED_SIZE_MASK & measuredHeight;
+        int height = Math.max(heightIn, lp.minHeight);
+        if (lp.maxHeight != SizeAdaptiveLayout.LayoutParams.UNBOUNDED) {
+            height = Math.min(height, lp.maxHeight);
+        }
+
+        if (REPORT_BAD_BOUNDS && heightIn != height) {
+            Log.d(TAG, this + "child view " + child + " " +
+                  "measured out of bounds at " + heightIn +"px " +
+                  "clamped to " + height + "px");
+        }
+
+        return height;
+    }
+
+    //TODO extend to width and height
+    private View selectActiveChild(int heightMeasureSpec) {
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        View unboundedView = null;
+        View tallestView = null;
+        int tallestViewSize = 0;
+        View smallestView = null;
+        int smallestViewSize = Integer.MAX_VALUE;
+        for (int i = 0; i < getChildCount(); i++) {
+            View child = getChildAt(i);
+            if (child != mModestyPanel) {
+                SizeAdaptiveLayout.LayoutParams lp =
+                    (SizeAdaptiveLayout.LayoutParams) child.getLayoutParams();
+                if (DEBUG) Log.d(TAG, "looking at " + i +
+                                 " with min: " + lp.minHeight +
+                                 " max: " +  lp.maxHeight);
+                if (lp.maxHeight == SizeAdaptiveLayout.LayoutParams.UNBOUNDED &&
+                    unboundedView == null) {
+                    unboundedView = child;
+                }
+                if (lp.maxHeight > tallestViewSize) {
+                    tallestViewSize = lp.maxHeight;
+                    tallestView = child;
+                }
+                if (lp.minHeight < smallestViewSize) {
+                    smallestViewSize = lp.minHeight;
+                    smallestView = child;
+                }
+                if (heightMode != MeasureSpec.UNSPECIFIED &&
+                    heightSize >= lp.minHeight && heightSize <= lp.maxHeight) {
+                    if (DEBUG) Log.d(TAG, "  found exact match, finishing early");
+                    return child;
+                }
+            }
+        }
+        if (unboundedView != null) {
+            tallestView = unboundedView;
+        }
+        if (heightMode == MeasureSpec.UNSPECIFIED || heightSize > tallestViewSize) {
+            return tallestView;
+        } else {
+            return smallestView;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (DEBUG) Log.d(TAG, this + " onlayout height: " + (bottom - top));
+        mLastActive = mActiveChild;
+        int measureSpec = View.MeasureSpec.makeMeasureSpec(bottom - top,
+                                                           View.MeasureSpec.EXACTLY);
+        mActiveChild = selectActiveChild(measureSpec);
+        if (mActiveChild == null) return;
+
+        mActiveChild.setVisibility(View.VISIBLE);
+
+        if (mLastActive != mActiveChild && mLastActive != null) {
+            if (DEBUG) Log.d(TAG, this + " changed children from: " + mLastActive +
+                    " to: " + mActiveChild);
+
+            mEnteringView = mActiveChild;
+            mLeavingView = mLastActive;
+
+            mEnteringView.setAlpha(1f);
+
+            mModestyPanel.setAlpha(1f);
+            mModestyPanel.bringToFront();
+            mModestyPanelTop = mLeavingView.getHeight();
+            mModestyPanel.setVisibility(View.VISIBLE);
+            // TODO: mModestyPanel background should be compatible with mLeavingView
+
+            mLeavingView.bringToFront();
+
+            if (mTransitionAnimation.isRunning()) {
+                mTransitionAnimation.cancel();
+            }
+            mFadeView.setTarget(mLeavingView);
+            mFadeView.setFloatValues(0f);
+            mFadePanel.setFloatValues(0f);
+            mTransitionAnimation.setupStartValues();
+            mTransitionAnimation.start();
+        }
+        final int childWidth = mActiveChild.getMeasuredWidth();
+        final int childHeight = mActiveChild.getMeasuredHeight();
+        // TODO investigate setting LAYER_TYPE_HARDWARE on mLastActive
+        mActiveChild.layout(0, 0, childWidth, childHeight);
+
+        if (DEBUG) Log.d(TAG, "got modesty offset of " + mModestyPanelTop);
+        mModestyPanel.layout(0, mModestyPanelTop, childWidth, mModestyPanelTop + childHeight);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        if (DEBUG) Log.d(TAG, "generate layout from attrs");
+        return new SizeAdaptiveLayout.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (DEBUG) Log.d(TAG, "generate default layout from viewgroup");
+        return new SizeAdaptiveLayout.LayoutParams(p);
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        if (DEBUG) Log.d(TAG, "generate default layout from null");
+        return new SizeAdaptiveLayout.LayoutParams();
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof SizeAdaptiveLayout.LayoutParams;
+    }
+
+    /**
+     * Per-child layout information associated with ViewSizeAdaptiveLayout.
+     *
+     * TODO extend to width and height
+     *
+     * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_minHeight
+     * @attr ref android.R.styleable#SizeAdaptiveLayout_Layout_layout_maxHeight
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+
+        /**
+         * Indicates the minimum valid height for the child.
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int minHeight;
+
+        /**
+         * Indicates the maximum valid height for the child.
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int maxHeight;
+
+        /**
+         * Constant value for maxHeight that indicates there is not maximum height.
+         */
+        public static final int UNBOUNDED = -1;
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            if (DEBUG) {
+                Log.d(TAG, "construct layout from attrs");
+                for (int i = 0; i < attrs.getAttributeCount(); i++) {
+                    Log.d(TAG, " " + attrs.getAttributeName(i) + " = " +
+                          attrs.getAttributeValue(i));
+                }
+            }
+            TypedArray a =
+                    c.obtainStyledAttributes(attrs,
+                            R.styleable.SizeAdaptiveLayout_Layout);
+
+            minHeight = a.getDimensionPixelSize(MIN_VALID_HEIGHT, 0);
+            if (DEBUG) Log.d(TAG, "got minHeight of: " + minHeight);
+
+            try {
+                maxHeight = a.getLayoutDimension(MAX_VALID_HEIGHT, UNBOUNDED);
+                if (DEBUG) Log.d(TAG, "got maxHeight of: " + maxHeight);
+            } catch (Exception e) {
+                if (DEBUG) Log.d(TAG, "caught exception looking for maxValidHeight " + e);
+            }
+
+            a.recycle();
+        }
+
+        /**
+         * Creates a new set of layout parameters with the specified width, height
+         * and valid height bounds.
+         *
+         * @param width the width, either {@link #MATCH_PARENT},
+         *        {@link #WRAP_CONTENT} or a fixed size in pixels
+         * @param height the height, either {@link #MATCH_PARENT},
+         *        {@link #WRAP_CONTENT} or a fixed size in pixels
+         * @param minHeight the minimum height of this child
+         * @param maxHeight the maximum height of this child
+         *        or {@link #UNBOUNDED} if the child can grow forever
+         */
+        public LayoutParams(int width, int height, int minHeight, int maxHeight) {
+            super(width, height);
+            this.minHeight = minHeight;
+            this.maxHeight = maxHeight;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int width, int height) {
+            this(width, height, UNBOUNDED, UNBOUNDED);
+        }
+
+        /**
+         * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}.
+         */
+        public LayoutParams() {
+            this(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams p) {
+            super(p);
+            minHeight = UNBOUNDED;
+            maxHeight = UNBOUNDED;
+        }
+
+        public String debug(String output) {
+            return output + "SizeAdaptiveLayout.LayoutParams={" +
+                    ", max=" + maxHeight +
+                    ", max=" + minHeight + "}";
+        }
+    }
+
+    class BringToFrontOnEnd implements AnimatorListener {
+        @Override
+            public void onAnimationEnd(Animator animation) {
+            if (mCanceledAnimationCount == 0) {
+                mLeavingView.setVisibility(View.GONE);
+                mModestyPanel.setVisibility(View.GONE);
+                mEnteringView.bringToFront();
+                mEnteringView = null;
+                mLeavingView = null;
+            } else {
+                mCanceledAnimationCount--;
+            }
+        }
+
+        @Override
+            public void onAnimationCancel(Animator animation) {
+            mCanceledAnimationCount++;
+        }
+
+        @Override
+            public void onAnimationRepeat(Animator animation) {
+            if (DEBUG) Log.d(TAG, "fade animation repeated: should never happen.");
+            assert(false);
+        }
+
+        @Override
+            public void onAnimationStart(Animator animation) {
+        }
+    }
+}
diff --git a/core/java/com/android/internal/widget/WaveView.java b/core/java/com/android/internal/widget/WaveView.java
new file mode 100644
index 0000000..9e7a649
--- /dev/null
+++ b/core/java/com/android/internal/widget/WaveView.java
@@ -0,0 +1,663 @@
+/*
+ * Copyright (C) 2010 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import java.util.ArrayList;
+
+import android.animation.ValueAnimator;
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.graphics.Canvas;
+import android.graphics.drawable.BitmapDrawable;
+import android.media.AudioAttributes;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+
+/**
+ * A special widget containing a center and outer ring. Moving the center ring to the outer ring
+ * causes an event that can be caught by implementing OnTriggerListener.
+ */
+public class WaveView extends View implements ValueAnimator.AnimatorUpdateListener {
+    private static final String TAG = "WaveView";
+    private static final boolean DBG = false;
+    private static final int WAVE_COUNT = 20; // default wave count
+    private static final long VIBRATE_SHORT = 20;  // msec
+    private static final long VIBRATE_LONG = 20;  // msec
+
+    // Lock state machine states
+    private static final int STATE_RESET_LOCK = 0;
+    private static final int STATE_READY = 1;
+    private static final int STATE_START_ATTEMPT = 2;
+    private static final int STATE_ATTEMPTING = 3;
+    private static final int STATE_UNLOCK_ATTEMPT = 4;
+    private static final int STATE_UNLOCK_SUCCESS = 5;
+
+    // Animation properties.
+    private static final long DURATION = 300; // duration of transitional animations
+    private static final long FINAL_DURATION = 200; // duration of final animations when unlocking
+    private static final long RING_DELAY = 1300; // when to start fading animated rings
+    private static final long FINAL_DELAY = 200; // delay for unlock success animation
+    private static final long SHORT_DELAY = 100; // for starting one animation after another.
+    private static final long WAVE_DURATION = 2000; // amount of time for way to expand/decay
+    private static final long RESET_TIMEOUT = 3000; // elapsed time of inactivity before we reset
+    private static final long DELAY_INCREMENT = 15; // increment per wave while tracking motion
+    private static final long DELAY_INCREMENT2 = 12; // increment per wave while not tracking
+    private static final long WAVE_DELAY = WAVE_DURATION / WAVE_COUNT; // initial propagation delay
+
+    /**
+     * The scale by which to multiply the unlock handle width to compute the radius
+     * in which it can be grabbed when accessibility is disabled.
+     */
+    private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED = 0.5f;
+
+    /**
+     * The scale by which to multiply the unlock handle width to compute the radius
+     * in which it can be grabbed when accessibility is enabled (more generous).
+     */
+    private static final float GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.0f;
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    private Vibrator mVibrator;
+    private OnTriggerListener mOnTriggerListener;
+    private ArrayList<DrawableHolder> mDrawables = new ArrayList<DrawableHolder>(3);
+    private ArrayList<DrawableHolder> mLightWaves = new ArrayList<DrawableHolder>(WAVE_COUNT);
+    private boolean mFingerDown = false;
+    private float mRingRadius = 182.0f; // Radius of bitmap ring. Used to snap halo to it
+    private int mSnapRadius = 136; // minimum threshold for drag unlock
+    private int mWaveCount = WAVE_COUNT;  // number of waves
+    private long mWaveTimerDelay = WAVE_DELAY;
+    private int mCurrentWave = 0;
+    private float mLockCenterX; // center of widget as dictated by widget size
+    private float mLockCenterY;
+    private float mMouseX; // current mouse position as of last touch event
+    private float mMouseY;
+    private DrawableHolder mUnlockRing;
+    private DrawableHolder mUnlockDefault;
+    private DrawableHolder mUnlockHalo;
+    private int mLockState = STATE_RESET_LOCK;
+    private int mGrabbedState = OnTriggerListener.NO_HANDLE;
+    private boolean mWavesRunning;
+    private boolean mFinishWaves;
+
+    public WaveView(Context context) {
+        this(context, null);
+    }
+
+    public WaveView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.WaveView);
+        // mOrientation = a.getInt(R.styleable.WaveView_orientation, HORIZONTAL);
+        // a.recycle();
+
+        initDrawables();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mLockCenterX = 0.5f * w;
+        mLockCenterY = 0.5f * h;
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    @Override
+    protected int getSuggestedMinimumWidth() {
+        // View should be large enough to contain the unlock ring + halo
+        return mUnlockRing.getWidth() + mUnlockHalo.getWidth();
+    }
+
+    @Override
+    protected int getSuggestedMinimumHeight() {
+        // View should be large enough to contain the unlock ring + halo
+        return mUnlockRing.getHeight() + mUnlockHalo.getHeight();
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
+        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
+        int width;
+        int height;
+
+        if (widthSpecMode == MeasureSpec.AT_MOST) {
+            width = Math.min(widthSpecSize, getSuggestedMinimumWidth());
+        } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+            width = widthSpecSize;
+        } else {
+            width = getSuggestedMinimumWidth();
+        }
+
+        if (heightSpecMode == MeasureSpec.AT_MOST) {
+            height = Math.min(heightSpecSize, getSuggestedMinimumWidth());
+        } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+            height = heightSpecSize;
+        } else {
+            height = getSuggestedMinimumHeight();
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    private void initDrawables() {
+        mUnlockRing = new DrawableHolder(createDrawable(R.drawable.unlock_ring));
+        mUnlockRing.setX(mLockCenterX);
+        mUnlockRing.setY(mLockCenterY);
+        mUnlockRing.setScaleX(0.1f);
+        mUnlockRing.setScaleY(0.1f);
+        mUnlockRing.setAlpha(0.0f);
+        mDrawables.add(mUnlockRing);
+
+        mUnlockDefault = new DrawableHolder(createDrawable(R.drawable.unlock_default));
+        mUnlockDefault.setX(mLockCenterX);
+        mUnlockDefault.setY(mLockCenterY);
+        mUnlockDefault.setScaleX(0.1f);
+        mUnlockDefault.setScaleY(0.1f);
+        mUnlockDefault.setAlpha(0.0f);
+        mDrawables.add(mUnlockDefault);
+
+        mUnlockHalo = new DrawableHolder(createDrawable(R.drawable.unlock_halo));
+        mUnlockHalo.setX(mLockCenterX);
+        mUnlockHalo.setY(mLockCenterY);
+        mUnlockHalo.setScaleX(0.1f);
+        mUnlockHalo.setScaleY(0.1f);
+        mUnlockHalo.setAlpha(0.0f);
+        mDrawables.add(mUnlockHalo);
+
+        BitmapDrawable wave = createDrawable(R.drawable.unlock_wave);
+        for (int i = 0; i < mWaveCount; i++) {
+            DrawableHolder holder = new DrawableHolder(wave);
+            mLightWaves.add(holder);
+            holder.setAlpha(0.0f);
+        }
+    }
+
+    private void waveUpdateFrame(float mouseX, float mouseY, boolean fingerDown) {
+        double distX = mouseX - mLockCenterX;
+        double distY = mouseY - mLockCenterY;
+        int dragDistance = (int) Math.ceil(Math.hypot(distX, distY));
+        double touchA = Math.atan2(distX, distY);
+        float ringX = (float) (mLockCenterX + mRingRadius * Math.sin(touchA));
+        float ringY = (float) (mLockCenterY + mRingRadius * Math.cos(touchA));
+
+        switch (mLockState) {
+            case STATE_RESET_LOCK:
+                if (DBG) Log.v(TAG, "State RESET_LOCK");
+                mWaveTimerDelay = WAVE_DELAY;
+                for (int i = 0; i < mLightWaves.size(); i++) {
+                    DrawableHolder holder = mLightWaves.get(i);
+                    holder.addAnimTo(300, 0, "alpha", 0.0f, false);
+                }
+                for (int i = 0; i < mLightWaves.size(); i++) {
+                    mLightWaves.get(i).startAnimations(this);
+                }
+
+                mUnlockRing.addAnimTo(DURATION, 0, "x", mLockCenterX, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "y", mLockCenterY, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 0.1f, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 0.1f, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "alpha", 0.0f, true);
+
+                mUnlockDefault.removeAnimationFor("x");
+                mUnlockDefault.removeAnimationFor("y");
+                mUnlockDefault.removeAnimationFor("scaleX");
+                mUnlockDefault.removeAnimationFor("scaleY");
+                mUnlockDefault.removeAnimationFor("alpha");
+                mUnlockDefault.setX(mLockCenterX);
+                mUnlockDefault.setY(mLockCenterY);
+                mUnlockDefault.setScaleX(0.1f);
+                mUnlockDefault.setScaleY(0.1f);
+                mUnlockDefault.setAlpha(0.0f);
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true);
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true);
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true);
+
+                mUnlockHalo.removeAnimationFor("x");
+                mUnlockHalo.removeAnimationFor("y");
+                mUnlockHalo.removeAnimationFor("scaleX");
+                mUnlockHalo.removeAnimationFor("scaleY");
+                mUnlockHalo.removeAnimationFor("alpha");
+                mUnlockHalo.setX(mLockCenterX);
+                mUnlockHalo.setY(mLockCenterY);
+                mUnlockHalo.setScaleX(0.1f);
+                mUnlockHalo.setScaleY(0.1f);
+                mUnlockHalo.setAlpha(0.0f);
+                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "x", mLockCenterX, true);
+                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "y", mLockCenterY, true);
+                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, true);
+                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, true);
+                mUnlockHalo.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, true);
+
+                removeCallbacks(mLockTimerActions);
+
+                mLockState = STATE_READY;
+                break;
+
+            case STATE_READY:
+                if (DBG) Log.v(TAG, "State READY");
+                mWaveTimerDelay = WAVE_DELAY;
+                break;
+
+            case STATE_START_ATTEMPT:
+                if (DBG) Log.v(TAG, "State START_ATTEMPT");
+                mUnlockDefault.removeAnimationFor("x");
+                mUnlockDefault.removeAnimationFor("y");
+                mUnlockDefault.removeAnimationFor("scaleX");
+                mUnlockDefault.removeAnimationFor("scaleY");
+                mUnlockDefault.removeAnimationFor("alpha");
+                mUnlockDefault.setX(mLockCenterX + 182);
+                mUnlockDefault.setY(mLockCenterY);
+                mUnlockDefault.setScaleX(0.1f);
+                mUnlockDefault.setScaleY(0.1f);
+                mUnlockDefault.setAlpha(0.0f);
+
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleX", 1.0f, false);
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "scaleY", 1.0f, false);
+                mUnlockDefault.addAnimTo(DURATION, SHORT_DELAY, "alpha", 1.0f, false);
+
+                mUnlockRing.addAnimTo(DURATION, 0, "scaleX", 1.0f, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "scaleY", 1.0f, true);
+                mUnlockRing.addAnimTo(DURATION, 0, "alpha", 1.0f, true);
+
+                mLockState = STATE_ATTEMPTING;
+                break;
+
+            case STATE_ATTEMPTING:
+                if (DBG) Log.v(TAG, "State ATTEMPTING (fingerDown = " + fingerDown + ")");
+                if (dragDistance > mSnapRadius) {
+                    mFinishWaves = true; // don't start any more waves.
+                    if (fingerDown) {
+                        mUnlockHalo.addAnimTo(0, 0, "x", ringX, true);
+                        mUnlockHalo.addAnimTo(0, 0, "y", ringY, true);
+                        mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true);
+                        mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true);
+                        mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true);
+                    }  else {
+                        if (DBG) Log.v(TAG, "up detected, moving to STATE_UNLOCK_ATTEMPT");
+                        mLockState = STATE_UNLOCK_ATTEMPT;
+                    }
+                } else {
+                    // If waves have stopped, we need to kick them off again...
+                    if (!mWavesRunning) {
+                        mWavesRunning = true;
+                        mFinishWaves = false;
+                        // mWaveTimerDelay = WAVE_DELAY;
+                        postDelayed(mAddWaveAction, mWaveTimerDelay);
+                    }
+                    mUnlockHalo.addAnimTo(0, 0, "x", mouseX, true);
+                    mUnlockHalo.addAnimTo(0, 0, "y", mouseY, true);
+                    mUnlockHalo.addAnimTo(0, 0, "scaleX", 1.0f, true);
+                    mUnlockHalo.addAnimTo(0, 0, "scaleY", 1.0f, true);
+                    mUnlockHalo.addAnimTo(0, 0, "alpha", 1.0f, true);
+                }
+                break;
+
+            case STATE_UNLOCK_ATTEMPT:
+                if (DBG) Log.v(TAG, "State UNLOCK_ATTEMPT");
+                if (dragDistance > mSnapRadius) {
+                    for (int n = 0; n < mLightWaves.size(); n++) {
+                        DrawableHolder wave = mLightWaves.get(n);
+                        long delay = 1000L*(6 + n - mCurrentWave)/10L;
+                        wave.addAnimTo(FINAL_DURATION, delay, "x", ringX, true);
+                        wave.addAnimTo(FINAL_DURATION, delay, "y", ringY, true);
+                        wave.addAnimTo(FINAL_DURATION, delay, "scaleX", 0.1f, true);
+                        wave.addAnimTo(FINAL_DURATION, delay, "scaleY", 0.1f, true);
+                        wave.addAnimTo(FINAL_DURATION, delay, "alpha", 0.0f, true);
+                    }
+                    for (int i = 0; i < mLightWaves.size(); i++) {
+                        mLightWaves.get(i).startAnimations(this);
+                    }
+
+                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "x", ringX, false);
+                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "y", ringY, false);
+                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleX", 0.1f, false);
+                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "scaleY", 0.1f, false);
+                    mUnlockRing.addAnimTo(FINAL_DURATION, 0, "alpha", 0.0f, false);
+
+                    mUnlockRing.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
+
+                    mUnlockDefault.removeAnimationFor("x");
+                    mUnlockDefault.removeAnimationFor("y");
+                    mUnlockDefault.removeAnimationFor("scaleX");
+                    mUnlockDefault.removeAnimationFor("scaleY");
+                    mUnlockDefault.removeAnimationFor("alpha");
+                    mUnlockDefault.setX(ringX);
+                    mUnlockDefault.setY(ringY);
+                    mUnlockDefault.setScaleX(0.1f);
+                    mUnlockDefault.setScaleY(0.1f);
+                    mUnlockDefault.setAlpha(0.0f);
+
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "x", ringX, true);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "y", ringY, true);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleX", 1.0f, true);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "scaleY", 1.0f, true);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, 0, "alpha", 1.0f, true);
+
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false);
+                    mUnlockDefault.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
+
+                    mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "x", ringX, false);
+                    mUnlockHalo.addAnimTo(FINAL_DURATION, 0, "y", ringY, false);
+
+                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleX", 3.0f, false);
+                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "scaleY", 3.0f, false);
+                    mUnlockHalo.addAnimTo(FINAL_DURATION, FINAL_DELAY, "alpha", 0.0f, false);
+
+                    removeCallbacks(mLockTimerActions);
+
+                    postDelayed(mLockTimerActions, RESET_TIMEOUT);
+
+                    dispatchTriggerEvent(OnTriggerListener.CENTER_HANDLE);
+                    mLockState = STATE_UNLOCK_SUCCESS;
+                } else {
+                    mLockState = STATE_RESET_LOCK;
+                }
+                break;
+
+            case STATE_UNLOCK_SUCCESS:
+                if (DBG) Log.v(TAG, "State UNLOCK_SUCCESS");
+                removeCallbacks(mAddWaveAction);
+                break;
+
+            default:
+                if (DBG) Log.v(TAG, "Unknown state " + mLockState);
+                break;
+        }
+        mUnlockDefault.startAnimations(this);
+        mUnlockHalo.startAnimations(this);
+        mUnlockRing.startAnimations(this);
+    }
+
+    BitmapDrawable createDrawable(int resId) {
+        Resources res = getResources();
+        Bitmap bitmap = BitmapFactory.decodeResource(res, resId);
+        return new BitmapDrawable(res, bitmap);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        waveUpdateFrame(mMouseX, mMouseY, mFingerDown);
+        for (int i = 0; i < mDrawables.size(); ++i) {
+            mDrawables.get(i).draw(canvas);
+        }
+        for (int i = 0; i < mLightWaves.size(); ++i) {
+            mLightWaves.get(i).draw(canvas);
+        }
+    }
+
+    private final Runnable mLockTimerActions = new Runnable() {
+        public void run() {
+            if (DBG) Log.v(TAG, "LockTimerActions");
+            // reset lock after inactivity
+            if (mLockState == STATE_ATTEMPTING) {
+                if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK");
+                mLockState = STATE_RESET_LOCK;
+            }
+            // for prototype, reset after successful unlock
+            if (mLockState == STATE_UNLOCK_SUCCESS) {
+                if (DBG) Log.v(TAG, "Timer resets to STATE_RESET_LOCK after success");
+                mLockState = STATE_RESET_LOCK;
+            }
+            invalidate();
+        }
+    };
+
+    private final Runnable mAddWaveAction = new Runnable() {
+        public void run() {
+            double distX = mMouseX - mLockCenterX;
+            double distY = mMouseY - mLockCenterY;
+            int dragDistance = (int) Math.ceil(Math.hypot(distX, distY));
+            if (mLockState == STATE_ATTEMPTING && dragDistance < mSnapRadius
+                    && mWaveTimerDelay >= WAVE_DELAY) {
+                mWaveTimerDelay = Math.min(WAVE_DURATION, mWaveTimerDelay + DELAY_INCREMENT);
+
+                DrawableHolder wave = mLightWaves.get(mCurrentWave);
+                wave.setAlpha(0.0f);
+                wave.setScaleX(0.2f);
+                wave.setScaleY(0.2f);
+                wave.setX(mMouseX);
+                wave.setY(mMouseY);
+
+                wave.addAnimTo(WAVE_DURATION, 0, "x", mLockCenterX, true);
+                wave.addAnimTo(WAVE_DURATION, 0, "y", mLockCenterY, true);
+                wave.addAnimTo(WAVE_DURATION*2/3, 0, "alpha", 1.0f, true);
+                wave.addAnimTo(WAVE_DURATION, 0, "scaleX", 1.0f, true);
+                wave.addAnimTo(WAVE_DURATION, 0, "scaleY", 1.0f, true);
+
+                wave.addAnimTo(1000, RING_DELAY, "alpha", 0.0f, false);
+                wave.startAnimations(WaveView.this);
+
+                mCurrentWave = (mCurrentWave+1) % mWaveCount;
+                if (DBG) Log.v(TAG, "WaveTimerDelay: start new wave in " + mWaveTimerDelay);
+            } else {
+                mWaveTimerDelay += DELAY_INCREMENT2;
+            }
+            if (mFinishWaves) {
+                // sentinel used to restart the waves after they've stopped
+                mWavesRunning = false;
+            } else {
+                postDelayed(mAddWaveAction, mWaveTimerDelay);
+            }
+        }
+    };
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
+            final int action = event.getAction();
+            switch (action) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    event.setAction(MotionEvent.ACTION_DOWN);
+                    break;
+                case MotionEvent.ACTION_HOVER_MOVE:
+                    event.setAction(MotionEvent.ACTION_MOVE);
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    event.setAction(MotionEvent.ACTION_UP);
+                    break;
+            }
+            onTouchEvent(event);
+            event.setAction(action);
+        }
+        return super.onHoverEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int action = event.getAction();
+        mMouseX = event.getX();
+        mMouseY = event.getY();
+        boolean handled = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                removeCallbacks(mLockTimerActions);
+                mFingerDown = true;
+                tryTransitionToStartAttemptState(event);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                tryTransitionToStartAttemptState(event);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_UP:
+                if (DBG) Log.v(TAG, "ACTION_UP");
+                mFingerDown = false;
+                postDelayed(mLockTimerActions, RESET_TIMEOUT);
+                setGrabbedState(OnTriggerListener.NO_HANDLE);
+                // Normally the state machine is driven by user interaction causing redraws.
+                // However, when there's no more user interaction and no running animations,
+                // the state machine stops advancing because onDraw() never gets called.
+                // The following ensures we advance to the next state in this case,
+                // either STATE_UNLOCK_ATTEMPT or STATE_RESET_LOCK.
+                waveUpdateFrame(mMouseX, mMouseY, mFingerDown);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                mFingerDown = false;
+                handled = true;
+                break;
+        }
+        invalidate();
+        return handled ? true : super.onTouchEvent(event);
+    }
+
+    /**
+     * Tries to transition to start attempt state.
+     *
+     * @param event A motion event.
+     */
+    private void tryTransitionToStartAttemptState(MotionEvent event) {
+        final float dx = event.getX() - mUnlockHalo.getX();
+        final float dy = event.getY() - mUnlockHalo.getY();
+        float dist = (float) Math.hypot(dx, dy);
+        if (dist <= getScaledGrabHandleRadius()) {
+            setGrabbedState(OnTriggerListener.CENTER_HANDLE);
+            if (mLockState == STATE_READY) {
+                mLockState = STATE_START_ATTEMPT;
+                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                    announceUnlockHandle();
+                }
+            }
+        }
+    }
+
+    /**
+     * @return The radius in which the handle is grabbed scaled based on
+     *     whether accessibility is enabled.
+     */
+    private float getScaledGrabHandleRadius() {
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mUnlockHalo.getWidth();
+        } else {
+            return GRAB_HANDLE_RADIUS_SCALE_ACCESSIBILITY_DISABLED * mUnlockHalo.getWidth();
+        }
+    }
+
+    /**
+     * Announces the unlock handle if accessibility is enabled.
+     */
+    private void announceUnlockHandle() {
+        setContentDescription(mContext.getString(R.string.description_target_unlock_tablet));
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_FOCUSED);
+        setContentDescription(null);
+    }
+
+    /**
+     * Triggers haptic feedback.
+     */
+    private synchronized void vibrate(long duration) {
+        final boolean hapticEnabled = Settings.System.getIntForUser(
+                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
+                UserHandle.USER_CURRENT) != 0;
+        if (hapticEnabled) {
+            if (mVibrator == null) {
+                mVibrator = (android.os.Vibrator) getContext()
+                        .getSystemService(Context.VIBRATOR_SERVICE);
+            }
+            mVibrator.vibrate(duration, VIBRATION_ATTRIBUTES);
+        }
+    }
+
+    /**
+     * Registers a callback to be invoked when the user triggers an event.
+     *
+     * @param listener the OnDialTriggerListener to attach to this view
+     */
+    public void setOnTriggerListener(OnTriggerListener listener) {
+        mOnTriggerListener = listener;
+    }
+
+    /**
+     * Dispatches a trigger event to listener. Ignored if a listener is not set.
+     * @param whichHandle the handle that triggered the event.
+     */
+    private void dispatchTriggerEvent(int whichHandle) {
+        vibrate(VIBRATE_LONG);
+        if (mOnTriggerListener != null) {
+            mOnTriggerListener.onTrigger(this, whichHandle);
+        }
+    }
+
+    /**
+     * Sets the current grabbed state, and dispatches a grabbed state change
+     * event to our listener.
+     */
+    private void setGrabbedState(int newState) {
+        if (newState != mGrabbedState) {
+            mGrabbedState = newState;
+            if (mOnTriggerListener != null) {
+                mOnTriggerListener.onGrabbedStateChange(this, mGrabbedState);
+            }
+        }
+    }
+
+    public interface OnTriggerListener {
+        /**
+         * Sent when the user releases the handle.
+         */
+        public static final int NO_HANDLE = 0;
+
+        /**
+         * Sent when the user grabs the center handle
+         */
+        public static final int CENTER_HANDLE = 10;
+
+        /**
+         * Called when the user drags the center ring beyond a threshold.
+         */
+        void onTrigger(View v, int whichHandle);
+
+        /**
+         * Called when the "grabbed state" changes (i.e. when the user either grabs or releases
+         * one of the handles.)
+         *
+         * @param v the view that was triggered
+         * @param grabbedState the new state: {@link #NO_HANDLE}, {@link #CENTER_HANDLE},
+         */
+        void onGrabbedStateChange(View v, int grabbedState);
+    }
+
+    public void onAnimationUpdate(ValueAnimator animation) {
+        invalidate();
+    }
+
+    public void reset() {
+        if (DBG) Log.v(TAG, "reset() : resets state to STATE_RESET_LOCK");
+        mLockState = STATE_RESET_LOCK;
+        invalidate();
+    }
+}
diff --git a/core/java/com/android/internal/widget/multiwaveview/Ease.java b/core/java/com/android/internal/widget/multiwaveview/Ease.java
new file mode 100644
index 0000000..7f90c44
--- /dev/null
+++ b/core/java/com/android/internal/widget/multiwaveview/Ease.java
@@ -0,0 +1,132 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.multiwaveview;
+
+import android.animation.TimeInterpolator;
+
+class Ease {
+    private static final float DOMAIN = 1.0f;
+    private static final float DURATION = 1.0f;
+    private static final float START = 0.0f;
+
+    static class Linear {
+        public static final TimeInterpolator easeNone = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return input;
+            }
+        };
+    }
+
+    static class Cubic {
+        public static final TimeInterpolator easeIn = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN*(input/=DURATION)*input*input + START;
+            }
+        };
+        public static final TimeInterpolator easeOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN*((input=input/DURATION-1)*input*input + 1) + START;
+            }
+        };
+        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return ((input/=DURATION/2) < 1.0f) ?
+                        (DOMAIN/2*input*input*input + START)
+                            : (DOMAIN/2*((input-=2)*input*input + 2) + START);
+            }
+        };
+    }
+
+    static class Quad {
+        public static final TimeInterpolator easeIn = new TimeInterpolator() {
+            public float getInterpolation (float input) {
+                return DOMAIN*(input/=DURATION)*input + START;
+            }
+        };
+        public static final TimeInterpolator easeOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return -DOMAIN *(input/=DURATION)*(input-2) + START;
+            }
+        };
+        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return ((input/=DURATION/2) < 1) ?
+                        (DOMAIN/2*input*input + START)
+                            : (-DOMAIN/2 * ((--input)*(input-2) - 1) + START);
+            }
+        };
+    }
+
+    static class Quart {
+        public static final TimeInterpolator easeIn = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN*(input/=DURATION)*input*input*input + START;
+            }
+        };
+        public static final TimeInterpolator easeOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return -DOMAIN * ((input=input/DURATION-1)*input*input*input - 1) + START;
+            }
+        };
+        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return ((input/=DURATION/2) < 1) ?
+                        (DOMAIN/2*input*input*input*input + START)
+                            : (-DOMAIN/2 * ((input-=2)*input*input*input - 2) + START);
+            }
+        };
+    }
+
+    static class Quint {
+        public static final TimeInterpolator easeIn = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN*(input/=DURATION)*input*input*input*input + START;
+            }
+        };
+        public static final TimeInterpolator easeOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN*((input=input/DURATION-1)*input*input*input*input + 1) + START;
+            }
+        };
+        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return ((input/=DURATION/2) < 1) ?
+                        (DOMAIN/2*input*input*input*input*input + START)
+                            : (DOMAIN/2*((input-=2)*input*input*input*input + 2) + START);
+            }
+        };
+    }
+
+    static class Sine {
+        public static final TimeInterpolator easeIn = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return -DOMAIN * (float) Math.cos(input/DURATION * (Math.PI/2)) + DOMAIN + START;
+            }
+        };
+        public static final TimeInterpolator easeOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return DOMAIN * (float) Math.sin(input/DURATION * (Math.PI/2)) + START;
+            }
+        };
+        public static final TimeInterpolator easeInOut = new TimeInterpolator() {
+            public float getInterpolation(float input) {
+                return -DOMAIN/2 * ((float)Math.cos(Math.PI*input/DURATION) - 1.0f) + START;
+            }
+        };
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java
new file mode 100644
index 0000000..11ac19e
--- /dev/null
+++ b/core/java/com/android/internal/widget/multiwaveview/GlowPadView.java
@@ -0,0 +1,1383 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.multiwaveview;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.media.AudioAttributes;
+import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.Vibrator;
+import android.provider.Settings;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * A re-usable widget containing a center, outer ring and wave animation.
+ */
+public class GlowPadView extends View {
+    private static final String TAG = "GlowPadView";
+    private static final boolean DEBUG = false;
+
+    // Wave state machine
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_START = 1;
+    private static final int STATE_FIRST_TOUCH = 2;
+    private static final int STATE_TRACKING = 3;
+    private static final int STATE_SNAP = 4;
+    private static final int STATE_FINISH = 5;
+
+    // Animation properties.
+    private static final float SNAP_MARGIN_DEFAULT = 20.0f; // distance to ring before we snap to it
+
+    public interface OnTriggerListener {
+        int NO_HANDLE = 0;
+        int CENTER_HANDLE = 1;
+        public void onGrabbed(View v, int handle);
+        public void onReleased(View v, int handle);
+        public void onTrigger(View v, int target);
+        public void onGrabbedStateChange(View v, int handle);
+        public void onFinishFinalAnimation();
+    }
+
+    // Tuneable parameters for animation
+    private static final int WAVE_ANIMATION_DURATION = 1000;
+    private static final int RETURN_TO_HOME_DELAY = 1200;
+    private static final int RETURN_TO_HOME_DURATION = 200;
+    private static final int HIDE_ANIMATION_DELAY = 200;
+    private static final int HIDE_ANIMATION_DURATION = 200;
+    private static final int SHOW_ANIMATION_DURATION = 200;
+    private static final int SHOW_ANIMATION_DELAY = 50;
+    private static final int INITIAL_SHOW_HANDLE_DURATION = 200;
+    private static final int REVEAL_GLOW_DELAY = 0;
+    private static final int REVEAL_GLOW_DURATION = 0;
+
+    private static final float TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED = 1.3f;
+    private static final float TARGET_SCALE_EXPANDED = 1.0f;
+    private static final float TARGET_SCALE_COLLAPSED = 0.8f;
+    private static final float RING_SCALE_EXPANDED = 1.0f;
+    private static final float RING_SCALE_COLLAPSED = 0.5f;
+
+    private static final AudioAttributes VIBRATION_ATTRIBUTES = new AudioAttributes.Builder()
+            .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+            .setUsage(AudioAttributes.USAGE_ASSISTANCE_SONIFICATION)
+            .build();
+
+    private ArrayList<TargetDrawable> mTargetDrawables = new ArrayList<TargetDrawable>();
+    private AnimationBundle mWaveAnimations = new AnimationBundle();
+    private AnimationBundle mTargetAnimations = new AnimationBundle();
+    private AnimationBundle mGlowAnimations = new AnimationBundle();
+    private ArrayList<String> mTargetDescriptions;
+    private ArrayList<String> mDirectionDescriptions;
+    private OnTriggerListener mOnTriggerListener;
+    private TargetDrawable mHandleDrawable;
+    private TargetDrawable mOuterRing;
+    private Vibrator mVibrator;
+
+    private int mFeedbackCount = 3;
+    private int mVibrationDuration = 0;
+    private int mGrabbedState;
+    private int mActiveTarget = -1;
+    private float mGlowRadius;
+    private float mWaveCenterX;
+    private float mWaveCenterY;
+    private int mMaxTargetHeight;
+    private int mMaxTargetWidth;
+    private float mRingScaleFactor = 1f;
+    private boolean mAllowScaling;
+
+    private float mOuterRadius = 0.0f;
+    private float mSnapMargin = 0.0f;
+    private float mFirstItemOffset = 0.0f;
+    private boolean mMagneticTargets = false;
+    private boolean mDragging;
+    private int mNewTargetResources;
+
+    private class AnimationBundle extends ArrayList<Tweener> {
+        private static final long serialVersionUID = 0xA84D78726F127468L;
+        private boolean mSuspended;
+
+        public void start() {
+            if (mSuspended) return; // ignore attempts to start animations
+            final int count = size();
+            for (int i = 0; i < count; i++) {
+                Tweener anim = get(i);
+                anim.animator.start();
+            }
+        }
+
+        public void cancel() {
+            final int count = size();
+            for (int i = 0; i < count; i++) {
+                Tweener anim = get(i);
+                anim.animator.cancel();
+            }
+            clear();
+        }
+
+        public void stop() {
+            final int count = size();
+            for (int i = 0; i < count; i++) {
+                Tweener anim = get(i);
+                anim.animator.end();
+            }
+            clear();
+        }
+
+        public void setSuspended(boolean suspend) {
+            mSuspended = suspend;
+        }
+    };
+
+    private AnimatorListener mResetListener = new AnimatorListenerAdapter() {
+        public void onAnimationEnd(Animator animator) {
+            switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+            dispatchOnFinishFinalAnimation();
+        }
+    };
+
+    private AnimatorListener mResetListenerWithPing = new AnimatorListenerAdapter() {
+        public void onAnimationEnd(Animator animator) {
+            ping();
+            switchToState(STATE_IDLE, mWaveCenterX, mWaveCenterY);
+            dispatchOnFinishFinalAnimation();
+        }
+    };
+
+    private AnimatorUpdateListener mUpdateListener = new AnimatorUpdateListener() {
+        public void onAnimationUpdate(ValueAnimator animation) {
+            invalidate();
+        }
+    };
+
+    private boolean mAnimatingTargets;
+    private AnimatorListener mTargetUpdateListener = new AnimatorListenerAdapter() {
+        public void onAnimationEnd(Animator animator) {
+            if (mNewTargetResources != 0) {
+                internalSetTargetResources(mNewTargetResources);
+                mNewTargetResources = 0;
+                hideTargets(false, false);
+            }
+            mAnimatingTargets = false;
+        }
+    };
+    private int mTargetResourceId;
+    private int mTargetDescriptionsResourceId;
+    private int mDirectionDescriptionsResourceId;
+    private boolean mAlwaysTrackFinger;
+    private int mHorizontalInset;
+    private int mVerticalInset;
+    private int mGravity = Gravity.TOP;
+    private boolean mInitialLayout = true;
+    private Tweener mBackgroundAnimator;
+    private PointCloud mPointCloud;
+    private float mInnerRadius;
+    private int mPointerId;
+
+    public GlowPadView(Context context) {
+        this(context, null);
+    }
+
+    public GlowPadView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        Resources res = context.getResources();
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GlowPadView);
+        mInnerRadius = a.getDimension(R.styleable.GlowPadView_innerRadius, mInnerRadius);
+        mOuterRadius = a.getDimension(R.styleable.GlowPadView_outerRadius, mOuterRadius);
+        mSnapMargin = a.getDimension(R.styleable.GlowPadView_snapMargin, mSnapMargin);
+        mFirstItemOffset = (float) Math.toRadians(
+                a.getFloat(R.styleable.GlowPadView_firstItemOffset,
+                        (float) Math.toDegrees(mFirstItemOffset)));
+        mVibrationDuration = a.getInt(R.styleable.GlowPadView_vibrationDuration,
+                mVibrationDuration);
+        mFeedbackCount = a.getInt(R.styleable.GlowPadView_feedbackCount,
+                mFeedbackCount);
+        mAllowScaling = a.getBoolean(R.styleable.GlowPadView_allowScaling, false);
+        TypedValue handle = a.peekValue(R.styleable.GlowPadView_handleDrawable);
+        mHandleDrawable = new TargetDrawable(res, handle != null ? handle.resourceId : 0);
+        mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
+        mOuterRing = new TargetDrawable(res,
+                getResourceId(a, R.styleable.GlowPadView_outerRingDrawable));
+
+        mAlwaysTrackFinger = a.getBoolean(R.styleable.GlowPadView_alwaysTrackFinger, false);
+        mMagneticTargets = a.getBoolean(R.styleable.GlowPadView_magneticTargets, mMagneticTargets);
+
+        int pointId = getResourceId(a, R.styleable.GlowPadView_pointDrawable);
+        Drawable pointDrawable = pointId != 0 ? context.getDrawable(pointId) : null;
+        mGlowRadius = a.getDimension(R.styleable.GlowPadView_glowRadius, 0.0f);
+
+        mPointCloud = new PointCloud(pointDrawable);
+        mPointCloud.makePointCloud(mInnerRadius, mOuterRadius);
+        mPointCloud.glowManager.setRadius(mGlowRadius);
+
+        TypedValue outValue = new TypedValue();
+
+        // Read array of target drawables
+        if (a.getValue(R.styleable.GlowPadView_targetDrawables, outValue)) {
+            internalSetTargetResources(outValue.resourceId);
+        }
+        if (mTargetDrawables == null || mTargetDrawables.size() == 0) {
+            throw new IllegalStateException("Must specify at least one target drawable");
+        }
+
+        // Read array of target descriptions
+        if (a.getValue(R.styleable.GlowPadView_targetDescriptions, outValue)) {
+            final int resourceId = outValue.resourceId;
+            if (resourceId == 0) {
+                throw new IllegalStateException("Must specify target descriptions");
+            }
+            setTargetDescriptionsResourceId(resourceId);
+        }
+
+        // Read array of direction descriptions
+        if (a.getValue(R.styleable.GlowPadView_directionDescriptions, outValue)) {
+            final int resourceId = outValue.resourceId;
+            if (resourceId == 0) {
+                throw new IllegalStateException("Must specify direction descriptions");
+            }
+            setDirectionDescriptionsResourceId(resourceId);
+        }
+
+        mGravity = a.getInt(R.styleable.GlowPadView_gravity, Gravity.TOP);
+
+        a.recycle();
+
+        setVibrateEnabled(mVibrationDuration > 0);
+
+        assignDefaultsIfNeeded();
+    }
+
+    private int getResourceId(TypedArray a, int id) {
+        TypedValue tv = a.peekValue(id);
+        return tv == null ? 0 : tv.resourceId;
+    }
+
+    private void dump() {
+        Log.v(TAG, "Outer Radius = " + mOuterRadius);
+        Log.v(TAG, "SnapMargin = " + mSnapMargin);
+        Log.v(TAG, "FeedbackCount = " + mFeedbackCount);
+        Log.v(TAG, "VibrationDuration = " + mVibrationDuration);
+        Log.v(TAG, "GlowRadius = " + mGlowRadius);
+        Log.v(TAG, "WaveCenterX = " + mWaveCenterX);
+        Log.v(TAG, "WaveCenterY = " + mWaveCenterY);
+    }
+
+    public void suspendAnimations() {
+        mWaveAnimations.setSuspended(true);
+        mTargetAnimations.setSuspended(true);
+        mGlowAnimations.setSuspended(true);
+    }
+
+    public void resumeAnimations() {
+        mWaveAnimations.setSuspended(false);
+        mTargetAnimations.setSuspended(false);
+        mGlowAnimations.setSuspended(false);
+        mWaveAnimations.start();
+        mTargetAnimations.start();
+        mGlowAnimations.start();
+    }
+
+    @Override
+    protected int getSuggestedMinimumWidth() {
+        // View should be large enough to contain the background + handle and
+        // target drawable on either edge.
+        return (int) (Math.max(mOuterRing.getWidth(), 2 * mOuterRadius) + mMaxTargetWidth);
+    }
+
+    @Override
+    protected int getSuggestedMinimumHeight() {
+        // View should be large enough to contain the unlock ring + target and
+        // target drawable on either edge
+        return (int) (Math.max(mOuterRing.getHeight(), 2 * mOuterRadius) + mMaxTargetHeight);
+    }
+
+    /**
+     * This gets the suggested width accounting for the ring's scale factor.
+     */
+    protected int getScaledSuggestedMinimumWidth() {
+        return (int) (mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius)
+                + mMaxTargetWidth);
+    }
+
+    /**
+     * This gets the suggested height accounting for the ring's scale factor.
+     */
+    protected int getScaledSuggestedMinimumHeight() {
+        return (int) (mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius)
+                + mMaxTargetHeight);
+    }
+
+    private int resolveMeasured(int measureSpec, int desired)
+    {
+        int result = 0;
+        int specSize = MeasureSpec.getSize(measureSpec);
+        switch (MeasureSpec.getMode(measureSpec)) {
+            case MeasureSpec.UNSPECIFIED:
+                result = desired;
+                break;
+            case MeasureSpec.AT_MOST:
+                result = Math.min(specSize, desired);
+                break;
+            case MeasureSpec.EXACTLY:
+            default:
+                result = specSize;
+        }
+        return result;
+    }
+
+    private void switchToState(int state, float x, float y) {
+        switch (state) {
+            case STATE_IDLE:
+                deactivateTargets();
+                hideGlow(0, 0, 0.0f, null);
+                startBackgroundAnimation(0, 0.0f);
+                mHandleDrawable.setState(TargetDrawable.STATE_INACTIVE);
+                mHandleDrawable.setAlpha(1.0f);
+                break;
+
+            case STATE_START:
+                startBackgroundAnimation(0, 0.0f);
+                break;
+
+            case STATE_FIRST_TOUCH:
+                mHandleDrawable.setAlpha(0.0f);
+                deactivateTargets();
+                showTargets(true);
+                startBackgroundAnimation(INITIAL_SHOW_HANDLE_DURATION, 1.0f);
+                setGrabbedState(OnTriggerListener.CENTER_HANDLE);
+                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                    announceTargets();
+                }
+                break;
+
+            case STATE_TRACKING:
+                mHandleDrawable.setAlpha(0.0f);
+                showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 1.0f, null);
+                break;
+
+            case STATE_SNAP:
+                // TODO: Add transition states (see list_selector_background_transition.xml)
+                mHandleDrawable.setAlpha(0.0f);
+                showGlow(REVEAL_GLOW_DURATION , REVEAL_GLOW_DELAY, 0.0f, null);
+                break;
+
+            case STATE_FINISH:
+                doFinish();
+                break;
+        }
+    }
+
+    private void showGlow(int duration, int delay, float finalAlpha,
+            AnimatorListener finishListener) {
+        mGlowAnimations.cancel();
+        mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
+                "ease", Ease.Cubic.easeIn,
+                "delay", delay,
+                "alpha", finalAlpha,
+                "onUpdate", mUpdateListener,
+                "onComplete", finishListener));
+        mGlowAnimations.start();
+    }
+
+    private void hideGlow(int duration, int delay, float finalAlpha,
+            AnimatorListener finishListener) {
+        mGlowAnimations.cancel();
+        mGlowAnimations.add(Tweener.to(mPointCloud.glowManager, duration,
+                "ease", Ease.Quart.easeOut,
+                "delay", delay,
+                "alpha", finalAlpha,
+                "x", 0.0f,
+                "y", 0.0f,
+                "onUpdate", mUpdateListener,
+                "onComplete", finishListener));
+        mGlowAnimations.start();
+    }
+
+    private void deactivateTargets() {
+        final int count = mTargetDrawables.size();
+        for (int i = 0; i < count; i++) {
+            TargetDrawable target = mTargetDrawables.get(i);
+            target.setState(TargetDrawable.STATE_INACTIVE);
+        }
+        mActiveTarget = -1;
+    }
+
+    /**
+     * Dispatches a trigger event to listener. Ignored if a listener is not set.
+     * @param whichTarget the target that was triggered.
+     */
+    private void dispatchTriggerEvent(int whichTarget) {
+        vibrate();
+        if (mOnTriggerListener != null) {
+            mOnTriggerListener.onTrigger(this, whichTarget);
+        }
+    }
+
+    private void dispatchOnFinishFinalAnimation() {
+        if (mOnTriggerListener != null) {
+            mOnTriggerListener.onFinishFinalAnimation();
+        }
+    }
+
+    private void doFinish() {
+        final int activeTarget = mActiveTarget;
+        final boolean targetHit =  activeTarget != -1;
+
+        if (targetHit) {
+            if (DEBUG) Log.v(TAG, "Finish with target hit = " + targetHit);
+
+            highlightSelected(activeTarget);
+
+            // Inform listener of any active targets.  Typically only one will be active.
+            hideGlow(RETURN_TO_HOME_DURATION, RETURN_TO_HOME_DELAY, 0.0f, mResetListener);
+            dispatchTriggerEvent(activeTarget);
+            if (!mAlwaysTrackFinger) {
+                // Force ring and targets to finish animation to final expanded state
+                mTargetAnimations.stop();
+            }
+        } else {
+            // Animate handle back to the center based on current state.
+            hideGlow(HIDE_ANIMATION_DURATION, 0, 0.0f, mResetListenerWithPing);
+            hideTargets(true, false);
+        }
+
+        setGrabbedState(OnTriggerListener.NO_HANDLE);
+    }
+
+    private void highlightSelected(int activeTarget) {
+        // Highlight the given target and fade others
+        mTargetDrawables.get(activeTarget).setState(TargetDrawable.STATE_ACTIVE);
+        hideUnselected(activeTarget);
+    }
+
+    private void hideUnselected(int active) {
+        for (int i = 0; i < mTargetDrawables.size(); i++) {
+            if (i != active) {
+                mTargetDrawables.get(i).setAlpha(0.0f);
+            }
+        }
+    }
+
+    private void hideTargets(boolean animate, boolean expanded) {
+        mTargetAnimations.cancel();
+        // Note: these animations should complete at the same time so that we can swap out
+        // the target assets asynchronously from the setTargetResources() call.
+        mAnimatingTargets = animate;
+        final int duration = animate ? HIDE_ANIMATION_DURATION : 0;
+        final int delay = animate ? HIDE_ANIMATION_DELAY : 0;
+
+        final float targetScale = expanded ?
+                TARGET_SCALE_EXPANDED : TARGET_SCALE_COLLAPSED;
+        final int length = mTargetDrawables.size();
+        final TimeInterpolator interpolator = Ease.Cubic.easeOut;
+        for (int i = 0; i < length; i++) {
+            TargetDrawable target = mTargetDrawables.get(i);
+            target.setState(TargetDrawable.STATE_INACTIVE);
+            mTargetAnimations.add(Tweener.to(target, duration,
+                    "ease", interpolator,
+                    "alpha", 0.0f,
+                    "scaleX", targetScale,
+                    "scaleY", targetScale,
+                    "delay", delay,
+                    "onUpdate", mUpdateListener));
+        }
+
+        float ringScaleTarget = expanded ?
+                RING_SCALE_EXPANDED : RING_SCALE_COLLAPSED;
+        ringScaleTarget *= mRingScaleFactor;
+        mTargetAnimations.add(Tweener.to(mOuterRing, duration,
+                "ease", interpolator,
+                "alpha", 0.0f,
+                "scaleX", ringScaleTarget,
+                "scaleY", ringScaleTarget,
+                "delay", delay,
+                "onUpdate", mUpdateListener,
+                "onComplete", mTargetUpdateListener));
+
+        mTargetAnimations.start();
+    }
+
+    private void showTargets(boolean animate) {
+        mTargetAnimations.stop();
+        mAnimatingTargets = animate;
+        final int delay = animate ? SHOW_ANIMATION_DELAY : 0;
+        final int duration = animate ? SHOW_ANIMATION_DURATION : 0;
+        final int length = mTargetDrawables.size();
+        for (int i = 0; i < length; i++) {
+            TargetDrawable target = mTargetDrawables.get(i);
+            target.setState(TargetDrawable.STATE_INACTIVE);
+            mTargetAnimations.add(Tweener.to(target, duration,
+                    "ease", Ease.Cubic.easeOut,
+                    "alpha", 1.0f,
+                    "scaleX", 1.0f,
+                    "scaleY", 1.0f,
+                    "delay", delay,
+                    "onUpdate", mUpdateListener));
+        }
+
+        float ringScale = mRingScaleFactor * RING_SCALE_EXPANDED;
+        mTargetAnimations.add(Tweener.to(mOuterRing, duration,
+                "ease", Ease.Cubic.easeOut,
+                "alpha", 1.0f,
+                "scaleX", ringScale,
+                "scaleY", ringScale,
+                "delay", delay,
+                "onUpdate", mUpdateListener,
+                "onComplete", mTargetUpdateListener));
+
+        mTargetAnimations.start();
+    }
+
+    private void vibrate() {
+        final boolean hapticEnabled = Settings.System.getIntForUser(
+                mContext.getContentResolver(), Settings.System.HAPTIC_FEEDBACK_ENABLED, 1,
+                UserHandle.USER_CURRENT) != 0;
+        if (mVibrator != null && hapticEnabled) {
+            mVibrator.vibrate(mVibrationDuration, VIBRATION_ATTRIBUTES);
+        }
+    }
+
+    private ArrayList<TargetDrawable> loadDrawableArray(int resourceId) {
+        Resources res = getContext().getResources();
+        TypedArray array = res.obtainTypedArray(resourceId);
+        final int count = array.length();
+        ArrayList<TargetDrawable> drawables = new ArrayList<TargetDrawable>(count);
+        for (int i = 0; i < count; i++) {
+            TypedValue value = array.peekValue(i);
+            TargetDrawable target = new TargetDrawable(res, value != null ? value.resourceId : 0);
+            drawables.add(target);
+        }
+        array.recycle();
+        return drawables;
+    }
+
+    private void internalSetTargetResources(int resourceId) {
+        final ArrayList<TargetDrawable> targets = loadDrawableArray(resourceId);
+        mTargetDrawables = targets;
+        mTargetResourceId = resourceId;
+
+        int maxWidth = mHandleDrawable.getWidth();
+        int maxHeight = mHandleDrawable.getHeight();
+        final int count = targets.size();
+        for (int i = 0; i < count; i++) {
+            TargetDrawable target = targets.get(i);
+            maxWidth = Math.max(maxWidth, target.getWidth());
+            maxHeight = Math.max(maxHeight, target.getHeight());
+        }
+        if (mMaxTargetWidth != maxWidth || mMaxTargetHeight != maxHeight) {
+            mMaxTargetWidth = maxWidth;
+            mMaxTargetHeight = maxHeight;
+            requestLayout(); // required to resize layout and call updateTargetPositions()
+        } else {
+            updateTargetPositions(mWaveCenterX, mWaveCenterY);
+            updatePointCloudPosition(mWaveCenterX, mWaveCenterY);
+        }
+    }
+
+    /**
+     * Loads an array of drawables from the given resourceId.
+     *
+     * @param resourceId
+     */
+    public void setTargetResources(int resourceId) {
+        if (mAnimatingTargets) {
+            // postpone this change until we return to the initial state
+            mNewTargetResources = resourceId;
+        } else {
+            internalSetTargetResources(resourceId);
+        }
+    }
+
+    public int getTargetResourceId() {
+        return mTargetResourceId;
+    }
+
+    /**
+     * Sets the resource id specifying the target descriptions for accessibility.
+     *
+     * @param resourceId The resource id.
+     */
+    public void setTargetDescriptionsResourceId(int resourceId) {
+        mTargetDescriptionsResourceId = resourceId;
+        if (mTargetDescriptions != null) {
+            mTargetDescriptions.clear();
+        }
+    }
+
+    /**
+     * Gets the resource id specifying the target descriptions for accessibility.
+     *
+     * @return The resource id.
+     */
+    public int getTargetDescriptionsResourceId() {
+        return mTargetDescriptionsResourceId;
+    }
+
+    /**
+     * Sets the resource id specifying the target direction descriptions for accessibility.
+     *
+     * @param resourceId The resource id.
+     */
+    public void setDirectionDescriptionsResourceId(int resourceId) {
+        mDirectionDescriptionsResourceId = resourceId;
+        if (mDirectionDescriptions != null) {
+            mDirectionDescriptions.clear();
+        }
+    }
+
+    /**
+     * Gets the resource id specifying the target direction descriptions.
+     *
+     * @return The resource id.
+     */
+    public int getDirectionDescriptionsResourceId() {
+        return mDirectionDescriptionsResourceId;
+    }
+
+    /**
+     * Enable or disable vibrate on touch.
+     *
+     * @param enabled
+     */
+    public void setVibrateEnabled(boolean enabled) {
+        if (enabled && mVibrator == null) {
+            mVibrator = (Vibrator) getContext().getSystemService(Context.VIBRATOR_SERVICE);
+        } else {
+            mVibrator = null;
+        }
+    }
+
+    /**
+     * Starts wave animation.
+     *
+     */
+    public void ping() {
+        if (mFeedbackCount > 0) {
+            boolean doWaveAnimation = true;
+            final AnimationBundle waveAnimations = mWaveAnimations;
+
+            // Don't do a wave if there's already one in progress
+            if (waveAnimations.size() > 0 && waveAnimations.get(0).animator.isRunning()) {
+                long t = waveAnimations.get(0).animator.getCurrentPlayTime();
+                if (t < WAVE_ANIMATION_DURATION/2) {
+                    doWaveAnimation = false;
+                }
+            }
+
+            if (doWaveAnimation) {
+                startWaveAnimation();
+            }
+        }
+    }
+
+    private void stopAndHideWaveAnimation() {
+        mWaveAnimations.cancel();
+        mPointCloud.waveManager.setAlpha(0.0f);
+    }
+
+    private void startWaveAnimation() {
+        mWaveAnimations.cancel();
+        mPointCloud.waveManager.setAlpha(1.0f);
+        mPointCloud.waveManager.setRadius(mHandleDrawable.getWidth()/2.0f);
+        mWaveAnimations.add(Tweener.to(mPointCloud.waveManager, WAVE_ANIMATION_DURATION,
+                "ease", Ease.Quad.easeOut,
+                "delay", 0,
+                "radius", 2.0f * mOuterRadius,
+                "onUpdate", mUpdateListener,
+                "onComplete",
+                new AnimatorListenerAdapter() {
+                    public void onAnimationEnd(Animator animator) {
+                        mPointCloud.waveManager.setRadius(0.0f);
+                        mPointCloud.waveManager.setAlpha(0.0f);
+                    }
+                }));
+        mWaveAnimations.start();
+    }
+
+    /**
+     * Resets the widget to default state and cancels all animation. If animate is 'true', will
+     * animate objects into place. Otherwise, objects will snap back to place.
+     *
+     * @param animate
+     */
+    public void reset(boolean animate) {
+        mGlowAnimations.stop();
+        mTargetAnimations.stop();
+        startBackgroundAnimation(0, 0.0f);
+        stopAndHideWaveAnimation();
+        hideTargets(animate, false);
+        hideGlow(0, 0, 0.0f, null);
+        Tweener.reset();
+    }
+
+    private void startBackgroundAnimation(int duration, float alpha) {
+        final Drawable background = getBackground();
+        if (mAlwaysTrackFinger && background != null) {
+            if (mBackgroundAnimator != null) {
+                mBackgroundAnimator.animator.cancel();
+            }
+            mBackgroundAnimator = Tweener.to(background, duration,
+                    "ease", Ease.Cubic.easeIn,
+                    "alpha", (int)(255.0f * alpha),
+                    "delay", SHOW_ANIMATION_DELAY);
+            mBackgroundAnimator.animator.start();
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int action = event.getActionMasked();
+        boolean handled = false;
+        switch (action) {
+            case MotionEvent.ACTION_POINTER_DOWN:
+            case MotionEvent.ACTION_DOWN:
+                if (DEBUG) Log.v(TAG, "*** DOWN ***");
+                handleDown(event);
+                handleMove(event);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (DEBUG) Log.v(TAG, "*** MOVE ***");
+                handleMove(event);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_POINTER_UP:
+            case MotionEvent.ACTION_UP:
+                if (DEBUG) Log.v(TAG, "*** UP ***");
+                handleMove(event);
+                handleUp(event);
+                handled = true;
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                if (DEBUG) Log.v(TAG, "*** CANCEL ***");
+                handleMove(event);
+                handleCancel(event);
+                handled = true;
+                break;
+
+        }
+        invalidate();
+        return handled ? true : super.onTouchEvent(event);
+    }
+
+    private void updateGlowPosition(float x, float y) {
+        float dx = x - mOuterRing.getX();
+        float dy = y - mOuterRing.getY();
+        dx *= 1f / mRingScaleFactor;
+        dy *= 1f / mRingScaleFactor;
+        mPointCloud.glowManager.setX(mOuterRing.getX() + dx);
+        mPointCloud.glowManager.setY(mOuterRing.getY() + dy);
+    }
+
+    private void handleDown(MotionEvent event) {
+        int actionIndex = event.getActionIndex();
+        float eventX = event.getX(actionIndex);
+        float eventY = event.getY(actionIndex);
+        switchToState(STATE_START, eventX, eventY);
+        if (!trySwitchToFirstTouchState(eventX, eventY)) {
+            mDragging = false;
+        } else {
+            mPointerId = event.getPointerId(actionIndex);
+            updateGlowPosition(eventX, eventY);
+        }
+    }
+
+    private void handleUp(MotionEvent event) {
+        if (DEBUG && mDragging) Log.v(TAG, "** Handle RELEASE");
+        int actionIndex = event.getActionIndex();
+        if (event.getPointerId(actionIndex) == mPointerId) {
+            switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
+        }
+    }
+
+    private void handleCancel(MotionEvent event) {
+        if (DEBUG && mDragging) Log.v(TAG, "** Handle CANCEL");
+
+        // Drop the active target if canceled.
+        mActiveTarget = -1; 
+
+        int actionIndex = event.findPointerIndex(mPointerId);
+        actionIndex = actionIndex == -1 ? 0 : actionIndex;
+        switchToState(STATE_FINISH, event.getX(actionIndex), event.getY(actionIndex));
+    }
+
+    private void handleMove(MotionEvent event) {
+        int activeTarget = -1;
+        final int historySize = event.getHistorySize();
+        ArrayList<TargetDrawable> targets = mTargetDrawables;
+        int ntargets = targets.size();
+        float x = 0.0f;
+        float y = 0.0f;
+        float activeAngle = 0.0f;
+        int actionIndex = event.findPointerIndex(mPointerId);
+
+        if (actionIndex == -1) {
+            return;  // no data for this pointer
+        }
+
+        for (int k = 0; k < historySize + 1; k++) {
+            float eventX = k < historySize ? event.getHistoricalX(actionIndex, k)
+                    : event.getX(actionIndex);
+            float eventY = k < historySize ? event.getHistoricalY(actionIndex, k)
+                    : event.getY(actionIndex);
+            // tx and ty are relative to wave center
+            float tx = eventX - mWaveCenterX;
+            float ty = eventY - mWaveCenterY;
+            float touchRadius = (float) Math.hypot(tx, ty);
+            final float scale = touchRadius > mOuterRadius ? mOuterRadius / touchRadius : 1.0f;
+            float limitX = tx * scale;
+            float limitY = ty * scale;
+            double angleRad = Math.atan2(-ty, tx);
+
+            if (!mDragging) {
+                trySwitchToFirstTouchState(eventX, eventY);
+            }
+
+            if (mDragging) {
+                // For multiple targets, snap to the one that matches
+                final float snapRadius = mRingScaleFactor * mOuterRadius - mSnapMargin;
+                final float snapDistance2 = snapRadius * snapRadius;
+                // Find first target in range
+                for (int i = 0; i < ntargets; i++) {
+                    TargetDrawable target = targets.get(i);
+
+                    double targetMinRad = mFirstItemOffset + (i - 0.5) * 2 * Math.PI / ntargets;
+                    double targetMaxRad = mFirstItemOffset + (i + 0.5) * 2 * Math.PI / ntargets;
+                    if (target.isEnabled()) {
+                        boolean angleMatches =
+                            (angleRad > targetMinRad && angleRad <= targetMaxRad) ||
+                            (angleRad + 2 * Math.PI > targetMinRad &&
+                             angleRad + 2 * Math.PI <= targetMaxRad) ||
+                            (angleRad - 2 * Math.PI > targetMinRad &&
+                             angleRad - 2 * Math.PI <= targetMaxRad);
+                        if (angleMatches && (dist2(tx, ty) > snapDistance2)) {
+                            activeTarget = i;
+                            activeAngle = (float) -angleRad;
+                        }
+                    }
+                }
+            }
+            x = limitX;
+            y = limitY;
+        }
+
+        if (!mDragging) {
+            return;
+        }
+
+        if (activeTarget != -1) {
+            switchToState(STATE_SNAP, x,y);
+            updateGlowPosition(x, y);
+        } else {
+            switchToState(STATE_TRACKING, x, y);
+            updateGlowPosition(x, y);
+        }
+
+        if (mActiveTarget != activeTarget) {
+            // Defocus the old target
+            if (mActiveTarget != -1) {
+                TargetDrawable target = targets.get(mActiveTarget);
+                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
+                    target.setState(TargetDrawable.STATE_INACTIVE);
+                }
+                if (mMagneticTargets) {
+                    updateTargetPosition(mActiveTarget, mWaveCenterX, mWaveCenterY);
+                }
+            }
+            // Focus the new target
+            if (activeTarget != -1) {
+                TargetDrawable target = targets.get(activeTarget);
+                if (target.hasState(TargetDrawable.STATE_FOCUSED)) {
+                    target.setState(TargetDrawable.STATE_FOCUSED);
+                }
+                if (mMagneticTargets) {
+                    updateTargetPosition(activeTarget, mWaveCenterX, mWaveCenterY, activeAngle);
+                }
+                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                    String targetContentDescription = getTargetDescription(activeTarget);
+                    announceForAccessibility(targetContentDescription);
+                }
+            }
+        }
+        mActiveTarget = activeTarget;
+    }
+
+    @Override
+    public boolean onHoverEvent(MotionEvent event) {
+        if (AccessibilityManager.getInstance(mContext).isTouchExplorationEnabled()) {
+            final int action = event.getAction();
+            switch (action) {
+                case MotionEvent.ACTION_HOVER_ENTER:
+                    event.setAction(MotionEvent.ACTION_DOWN);
+                    break;
+                case MotionEvent.ACTION_HOVER_MOVE:
+                    event.setAction(MotionEvent.ACTION_MOVE);
+                    break;
+                case MotionEvent.ACTION_HOVER_EXIT:
+                    event.setAction(MotionEvent.ACTION_UP);
+                    break;
+            }
+            onTouchEvent(event);
+            event.setAction(action);
+        }
+        super.onHoverEvent(event);
+        return true;
+    }
+
+    /**
+     * Sets the current grabbed state, and dispatches a grabbed state change
+     * event to our listener.
+     */
+    private void setGrabbedState(int newState) {
+        if (newState != mGrabbedState) {
+            if (newState != OnTriggerListener.NO_HANDLE) {
+                vibrate();
+            }
+            mGrabbedState = newState;
+            if (mOnTriggerListener != null) {
+                if (newState == OnTriggerListener.NO_HANDLE) {
+                    mOnTriggerListener.onReleased(this, OnTriggerListener.CENTER_HANDLE);
+                } else {
+                    mOnTriggerListener.onGrabbed(this, OnTriggerListener.CENTER_HANDLE);
+                }
+                mOnTriggerListener.onGrabbedStateChange(this, newState);
+            }
+        }
+    }
+
+    private boolean trySwitchToFirstTouchState(float x, float y) {
+        final float tx = x - mWaveCenterX;
+        final float ty = y - mWaveCenterY;
+        if (mAlwaysTrackFinger || dist2(tx,ty) <= getScaledGlowRadiusSquared()) {
+            if (DEBUG) Log.v(TAG, "** Handle HIT");
+            switchToState(STATE_FIRST_TOUCH, x, y);
+            updateGlowPosition(tx, ty);
+            mDragging = true;
+            return true;
+        }
+        return false;
+    }
+
+    private void assignDefaultsIfNeeded() {
+        if (mOuterRadius == 0.0f) {
+            mOuterRadius = Math.max(mOuterRing.getWidth(), mOuterRing.getHeight())/2.0f;
+        }
+        if (mSnapMargin == 0.0f) {
+            mSnapMargin = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    SNAP_MARGIN_DEFAULT, getContext().getResources().getDisplayMetrics());
+        }
+        if (mInnerRadius == 0.0f) {
+            mInnerRadius = mHandleDrawable.getWidth() / 10.0f;
+        }
+    }
+
+    private void computeInsets(int dx, int dy) {
+        final int layoutDirection = getLayoutDirection();
+        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+
+        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.LEFT:
+                mHorizontalInset = 0;
+                break;
+            case Gravity.RIGHT:
+                mHorizontalInset = dx;
+                break;
+            case Gravity.CENTER_HORIZONTAL:
+            default:
+                mHorizontalInset = dx / 2;
+                break;
+        }
+        switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+            case Gravity.TOP:
+                mVerticalInset = 0;
+                break;
+            case Gravity.BOTTOM:
+                mVerticalInset = dy;
+                break;
+            case Gravity.CENTER_VERTICAL:
+            default:
+                mVerticalInset = dy / 2;
+                break;
+        }
+    }
+
+    /**
+     * Given the desired width and height of the ring and the allocated width and height, compute
+     * how much we need to scale the ring.
+     */
+    private float computeScaleFactor(int desiredWidth, int desiredHeight,
+            int actualWidth, int actualHeight) {
+
+        // Return unity if scaling is not allowed.
+        if (!mAllowScaling) return 1f;
+
+        final int layoutDirection = getLayoutDirection();
+        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+
+        float scaleX = 1f;
+        float scaleY = 1f;
+
+        // We use the gravity as a cue for whether we want to scale on a particular axis.
+        // We only scale to fit horizontally if we're not pinned to the left or right. Likewise,
+        // we only scale to fit vertically if we're not pinned to the top or bottom. In these
+        // cases, we want the ring to hang off the side or top/bottom, respectively.
+        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.LEFT:
+            case Gravity.RIGHT:
+                break;
+            case Gravity.CENTER_HORIZONTAL:
+            default:
+                if (desiredWidth > actualWidth) {
+                    scaleX = (1f * actualWidth - mMaxTargetWidth) /
+                            (desiredWidth - mMaxTargetWidth);
+                }
+                break;
+        }
+        switch (absoluteGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+            case Gravity.TOP:
+            case Gravity.BOTTOM:
+                break;
+            case Gravity.CENTER_VERTICAL:
+            default:
+                if (desiredHeight > actualHeight) {
+                    scaleY = (1f * actualHeight - mMaxTargetHeight) /
+                            (desiredHeight - mMaxTargetHeight);
+                }
+                break;
+        }
+        return Math.min(scaleX, scaleY);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int minimumWidth = getSuggestedMinimumWidth();
+        final int minimumHeight = getSuggestedMinimumHeight();
+        int computedWidth = resolveMeasured(widthMeasureSpec, minimumWidth);
+        int computedHeight = resolveMeasured(heightMeasureSpec, minimumHeight);
+
+        mRingScaleFactor = computeScaleFactor(minimumWidth, minimumHeight,
+                computedWidth, computedHeight);
+
+        int scaledWidth = getScaledSuggestedMinimumWidth();
+        int scaledHeight = getScaledSuggestedMinimumHeight();
+
+        computeInsets(computedWidth - scaledWidth, computedHeight - scaledHeight);
+        setMeasuredDimension(computedWidth, computedHeight);
+    }
+
+    private float getRingWidth() {
+        return mRingScaleFactor * Math.max(mOuterRing.getWidth(), 2 * mOuterRadius);
+    }
+
+    private float getRingHeight() {
+        return mRingScaleFactor * Math.max(mOuterRing.getHeight(), 2 * mOuterRadius);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        final int width = right - left;
+        final int height = bottom - top;
+
+        // Target placement width/height. This puts the targets on the greater of the ring
+        // width or the specified outer radius.
+        final float placementWidth = getRingWidth();
+        final float placementHeight = getRingHeight();
+        float newWaveCenterX = mHorizontalInset
+                + Math.max(width, mMaxTargetWidth + placementWidth) / 2;
+        float newWaveCenterY = mVerticalInset
+                + Math.max(height, + mMaxTargetHeight + placementHeight) / 2;
+
+        if (mInitialLayout) {
+            stopAndHideWaveAnimation();
+            hideTargets(false, false);
+            mInitialLayout = false;
+        }
+
+        mOuterRing.setPositionX(newWaveCenterX);
+        mOuterRing.setPositionY(newWaveCenterY);
+
+        mPointCloud.setScale(mRingScaleFactor);
+
+        mHandleDrawable.setPositionX(newWaveCenterX);
+        mHandleDrawable.setPositionY(newWaveCenterY);
+
+        updateTargetPositions(newWaveCenterX, newWaveCenterY);
+        updatePointCloudPosition(newWaveCenterX, newWaveCenterY);
+        updateGlowPosition(newWaveCenterX, newWaveCenterY);
+
+        mWaveCenterX = newWaveCenterX;
+        mWaveCenterY = newWaveCenterY;
+
+        if (DEBUG) dump();
+    }
+
+    private void updateTargetPosition(int i, float centerX, float centerY) {
+        final float angle = getAngle(getSliceAngle(), i);
+        updateTargetPosition(i, centerX, centerY, angle);
+    }
+
+    private void updateTargetPosition(int i, float centerX, float centerY, float angle) {
+        final float placementRadiusX = getRingWidth() / 2;
+        final float placementRadiusY = getRingHeight() / 2;
+        if (i >= 0) {
+            ArrayList<TargetDrawable> targets = mTargetDrawables;
+            final TargetDrawable targetIcon = targets.get(i);
+            targetIcon.setPositionX(centerX);
+            targetIcon.setPositionY(centerY);
+            targetIcon.setX(placementRadiusX * (float) Math.cos(angle));
+            targetIcon.setY(placementRadiusY * (float) Math.sin(angle));
+        }
+    }
+
+    private void updateTargetPositions(float centerX, float centerY) {
+        updateTargetPositions(centerX, centerY, false);
+    }
+
+    private void updateTargetPositions(float centerX, float centerY, boolean skipActive) {
+        final int size = mTargetDrawables.size();
+        final float alpha = getSliceAngle();
+        // Reposition the target drawables if the view changed.
+        for (int i = 0; i < size; i++) {
+            if (!skipActive || i != mActiveTarget) {
+                updateTargetPosition(i, centerX, centerY, getAngle(alpha, i));
+            }
+        }
+    }
+
+    private float getAngle(float alpha, int i) {
+        return mFirstItemOffset + alpha * i;
+    }
+
+    private float getSliceAngle() {
+        return (float) (-2.0f * Math.PI / mTargetDrawables.size());
+    }
+
+    private void updatePointCloudPosition(float centerX, float centerY) {
+        mPointCloud.setCenter(centerX, centerY);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        mPointCloud.draw(canvas);
+        mOuterRing.draw(canvas);
+        final int ntargets = mTargetDrawables.size();
+        for (int i = 0; i < ntargets; i++) {
+            TargetDrawable target = mTargetDrawables.get(i);
+            if (target != null) {
+                target.draw(canvas);
+            }
+        }
+        mHandleDrawable.draw(canvas);
+    }
+
+    public void setOnTriggerListener(OnTriggerListener listener) {
+        mOnTriggerListener = listener;
+    }
+
+    private float square(float d) {
+        return d * d;
+    }
+
+    private float dist2(float dx, float dy) {
+        return dx*dx + dy*dy;
+    }
+
+    private float getScaledGlowRadiusSquared() {
+        final float scaledTapRadius;
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            scaledTapRadius = TAP_RADIUS_SCALE_ACCESSIBILITY_ENABLED * mGlowRadius;
+        } else {
+            scaledTapRadius = mGlowRadius;
+        }
+        return square(scaledTapRadius);
+    }
+
+    private void announceTargets() {
+        StringBuilder utterance = new StringBuilder();
+        final int targetCount = mTargetDrawables.size();
+        for (int i = 0; i < targetCount; i++) {
+            String targetDescription = getTargetDescription(i);
+            String directionDescription = getDirectionDescription(i);
+            if (!TextUtils.isEmpty(targetDescription)
+                    && !TextUtils.isEmpty(directionDescription)) {
+                String text = String.format(directionDescription, targetDescription);
+                utterance.append(text);
+            }
+        }
+        if (utterance.length() > 0) {
+            announceForAccessibility(utterance.toString());
+        }
+    }
+
+    private String getTargetDescription(int index) {
+        if (mTargetDescriptions == null || mTargetDescriptions.isEmpty()) {
+            mTargetDescriptions = loadDescriptions(mTargetDescriptionsResourceId);
+            if (mTargetDrawables.size() != mTargetDescriptions.size()) {
+                Log.w(TAG, "The number of target drawables must be"
+                        + " equal to the number of target descriptions.");
+                return null;
+            }
+        }
+        return mTargetDescriptions.get(index);
+    }
+
+    private String getDirectionDescription(int index) {
+        if (mDirectionDescriptions == null || mDirectionDescriptions.isEmpty()) {
+            mDirectionDescriptions = loadDescriptions(mDirectionDescriptionsResourceId);
+            if (mTargetDrawables.size() != mDirectionDescriptions.size()) {
+                Log.w(TAG, "The number of target drawables must be"
+                        + " equal to the number of direction descriptions.");
+                return null;
+            }
+        }
+        return mDirectionDescriptions.get(index);
+    }
+
+    private ArrayList<String> loadDescriptions(int resourceId) {
+        TypedArray array = getContext().getResources().obtainTypedArray(resourceId);
+        final int count = array.length();
+        ArrayList<String> targetContentDescriptions = new ArrayList<String>(count);
+        for (int i = 0; i < count; i++) {
+            String contentDescription = array.getString(i);
+            targetContentDescriptions.add(contentDescription);
+        }
+        array.recycle();
+        return targetContentDescriptions;
+    }
+
+    public int getResourceIdForTarget(int index) {
+        final TargetDrawable drawable = mTargetDrawables.get(index);
+        return drawable == null ? 0 : drawable.getResourceId();
+    }
+
+    public void setEnableTarget(int resourceId, boolean enabled) {
+        for (int i = 0; i < mTargetDrawables.size(); i++) {
+            final TargetDrawable target = mTargetDrawables.get(i);
+            if (target.getResourceId() == resourceId) {
+                target.setEnabled(enabled);
+                break; // should never be more than one match
+            }
+        }
+    }
+
+    /**
+     * Gets the position of a target in the array that matches the given resource.
+     * @param resourceId
+     * @return the index or -1 if not found
+     */
+    public int getTargetPosition(int resourceId) {
+        for (int i = 0; i < mTargetDrawables.size(); i++) {
+            final TargetDrawable target = mTargetDrawables.get(i);
+            if (target.getResourceId() == resourceId) {
+                return i; // should never be more than one match
+            }
+        }
+        return -1;
+    }
+
+    private boolean replaceTargetDrawables(Resources res, int existingResourceId,
+            int newResourceId) {
+        if (existingResourceId == 0 || newResourceId == 0) {
+            return false;
+        }
+
+        boolean result = false;
+        final ArrayList<TargetDrawable> drawables = mTargetDrawables;
+        final int size = drawables.size();
+        for (int i = 0; i < size; i++) {
+            final TargetDrawable target = drawables.get(i);
+            if (target != null && target.getResourceId() == existingResourceId) {
+                target.setDrawable(res, newResourceId);
+                result = true;
+            }
+        }
+
+        if (result) {
+            requestLayout(); // in case any given drawable's size changes
+        }
+
+        return result;
+    }
+
+    /**
+     * Searches the given package for a resource to use to replace the Drawable on the
+     * target with the given resource id
+     * @param component of the .apk that contains the resource
+     * @param name of the metadata in the .apk
+     * @param existingResId the resource id of the target to search for
+     * @return true if found in the given package and replaced at least one target Drawables
+     */
+    public boolean replaceTargetDrawablesIfPresent(ComponentName component, String name,
+                int existingResId) {
+        if (existingResId == 0) return false;
+
+        boolean replaced = false;
+        if (component != null) {
+            try {
+                PackageManager packageManager = mContext.getPackageManager();
+                // Look for the search icon specified in the activity meta-data
+                Bundle metaData = packageManager.getActivityInfo(
+                        component, PackageManager.GET_META_DATA).metaData;
+                if (metaData != null) {
+                    int iconResId = metaData.getInt(name);
+                    if (iconResId != 0) {
+                        Resources res = packageManager.getResourcesForActivity(component);
+                        replaced = replaceTargetDrawables(res, existingResId, iconResId);
+                    }
+                }
+            } catch (NameNotFoundException e) {
+                Log.w(TAG, "Failed to swap drawable; "
+                        + component.flattenToShortString() + " not found", e);
+            } catch (Resources.NotFoundException nfe) {
+                Log.w(TAG, "Failed to swap drawable from "
+                        + component.flattenToShortString(), nfe);
+            }
+        }
+        if (!replaced) {
+            // Restore the original drawable
+            replaceTargetDrawables(mContext.getResources(), existingResId, existingResId);
+        }
+        return replaced;
+    }
+}
diff --git a/core/java/com/android/internal/widget/multiwaveview/PointCloud.java b/core/java/com/android/internal/widget/multiwaveview/PointCloud.java
new file mode 100644
index 0000000..6f26b99
--- /dev/null
+++ b/core/java/com/android/internal/widget/multiwaveview/PointCloud.java
@@ -0,0 +1,225 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.multiwaveview;
+
+import java.util.ArrayList;
+
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.drawable.Drawable;
+import android.util.Log;
+
+public class PointCloud {
+    private static final float MIN_POINT_SIZE = 2.0f;
+    private static final float MAX_POINT_SIZE = 4.0f;
+    private static final int INNER_POINTS = 8;
+    private static final String TAG = "PointCloud";
+    private ArrayList<Point> mPointCloud = new ArrayList<Point>();
+    private Drawable mDrawable;
+    private float mCenterX;
+    private float mCenterY;
+    private Paint mPaint;
+    private float mScale = 1.0f;
+    private static final float PI = (float) Math.PI;
+
+    // These allow us to have multiple concurrent animations.
+    WaveManager waveManager = new WaveManager();
+    GlowManager glowManager = new GlowManager();
+    private float mOuterRadius;
+
+    public class WaveManager {
+        private float radius = 50;
+        private float alpha = 0.0f;
+
+        public void setRadius(float r) {
+            radius = r;
+        }
+
+        public float getRadius() {
+            return radius;
+        }
+
+        public void setAlpha(float a) {
+            alpha = a;
+        }
+
+        public float getAlpha() {
+            return alpha;
+        }
+    };
+
+    public class GlowManager {
+        private float x;
+        private float y;
+        private float radius = 0.0f;
+        private float alpha = 0.0f;
+
+        public void setX(float x1) {
+            x = x1;
+        }
+
+        public float getX() {
+            return x;
+        }
+
+        public void setY(float y1) {
+            y = y1;
+        }
+
+        public float getY() {
+            return y;
+        }
+
+        public void setAlpha(float a) {
+            alpha = a;
+        }
+
+        public float getAlpha() {
+            return alpha;
+        }
+
+        public void setRadius(float r) {
+            radius = r;
+        }
+
+        public float getRadius() {
+            return radius;
+        }
+    }
+
+    class Point {
+        float x;
+        float y;
+        float radius;
+
+        public Point(float x2, float y2, float r) {
+            x = (float) x2;
+            y = (float) y2;
+            radius = r;
+        }
+    }
+
+    public PointCloud(Drawable drawable) {
+        mPaint = new Paint();
+        mPaint.setFilterBitmap(true);
+        mPaint.setColor(Color.rgb(255, 255, 255)); // TODO: make configurable
+        mPaint.setAntiAlias(true);
+        mPaint.setDither(true);
+
+        mDrawable = drawable;
+        if (mDrawable != null) {
+            drawable.setBounds(0, 0, drawable.getIntrinsicWidth(), drawable.getIntrinsicHeight());
+        }
+    }
+
+    public void setCenter(float x, float y) {
+        mCenterX = x;
+        mCenterY = y;
+    }
+
+    public void makePointCloud(float innerRadius, float outerRadius) {
+        if (innerRadius == 0) {
+            Log.w(TAG, "Must specify an inner radius");
+            return;
+        }
+        mOuterRadius = outerRadius;
+        mPointCloud.clear();
+        final float pointAreaRadius =  (outerRadius - innerRadius);
+        final float ds = (2.0f * PI * innerRadius / INNER_POINTS);
+        final int bands = (int) Math.round(pointAreaRadius / ds);
+        final float dr = pointAreaRadius / bands;
+        float r = innerRadius;
+        for (int b = 0; b <= bands; b++, r += dr) {
+            float circumference = 2.0f * PI * r;
+            final int pointsInBand = (int) (circumference / ds);
+            float eta = PI/2.0f;
+            float dEta = 2.0f * PI / pointsInBand;
+            for (int i = 0; i < pointsInBand; i++) {
+                float x = r * (float) Math.cos(eta);
+                float y = r * (float) Math.sin(eta);
+                eta += dEta;
+                mPointCloud.add(new Point(x, y, r));
+            }
+        }
+    }
+
+    public void setScale(float scale) {
+        mScale  = scale;
+    }
+
+    public float getScale() {
+        return mScale;
+    }
+
+    public int getAlphaForPoint(Point point) {
+        // Contribution from positional glow
+        float glowDistance = (float) Math.hypot(glowManager.x - point.x, glowManager.y - point.y);
+        float glowAlpha = 0.0f;
+        if (glowDistance < glowManager.radius) {
+            float cosf = (float) Math.cos(PI * 0.25f * glowDistance / glowManager.radius);
+            glowAlpha = glowManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 10.0f));
+        }
+
+        // Compute contribution from Wave
+        float radius = (float) Math.hypot(point.x, point.y);
+        float waveAlpha = 0.0f;
+        if (radius < waveManager.radius * 2) {
+            float distanceToWaveRing = (radius - waveManager.radius);
+            float cosf = (float) Math.cos(PI * 0.5f * distanceToWaveRing / waveManager.radius);
+            waveAlpha = waveManager.alpha * Math.max(0.0f, (float) Math.pow(cosf, 6.0f));
+        }
+        return (int) (Math.max(glowAlpha, waveAlpha) * 255);
+    }
+
+    private float interp(float min, float max, float f) {
+        return min + (max - min) * f;
+    }
+
+    public void draw(Canvas canvas) {
+        ArrayList<Point> points = mPointCloud;
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.scale(mScale, mScale, mCenterX, mCenterY);
+        for (int i = 0; i < points.size(); i++) {
+            Point point = points.get(i);
+            final float pointSize = interp(MAX_POINT_SIZE, MIN_POINT_SIZE,
+                    point.radius / mOuterRadius);
+            final float px = point.x + mCenterX;
+            final float py = point.y + mCenterY;
+            int alpha = getAlphaForPoint(point);
+
+            if (alpha == 0) continue;
+
+            if (mDrawable != null) {
+                canvas.save(Canvas.MATRIX_SAVE_FLAG);
+                final float cx = mDrawable.getIntrinsicWidth() * 0.5f;
+                final float cy = mDrawable.getIntrinsicHeight() * 0.5f;
+                final float s = pointSize / MAX_POINT_SIZE;
+                canvas.scale(s, s, px, py);
+                canvas.translate(px - cx, py - cy);
+                mDrawable.setAlpha(alpha);
+                mDrawable.draw(canvas);
+                canvas.restore();
+            } else {
+                mPaint.setAlpha(alpha);
+                canvas.drawCircle(px, py, pointSize, mPaint);
+            }
+        }
+        canvas.restore();
+    }
+
+}
diff --git a/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java
new file mode 100644
index 0000000..5a4c441
--- /dev/null
+++ b/core/java/com/android/internal/widget/multiwaveview/TargetDrawable.java
@@ -0,0 +1,229 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.multiwaveview;
+
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.util.Log;
+
+public class TargetDrawable {
+    private static final String TAG = "TargetDrawable";
+    private static final boolean DEBUG = false;
+
+    public static final int[] STATE_ACTIVE =
+            { android.R.attr.state_enabled, android.R.attr.state_active };
+    public static final int[] STATE_INACTIVE =
+            { android.R.attr.state_enabled, -android.R.attr.state_active };
+    public static final int[] STATE_FOCUSED =
+            { android.R.attr.state_enabled, -android.R.attr.state_active,
+                android.R.attr.state_focused };
+
+    private float mTranslationX = 0.0f;
+    private float mTranslationY = 0.0f;
+    private float mPositionX = 0.0f;
+    private float mPositionY = 0.0f;
+    private float mScaleX = 1.0f;
+    private float mScaleY = 1.0f;
+    private float mAlpha = 1.0f;
+    private Drawable mDrawable;
+    private boolean mEnabled = true;
+    private final int mResourceId;
+
+    public TargetDrawable(Resources res, int resId) {
+        mResourceId = resId;
+        setDrawable(res, resId);
+    }
+
+    public void setDrawable(Resources res, int resId) {
+        // Note we explicitly don't set mResourceId to resId since we allow the drawable to be
+        // swapped at runtime and want to re-use the existing resource id for identification.
+        Drawable drawable = resId == 0 ? null : res.getDrawable(resId);
+        // Mutate the drawable so we can animate shared drawable properties.
+        mDrawable = drawable != null ? drawable.mutate() : null;
+        resizeDrawables();
+        setState(STATE_INACTIVE);
+    }
+
+    public TargetDrawable(TargetDrawable other) {
+        mResourceId = other.mResourceId;
+        // Mutate the drawable so we can animate shared drawable properties.
+        mDrawable = other.mDrawable != null ? other.mDrawable.mutate() : null;
+        resizeDrawables();
+        setState(STATE_INACTIVE);
+    }
+
+    public void setState(int [] state) {
+        if (mDrawable instanceof StateListDrawable) {
+            StateListDrawable d = (StateListDrawable) mDrawable;
+            d.setState(state);
+        }
+    }
+
+    public boolean hasState(int [] state) {
+        if (mDrawable instanceof StateListDrawable) {
+            StateListDrawable d = (StateListDrawable) mDrawable;
+            // TODO: this doesn't seem to work
+            return d.getStateDrawableIndex(state) != -1;
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if the drawable is a StateListDrawable and is in the focused state.
+     *
+     * @return
+     */
+    public boolean isActive() {
+        if (mDrawable instanceof StateListDrawable) {
+            StateListDrawable d = (StateListDrawable) mDrawable;
+            int[] states = d.getState();
+            for (int i = 0; i < states.length; i++) {
+                if (states[i] == android.R.attr.state_focused) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns true if this target is enabled. Typically an enabled target contains a valid
+     * drawable in a valid state. Currently all targets with valid drawables are valid.
+     *
+     * @return
+     */
+    public boolean isEnabled() {
+        return mDrawable != null && mEnabled;
+    }
+
+    /**
+     * Makes drawables in a StateListDrawable all the same dimensions.
+     * If not a StateListDrawable, then justs sets the bounds to the intrinsic size of the
+     * drawable.
+     */
+    private void resizeDrawables() {
+        if (mDrawable instanceof StateListDrawable) {
+            StateListDrawable d = (StateListDrawable) mDrawable;
+            int maxWidth = 0;
+            int maxHeight = 0;
+            for (int i = 0; i < d.getStateCount(); i++) {
+                Drawable childDrawable = d.getStateDrawable(i);
+                maxWidth = Math.max(maxWidth, childDrawable.getIntrinsicWidth());
+                maxHeight = Math.max(maxHeight, childDrawable.getIntrinsicHeight());
+            }
+            if (DEBUG) Log.v(TAG, "union of childDrawable rects " + d + " to: "
+                        + maxWidth + "x" + maxHeight);
+            d.setBounds(0, 0, maxWidth, maxHeight);
+            for (int i = 0; i < d.getStateCount(); i++) {
+                Drawable childDrawable = d.getStateDrawable(i);
+                if (DEBUG) Log.v(TAG, "sizing drawable " + childDrawable + " to: "
+                            + maxWidth + "x" + maxHeight);
+                childDrawable.setBounds(0, 0, maxWidth, maxHeight);
+            }
+        } else if (mDrawable != null) {
+            mDrawable.setBounds(0, 0,
+                    mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
+        }
+    }
+
+    public void setX(float x) {
+        mTranslationX = x;
+    }
+
+    public void setY(float y) {
+        mTranslationY = y;
+    }
+
+    public void setScaleX(float x) {
+        mScaleX = x;
+    }
+
+    public void setScaleY(float y) {
+        mScaleY = y;
+    }
+
+    public void setAlpha(float alpha) {
+        mAlpha = alpha;
+    }
+
+    public float getX() {
+        return mTranslationX;
+    }
+
+    public float getY() {
+        return mTranslationY;
+    }
+
+    public float getScaleX() {
+        return mScaleX;
+    }
+
+    public float getScaleY() {
+        return mScaleY;
+    }
+
+    public float getAlpha() {
+        return mAlpha;
+    }
+
+    public void setPositionX(float x) {
+        mPositionX = x;
+    }
+
+    public void setPositionY(float y) {
+        mPositionY = y;
+    }
+
+    public float getPositionX() {
+        return mPositionX;
+    }
+
+    public float getPositionY() {
+        return mPositionY;
+    }
+
+    public int getWidth() {
+        return mDrawable != null ? mDrawable.getIntrinsicWidth() : 0;
+    }
+
+    public int getHeight() {
+        return mDrawable != null ? mDrawable.getIntrinsicHeight() : 0;
+    }
+
+    public void draw(Canvas canvas) {
+        if (mDrawable == null || !mEnabled) {
+            return;
+        }
+        canvas.save(Canvas.MATRIX_SAVE_FLAG);
+        canvas.scale(mScaleX, mScaleY, mPositionX, mPositionY);
+        canvas.translate(mTranslationX + mPositionX, mTranslationY + mPositionY);
+        canvas.translate(-0.5f * getWidth(), -0.5f * getHeight());
+        mDrawable.setAlpha((int) Math.round(mAlpha * 255f));
+        mDrawable.draw(canvas);
+        canvas.restore();
+    }
+
+    public void setEnabled(boolean enabled) {
+        mEnabled  = enabled;
+    }
+
+    public int getResourceId() {
+        return mResourceId;
+    }
+}
diff --git a/core/java/com/android/internal/widget/multiwaveview/Tweener.java b/core/java/com/android/internal/widget/multiwaveview/Tweener.java
new file mode 100644
index 0000000..d559d9d
--- /dev/null
+++ b/core/java/com/android/internal/widget/multiwaveview/Tweener.java
@@ -0,0 +1,177 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget.multiwaveview;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map.Entry;
+
+import android.animation.Animator.AnimatorListener;
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.animation.TimeInterpolator;
+import android.animation.ValueAnimator.AnimatorUpdateListener;
+import android.util.Log;
+
+class Tweener {
+    private static final String TAG = "Tweener";
+    private static final boolean DEBUG = false;
+
+    ObjectAnimator animator;
+    private static HashMap<Object, Tweener> sTweens = new HashMap<Object, Tweener>();
+
+    public Tweener(ObjectAnimator anim) {
+        animator = anim;
+    }
+
+    private static void remove(Animator animator) {
+        Iterator<Entry<Object, Tweener>> iter = sTweens.entrySet().iterator();
+        while (iter.hasNext()) {
+            Entry<Object, Tweener> entry = iter.next();
+            if (entry.getValue().animator == animator) {
+                if (DEBUG) Log.v(TAG, "Removing tweener " + sTweens.get(entry.getKey())
+                        + " sTweens.size() = " + sTweens.size());
+                iter.remove();
+                break; // an animator can only be attached to one object
+            }
+        }
+    }
+
+    public static Tweener to(Object object, long duration, Object... vars) {
+        long delay = 0;
+        AnimatorUpdateListener updateListener = null;
+        AnimatorListener listener = null;
+        TimeInterpolator interpolator = null;
+
+        // Iterate through arguments and discover properties to animate
+        ArrayList<PropertyValuesHolder> props = new ArrayList<PropertyValuesHolder>(vars.length/2);
+        for (int i = 0; i < vars.length; i+=2) {
+            if (!(vars[i] instanceof String)) {
+                throw new IllegalArgumentException("Key must be a string: " + vars[i]);
+            }
+            String key = (String) vars[i];
+            Object value = vars[i+1];
+            if ("simultaneousTween".equals(key)) {
+                // TODO
+            } else if ("ease".equals(key)) {
+                interpolator = (TimeInterpolator) value; // TODO: multiple interpolators?
+            } else if ("onUpdate".equals(key) || "onUpdateListener".equals(key)) {
+                updateListener = (AnimatorUpdateListener) value;
+            } else if ("onComplete".equals(key) || "onCompleteListener".equals(key)) {
+                listener = (AnimatorListener) value;
+            } else if ("delay".equals(key)) {
+                delay = ((Number) value).longValue();
+            } else if ("syncWith".equals(key)) {
+                // TODO
+            } else if (value instanceof float[]) {
+                props.add(PropertyValuesHolder.ofFloat(key,
+                        ((float[])value)[0], ((float[])value)[1]));
+            } else if (value instanceof int[]) {
+                props.add(PropertyValuesHolder.ofInt(key,
+                        ((int[])value)[0], ((int[])value)[1]));
+            } else if (value instanceof Number) {
+                float floatValue = ((Number)value).floatValue();
+                props.add(PropertyValuesHolder.ofFloat(key, floatValue));
+            } else {
+                throw new IllegalArgumentException(
+                        "Bad argument for key \"" + key + "\" with value " + value.getClass());
+            }
+        }
+
+        // Re-use existing tween, if present
+        Tweener tween = sTweens.get(object);
+        ObjectAnimator anim = null;
+        if (tween == null) {
+            anim = ObjectAnimator.ofPropertyValuesHolder(object,
+                    props.toArray(new PropertyValuesHolder[props.size()]));
+            tween = new Tweener(anim);
+            sTweens.put(object, tween);
+            if (DEBUG) Log.v(TAG, "Added new Tweener " + tween);
+        } else {
+            anim = sTweens.get(object).animator;
+            replace(props, object); // Cancel all animators for given object
+        }
+
+        if (interpolator != null) {
+            anim.setInterpolator(interpolator);
+        }
+
+        // Update animation with properties discovered in loop above
+        anim.setStartDelay(delay);
+        anim.setDuration(duration);
+        if (updateListener != null) {
+            anim.removeAllUpdateListeners(); // There should be only one
+            anim.addUpdateListener(updateListener);
+        }
+        if (listener != null) {
+            anim.removeAllListeners(); // There should be only one.
+            anim.addListener(listener);
+        }
+        anim.addListener(mCleanupListener);
+
+        return tween;
+    }
+
+    Tweener from(Object object, long duration, Object... vars) {
+        // TODO:  for v of vars
+        //            toVars[v] = object[v]
+        //            object[v] = vars[v]
+        return Tweener.to(object, duration, vars);
+    }
+
+    // Listener to watch for completed animations and remove them.
+    private static AnimatorListener mCleanupListener = new AnimatorListenerAdapter() {
+
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            remove(animation);
+        }
+
+        @Override
+        public void onAnimationCancel(Animator animation) {
+            remove(animation);
+        }
+    };
+
+    public static void reset() {
+        if (DEBUG) {
+            Log.v(TAG, "Reset()");
+            if (sTweens.size() > 0) {
+                Log.v(TAG, "Cleaning up " + sTweens.size() + " animations");
+            }
+        }
+        sTweens.clear();
+    }
+
+    private static void replace(ArrayList<PropertyValuesHolder> props, Object... args) {
+        for (final Object killobject : args) {
+            Tweener tween = sTweens.get(killobject);
+            if (tween != null) {
+                tween.animator.cancel();
+                if (props != null) {
+                    tween.animator.setValues(
+                            props.toArray(new PropertyValuesHolder[props.size()]));
+                } else {
+                    sTweens.remove(tween);
+                }
+            }
+        }
+    }
+}
diff --git a/core/res/res/layout/notification_template_material_base.xml b/core/res/res/layout/notification_template_material_base.xml
index ea22b15..3fdcaf7 100644
--- a/core/res/res/layout/notification_template_material_base.xml
+++ b/core/res/res/layout/notification_template_material_base.xml
@@ -16,9 +16,12 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="64dp"
+    internal:layout_minHeight="64dp"
+    internal:layout_maxHeight="64dp"
     >
     <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_big_base.xml b/core/res/res/layout/notification_template_material_big_base.xml
index 2a3ee90..935424a 100644
--- a/core/res/res/layout/notification_template_material_big_base.xml
+++ b/core/res/res/layout/notification_template_material_big_base.xml
@@ -16,9 +16,12 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    internal:layout_minHeight="65dp"
+    internal:layout_maxHeight="unbounded"
     >
     <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_big_picture.xml b/core/res/res/layout/notification_template_material_big_picture.xml
index f1a9549..302e651 100644
--- a/core/res/res/layout/notification_template_material_big_picture.xml
+++ b/core/res/res/layout/notification_template_material_big_picture.xml
@@ -16,9 +16,12 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
+    internal:layout_minHeight="65dp"
+    internal:layout_maxHeight="unbounded"
     >
     <ImageView
         android:id="@+id/big_picture"
diff --git a/core/res/res/layout/notification_template_material_big_text.xml b/core/res/res/layout/notification_template_material_big_text.xml
index f657f04..d0c10b2 100644
--- a/core/res/res/layout/notification_template_material_big_text.xml
+++ b/core/res/res/layout/notification_template_material_big_text.xml
@@ -16,9 +16,12 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    internal:layout_minHeight="65dp"
+    internal:layout_maxHeight="unbounded"
     >
     <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_inbox.xml b/core/res/res/layout/notification_template_material_inbox.xml
index d292d4e..ac448ee 100644
--- a/core/res/res/layout/notification_template_material_inbox.xml
+++ b/core/res/res/layout/notification_template_material_inbox.xml
@@ -16,9 +16,12 @@
   -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
+    internal:layout_minHeight="65dp"
+    internal:layout_maxHeight="unbounded"
     >
     <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/layout/notification_template_material_media.xml b/core/res/res/layout/notification_template_material_media.xml
index 0292d28..69020a4 100644
--- a/core/res/res/layout/notification_template_material_media.xml
+++ b/core/res/res/layout/notification_template_material_media.xml
@@ -16,11 +16,14 @@
   -->
 
 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="64dp"
     android:orientation="horizontal"
     android:background="#00000000"
+    internal:layout_minHeight="64dp"
+    internal:layout_maxHeight="64dp"
     >
     <include layout="@layout/notification_template_icon_group"
         android:layout_width="@dimen/notification_large_icon_width"
diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml
index 39c42ee..551c044 100644
--- a/core/res/res/values/attrs.xml
+++ b/core/res/res/values/attrs.xml
@@ -7034,16 +7034,75 @@
     <!-- =============================== -->
     <eat-comment />
     <declare-styleable name="GlowPadView">
-        <!-- Reference to an array resource that be used as description for the targets around the circle.
-             {@deprecated Removed.} -->
+        <!-- Reference to an array resource that be shown as targets around a circle. -->
+        <attr name="targetDrawables" format="reference" />
+
+        <!-- Reference to an array resource that be used as description for the targets around the circle. -->
         <attr name="targetDescriptions" format="reference" />
 
-        <!-- Reference to an array resource that be used to announce the directions with targets around the circle.
-             {@deprecated Removed.}-->
+        <!-- Reference to an array resource that be used to announce the directions with targets around the circle. -->
         <attr name="directionDescriptions" format="reference" />
+
+        <!-- Sets a drawable as the center. -->
+        <attr name="handleDrawable" format="reference" />
+
+        <!-- Drawable to use for wave ripple animation. -->
+        <attr name="outerRingDrawable" format="reference"/>
+
+        <!-- Drawble used for drawing points -->
+        <attr name="pointDrawable" format="reference" />
+
+        <!-- Inner radius of glow area. -->
+        <attr name="innerRadius"/>
+
+        <!-- Outer radius of glow area. Target icons will be drawn on this circle. -->
+        <attr name="outerRadius" format="dimension" />
+
+        <!-- Radius of glow under finger. -->
+        <attr name="glowRadius" format="dimension" />
+
+        <!-- Tactile feedback duration for actions. Set to '0' for no vibration. -->
+        <attr name="vibrationDuration" format="integer" />
+
+        <!-- How close we need to be before snapping to a target. -->
+        <attr name="snapMargin" format="dimension" />
+
+        <!-- Number of waves/chevrons to show in animation. -->
+        <attr name="feedbackCount" format="integer" />
+
+        <!-- Used when the handle shouldn't wait to be hit before following the finger -->
+        <attr name="alwaysTrackFinger" format="boolean" />
+
+        <!-- Location along the circle of the first item, in degrees.-->
+        <attr name="firstItemOffset" format="float" />
+
+        <!-- Causes targets to snap to the finger location on activation. -->
+        <attr name="magneticTargets" format="boolean" />
+
+        <attr name="gravity" />
+
+        <!-- Determine whether the glow pad is allowed to scale to fit the bounds indicated
+            by its parent. If this is set to false, no scaling will occur. If this is set to true
+            scaling will occur to fit for any axis in which gravity is set to center. -->
+        <attr name="allowScaling" format="boolean" />
     </declare-styleable>
 
     <!-- =============================== -->
+    <!-- SizeAdaptiveLayout class attributes -->
+    <!-- =============================== -->
+    <eat-comment />
+    <declare-styleable name="SizeAdaptiveLayout_Layout">
+      <!-- The maximum valid height for this item. -->
+      <attr name="layout_maxHeight" format="dimension">
+        <!-- Indicates that the view may be resized arbitrarily large. -->
+        <enum name="unbounded" value="-1" />
+      </attr>
+      <!-- The minimum valid height for this item. -->
+      <attr name="layout_minHeight" format="dimension" />
+    </declare-styleable>
+    <declare-styleable name="SizeAdaptiveLayout" />
+
+    <!-- =============================== -->
     <!-- Location package class attributes -->
     <!-- =============================== -->
     <eat-comment />
@@ -7450,6 +7509,11 @@
         <enum name="pageDeleteDropTarget" value="7" />
     </attr>
 
+    <declare-styleable name="SlidingChallengeLayout_Layout">
+        <attr name="layout_childType" />
+        <attr name="layout_maxHeight" />
+    </declare-styleable>
+
     <!-- Attributes that can be used with <code>&lt;FragmentBreadCrumbs&gt;</code>
     tags. -->
     <declare-styleable name="FragmentBreadCrumbs">
@@ -7458,6 +7522,27 @@
         <attr name="itemColor" format="color|reference" />
     </declare-styleable>
 
+    <declare-styleable name="MultiPaneChallengeLayout">
+        <!-- Influences how layout_centerWithinArea behaves -->
+        <attr name="orientation" />
+    </declare-styleable>
+
+    <declare-styleable name="MultiPaneChallengeLayout_Layout">
+        <!-- Percentage of the screen this child should consume or center within.
+             If 0/default, the view will be measured by standard rules
+             as if this were a FrameLayout. -->
+        <attr name="layout_centerWithinArea" format="float" />
+        <attr name="layout_childType" />
+        <attr name="layout_gravity" />
+        <attr name="layout_maxWidth" format="dimension" />
+        <attr name="layout_maxHeight" />
+    </declare-styleable>
+
+    <declare-styleable name="KeyguardSecurityViewFlipper_Layout">
+        <attr name="layout_maxWidth" />
+        <attr name="layout_maxHeight" />
+    </declare-styleable>
+
     <declare-styleable name="Toolbar">
         <attr name="titleTextAppearance" format="reference" />
         <attr name="subtitleTextAppearance" format="reference" />
diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml
index 9945c63..9f49b08 100644
--- a/core/res/res/values/public.xml
+++ b/core/res/res/values/public.xml
@@ -1759,9 +1759,7 @@
   <public type="attr" name="actionModeSplitBackground" id="0x0101039d" />
   <public type="attr" name="textAppearanceListItem" id="0x0101039e" />
   <public type="attr" name="textAppearanceListItemSmall" id="0x0101039f" />
-  <!-- @deprecated Removed. -->
   <public type="attr" name="targetDescriptions" id="0x010103a0" />
-  <!-- @deprecated Removed. -->
   <public type="attr" name="directionDescriptions" id="0x010103a1" />
   <public type="attr" name="overridesImplicitlyEnabledSubtype" id="0x010103a2" />
   <public type="attr" name="listPreferredItemPaddingLeft" id="0x010103a3" />
diff --git a/core/tests/coretests/res/layout/size_adaptive.xml b/core/tests/coretests/res/layout/size_adaptive.xml
new file mode 100644
index 0000000..03d0574
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/multi1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_color.xml b/core/tests/coretests/res/layout/size_adaptive_color.xml
new file mode 100644
index 0000000..cdb7a59
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_color.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:background="#ffffff"
+    android:id="@+id/multi1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml b/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml
new file mode 100644
index 0000000..d24df5b
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_color_statelist.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:background="@drawable/size_adaptive_statelist"
+    android:id="@+id/multi1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_four_u.xml b/core/tests/coretests/res/layout/size_adaptive_four_u.xml
new file mode 100644
index 0000000..232b921
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_four_u.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gridLayout4"
+    android:layout_width="match_parent"
+    android:layout_height="256dp"
+    android:background="#000000"
+    android:columnCount="2"
+    android:padding="1dp" >
+
+    <ImageView
+        android:id="@+id/actor"
+        android:layout_width="62dp"
+        android:layout_height="62dp"
+        android:layout_row="0"
+        android:layout_column="0"
+        android:layout_rowSpan="2"
+        android:contentDescription="@string/actor"
+        android:src="@drawable/abe" />
+
+    <TextView
+        android:layout_width="0dp"
+        android:id="@+id/name"
+        android:layout_row="0"
+        android:layout_column="1"
+        android:layout_gravity="fill_horizontal"
+        android:padding="3dp"
+        android:text="@string/actor"
+        android:textColor="#ffffff"
+        android:textStyle="bold" />
+
+    <ImageView
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_row="1"
+        android:layout_column="1"
+        android:layout_gravity="fill_horizontal"
+        android:padding="5dp"
+        android:adjustViewBounds="true"
+        android:background="#555555"
+        android:scaleType="centerCrop"
+        android:src="@drawable/gettysburg"
+        android:contentDescription="@string/caption" />
+
+    <TextView
+        android:layout_width="0dp"
+        android:id="@+id/note"
+        android:layout_row="2"
+        android:layout_column="1"
+        android:layout_gravity="fill_horizontal"
+        android:padding="3dp"
+        android:singleLine="true"
+        android:text="@string/first"
+        android:textColor="#ffffff" />
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml b/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml
new file mode 100644
index 0000000..93a10de
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_four_u_text.xml
@@ -0,0 +1,59 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gridLayout4"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:background="#000000"
+    android:columnCount="2"
+    android:padding="1dp"
+    android:orientation="horizontal" >
+
+    <ImageView
+        android:id="@+id/actor"
+        android:layout_width="62dp"
+        android:layout_height="62dp"
+        android:layout_row="0"
+        android:layout_column="0"
+        android:layout_rowSpan="2"
+        android:contentDescription="@string/actor"
+        android:src="@drawable/abe" />
+
+    <TextView
+        android:layout_width="0dp"
+        android:id="@+id/name"
+        android:layout_row="0"
+        android:layout_column="1"
+        android:layout_gravity="fill_horizontal"
+        android:padding="3dp"
+        android:text="@string/actor"
+        android:textColor="#ffffff"
+        android:textStyle="bold" />
+
+    <TextView
+        android:id="@+id/note"
+        android:layout_width="0dp"
+        android:layout_height="wrap_content"
+        android:layout_column="1"
+        android:layout_gravity="fill_horizontal"
+        android:layout_marginTop="5dp"
+        android:layout_row="1"
+        android:padding="3dp"
+        android:singleLine="false"
+        android:text="@string/first"
+        android:textColor="#ffffff" />
+
+    </GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_gappy.xml b/core/tests/coretests/res/layout/size_adaptive_gappy.xml
new file mode 100644
index 0000000..d5e3b41
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_gappy.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/multi_with_gap"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="256dp"
+        internal:layout_minHeight="128dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_large_only.xml b/core/tests/coretests/res/layout/size_adaptive_large_only.xml
new file mode 100644
index 0000000..cf58265
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_large_only.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/large_only_multi"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="256dp"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_lies.xml b/core/tests/coretests/res/layout/size_adaptive_lies.xml
new file mode 100644
index 0000000..7de892e
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_lies.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/multi1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_one_u.xml b/core/tests/coretests/res/layout/size_adaptive_one_u.xml
new file mode 100644
index 0000000..b6fe4a0
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_one_u.xml
@@ -0,0 +1,66 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gridLayout1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp"
+    android:background="#000000"
+    android:columnCount="3"
+    android:padding="1dp"
+    android:rowCount="2" >
+
+    <ImageView
+        android:id="@+id/actor"
+        android:layout_width="62dp"
+        android:layout_height="62dp"
+        android:layout_column="0"
+        android:layout_row="0"
+        android:layout_rowSpan="2"
+        android:contentDescription="@string/actor"
+        android:src="@drawable/abe" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_gravity="fill"
+        android:padding="3dp"
+        android:text="@string/actor"
+        android:textColor="#ffffff"
+        android:textStyle="bold" />
+
+    <ImageView
+        android:layout_width="62dp"
+        android:layout_height="62dp"
+        android:layout_gravity="fill_vertical"
+        android:layout_rowSpan="2"
+        android:adjustViewBounds="true"
+        android:background="#555555"
+        android:padding="2dp"
+        android:scaleType="fitXY"
+        android:src="@drawable/gettysburg"
+        android:contentDescription="@string/caption" />
+
+    <TextView
+        android:id="@+id/note"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_gravity="fill"
+        android:layout_marginTop="5dp"
+        android:padding="3dp"
+        android:singleLine="true"
+        android:text="@string/first"
+        android:textColor="#ffffff" />
+
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml b/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml
new file mode 100644
index 0000000..df54eb6
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_one_u_text.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<GridLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/gridLayout1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp"
+    android:background="#000000"
+    android:columnCount="2"
+    android:padding="1dp"
+    android:rowCount="2" >
+
+    <ImageView
+        android:id="@+id/actor"
+        android:layout_width="62dp"
+        android:layout_height="62dp"
+        android:layout_column="0"
+        android:layout_row="0"
+        android:layout_rowSpan="2"
+        android:contentDescription="@string/actor"
+        android:src="@drawable/abe" />
+
+    <TextView
+        android:id="@+id/name"
+        android:layout_gravity="fill"
+        android:padding="3dp"
+        android:text="@string/actor"
+        android:textColor="#ffffff"
+        android:textStyle="bold" />
+
+    <TextView
+        android:id="@+id/note"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        android:layout_gravity="fill"
+        android:layout_marginTop="5dp"
+        android:padding="3dp"
+        android:singleLine="true"
+        android:text="@string/first"
+        android:textColor="#ffffff" />
+
+</GridLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_overlapping.xml b/core/tests/coretests/res/layout/size_adaptive_overlapping.xml
new file mode 100644
index 0000000..4abe8b0
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_overlapping.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/multi_with_overlap"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="256dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="256dp"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_singleton.xml b/core/tests/coretests/res/layout/size_adaptive_singleton.xml
new file mode 100644
index 0000000..eba387f
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_singleton.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u_text"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_text.xml b/core/tests/coretests/res/layout/size_adaptive_text.xml
new file mode 100644
index 0000000..a9f0ba9
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_text.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/multi1"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u_text"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u_text"
+        android:layout_width="fill_parent"
+        android:layout_height="wrap_content"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/res/layout/size_adaptive_three_way.xml b/core/tests/coretests/res/layout/size_adaptive_three_way.xml
new file mode 100644
index 0000000..1eb5396
--- /dev/null
+++ b/core/tests/coretests/res/layout/size_adaptive_three_way.xml
@@ -0,0 +1,48 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Copyright (C) 2012 The Android Open Source Project
+
+     Licensed under the Apache License, Version 2.0 (the "License");
+     you may not use this file except in compliance with the License.
+     You may obtain a copy of the License at
+
+          http://www.apache.org/licenses/LICENSE-2.0
+
+     Unless required by applicable law or agreed to in writing, software
+     distributed under the License is distributed on an "AS IS" BASIS,
+     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+     See the License for the specific language governing permissions and
+     limitations under the License.
+-->
+<com.android.internal.widget.SizeAdaptiveLayout
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
+    android:id="@+id/three_way_multi"
+    android:layout_width="match_parent"
+    android:layout_height="64dp" >
+
+    <include
+        android:id="@+id/one_u"
+        layout="@layout/size_adaptive_one_u"
+        android:layout_width="fill_parent"
+        android:layout_height="64dp"
+        internal:layout_minHeight="64dp"
+        internal:layout_maxHeight="64dp"
+        />
+
+    <include
+        android:id="@+id/two_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="128dp"
+        internal:layout_minHeight="65dp"
+        internal:layout_maxHeight="128dp"/>
+
+    <include
+        android:id="@+id/four_u"
+        layout="@layout/size_adaptive_four_u"
+        android:layout_width="fill_parent"
+        android:layout_height="256dp"
+        internal:layout_minHeight="129dp"
+        internal:layout_maxHeight="unbounded"/>
+
+</com.android.internal.widget.SizeAdaptiveLayout>
diff --git a/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java b/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java
new file mode 100644
index 0000000..18411b0
--- /dev/null
+++ b/core/tests/coretests/src/com/android/internal/widget/SizeAdaptiveLayoutTest.java
@@ -0,0 +1,479 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.internal.widget;
+
+import com.android.frameworks.coretests.R;
+
+import android.content.Context;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.LayoutInflater;
+import android.view.View;
+
+import com.android.internal.widget.SizeAdaptiveLayout;
+
+
+public class SizeAdaptiveLayoutTest extends AndroidTestCase {
+
+    private LayoutInflater mInflater;
+    private int mOneU;
+    private int mFourU;
+    private SizeAdaptiveLayout mSizeAdaptiveLayout;
+    private View mSmallView;
+    private View mMediumView;
+    private View mLargeView;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        // inflate the layout
+        final Context context = getContext();
+        mInflater = LayoutInflater.from(context);
+        mOneU = 64;
+        mFourU = 4 * mOneU;
+    }
+
+    private void inflate(int resource){
+        mSizeAdaptiveLayout = (SizeAdaptiveLayout) mInflater.inflate(resource, null);
+        mSizeAdaptiveLayout.onAttachedToWindow();
+
+        mSmallView = mSizeAdaptiveLayout.findViewById(R.id.one_u);
+        mMediumView = mSizeAdaptiveLayout.findViewById(R.id.two_u);
+        mLargeView = mSizeAdaptiveLayout.findViewById(R.id.four_u);
+    }
+
+    /**
+     * The name 'test preconditions' is a convention to signal that if this
+     * test doesn't pass, the test case was not set up properly and it might
+     * explain any and all failures in other tests.  This is not guaranteed
+     * to run before other tests, as junit uses reflection to find the tests.
+     */
+    @SmallTest
+    public void testPreconditions() {
+        assertNotNull(mInflater);
+
+        inflate(R.layout.size_adaptive);
+        assertNotNull(mSizeAdaptiveLayout);
+        assertNotNull(mSmallView);
+        assertNotNull(mLargeView);
+    }
+
+    @SmallTest
+    public void testOpenLarge() {
+        inflate(R.layout.size_adaptive);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight + 10;
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertEquals("1U should be gone",
+                View.GONE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenSmall() {
+        inflate(R.layout.size_adaptive);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+        assertEquals("4U should be gone",
+                View.GONE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenTooSmall() {
+        inflate(R.layout.size_adaptive);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight - 10;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+        assertEquals("4U should be gone",
+                View.GONE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenTooBig() {
+        inflate(R.layout.size_adaptive);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        lp.maxHeight = 500;
+        mLargeView.setLayoutParams(lp);
+        int height = (int) (lp.minHeight + 10);
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertEquals("1U should be gone",
+                View.GONE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenWrapContent() {
+        inflate(R.layout.size_adaptive_text);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight + 10;
+
+        // manually measure it, and lay it out
+        int measureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
+        mSizeAdaptiveLayout.measure(500, measureSpec);
+        assertTrue("should not be forced to 4U",
+                mSizeAdaptiveLayout.getMeasuredHeight() < mFourU);
+    }
+
+    @SmallTest
+    public void testOpenOneUOnlySmall() {
+        inflate(R.layout.size_adaptive_singleton);
+        assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight - 10;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenOneUOnlyLarge() {
+        inflate(R.layout.size_adaptive_singleton);
+        assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.maxHeight + 10;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenOneUOnlyJustRight() {
+        inflate(R.layout.size_adaptive_singleton);
+        assertNull("largeView should be NULL in the singleton layout", mLargeView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenFourUOnlySmall() {
+        inflate(R.layout.size_adaptive_large_only);
+        assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight - 10;
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenFourUOnlyLarge() {
+        inflate(R.layout.size_adaptive_large_only);
+        assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.maxHeight + 10;
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenFourUOnlyJustRight() {
+        inflate(R.layout.size_adaptive_large_only);
+        assertNull("smallView should be NULL in the singleton layout", mSmallView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenIntoAGap() {
+        inflate(R.layout.size_adaptive_gappy);
+
+        SizeAdaptiveLayout.LayoutParams smallParams =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        SizeAdaptiveLayout.LayoutParams largeParams =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        assertTrue("gappy layout should have a gap",
+                smallParams.maxHeight + 10 < largeParams.minHeight);
+        int height = (int) smallParams.maxHeight + 10;
+
+        measureAndLayout(height);
+
+        assertTrue("one and only one view should be visible",
+                mLargeView.getVisibility() != mSmallView.getVisibility());
+        // behavior is undefined in this case.
+    }
+
+    @SmallTest
+    public void testOpenIntoAnOverlap() {
+        inflate(R.layout.size_adaptive_overlapping);
+
+        SizeAdaptiveLayout.LayoutParams smallParams =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        SizeAdaptiveLayout.LayoutParams largeParams =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        assertEquals("overlapping layout should overlap",
+                smallParams.minHeight,
+                largeParams.minHeight);
+        int height = (int) smallParams.maxHeight;
+
+        measureAndLayout(height);
+
+        assertTrue("one and only one view should be visible",
+                mLargeView.getVisibility() != mSmallView.getVisibility());
+        assertEquals("1U should get priority in an overlap because it is first",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenThreeWayViewSmall() {
+        inflate(R.layout.size_adaptive_three_way);
+        assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+        assertEquals("2U should be gone",
+                View.GONE,
+                mMediumView.getVisibility());
+        assertEquals("4U should be gone",
+                View.GONE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenThreeWayViewMedium() {
+        inflate(R.layout.size_adaptive_three_way);
+        assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mMediumView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be gone",
+                View.GONE,
+                mSmallView.getVisibility());
+        assertEquals("2U should be visible",
+                View.VISIBLE,
+                mMediumView.getVisibility());
+        assertEquals("4U should be gone",
+                View.GONE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testOpenThreeWayViewLarge() {
+        inflate(R.layout.size_adaptive_three_way);
+        assertNotNull("mMediumView should not be NULL in the three view layout", mMediumView);
+
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be gone",
+                View.GONE,
+                mSmallView.getVisibility());
+        assertEquals("2U should be gone",
+                View.GONE,
+                mMediumView.getVisibility());
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+    }
+
+    @SmallTest
+    public void testResizeWithoutAnimation() {
+        inflate(R.layout.size_adaptive);
+
+        SizeAdaptiveLayout.LayoutParams largeParams =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int startHeight = (int) largeParams.minHeight + 10;
+        int endHeight = (int) largeParams.minHeight + 10;
+
+        measureAndLayout(startHeight);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertFalse("There should be no animation on initial rendering.",
+                    mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+
+        measureAndLayout(endHeight);
+
+        assertEquals("4U should still be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertFalse("There should be no animation on scale within a view.",
+                    mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+    }
+
+    @SmallTest
+    public void testResizeWithAnimation() {
+        inflate(R.layout.size_adaptive);
+
+        SizeAdaptiveLayout.LayoutParams smallParams =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        SizeAdaptiveLayout.LayoutParams largeParams =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int startHeight = (int) largeParams.minHeight + 10;
+        int endHeight = (int) smallParams.maxHeight;
+
+        measureAndLayout(startHeight);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertFalse("There should be no animation on initial rendering.",
+                    mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+
+        measureAndLayout(endHeight);
+
+        assertEquals("1U should now be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+        assertTrue("There should be an animation on scale between views.",
+                   mSizeAdaptiveLayout.getTransitionAnimation().isRunning());
+    }
+
+    @SmallTest
+    public void testModestyPanelChangesColorWhite() {
+        inflate(R.layout.size_adaptive_color);
+        View panel = mSizeAdaptiveLayout.getModestyPanel();
+        assertTrue("ModestyPanel should have a ColorDrawable background",
+                   panel.getBackground() instanceof ColorDrawable);
+        ColorDrawable panelColor = (ColorDrawable) panel.getBackground();
+        ColorDrawable salColor = (ColorDrawable) mSizeAdaptiveLayout.getBackground();
+        assertEquals("ModestyPanel color should match the SizeAdaptiveLayout",
+                     panelColor.getColor(), salColor.getColor());
+    }
+
+    @SmallTest
+    public void testModestyPanelTracksStateListColor() {
+        inflate(R.layout.size_adaptive_color_statelist);
+        View panel = mSizeAdaptiveLayout.getModestyPanel();
+        assertEquals("ModestyPanel should have a ColorDrawable background" ,
+                     panel.getBackground().getClass(), ColorDrawable.class);
+        ColorDrawable panelColor = (ColorDrawable) panel.getBackground();
+        assertEquals("ModestyPanel color should match the SizeAdaptiveLayout",
+                     panelColor.getColor(), Color.RED);
+    }
+    @SmallTest
+    public void testOpenSmallEvenWhenLargeIsActuallySmall() {
+        inflate(R.layout.size_adaptive_lies);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mSmallView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("1U should be visible",
+                View.VISIBLE,
+                mSmallView.getVisibility());
+        assertTrue("1U should also have been measured",
+                   mSmallView.getMeasuredHeight() > 0);
+    }
+
+    @SmallTest
+    public void testOpenLargeEvenWhenLargeIsActuallySmall() {
+        inflate(R.layout.size_adaptive_lies);
+        SizeAdaptiveLayout.LayoutParams lp =
+          (SizeAdaptiveLayout.LayoutParams) mLargeView.getLayoutParams();
+        int height = (int) lp.minHeight;
+
+        measureAndLayout(height);
+
+        assertEquals("4U should be visible",
+                View.VISIBLE,
+                mLargeView.getVisibility());
+        assertTrue("4U should also have been measured",
+                   mLargeView.getMeasuredHeight() > 0);
+    }
+
+    private void measureAndLayout(int height) {
+        // manually measure it, and lay it out
+        int measureSpec = View.MeasureSpec.makeMeasureSpec(height, View.MeasureSpec.AT_MOST);
+        mSizeAdaptiveLayout.measure(500, measureSpec);
+        mSizeAdaptiveLayout.layout(0, 0, 500, mSizeAdaptiveLayout.getMeasuredHeight());
+    }
+}
diff --git a/packages/SystemUI/res/layout/notification_public_default.xml b/packages/SystemUI/res/layout/notification_public_default.xml
index 044ba09..efabc06 100644
--- a/packages/SystemUI/res/layout/notification_public_default.xml
+++ b/packages/SystemUI/res/layout/notification_public_default.xml
@@ -16,9 +16,12 @@
   -->
 
 <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:internal="http://schemas.android.com/apk/prv/res/android"
     android:id="@+id/status_bar_latest_event_content"
     android:layout_width="match_parent"
     android:layout_height="64dp"
+    internal:layout_minHeight="64dp"
+    internal:layout_maxHeight="64dp"
     >
     <ImageView android:id="@+id/icon"
         android:layout_width="40dp"