Import Android SDK Platform PI [4335822]

/google/data/ro/projects/android/fetch_artifact \
    --bid 4335822 \
    --target sdk_phone_armv7-win_sdk \
    sdk-repo-linux-sources-4335822.zip

AndroidVersion.ApiLevel has been modified to appear as 28

Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
diff --git a/android/widget/AbsListView.java b/android/widget/AbsListView.java
new file mode 100644
index 0000000..170582b
--- /dev/null
+++ b/android/widget/AbsListView.java
@@ -0,0 +1,7687 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.TransitionDrawable;
+import android.os.Bundle;
+import android.os.Debug;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.os.Trace;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.LongSparseArray;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.StateSet;
+import android.view.ActionMode;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputContentInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.RemoteViews.OnClickHandler;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Base class that can be used to implement virtualized lists of items. A list does
+ * not have a spatial definition here. For instance, subclases of this class can
+ * display the content of the list in a grid, in a carousel, as stack, etc.
+ *
+ * @attr ref android.R.styleable#AbsListView_listSelector
+ * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+ * @attr ref android.R.styleable#AbsListView_stackFromBottom
+ * @attr ref android.R.styleable#AbsListView_scrollingCache
+ * @attr ref android.R.styleable#AbsListView_textFilterEnabled
+ * @attr ref android.R.styleable#AbsListView_transcriptMode
+ * @attr ref android.R.styleable#AbsListView_cacheColorHint
+ * @attr ref android.R.styleable#AbsListView_fastScrollEnabled
+ * @attr ref android.R.styleable#AbsListView_smoothScrollbar
+ * @attr ref android.R.styleable#AbsListView_choiceMode
+ */
+public abstract class AbsListView extends AdapterView<ListAdapter> implements TextWatcher,
+        ViewTreeObserver.OnGlobalLayoutListener, Filter.FilterListener,
+        ViewTreeObserver.OnTouchModeChangeListener,
+        RemoteViewsAdapter.RemoteAdapterConnectionCallback {
+
+    @SuppressWarnings("UnusedDeclaration")
+    private static final String TAG = "AbsListView";
+
+    /**
+     * Disables the transcript mode.
+     *
+     * @see #setTranscriptMode(int)
+     */
+    public static final int TRANSCRIPT_MODE_DISABLED = 0;
+
+    /**
+     * The list will automatically scroll to the bottom when a data set change
+     * notification is received and only if the last item is already visible
+     * on screen.
+     *
+     * @see #setTranscriptMode(int)
+     */
+    public static final int TRANSCRIPT_MODE_NORMAL = 1;
+
+    /**
+     * The list will automatically scroll to the bottom, no matter what items
+     * are currently visible.
+     *
+     * @see #setTranscriptMode(int)
+     */
+    public static final int TRANSCRIPT_MODE_ALWAYS_SCROLL = 2;
+
+    /**
+     * Indicates that we are not in the middle of a touch gesture
+     */
+    static final int TOUCH_MODE_REST = -1;
+
+    /**
+     * Indicates we just received the touch event and we are waiting to see if the it is a tap or a
+     * scroll gesture.
+     */
+    static final int TOUCH_MODE_DOWN = 0;
+
+    /**
+     * Indicates the touch has been recognized as a tap and we are now waiting to see if the touch
+     * is a longpress
+     */
+    static final int TOUCH_MODE_TAP = 1;
+
+    /**
+     * Indicates we have waited for everything we can wait for, but the user's finger is still down
+     */
+    static final int TOUCH_MODE_DONE_WAITING = 2;
+
+    /**
+     * Indicates the touch gesture is a scroll
+     */
+    static final int TOUCH_MODE_SCROLL = 3;
+
+    /**
+     * Indicates the view is in the process of being flung
+     */
+    static final int TOUCH_MODE_FLING = 4;
+
+    /**
+     * Indicates the touch gesture is an overscroll - a scroll beyond the beginning or end.
+     */
+    static final int TOUCH_MODE_OVERSCROLL = 5;
+
+    /**
+     * Indicates the view is being flung outside of normal content bounds
+     * and will spring back.
+     */
+    static final int TOUCH_MODE_OVERFLING = 6;
+
+    /**
+     * Regular layout - usually an unsolicited layout from the view system
+     */
+    static final int LAYOUT_NORMAL = 0;
+
+    /**
+     * Show the first item
+     */
+    static final int LAYOUT_FORCE_TOP = 1;
+
+    /**
+     * Force the selected item to be on somewhere on the screen
+     */
+    static final int LAYOUT_SET_SELECTION = 2;
+
+    /**
+     * Show the last item
+     */
+    static final int LAYOUT_FORCE_BOTTOM = 3;
+
+    /**
+     * Make a mSelectedItem appear in a specific location and build the rest of
+     * the views from there. The top is specified by mSpecificTop.
+     */
+    static final int LAYOUT_SPECIFIC = 4;
+
+    /**
+     * Layout to sync as a result of a data change. Restore mSyncPosition to have its top
+     * at mSpecificTop
+     */
+    static final int LAYOUT_SYNC = 5;
+
+    /**
+     * Layout as a result of using the navigation keys
+     */
+    static final int LAYOUT_MOVE_SELECTION = 6;
+
+    /**
+     * Normal list that does not indicate choices
+     */
+    public static final int CHOICE_MODE_NONE = 0;
+
+    /**
+     * The list allows up to one choice
+     */
+    public static final int CHOICE_MODE_SINGLE = 1;
+
+    /**
+     * The list allows multiple choices
+     */
+    public static final int CHOICE_MODE_MULTIPLE = 2;
+
+    /**
+     * The list allows multiple choices in a modal selection mode
+     */
+    public static final int CHOICE_MODE_MULTIPLE_MODAL = 3;
+
+    /**
+     * The thread that created this view.
+     */
+    private final Thread mOwnerThread;
+
+    /**
+     * Controls if/how the user may choose/check items in the list
+     */
+    int mChoiceMode = CHOICE_MODE_NONE;
+
+    /**
+     * Controls CHOICE_MODE_MULTIPLE_MODAL. null when inactive.
+     */
+    ActionMode mChoiceActionMode;
+
+    /**
+     * Wrapper for the multiple choice mode callback; AbsListView needs to perform
+     * a few extra actions around what application code does.
+     */
+    MultiChoiceModeWrapper mMultiChoiceModeCallback;
+
+    /**
+     * Running count of how many items are currently checked
+     */
+    int mCheckedItemCount;
+
+    /**
+     * Running state of which positions are currently checked
+     */
+    SparseBooleanArray mCheckStates;
+
+    /**
+     * Running state of which IDs are currently checked.
+     * If there is a value for a given key, the checked state for that ID is true
+     * and the value holds the last known position in the adapter for that id.
+     */
+    LongSparseArray<Integer> mCheckedIdStates;
+
+    /**
+     * Controls how the next layout will happen
+     */
+    int mLayoutMode = LAYOUT_NORMAL;
+
+    /**
+     * Should be used by subclasses to listen to changes in the dataset
+     */
+    AdapterDataSetObserver mDataSetObserver;
+
+    /**
+     * The adapter containing the data to be displayed by this view
+     */
+    ListAdapter mAdapter;
+
+    /**
+     * The remote adapter containing the data to be displayed by this view to be set
+     */
+    private RemoteViewsAdapter mRemoteAdapter;
+
+    /**
+     * If mAdapter != null, whenever this is true the adapter has stable IDs.
+     */
+    boolean mAdapterHasStableIds;
+
+    /**
+     * This flag indicates the a full notify is required when the RemoteViewsAdapter connects
+     */
+    private boolean mDeferNotifyDataSetChanged = false;
+
+    /**
+     * Indicates whether the list selector should be drawn on top of the children or behind
+     */
+    boolean mDrawSelectorOnTop = false;
+
+    /**
+     * The drawable used to draw the selector
+     */
+    Drawable mSelector;
+
+    /**
+     * The current position of the selector in the list.
+     */
+    int mSelectorPosition = INVALID_POSITION;
+
+    /**
+     * Defines the selector's location and dimension at drawing time
+     */
+    Rect mSelectorRect = new Rect();
+
+    /**
+     * The data set used to store unused views that should be reused during the next layout
+     * to avoid creating new ones
+     */
+    final RecycleBin mRecycler = new RecycleBin();
+
+    /**
+     * The selection's left padding
+     */
+    int mSelectionLeftPadding = 0;
+
+    /**
+     * The selection's top padding
+     */
+    int mSelectionTopPadding = 0;
+
+    /**
+     * The selection's right padding
+     */
+    int mSelectionRightPadding = 0;
+
+    /**
+     * The selection's bottom padding
+     */
+    int mSelectionBottomPadding = 0;
+
+    /**
+     * This view's padding
+     */
+    Rect mListPadding = new Rect();
+
+    /**
+     * Subclasses must retain their measure spec from onMeasure() into this member
+     */
+    int mWidthMeasureSpec = 0;
+
+    /**
+     * The top scroll indicator
+     */
+    View mScrollUp;
+
+    /**
+     * The down scroll indicator
+     */
+    View mScrollDown;
+
+    /**
+     * When the view is scrolling, this flag is set to true to indicate subclasses that
+     * the drawing cache was enabled on the children
+     */
+    boolean mCachingStarted;
+    boolean mCachingActive;
+
+    /**
+     * The position of the view that received the down motion event
+     */
+    int mMotionPosition;
+
+    /**
+     * The offset to the top of the mMotionPosition view when the down motion event was received
+     */
+    int mMotionViewOriginalTop;
+
+    /**
+     * The desired offset to the top of the mMotionPosition view after a scroll
+     */
+    int mMotionViewNewTop;
+
+    /**
+     * The X value associated with the the down motion event
+     */
+    int mMotionX;
+
+    /**
+     * The Y value associated with the the down motion event
+     */
+    int mMotionY;
+
+    /**
+     * One of TOUCH_MODE_REST, TOUCH_MODE_DOWN, TOUCH_MODE_TAP, TOUCH_MODE_SCROLL, or
+     * TOUCH_MODE_DONE_WAITING
+     */
+    int mTouchMode = TOUCH_MODE_REST;
+
+    /**
+     * Y value from on the previous motion event (if any)
+     */
+    int mLastY;
+
+    /**
+     * How far the finger moved before we started scrolling
+     */
+    int mMotionCorrection;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * Handles one frame of a fling
+     */
+    private FlingRunnable mFlingRunnable;
+
+    /**
+     * Handles scrolling between positions within the list.
+     */
+    AbsPositionScroller mPositionScroller;
+
+    /**
+     * The offset in pixels form the top of the AdapterView to the top
+     * of the currently selected view. Used to save and restore state.
+     */
+    int mSelectedTop = 0;
+
+    /**
+     * Indicates whether the list is stacked from the bottom edge or
+     * the top edge.
+     */
+    boolean mStackFromBottom;
+
+    /**
+     * When set to true, the list automatically discards the children's
+     * bitmap cache after scrolling.
+     */
+    boolean mScrollingCacheEnabled;
+
+    /**
+     * Whether or not to enable the fast scroll feature on this list
+     */
+    boolean mFastScrollEnabled;
+
+    /**
+     * Whether or not to always show the fast scroll feature on this list
+     */
+    boolean mFastScrollAlwaysVisible;
+
+    /**
+     * Optional callback to notify client when scroll position has changed
+     */
+    private OnScrollListener mOnScrollListener;
+
+    /**
+     * Keeps track of our accessory window
+     */
+    PopupWindow mPopup;
+
+    /**
+     * Used with type filter window
+     */
+    EditText mTextFilter;
+
+    /**
+     * Indicates whether to use pixels-based or position-based scrollbar
+     * properties.
+     */
+    private boolean mSmoothScrollbarEnabled = true;
+
+    /**
+     * Indicates that this view supports filtering
+     */
+    private boolean mTextFilterEnabled;
+
+    /**
+     * Indicates that this view is currently displaying a filtered view of the data
+     */
+    private boolean mFiltered;
+
+    /**
+     * Rectangle used for hit testing children
+     */
+    private Rect mTouchFrame;
+
+    /**
+     * The position to resurrect the selected position to.
+     */
+    int mResurrectToPosition = INVALID_POSITION;
+
+    private ContextMenuInfo mContextMenuInfo = null;
+
+    /**
+     * Maximum distance to record overscroll
+     */
+    int mOverscrollMax;
+
+    /**
+     * Content height divided by this is the overscroll limit.
+     */
+    static final int OVERSCROLL_LIMIT_DIVISOR = 3;
+
+    /**
+     * How many positions in either direction we will search to try to
+     * find a checked item with a stable ID that moved position across
+     * a data set change. If the item isn't found it will be unselected.
+     */
+    private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
+
+    /**
+     * Used to request a layout when we changed touch mode
+     */
+    private static final int TOUCH_MODE_UNKNOWN = -1;
+    private static final int TOUCH_MODE_ON = 0;
+    private static final int TOUCH_MODE_OFF = 1;
+
+    private int mLastTouchMode = TOUCH_MODE_UNKNOWN;
+
+    private static final boolean PROFILE_SCROLLING = false;
+    private boolean mScrollProfilingStarted = false;
+
+    private static final boolean PROFILE_FLINGING = false;
+    private boolean mFlingProfilingStarted = false;
+
+    /**
+     * The StrictMode "critical time span" objects to catch animation
+     * stutters.  Non-null when a time-sensitive animation is
+     * in-flight.  Must call finish() on them when done animating.
+     * These are no-ops on user builds.
+     */
+    private StrictMode.Span mScrollStrictSpan = null;
+    private StrictMode.Span mFlingStrictSpan = null;
+
+    /**
+     * The last CheckForLongPress runnable we posted, if any
+     */
+    private CheckForLongPress mPendingCheckForLongPress;
+
+    /**
+     * The last CheckForTap runnable we posted, if any
+     */
+    private CheckForTap mPendingCheckForTap;
+
+    /**
+     * The last CheckForKeyLongPress runnable we posted, if any
+     */
+    private CheckForKeyLongPress mPendingCheckForKeyLongPress;
+
+    /**
+     * Acts upon click
+     */
+    private AbsListView.PerformClick mPerformClick;
+
+    /**
+     * Delayed action for touch mode.
+     */
+    private Runnable mTouchModeReset;
+
+    /**
+     * Whether the most recent touch event stream resulted in a successful
+     * long-press action. This is reset on TOUCH_DOWN.
+     */
+    private boolean mHasPerformedLongPress;
+
+    /**
+     * This view is in transcript mode -- it shows the bottom of the list when the data
+     * changes
+     */
+    private int mTranscriptMode;
+
+    /**
+     * Indicates that this list is always drawn on top of a solid, single-color, opaque
+     * background
+     */
+    private int mCacheColorHint;
+
+    /**
+     * The select child's view (from the adapter's getView) is enabled.
+     */
+    private boolean mIsChildViewEnabled;
+
+    /**
+     * The cached drawable state for the selector. Accounts for child enabled
+     * state, but otherwise identical to the view's own drawable state.
+     */
+    private int[] mSelectorState;
+
+    /**
+     * The last scroll state reported to clients through {@link OnScrollListener}.
+     */
+    private int mLastScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+    /**
+     * Helper object that renders and controls the fast scroll thumb.
+     */
+    private FastScroller mFastScroll;
+
+    /**
+     * Temporary holder for fast scroller style until a FastScroller object
+     * is created.
+     */
+    private int mFastScrollStyle;
+
+    private boolean mGlobalLayoutListenerAddedFilter;
+
+    private int mTouchSlop;
+    private float mDensityScale;
+
+    private float mVerticalScrollFactor;
+
+    private InputConnection mDefInputConnection;
+    private InputConnectionWrapper mPublicInputConnection;
+
+    private Runnable mClearScrollingCache;
+    Runnable mPositionScrollAfterLayout;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+    private float mVelocityScale = 1.0f;
+
+    final boolean[] mIsScrap = new boolean[1];
+
+    private final int[] mScrollOffset = new int[2];
+    private final int[] mScrollConsumed = new int[2];
+
+    private final float[] mTmpPoint = new float[2];
+
+    // Used for offsetting MotionEvents that we feed to the VelocityTracker.
+    // In the future it would be nice to be able to give this to the VelocityTracker
+    // directly, or alternatively put a VT into absolute-positioning mode that only
+    // reads the raw screen-coordinate x/y values.
+    private int mNestedYOffset = 0;
+
+    // True when the popup should be hidden because of a call to
+    // dispatchDisplayHint()
+    private boolean mPopupHidden;
+
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * Maximum distance to overscroll by during edge effects
+     */
+    int mOverscrollDistance;
+
+    /**
+     * Maximum distance to overfling during edge effects
+     */
+    int mOverflingDistance;
+
+    // These two EdgeGlows are always set and used together.
+    // Checking one for null is as good as checking both.
+
+    /**
+     * Tracks the state of the top edge glow.
+     */
+    private EdgeEffect mEdgeGlowTop;
+
+    /**
+     * Tracks the state of the bottom edge glow.
+     */
+    private EdgeEffect mEdgeGlowBottom;
+
+    /**
+     * An estimate of how many pixels are between the top of the list and
+     * the top of the first position in the adapter, based on the last time
+     * we saw it. Used to hint where to draw edge glows.
+     */
+    private int mFirstPositionDistanceGuess;
+
+    /**
+     * An estimate of how many pixels are between the bottom of the list and
+     * the bottom of the last position in the adapter, based on the last time
+     * we saw it. Used to hint where to draw edge glows.
+     */
+    private int mLastPositionDistanceGuess;
+
+    /**
+     * Used for determining when to cancel out of overscroll.
+     */
+    private int mDirection = 0;
+
+    /**
+     * Tracked on measurement in transcript mode. Makes sure that we can still pin to
+     * the bottom correctly on resizes.
+     */
+    private boolean mForceTranscriptScroll;
+
+    /**
+     * Used for interacting with list items from an accessibility service.
+     */
+    private ListItemAccessibilityDelegate mAccessibilityDelegate;
+
+    private int mLastAccessibilityScrollEventFromIndex;
+    private int mLastAccessibilityScrollEventToIndex;
+
+    /**
+     * Track the item count from the last time we handled a data change.
+     */
+    private int mLastHandledItemCount;
+
+    /**
+     * Used for smooth scrolling at a consistent rate
+     */
+    static final Interpolator sLinearInterpolator = new LinearInterpolator();
+
+    /**
+     * The saved state that we will be restoring from when we next sync.
+     * Kept here so that if we happen to be asked to save our state before
+     * the sync happens, we can return this existing data rather than losing
+     * it.
+     */
+    private SavedState mPendingSync;
+
+    /**
+     * Whether the view is in the process of detaching from its window.
+     */
+    private boolean mIsDetaching;
+
+    /**
+     * Interface definition for a callback to be invoked when the list or grid
+     * has been scrolled.
+     */
+    public interface OnScrollListener {
+
+        /**
+         * The view is not scrolling. Note navigating the list using the trackball counts as
+         * being in the idle state since these transitions are not animated.
+         */
+        public static int SCROLL_STATE_IDLE = 0;
+
+        /**
+         * The user is scrolling using touch, and their finger is still on the screen
+         */
+        public static int SCROLL_STATE_TOUCH_SCROLL = 1;
+
+        /**
+         * The user had previously been scrolling using touch and had performed a fling. The
+         * animation is now coasting to a stop
+         */
+        public static int SCROLL_STATE_FLING = 2;
+
+        /**
+         * Callback method to be invoked while the list view or grid view is being scrolled. If the
+         * view is being scrolled, this method will be called before the next frame of the scroll is
+         * rendered. In particular, it will be called before any calls to
+         * {@link Adapter#getView(int, View, ViewGroup)}.
+         *
+         * @param view The view whose scroll state is being reported
+         *
+         * @param scrollState The current scroll state. One of
+         * {@link #SCROLL_STATE_TOUCH_SCROLL} or {@link #SCROLL_STATE_IDLE}.
+         */
+        public void onScrollStateChanged(AbsListView view, int scrollState);
+
+        /**
+         * Callback method to be invoked when the list or grid has been scrolled. This will be
+         * called after the scroll has completed
+         * @param view The view whose scroll state is being reported
+         * @param firstVisibleItem the index of the first visible cell (ignore if
+         *        visibleItemCount == 0)
+         * @param visibleItemCount the number of visible cells
+         * @param totalItemCount the number of items in the list adaptor
+         */
+        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                int totalItemCount);
+    }
+
+    /**
+     * The top-level view of a list item can implement this interface to allow
+     * itself to modify the bounds of the selection shown for that item.
+     */
+    public interface SelectionBoundsAdjuster {
+        /**
+         * Called to allow the list item to adjust the bounds shown for
+         * its selection.
+         *
+         * @param bounds On call, this contains the bounds the list has
+         * selected for the item (that is the bounds of the entire view).  The
+         * values can be modified as desired.
+         */
+        public void adjustListItemSelectionBounds(Rect bounds);
+    }
+
+    public AbsListView(Context context) {
+        super(context);
+        initAbsListView();
+
+        mOwnerThread = Thread.currentThread();
+
+        setVerticalScrollBarEnabled(true);
+        TypedArray a = context.obtainStyledAttributes(R.styleable.View);
+        initializeScrollbarsInternal(a);
+        a.recycle();
+    }
+
+    public AbsListView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.absListViewStyle);
+    }
+
+    public AbsListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AbsListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initAbsListView();
+
+        mOwnerThread = Thread.currentThread();
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.AbsListView, defStyleAttr, defStyleRes);
+
+        final Drawable selector = a.getDrawable(R.styleable.AbsListView_listSelector);
+        if (selector != null) {
+            setSelector(selector);
+        }
+
+        mDrawSelectorOnTop = a.getBoolean(R.styleable.AbsListView_drawSelectorOnTop, false);
+
+        setStackFromBottom(a.getBoolean(
+                R.styleable.AbsListView_stackFromBottom, false));
+        setScrollingCacheEnabled(a.getBoolean(
+                R.styleable.AbsListView_scrollingCache, true));
+        setTextFilterEnabled(a.getBoolean(
+                R.styleable.AbsListView_textFilterEnabled, false));
+        setTranscriptMode(a.getInt(
+                R.styleable.AbsListView_transcriptMode, TRANSCRIPT_MODE_DISABLED));
+        setCacheColorHint(a.getColor(
+                R.styleable.AbsListView_cacheColorHint, 0));
+        setSmoothScrollbarEnabled(a.getBoolean(
+                R.styleable.AbsListView_smoothScrollbar, true));
+        setChoiceMode(a.getInt(
+                R.styleable.AbsListView_choiceMode, CHOICE_MODE_NONE));
+
+        setFastScrollEnabled(a.getBoolean(
+                R.styleable.AbsListView_fastScrollEnabled, false));
+        setFastScrollStyle(a.getResourceId(
+                R.styleable.AbsListView_fastScrollStyle, 0));
+        setFastScrollAlwaysVisible(a.getBoolean(
+                R.styleable.AbsListView_fastScrollAlwaysVisible, false));
+
+        a.recycle();
+
+        if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+            setRevealOnFocusHint(false);
+        }
+    }
+
+    private void initAbsListView() {
+        // Setting focusable in touch mode will set the focusable property to true
+        setClickable(true);
+        setFocusableInTouchMode(true);
+        setWillNotDraw(false);
+        setAlwaysDrawnWithCacheEnabled(false);
+        setScrollingCacheEnabled(true);
+
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverscrollDistance = configuration.getScaledOverscrollDistance();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+
+        mDensityScale = getContext().getResources().getDisplayMetrics().density;
+    }
+
+    @Override
+    public void setOverScrollMode(int mode) {
+        if (mode != OVER_SCROLL_NEVER) {
+            if (mEdgeGlowTop == null) {
+                Context context = getContext();
+                mEdgeGlowTop = new EdgeEffect(context);
+                mEdgeGlowBottom = new EdgeEffect(context);
+            }
+        } else {
+            mEdgeGlowTop = null;
+            mEdgeGlowBottom = null;
+        }
+        super.setOverScrollMode(mode);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        if (adapter != null) {
+            mAdapterHasStableIds = mAdapter.hasStableIds();
+            if (mChoiceMode != CHOICE_MODE_NONE && mAdapterHasStableIds &&
+                    mCheckedIdStates == null) {
+                mCheckedIdStates = new LongSparseArray<Integer>();
+            }
+        }
+        clearChoices();
+    }
+
+    /**
+     * Returns the number of items currently selected. This will only be valid
+     * if the choice mode is not {@link #CHOICE_MODE_NONE} (default).
+     *
+     * <p>To determine the specific items that are currently selected, use one of
+     * the <code>getChecked*</code> methods.
+     *
+     * @return The number of items currently selected
+     *
+     * @see #getCheckedItemPosition()
+     * @see #getCheckedItemPositions()
+     * @see #getCheckedItemIds()
+     */
+    public int getCheckedItemCount() {
+        return mCheckedItemCount;
+    }
+
+    /**
+     * Returns the checked state of the specified position. The result is only
+     * valid if the choice mode has been set to {@link #CHOICE_MODE_SINGLE}
+     * or {@link #CHOICE_MODE_MULTIPLE}.
+     *
+     * @param position The item whose checked state to return
+     * @return The item's checked state or <code>false</code> if choice mode
+     *         is invalid
+     *
+     * @see #setChoiceMode(int)
+     */
+    public boolean isItemChecked(int position) {
+        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+            return mCheckStates.get(position);
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns the currently checked item. The result is only valid if the choice
+     * mode has been set to {@link #CHOICE_MODE_SINGLE}.
+     *
+     * @return The position of the currently checked item or
+     *         {@link #INVALID_POSITION} if nothing is selected
+     *
+     * @see #setChoiceMode(int)
+     */
+    public int getCheckedItemPosition() {
+        if (mChoiceMode == CHOICE_MODE_SINGLE && mCheckStates != null && mCheckStates.size() == 1) {
+            return mCheckStates.keyAt(0);
+        }
+
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Returns the set of checked items in the list. The result is only valid if
+     * the choice mode has not been set to {@link #CHOICE_MODE_NONE}.
+     *
+     * @return  A SparseBooleanArray which will return true for each call to
+     *          get(int position) where position is a checked position in the
+     *          list and false otherwise, or <code>null</code> if the choice
+     *          mode is set to {@link #CHOICE_MODE_NONE}.
+     */
+    public SparseBooleanArray getCheckedItemPositions() {
+        if (mChoiceMode != CHOICE_MODE_NONE) {
+            return mCheckStates;
+        }
+        return null;
+    }
+
+    /**
+     * Returns the set of checked items ids. The result is only valid if the
+     * choice mode has not been set to {@link #CHOICE_MODE_NONE} and the adapter
+     * has stable IDs. ({@link ListAdapter#hasStableIds()} == {@code true})
+     *
+     * @return A new array which contains the id of each checked item in the
+     *         list.
+     */
+    public long[] getCheckedItemIds() {
+        if (mChoiceMode == CHOICE_MODE_NONE || mCheckedIdStates == null || mAdapter == null) {
+            return new long[0];
+        }
+
+        final LongSparseArray<Integer> idStates = mCheckedIdStates;
+        final int count = idStates.size();
+        final long[] ids = new long[count];
+
+        for (int i = 0; i < count; i++) {
+            ids[i] = idStates.keyAt(i);
+        }
+
+        return ids;
+    }
+
+    /**
+     * Clear any choices previously set
+     */
+    public void clearChoices() {
+        if (mCheckStates != null) {
+            mCheckStates.clear();
+        }
+        if (mCheckedIdStates != null) {
+            mCheckedIdStates.clear();
+        }
+        mCheckedItemCount = 0;
+    }
+
+    /**
+     * Sets the checked state of the specified position. The is only valid if
+     * the choice mode has been set to {@link #CHOICE_MODE_SINGLE} or
+     * {@link #CHOICE_MODE_MULTIPLE}.
+     *
+     * @param position The item whose checked state is to be checked
+     * @param value The new checked state for the item
+     */
+    public void setItemChecked(int position, boolean value) {
+        if (mChoiceMode == CHOICE_MODE_NONE) {
+            return;
+        }
+
+        // Start selection mode if needed. We don't need to if we're unchecking something.
+        if (value && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
+            if (mMultiChoiceModeCallback == null ||
+                    !mMultiChoiceModeCallback.hasWrappedCallback()) {
+                throw new IllegalStateException("AbsListView: attempted to start selection mode " +
+                        "for CHOICE_MODE_MULTIPLE_MODAL but no choice mode callback was " +
+                        "supplied. Call setMultiChoiceModeListener to set a callback.");
+            }
+            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
+        }
+
+        final boolean itemCheckChanged;
+        if (mChoiceMode == CHOICE_MODE_MULTIPLE || mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+            boolean oldValue = mCheckStates.get(position);
+            mCheckStates.put(position, value);
+            if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+                if (value) {
+                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
+                } else {
+                    mCheckedIdStates.delete(mAdapter.getItemId(position));
+                }
+            }
+            itemCheckChanged = oldValue != value;
+            if (itemCheckChanged) {
+                if (value) {
+                    mCheckedItemCount++;
+                } else {
+                    mCheckedItemCount--;
+                }
+            }
+            if (mChoiceActionMode != null) {
+                final long id = mAdapter.getItemId(position);
+                mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+                        position, id, value);
+            }
+        } else {
+            boolean updateIds = mCheckedIdStates != null && mAdapter.hasStableIds();
+            // Clear all values if we're checking something, or unchecking the currently
+            // selected item
+            itemCheckChanged = isItemChecked(position) != value;
+            if (value || isItemChecked(position)) {
+                mCheckStates.clear();
+                if (updateIds) {
+                    mCheckedIdStates.clear();
+                }
+            }
+            // this may end up selecting the value we just cleared but this way
+            // we ensure length of mCheckStates is 1, a fact getCheckedItemPosition relies on
+            if (value) {
+                mCheckStates.put(position, true);
+                if (updateIds) {
+                    mCheckedIdStates.put(mAdapter.getItemId(position), position);
+                }
+                mCheckedItemCount = 1;
+            } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
+                mCheckedItemCount = 0;
+            }
+        }
+
+        // Do not generate a data change while we are in the layout phase or data has not changed
+        if (!mInLayout && !mBlockLayoutRequests && itemCheckChanged) {
+            mDataChanged = true;
+            rememberSyncState();
+            requestLayout();
+        }
+    }
+
+    @Override
+    public boolean performItemClick(View view, int position, long id) {
+        boolean handled = false;
+        boolean dispatchItemClick = true;
+
+        if (mChoiceMode != CHOICE_MODE_NONE) {
+            handled = true;
+            boolean checkedStateChanged = false;
+
+            if (mChoiceMode == CHOICE_MODE_MULTIPLE ||
+                    (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null)) {
+                boolean checked = !mCheckStates.get(position, false);
+                mCheckStates.put(position, checked);
+                if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+                    if (checked) {
+                        mCheckedIdStates.put(mAdapter.getItemId(position), position);
+                    } else {
+                        mCheckedIdStates.delete(mAdapter.getItemId(position));
+                    }
+                }
+                if (checked) {
+                    mCheckedItemCount++;
+                } else {
+                    mCheckedItemCount--;
+                }
+                if (mChoiceActionMode != null) {
+                    mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+                            position, id, checked);
+                    dispatchItemClick = false;
+                }
+                checkedStateChanged = true;
+            } else if (mChoiceMode == CHOICE_MODE_SINGLE) {
+                boolean checked = !mCheckStates.get(position, false);
+                if (checked) {
+                    mCheckStates.clear();
+                    mCheckStates.put(position, true);
+                    if (mCheckedIdStates != null && mAdapter.hasStableIds()) {
+                        mCheckedIdStates.clear();
+                        mCheckedIdStates.put(mAdapter.getItemId(position), position);
+                    }
+                    mCheckedItemCount = 1;
+                } else if (mCheckStates.size() == 0 || !mCheckStates.valueAt(0)) {
+                    mCheckedItemCount = 0;
+                }
+                checkedStateChanged = true;
+            }
+
+            if (checkedStateChanged) {
+                updateOnScreenCheckedViews();
+            }
+        }
+
+        if (dispatchItemClick) {
+            handled |= super.performItemClick(view, position, id);
+        }
+
+        return handled;
+    }
+
+    /**
+     * Perform a quick, in-place update of the checked or activated state
+     * on all visible item views. This should only be called when a valid
+     * choice mode is active.
+     */
+    private void updateOnScreenCheckedViews() {
+        final int firstPos = mFirstPosition;
+        final int count = getChildCount();
+        final boolean useActivated = getContext().getApplicationInfo().targetSdkVersion
+                >= android.os.Build.VERSION_CODES.HONEYCOMB;
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            final int position = firstPos + i;
+
+            if (child instanceof Checkable) {
+                ((Checkable) child).setChecked(mCheckStates.get(position));
+            } else if (useActivated) {
+                child.setActivated(mCheckStates.get(position));
+            }
+        }
+    }
+
+    /**
+     * @see #setChoiceMode(int)
+     *
+     * @return The current choice mode
+     */
+    public int getChoiceMode() {
+        return mChoiceMode;
+    }
+
+    /**
+     * Defines the choice behavior for the List. By default, Lists do not have any choice behavior
+     * ({@link #CHOICE_MODE_NONE}). By setting the choiceMode to {@link #CHOICE_MODE_SINGLE}, the
+     * List allows up to one item to  be in a chosen state. By setting the choiceMode to
+     * {@link #CHOICE_MODE_MULTIPLE}, the list allows any number of items to be chosen.
+     *
+     * @param choiceMode One of {@link #CHOICE_MODE_NONE}, {@link #CHOICE_MODE_SINGLE}, or
+     * {@link #CHOICE_MODE_MULTIPLE}
+     */
+    public void setChoiceMode(int choiceMode) {
+        mChoiceMode = choiceMode;
+        if (mChoiceActionMode != null) {
+            mChoiceActionMode.finish();
+            mChoiceActionMode = null;
+        }
+        if (mChoiceMode != CHOICE_MODE_NONE) {
+            if (mCheckStates == null) {
+                mCheckStates = new SparseBooleanArray(0);
+            }
+            if (mCheckedIdStates == null && mAdapter != null && mAdapter.hasStableIds()) {
+                mCheckedIdStates = new LongSparseArray<Integer>(0);
+            }
+            // Modal multi-choice mode only has choices when the mode is active. Clear them.
+            if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+                clearChoices();
+                setLongClickable(true);
+            }
+        }
+    }
+
+    /**
+     * Set a {@link MultiChoiceModeListener} that will manage the lifecycle of the
+     * selection {@link ActionMode}. Only used when the choice mode is set to
+     * {@link #CHOICE_MODE_MULTIPLE_MODAL}.
+     *
+     * @param listener Listener that will manage the selection mode
+     *
+     * @see #setChoiceMode(int)
+     */
+    public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
+        if (mMultiChoiceModeCallback == null) {
+            mMultiChoiceModeCallback = new MultiChoiceModeWrapper();
+        }
+        mMultiChoiceModeCallback.setWrapped(listener);
+    }
+
+    /**
+     * @return true if all list content currently fits within the view boundaries
+     */
+    private boolean contentFits() {
+        final int childCount = getChildCount();
+        if (childCount == 0) return true;
+        if (childCount != mItemCount) return false;
+
+        return getChildAt(0).getTop() >= mListPadding.top &&
+                getChildAt(childCount - 1).getBottom() <= getHeight() - mListPadding.bottom;
+    }
+
+    /**
+     * Specifies whether fast scrolling is enabled or disabled.
+     * <p>
+     * When fast scrolling is enabled, the user can quickly scroll through lists
+     * by dragging the fast scroll thumb.
+     * <p>
+     * If the adapter backing this list implements {@link SectionIndexer}, the
+     * fast scroller will display section header previews as the user scrolls.
+     * Additionally, the user will be able to quickly jump between sections by
+     * tapping along the length of the scroll bar.
+     *
+     * @see SectionIndexer
+     * @see #isFastScrollEnabled()
+     * @param enabled true to enable fast scrolling, false otherwise
+     */
+    public void setFastScrollEnabled(final boolean enabled) {
+        if (mFastScrollEnabled != enabled) {
+            mFastScrollEnabled = enabled;
+
+            if (isOwnerThread()) {
+                setFastScrollerEnabledUiThread(enabled);
+            } else {
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        setFastScrollerEnabledUiThread(enabled);
+                    }
+                });
+            }
+        }
+    }
+
+    private void setFastScrollerEnabledUiThread(boolean enabled) {
+        if (mFastScroll != null) {
+            mFastScroll.setEnabled(enabled);
+        } else if (enabled) {
+            mFastScroll = new FastScroller(this, mFastScrollStyle);
+            mFastScroll.setEnabled(true);
+        }
+
+        resolvePadding();
+
+        if (mFastScroll != null) {
+            mFastScroll.updateLayout();
+        }
+    }
+
+    /**
+     * Specifies the style of the fast scroller decorations.
+     *
+     * @param styleResId style resource containing fast scroller properties
+     * @see android.R.styleable#FastScroll
+     */
+    public void setFastScrollStyle(int styleResId) {
+        if (mFastScroll == null) {
+            mFastScrollStyle = styleResId;
+        } else {
+            mFastScroll.setStyle(styleResId);
+        }
+    }
+
+    /**
+     * Set whether or not the fast scroller should always be shown in place of
+     * the standard scroll bars. This will enable fast scrolling if it is not
+     * already enabled.
+     * <p>
+     * Fast scrollers shown in this way will not fade out and will be a
+     * permanent fixture within the list. This is best combined with an inset
+     * scroll bar style to ensure the scroll bar does not overlap content.
+     *
+     * @param alwaysShow true if the fast scroller should always be displayed,
+     *            false otherwise
+     * @see #setScrollBarStyle(int)
+     * @see #setFastScrollEnabled(boolean)
+     */
+    public void setFastScrollAlwaysVisible(final boolean alwaysShow) {
+        if (mFastScrollAlwaysVisible != alwaysShow) {
+            if (alwaysShow && !mFastScrollEnabled) {
+                setFastScrollEnabled(true);
+            }
+
+            mFastScrollAlwaysVisible = alwaysShow;
+
+            if (isOwnerThread()) {
+                setFastScrollerAlwaysVisibleUiThread(alwaysShow);
+            } else {
+                post(new Runnable() {
+                    @Override
+                    public void run() {
+                        setFastScrollerAlwaysVisibleUiThread(alwaysShow);
+                    }
+                });
+            }
+        }
+    }
+
+    private void setFastScrollerAlwaysVisibleUiThread(boolean alwaysShow) {
+        if (mFastScroll != null) {
+            mFastScroll.setAlwaysShow(alwaysShow);
+        }
+    }
+
+    /**
+     * @return whether the current thread is the one that created the view
+     */
+    private boolean isOwnerThread() {
+        return mOwnerThread == Thread.currentThread();
+    }
+
+    /**
+     * Returns true if the fast scroller is set to always show on this view.
+     *
+     * @return true if the fast scroller will always show
+     * @see #setFastScrollAlwaysVisible(boolean)
+     */
+    public boolean isFastScrollAlwaysVisible() {
+        if (mFastScroll == null) {
+            return mFastScrollEnabled && mFastScrollAlwaysVisible;
+        } else {
+            return mFastScroll.isEnabled() && mFastScroll.isAlwaysShowEnabled();
+        }
+    }
+
+    @Override
+    public int getVerticalScrollbarWidth() {
+        if (mFastScroll != null && mFastScroll.isEnabled()) {
+            return Math.max(super.getVerticalScrollbarWidth(), mFastScroll.getWidth());
+        }
+        return super.getVerticalScrollbarWidth();
+    }
+
+    /**
+     * Returns true if the fast scroller is enabled.
+     *
+     * @see #setFastScrollEnabled(boolean)
+     * @return true if fast scroll is enabled, false otherwise
+     */
+    @ViewDebug.ExportedProperty
+    public boolean isFastScrollEnabled() {
+        if (mFastScroll == null) {
+            return mFastScrollEnabled;
+        } else {
+            return mFastScroll.isEnabled();
+        }
+    }
+
+    @Override
+    public void setVerticalScrollbarPosition(int position) {
+        super.setVerticalScrollbarPosition(position);
+        if (mFastScroll != null) {
+            mFastScroll.setScrollbarPosition(position);
+        }
+    }
+
+    @Override
+    public void setScrollBarStyle(int style) {
+        super.setScrollBarStyle(style);
+        if (mFastScroll != null) {
+            mFastScroll.setScrollBarStyle(style);
+        }
+    }
+
+    /**
+     * If fast scroll is enabled, then don't draw the vertical scrollbar.
+     * @hide
+     */
+    @Override
+    protected boolean isVerticalScrollBarHidden() {
+        return isFastScrollEnabled();
+    }
+
+    /**
+     * When smooth scrollbar is enabled, the position and size of the scrollbar thumb
+     * is computed based on the number of visible pixels in the visible items. This
+     * however assumes that all list items have the same height. If you use a list in
+     * which items have different heights, the scrollbar will change appearance as the
+     * user scrolls through the list. To avoid this issue, you need to disable this
+     * property.
+     *
+     * When smooth scrollbar is disabled, the position and size of the scrollbar thumb
+     * is based solely on the number of items in the adapter and the position of the
+     * visible items inside the adapter. This provides a stable scrollbar as the user
+     * navigates through a list of items with varying heights.
+     *
+     * @param enabled Whether or not to enable smooth scrollbar.
+     *
+     * @see #setSmoothScrollbarEnabled(boolean)
+     * @attr ref android.R.styleable#AbsListView_smoothScrollbar
+     */
+    public void setSmoothScrollbarEnabled(boolean enabled) {
+        mSmoothScrollbarEnabled = enabled;
+    }
+
+    /**
+     * Returns the current state of the fast scroll feature.
+     *
+     * @return True if smooth scrollbar is enabled is enabled, false otherwise.
+     *
+     * @see #setSmoothScrollbarEnabled(boolean)
+     */
+    @ViewDebug.ExportedProperty
+    public boolean isSmoothScrollbarEnabled() {
+        return mSmoothScrollbarEnabled;
+    }
+
+    /**
+     * Set the listener that will receive notifications every time the list scrolls.
+     *
+     * @param l the scroll listener
+     */
+    public void setOnScrollListener(OnScrollListener l) {
+        mOnScrollListener = l;
+        invokeOnItemScrollListener();
+    }
+
+    /**
+     * Notify our scroll listener (if there is one) of a change in scroll state
+     */
+    void invokeOnItemScrollListener() {
+        if (mFastScroll != null) {
+            mFastScroll.onScroll(mFirstPosition, getChildCount(), mItemCount);
+        }
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScroll(this, mFirstPosition, getChildCount(), mItemCount);
+        }
+        onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
+    }
+
+    /** @hide */
+    @Override
+    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+        // Since this class calls onScrollChanged even if the mFirstPosition and the
+        // child count have not changed we will avoid sending duplicate accessibility
+        // events.
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            final int firstVisiblePosition = getFirstVisiblePosition();
+            final int lastVisiblePosition = getLastVisiblePosition();
+            if (mLastAccessibilityScrollEventFromIndex == firstVisiblePosition
+                    && mLastAccessibilityScrollEventToIndex == lastVisiblePosition) {
+                return;
+            } else {
+                mLastAccessibilityScrollEventFromIndex = firstVisiblePosition;
+                mLastAccessibilityScrollEventToIndex = lastVisiblePosition;
+            }
+        }
+        super.sendAccessibilityEventUnchecked(event);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AbsListView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (isEnabled()) {
+            if (canScrollUp()) {
+                info.addAction(AccessibilityAction.ACTION_SCROLL_BACKWARD);
+                info.addAction(AccessibilityAction.ACTION_SCROLL_UP);
+                info.setScrollable(true);
+            }
+            if (canScrollDown()) {
+                info.addAction(AccessibilityAction.ACTION_SCROLL_FORWARD);
+                info.addAction(AccessibilityAction.ACTION_SCROLL_DOWN);
+                info.setScrollable(true);
+            }
+        }
+
+        info.removeAction(AccessibilityAction.ACTION_CLICK);
+        info.setClickable(false);
+    }
+
+    int getSelectionModeForAccessibility() {
+        final int choiceMode = getChoiceMode();
+        switch (choiceMode) {
+            case CHOICE_MODE_NONE:
+                return CollectionInfo.SELECTION_MODE_NONE;
+            case CHOICE_MODE_SINGLE:
+                return CollectionInfo.SELECTION_MODE_SINGLE;
+            case CHOICE_MODE_MULTIPLE:
+            case CHOICE_MODE_MULTIPLE_MODAL:
+                return CollectionInfo.SELECTION_MODE_MULTIPLE;
+            default:
+                return CollectionInfo.SELECTION_MODE_NONE;
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case R.id.accessibilityActionScrollDown: {
+                if (isEnabled() && canScrollDown()) {
+                    final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+                    smoothScrollBy(viewportHeight, PositionScroller.SCROLL_DURATION);
+                    return true;
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+            case R.id.accessibilityActionScrollUp: {
+                if (isEnabled() && canScrollUp()) {
+                    final int viewportHeight = getHeight() - mListPadding.top - mListPadding.bottom;
+                    smoothScrollBy(-viewportHeight, PositionScroller.SCROLL_DURATION);
+                    return true;
+                }
+            } return false;
+        }
+        return false;
+    }
+
+    /** @hide */
+    @Override
+    public View findViewByAccessibilityIdTraversal(int accessibilityId) {
+        if (accessibilityId == getAccessibilityViewId()) {
+            return this;
+        }
+        return super.findViewByAccessibilityIdTraversal(accessibilityId);
+    }
+
+    /**
+     * Indicates whether the children's drawing cache is used during a scroll.
+     * By default, the drawing cache is enabled but this will consume more memory.
+     *
+     * @return true if the scrolling cache is enabled, false otherwise
+     *
+     * @see #setScrollingCacheEnabled(boolean)
+     * @see View#setDrawingCacheEnabled(boolean)
+     */
+    @ViewDebug.ExportedProperty
+    public boolean isScrollingCacheEnabled() {
+        return mScrollingCacheEnabled;
+    }
+
+    /**
+     * Enables or disables the children's drawing cache during a scroll.
+     * By default, the drawing cache is enabled but this will use more memory.
+     *
+     * When the scrolling cache is enabled, the caches are kept after the
+     * first scrolling. You can manually clear the cache by calling
+     * {@link android.view.ViewGroup#setChildrenDrawingCacheEnabled(boolean)}.
+     *
+     * @param enabled true to enable the scroll cache, false otherwise
+     *
+     * @see #isScrollingCacheEnabled()
+     * @see View#setDrawingCacheEnabled(boolean)
+     */
+    public void setScrollingCacheEnabled(boolean enabled) {
+        if (mScrollingCacheEnabled && !enabled) {
+            clearScrollingCache();
+        }
+        mScrollingCacheEnabled = enabled;
+    }
+
+    /**
+     * Enables or disables the type filter window. If enabled, typing when
+     * this view has focus will filter the children to match the users input.
+     * Note that the {@link Adapter} used by this view must implement the
+     * {@link Filterable} interface.
+     *
+     * @param textFilterEnabled true to enable type filtering, false otherwise
+     *
+     * @see Filterable
+     */
+    public void setTextFilterEnabled(boolean textFilterEnabled) {
+        mTextFilterEnabled = textFilterEnabled;
+    }
+
+    /**
+     * Indicates whether type filtering is enabled for this view
+     *
+     * @return true if type filtering is enabled, false otherwise
+     *
+     * @see #setTextFilterEnabled(boolean)
+     * @see Filterable
+     */
+    @ViewDebug.ExportedProperty
+    public boolean isTextFilterEnabled() {
+        return mTextFilterEnabled;
+    }
+
+    @Override
+    public void getFocusedRect(Rect r) {
+        View view = getSelectedView();
+        if (view != null && view.getParent() == this) {
+            // the focused rectangle of the selected view offset into the
+            // coordinate space of this view.
+            view.getFocusedRect(r);
+            offsetDescendantRectToMyCoords(view, r);
+        } else {
+            // otherwise, just the norm
+            super.getFocusedRect(r);
+        }
+    }
+
+    private void useDefaultSelector() {
+        setSelector(getContext().getDrawable(
+                com.android.internal.R.drawable.list_selector_background));
+    }
+
+    /**
+     * Indicates whether the content of this view is pinned to, or stacked from,
+     * the bottom edge.
+     *
+     * @return true if the content is stacked from the bottom edge, false otherwise
+     */
+    @ViewDebug.ExportedProperty
+    public boolean isStackFromBottom() {
+        return mStackFromBottom;
+    }
+
+    /**
+     * When stack from bottom is set to true, the list fills its content starting from
+     * the bottom of the view.
+     *
+     * @param stackFromBottom true to pin the view's content to the bottom edge,
+     *        false to pin the view's content to the top edge
+     */
+    public void setStackFromBottom(boolean stackFromBottom) {
+        if (mStackFromBottom != stackFromBottom) {
+            mStackFromBottom = stackFromBottom;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    void requestLayoutIfNecessary() {
+        if (getChildCount() > 0) {
+            resetList();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        long selectedId;
+        long firstId;
+        int viewTop;
+        int position;
+        int height;
+        String filter;
+        boolean inActionMode;
+        int checkedItemCount;
+        SparseBooleanArray checkState;
+        LongSparseArray<Integer> checkIdState;
+
+        /**
+         * Constructor called from {@link AbsListView#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            selectedId = in.readLong();
+            firstId = in.readLong();
+            viewTop = in.readInt();
+            position = in.readInt();
+            height = in.readInt();
+            filter = in.readString();
+            inActionMode = in.readByte() != 0;
+            checkedItemCount = in.readInt();
+            checkState = in.readSparseBooleanArray();
+            final int N = in.readInt();
+            if (N > 0) {
+                checkIdState = new LongSparseArray<Integer>();
+                for (int i=0; i<N; i++) {
+                    final long key = in.readLong();
+                    final int value = in.readInt();
+                    checkIdState.put(key, value);
+                }
+            }
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeLong(selectedId);
+            out.writeLong(firstId);
+            out.writeInt(viewTop);
+            out.writeInt(position);
+            out.writeInt(height);
+            out.writeString(filter);
+            out.writeByte((byte) (inActionMode ? 1 : 0));
+            out.writeInt(checkedItemCount);
+            out.writeSparseBooleanArray(checkState);
+            final int N = checkIdState != null ? checkIdState.size() : 0;
+            out.writeInt(N);
+            for (int i=0; i<N; i++) {
+                out.writeLong(checkIdState.keyAt(i));
+                out.writeInt(checkIdState.valueAt(i));
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "AbsListView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " selectedId=" + selectedId
+                    + " firstId=" + firstId
+                    + " viewTop=" + viewTop
+                    + " position=" + position
+                    + " height=" + height
+                    + " filter=" + filter
+                    + " checkState=" + checkState + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        /*
+         * This doesn't really make sense as the place to dismiss the
+         * popups, but there don't seem to be any other useful hooks
+         * that happen early enough to keep from getting complaints
+         * about having leaked the window.
+         */
+        dismissPopup();
+
+        Parcelable superState = super.onSaveInstanceState();
+
+        SavedState ss = new SavedState(superState);
+
+        if (mPendingSync != null) {
+            // Just keep what we last restored.
+            ss.selectedId = mPendingSync.selectedId;
+            ss.firstId = mPendingSync.firstId;
+            ss.viewTop = mPendingSync.viewTop;
+            ss.position = mPendingSync.position;
+            ss.height = mPendingSync.height;
+            ss.filter = mPendingSync.filter;
+            ss.inActionMode = mPendingSync.inActionMode;
+            ss.checkedItemCount = mPendingSync.checkedItemCount;
+            ss.checkState = mPendingSync.checkState;
+            ss.checkIdState = mPendingSync.checkIdState;
+            return ss;
+        }
+
+        boolean haveChildren = getChildCount() > 0 && mItemCount > 0;
+        long selectedId = getSelectedItemId();
+        ss.selectedId = selectedId;
+        ss.height = getHeight();
+
+        if (selectedId >= 0) {
+            // Remember the selection
+            ss.viewTop = mSelectedTop;
+            ss.position = getSelectedItemPosition();
+            ss.firstId = INVALID_POSITION;
+        } else {
+            if (haveChildren && mFirstPosition > 0) {
+                // Remember the position of the first child.
+                // We only do this if we are not currently at the top of
+                // the list, for two reasons:
+                // (1) The list may be in the process of becoming empty, in
+                // which case mItemCount may not be 0, but if we try to
+                // ask for any information about position 0 we will crash.
+                // (2) Being "at the top" seems like a special case, anyway,
+                // and the user wouldn't expect to end up somewhere else when
+                // they revisit the list even if its content has changed.
+                View v = getChildAt(0);
+                ss.viewTop = v.getTop();
+                int firstPos = mFirstPosition;
+                if (firstPos >= mItemCount) {
+                    firstPos = mItemCount - 1;
+                }
+                ss.position = firstPos;
+                ss.firstId = mAdapter.getItemId(firstPos);
+            } else {
+                ss.viewTop = 0;
+                ss.firstId = INVALID_POSITION;
+                ss.position = 0;
+            }
+        }
+
+        ss.filter = null;
+        if (mFiltered) {
+            final EditText textFilter = mTextFilter;
+            if (textFilter != null) {
+                Editable filterText = textFilter.getText();
+                if (filterText != null) {
+                    ss.filter = filterText.toString();
+                }
+            }
+        }
+
+        ss.inActionMode = mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode != null;
+
+        if (mCheckStates != null) {
+            ss.checkState = mCheckStates.clone();
+        }
+        if (mCheckedIdStates != null) {
+            final LongSparseArray<Integer> idState = new LongSparseArray<Integer>();
+            final int count = mCheckedIdStates.size();
+            for (int i = 0; i < count; i++) {
+                idState.put(mCheckedIdStates.keyAt(i), mCheckedIdStates.valueAt(i));
+            }
+            ss.checkIdState = idState;
+        }
+        ss.checkedItemCount = mCheckedItemCount;
+
+        if (mRemoteAdapter != null) {
+            mRemoteAdapter.saveRemoteViewsCache();
+        }
+
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+        mDataChanged = true;
+
+        mSyncHeight = ss.height;
+
+        if (ss.selectedId >= 0) {
+            mNeedSync = true;
+            mPendingSync = ss;
+            mSyncRowId = ss.selectedId;
+            mSyncPosition = ss.position;
+            mSpecificTop = ss.viewTop;
+            mSyncMode = SYNC_SELECTED_POSITION;
+        } else if (ss.firstId >= 0) {
+            setSelectedPositionInt(INVALID_POSITION);
+            // Do this before setting mNeedSync since setNextSelectedPosition looks at mNeedSync
+            setNextSelectedPositionInt(INVALID_POSITION);
+            mSelectorPosition = INVALID_POSITION;
+            mNeedSync = true;
+            mPendingSync = ss;
+            mSyncRowId = ss.firstId;
+            mSyncPosition = ss.position;
+            mSpecificTop = ss.viewTop;
+            mSyncMode = SYNC_FIRST_POSITION;
+        }
+
+        setFilterText(ss.filter);
+
+        if (ss.checkState != null) {
+            mCheckStates = ss.checkState;
+        }
+
+        if (ss.checkIdState != null) {
+            mCheckedIdStates = ss.checkIdState;
+        }
+
+        mCheckedItemCount = ss.checkedItemCount;
+
+        if (ss.inActionMode && mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL &&
+                mMultiChoiceModeCallback != null) {
+            mChoiceActionMode = startActionMode(mMultiChoiceModeCallback);
+        }
+
+        requestLayout();
+    }
+
+    private boolean acceptFilter() {
+        return mTextFilterEnabled && getAdapter() instanceof Filterable &&
+                ((Filterable) getAdapter()).getFilter() != null;
+    }
+
+    /**
+     * Sets the initial value for the text filter.
+     * @param filterText The text to use for the filter.
+     *
+     * @see #setTextFilterEnabled
+     */
+    public void setFilterText(String filterText) {
+        // TODO: Should we check for acceptFilter()?
+        if (mTextFilterEnabled && !TextUtils.isEmpty(filterText)) {
+            createTextFilter(false);
+            // This is going to call our listener onTextChanged, but we might not
+            // be ready to bring up a window yet
+            mTextFilter.setText(filterText);
+            mTextFilter.setSelection(filterText.length());
+            if (mAdapter instanceof Filterable) {
+                // if mPopup is non-null, then onTextChanged will do the filtering
+                if (mPopup == null) {
+                    Filter f = ((Filterable) mAdapter).getFilter();
+                    f.filter(filterText);
+                }
+                // Set filtered to true so we will display the filter window when our main
+                // window is ready
+                mFiltered = true;
+                mDataSetObserver.clearSavedState();
+            }
+        }
+    }
+
+    /**
+     * Returns the list's text filter, if available.
+     * @return the list's text filter or null if filtering isn't enabled
+     */
+    public CharSequence getTextFilter() {
+        if (mTextFilterEnabled && mTextFilter != null) {
+            return mTextFilter.getText();
+        }
+        return null;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+        if (gainFocus && mSelectedPosition < 0 && !isInTouchMode()) {
+            if (!isAttachedToWindow() && mAdapter != null) {
+                // Data may have changed while we were detached and it's valid
+                // to change focus while detached. Refresh so we don't die.
+                mDataChanged = true;
+                mOldItemCount = mItemCount;
+                mItemCount = mAdapter.getCount();
+            }
+            resurrectSelection();
+        }
+    }
+
+    @Override
+    public void requestLayout() {
+        if (!mBlockLayoutRequests && !mInLayout) {
+            super.requestLayout();
+        }
+    }
+
+    /**
+     * The list is empty. Clear everything out.
+     */
+    void resetList() {
+        removeAllViewsInLayout();
+        mFirstPosition = 0;
+        mDataChanged = false;
+        mPositionScrollAfterLayout = null;
+        mNeedSync = false;
+        mPendingSync = null;
+        mOldSelectedPosition = INVALID_POSITION;
+        mOldSelectedRowId = INVALID_ROW_ID;
+        setSelectedPositionInt(INVALID_POSITION);
+        setNextSelectedPositionInt(INVALID_POSITION);
+        mSelectedTop = 0;
+        mSelectorPosition = INVALID_POSITION;
+        mSelectorRect.setEmpty();
+        invalidate();
+    }
+
+    @Override
+    protected int computeVerticalScrollExtent() {
+        final int count = getChildCount();
+        if (count > 0) {
+            if (mSmoothScrollbarEnabled) {
+                int extent = count * 100;
+
+                View view = getChildAt(0);
+                final int top = view.getTop();
+                int height = view.getHeight();
+                if (height > 0) {
+                    extent += (top * 100) / height;
+                }
+
+                view = getChildAt(count - 1);
+                final int bottom = view.getBottom();
+                height = view.getHeight();
+                if (height > 0) {
+                    extent -= ((bottom - getHeight()) * 100) / height;
+                }
+
+                return extent;
+            } else {
+                return 1;
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        final int firstPosition = mFirstPosition;
+        final int childCount = getChildCount();
+        if (firstPosition >= 0 && childCount > 0) {
+            if (mSmoothScrollbarEnabled) {
+                final View view = getChildAt(0);
+                final int top = view.getTop();
+                int height = view.getHeight();
+                if (height > 0) {
+                    return Math.max(firstPosition * 100 - (top * 100) / height +
+                            (int)((float)mScrollY / getHeight() * mItemCount * 100), 0);
+                }
+            } else {
+                int index;
+                final int count = mItemCount;
+                if (firstPosition == 0) {
+                    index = 0;
+                } else if (firstPosition + childCount == count) {
+                    index = count;
+                } else {
+                    index = firstPosition + childCount / 2;
+                }
+                return (int) (firstPosition + childCount * (index / (float) count));
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    protected int computeVerticalScrollRange() {
+        int result;
+        if (mSmoothScrollbarEnabled) {
+            result = Math.max(mItemCount * 100, 0);
+            if (mScrollY != 0) {
+                // Compensate for overscroll
+                result += Math.abs((int) ((float) mScrollY / getHeight() * mItemCount * 100));
+            }
+        } else {
+            result = mItemCount;
+        }
+        return result;
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        final int count = getChildCount();
+        final float fadeEdge = super.getTopFadingEdgeStrength();
+        if (count == 0) {
+            return fadeEdge;
+        } else {
+            if (mFirstPosition > 0) {
+                return 1.0f;
+            }
+
+            final int top = getChildAt(0).getTop();
+            final float fadeLength = getVerticalFadingEdgeLength();
+            return top < mPaddingTop ? -(top - mPaddingTop) / fadeLength : fadeEdge;
+        }
+    }
+
+    @Override
+    protected float getBottomFadingEdgeStrength() {
+        final int count = getChildCount();
+        final float fadeEdge = super.getBottomFadingEdgeStrength();
+        if (count == 0) {
+            return fadeEdge;
+        } else {
+            if (mFirstPosition + count - 1 < mItemCount - 1) {
+                return 1.0f;
+            }
+
+            final int bottom = getChildAt(count - 1).getBottom();
+            final int height = getHeight();
+            final float fadeLength = getVerticalFadingEdgeLength();
+            return bottom > height - mPaddingBottom ?
+                    (bottom - height + mPaddingBottom) / fadeLength : fadeEdge;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mSelector == null) {
+            useDefaultSelector();
+        }
+        final Rect listPadding = mListPadding;
+        listPadding.left = mSelectionLeftPadding + mPaddingLeft;
+        listPadding.top = mSelectionTopPadding + mPaddingTop;
+        listPadding.right = mSelectionRightPadding + mPaddingRight;
+        listPadding.bottom = mSelectionBottomPadding + mPaddingBottom;
+
+        // Check if our previous measured size was at a point where we should scroll later.
+        if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
+            final int childCount = getChildCount();
+            final int listBottom = getHeight() - getPaddingBottom();
+            final View lastChild = getChildAt(childCount - 1);
+            final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
+            mForceTranscriptScroll = mFirstPosition + childCount >= mLastHandledItemCount &&
+                    lastBottom <= listBottom;
+        }
+    }
+
+    /**
+     * Subclasses should NOT override this method but
+     *  {@link #layoutChildren()} instead.
+     */
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+
+        mInLayout = true;
+
+        final int childCount = getChildCount();
+        if (changed) {
+            for (int i = 0; i < childCount; i++) {
+                getChildAt(i).forceLayout();
+            }
+            mRecycler.markChildrenDirty();
+        }
+
+        layoutChildren();
+
+        mOverscrollMax = (b - t) / OVERSCROLL_LIMIT_DIVISOR;
+
+        // TODO: Move somewhere sane. This doesn't belong in onLayout().
+        if (mFastScroll != null) {
+            mFastScroll.onItemCountChanged(getChildCount(), mItemCount);
+        }
+        mInLayout = false;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected boolean setFrame(int left, int top, int right, int bottom) {
+        final boolean changed = super.setFrame(left, top, right, bottom);
+
+        if (changed) {
+            // Reposition the popup when the frame has changed. This includes
+            // translating the widget, not just changing its dimension. The
+            // filter popup needs to follow the widget.
+            final boolean visible = getWindowVisibility() == View.VISIBLE;
+            if (mFiltered && visible && mPopup != null && mPopup.isShowing()) {
+                positionPopup();
+            }
+        }
+
+        return changed;
+    }
+
+    /**
+     * Subclasses must override this method to layout their children.
+     */
+    protected void layoutChildren() {
+    }
+
+    /**
+     * @param focusedView view that holds accessibility focus
+     * @return direct child that contains accessibility focus, or null if no
+     *         child contains accessibility focus
+     */
+    View getAccessibilityFocusedChild(View focusedView) {
+        ViewParent viewParent = focusedView.getParent();
+        while ((viewParent instanceof View) && (viewParent != this)) {
+            focusedView = (View) viewParent;
+            viewParent = viewParent.getParent();
+        }
+
+        if (!(viewParent instanceof View)) {
+            return null;
+        }
+
+        return focusedView;
+    }
+
+    void updateScrollIndicators() {
+        if (mScrollUp != null) {
+            mScrollUp.setVisibility(canScrollUp() ? View.VISIBLE : View.INVISIBLE);
+        }
+
+        if (mScrollDown != null) {
+            mScrollDown.setVisibility(canScrollDown() ? View.VISIBLE : View.INVISIBLE);
+        }
+    }
+
+    private boolean canScrollUp() {
+        boolean canScrollUp;
+        // 0th element is not visible
+        canScrollUp = mFirstPosition > 0;
+
+        // ... Or top of 0th element is not visible
+        if (!canScrollUp) {
+            if (getChildCount() > 0) {
+                View child = getChildAt(0);
+                canScrollUp = child.getTop() < mListPadding.top;
+            }
+        }
+
+        return canScrollUp;
+    }
+
+    private boolean canScrollDown() {
+        boolean canScrollDown;
+        int count = getChildCount();
+
+        // Last item is not visible
+        canScrollDown = (mFirstPosition + count) < mItemCount;
+
+        // ... Or bottom of the last element is not visible
+        if (!canScrollDown && count > 0) {
+            View child = getChildAt(count - 1);
+            canScrollDown = child.getBottom() > mBottom - mListPadding.bottom;
+        }
+
+        return canScrollDown;
+    }
+
+    @Override
+    @ViewDebug.ExportedProperty
+    public View getSelectedView() {
+        if (mItemCount > 0 && mSelectedPosition >= 0) {
+            return getChildAt(mSelectedPosition - mFirstPosition);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * List padding is the maximum of the normal view's padding and the padding of the selector.
+     *
+     * @see android.view.View#getPaddingTop()
+     * @see #getSelector()
+     *
+     * @return The top list padding.
+     */
+    public int getListPaddingTop() {
+        return mListPadding.top;
+    }
+
+    /**
+     * List padding is the maximum of the normal view's padding and the padding of the selector.
+     *
+     * @see android.view.View#getPaddingBottom()
+     * @see #getSelector()
+     *
+     * @return The bottom list padding.
+     */
+    public int getListPaddingBottom() {
+        return mListPadding.bottom;
+    }
+
+    /**
+     * List padding is the maximum of the normal view's padding and the padding of the selector.
+     *
+     * @see android.view.View#getPaddingLeft()
+     * @see #getSelector()
+     *
+     * @return The left list padding.
+     */
+    public int getListPaddingLeft() {
+        return mListPadding.left;
+    }
+
+    /**
+     * List padding is the maximum of the normal view's padding and the padding of the selector.
+     *
+     * @see android.view.View#getPaddingRight()
+     * @see #getSelector()
+     *
+     * @return The right list padding.
+     */
+    public int getListPaddingRight() {
+        return mListPadding.right;
+    }
+
+    /**
+     * Gets a view and have it show the data associated with the specified
+     * position. This is called when we have already discovered that the view
+     * is not available for reuse in the recycle bin. The only choices left are
+     * converting an old view or making a new one.
+     *
+     * @param position the position to display
+     * @param outMetadata an array of at least 1 boolean where the first entry
+     *                    will be set {@code true} if the view is currently
+     *                    attached to the window, {@code false} otherwise (e.g.
+     *                    newly-inflated or remained scrap for multiple layout
+     *                    passes)
+     *
+     * @return A view displaying the data associated with the specified position
+     */
+    View obtainView(int position, boolean[] outMetadata) {
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "obtainView");
+
+        outMetadata[0] = false;
+
+        // Check whether we have a transient state view. Attempt to re-bind the
+        // data and discard the view if we fail.
+        final View transientView = mRecycler.getTransientStateView(position);
+        if (transientView != null) {
+            final LayoutParams params = (LayoutParams) transientView.getLayoutParams();
+
+            // If the view type hasn't changed, attempt to re-bind the data.
+            if (params.viewType == mAdapter.getItemViewType(position)) {
+                final View updatedView = mAdapter.getView(position, transientView, this);
+
+                // If we failed to re-bind the data, scrap the obtained view.
+                if (updatedView != transientView) {
+                    setItemViewLayoutParams(updatedView, position);
+                    mRecycler.addScrapView(updatedView, position);
+                }
+            }
+
+            outMetadata[0] = true;
+
+            // Finish the temporary detach started in addScrapView().
+            transientView.dispatchFinishTemporaryDetach();
+            return transientView;
+        }
+
+        final View scrapView = mRecycler.getScrapView(position);
+        final View child = mAdapter.getView(position, scrapView, this);
+        if (scrapView != null) {
+            if (child != scrapView) {
+                // Failed to re-bind the data, return scrap to the heap.
+                mRecycler.addScrapView(scrapView, position);
+            } else if (child.isTemporarilyDetached()) {
+                outMetadata[0] = true;
+
+                // Finish the temporary detach started in addScrapView().
+                child.dispatchFinishTemporaryDetach();
+            }
+        }
+
+        if (mCacheColorHint != 0) {
+            child.setDrawingCacheBackgroundColor(mCacheColorHint);
+        }
+
+        if (child.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            child.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        setItemViewLayoutParams(child, position);
+
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            if (mAccessibilityDelegate == null) {
+                mAccessibilityDelegate = new ListItemAccessibilityDelegate();
+            }
+            if (child.getAccessibilityDelegate() == null) {
+                child.setAccessibilityDelegate(mAccessibilityDelegate);
+            }
+        }
+
+        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+
+        return child;
+    }
+
+    private void setItemViewLayoutParams(View child, int position) {
+        final ViewGroup.LayoutParams vlp = child.getLayoutParams();
+        LayoutParams lp;
+        if (vlp == null) {
+            lp = (LayoutParams) generateDefaultLayoutParams();
+        } else if (!checkLayoutParams(vlp)) {
+            lp = (LayoutParams) generateLayoutParams(vlp);
+        } else {
+            lp = (LayoutParams) vlp;
+        }
+
+        if (mAdapterHasStableIds) {
+            lp.itemId = mAdapter.getItemId(position);
+        }
+        lp.viewType = mAdapter.getItemViewType(position);
+        lp.isEnabled = mAdapter.isEnabled(position);
+        if (lp != vlp) {
+          child.setLayoutParams(lp);
+        }
+    }
+
+    class ListItemAccessibilityDelegate extends AccessibilityDelegate {
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+
+            final int position = getPositionForView(host);
+            onInitializeAccessibilityNodeInfoForItem(host, position, info);
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+            if (super.performAccessibilityAction(host, action, arguments)) {
+                return true;
+            }
+
+            final int position = getPositionForView(host);
+            if (position == INVALID_POSITION || mAdapter == null) {
+                // Cannot perform actions on invalid items.
+                return false;
+            }
+
+            if (position >= mAdapter.getCount()) {
+                // The position is no longer valid, likely due to a data set
+                // change. We could fail here for all data set changes, since
+                // there is a chance that the data bound to the view may no
+                // longer exist at the same position within the adapter, but
+                // it's more consistent with the standard touch interaction to
+                // click at whatever may have moved into that position.
+                return false;
+            }
+
+            final boolean isItemEnabled;
+            final ViewGroup.LayoutParams lp = host.getLayoutParams();
+            if (lp instanceof AbsListView.LayoutParams) {
+                isItemEnabled = ((AbsListView.LayoutParams) lp).isEnabled;
+            } else {
+                isItemEnabled = false;
+            }
+
+            if (!isEnabled() || !isItemEnabled) {
+                // Cannot perform actions on disabled items.
+                return false;
+            }
+
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_CLEAR_SELECTION: {
+                    if (getSelectedItemPosition() == position) {
+                        setSelection(INVALID_POSITION);
+                        return true;
+                    }
+                } return false;
+                case AccessibilityNodeInfo.ACTION_SELECT: {
+                    if (getSelectedItemPosition() != position) {
+                        setSelection(position);
+                        return true;
+                    }
+                } return false;
+                case AccessibilityNodeInfo.ACTION_CLICK: {
+                    if (isItemClickable(host)) {
+                        final long id = getItemIdAtPosition(position);
+                        return performItemClick(host, position, id);
+                    }
+                } return false;
+                case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
+                    if (isLongClickable()) {
+                        final long id = getItemIdAtPosition(position);
+                        return performLongPress(host, position, id);
+                    }
+                } return false;
+            }
+
+            return false;
+        }
+    }
+
+    /**
+     * Initializes an {@link AccessibilityNodeInfo} with information about a
+     * particular item in the list.
+     *
+     * @param view View representing the list item.
+     * @param position Position of the list item within the adapter.
+     * @param info Node info to populate.
+     */
+    public void onInitializeAccessibilityNodeInfoForItem(
+            View view, int position, AccessibilityNodeInfo info) {
+        if (position == INVALID_POSITION) {
+            // The item doesn't exist, so there's not much we can do here.
+            return;
+        }
+
+        final boolean isItemEnabled;
+        final ViewGroup.LayoutParams lp = view.getLayoutParams();
+        if (lp instanceof AbsListView.LayoutParams) {
+            isItemEnabled = ((AbsListView.LayoutParams) lp).isEnabled;
+        } else {
+            isItemEnabled = false;
+        }
+
+        if (!isEnabled() || !isItemEnabled) {
+            info.setEnabled(false);
+            return;
+        }
+
+        if (position == getSelectedItemPosition()) {
+            info.setSelected(true);
+            info.addAction(AccessibilityAction.ACTION_CLEAR_SELECTION);
+        } else {
+            info.addAction(AccessibilityAction.ACTION_SELECT);
+        }
+
+        if (isItemClickable(view)) {
+            info.addAction(AccessibilityAction.ACTION_CLICK);
+            info.setClickable(true);
+        }
+
+        if (isLongClickable()) {
+            info.addAction(AccessibilityAction.ACTION_LONG_CLICK);
+            info.setLongClickable(true);
+        }
+    }
+
+    private boolean isItemClickable(View view) {
+        return !view.hasExplicitFocusable();
+    }
+
+    /**
+     * Positions the selector in a way that mimics touch.
+     */
+    void positionSelectorLikeTouch(int position, View sel, float x, float y) {
+        positionSelector(position, sel, true, x, y);
+    }
+
+    /**
+     * Positions the selector in a way that mimics keyboard focus.
+     */
+    void positionSelectorLikeFocus(int position, View sel) {
+        if (mSelector != null && mSelectorPosition != position && position != INVALID_POSITION) {
+            final Rect bounds = mSelectorRect;
+            final float x = bounds.exactCenterX();
+            final float y = bounds.exactCenterY();
+            positionSelector(position, sel, true, x, y);
+        } else {
+            positionSelector(position, sel);
+        }
+    }
+
+    void positionSelector(int position, View sel) {
+        positionSelector(position, sel, false, -1, -1);
+    }
+
+    private void positionSelector(int position, View sel, boolean manageHotspot, float x, float y) {
+        final boolean positionChanged = position != mSelectorPosition;
+        if (position != INVALID_POSITION) {
+            mSelectorPosition = position;
+        }
+
+        final Rect selectorRect = mSelectorRect;
+        selectorRect.set(sel.getLeft(), sel.getTop(), sel.getRight(), sel.getBottom());
+        if (sel instanceof SelectionBoundsAdjuster) {
+            ((SelectionBoundsAdjuster)sel).adjustListItemSelectionBounds(selectorRect);
+        }
+
+        // Adjust for selection padding.
+        selectorRect.left -= mSelectionLeftPadding;
+        selectorRect.top -= mSelectionTopPadding;
+        selectorRect.right += mSelectionRightPadding;
+        selectorRect.bottom += mSelectionBottomPadding;
+
+        // Update the child enabled state prior to updating the selector.
+        final boolean isChildViewEnabled = sel.isEnabled();
+        if (mIsChildViewEnabled != isChildViewEnabled) {
+            mIsChildViewEnabled = isChildViewEnabled;
+        }
+
+        // Update the selector drawable's state and position.
+        final Drawable selector = mSelector;
+        if (selector != null) {
+            if (positionChanged) {
+                // Wipe out the current selector state so that we can start
+                // over in the new position with a fresh state.
+                selector.setVisible(false, false);
+                selector.setState(StateSet.NOTHING);
+            }
+            selector.setBounds(selectorRect);
+            if (positionChanged) {
+                if (getVisibility() == VISIBLE) {
+                    selector.setVisible(true, false);
+                }
+                updateSelectorState();
+            }
+            if (manageHotspot) {
+                selector.setHotspot(x, y);
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        int saveCount = 0;
+        final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+        if (clipToPadding) {
+            saveCount = canvas.save();
+            final int scrollX = mScrollX;
+            final int scrollY = mScrollY;
+            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+                    scrollX + mRight - mLeft - mPaddingRight,
+                    scrollY + mBottom - mTop - mPaddingBottom);
+            mGroupFlags &= ~CLIP_TO_PADDING_MASK;
+        }
+
+        final boolean drawSelectorOnTop = mDrawSelectorOnTop;
+        if (!drawSelectorOnTop) {
+            drawSelector(canvas);
+        }
+
+        super.dispatchDraw(canvas);
+
+        if (drawSelectorOnTop) {
+            drawSelector(canvas);
+        }
+
+        if (clipToPadding) {
+            canvas.restoreToCount(saveCount);
+            mGroupFlags |= CLIP_TO_PADDING_MASK;
+        }
+    }
+
+    @Override
+    protected boolean isPaddingOffsetRequired() {
+        return (mGroupFlags & CLIP_TO_PADDING_MASK) != CLIP_TO_PADDING_MASK;
+    }
+
+    @Override
+    protected int getLeftPaddingOffset() {
+        return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingLeft;
+    }
+
+    @Override
+    protected int getTopPaddingOffset() {
+        return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : -mPaddingTop;
+    }
+
+    @Override
+    protected int getRightPaddingOffset() {
+        return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingRight;
+    }
+
+    @Override
+    protected int getBottomPaddingOffset() {
+        return (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK ? 0 : mPaddingBottom;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected void internalSetPadding(int left, int top, int right, int bottom) {
+        super.internalSetPadding(left, top, right, bottom);
+        if (isLayoutRequested()) {
+            handleBoundsChange();
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        handleBoundsChange();
+        if (mFastScroll != null) {
+            mFastScroll.onSizeChanged(w, h, oldw, oldh);
+        }
+    }
+
+    /**
+     * Called when bounds of the AbsListView are changed. AbsListView marks data set as changed
+     * and force layouts all children that don't have exact measure specs.
+     * <p>
+     * This invalidation is necessary, otherwise, AbsListView may think the children are valid and
+     * fail to relayout them properly to accommodate for new bounds.
+     */
+    void handleBoundsChange() {
+        if (mInLayout) {
+            return;
+        }
+        final int childCount = getChildCount();
+        if (childCount > 0) {
+            mDataChanged = true;
+            rememberSyncState();
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final ViewGroup.LayoutParams lp = child.getLayoutParams();
+                // force layout child unless it has exact specs
+                if (lp == null || lp.width < 1 || lp.height < 1) {
+                    child.forceLayout();
+                }
+            }
+        }
+    }
+
+    /**
+     * @return True if the current touch mode requires that we draw the selector in the pressed
+     *         state.
+     */
+    boolean touchModeDrawsInPressedState() {
+        // FIXME use isPressed for this
+        switch (mTouchMode) {
+        case TOUCH_MODE_TAP:
+        case TOUCH_MODE_DONE_WAITING:
+            return true;
+        default:
+            return false;
+        }
+    }
+
+    /**
+     * Indicates whether this view is in a state where the selector should be drawn. This will
+     * happen if we have focus but are not in touch mode, or we are in the middle of displaying
+     * the pressed state for an item.
+     *
+     * @return True if the selector should be shown
+     */
+    boolean shouldShowSelector() {
+        return (isFocused() && !isInTouchMode()) || (touchModeDrawsInPressedState() && isPressed());
+    }
+
+    private void drawSelector(Canvas canvas) {
+        if (!mSelectorRect.isEmpty()) {
+            final Drawable selector = mSelector;
+            selector.setBounds(mSelectorRect);
+            selector.draw(canvas);
+        }
+    }
+
+    /**
+     * Controls whether the selection highlight drawable should be drawn on top of the item or
+     * behind it.
+     *
+     * @param onTop If true, the selector will be drawn on the item it is highlighting. The default
+     *        is false.
+     *
+     * @attr ref android.R.styleable#AbsListView_drawSelectorOnTop
+     */
+    public void setDrawSelectorOnTop(boolean onTop) {
+        mDrawSelectorOnTop = onTop;
+    }
+
+    /**
+     * Set a Drawable that should be used to highlight the currently selected item.
+     *
+     * @param resID A Drawable resource to use as the selection highlight.
+     *
+     * @attr ref android.R.styleable#AbsListView_listSelector
+     */
+    public void setSelector(@DrawableRes int resID) {
+        setSelector(getContext().getDrawable(resID));
+    }
+
+    public void setSelector(Drawable sel) {
+        if (mSelector != null) {
+            mSelector.setCallback(null);
+            unscheduleDrawable(mSelector);
+        }
+        mSelector = sel;
+        Rect padding = new Rect();
+        sel.getPadding(padding);
+        mSelectionLeftPadding = padding.left;
+        mSelectionTopPadding = padding.top;
+        mSelectionRightPadding = padding.right;
+        mSelectionBottomPadding = padding.bottom;
+        sel.setCallback(this);
+        updateSelectorState();
+    }
+
+    /**
+     * Returns the selector {@link android.graphics.drawable.Drawable} that is used to draw the
+     * selection in the list.
+     *
+     * @return the drawable used to display the selector
+     */
+    public Drawable getSelector() {
+        return mSelector;
+    }
+
+    /**
+     * Sets the selector state to "pressed" and posts a CheckForKeyLongPress to see if
+     * this is a long press.
+     */
+    void keyPressed() {
+        if (!isEnabled() || !isClickable()) {
+            return;
+        }
+
+        Drawable selector = mSelector;
+        Rect selectorRect = mSelectorRect;
+        if (selector != null && (isFocused() || touchModeDrawsInPressedState())
+                && !selectorRect.isEmpty()) {
+
+            final View v = getChildAt(mSelectedPosition - mFirstPosition);
+
+            if (v != null) {
+                if (v.hasExplicitFocusable()) return;
+                v.setPressed(true);
+            }
+            setPressed(true);
+
+            final boolean longClickable = isLongClickable();
+            Drawable d = selector.getCurrent();
+            if (d != null && d instanceof TransitionDrawable) {
+                if (longClickable) {
+                    ((TransitionDrawable) d).startTransition(
+                            ViewConfiguration.getLongPressTimeout());
+                } else {
+                    ((TransitionDrawable) d).resetTransition();
+                }
+            }
+            if (longClickable && !mDataChanged) {
+                if (mPendingCheckForKeyLongPress == null) {
+                    mPendingCheckForKeyLongPress = new CheckForKeyLongPress();
+                }
+                mPendingCheckForKeyLongPress.rememberWindowAttachCount();
+                postDelayed(mPendingCheckForKeyLongPress, ViewConfiguration.getLongPressTimeout());
+            }
+        }
+    }
+
+    public void setScrollIndicators(View up, View down) {
+        mScrollUp = up;
+        mScrollDown = down;
+    }
+
+    void updateSelectorState() {
+        final Drawable selector = mSelector;
+        if (selector != null && selector.isStateful()) {
+            if (shouldShowSelector()) {
+                if (selector.setState(getDrawableStateForSelector())) {
+                    invalidateDrawable(selector);
+                }
+            } else {
+                selector.setState(StateSet.NOTHING);
+            }
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        updateSelectorState();
+    }
+
+    private int[] getDrawableStateForSelector() {
+        // If the child view is enabled then do the default behavior.
+        if (mIsChildViewEnabled) {
+            // Common case
+            return super.getDrawableState();
+        }
+
+        // The selector uses this View's drawable state. The selected child view
+        // is disabled, so we need to remove the enabled state from the drawable
+        // states.
+        final int enabledState = ENABLED_STATE_SET[0];
+
+        // If we don't have any extra space, it will return one of the static
+        // state arrays, and clearing the enabled state on those arrays is a
+        // bad thing! If we specify we need extra space, it will create+copy
+        // into a new array that is safely mutable.
+        final int[] state = onCreateDrawableState(1);
+
+        int enabledPos = -1;
+        for (int i = state.length - 1; i >= 0; i--) {
+            if (state[i] == enabledState) {
+                enabledPos = i;
+                break;
+            }
+        }
+
+        // Remove the enabled state
+        if (enabledPos >= 0) {
+            System.arraycopy(state, enabledPos + 1, state, enabledPos,
+                    state.length - enabledPos - 1);
+        }
+
+        return state;
+    }
+
+    @Override
+    public boolean verifyDrawable(@NonNull Drawable dr) {
+        return mSelector == dr || super.verifyDrawable(dr);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mSelector != null) mSelector.jumpToCurrentState();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        final ViewTreeObserver treeObserver = getViewTreeObserver();
+        treeObserver.addOnTouchModeChangeListener(this);
+        if (mTextFilterEnabled && mPopup != null && !mGlobalLayoutListenerAddedFilter) {
+            treeObserver.addOnGlobalLayoutListener(this);
+        }
+
+        if (mAdapter != null && mDataSetObserver == null) {
+            mDataSetObserver = new AdapterDataSetObserver();
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+
+            // Data may have changed while we were detached. Refresh.
+            mDataChanged = true;
+            mOldItemCount = mItemCount;
+            mItemCount = mAdapter.getCount();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        mIsDetaching = true;
+
+        // Dismiss the popup in case onSaveInstanceState() was not invoked
+        dismissPopup();
+
+        // Detach any view left in the scrap heap
+        mRecycler.clear();
+
+        final ViewTreeObserver treeObserver = getViewTreeObserver();
+        treeObserver.removeOnTouchModeChangeListener(this);
+        if (mTextFilterEnabled && mPopup != null) {
+            treeObserver.removeOnGlobalLayoutListener(this);
+            mGlobalLayoutListenerAddedFilter = false;
+        }
+
+        if (mAdapter != null && mDataSetObserver != null) {
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
+            mDataSetObserver = null;
+        }
+
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+
+        if (mFlingStrictSpan != null) {
+            mFlingStrictSpan.finish();
+            mFlingStrictSpan = null;
+        }
+
+        if (mFlingRunnable != null) {
+            removeCallbacks(mFlingRunnable);
+        }
+
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+
+        if (mClearScrollingCache != null) {
+            removeCallbacks(mClearScrollingCache);
+        }
+
+        if (mPerformClick != null) {
+            removeCallbacks(mPerformClick);
+        }
+
+        if (mTouchModeReset != null) {
+            removeCallbacks(mTouchModeReset);
+            mTouchModeReset.run();
+        }
+
+        mIsDetaching = false;
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+
+        final int touchMode = isInTouchMode() ? TOUCH_MODE_ON : TOUCH_MODE_OFF;
+
+        if (!hasWindowFocus) {
+            setChildrenDrawingCacheEnabled(false);
+            if (mFlingRunnable != null) {
+                removeCallbacks(mFlingRunnable);
+                // let the fling runnable report its new state which
+                // should be idle
+                mFlingRunnable.mSuppressIdleStateChangeCall = false;
+                mFlingRunnable.endFling();
+                if (mPositionScroller != null) {
+                    mPositionScroller.stop();
+                }
+                if (mScrollY != 0) {
+                    mScrollY = 0;
+                    invalidateParentCaches();
+                    finishGlows();
+                    invalidate();
+                }
+            }
+            // Always hide the type filter
+            dismissPopup();
+
+            if (touchMode == TOUCH_MODE_OFF) {
+                // Remember the last selected element
+                mResurrectToPosition = mSelectedPosition;
+            }
+        } else {
+            if (mFiltered && !mPopupHidden) {
+                // Show the type filter only if a filter is in effect
+                showPopup();
+            }
+
+            // If we changed touch mode since the last time we had focus
+            if (touchMode != mLastTouchMode && mLastTouchMode != TOUCH_MODE_UNKNOWN) {
+                // If we come back in trackball mode, we bring the selection back
+                if (touchMode == TOUCH_MODE_OFF) {
+                    // This will trigger a layout
+                    resurrectSelection();
+
+                // If we come back in touch mode, then we want to hide the selector
+                } else {
+                    hideSelector();
+                    mLayoutMode = LAYOUT_NORMAL;
+                    layoutChildren();
+                }
+            }
+        }
+
+        mLastTouchMode = touchMode;
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        if (mFastScroll != null) {
+           mFastScroll.setScrollbarPosition(getVerticalScrollbarPosition());
+        }
+    }
+
+    /**
+     * Creates the ContextMenuInfo returned from {@link #getContextMenuInfo()}. This
+     * methods knows the view, position and ID of the item that received the
+     * long press.
+     *
+     * @param view The view that received the long press.
+     * @param position The position of the item that received the long press.
+     * @param id The ID of the item that received the long press.
+     * @return The extra information that should be returned by
+     *         {@link #getContextMenuInfo()}.
+     */
+    ContextMenuInfo createContextMenuInfo(View view, int position, long id) {
+        return new AdapterContextMenuInfo(view, position, id);
+    }
+
+    @Override
+    public void onCancelPendingInputEvents() {
+        super.onCancelPendingInputEvents();
+        if (mPerformClick != null) {
+            removeCallbacks(mPerformClick);
+        }
+        if (mPendingCheckForTap != null) {
+            removeCallbacks(mPendingCheckForTap);
+        }
+        if (mPendingCheckForLongPress != null) {
+            removeCallbacks(mPendingCheckForLongPress);
+        }
+        if (mPendingCheckForKeyLongPress != null) {
+            removeCallbacks(mPendingCheckForKeyLongPress);
+        }
+    }
+
+    /**
+     * A base class for Runnables that will check that their view is still attached to
+     * the original window as when the Runnable was created.
+     *
+     */
+    private class WindowRunnnable {
+        private int mOriginalAttachCount;
+
+        public void rememberWindowAttachCount() {
+            mOriginalAttachCount = getWindowAttachCount();
+        }
+
+        public boolean sameWindow() {
+            return getWindowAttachCount() == mOriginalAttachCount;
+        }
+    }
+
+    private class PerformClick extends WindowRunnnable implements Runnable {
+        int mClickMotionPosition;
+
+        @Override
+        public void run() {
+            // The data has changed since we posted this action in the event queue,
+            // bail out before bad things happen
+            if (mDataChanged) return;
+
+            final ListAdapter adapter = mAdapter;
+            final int motionPosition = mClickMotionPosition;
+            if (adapter != null && mItemCount > 0 &&
+                    motionPosition != INVALID_POSITION &&
+                    motionPosition < adapter.getCount() && sameWindow() &&
+                    adapter.isEnabled(motionPosition)) {
+                final View view = getChildAt(motionPosition - mFirstPosition);
+                // If there is no view, something bad happened (the view scrolled off the
+                // screen, etc.) and we should cancel the click
+                if (view != null) {
+                    performItemClick(view, motionPosition, adapter.getItemId(motionPosition));
+                }
+            }
+        }
+    }
+
+    private class CheckForLongPress extends WindowRunnnable implements Runnable {
+        private static final int INVALID_COORD = -1;
+        private float mX = INVALID_COORD;
+        private float mY = INVALID_COORD;
+
+        private void setCoords(float x, float y) {
+            mX = x;
+            mY = y;
+        }
+
+        @Override
+        public void run() {
+            final int motionPosition = mMotionPosition;
+            final View child = getChildAt(motionPosition - mFirstPosition);
+            if (child != null) {
+                final int longPressPosition = mMotionPosition;
+                final long longPressId = mAdapter.getItemId(mMotionPosition);
+
+                boolean handled = false;
+                if (sameWindow() && !mDataChanged) {
+                    if (mX != INVALID_COORD && mY != INVALID_COORD) {
+                        handled = performLongPress(child, longPressPosition, longPressId, mX, mY);
+                    } else {
+                        handled = performLongPress(child, longPressPosition, longPressId);
+                    }
+                }
+
+                if (handled) {
+                    mHasPerformedLongPress = true;
+                    mTouchMode = TOUCH_MODE_REST;
+                    setPressed(false);
+                    child.setPressed(false);
+                } else {
+                    mTouchMode = TOUCH_MODE_DONE_WAITING;
+                }
+            }
+        }
+    }
+
+    private class CheckForKeyLongPress extends WindowRunnnable implements Runnable {
+        @Override
+        public void run() {
+            if (isPressed() && mSelectedPosition >= 0) {
+                int index = mSelectedPosition - mFirstPosition;
+                View v = getChildAt(index);
+
+                if (!mDataChanged) {
+                    boolean handled = false;
+                    if (sameWindow()) {
+                        handled = performLongPress(v, mSelectedPosition, mSelectedRowId);
+                    }
+                    if (handled) {
+                        setPressed(false);
+                        v.setPressed(false);
+                    }
+                } else {
+                    setPressed(false);
+                    if (v != null) v.setPressed(false);
+                }
+            }
+        }
+    }
+
+    private boolean performStylusButtonPressAction(MotionEvent ev) {
+        if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL && mChoiceActionMode == null) {
+            final View child = getChildAt(mMotionPosition - mFirstPosition);
+            if (child != null) {
+                final int longPressPosition = mMotionPosition;
+                final long longPressId = mAdapter.getItemId(mMotionPosition);
+                if (performLongPress(child, longPressPosition, longPressId)) {
+                    mTouchMode = TOUCH_MODE_REST;
+                    setPressed(false);
+                    child.setPressed(false);
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    boolean performLongPress(final View child,
+            final int longPressPosition, final long longPressId) {
+        return performLongPress(
+                child,
+                longPressPosition,
+                longPressId,
+                CheckForLongPress.INVALID_COORD,
+                CheckForLongPress.INVALID_COORD);
+    }
+
+    boolean performLongPress(final View child,
+            final int longPressPosition, final long longPressId, float x, float y) {
+        // CHOICE_MODE_MULTIPLE_MODAL takes over long press.
+        if (mChoiceMode == CHOICE_MODE_MULTIPLE_MODAL) {
+            if (mChoiceActionMode == null &&
+                    (mChoiceActionMode = startActionMode(mMultiChoiceModeCallback)) != null) {
+                setItemChecked(longPressPosition, true);
+                performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            }
+            return true;
+        }
+
+        boolean handled = false;
+        if (mOnItemLongClickListener != null) {
+            handled = mOnItemLongClickListener.onItemLongClick(AbsListView.this, child,
+                    longPressPosition, longPressId);
+        }
+        if (!handled) {
+            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+            if (x != CheckForLongPress.INVALID_COORD && y != CheckForLongPress.INVALID_COORD) {
+                handled = super.showContextMenuForChild(AbsListView.this, x, y);
+            } else {
+                handled = super.showContextMenuForChild(AbsListView.this);
+            }
+        }
+        if (handled) {
+            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        }
+        return handled;
+    }
+
+    @Override
+    protected ContextMenuInfo getContextMenuInfo() {
+        return mContextMenuInfo;
+    }
+
+    @Override
+    public boolean showContextMenu() {
+        return showContextMenuInternal(0, 0, false);
+    }
+
+    @Override
+    public boolean showContextMenu(float x, float y) {
+        return showContextMenuInternal(x, y, true);
+    }
+
+    private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
+        final int position = pointToPosition((int)x, (int)y);
+        if (position != INVALID_POSITION) {
+            final long id = mAdapter.getItemId(position);
+            View child = getChildAt(position - mFirstPosition);
+            if (child != null) {
+                mContextMenuInfo = createContextMenuInfo(child, position, id);
+                if (useOffsets) {
+                    return super.showContextMenuForChild(this, x, y);
+                } else {
+                    return super.showContextMenuForChild(this);
+                }
+            }
+        }
+        if (useOffsets) {
+            return super.showContextMenu(x, y);
+        } else {
+            return super.showContextMenu();
+        }
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView) {
+        if (isShowingContextMenuWithCoords()) {
+            return false;
+        }
+        return showContextMenuForChildInternal(originalView, 0, 0, false);
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView, float x, float y) {
+        return showContextMenuForChildInternal(originalView,x, y, true);
+    }
+
+    private boolean showContextMenuForChildInternal(View originalView, float x, float y,
+            boolean useOffsets) {
+        final int longPressPosition = getPositionForView(originalView);
+        if (longPressPosition < 0) {
+            return false;
+        }
+
+        final long longPressId = mAdapter.getItemId(longPressPosition);
+        boolean handled = false;
+
+        if (mOnItemLongClickListener != null) {
+            handled = mOnItemLongClickListener.onItemLongClick(this, originalView,
+                    longPressPosition, longPressId);
+        }
+
+        if (!handled) {
+            final View child = getChildAt(longPressPosition - mFirstPosition);
+            mContextMenuInfo = createContextMenuInfo(child, longPressPosition, longPressId);
+
+            if (useOffsets) {
+                handled = super.showContextMenuForChild(originalView, x, y);
+            } else {
+                handled = super.showContextMenuForChild(originalView);
+            }
+        }
+
+        return handled;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return false;
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (KeyEvent.isConfirmKey(keyCode)) {
+            if (!isEnabled()) {
+                return true;
+            }
+            if (isClickable() && isPressed() &&
+                    mSelectedPosition >= 0 && mAdapter != null &&
+                    mSelectedPosition < mAdapter.getCount()) {
+
+                final View view = getChildAt(mSelectedPosition - mFirstPosition);
+                if (view != null) {
+                    performItemClick(view, mSelectedPosition, mSelectedRowId);
+                    view.setPressed(false);
+                }
+                setPressed(false);
+                return true;
+            }
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    protected void dispatchSetPressed(boolean pressed) {
+        // Don't dispatch setPressed to our children. We call setPressed on ourselves to
+        // get the selector in the right state, but we don't want to press each child.
+    }
+
+    @Override
+    public void dispatchDrawableHotspotChanged(float x, float y) {
+        // Don't dispatch hotspot changes to children. We'll manually handle
+        // calling drawableHotspotChanged on the correct child.
+    }
+
+    /**
+     * Maps a point to a position in the list.
+     *
+     * @param x X in local coordinate
+     * @param y Y in local coordinate
+     * @return The position of the item which contains the specified point, or
+     *         {@link #INVALID_POSITION} if the point does not intersect an item.
+     */
+    public int pointToPosition(int x, int y) {
+        Rect frame = mTouchFrame;
+        if (frame == null) {
+            mTouchFrame = new Rect();
+            frame = mTouchFrame;
+        }
+
+        final int count = getChildCount();
+        for (int i = count - 1; i >= 0; i--) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == View.VISIBLE) {
+                child.getHitRect(frame);
+                if (frame.contains(x, y)) {
+                    return mFirstPosition + i;
+                }
+            }
+        }
+        return INVALID_POSITION;
+    }
+
+
+    /**
+     * Maps a point to a the rowId of the item which intersects that point.
+     *
+     * @param x X in local coordinate
+     * @param y Y in local coordinate
+     * @return The rowId of the item which contains the specified point, or {@link #INVALID_ROW_ID}
+     *         if the point does not intersect an item.
+     */
+    public long pointToRowId(int x, int y) {
+        int position = pointToPosition(x, y);
+        if (position >= 0) {
+            return mAdapter.getItemId(position);
+        }
+        return INVALID_ROW_ID;
+    }
+
+    private final class CheckForTap implements Runnable {
+        float x;
+        float y;
+
+        @Override
+        public void run() {
+            if (mTouchMode == TOUCH_MODE_DOWN) {
+                mTouchMode = TOUCH_MODE_TAP;
+                final View child = getChildAt(mMotionPosition - mFirstPosition);
+                if (child != null && !child.hasExplicitFocusable()) {
+                    mLayoutMode = LAYOUT_NORMAL;
+
+                    if (!mDataChanged) {
+                        final float[] point = mTmpPoint;
+                        point[0] = x;
+                        point[1] = y;
+                        transformPointToViewLocal(point, child);
+                        child.drawableHotspotChanged(point[0], point[1]);
+                        child.setPressed(true);
+                        setPressed(true);
+                        layoutChildren();
+                        positionSelector(mMotionPosition, child);
+                        refreshDrawableState();
+
+                        final int longPressTimeout = ViewConfiguration.getLongPressTimeout();
+                        final boolean longClickable = isLongClickable();
+
+                        if (mSelector != null) {
+                            final Drawable d = mSelector.getCurrent();
+                            if (d != null && d instanceof TransitionDrawable) {
+                                if (longClickable) {
+                                    ((TransitionDrawable) d).startTransition(longPressTimeout);
+                                } else {
+                                    ((TransitionDrawable) d).resetTransition();
+                                }
+                            }
+                            mSelector.setHotspot(x, y);
+                        }
+
+                        if (longClickable) {
+                            if (mPendingCheckForLongPress == null) {
+                                mPendingCheckForLongPress = new CheckForLongPress();
+                            }
+                            mPendingCheckForLongPress.setCoords(x, y);
+                            mPendingCheckForLongPress.rememberWindowAttachCount();
+                            postDelayed(mPendingCheckForLongPress, longPressTimeout);
+                        } else {
+                            mTouchMode = TOUCH_MODE_DONE_WAITING;
+                        }
+                    } else {
+                        mTouchMode = TOUCH_MODE_DONE_WAITING;
+                    }
+                }
+            }
+        }
+    }
+
+    private boolean startScrollIfNeeded(int x, int y, MotionEvent vtev) {
+        // Check if we have moved far enough that it looks more like a
+        // scroll than a tap
+        final int deltaY = y - mMotionY;
+        final int distance = Math.abs(deltaY);
+        final boolean overscroll = mScrollY != 0;
+        if ((overscroll || distance > mTouchSlop) &&
+                (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+            createScrollingCache();
+            if (overscroll) {
+                mTouchMode = TOUCH_MODE_OVERSCROLL;
+                mMotionCorrection = 0;
+            } else {
+                mTouchMode = TOUCH_MODE_SCROLL;
+                mMotionCorrection = deltaY > 0 ? mTouchSlop : -mTouchSlop;
+            }
+            removeCallbacks(mPendingCheckForLongPress);
+            setPressed(false);
+            final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+            if (motionView != null) {
+                motionView.setPressed(false);
+            }
+            reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+            // Time to start stealing events! Once we've stolen them, don't let anyone
+            // steal from us
+            final ViewParent parent = getParent();
+            if (parent != null) {
+                parent.requestDisallowInterceptTouchEvent(true);
+            }
+            scrollIfNeeded(x, y, vtev);
+            return true;
+        }
+
+        return false;
+    }
+
+    private void scrollIfNeeded(int x, int y, MotionEvent vtev) {
+        int rawDeltaY = y - mMotionY;
+        int scrollOffsetCorrection = 0;
+        int scrollConsumedCorrection = 0;
+        if (mLastY == Integer.MIN_VALUE) {
+            rawDeltaY -= mMotionCorrection;
+        }
+        if (dispatchNestedPreScroll(0, mLastY != Integer.MIN_VALUE ? mLastY - y : -rawDeltaY,
+                mScrollConsumed, mScrollOffset)) {
+            rawDeltaY += mScrollConsumed[1];
+            scrollOffsetCorrection = -mScrollOffset[1];
+            scrollConsumedCorrection = mScrollConsumed[1];
+            if (vtev != null) {
+                vtev.offsetLocation(0, mScrollOffset[1]);
+                mNestedYOffset += mScrollOffset[1];
+            }
+        }
+        final int deltaY = rawDeltaY;
+        int incrementalDeltaY =
+                mLastY != Integer.MIN_VALUE ? y - mLastY + scrollConsumedCorrection : deltaY;
+        int lastYCorrection = 0;
+
+        if (mTouchMode == TOUCH_MODE_SCROLL) {
+            if (PROFILE_SCROLLING) {
+                if (!mScrollProfilingStarted) {
+                    Debug.startMethodTracing("AbsListViewScroll");
+                    mScrollProfilingStarted = true;
+                }
+            }
+
+            if (mScrollStrictSpan == null) {
+                // If it's non-null, we're already in a scroll.
+                mScrollStrictSpan = StrictMode.enterCriticalSpan("AbsListView-scroll");
+            }
+
+            if (y != mLastY) {
+                // We may be here after stopping a fling and continuing to scroll.
+                // If so, we haven't disallowed intercepting touch events yet.
+                // Make sure that we do so in case we're in a parent that can intercept.
+                if ((mGroupFlags & FLAG_DISALLOW_INTERCEPT) == 0 &&
+                        Math.abs(rawDeltaY) > mTouchSlop) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+
+                final int motionIndex;
+                if (mMotionPosition >= 0) {
+                    motionIndex = mMotionPosition - mFirstPosition;
+                } else {
+                    // If we don't have a motion position that we can reliably track,
+                    // pick something in the middle to make a best guess at things below.
+                    motionIndex = getChildCount() / 2;
+                }
+
+                int motionViewPrevTop = 0;
+                View motionView = this.getChildAt(motionIndex);
+                if (motionView != null) {
+                    motionViewPrevTop = motionView.getTop();
+                }
+
+                // No need to do all this work if we're not going to move anyway
+                boolean atEdge = false;
+                if (incrementalDeltaY != 0) {
+                    atEdge = trackMotionScroll(deltaY, incrementalDeltaY);
+                }
+
+                // Check to see if we have bumped into the scroll limit
+                motionView = this.getChildAt(motionIndex);
+                if (motionView != null) {
+                    // Check if the top of the motion view is where it is
+                    // supposed to be
+                    final int motionViewRealTop = motionView.getTop();
+                    if (atEdge) {
+                        // Apply overscroll
+
+                        int overscroll = -incrementalDeltaY -
+                                (motionViewRealTop - motionViewPrevTop);
+                        if (dispatchNestedScroll(0, overscroll - incrementalDeltaY, 0, overscroll,
+                                mScrollOffset)) {
+                            lastYCorrection -= mScrollOffset[1];
+                            if (vtev != null) {
+                                vtev.offsetLocation(0, mScrollOffset[1]);
+                                mNestedYOffset += mScrollOffset[1];
+                            }
+                        } else {
+                            final boolean atOverscrollEdge = overScrollBy(0, overscroll,
+                                    0, mScrollY, 0, 0, 0, mOverscrollDistance, true);
+
+                            if (atOverscrollEdge && mVelocityTracker != null) {
+                                // Don't allow overfling if we're at the edge
+                                mVelocityTracker.clear();
+                            }
+
+                            final int overscrollMode = getOverScrollMode();
+                            if (overscrollMode == OVER_SCROLL_ALWAYS ||
+                                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
+                                            !contentFits())) {
+                                if (!atOverscrollEdge) {
+                                    mDirection = 0; // Reset when entering overscroll.
+                                    mTouchMode = TOUCH_MODE_OVERSCROLL;
+                                }
+                                if (incrementalDeltaY > 0) {
+                                    mEdgeGlowTop.onPull((float) -overscroll / getHeight(),
+                                            (float) x / getWidth());
+                                    if (!mEdgeGlowBottom.isFinished()) {
+                                        mEdgeGlowBottom.onRelease();
+                                    }
+                                    invalidateTopGlow();
+                                } else if (incrementalDeltaY < 0) {
+                                    mEdgeGlowBottom.onPull((float) overscroll / getHeight(),
+                                            1.f - (float) x / getWidth());
+                                    if (!mEdgeGlowTop.isFinished()) {
+                                        mEdgeGlowTop.onRelease();
+                                    }
+                                    invalidateBottomGlow();
+                                }
+                            }
+                        }
+                    }
+                    mMotionY = y + lastYCorrection + scrollOffsetCorrection;
+                }
+                mLastY = y + lastYCorrection + scrollOffsetCorrection;
+            }
+        } else if (mTouchMode == TOUCH_MODE_OVERSCROLL) {
+            if (y != mLastY) {
+                final int oldScroll = mScrollY;
+                final int newScroll = oldScroll - incrementalDeltaY;
+                int newDirection = y > mLastY ? 1 : -1;
+
+                if (mDirection == 0) {
+                    mDirection = newDirection;
+                }
+
+                int overScrollDistance = -incrementalDeltaY;
+                if ((newScroll < 0 && oldScroll >= 0) || (newScroll > 0 && oldScroll <= 0)) {
+                    overScrollDistance = -oldScroll;
+                    incrementalDeltaY += overScrollDistance;
+                } else {
+                    incrementalDeltaY = 0;
+                }
+
+                if (overScrollDistance != 0) {
+                    overScrollBy(0, overScrollDistance, 0, mScrollY, 0, 0,
+                            0, mOverscrollDistance, true);
+                    final int overscrollMode = getOverScrollMode();
+                    if (overscrollMode == OVER_SCROLL_ALWAYS ||
+                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS &&
+                                    !contentFits())) {
+                        if (rawDeltaY > 0) {
+                            mEdgeGlowTop.onPull((float) overScrollDistance / getHeight(),
+                                    (float) x / getWidth());
+                            if (!mEdgeGlowBottom.isFinished()) {
+                                mEdgeGlowBottom.onRelease();
+                            }
+                            invalidateTopGlow();
+                        } else if (rawDeltaY < 0) {
+                            mEdgeGlowBottom.onPull((float) overScrollDistance / getHeight(),
+                                    1.f - (float) x / getWidth());
+                            if (!mEdgeGlowTop.isFinished()) {
+                                mEdgeGlowTop.onRelease();
+                            }
+                            invalidateBottomGlow();
+                        }
+                    }
+                }
+
+                if (incrementalDeltaY != 0) {
+                    // Coming back to 'real' list scrolling
+                    if (mScrollY != 0) {
+                        mScrollY = 0;
+                        invalidateParentIfNeeded();
+                    }
+
+                    trackMotionScroll(incrementalDeltaY, incrementalDeltaY);
+
+                    mTouchMode = TOUCH_MODE_SCROLL;
+
+                    // We did not scroll the full amount. Treat this essentially like the
+                    // start of a new touch scroll
+                    final int motionPosition = findClosestMotionRow(y);
+
+                    mMotionCorrection = 0;
+                    View motionView = getChildAt(motionPosition - mFirstPosition);
+                    mMotionViewOriginalTop = motionView != null ? motionView.getTop() : 0;
+                    mMotionY =  y + scrollOffsetCorrection;
+                    mMotionPosition = motionPosition;
+                }
+                mLastY = y + lastYCorrection + scrollOffsetCorrection;
+                mDirection = newDirection;
+            }
+        }
+    }
+
+    private void invalidateTopGlow() {
+        if (mEdgeGlowTop == null) {
+            return;
+        }
+        final boolean clipToPadding = getClipToPadding();
+        final int top = clipToPadding ? mPaddingTop : 0;
+        final int left = clipToPadding ? mPaddingLeft : 0;
+        final int right = clipToPadding ? getWidth() - mPaddingRight : getWidth();
+        invalidate(left, top, right, top + mEdgeGlowTop.getMaxHeight());
+    }
+
+    private void invalidateBottomGlow() {
+        if (mEdgeGlowBottom == null) {
+            return;
+        }
+        final boolean clipToPadding = getClipToPadding();
+        final int bottom = clipToPadding ? getHeight() - mPaddingBottom : getHeight();
+        final int left = clipToPadding ? mPaddingLeft : 0;
+        final int right = clipToPadding ? getWidth() - mPaddingRight : getWidth();
+        invalidate(left, bottom - mEdgeGlowBottom.getMaxHeight(), right, bottom);
+    }
+
+    @Override
+    public void onTouchModeChanged(boolean isInTouchMode) {
+        if (isInTouchMode) {
+            // Get rid of the selection when we enter touch mode
+            hideSelector();
+            // Layout, but only if we already have done so previously.
+            // (Otherwise may clobber a LAYOUT_SYNC layout that was requested to restore
+            // state.)
+            if (getHeight() > 0 && getChildCount() > 0) {
+                // We do not lose focus initiating a touch (since AbsListView is focusable in
+                // touch mode). Force an initial layout to get rid of the selection.
+                layoutChildren();
+            }
+            updateSelectorState();
+        } else {
+            int touchMode = mTouchMode;
+            if (touchMode == TOUCH_MODE_OVERSCROLL || touchMode == TOUCH_MODE_OVERFLING) {
+                if (mFlingRunnable != null) {
+                    mFlingRunnable.endFling();
+                }
+                if (mPositionScroller != null) {
+                    mPositionScroller.stop();
+                }
+
+                if (mScrollY != 0) {
+                    mScrollY = 0;
+                    invalidateParentCaches();
+                    finishGlows();
+                    invalidate();
+                }
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected boolean handleScrollBarDragging(MotionEvent event) {
+        // Doesn't support normal scroll bar dragging. Use FastScroller.
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (!isEnabled()) {
+            // A disabled view that is clickable still consumes the touch
+            // events, it just doesn't respond to them.
+            return isClickable() || isLongClickable();
+        }
+
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+
+        if (mIsDetaching || !isAttachedToWindow()) {
+            // Something isn't right.
+            // Since we rely on being attached to get data set change notifications,
+            // don't risk doing anything where we might try to resync and find things
+            // in a bogus state.
+            return false;
+        }
+
+        startNestedScroll(SCROLL_AXIS_VERTICAL);
+
+        if (mFastScroll != null && mFastScroll.onTouchEvent(ev)) {
+            return true;
+        }
+
+        initVelocityTrackerIfNotExists();
+        final MotionEvent vtev = MotionEvent.obtain(ev);
+
+        final int actionMasked = ev.getActionMasked();
+        if (actionMasked == MotionEvent.ACTION_DOWN) {
+            mNestedYOffset = 0;
+        }
+        vtev.offsetLocation(0, mNestedYOffset);
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN: {
+                onTouchDown(ev);
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                onTouchMove(ev, vtev);
+                break;
+            }
+
+            case MotionEvent.ACTION_UP: {
+                onTouchUp(ev);
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL: {
+                onTouchCancel();
+                break;
+            }
+
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+                final int x = mMotionX;
+                final int y = mMotionY;
+                final int motionPosition = pointToPosition(x, y);
+                if (motionPosition >= 0) {
+                    // Remember where the motion event started
+                    final View child = getChildAt(motionPosition - mFirstPosition);
+                    mMotionViewOriginalTop = child.getTop();
+                    mMotionPosition = motionPosition;
+                }
+                mLastY = y;
+                break;
+            }
+
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                // New pointers take over dragging duties
+                final int index = ev.getActionIndex();
+                final int id = ev.getPointerId(index);
+                final int x = (int) ev.getX(index);
+                final int y = (int) ev.getY(index);
+                mMotionCorrection = 0;
+                mActivePointerId = id;
+                mMotionX = x;
+                mMotionY = y;
+                final int motionPosition = pointToPosition(x, y);
+                if (motionPosition >= 0) {
+                    // Remember where the motion event started
+                    final View child = getChildAt(motionPosition - mFirstPosition);
+                    mMotionViewOriginalTop = child.getTop();
+                    mMotionPosition = motionPosition;
+                }
+                mLastY = y;
+                break;
+            }
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.addMovement(vtev);
+        }
+        vtev.recycle();
+        return true;
+    }
+
+    private void onTouchDown(MotionEvent ev) {
+        mHasPerformedLongPress = false;
+        mActivePointerId = ev.getPointerId(0);
+
+        if (mTouchMode == TOUCH_MODE_OVERFLING) {
+            // Stopped the fling. It is a scroll.
+            mFlingRunnable.endFling();
+            if (mPositionScroller != null) {
+                mPositionScroller.stop();
+            }
+            mTouchMode = TOUCH_MODE_OVERSCROLL;
+            mMotionX = (int) ev.getX();
+            mMotionY = (int) ev.getY();
+            mLastY = mMotionY;
+            mMotionCorrection = 0;
+            mDirection = 0;
+        } else {
+            final int x = (int) ev.getX();
+            final int y = (int) ev.getY();
+            int motionPosition = pointToPosition(x, y);
+
+            if (!mDataChanged) {
+                if (mTouchMode == TOUCH_MODE_FLING) {
+                    // Stopped a fling. It is a scroll.
+                    createScrollingCache();
+                    mTouchMode = TOUCH_MODE_SCROLL;
+                    mMotionCorrection = 0;
+                    motionPosition = findMotionRow(y);
+                    mFlingRunnable.flywheelTouch();
+                } else if ((motionPosition >= 0) && getAdapter().isEnabled(motionPosition)) {
+                    // User clicked on an actual view (and was not stopping a
+                    // fling). It might be a click or a scroll. Assume it is a
+                    // click until proven otherwise.
+                    mTouchMode = TOUCH_MODE_DOWN;
+
+                    // FIXME Debounce
+                    if (mPendingCheckForTap == null) {
+                        mPendingCheckForTap = new CheckForTap();
+                    }
+
+                    mPendingCheckForTap.x = ev.getX();
+                    mPendingCheckForTap.y = ev.getY();
+                    postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+                }
+            }
+
+            if (motionPosition >= 0) {
+                // Remember where the motion event started
+                final View v = getChildAt(motionPosition - mFirstPosition);
+                mMotionViewOriginalTop = v.getTop();
+            }
+
+            mMotionX = x;
+            mMotionY = y;
+            mMotionPosition = motionPosition;
+            mLastY = Integer.MIN_VALUE;
+        }
+
+        if (mTouchMode == TOUCH_MODE_DOWN && mMotionPosition != INVALID_POSITION
+                && performButtonActionOnTouchDown(ev)) {
+                removeCallbacks(mPendingCheckForTap);
+        }
+    }
+
+    private void onTouchMove(MotionEvent ev, MotionEvent vtev) {
+        if (mHasPerformedLongPress) {
+            // Consume all move events following a successful long press.
+            return;
+        }
+
+        int pointerIndex = ev.findPointerIndex(mActivePointerId);
+        if (pointerIndex == -1) {
+            pointerIndex = 0;
+            mActivePointerId = ev.getPointerId(pointerIndex);
+        }
+
+        if (mDataChanged) {
+            // Re-sync everything if data has been changed
+            // since the scroll operation can query the adapter.
+            layoutChildren();
+        }
+
+        final int y = (int) ev.getY(pointerIndex);
+
+        switch (mTouchMode) {
+            case TOUCH_MODE_DOWN:
+            case TOUCH_MODE_TAP:
+            case TOUCH_MODE_DONE_WAITING:
+                // Check if we have moved far enough that it looks more like a
+                // scroll than a tap. If so, we'll enter scrolling mode.
+                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, vtev)) {
+                    break;
+                }
+                // Otherwise, check containment within list bounds. If we're
+                // outside bounds, cancel any active presses.
+                final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+                final float x = ev.getX(pointerIndex);
+                if (!pointInView(x, y, mTouchSlop)) {
+                    setPressed(false);
+                    if (motionView != null) {
+                        motionView.setPressed(false);
+                    }
+                    removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+                            mPendingCheckForTap : mPendingCheckForLongPress);
+                    mTouchMode = TOUCH_MODE_DONE_WAITING;
+                    updateSelectorState();
+                } else if (motionView != null) {
+                    // Still within bounds, update the hotspot.
+                    final float[] point = mTmpPoint;
+                    point[0] = x;
+                    point[1] = y;
+                    transformPointToViewLocal(point, motionView);
+                    motionView.drawableHotspotChanged(point[0], point[1]);
+                }
+                break;
+            case TOUCH_MODE_SCROLL:
+            case TOUCH_MODE_OVERSCROLL:
+                scrollIfNeeded((int) ev.getX(pointerIndex), y, vtev);
+                break;
+        }
+    }
+
+    private void onTouchUp(MotionEvent ev) {
+        switch (mTouchMode) {
+        case TOUCH_MODE_DOWN:
+        case TOUCH_MODE_TAP:
+        case TOUCH_MODE_DONE_WAITING:
+            final int motionPosition = mMotionPosition;
+            final View child = getChildAt(motionPosition - mFirstPosition);
+            if (child != null) {
+                if (mTouchMode != TOUCH_MODE_DOWN) {
+                    child.setPressed(false);
+                }
+
+                final float x = ev.getX();
+                final boolean inList = x > mListPadding.left && x < getWidth() - mListPadding.right;
+                if (inList && !child.hasExplicitFocusable()) {
+                    if (mPerformClick == null) {
+                        mPerformClick = new PerformClick();
+                    }
+
+                    final AbsListView.PerformClick performClick = mPerformClick;
+                    performClick.mClickMotionPosition = motionPosition;
+                    performClick.rememberWindowAttachCount();
+
+                    mResurrectToPosition = motionPosition;
+
+                    if (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP) {
+                        removeCallbacks(mTouchMode == TOUCH_MODE_DOWN ?
+                                mPendingCheckForTap : mPendingCheckForLongPress);
+                        mLayoutMode = LAYOUT_NORMAL;
+                        if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
+                            mTouchMode = TOUCH_MODE_TAP;
+                            setSelectedPositionInt(mMotionPosition);
+                            layoutChildren();
+                            child.setPressed(true);
+                            positionSelector(mMotionPosition, child);
+                            setPressed(true);
+                            if (mSelector != null) {
+                                Drawable d = mSelector.getCurrent();
+                                if (d != null && d instanceof TransitionDrawable) {
+                                    ((TransitionDrawable) d).resetTransition();
+                                }
+                                mSelector.setHotspot(x, ev.getY());
+                            }
+                            if (mTouchModeReset != null) {
+                                removeCallbacks(mTouchModeReset);
+                            }
+                            mTouchModeReset = new Runnable() {
+                                @Override
+                                public void run() {
+                                    mTouchModeReset = null;
+                                    mTouchMode = TOUCH_MODE_REST;
+                                    child.setPressed(false);
+                                    setPressed(false);
+                                    if (!mDataChanged && !mIsDetaching && isAttachedToWindow()) {
+                                        performClick.run();
+                                    }
+                                }
+                            };
+                            postDelayed(mTouchModeReset,
+                                    ViewConfiguration.getPressedStateDuration());
+                        } else {
+                            mTouchMode = TOUCH_MODE_REST;
+                            updateSelectorState();
+                        }
+                        return;
+                    } else if (!mDataChanged && mAdapter.isEnabled(motionPosition)) {
+                        performClick.run();
+                    }
+                }
+            }
+            mTouchMode = TOUCH_MODE_REST;
+            updateSelectorState();
+            break;
+        case TOUCH_MODE_SCROLL:
+            final int childCount = getChildCount();
+            if (childCount > 0) {
+                final int firstChildTop = getChildAt(0).getTop();
+                final int lastChildBottom = getChildAt(childCount - 1).getBottom();
+                final int contentTop = mListPadding.top;
+                final int contentBottom = getHeight() - mListPadding.bottom;
+                if (mFirstPosition == 0 && firstChildTop >= contentTop &&
+                        mFirstPosition + childCount < mItemCount &&
+                        lastChildBottom <= getHeight() - contentBottom) {
+                    mTouchMode = TOUCH_MODE_REST;
+                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                } else {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+
+                    final int initialVelocity = (int)
+                            (velocityTracker.getYVelocity(mActivePointerId) * mVelocityScale);
+                    // Fling if we have enough velocity and we aren't at a boundary.
+                    // Since we can potentially overfling more than we can overscroll, don't
+                    // allow the weird behavior where you can scroll to a boundary then
+                    // fling further.
+                    boolean flingVelocity = Math.abs(initialVelocity) > mMinimumVelocity;
+                    if (flingVelocity &&
+                            !((mFirstPosition == 0 &&
+                                    firstChildTop == contentTop - mOverscrollDistance) ||
+                              (mFirstPosition + childCount == mItemCount &&
+                                    lastChildBottom == contentBottom + mOverscrollDistance))) {
+                        if (!dispatchNestedPreFling(0, -initialVelocity)) {
+                            if (mFlingRunnable == null) {
+                                mFlingRunnable = new FlingRunnable();
+                            }
+                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+                            mFlingRunnable.start(-initialVelocity);
+                            dispatchNestedFling(0, -initialVelocity, true);
+                        } else {
+                            mTouchMode = TOUCH_MODE_REST;
+                            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                        }
+                    } else {
+                        mTouchMode = TOUCH_MODE_REST;
+                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                        if (mFlingRunnable != null) {
+                            mFlingRunnable.endFling();
+                        }
+                        if (mPositionScroller != null) {
+                            mPositionScroller.stop();
+                        }
+                        if (flingVelocity && !dispatchNestedPreFling(0, -initialVelocity)) {
+                            dispatchNestedFling(0, -initialVelocity, false);
+                        }
+                    }
+                }
+            } else {
+                mTouchMode = TOUCH_MODE_REST;
+                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+            }
+            break;
+
+        case TOUCH_MODE_OVERSCROLL:
+            if (mFlingRunnable == null) {
+                mFlingRunnable = new FlingRunnable();
+            }
+            final VelocityTracker velocityTracker = mVelocityTracker;
+            velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+            final int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+            if (Math.abs(initialVelocity) > mMinimumVelocity) {
+                mFlingRunnable.startOverfling(-initialVelocity);
+            } else {
+                mFlingRunnable.startSpringback();
+            }
+
+            break;
+        }
+
+        setPressed(false);
+
+        if (mEdgeGlowTop != null) {
+            mEdgeGlowTop.onRelease();
+            mEdgeGlowBottom.onRelease();
+        }
+
+        // Need to redraw since we probably aren't drawing the selector anymore
+        invalidate();
+        removeCallbacks(mPendingCheckForLongPress);
+        recycleVelocityTracker();
+
+        mActivePointerId = INVALID_POINTER;
+
+        if (PROFILE_SCROLLING) {
+            if (mScrollProfilingStarted) {
+                Debug.stopMethodTracing();
+                mScrollProfilingStarted = false;
+            }
+        }
+
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+    }
+
+    private void onTouchCancel() {
+        switch (mTouchMode) {
+        case TOUCH_MODE_OVERSCROLL:
+            if (mFlingRunnable == null) {
+                mFlingRunnable = new FlingRunnable();
+            }
+            mFlingRunnable.startSpringback();
+            break;
+
+        case TOUCH_MODE_OVERFLING:
+            // Do nothing - let it play out.
+            break;
+
+        default:
+            mTouchMode = TOUCH_MODE_REST;
+            setPressed(false);
+            final View motionView = this.getChildAt(mMotionPosition - mFirstPosition);
+            if (motionView != null) {
+                motionView.setPressed(false);
+            }
+            clearScrollingCache();
+            removeCallbacks(mPendingCheckForLongPress);
+            recycleVelocityTracker();
+        }
+
+        if (mEdgeGlowTop != null) {
+            mEdgeGlowTop.onRelease();
+            mEdgeGlowBottom.onRelease();
+        }
+        mActivePointerId = INVALID_POINTER;
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY, boolean clampedX, boolean clampedY) {
+        if (mScrollY != scrollY) {
+            onScrollChanged(mScrollX, scrollY, mScrollX, mScrollY);
+            mScrollY = scrollY;
+            invalidateParentIfNeeded();
+
+            awakenScrollBars();
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_SCROLL:
+                final float axisValue;
+                if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+                    axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+                    axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+                } else {
+                    axisValue = 0;
+                }
+
+                final int delta = Math.round(axisValue * mVerticalScrollFactor);
+                if (delta != 0) {
+                    if (!trackMotionScroll(delta, delta)) {
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_BUTTON_PRESS:
+                if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+                    int actionButton = event.getActionButton();
+                    if ((actionButton == MotionEvent.BUTTON_STYLUS_PRIMARY
+                            || actionButton == MotionEvent.BUTTON_SECONDARY)
+                            && (mTouchMode == TOUCH_MODE_DOWN || mTouchMode == TOUCH_MODE_TAP)) {
+                        if (performStylusButtonPressAction(event)) {
+                            removeCallbacks(mPendingCheckForLongPress);
+                            removeCallbacks(mPendingCheckForTap);
+                        }
+                    }
+                }
+                break;
+        }
+
+        return super.onGenericMotionEvent(event);
+    }
+
+    /**
+     * Initiate a fling with the given velocity.
+     *
+     * <p>Applications can use this method to manually initiate a fling as if the user
+     * initiated it via touch interaction.</p>
+     *
+     * @param velocityY Vertical velocity in pixels per second. Note that this is velocity of
+     *                  content, not velocity of a touch that initiated the fling.
+     */
+    public void fling(int velocityY) {
+        if (mFlingRunnable == null) {
+            mFlingRunnable = new FlingRunnable();
+        }
+        reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+        mFlingRunnable.start(velocityY);
+    }
+
+    @Override
+    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+        return ((nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0);
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int axes) {
+        super.onNestedScrollAccepted(child, target, axes);
+        startNestedScroll(SCROLL_AXIS_VERTICAL);
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
+        final int motionIndex = getChildCount() / 2;
+        final View motionView = getChildAt(motionIndex);
+        final int oldTop = motionView != null ? motionView.getTop() : 0;
+        if (motionView == null || trackMotionScroll(-dyUnconsumed, -dyUnconsumed)) {
+            int myUnconsumed = dyUnconsumed;
+            int myConsumed = 0;
+            if (motionView != null) {
+                myConsumed = motionView.getTop() - oldTop;
+                myUnconsumed -= myConsumed;
+            }
+            dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
+        }
+    }
+
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        final int childCount = getChildCount();
+        if (!consumed && childCount > 0 && canScrollList((int) velocityY) &&
+                Math.abs(velocityY) > mMinimumVelocity) {
+            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+            if (mFlingRunnable == null) {
+                mFlingRunnable = new FlingRunnable();
+            }
+            if (!dispatchNestedPreFling(0, velocityY)) {
+                mFlingRunnable.start((int) velocityY);
+            }
+            return true;
+        }
+        return dispatchNestedFling(velocityX, velocityY, consumed);
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        if (mEdgeGlowTop != null) {
+            final int scrollY = mScrollY;
+            final boolean clipToPadding = getClipToPadding();
+            final int width;
+            final int height;
+            final int translateX;
+            final int translateY;
+
+            if (clipToPadding) {
+                width = getWidth() - mPaddingLeft - mPaddingRight;
+                height = getHeight() - mPaddingTop - mPaddingBottom;
+                translateX = mPaddingLeft;
+                translateY = mPaddingTop;
+            } else {
+                width = getWidth();
+                height = getHeight();
+                translateX = 0;
+                translateY = 0;
+            }
+            if (!mEdgeGlowTop.isFinished()) {
+                final int restoreCount = canvas.save();
+                canvas.clipRect(translateX, translateY,
+                         translateX + width ,translateY + mEdgeGlowTop.getMaxHeight());
+                final int edgeY = Math.min(0, scrollY + mFirstPositionDistanceGuess) + translateY;
+                canvas.translate(translateX, edgeY);
+                mEdgeGlowTop.setSize(width, height);
+                if (mEdgeGlowTop.draw(canvas)) {
+                    invalidateTopGlow();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mEdgeGlowBottom.isFinished()) {
+                final int restoreCount = canvas.save();
+                canvas.clipRect(translateX, translateY + height - mEdgeGlowBottom.getMaxHeight(),
+                        translateX + width, translateY + height);
+                final int edgeX = -width + translateX;
+                final int edgeY = Math.max(getHeight(), scrollY + mLastPositionDistanceGuess)
+                        - (clipToPadding ? mPaddingBottom : 0);
+                canvas.translate(edgeX, edgeY);
+                canvas.rotate(180, width, 0);
+                mEdgeGlowBottom.setSize(width, height);
+                if (mEdgeGlowBottom.draw(canvas)) {
+                    invalidateBottomGlow();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+        }
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (disallowIntercept) {
+            recycleVelocityTracker();
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+    @Override
+    public boolean onInterceptHoverEvent(MotionEvent event) {
+        if (mFastScroll != null && mFastScroll.onInterceptHoverEvent(event)) {
+            return true;
+        }
+
+        return super.onInterceptHoverEvent(event);
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (mFastScroll != null) {
+            PointerIcon pointerIcon = mFastScroll.onResolvePointerIcon(event, pointerIndex);
+            if (pointerIcon != null) {
+                return pointerIcon;
+            }
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        final int actionMasked = ev.getActionMasked();
+        View v;
+
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+
+        if (mIsDetaching || !isAttachedToWindow()) {
+            // Something isn't right.
+            // Since we rely on being attached to get data set change notifications,
+            // don't risk doing anything where we might try to resync and find things
+            // in a bogus state.
+            return false;
+        }
+
+        if (mFastScroll != null && mFastScroll.onInterceptTouchEvent(ev)) {
+            return true;
+        }
+
+        switch (actionMasked) {
+        case MotionEvent.ACTION_DOWN: {
+            int touchMode = mTouchMode;
+            if (touchMode == TOUCH_MODE_OVERFLING || touchMode == TOUCH_MODE_OVERSCROLL) {
+                mMotionCorrection = 0;
+                return true;
+            }
+
+            final int x = (int) ev.getX();
+            final int y = (int) ev.getY();
+            mActivePointerId = ev.getPointerId(0);
+
+            int motionPosition = findMotionRow(y);
+            if (touchMode != TOUCH_MODE_FLING && motionPosition >= 0) {
+                // User clicked on an actual view (and was not stopping a fling).
+                // Remember where the motion event started
+                v = getChildAt(motionPosition - mFirstPosition);
+                mMotionViewOriginalTop = v.getTop();
+                mMotionX = x;
+                mMotionY = y;
+                mMotionPosition = motionPosition;
+                mTouchMode = TOUCH_MODE_DOWN;
+                clearScrollingCache();
+            }
+            mLastY = Integer.MIN_VALUE;
+            initOrResetVelocityTracker();
+            mVelocityTracker.addMovement(ev);
+            mNestedYOffset = 0;
+            startNestedScroll(SCROLL_AXIS_VERTICAL);
+            if (touchMode == TOUCH_MODE_FLING) {
+                return true;
+            }
+            break;
+        }
+
+        case MotionEvent.ACTION_MOVE: {
+            switch (mTouchMode) {
+            case TOUCH_MODE_DOWN:
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == -1) {
+                    pointerIndex = 0;
+                    mActivePointerId = ev.getPointerId(pointerIndex);
+                }
+                final int y = (int) ev.getY(pointerIndex);
+                initVelocityTrackerIfNotExists();
+                mVelocityTracker.addMovement(ev);
+                if (startScrollIfNeeded((int) ev.getX(pointerIndex), y, null)) {
+                    return true;
+                }
+                break;
+            }
+            break;
+        }
+
+        case MotionEvent.ACTION_CANCEL:
+        case MotionEvent.ACTION_UP: {
+            mTouchMode = TOUCH_MODE_REST;
+            mActivePointerId = INVALID_POINTER;
+            recycleVelocityTracker();
+            reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+            stopNestedScroll();
+            break;
+        }
+
+        case MotionEvent.ACTION_POINTER_UP: {
+            onSecondaryPointerUp(ev);
+            break;
+        }
+        }
+
+        return false;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mMotionX = (int) ev.getX(newPointerIndex);
+            mMotionY = (int) ev.getY(newPointerIndex);
+            mMotionCorrection = 0;
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addTouchables(ArrayList<View> views) {
+        final int count = getChildCount();
+        final int firstPosition = mFirstPosition;
+        final ListAdapter adapter = mAdapter;
+
+        if (adapter == null) {
+            return;
+        }
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (adapter.isEnabled(firstPosition + i)) {
+                views.add(child);
+            }
+            child.addTouchables(views);
+        }
+    }
+
+    /**
+     * Fires an "on scroll state changed" event to the registered
+     * {@link android.widget.AbsListView.OnScrollListener}, if any. The state change
+     * is fired only if the specified state is different from the previously known state.
+     *
+     * @param newState The new scroll state.
+     */
+    void reportScrollStateChange(int newState) {
+        if (newState != mLastScrollState) {
+            if (mOnScrollListener != null) {
+                mLastScrollState = newState;
+                mOnScrollListener.onScrollStateChanged(this, newState);
+            }
+        }
+    }
+
+    /**
+     * Responsible for fling behavior. Use {@link #start(int)} to
+     * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+     * A FlingRunnable will keep re-posting itself until the fling is done.
+     *
+     */
+    private class FlingRunnable implements Runnable {
+        /**
+         * Tracks the decay of a fling scroll
+         */
+        private final OverScroller mScroller;
+
+        /**
+         * Y value reported by mScroller on the previous fling
+         */
+        private int mLastFlingY;
+
+        /**
+         * If true, {@link #endFling()} will not report scroll state change to
+         * {@link OnScrollListener#SCROLL_STATE_IDLE}.
+         */
+        private boolean mSuppressIdleStateChangeCall;
+
+        private final Runnable mCheckFlywheel = new Runnable() {
+            @Override
+            public void run() {
+                final int activeId = mActivePointerId;
+                final VelocityTracker vt = mVelocityTracker;
+                final OverScroller scroller = mScroller;
+                if (vt == null || activeId == INVALID_POINTER) {
+                    return;
+                }
+
+                vt.computeCurrentVelocity(1000, mMaximumVelocity);
+                final float yvel = -vt.getYVelocity(activeId);
+
+                if (Math.abs(yvel) >= mMinimumVelocity
+                        && scroller.isScrollingInDirection(0, yvel)) {
+                    // Keep the fling alive a little longer
+                    postDelayed(this, FLYWHEEL_TIMEOUT);
+                } else {
+                    endFling();
+                    mTouchMode = TOUCH_MODE_SCROLL;
+                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+                }
+            }
+        };
+
+        private static final int FLYWHEEL_TIMEOUT = 40; // milliseconds
+
+        FlingRunnable() {
+            mScroller = new OverScroller(getContext());
+        }
+
+        void start(int initialVelocity) {
+            int initialY = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+            mLastFlingY = initialY;
+            mScroller.setInterpolator(null);
+            mScroller.fling(0, initialY, 0, initialVelocity,
+                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+            mTouchMode = TOUCH_MODE_FLING;
+            mSuppressIdleStateChangeCall = false;
+            postOnAnimation(this);
+
+            if (PROFILE_FLINGING) {
+                if (!mFlingProfilingStarted) {
+                    Debug.startMethodTracing("AbsListViewFling");
+                    mFlingProfilingStarted = true;
+                }
+            }
+
+            if (mFlingStrictSpan == null) {
+                mFlingStrictSpan = StrictMode.enterCriticalSpan("AbsListView-fling");
+            }
+        }
+
+        void startSpringback() {
+            mSuppressIdleStateChangeCall = false;
+            if (mScroller.springBack(0, mScrollY, 0, 0, 0, 0)) {
+                mTouchMode = TOUCH_MODE_OVERFLING;
+                invalidate();
+                postOnAnimation(this);
+            } else {
+                mTouchMode = TOUCH_MODE_REST;
+                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+            }
+        }
+
+        void startOverfling(int initialVelocity) {
+            mScroller.setInterpolator(null);
+            mScroller.fling(0, mScrollY, 0, initialVelocity, 0, 0,
+                    Integer.MIN_VALUE, Integer.MAX_VALUE, 0, getHeight());
+            mTouchMode = TOUCH_MODE_OVERFLING;
+            mSuppressIdleStateChangeCall = false;
+            invalidate();
+            postOnAnimation(this);
+        }
+
+        void edgeReached(int delta) {
+            mScroller.notifyVerticalEdgeReached(mScrollY, 0, mOverflingDistance);
+            final int overscrollMode = getOverScrollMode();
+            if (overscrollMode == OVER_SCROLL_ALWAYS ||
+                    (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && !contentFits())) {
+                mTouchMode = TOUCH_MODE_OVERFLING;
+                final int vel = (int) mScroller.getCurrVelocity();
+                if (delta > 0) {
+                    mEdgeGlowTop.onAbsorb(vel);
+                } else {
+                    mEdgeGlowBottom.onAbsorb(vel);
+                }
+            } else {
+                mTouchMode = TOUCH_MODE_REST;
+                if (mPositionScroller != null) {
+                    mPositionScroller.stop();
+                }
+            }
+            invalidate();
+            postOnAnimation(this);
+        }
+
+        void startScroll(int distance, int duration, boolean linear,
+                boolean suppressEndFlingStateChangeCall) {
+            int initialY = distance < 0 ? Integer.MAX_VALUE : 0;
+            mLastFlingY = initialY;
+            mScroller.setInterpolator(linear ? sLinearInterpolator : null);
+            mScroller.startScroll(0, initialY, 0, distance, duration);
+            mTouchMode = TOUCH_MODE_FLING;
+            mSuppressIdleStateChangeCall = suppressEndFlingStateChangeCall;
+            postOnAnimation(this);
+        }
+
+        void endFling() {
+            mTouchMode = TOUCH_MODE_REST;
+
+            removeCallbacks(this);
+            removeCallbacks(mCheckFlywheel);
+
+            if (!mSuppressIdleStateChangeCall) {
+                reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+            }
+            clearScrollingCache();
+            mScroller.abortAnimation();
+
+            if (mFlingStrictSpan != null) {
+                mFlingStrictSpan.finish();
+                mFlingStrictSpan = null;
+            }
+        }
+
+        void flywheelTouch() {
+            postDelayed(mCheckFlywheel, FLYWHEEL_TIMEOUT);
+        }
+
+        @Override
+        public void run() {
+            switch (mTouchMode) {
+            default:
+                endFling();
+                return;
+
+            case TOUCH_MODE_SCROLL:
+                if (mScroller.isFinished()) {
+                    return;
+                }
+                // Fall through
+            case TOUCH_MODE_FLING: {
+                if (mDataChanged) {
+                    layoutChildren();
+                }
+
+                if (mItemCount == 0 || getChildCount() == 0) {
+                    endFling();
+                    return;
+                }
+
+                final OverScroller scroller = mScroller;
+                boolean more = scroller.computeScrollOffset();
+                final int y = scroller.getCurrY();
+
+                // Flip sign to convert finger direction to list items direction
+                // (e.g. finger moving down means list is moving towards the top)
+                int delta = mLastFlingY - y;
+
+                // Pretend that each frame of a fling scroll is a touch scroll
+                if (delta > 0) {
+                    // List is moving towards the top. Use first view as mMotionPosition
+                    mMotionPosition = mFirstPosition;
+                    final View firstView = getChildAt(0);
+                    mMotionViewOriginalTop = firstView.getTop();
+
+                    // Don't fling more than 1 screen
+                    delta = Math.min(getHeight() - mPaddingBottom - mPaddingTop - 1, delta);
+                } else {
+                    // List is moving towards the bottom. Use last view as mMotionPosition
+                    int offsetToLast = getChildCount() - 1;
+                    mMotionPosition = mFirstPosition + offsetToLast;
+
+                    final View lastView = getChildAt(offsetToLast);
+                    mMotionViewOriginalTop = lastView.getTop();
+
+                    // Don't fling more than 1 screen
+                    delta = Math.max(-(getHeight() - mPaddingBottom - mPaddingTop - 1), delta);
+                }
+
+                // Check to see if we have bumped into the scroll limit
+                View motionView = getChildAt(mMotionPosition - mFirstPosition);
+                int oldTop = 0;
+                if (motionView != null) {
+                    oldTop = motionView.getTop();
+                }
+
+                // Don't stop just because delta is zero (it could have been rounded)
+                final boolean atEdge = trackMotionScroll(delta, delta);
+                final boolean atEnd = atEdge && (delta != 0);
+                if (atEnd) {
+                    if (motionView != null) {
+                        // Tweak the scroll for how far we overshot
+                        int overshoot = -(delta - (motionView.getTop() - oldTop));
+                        overScrollBy(0, overshoot, 0, mScrollY, 0, 0,
+                                0, mOverflingDistance, false);
+                    }
+                    if (more) {
+                        edgeReached(delta);
+                    }
+                    break;
+                }
+
+                if (more && !atEnd) {
+                    if (atEdge) invalidate();
+                    mLastFlingY = y;
+                    postOnAnimation(this);
+                } else {
+                    endFling();
+
+                    if (PROFILE_FLINGING) {
+                        if (mFlingProfilingStarted) {
+                            Debug.stopMethodTracing();
+                            mFlingProfilingStarted = false;
+                        }
+
+                        if (mFlingStrictSpan != null) {
+                            mFlingStrictSpan.finish();
+                            mFlingStrictSpan = null;
+                        }
+                    }
+                }
+                break;
+            }
+
+            case TOUCH_MODE_OVERFLING: {
+                final OverScroller scroller = mScroller;
+                if (scroller.computeScrollOffset()) {
+                    final int scrollY = mScrollY;
+                    final int currY = scroller.getCurrY();
+                    final int deltaY = currY - scrollY;
+                    if (overScrollBy(0, deltaY, 0, scrollY, 0, 0,
+                            0, mOverflingDistance, false)) {
+                        final boolean crossDown = scrollY <= 0 && currY > 0;
+                        final boolean crossUp = scrollY >= 0 && currY < 0;
+                        if (crossDown || crossUp) {
+                            int velocity = (int) scroller.getCurrVelocity();
+                            if (crossUp) velocity = -velocity;
+
+                            // Don't flywheel from this; we're just continuing things.
+                            scroller.abortAnimation();
+                            start(velocity);
+                        } else {
+                            startSpringback();
+                        }
+                    } else {
+                        invalidate();
+                        postOnAnimation(this);
+                    }
+                } else {
+                    endFling();
+                }
+                break;
+            }
+            }
+        }
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     */
+    public void setFriction(float friction) {
+        if (mFlingRunnable == null) {
+            mFlingRunnable = new FlingRunnable();
+        }
+        mFlingRunnable.mScroller.setFriction(friction);
+    }
+
+    /**
+     * Sets a scale factor for the fling velocity. The initial scale
+     * factor is 1.0.
+     *
+     * @param scale The scale factor to multiply the velocity by.
+     */
+    public void setVelocityScale(float scale) {
+        mVelocityScale = scale;
+    }
+
+    /**
+     * Override this for better control over position scrolling.
+     */
+    AbsPositionScroller createPositionScroller() {
+        return new PositionScroller();
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will
+     * scroll such that the indicated position is displayed.
+     * @param position Scroll to this adapter position.
+     */
+    public void smoothScrollToPosition(int position) {
+        if (mPositionScroller == null) {
+            mPositionScroller = createPositionScroller();
+        }
+        mPositionScroller.start(position);
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will scroll
+     * such that the indicated position is displayed <code>offset</code> pixels below
+     * the top edge of the view. If this is impossible, (e.g. the offset would scroll
+     * the first or last item beyond the boundaries of the list) it will get as close
+     * as possible. The scroll will take <code>duration</code> milliseconds to complete.
+     *
+     * @param position Position to scroll to
+     * @param offset Desired distance in pixels of <code>position</code> from the top
+     *               of the view when scrolling is finished
+     * @param duration Number of milliseconds to use for the scroll
+     */
+    public void smoothScrollToPositionFromTop(int position, int offset, int duration) {
+        if (mPositionScroller == null) {
+            mPositionScroller = createPositionScroller();
+        }
+        mPositionScroller.startWithOffset(position, offset, duration);
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will scroll
+     * such that the indicated position is displayed <code>offset</code> pixels below
+     * the top edge of the view. If this is impossible, (e.g. the offset would scroll
+     * the first or last item beyond the boundaries of the list) it will get as close
+     * as possible.
+     *
+     * @param position Position to scroll to
+     * @param offset Desired distance in pixels of <code>position</code> from the top
+     *               of the view when scrolling is finished
+     */
+    public void smoothScrollToPositionFromTop(int position, int offset) {
+        if (mPositionScroller == null) {
+            mPositionScroller = createPositionScroller();
+        }
+        mPositionScroller.startWithOffset(position, offset);
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will
+     * scroll such that the indicated position is displayed, but it will
+     * stop early if scrolling further would scroll boundPosition out of
+     * view.
+     *
+     * @param position Scroll to this adapter position.
+     * @param boundPosition Do not scroll if it would move this adapter
+     *          position out of view.
+     */
+    public void smoothScrollToPosition(int position, int boundPosition) {
+        if (mPositionScroller == null) {
+            mPositionScroller = createPositionScroller();
+        }
+        mPositionScroller.start(position, boundPosition);
+    }
+
+    /**
+     * Smoothly scroll by distance pixels over duration milliseconds.
+     * @param distance Distance to scroll in pixels.
+     * @param duration Duration of the scroll animation in milliseconds.
+     */
+    public void smoothScrollBy(int distance, int duration) {
+        smoothScrollBy(distance, duration, false, false);
+    }
+
+    void smoothScrollBy(int distance, int duration, boolean linear,
+            boolean suppressEndFlingStateChangeCall) {
+        if (mFlingRunnable == null) {
+            mFlingRunnable = new FlingRunnable();
+        }
+
+        // No sense starting to scroll if we're not going anywhere
+        final int firstPos = mFirstPosition;
+        final int childCount = getChildCount();
+        final int lastPos = firstPos + childCount;
+        final int topLimit = getPaddingTop();
+        final int bottomLimit = getHeight() - getPaddingBottom();
+
+        if (distance == 0 || mItemCount == 0 || childCount == 0 ||
+                (firstPos == 0 && getChildAt(0).getTop() == topLimit && distance < 0) ||
+                (lastPos == mItemCount &&
+                        getChildAt(childCount - 1).getBottom() == bottomLimit && distance > 0)) {
+            mFlingRunnable.endFling();
+            if (mPositionScroller != null) {
+                mPositionScroller.stop();
+            }
+        } else {
+            reportScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+            mFlingRunnable.startScroll(distance, duration, linear, suppressEndFlingStateChangeCall);
+        }
+    }
+
+    /**
+     * Allows RemoteViews to scroll relatively to a position.
+     */
+    void smoothScrollByOffset(int position) {
+        int index = -1;
+        if (position < 0) {
+            index = getFirstVisiblePosition();
+        } else if (position > 0) {
+            index = getLastVisiblePosition();
+        }
+
+        if (index > -1) {
+            View child = getChildAt(index - getFirstVisiblePosition());
+            if (child != null) {
+                Rect visibleRect = new Rect();
+                if (child.getGlobalVisibleRect(visibleRect)) {
+                    // the child is partially visible
+                    int childRectArea = child.getWidth() * child.getHeight();
+                    int visibleRectArea = visibleRect.width() * visibleRect.height();
+                    float visibleArea = (visibleRectArea / (float) childRectArea);
+                    final float visibleThreshold = 0.75f;
+                    if ((position < 0) && (visibleArea < visibleThreshold)) {
+                        // the top index is not perceivably visible so offset
+                        // to account for showing that top index as well
+                        ++index;
+                    } else if ((position > 0) && (visibleArea < visibleThreshold)) {
+                        // the bottom index is not perceivably visible so offset
+                        // to account for showing that bottom index as well
+                        --index;
+                    }
+                }
+                smoothScrollToPosition(Math.max(0, Math.min(getCount(), index + position)));
+            }
+        }
+    }
+
+    private void createScrollingCache() {
+        if (mScrollingCacheEnabled && !mCachingStarted && !isHardwareAccelerated()) {
+            setChildrenDrawnWithCacheEnabled(true);
+            setChildrenDrawingCacheEnabled(true);
+            mCachingStarted = mCachingActive = true;
+        }
+    }
+
+    private void clearScrollingCache() {
+        if (!isHardwareAccelerated()) {
+            if (mClearScrollingCache == null) {
+                mClearScrollingCache = new Runnable() {
+                    @Override
+                    public void run() {
+                        if (mCachingStarted) {
+                            mCachingStarted = mCachingActive = false;
+                            setChildrenDrawnWithCacheEnabled(false);
+                            if ((mPersistentDrawingCache & PERSISTENT_SCROLLING_CACHE) == 0) {
+                                setChildrenDrawingCacheEnabled(false);
+                            }
+                            if (!isAlwaysDrawnWithCacheEnabled()) {
+                                invalidate();
+                            }
+                        }
+                    }
+                };
+            }
+            post(mClearScrollingCache);
+        }
+    }
+
+    /**
+     * Scrolls the list items within the view by a specified number of pixels.
+     *
+     * <p>The actual amount of scroll is capped by the list content viewport height
+     * which is the list height minus top and bottom paddings minus one pixel.</p>
+     *
+     * @param y the amount of pixels to scroll by vertically
+     * @see #canScrollList(int)
+     */
+    public void scrollListBy(int y) {
+        trackMotionScroll(-y, -y);
+    }
+
+    /**
+     * Check if the items in the list can be scrolled in a certain direction.
+     *
+     * @param direction Negative to check scrolling up, positive to check
+     *            scrolling down.
+     * @return true if the list can be scrolled in the specified direction,
+     *         false otherwise.
+     * @see #scrollListBy(int)
+     */
+    public boolean canScrollList(int direction) {
+        final int childCount = getChildCount();
+        if (childCount == 0) {
+            return false;
+        }
+
+        final int firstPosition = mFirstPosition;
+        final Rect listPadding = mListPadding;
+        if (direction > 0) {
+            final int lastBottom = getChildAt(childCount - 1).getBottom();
+            final int lastPosition = firstPosition + childCount;
+            return lastPosition < mItemCount || lastBottom > getHeight() - listPadding.bottom;
+        } else {
+            final int firstTop = getChildAt(0).getTop();
+            return firstPosition > 0 || firstTop < listPadding.top;
+        }
+    }
+
+    /**
+     * Track a motion scroll
+     *
+     * @param deltaY Amount to offset mMotionView. This is the accumulated delta since the motion
+     *        began. Positive numbers mean the user's finger is moving down the screen.
+     * @param incrementalDeltaY Change in deltaY from the previous event.
+     * @return true if we're already at the beginning/end of the list and have nothing to do.
+     */
+    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
+        final int childCount = getChildCount();
+        if (childCount == 0) {
+            return true;
+        }
+
+        final int firstTop = getChildAt(0).getTop();
+        final int lastBottom = getChildAt(childCount - 1).getBottom();
+
+        final Rect listPadding = mListPadding;
+
+        // "effective padding" In this case is the amount of padding that affects
+        // how much space should not be filled by items. If we don't clip to padding
+        // there is no effective padding.
+        int effectivePaddingTop = 0;
+        int effectivePaddingBottom = 0;
+        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+            effectivePaddingTop = listPadding.top;
+            effectivePaddingBottom = listPadding.bottom;
+        }
+
+         // FIXME account for grid vertical spacing too?
+        final int spaceAbove = effectivePaddingTop - firstTop;
+        final int end = getHeight() - effectivePaddingBottom;
+        final int spaceBelow = lastBottom - end;
+
+        final int height = getHeight() - mPaddingBottom - mPaddingTop;
+        if (deltaY < 0) {
+            deltaY = Math.max(-(height - 1), deltaY);
+        } else {
+            deltaY = Math.min(height - 1, deltaY);
+        }
+
+        if (incrementalDeltaY < 0) {
+            incrementalDeltaY = Math.max(-(height - 1), incrementalDeltaY);
+        } else {
+            incrementalDeltaY = Math.min(height - 1, incrementalDeltaY);
+        }
+
+        final int firstPosition = mFirstPosition;
+
+        // Update our guesses for where the first and last views are
+        if (firstPosition == 0) {
+            mFirstPositionDistanceGuess = firstTop - listPadding.top;
+        } else {
+            mFirstPositionDistanceGuess += incrementalDeltaY;
+        }
+        if (firstPosition + childCount == mItemCount) {
+            mLastPositionDistanceGuess = lastBottom + listPadding.bottom;
+        } else {
+            mLastPositionDistanceGuess += incrementalDeltaY;
+        }
+
+        final boolean cannotScrollDown = (firstPosition == 0 &&
+                firstTop >= listPadding.top && incrementalDeltaY >= 0);
+        final boolean cannotScrollUp = (firstPosition + childCount == mItemCount &&
+                lastBottom <= getHeight() - listPadding.bottom && incrementalDeltaY <= 0);
+
+        if (cannotScrollDown || cannotScrollUp) {
+            return incrementalDeltaY != 0;
+        }
+
+        final boolean down = incrementalDeltaY < 0;
+
+        final boolean inTouchMode = isInTouchMode();
+        if (inTouchMode) {
+            hideSelector();
+        }
+
+        final int headerViewsCount = getHeaderViewsCount();
+        final int footerViewsStart = mItemCount - getFooterViewsCount();
+
+        int start = 0;
+        int count = 0;
+
+        if (down) {
+            int top = -incrementalDeltaY;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                top += listPadding.top;
+            }
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                if (child.getBottom() >= top) {
+                    break;
+                } else {
+                    count++;
+                    int position = firstPosition + i;
+                    if (position >= headerViewsCount && position < footerViewsStart) {
+                        // The view will be rebound to new data, clear any
+                        // system-managed transient state.
+                        child.clearAccessibilityFocus();
+                        mRecycler.addScrapView(child, position);
+                    }
+                }
+            }
+        } else {
+            int bottom = getHeight() - incrementalDeltaY;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                bottom -= listPadding.bottom;
+            }
+            for (int i = childCount - 1; i >= 0; i--) {
+                final View child = getChildAt(i);
+                if (child.getTop() <= bottom) {
+                    break;
+                } else {
+                    start = i;
+                    count++;
+                    int position = firstPosition + i;
+                    if (position >= headerViewsCount && position < footerViewsStart) {
+                        // The view will be rebound to new data, clear any
+                        // system-managed transient state.
+                        child.clearAccessibilityFocus();
+                        mRecycler.addScrapView(child, position);
+                    }
+                }
+            }
+        }
+
+        mMotionViewNewTop = mMotionViewOriginalTop + deltaY;
+
+        mBlockLayoutRequests = true;
+
+        if (count > 0) {
+            detachViewsFromParent(start, count);
+            mRecycler.removeSkippedScrap();
+        }
+
+        // invalidate before moving the children to avoid unnecessary invalidate
+        // calls to bubble up from the children all the way to the top
+        if (!awakenScrollBars()) {
+           invalidate();
+        }
+
+        offsetChildrenTopAndBottom(incrementalDeltaY);
+
+        if (down) {
+            mFirstPosition += count;
+        }
+
+        final int absIncrementalDeltaY = Math.abs(incrementalDeltaY);
+        if (spaceAbove < absIncrementalDeltaY || spaceBelow < absIncrementalDeltaY) {
+            fillGap(down);
+        }
+
+        mRecycler.fullyDetachScrapViews();
+        if (!inTouchMode && mSelectedPosition != INVALID_POSITION) {
+            final int childIndex = mSelectedPosition - mFirstPosition;
+            if (childIndex >= 0 && childIndex < getChildCount()) {
+                positionSelector(mSelectedPosition, getChildAt(childIndex));
+            }
+        } else if (mSelectorPosition != INVALID_POSITION) {
+            final int childIndex = mSelectorPosition - mFirstPosition;
+            if (childIndex >= 0 && childIndex < getChildCount()) {
+                positionSelector(INVALID_POSITION, getChildAt(childIndex));
+            }
+        } else {
+            mSelectorRect.setEmpty();
+        }
+
+        mBlockLayoutRequests = false;
+
+        invokeOnItemScrollListener();
+
+        return false;
+    }
+
+    /**
+     * Returns the number of header views in the list. Header views are special views
+     * at the top of the list that should not be recycled during a layout.
+     *
+     * @return The number of header views, 0 in the default implementation.
+     */
+    int getHeaderViewsCount() {
+        return 0;
+    }
+
+    /**
+     * Returns the number of footer views in the list. Footer views are special views
+     * at the bottom of the list that should not be recycled during a layout.
+     *
+     * @return The number of footer views, 0 in the default implementation.
+     */
+    int getFooterViewsCount() {
+        return 0;
+    }
+
+    /**
+     * Fills the gap left open by a touch-scroll. During a touch scroll, children that
+     * remain on screen are shifted and the other ones are discarded. The role of this
+     * method is to fill the gap thus created by performing a partial layout in the
+     * empty space.
+     *
+     * @param down true if the scroll is going down, false if it is going up
+     */
+    abstract void fillGap(boolean down);
+
+    void hideSelector() {
+        if (mSelectedPosition != INVALID_POSITION) {
+            if (mLayoutMode != LAYOUT_SPECIFIC) {
+                mResurrectToPosition = mSelectedPosition;
+            }
+            if (mNextSelectedPosition >= 0 && mNextSelectedPosition != mSelectedPosition) {
+                mResurrectToPosition = mNextSelectedPosition;
+            }
+            setSelectedPositionInt(INVALID_POSITION);
+            setNextSelectedPositionInt(INVALID_POSITION);
+            mSelectedTop = 0;
+        }
+    }
+
+    /**
+     * @return A position to select. First we try mSelectedPosition. If that has been clobbered by
+     * entering touch mode, we then try mResurrectToPosition. Values are pinned to the range
+     * of items available in the adapter
+     */
+    int reconcileSelectedPosition() {
+        int position = mSelectedPosition;
+        if (position < 0) {
+            position = mResurrectToPosition;
+        }
+        position = Math.max(0, position);
+        position = Math.min(position, mItemCount - 1);
+        return position;
+    }
+
+    /**
+     * Find the row closest to y. This row will be used as the motion row when scrolling
+     *
+     * @param y Where the user touched
+     * @return The position of the first (or only) item in the row containing y
+     */
+    abstract int findMotionRow(int y);
+
+    /**
+     * Find the row closest to y. This row will be used as the motion row when scrolling.
+     *
+     * @param y Where the user touched
+     * @return The position of the first (or only) item in the row closest to y
+     */
+    int findClosestMotionRow(int y) {
+        final int childCount = getChildCount();
+        if (childCount == 0) {
+            return INVALID_POSITION;
+        }
+
+        final int motionRow = findMotionRow(y);
+        return motionRow != INVALID_POSITION ? motionRow : mFirstPosition + childCount - 1;
+    }
+
+    /**
+     * Causes all the views to be rebuilt and redrawn.
+     */
+    public void invalidateViews() {
+        mDataChanged = true;
+        rememberSyncState();
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * If there is a selection returns false.
+     * Otherwise resurrects the selection and returns true if resurrected.
+     */
+    boolean resurrectSelectionIfNeeded() {
+        if (mSelectedPosition < 0 && resurrectSelection()) {
+            updateSelectorState();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Makes the item at the supplied position selected.
+     *
+     * @param position the position of the new selection
+     */
+    abstract void setSelectionInt(int position);
+
+    /**
+     * Attempt to bring the selection back if the user is switching from touch
+     * to trackball mode
+     * @return Whether selection was set to something.
+     */
+    boolean resurrectSelection() {
+        final int childCount = getChildCount();
+
+        if (childCount <= 0) {
+            return false;
+        }
+
+        int selectedTop = 0;
+        int selectedPos;
+        int childrenTop = mListPadding.top;
+        int childrenBottom = mBottom - mTop - mListPadding.bottom;
+        final int firstPosition = mFirstPosition;
+        final int toPosition = mResurrectToPosition;
+        boolean down = true;
+
+        if (toPosition >= firstPosition && toPosition < firstPosition + childCount) {
+            selectedPos = toPosition;
+
+            final View selected = getChildAt(selectedPos - mFirstPosition);
+            selectedTop = selected.getTop();
+            int selectedBottom = selected.getBottom();
+
+            // We are scrolled, don't get in the fade
+            if (selectedTop < childrenTop) {
+                selectedTop = childrenTop + getVerticalFadingEdgeLength();
+            } else if (selectedBottom > childrenBottom) {
+                selectedTop = childrenBottom - selected.getMeasuredHeight()
+                        - getVerticalFadingEdgeLength();
+            }
+        } else {
+            if (toPosition < firstPosition) {
+                // Default to selecting whatever is first
+                selectedPos = firstPosition;
+                for (int i = 0; i < childCount; i++) {
+                    final View v = getChildAt(i);
+                    final int top = v.getTop();
+
+                    if (i == 0) {
+                        // Remember the position of the first item
+                        selectedTop = top;
+                        // See if we are scrolled at all
+                        if (firstPosition > 0 || top < childrenTop) {
+                            // If we are scrolled, don't select anything that is
+                            // in the fade region
+                            childrenTop += getVerticalFadingEdgeLength();
+                        }
+                    }
+                    if (top >= childrenTop) {
+                        // Found a view whose top is fully visisble
+                        selectedPos = firstPosition + i;
+                        selectedTop = top;
+                        break;
+                    }
+                }
+            } else {
+                final int itemCount = mItemCount;
+                down = false;
+                selectedPos = firstPosition + childCount - 1;
+
+                for (int i = childCount - 1; i >= 0; i--) {
+                    final View v = getChildAt(i);
+                    final int top = v.getTop();
+                    final int bottom = v.getBottom();
+
+                    if (i == childCount - 1) {
+                        selectedTop = top;
+                        if (firstPosition + childCount < itemCount || bottom > childrenBottom) {
+                            childrenBottom -= getVerticalFadingEdgeLength();
+                        }
+                    }
+
+                    if (bottom <= childrenBottom) {
+                        selectedPos = firstPosition + i;
+                        selectedTop = top;
+                        break;
+                    }
+                }
+            }
+        }
+
+        mResurrectToPosition = INVALID_POSITION;
+        removeCallbacks(mFlingRunnable);
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+        mTouchMode = TOUCH_MODE_REST;
+        clearScrollingCache();
+        mSpecificTop = selectedTop;
+        selectedPos = lookForSelectablePosition(selectedPos, down);
+        if (selectedPos >= firstPosition && selectedPos <= getLastVisiblePosition()) {
+            mLayoutMode = LAYOUT_SPECIFIC;
+            updateSelectorState();
+            setSelectionInt(selectedPos);
+            invokeOnItemScrollListener();
+        } else {
+            selectedPos = INVALID_POSITION;
+        }
+        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+
+        return selectedPos >= 0;
+    }
+
+    void confirmCheckedPositionsById() {
+        // Clear out the positional check states, we'll rebuild it below from IDs.
+        mCheckStates.clear();
+
+        boolean checkedCountChanged = false;
+        for (int checkedIndex = 0; checkedIndex < mCheckedIdStates.size(); checkedIndex++) {
+            final long id = mCheckedIdStates.keyAt(checkedIndex);
+            final int lastPos = mCheckedIdStates.valueAt(checkedIndex);
+
+            final long lastPosId = mAdapter.getItemId(lastPos);
+            if (id != lastPosId) {
+                // Look around to see if the ID is nearby. If not, uncheck it.
+                final int start = Math.max(0, lastPos - CHECK_POSITION_SEARCH_DISTANCE);
+                final int end = Math.min(lastPos + CHECK_POSITION_SEARCH_DISTANCE, mItemCount);
+                boolean found = false;
+                for (int searchPos = start; searchPos < end; searchPos++) {
+                    final long searchId = mAdapter.getItemId(searchPos);
+                    if (id == searchId) {
+                        found = true;
+                        mCheckStates.put(searchPos, true);
+                        mCheckedIdStates.setValueAt(checkedIndex, searchPos);
+                        break;
+                    }
+                }
+
+                if (!found) {
+                    mCheckedIdStates.delete(id);
+                    checkedIndex--;
+                    mCheckedItemCount--;
+                    checkedCountChanged = true;
+                    if (mChoiceActionMode != null && mMultiChoiceModeCallback != null) {
+                        mMultiChoiceModeCallback.onItemCheckedStateChanged(mChoiceActionMode,
+                                lastPos, id, false);
+                    }
+                }
+            } else {
+                mCheckStates.put(lastPos, true);
+            }
+        }
+
+        if (checkedCountChanged && mChoiceActionMode != null) {
+            mChoiceActionMode.invalidate();
+        }
+    }
+
+    @Override
+    protected void handleDataChanged() {
+        int count = mItemCount;
+        int lastHandledItemCount = mLastHandledItemCount;
+        mLastHandledItemCount = mItemCount;
+
+        if (mChoiceMode != CHOICE_MODE_NONE && mAdapter != null && mAdapter.hasStableIds()) {
+            confirmCheckedPositionsById();
+        }
+
+        // TODO: In the future we can recycle these views based on stable ID instead.
+        mRecycler.clearTransientStateViews();
+
+        if (count > 0) {
+            int newPos;
+            int selectablePos;
+
+            // Find the row we are supposed to sync to
+            if (mNeedSync) {
+                // Update this first, since setNextSelectedPositionInt inspects it
+                mNeedSync = false;
+                mPendingSync = null;
+
+                if (mTranscriptMode == TRANSCRIPT_MODE_ALWAYS_SCROLL) {
+                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
+                    return;
+                } else if (mTranscriptMode == TRANSCRIPT_MODE_NORMAL) {
+                    if (mForceTranscriptScroll) {
+                        mForceTranscriptScroll = false;
+                        mLayoutMode = LAYOUT_FORCE_BOTTOM;
+                        return;
+                    }
+                    final int childCount = getChildCount();
+                    final int listBottom = getHeight() - getPaddingBottom();
+                    final View lastChild = getChildAt(childCount - 1);
+                    final int lastBottom = lastChild != null ? lastChild.getBottom() : listBottom;
+                    if (mFirstPosition + childCount >= lastHandledItemCount &&
+                            lastBottom <= listBottom) {
+                        mLayoutMode = LAYOUT_FORCE_BOTTOM;
+                        return;
+                    }
+                    // Something new came in and we didn't scroll; give the user a clue that
+                    // there's something new.
+                    awakenScrollBars();
+                }
+
+                switch (mSyncMode) {
+                case SYNC_SELECTED_POSITION:
+                    if (isInTouchMode()) {
+                        // We saved our state when not in touch mode. (We know this because
+                        // mSyncMode is SYNC_SELECTED_POSITION.) Now we are trying to
+                        // restore in touch mode. Just leave mSyncPosition as it is (possibly
+                        // adjusting if the available range changed) and return.
+                        mLayoutMode = LAYOUT_SYNC;
+                        mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
+
+                        return;
+                    } else {
+                        // See if we can find a position in the new data with the same
+                        // id as the old selection. This will change mSyncPosition.
+                        newPos = findSyncPosition();
+                        if (newPos >= 0) {
+                            // Found it. Now verify that new selection is still selectable
+                            selectablePos = lookForSelectablePosition(newPos, true);
+                            if (selectablePos == newPos) {
+                                // Same row id is selected
+                                mSyncPosition = newPos;
+
+                                if (mSyncHeight == getHeight()) {
+                                    // If we are at the same height as when we saved state, try
+                                    // to restore the scroll position too.
+                                    mLayoutMode = LAYOUT_SYNC;
+                                } else {
+                                    // We are not the same height as when the selection was saved, so
+                                    // don't try to restore the exact position
+                                    mLayoutMode = LAYOUT_SET_SELECTION;
+                                }
+
+                                // Restore selection
+                                setNextSelectedPositionInt(newPos);
+                                return;
+                            }
+                        }
+                    }
+                    break;
+                case SYNC_FIRST_POSITION:
+                    // Leave mSyncPosition as it is -- just pin to available range
+                    mLayoutMode = LAYOUT_SYNC;
+                    mSyncPosition = Math.min(Math.max(0, mSyncPosition), count - 1);
+
+                    return;
+                }
+            }
+
+            if (!isInTouchMode()) {
+                // We couldn't find matching data -- try to use the same position
+                newPos = getSelectedItemPosition();
+
+                // Pin position to the available range
+                if (newPos >= count) {
+                    newPos = count - 1;
+                }
+                if (newPos < 0) {
+                    newPos = 0;
+                }
+
+                // Make sure we select something selectable -- first look down
+                selectablePos = lookForSelectablePosition(newPos, true);
+
+                if (selectablePos >= 0) {
+                    setNextSelectedPositionInt(selectablePos);
+                    return;
+                } else {
+                    // Looking down didn't work -- try looking up
+                    selectablePos = lookForSelectablePosition(newPos, false);
+                    if (selectablePos >= 0) {
+                        setNextSelectedPositionInt(selectablePos);
+                        return;
+                    }
+                }
+            } else {
+
+                // We already know where we want to resurrect the selection
+                if (mResurrectToPosition >= 0) {
+                    return;
+                }
+            }
+
+        }
+
+        // Nothing is selected. Give up and reset everything.
+        mLayoutMode = mStackFromBottom ? LAYOUT_FORCE_BOTTOM : LAYOUT_FORCE_TOP;
+        mSelectedPosition = INVALID_POSITION;
+        mSelectedRowId = INVALID_ROW_ID;
+        mNextSelectedPosition = INVALID_POSITION;
+        mNextSelectedRowId = INVALID_ROW_ID;
+        mNeedSync = false;
+        mPendingSync = null;
+        mSelectorPosition = INVALID_POSITION;
+        checkSelectionChanged();
+    }
+
+    @Override
+    protected void onDisplayHint(int hint) {
+        super.onDisplayHint(hint);
+        switch (hint) {
+            case INVISIBLE:
+                if (mPopup != null && mPopup.isShowing()) {
+                    dismissPopup();
+                }
+                break;
+            case VISIBLE:
+                if (mFiltered && mPopup != null && !mPopup.isShowing()) {
+                    showPopup();
+                }
+                break;
+        }
+        mPopupHidden = hint == INVISIBLE;
+    }
+
+    /**
+     * Removes the filter window
+     */
+    private void dismissPopup() {
+        if (mPopup != null) {
+            mPopup.dismiss();
+        }
+    }
+
+    /**
+     * Shows the filter window
+     */
+    private void showPopup() {
+        // Make sure we have a window before showing the popup
+        if (getWindowVisibility() == View.VISIBLE) {
+            createTextFilter(true);
+            positionPopup();
+            // Make sure we get focus if we are showing the popup
+            checkFocus();
+        }
+    }
+
+    private void positionPopup() {
+        int screenHeight = getResources().getDisplayMetrics().heightPixels;
+        final int[] xy = new int[2];
+        getLocationOnScreen(xy);
+        // TODO: The 20 below should come from the theme
+        // TODO: And the gravity should be defined in the theme as well
+        final int bottomGap = screenHeight - xy[1] - getHeight() + (int) (mDensityScale * 20);
+        if (!mPopup.isShowing()) {
+            mPopup.showAtLocation(this, Gravity.BOTTOM | Gravity.CENTER_HORIZONTAL,
+                    xy[0], bottomGap);
+        } else {
+            mPopup.update(xy[0], bottomGap, -1, -1);
+        }
+    }
+
+    /**
+     * What is the distance between the source and destination rectangles given the direction of
+     * focus navigation between them? The direction basically helps figure out more quickly what is
+     * self evident by the relationship between the rects...
+     *
+     * @param source the source rectangle
+     * @param dest the destination rectangle
+     * @param direction the direction
+     * @return the distance between the rectangles
+     */
+    static int getDistance(Rect source, Rect dest, int direction) {
+        int sX, sY; // source x, y
+        int dX, dY; // dest x, y
+        switch (direction) {
+        case View.FOCUS_RIGHT:
+            sX = source.right;
+            sY = source.top + source.height() / 2;
+            dX = dest.left;
+            dY = dest.top + dest.height() / 2;
+            break;
+        case View.FOCUS_DOWN:
+            sX = source.left + source.width() / 2;
+            sY = source.bottom;
+            dX = dest.left + dest.width() / 2;
+            dY = dest.top;
+            break;
+        case View.FOCUS_LEFT:
+            sX = source.left;
+            sY = source.top + source.height() / 2;
+            dX = dest.right;
+            dY = dest.top + dest.height() / 2;
+            break;
+        case View.FOCUS_UP:
+            sX = source.left + source.width() / 2;
+            sY = source.top;
+            dX = dest.left + dest.width() / 2;
+            dY = dest.bottom;
+            break;
+        case View.FOCUS_FORWARD:
+        case View.FOCUS_BACKWARD:
+            sX = source.right + source.width() / 2;
+            sY = source.top + source.height() / 2;
+            dX = dest.left + dest.width() / 2;
+            dY = dest.top + dest.height() / 2;
+            break;
+        default:
+            throw new IllegalArgumentException("direction must be one of "
+                    + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+                    + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
+        }
+        int deltaX = dX - sX;
+        int deltaY = dY - sY;
+        return deltaY * deltaY + deltaX * deltaX;
+    }
+
+    @Override
+    protected boolean isInFilterMode() {
+        return mFiltered;
+    }
+
+    /**
+     * Sends a key to the text filter window
+     *
+     * @param keyCode The keycode for the event
+     * @param event The actual key event
+     *
+     * @return True if the text filter handled the event, false otherwise.
+     */
+    boolean sendToTextFilter(int keyCode, int count, KeyEvent event) {
+        if (!acceptFilter()) {
+            return false;
+        }
+
+        boolean handled = false;
+        boolean okToSend = true;
+        switch (keyCode) {
+        case KeyEvent.KEYCODE_DPAD_UP:
+        case KeyEvent.KEYCODE_DPAD_DOWN:
+        case KeyEvent.KEYCODE_DPAD_LEFT:
+        case KeyEvent.KEYCODE_DPAD_RIGHT:
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+        case KeyEvent.KEYCODE_ENTER:
+            okToSend = false;
+            break;
+        case KeyEvent.KEYCODE_BACK:
+            if (mFiltered && mPopup != null && mPopup.isShowing()) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN
+                        && event.getRepeatCount() == 0) {
+                    KeyEvent.DispatcherState state = getKeyDispatcherState();
+                    if (state != null) {
+                        state.startTracking(event, this);
+                    }
+                    handled = true;
+                } else if (event.getAction() == KeyEvent.ACTION_UP
+                        && event.isTracking() && !event.isCanceled()) {
+                    handled = true;
+                    mTextFilter.setText("");
+                }
+            }
+            okToSend = false;
+            break;
+        case KeyEvent.KEYCODE_SPACE:
+            // Only send spaces once we are filtered
+            okToSend = mFiltered;
+            break;
+        }
+
+        if (okToSend) {
+            createTextFilter(true);
+
+            KeyEvent forwardEvent = event;
+            if (forwardEvent.getRepeatCount() > 0) {
+                forwardEvent = KeyEvent.changeTimeRepeat(event, event.getEventTime(), 0);
+            }
+
+            int action = event.getAction();
+            switch (action) {
+                case KeyEvent.ACTION_DOWN:
+                    handled = mTextFilter.onKeyDown(keyCode, forwardEvent);
+                    break;
+
+                case KeyEvent.ACTION_UP:
+                    handled = mTextFilter.onKeyUp(keyCode, forwardEvent);
+                    break;
+
+                case KeyEvent.ACTION_MULTIPLE:
+                    handled = mTextFilter.onKeyMultiple(keyCode, count, event);
+                    break;
+            }
+        }
+        return handled;
+    }
+
+    /**
+     * Return an InputConnection for editing of the filter text.
+     */
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        if (isTextFilterEnabled()) {
+            if (mPublicInputConnection == null) {
+                mDefInputConnection = new BaseInputConnection(this, false);
+                mPublicInputConnection = new InputConnectionWrapper(outAttrs);
+            }
+            outAttrs.inputType = EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_FILTER;
+            outAttrs.imeOptions = EditorInfo.IME_ACTION_DONE;
+            return mPublicInputConnection;
+        }
+        return null;
+    }
+
+    private class InputConnectionWrapper implements InputConnection {
+        private final EditorInfo mOutAttrs;
+        private InputConnection mTarget;
+
+        public InputConnectionWrapper(EditorInfo outAttrs) {
+            mOutAttrs = outAttrs;
+        }
+
+        private InputConnection getTarget() {
+            if (mTarget == null) {
+                mTarget = getTextFilterInput().onCreateInputConnection(mOutAttrs);
+            }
+            return mTarget;
+        }
+
+        @Override
+        public boolean reportFullscreenMode(boolean enabled) {
+            // Use our own input connection, since it is
+            // the "real" one the IME is talking with.
+            return mDefInputConnection.reportFullscreenMode(enabled);
+        }
+
+        @Override
+        public boolean performEditorAction(int editorAction) {
+            // The editor is off in its own window; we need to be
+            // the one that does this.
+            if (editorAction == EditorInfo.IME_ACTION_DONE) {
+                InputMethodManager imm =
+                        getContext().getSystemService(InputMethodManager.class);
+                if (imm != null) {
+                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                }
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean sendKeyEvent(KeyEvent event) {
+            // Use our own input connection, since the filter
+            // text view may not be shown in a window so has
+            // no ViewAncestor to dispatch events with.
+            return mDefInputConnection.sendKeyEvent(event);
+        }
+
+        @Override
+        public CharSequence getTextBeforeCursor(int n, int flags) {
+            if (mTarget == null) return "";
+            return mTarget.getTextBeforeCursor(n, flags);
+        }
+
+        @Override
+        public CharSequence getTextAfterCursor(int n, int flags) {
+            if (mTarget == null) return "";
+            return mTarget.getTextAfterCursor(n, flags);
+        }
+
+        @Override
+        public CharSequence getSelectedText(int flags) {
+            if (mTarget == null) return "";
+            return mTarget.getSelectedText(flags);
+        }
+
+        @Override
+        public int getCursorCapsMode(int reqModes) {
+            if (mTarget == null) return InputType.TYPE_TEXT_FLAG_CAP_SENTENCES;
+            return mTarget.getCursorCapsMode(reqModes);
+        }
+
+        @Override
+        public ExtractedText getExtractedText(ExtractedTextRequest request, int flags) {
+            return getTarget().getExtractedText(request, flags);
+        }
+
+        @Override
+        public boolean deleteSurroundingText(int beforeLength, int afterLength) {
+            return getTarget().deleteSurroundingText(beforeLength, afterLength);
+        }
+
+        @Override
+        public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) {
+            return getTarget().deleteSurroundingTextInCodePoints(beforeLength, afterLength);
+        }
+
+        @Override
+        public boolean setComposingText(CharSequence text, int newCursorPosition) {
+            return getTarget().setComposingText(text, newCursorPosition);
+        }
+
+        @Override
+        public boolean setComposingRegion(int start, int end) {
+            return getTarget().setComposingRegion(start, end);
+        }
+
+        @Override
+        public boolean finishComposingText() {
+            return mTarget == null || mTarget.finishComposingText();
+        }
+
+        @Override
+        public boolean commitText(CharSequence text, int newCursorPosition) {
+            return getTarget().commitText(text, newCursorPosition);
+        }
+
+        @Override
+        public boolean commitCompletion(CompletionInfo text) {
+            return getTarget().commitCompletion(text);
+        }
+
+        @Override
+        public boolean commitCorrection(CorrectionInfo correctionInfo) {
+            return getTarget().commitCorrection(correctionInfo);
+        }
+
+        @Override
+        public boolean setSelection(int start, int end) {
+            return getTarget().setSelection(start, end);
+        }
+
+        @Override
+        public boolean performContextMenuAction(int id) {
+            return getTarget().performContextMenuAction(id);
+        }
+
+        @Override
+        public boolean beginBatchEdit() {
+            return getTarget().beginBatchEdit();
+        }
+
+        @Override
+        public boolean endBatchEdit() {
+            return getTarget().endBatchEdit();
+        }
+
+        @Override
+        public boolean clearMetaKeyStates(int states) {
+            return getTarget().clearMetaKeyStates(states);
+        }
+
+        @Override
+        public boolean performPrivateCommand(String action, Bundle data) {
+            return getTarget().performPrivateCommand(action, data);
+        }
+
+        @Override
+        public boolean requestCursorUpdates(int cursorUpdateMode) {
+            return getTarget().requestCursorUpdates(cursorUpdateMode);
+        }
+
+        @Override
+        public Handler getHandler() {
+            return getTarget().getHandler();
+        }
+
+        @Override
+        public void closeConnection() {
+            getTarget().closeConnection();
+        }
+
+        @Override
+        public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) {
+            return getTarget().commitContent(inputContentInfo, flags, opts);
+        }
+    }
+
+    /**
+     * For filtering we proxy an input connection to an internal text editor,
+     * and this allows the proxying to happen.
+     */
+    @Override
+    public boolean checkInputConnectionProxy(View view) {
+        return view == mTextFilter;
+    }
+
+    /**
+     * Creates the window for the text filter and populates it with an EditText field;
+     *
+     * @param animateEntrance true if the window should appear with an animation
+     */
+    private void createTextFilter(boolean animateEntrance) {
+        if (mPopup == null) {
+            PopupWindow p = new PopupWindow(getContext());
+            p.setFocusable(false);
+            p.setTouchable(false);
+            p.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+            p.setContentView(getTextFilterInput());
+            p.setWidth(LayoutParams.WRAP_CONTENT);
+            p.setHeight(LayoutParams.WRAP_CONTENT);
+            p.setBackgroundDrawable(null);
+            mPopup = p;
+            getViewTreeObserver().addOnGlobalLayoutListener(this);
+            mGlobalLayoutListenerAddedFilter = true;
+        }
+        if (animateEntrance) {
+            mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilter);
+        } else {
+            mPopup.setAnimationStyle(com.android.internal.R.style.Animation_TypingFilterRestore);
+        }
+    }
+
+    private EditText getTextFilterInput() {
+        if (mTextFilter == null) {
+            final LayoutInflater layoutInflater = LayoutInflater.from(getContext());
+            mTextFilter = (EditText) layoutInflater.inflate(
+                    com.android.internal.R.layout.typing_filter, null);
+            // For some reason setting this as the "real" input type changes
+            // the text view in some way that it doesn't work, and I don't
+            // want to figure out why this is.
+            mTextFilter.setRawInputType(EditorInfo.TYPE_CLASS_TEXT
+                    | EditorInfo.TYPE_TEXT_VARIATION_FILTER);
+            mTextFilter.setImeOptions(EditorInfo.IME_FLAG_NO_EXTRACT_UI);
+            mTextFilter.addTextChangedListener(this);
+        }
+        return mTextFilter;
+    }
+
+    /**
+     * Clear the text filter.
+     */
+    public void clearTextFilter() {
+        if (mFiltered) {
+            getTextFilterInput().setText("");
+            mFiltered = false;
+            if (mPopup != null && mPopup.isShowing()) {
+                dismissPopup();
+            }
+        }
+    }
+
+    /**
+     * Returns if the ListView currently has a text filter.
+     */
+    public boolean hasTextFilter() {
+        return mFiltered;
+    }
+
+    @Override
+    public void onGlobalLayout() {
+        if (isShown()) {
+            // Show the popup if we are filtered
+            if (mFiltered && mPopup != null && !mPopup.isShowing() && !mPopupHidden) {
+                showPopup();
+            }
+        } else {
+            // Hide the popup when we are no longer visible
+            if (mPopup != null && mPopup.isShowing()) {
+                dismissPopup();
+            }
+        }
+
+    }
+
+    /**
+     * For our text watcher that is associated with the text filter.  Does
+     * nothing.
+     */
+    @Override
+    public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+    }
+
+    /**
+     * For our text watcher that is associated with the text filter. Performs
+     * the actual filtering as the text changes, and takes care of hiding and
+     * showing the popup displaying the currently entered filter text.
+     */
+    @Override
+    public void onTextChanged(CharSequence s, int start, int before, int count) {
+        if (isTextFilterEnabled()) {
+            createTextFilter(true);
+            int length = s.length();
+            boolean showing = mPopup.isShowing();
+            if (!showing && length > 0) {
+                // Show the filter popup if necessary
+                showPopup();
+                mFiltered = true;
+            } else if (showing && length == 0) {
+                // Remove the filter popup if the user has cleared all text
+                dismissPopup();
+                mFiltered = false;
+            }
+            if (mAdapter instanceof Filterable) {
+                Filter f = ((Filterable) mAdapter).getFilter();
+                // Filter should not be null when we reach this part
+                if (f != null) {
+                    f.filter(s, this);
+                } else {
+                    throw new IllegalStateException("You cannot call onTextChanged with a non "
+                            + "filterable adapter");
+                }
+            }
+        }
+    }
+
+    /**
+     * For our text watcher that is associated with the text filter.  Does
+     * nothing.
+     */
+    @Override
+    public void afterTextChanged(Editable s) {
+    }
+
+    @Override
+    public void onFilterComplete(int count) {
+        if (mSelectedPosition < 0 && count > 0) {
+            mResurrectToPosition = INVALID_POSITION;
+            resurrectSelection();
+        }
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new AbsListView.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT, 0);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new AbsListView.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof AbsListView.LayoutParams;
+    }
+
+    /**
+     * Puts the list or grid into transcript mode. In this mode the list or grid will always scroll
+     * to the bottom to show new items.
+     *
+     * @param mode the transcript mode to set
+     *
+     * @see #TRANSCRIPT_MODE_DISABLED
+     * @see #TRANSCRIPT_MODE_NORMAL
+     * @see #TRANSCRIPT_MODE_ALWAYS_SCROLL
+     */
+    public void setTranscriptMode(int mode) {
+        mTranscriptMode = mode;
+    }
+
+    /**
+     * Returns the current transcript mode.
+     *
+     * @return {@link #TRANSCRIPT_MODE_DISABLED}, {@link #TRANSCRIPT_MODE_NORMAL} or
+     *         {@link #TRANSCRIPT_MODE_ALWAYS_SCROLL}
+     */
+    public int getTranscriptMode() {
+        return mTranscriptMode;
+    }
+
+    @Override
+    public int getSolidColor() {
+        return mCacheColorHint;
+    }
+
+    /**
+     * When set to a non-zero value, the cache color hint indicates that this list is always drawn
+     * on top of a solid, single-color, opaque background.
+     *
+     * Zero means that what's behind this object is translucent (non solid) or is not made of a
+     * single color. This hint will not affect any existing background drawable set on this view (
+     * typically set via {@link #setBackgroundDrawable(Drawable)}).
+     *
+     * @param color The background color
+     */
+    public void setCacheColorHint(@ColorInt int color) {
+        if (color != mCacheColorHint) {
+            mCacheColorHint = color;
+            int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                getChildAt(i).setDrawingCacheBackgroundColor(color);
+            }
+            mRecycler.setCacheColorHint(color);
+        }
+    }
+
+    /**
+     * When set to a non-zero value, the cache color hint indicates that this list is always drawn
+     * on top of a solid, single-color, opaque background
+     *
+     * @return The cache color hint
+     */
+    @ViewDebug.ExportedProperty(category = "drawing")
+    @ColorInt
+    public int getCacheColorHint() {
+        return mCacheColorHint;
+    }
+
+    /**
+     * Move all views (excluding headers and footers) held by this AbsListView into the supplied
+     * List. This includes views displayed on the screen as well as views stored in AbsListView's
+     * internal view recycler.
+     *
+     * @param views A list into which to put the reclaimed views
+     */
+    public void reclaimViews(List<View> views) {
+        int childCount = getChildCount();
+        RecyclerListener listener = mRecycler.mRecyclerListener;
+
+        // Reclaim views on screen
+        for (int i = 0; i < childCount; i++) {
+            View child = getChildAt(i);
+            AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
+            // Don't reclaim header or footer views, or views that should be ignored
+            if (lp != null && mRecycler.shouldRecycleViewType(lp.viewType)) {
+                views.add(child);
+                child.setAccessibilityDelegate(null);
+                if (listener != null) {
+                    // Pretend they went through the scrap heap
+                    listener.onMovedToScrapHeap(child);
+                }
+            }
+        }
+        mRecycler.reclaimScrapViews(views);
+        removeAllViewsInLayout();
+    }
+
+    private void finishGlows() {
+        if (mEdgeGlowTop != null) {
+            mEdgeGlowTop.finish();
+            mEdgeGlowBottom.finish();
+        }
+    }
+
+    /**
+     * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+     * through the specified intent.
+     * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+     */
+    public void setRemoteViewsAdapter(Intent intent) {
+        setRemoteViewsAdapter(intent, false);
+    }
+
+    /** @hide **/
+    public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+        return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+    }
+
+    /** @hide **/
+    @Override
+    public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
+        // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+        // service handling the specified intent.
+        if (mRemoteAdapter != null) {
+            Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+            Intent.FilterComparison fcOld = new Intent.FilterComparison(
+                    mRemoteAdapter.getRemoteViewsServiceIntent());
+            if (fcNew.equals(fcOld)) {
+                return;
+            }
+        }
+        mDeferNotifyDataSetChanged = false;
+        // Otherwise, create a new RemoteViewsAdapter for binding
+        mRemoteAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
+        if (mRemoteAdapter.isDataReady()) {
+            setAdapter(mRemoteAdapter);
+        }
+    }
+
+    /**
+     * Sets up the onClickHandler to be used by the RemoteViewsAdapter when inflating RemoteViews
+     *
+     * @param handler The OnClickHandler to use when inflating RemoteViews.
+     *
+     * @hide
+     */
+    public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+        // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+        // service handling the specified intent.
+        if (mRemoteAdapter != null) {
+            mRemoteAdapter.setRemoteViewsOnClickHandler(handler);
+        }
+    }
+
+    /**
+     * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+     * connected yet.
+     */
+    @Override
+    public void deferNotifyDataSetChanged() {
+        mDeferNotifyDataSetChanged = true;
+    }
+
+    /**
+     * Called back when the adapter connects to the RemoteViewsService.
+     */
+    @Override
+    public boolean onRemoteAdapterConnected() {
+        if (mRemoteAdapter != mAdapter) {
+            setAdapter(mRemoteAdapter);
+            if (mDeferNotifyDataSetChanged) {
+                mRemoteAdapter.notifyDataSetChanged();
+                mDeferNotifyDataSetChanged = false;
+            }
+            return false;
+        } else if (mRemoteAdapter != null) {
+            mRemoteAdapter.superNotifyDataSetChanged();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Called back when the adapter disconnects from the RemoteViewsService.
+     */
+    @Override
+    public void onRemoteAdapterDisconnected() {
+        // If the remote adapter disconnects, we keep it around
+        // since the currently displayed items are still cached.
+        // Further, we want the service to eventually reconnect
+        // when necessary, as triggered by this view requesting
+        // items from the Adapter.
+    }
+
+    /**
+     * Hints the RemoteViewsAdapter, if it exists, about which views are currently
+     * being displayed by the AbsListView.
+     */
+    void setVisibleRangeHint(int start, int end) {
+        if (mRemoteAdapter != null) {
+            mRemoteAdapter.setVisibleRangeHint(start, end);
+        }
+    }
+
+    /**
+     * Sets the recycler listener to be notified whenever a View is set aside in
+     * the recycler for later reuse. This listener can be used to free resources
+     * associated to the View.
+     *
+     * @param listener The recycler listener to be notified of views set aside
+     *        in the recycler.
+     *
+     * @see android.widget.AbsListView.RecycleBin
+     * @see android.widget.AbsListView.RecyclerListener
+     */
+    public void setRecyclerListener(RecyclerListener listener) {
+        mRecycler.mRecyclerListener = listener;
+    }
+
+    class AdapterDataSetObserver extends AdapterView<ListAdapter>.AdapterDataSetObserver {
+        @Override
+        public void onChanged() {
+            super.onChanged();
+            if (mFastScroll != null) {
+                mFastScroll.onSectionsChanged();
+            }
+        }
+
+        @Override
+        public void onInvalidated() {
+            super.onInvalidated();
+            if (mFastScroll != null) {
+                mFastScroll.onSectionsChanged();
+            }
+        }
+    }
+
+    /**
+     * A MultiChoiceModeListener receives events for {@link AbsListView#CHOICE_MODE_MULTIPLE_MODAL}.
+     * It acts as the {@link ActionMode.Callback} for the selection mode and also receives
+     * {@link #onItemCheckedStateChanged(ActionMode, int, long, boolean)} events when the user
+     * selects and deselects list items.
+     */
+    public interface MultiChoiceModeListener extends ActionMode.Callback {
+        /**
+         * Called when an item is checked or unchecked during selection mode.
+         *
+         * @param mode The {@link ActionMode} providing the selection mode
+         * @param position Adapter position of the item that was checked or unchecked
+         * @param id Adapter ID of the item that was checked or unchecked
+         * @param checked <code>true</code> if the item is now checked, <code>false</code>
+         *                if the item is now unchecked.
+         */
+        public void onItemCheckedStateChanged(ActionMode mode,
+                int position, long id, boolean checked);
+    }
+
+    class MultiChoiceModeWrapper implements MultiChoiceModeListener {
+        private MultiChoiceModeListener mWrapped;
+
+        public void setWrapped(MultiChoiceModeListener wrapped) {
+            mWrapped = wrapped;
+        }
+
+        public boolean hasWrappedCallback() {
+            return mWrapped != null;
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            if (mWrapped.onCreateActionMode(mode, menu)) {
+                // Initialize checked graphic state?
+                setLongClickable(false);
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            return mWrapped.onPrepareActionMode(mode, menu);
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            return mWrapped.onActionItemClicked(mode, item);
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            mWrapped.onDestroyActionMode(mode);
+            mChoiceActionMode = null;
+
+            // Ending selection mode means deselecting everything.
+            clearChoices();
+
+            mDataChanged = true;
+            rememberSyncState();
+            requestLayout();
+
+            setLongClickable(true);
+        }
+
+        @Override
+        public void onItemCheckedStateChanged(ActionMode mode,
+                int position, long id, boolean checked) {
+            mWrapped.onItemCheckedStateChanged(mode, position, id, checked);
+
+            // If there are no items selected we no longer need the selection mode.
+            if (getCheckedItemCount() == 0) {
+                mode.finish();
+            }
+        }
+    }
+
+    /**
+     * AbsListView extends LayoutParams to provide a place to hold the view type.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        /**
+         * View type for this view, as returned by
+         * {@link android.widget.Adapter#getItemViewType(int) }
+         */
+        @ViewDebug.ExportedProperty(category = "list", mapping = {
+            @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_IGNORE, to = "ITEM_VIEW_TYPE_IGNORE"),
+            @ViewDebug.IntToString(from = ITEM_VIEW_TYPE_HEADER_OR_FOOTER, to = "ITEM_VIEW_TYPE_HEADER_OR_FOOTER")
+        })
+        int viewType;
+
+        /**
+         * When this boolean is set, the view has been added to the AbsListView
+         * at least once. It is used to know whether headers/footers have already
+         * been added to the list view and whether they should be treated as
+         * recycled views or not.
+         */
+        @ViewDebug.ExportedProperty(category = "list")
+        boolean recycledHeaderFooter;
+
+        /**
+         * When an AbsListView is measured with an AT_MOST measure spec, it needs
+         * to obtain children views to measure itself. When doing so, the children
+         * are not attached to the window, but put in the recycler which assumes
+         * they've been attached before. Setting this flag will force the reused
+         * view to be attached to the window rather than just attached to the
+         * parent.
+         */
+        @ViewDebug.ExportedProperty(category = "list")
+        boolean forceAdd;
+
+        /**
+         * The position the view was removed from when pulled out of the
+         * scrap heap.
+         * @hide
+         */
+        int scrappedFromPosition;
+
+        /**
+         * The ID the view represents
+         */
+        long itemId = -1;
+
+        /** Whether the adapter considers the item enabled. */
+        boolean isEnabled;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int w, int h) {
+            super(w, h);
+        }
+
+        public LayoutParams(int w, int h, int viewType) {
+            super(w, h);
+            this.viewType = viewType;
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        /** @hide */
+        @Override
+        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+            super.encodeProperties(encoder);
+
+            encoder.addProperty("list:viewType", viewType);
+            encoder.addProperty("list:recycledHeaderFooter", recycledHeaderFooter);
+            encoder.addProperty("list:forceAdd", forceAdd);
+            encoder.addProperty("list:isEnabled", isEnabled);
+        }
+    }
+
+    /**
+     * A RecyclerListener is used to receive a notification whenever a View is placed
+     * inside the RecycleBin's scrap heap. This listener is used to free resources
+     * associated to Views placed in the RecycleBin.
+     *
+     * @see android.widget.AbsListView.RecycleBin
+     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
+     */
+    public static interface RecyclerListener {
+        /**
+         * Indicates that the specified View was moved into the recycler's scrap heap.
+         * The view is not displayed on screen any more and any expensive resource
+         * associated with the view should be discarded.
+         *
+         * @param view
+         */
+        void onMovedToScrapHeap(View view);
+    }
+
+    /**
+     * The RecycleBin facilitates reuse of views across layouts. The RecycleBin has two levels of
+     * storage: ActiveViews and ScrapViews. ActiveViews are those views which were onscreen at the
+     * start of a layout. By construction, they are displaying current information. At the end of
+     * layout, all views in ActiveViews are demoted to ScrapViews. ScrapViews are old views that
+     * could potentially be used by the adapter to avoid allocating views unnecessarily.
+     *
+     * @see android.widget.AbsListView#setRecyclerListener(android.widget.AbsListView.RecyclerListener)
+     * @see android.widget.AbsListView.RecyclerListener
+     */
+    class RecycleBin {
+        private RecyclerListener mRecyclerListener;
+
+        /**
+         * The position of the first view stored in mActiveViews.
+         */
+        private int mFirstActivePosition;
+
+        /**
+         * Views that were on screen at the start of layout. This array is populated at the start of
+         * layout, and at the end of layout all view in mActiveViews are moved to mScrapViews.
+         * Views in mActiveViews represent a contiguous range of Views, with position of the first
+         * view store in mFirstActivePosition.
+         */
+        private View[] mActiveViews = new View[0];
+
+        /**
+         * Unsorted views that can be used by the adapter as a convert view.
+         */
+        private ArrayList<View>[] mScrapViews;
+
+        private int mViewTypeCount;
+
+        private ArrayList<View> mCurrentScrap;
+
+        private ArrayList<View> mSkippedScrap;
+
+        private SparseArray<View> mTransientStateViews;
+        private LongSparseArray<View> mTransientStateViewsById;
+
+        public void setViewTypeCount(int viewTypeCount) {
+            if (viewTypeCount < 1) {
+                throw new IllegalArgumentException("Can't have a viewTypeCount < 1");
+            }
+            //noinspection unchecked
+            ArrayList<View>[] scrapViews = new ArrayList[viewTypeCount];
+            for (int i = 0; i < viewTypeCount; i++) {
+                scrapViews[i] = new ArrayList<View>();
+            }
+            mViewTypeCount = viewTypeCount;
+            mCurrentScrap = scrapViews[0];
+            mScrapViews = scrapViews;
+        }
+
+        public void markChildrenDirty() {
+            if (mViewTypeCount == 1) {
+                final ArrayList<View> scrap = mCurrentScrap;
+                final int scrapCount = scrap.size();
+                for (int i = 0; i < scrapCount; i++) {
+                    scrap.get(i).forceLayout();
+                }
+            } else {
+                final int typeCount = mViewTypeCount;
+                for (int i = 0; i < typeCount; i++) {
+                    final ArrayList<View> scrap = mScrapViews[i];
+                    final int scrapCount = scrap.size();
+                    for (int j = 0; j < scrapCount; j++) {
+                        scrap.get(j).forceLayout();
+                    }
+                }
+            }
+            if (mTransientStateViews != null) {
+                final int count = mTransientStateViews.size();
+                for (int i = 0; i < count; i++) {
+                    mTransientStateViews.valueAt(i).forceLayout();
+                }
+            }
+            if (mTransientStateViewsById != null) {
+                final int count = mTransientStateViewsById.size();
+                for (int i = 0; i < count; i++) {
+                    mTransientStateViewsById.valueAt(i).forceLayout();
+                }
+            }
+        }
+
+        public boolean shouldRecycleViewType(int viewType) {
+            return viewType >= 0;
+        }
+
+        /**
+         * Clears the scrap heap.
+         */
+        void clear() {
+            if (mViewTypeCount == 1) {
+                final ArrayList<View> scrap = mCurrentScrap;
+                clearScrap(scrap);
+            } else {
+                final int typeCount = mViewTypeCount;
+                for (int i = 0; i < typeCount; i++) {
+                    final ArrayList<View> scrap = mScrapViews[i];
+                    clearScrap(scrap);
+                }
+            }
+
+            clearTransientStateViews();
+        }
+
+        /**
+         * Fill ActiveViews with all of the children of the AbsListView.
+         *
+         * @param childCount The minimum number of views mActiveViews should hold
+         * @param firstActivePosition The position of the first view that will be stored in
+         *        mActiveViews
+         */
+        void fillActiveViews(int childCount, int firstActivePosition) {
+            if (mActiveViews.length < childCount) {
+                mActiveViews = new View[childCount];
+            }
+            mFirstActivePosition = firstActivePosition;
+
+            //noinspection MismatchedReadAndWriteOfArray
+            final View[] activeViews = mActiveViews;
+            for (int i = 0; i < childCount; i++) {
+                View child = getChildAt(i);
+                AbsListView.LayoutParams lp = (AbsListView.LayoutParams) child.getLayoutParams();
+                // Don't put header or footer views into the scrap heap
+                if (lp != null && lp.viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+                    // Note:  We do place AdapterView.ITEM_VIEW_TYPE_IGNORE in active views.
+                    //        However, we will NOT place them into scrap views.
+                    activeViews[i] = child;
+                    // Remember the position so that setupChild() doesn't reset state.
+                    lp.scrappedFromPosition = firstActivePosition + i;
+                }
+            }
+        }
+
+        /**
+         * Get the view corresponding to the specified position. The view will be removed from
+         * mActiveViews if it is found.
+         *
+         * @param position The position to look up in mActiveViews
+         * @return The view if it is found, null otherwise
+         */
+        View getActiveView(int position) {
+            int index = position - mFirstActivePosition;
+            final View[] activeViews = mActiveViews;
+            if (index >=0 && index < activeViews.length) {
+                final View match = activeViews[index];
+                activeViews[index] = null;
+                return match;
+            }
+            return null;
+        }
+
+        View getTransientStateView(int position) {
+            if (mAdapter != null && mAdapterHasStableIds && mTransientStateViewsById != null) {
+                long id = mAdapter.getItemId(position);
+                View result = mTransientStateViewsById.get(id);
+                mTransientStateViewsById.remove(id);
+                return result;
+            }
+            if (mTransientStateViews != null) {
+                final int index = mTransientStateViews.indexOfKey(position);
+                if (index >= 0) {
+                    View result = mTransientStateViews.valueAt(index);
+                    mTransientStateViews.removeAt(index);
+                    return result;
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Dumps and fully detaches any currently saved views with transient
+         * state.
+         */
+        void clearTransientStateViews() {
+            final SparseArray<View> viewsByPos = mTransientStateViews;
+            if (viewsByPos != null) {
+                final int N = viewsByPos.size();
+                for (int i = 0; i < N; i++) {
+                    removeDetachedView(viewsByPos.valueAt(i), false);
+                }
+                viewsByPos.clear();
+            }
+
+            final LongSparseArray<View> viewsById = mTransientStateViewsById;
+            if (viewsById != null) {
+                final int N = viewsById.size();
+                for (int i = 0; i < N; i++) {
+                    removeDetachedView(viewsById.valueAt(i), false);
+                }
+                viewsById.clear();
+            }
+        }
+
+        /**
+         * @return A view from the ScrapViews collection. These are unordered.
+         */
+        View getScrapView(int position) {
+            final int whichScrap = mAdapter.getItemViewType(position);
+            if (whichScrap < 0) {
+                return null;
+            }
+            if (mViewTypeCount == 1) {
+                return retrieveFromScrap(mCurrentScrap, position);
+            } else if (whichScrap < mScrapViews.length) {
+                return retrieveFromScrap(mScrapViews[whichScrap], position);
+            }
+            return null;
+        }
+
+        /**
+         * Puts a view into the list of scrap views.
+         * <p>
+         * If the list data hasn't changed or the adapter has stable IDs, views
+         * with transient state will be preserved for later retrieval.
+         *
+         * @param scrap The view to add
+         * @param position The view's position within its parent
+         */
+        void addScrapView(View scrap, int position) {
+            final AbsListView.LayoutParams lp = (AbsListView.LayoutParams) scrap.getLayoutParams();
+            if (lp == null) {
+                // Can't recycle, but we don't know anything about the view.
+                // Ignore it completely.
+                return;
+            }
+
+            lp.scrappedFromPosition = position;
+
+            // Remove but don't scrap header or footer views, or views that
+            // should otherwise not be recycled.
+            final int viewType = lp.viewType;
+            if (!shouldRecycleViewType(viewType)) {
+                // Can't recycle. If it's not a header or footer, which have
+                // special handling and should be ignored, then skip the scrap
+                // heap and we'll fully detach the view later.
+                if (viewType != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+                    getSkippedScrap().add(scrap);
+                }
+                return;
+            }
+
+            scrap.dispatchStartTemporaryDetach();
+
+            // The the accessibility state of the view may change while temporary
+            // detached and we do not allow detached views to fire accessibility
+            // events. So we are announcing that the subtree changed giving a chance
+            // to clients holding on to a view in this subtree to refresh it.
+            notifyViewAccessibilityStateChangedIfNeeded(
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_SUBTREE);
+
+            // Don't scrap views that have transient state.
+            final boolean scrapHasTransientState = scrap.hasTransientState();
+            if (scrapHasTransientState) {
+                if (mAdapter != null && mAdapterHasStableIds) {
+                    // If the adapter has stable IDs, we can reuse the view for
+                    // the same data.
+                    if (mTransientStateViewsById == null) {
+                        mTransientStateViewsById = new LongSparseArray<>();
+                    }
+                    mTransientStateViewsById.put(lp.itemId, scrap);
+                } else if (!mDataChanged) {
+                    // If the data hasn't changed, we can reuse the views at
+                    // their old positions.
+                    if (mTransientStateViews == null) {
+                        mTransientStateViews = new SparseArray<>();
+                    }
+                    mTransientStateViews.put(position, scrap);
+                } else {
+                    // Otherwise, we'll have to remove the view and start over.
+                    clearScrapForRebind(scrap);
+                    getSkippedScrap().add(scrap);
+                }
+            } else {
+                clearScrapForRebind(scrap);
+                if (mViewTypeCount == 1) {
+                    mCurrentScrap.add(scrap);
+                } else {
+                    mScrapViews[viewType].add(scrap);
+                }
+
+                if (mRecyclerListener != null) {
+                    mRecyclerListener.onMovedToScrapHeap(scrap);
+                }
+            }
+        }
+
+        private ArrayList<View> getSkippedScrap() {
+            if (mSkippedScrap == null) {
+                mSkippedScrap = new ArrayList<>();
+            }
+            return mSkippedScrap;
+        }
+
+        /**
+         * Finish the removal of any views that skipped the scrap heap.
+         */
+        void removeSkippedScrap() {
+            if (mSkippedScrap == null) {
+                return;
+            }
+            final int count = mSkippedScrap.size();
+            for (int i = 0; i < count; i++) {
+                removeDetachedView(mSkippedScrap.get(i), false);
+            }
+            mSkippedScrap.clear();
+        }
+
+        /**
+         * Move all views remaining in mActiveViews to mScrapViews.
+         */
+        void scrapActiveViews() {
+            final View[] activeViews = mActiveViews;
+            final boolean hasListener = mRecyclerListener != null;
+            final boolean multipleScraps = mViewTypeCount > 1;
+
+            ArrayList<View> scrapViews = mCurrentScrap;
+            final int count = activeViews.length;
+            for (int i = count - 1; i >= 0; i--) {
+                final View victim = activeViews[i];
+                if (victim != null) {
+                    final AbsListView.LayoutParams lp
+                            = (AbsListView.LayoutParams) victim.getLayoutParams();
+                    final int whichScrap = lp.viewType;
+
+                    activeViews[i] = null;
+
+                    if (victim.hasTransientState()) {
+                        // Store views with transient state for later use.
+                        victim.dispatchStartTemporaryDetach();
+
+                        if (mAdapter != null && mAdapterHasStableIds) {
+                            if (mTransientStateViewsById == null) {
+                                mTransientStateViewsById = new LongSparseArray<View>();
+                            }
+                            long id = mAdapter.getItemId(mFirstActivePosition + i);
+                            mTransientStateViewsById.put(id, victim);
+                        } else if (!mDataChanged) {
+                            if (mTransientStateViews == null) {
+                                mTransientStateViews = new SparseArray<View>();
+                            }
+                            mTransientStateViews.put(mFirstActivePosition + i, victim);
+                        } else if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+                            // The data has changed, we can't keep this view.
+                            removeDetachedView(victim, false);
+                        }
+                    } else if (!shouldRecycleViewType(whichScrap)) {
+                        // Discard non-recyclable views except headers/footers.
+                        if (whichScrap != ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+                            removeDetachedView(victim, false);
+                        }
+                    } else {
+                        // Store everything else on the appropriate scrap heap.
+                        if (multipleScraps) {
+                            scrapViews = mScrapViews[whichScrap];
+                        }
+
+                        lp.scrappedFromPosition = mFirstActivePosition + i;
+                        removeDetachedView(victim, false);
+                        scrapViews.add(victim);
+
+                        if (hasListener) {
+                            mRecyclerListener.onMovedToScrapHeap(victim);
+                        }
+                    }
+                }
+            }
+            pruneScrapViews();
+        }
+
+        /**
+         * At the end of a layout pass, all temp detached views should either be re-attached or
+         * completely detached. This method ensures that any remaining view in the scrap list is
+         * fully detached.
+         */
+        void fullyDetachScrapViews() {
+            final int viewTypeCount = mViewTypeCount;
+            final ArrayList<View>[] scrapViews = mScrapViews;
+            for (int i = 0; i < viewTypeCount; ++i) {
+                final ArrayList<View> scrapPile = scrapViews[i];
+                for (int j = scrapPile.size() - 1; j >= 0; j--) {
+                    final View view = scrapPile.get(j);
+                    if (view.isTemporarilyDetached()) {
+                        removeDetachedView(view, false);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Makes sure that the size of mScrapViews does not exceed the size of
+         * mActiveViews, which can happen if an adapter does not recycle its
+         * views. Removes cached transient state views that no longer have
+         * transient state.
+         */
+        private void pruneScrapViews() {
+            final int maxViews = mActiveViews.length;
+            final int viewTypeCount = mViewTypeCount;
+            final ArrayList<View>[] scrapViews = mScrapViews;
+            for (int i = 0; i < viewTypeCount; ++i) {
+                final ArrayList<View> scrapPile = scrapViews[i];
+                int size = scrapPile.size();
+                while (size > maxViews) {
+                    scrapPile.remove(--size);
+                }
+            }
+
+            final SparseArray<View> transViewsByPos = mTransientStateViews;
+            if (transViewsByPos != null) {
+                for (int i = 0; i < transViewsByPos.size(); i++) {
+                    final View v = transViewsByPos.valueAt(i);
+                    if (!v.hasTransientState()) {
+                        removeDetachedView(v, false);
+                        transViewsByPos.removeAt(i);
+                        i--;
+                    }
+                }
+            }
+
+            final LongSparseArray<View> transViewsById = mTransientStateViewsById;
+            if (transViewsById != null) {
+                for (int i = 0; i < transViewsById.size(); i++) {
+                    final View v = transViewsById.valueAt(i);
+                    if (!v.hasTransientState()) {
+                        removeDetachedView(v, false);
+                        transViewsById.removeAt(i);
+                        i--;
+                    }
+                }
+            }
+        }
+
+        /**
+         * Puts all views in the scrap heap into the supplied list.
+         */
+        void reclaimScrapViews(List<View> views) {
+            if (mViewTypeCount == 1) {
+                views.addAll(mCurrentScrap);
+            } else {
+                final int viewTypeCount = mViewTypeCount;
+                final ArrayList<View>[] scrapViews = mScrapViews;
+                for (int i = 0; i < viewTypeCount; ++i) {
+                    final ArrayList<View> scrapPile = scrapViews[i];
+                    views.addAll(scrapPile);
+                }
+            }
+        }
+
+        /**
+         * Updates the cache color hint of all known views.
+         *
+         * @param color The new cache color hint.
+         */
+        void setCacheColorHint(int color) {
+            if (mViewTypeCount == 1) {
+                final ArrayList<View> scrap = mCurrentScrap;
+                final int scrapCount = scrap.size();
+                for (int i = 0; i < scrapCount; i++) {
+                    scrap.get(i).setDrawingCacheBackgroundColor(color);
+                }
+            } else {
+                final int typeCount = mViewTypeCount;
+                for (int i = 0; i < typeCount; i++) {
+                    final ArrayList<View> scrap = mScrapViews[i];
+                    final int scrapCount = scrap.size();
+                    for (int j = 0; j < scrapCount; j++) {
+                        scrap.get(j).setDrawingCacheBackgroundColor(color);
+                    }
+                }
+            }
+            // Just in case this is called during a layout pass
+            final View[] activeViews = mActiveViews;
+            final int count = activeViews.length;
+            for (int i = 0; i < count; ++i) {
+                final View victim = activeViews[i];
+                if (victim != null) {
+                    victim.setDrawingCacheBackgroundColor(color);
+                }
+            }
+        }
+
+        private View retrieveFromScrap(ArrayList<View> scrapViews, int position) {
+            final int size = scrapViews.size();
+            if (size > 0) {
+                // See if we still have a view for this position or ID.
+                // Traverse backwards to find the most recently used scrap view
+                for (int i = size - 1; i >= 0; i--) {
+                    final View view = scrapViews.get(i);
+                    final AbsListView.LayoutParams params =
+                            (AbsListView.LayoutParams) view.getLayoutParams();
+
+                    if (mAdapterHasStableIds) {
+                        final long id = mAdapter.getItemId(position);
+                        if (id == params.itemId) {
+                            return scrapViews.remove(i);
+                        }
+                    } else if (params.scrappedFromPosition == position) {
+                        final View scrap = scrapViews.remove(i);
+                        clearScrapForRebind(scrap);
+                        return scrap;
+                    }
+                }
+                final View scrap = scrapViews.remove(size - 1);
+                clearScrapForRebind(scrap);
+                return scrap;
+            } else {
+                return null;
+            }
+        }
+
+        private void clearScrap(final ArrayList<View> scrap) {
+            final int scrapCount = scrap.size();
+            for (int j = 0; j < scrapCount; j++) {
+                removeDetachedView(scrap.remove(scrapCount - 1 - j), false);
+            }
+        }
+
+        private void clearScrapForRebind(View view) {
+            view.clearAccessibilityFocus();
+            view.setAccessibilityDelegate(null);
+        }
+
+        private void removeDetachedView(View child, boolean animate) {
+            child.setAccessibilityDelegate(null);
+            AbsListView.this.removeDetachedView(child, animate);
+        }
+    }
+
+    /**
+     * Returns the height of the view for the specified position.
+     *
+     * @param position the item position
+     * @return view height in pixels
+     */
+    int getHeightForPosition(int position) {
+        final int firstVisiblePosition = getFirstVisiblePosition();
+        final int childCount = getChildCount();
+        final int index = position - firstVisiblePosition;
+        if (index >= 0 && index < childCount) {
+            // Position is on-screen, use existing view.
+            final View view = getChildAt(index);
+            return view.getHeight();
+        } else {
+            // Position is off-screen, obtain & recycle view.
+            final View view = obtainView(position, mIsScrap);
+            view.measure(mWidthMeasureSpec, MeasureSpec.UNSPECIFIED);
+            final int height = view.getMeasuredHeight();
+            mRecycler.addScrapView(view, position);
+            return height;
+        }
+    }
+
+    /**
+     * Sets the selected item and positions the selection y pixels from the top edge
+     * of the ListView. (If in touch mode, the item will not be selected but it will
+     * still be positioned appropriately.)
+     *
+     * @param position Index (starting at 0) of the data item to be selected.
+     * @param y The distance from the top edge of the ListView (plus padding) that the
+     *        item will be positioned.
+     */
+    public void setSelectionFromTop(int position, int y) {
+        if (mAdapter == null) {
+            return;
+        }
+
+        if (!isInTouchMode()) {
+            position = lookForSelectablePosition(position, true);
+            if (position >= 0) {
+                setNextSelectedPositionInt(position);
+            }
+        } else {
+            mResurrectToPosition = position;
+        }
+
+        if (position >= 0) {
+            mLayoutMode = LAYOUT_SPECIFIC;
+            mSpecificTop = mListPadding.top + y;
+
+            if (mNeedSync) {
+                mSyncPosition = position;
+                mSyncRowId = mAdapter.getItemId(position);
+            }
+
+            if (mPositionScroller != null) {
+                mPositionScroller.stop();
+            }
+            requestLayout();
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+
+        encoder.addProperty("drawing:cacheColorHint", getCacheColorHint());
+        encoder.addProperty("list:fastScrollEnabled", isFastScrollEnabled());
+        encoder.addProperty("list:scrollingCacheEnabled", isScrollingCacheEnabled());
+        encoder.addProperty("list:smoothScrollbarEnabled", isSmoothScrollbarEnabled());
+        encoder.addProperty("list:stackFromBottom", isStackFromBottom());
+        encoder.addProperty("list:textFilterEnabled", isTextFilterEnabled());
+
+        View selectedView = getSelectedView();
+        if (selectedView != null) {
+            encoder.addPropertyKey("selectedView");
+            selectedView.encode(encoder);
+        }
+    }
+
+    /**
+     * Abstract positon scroller used to handle smooth scrolling.
+     */
+    static abstract class AbsPositionScroller {
+        public abstract void start(int position);
+        public abstract void start(int position, int boundPosition);
+        public abstract void startWithOffset(int position, int offset);
+        public abstract void startWithOffset(int position, int offset, int duration);
+        public abstract void stop();
+    }
+
+    /**
+     * Default position scroller that simulates a fling.
+     */
+    class PositionScroller extends AbsPositionScroller implements Runnable {
+        private static final int SCROLL_DURATION = 200;
+
+        private static final int MOVE_DOWN_POS = 1;
+        private static final int MOVE_UP_POS = 2;
+        private static final int MOVE_DOWN_BOUND = 3;
+        private static final int MOVE_UP_BOUND = 4;
+        private static final int MOVE_OFFSET = 5;
+
+        private int mMode;
+        private int mTargetPos;
+        private int mBoundPos;
+        private int mLastSeenPos;
+        private int mScrollDuration;
+        private final int mExtraScroll;
+
+        private int mOffsetFromTop;
+
+        PositionScroller() {
+            mExtraScroll = ViewConfiguration.get(mContext).getScaledFadingEdgeLength();
+        }
+
+        @Override
+        public void start(final int position) {
+            stop();
+
+            if (mDataChanged) {
+                // Wait until we're back in a stable state to try this.
+                mPositionScrollAfterLayout = new Runnable() {
+                    @Override public void run() {
+                        start(position);
+                    }
+                };
+                return;
+            }
+
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Can't scroll without children.
+                return;
+            }
+
+            final int firstPos = mFirstPosition;
+            final int lastPos = firstPos + childCount - 1;
+
+            int viewTravelCount;
+            int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
+            if (clampedPosition < firstPos) {
+                viewTravelCount = firstPos - clampedPosition + 1;
+                mMode = MOVE_UP_POS;
+            } else if (clampedPosition > lastPos) {
+                viewTravelCount = clampedPosition - lastPos + 1;
+                mMode = MOVE_DOWN_POS;
+            } else {
+                scrollToVisible(clampedPosition, INVALID_POSITION, SCROLL_DURATION);
+                return;
+            }
+
+            if (viewTravelCount > 0) {
+                mScrollDuration = SCROLL_DURATION / viewTravelCount;
+            } else {
+                mScrollDuration = SCROLL_DURATION;
+            }
+            mTargetPos = clampedPosition;
+            mBoundPos = INVALID_POSITION;
+            mLastSeenPos = INVALID_POSITION;
+
+            postOnAnimation(this);
+        }
+
+        @Override
+        public void start(final int position, final int boundPosition) {
+            stop();
+
+            if (boundPosition == INVALID_POSITION) {
+                start(position);
+                return;
+            }
+
+            if (mDataChanged) {
+                // Wait until we're back in a stable state to try this.
+                mPositionScrollAfterLayout = new Runnable() {
+                    @Override public void run() {
+                        start(position, boundPosition);
+                    }
+                };
+                return;
+            }
+
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Can't scroll without children.
+                return;
+            }
+
+            final int firstPos = mFirstPosition;
+            final int lastPos = firstPos + childCount - 1;
+
+            int viewTravelCount;
+            int clampedPosition = Math.max(0, Math.min(getCount() - 1, position));
+            if (clampedPosition < firstPos) {
+                final int boundPosFromLast = lastPos - boundPosition;
+                if (boundPosFromLast < 1) {
+                    // Moving would shift our bound position off the screen. Abort.
+                    return;
+                }
+
+                final int posTravel = firstPos - clampedPosition + 1;
+                final int boundTravel = boundPosFromLast - 1;
+                if (boundTravel < posTravel) {
+                    viewTravelCount = boundTravel;
+                    mMode = MOVE_UP_BOUND;
+                } else {
+                    viewTravelCount = posTravel;
+                    mMode = MOVE_UP_POS;
+                }
+            } else if (clampedPosition > lastPos) {
+                final int boundPosFromFirst = boundPosition - firstPos;
+                if (boundPosFromFirst < 1) {
+                    // Moving would shift our bound position off the screen. Abort.
+                    return;
+                }
+
+                final int posTravel = clampedPosition - lastPos + 1;
+                final int boundTravel = boundPosFromFirst - 1;
+                if (boundTravel < posTravel) {
+                    viewTravelCount = boundTravel;
+                    mMode = MOVE_DOWN_BOUND;
+                } else {
+                    viewTravelCount = posTravel;
+                    mMode = MOVE_DOWN_POS;
+                }
+            } else {
+                scrollToVisible(clampedPosition, boundPosition, SCROLL_DURATION);
+                return;
+            }
+
+            if (viewTravelCount > 0) {
+                mScrollDuration = SCROLL_DURATION / viewTravelCount;
+            } else {
+                mScrollDuration = SCROLL_DURATION;
+            }
+            mTargetPos = clampedPosition;
+            mBoundPos = boundPosition;
+            mLastSeenPos = INVALID_POSITION;
+
+            postOnAnimation(this);
+        }
+
+        @Override
+        public void startWithOffset(int position, int offset) {
+            startWithOffset(position, offset, SCROLL_DURATION);
+        }
+
+        @Override
+        public void startWithOffset(final int position, int offset, final int duration) {
+            stop();
+
+            if (mDataChanged) {
+                // Wait until we're back in a stable state to try this.
+                final int postOffset = offset;
+                mPositionScrollAfterLayout = new Runnable() {
+                    @Override public void run() {
+                        startWithOffset(position, postOffset, duration);
+                    }
+                };
+                return;
+            }
+
+            final int childCount = getChildCount();
+            if (childCount == 0) {
+                // Can't scroll without children.
+                return;
+            }
+
+            offset += getPaddingTop();
+
+            mTargetPos = Math.max(0, Math.min(getCount() - 1, position));
+            mOffsetFromTop = offset;
+            mBoundPos = INVALID_POSITION;
+            mLastSeenPos = INVALID_POSITION;
+            mMode = MOVE_OFFSET;
+
+            final int firstPos = mFirstPosition;
+            final int lastPos = firstPos + childCount - 1;
+
+            int viewTravelCount;
+            if (mTargetPos < firstPos) {
+                viewTravelCount = firstPos - mTargetPos;
+            } else if (mTargetPos > lastPos) {
+                viewTravelCount = mTargetPos - lastPos;
+            } else {
+                // On-screen, just scroll.
+                final int targetTop = getChildAt(mTargetPos - firstPos).getTop();
+                smoothScrollBy(targetTop - offset, duration, true, false);
+                return;
+            }
+
+            // Estimate how many screens we should travel
+            final float screenTravelCount = (float) viewTravelCount / childCount;
+            mScrollDuration = screenTravelCount < 1 ?
+                    duration : (int) (duration / screenTravelCount);
+            mLastSeenPos = INVALID_POSITION;
+
+            postOnAnimation(this);
+        }
+
+        /**
+         * Scroll such that targetPos is in the visible padded region without scrolling
+         * boundPos out of view. Assumes targetPos is onscreen.
+         */
+        private void scrollToVisible(int targetPos, int boundPos, int duration) {
+            final int firstPos = mFirstPosition;
+            final int childCount = getChildCount();
+            final int lastPos = firstPos + childCount - 1;
+            final int paddedTop = mListPadding.top;
+            final int paddedBottom = getHeight() - mListPadding.bottom;
+
+            if (targetPos < firstPos || targetPos > lastPos) {
+                Log.w(TAG, "scrollToVisible called with targetPos " + targetPos +
+                        " not visible [" + firstPos + ", " + lastPos + "]");
+            }
+            if (boundPos < firstPos || boundPos > lastPos) {
+                // boundPos doesn't matter, it's already offscreen.
+                boundPos = INVALID_POSITION;
+            }
+
+            final View targetChild = getChildAt(targetPos - firstPos);
+            final int targetTop = targetChild.getTop();
+            final int targetBottom = targetChild.getBottom();
+            int scrollBy = 0;
+
+            if (targetBottom > paddedBottom) {
+                scrollBy = targetBottom - paddedBottom;
+            }
+            if (targetTop < paddedTop) {
+                scrollBy = targetTop - paddedTop;
+            }
+
+            if (scrollBy == 0) {
+                return;
+            }
+
+            if (boundPos >= 0) {
+                final View boundChild = getChildAt(boundPos - firstPos);
+                final int boundTop = boundChild.getTop();
+                final int boundBottom = boundChild.getBottom();
+                final int absScroll = Math.abs(scrollBy);
+
+                if (scrollBy < 0 && boundBottom + absScroll > paddedBottom) {
+                    // Don't scroll the bound view off the bottom of the screen.
+                    scrollBy = Math.max(0, boundBottom - paddedBottom);
+                } else if (scrollBy > 0 && boundTop - absScroll < paddedTop) {
+                    // Don't scroll the bound view off the top of the screen.
+                    scrollBy = Math.min(0, boundTop - paddedTop);
+                }
+            }
+
+            smoothScrollBy(scrollBy, duration);
+        }
+
+        @Override
+        public void stop() {
+            removeCallbacks(this);
+        }
+
+        @Override
+        public void run() {
+            final int listHeight = getHeight();
+            final int firstPos = mFirstPosition;
+
+            switch (mMode) {
+            case MOVE_DOWN_POS: {
+                final int lastViewIndex = getChildCount() - 1;
+                final int lastPos = firstPos + lastViewIndex;
+
+                if (lastViewIndex < 0) {
+                    return;
+                }
+
+                if (lastPos == mLastSeenPos) {
+                    // No new views, let things keep going.
+                    postOnAnimation(this);
+                    return;
+                }
+
+                final View lastView = getChildAt(lastViewIndex);
+                final int lastViewHeight = lastView.getHeight();
+                final int lastViewTop = lastView.getTop();
+                final int lastViewPixelsShowing = listHeight - lastViewTop;
+                final int extraScroll = lastPos < mItemCount - 1 ?
+                        Math.max(mListPadding.bottom, mExtraScroll) : mListPadding.bottom;
+
+                final int scrollBy = lastViewHeight - lastViewPixelsShowing + extraScroll;
+                smoothScrollBy(scrollBy, mScrollDuration, true, lastPos < mTargetPos);
+
+                mLastSeenPos = lastPos;
+                if (lastPos < mTargetPos) {
+                    postOnAnimation(this);
+                }
+                break;
+            }
+
+            case MOVE_DOWN_BOUND: {
+                final int nextViewIndex = 1;
+                final int childCount = getChildCount();
+
+                if (firstPos == mBoundPos || childCount <= nextViewIndex
+                        || firstPos + childCount >= mItemCount) {
+                    reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                    return;
+                }
+                final int nextPos = firstPos + nextViewIndex;
+
+                if (nextPos == mLastSeenPos) {
+                    // No new views, let things keep going.
+                    postOnAnimation(this);
+                    return;
+                }
+
+                final View nextView = getChildAt(nextViewIndex);
+                final int nextViewHeight = nextView.getHeight();
+                final int nextViewTop = nextView.getTop();
+                final int extraScroll = Math.max(mListPadding.bottom, mExtraScroll);
+                if (nextPos < mBoundPos) {
+                    smoothScrollBy(Math.max(0, nextViewHeight + nextViewTop - extraScroll),
+                            mScrollDuration, true, true);
+
+                    mLastSeenPos = nextPos;
+
+                    postOnAnimation(this);
+                } else  {
+                    if (nextViewTop > extraScroll) {
+                        smoothScrollBy(nextViewTop - extraScroll, mScrollDuration, true, false);
+                    } else {
+                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                    }
+                }
+                break;
+            }
+
+            case MOVE_UP_POS: {
+                if (firstPos == mLastSeenPos) {
+                    // No new views, let things keep going.
+                    postOnAnimation(this);
+                    return;
+                }
+
+                final View firstView = getChildAt(0);
+                if (firstView == null) {
+                    return;
+                }
+                final int firstViewTop = firstView.getTop();
+                final int extraScroll = firstPos > 0 ?
+                        Math.max(mExtraScroll, mListPadding.top) : mListPadding.top;
+
+                smoothScrollBy(firstViewTop - extraScroll, mScrollDuration, true,
+                        firstPos > mTargetPos);
+
+                mLastSeenPos = firstPos;
+
+                if (firstPos > mTargetPos) {
+                    postOnAnimation(this);
+                }
+                break;
+            }
+
+            case MOVE_UP_BOUND: {
+                final int lastViewIndex = getChildCount() - 2;
+                if (lastViewIndex < 0) {
+                    return;
+                }
+                final int lastPos = firstPos + lastViewIndex;
+
+                if (lastPos == mLastSeenPos) {
+                    // No new views, let things keep going.
+                    postOnAnimation(this);
+                    return;
+                }
+
+                final View lastView = getChildAt(lastViewIndex);
+                final int lastViewHeight = lastView.getHeight();
+                final int lastViewTop = lastView.getTop();
+                final int lastViewPixelsShowing = listHeight - lastViewTop;
+                final int extraScroll = Math.max(mListPadding.top, mExtraScroll);
+                mLastSeenPos = lastPos;
+                if (lastPos > mBoundPos) {
+                    smoothScrollBy(-(lastViewPixelsShowing - extraScroll), mScrollDuration, true,
+                            true);
+                    postOnAnimation(this);
+                } else {
+                    final int bottom = listHeight - extraScroll;
+                    final int lastViewBottom = lastViewTop + lastViewHeight;
+                    if (bottom > lastViewBottom) {
+                        smoothScrollBy(-(bottom - lastViewBottom), mScrollDuration, true, false);
+                    } else {
+                        reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                    }
+                }
+                break;
+            }
+
+            case MOVE_OFFSET: {
+                if (mLastSeenPos == firstPos) {
+                    // No new views, let things keep going.
+                    postOnAnimation(this);
+                    return;
+                }
+
+                mLastSeenPos = firstPos;
+
+                final int childCount = getChildCount();
+                final int position = mTargetPos;
+                final int lastPos = firstPos + childCount - 1;
+
+                // Account for the visible "portion" of the first / last child when we estimate
+                // how many screens we should travel to reach our target
+                final View firstChild = getChildAt(0);
+                final int firstChildHeight = firstChild.getHeight();
+                final View lastChild = getChildAt(childCount - 1);
+                final int lastChildHeight = lastChild.getHeight();
+                final float firstPositionVisiblePart = (firstChildHeight == 0.0f) ? 1.0f
+                        : (float) (firstChildHeight + firstChild.getTop()) / firstChildHeight;
+                final float lastPositionVisiblePart = (lastChildHeight == 0.0f) ? 1.0f
+                        : (float) (lastChildHeight + getHeight() - lastChild.getBottom())
+                                / lastChildHeight;
+
+                float viewTravelCount = 0;
+                if (position < firstPos) {
+                    viewTravelCount = firstPos - position + (1.0f - firstPositionVisiblePart) + 1;
+                } else if (position > lastPos) {
+                    viewTravelCount = position - lastPos + (1.0f - lastPositionVisiblePart);
+                }
+
+                // Estimate how many screens we should travel
+                final float screenTravelCount = viewTravelCount / childCount;
+
+                final float modifier = Math.min(Math.abs(screenTravelCount), 1.f);
+                if (position < firstPos) {
+                    final int distance = (int) (-getHeight() * modifier);
+                    final int duration = (int) (mScrollDuration * modifier);
+                    smoothScrollBy(distance, duration, true, true);
+                    postOnAnimation(this);
+                } else if (position > lastPos) {
+                    final int distance = (int) (getHeight() * modifier);
+                    final int duration = (int) (mScrollDuration * modifier);
+                    smoothScrollBy(distance, duration, true, true);
+                    postOnAnimation(this);
+                } else {
+                    // On-screen, just scroll.
+                    final int targetTop = getChildAt(position - firstPos).getTop();
+                    final int distance = targetTop - mOffsetFromTop;
+                    final int duration = (int) (mScrollDuration *
+                            ((float) Math.abs(distance) / getHeight()));
+                    smoothScrollBy(distance, duration, true, false);
+                }
+                break;
+            }
+
+            default:
+                break;
+            }
+        }
+    }
+}
diff --git a/android/widget/AbsSeekBar.java b/android/widget/AbsSeekBar.java
new file mode 100644
index 0000000..1d1fcc9
--- /dev/null
+++ b/android/widget/AbsSeekBar.java
@@ -0,0 +1,1016 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+
+/**
+ * AbsSeekBar extends the capabilities of ProgressBar by adding a draggable thumb.
+ */
+public abstract class AbsSeekBar extends ProgressBar {
+    private final Rect mTempRect = new Rect();
+
+    private Drawable mThumb;
+    private ColorStateList mThumbTintList = null;
+    private PorterDuff.Mode mThumbTintMode = null;
+    private boolean mHasThumbTint = false;
+    private boolean mHasThumbTintMode = false;
+
+    private Drawable mTickMark;
+    private ColorStateList mTickMarkTintList = null;
+    private PorterDuff.Mode mTickMarkTintMode = null;
+    private boolean mHasTickMarkTint = false;
+    private boolean mHasTickMarkTintMode = false;
+
+    private int mThumbOffset;
+    private boolean mSplitTrack;
+
+    /**
+     * On touch, this offset plus the scaled value from the position of the
+     * touch will form the progress value. Usually 0.
+     */
+    float mTouchProgressOffset;
+
+    /**
+     * Whether this is user seekable.
+     */
+    boolean mIsUserSeekable = true;
+
+    /**
+     * On key presses (right or left), the amount to increment/decrement the
+     * progress.
+     */
+    private int mKeyProgressIncrement = 1;
+
+    private static final int NO_ALPHA = 0xFF;
+    private float mDisabledAlpha;
+
+    private int mScaledTouchSlop;
+    private float mTouchDownX;
+    private boolean mIsDragging;
+
+    public AbsSeekBar(Context context) {
+        super(context);
+    }
+
+    public AbsSeekBar(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AbsSeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.SeekBar, defStyleAttr, defStyleRes);
+
+        final Drawable thumb = a.getDrawable(R.styleable.SeekBar_thumb);
+        setThumb(thumb);
+
+        if (a.hasValue(R.styleable.SeekBar_thumbTintMode)) {
+            mThumbTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.SeekBar_thumbTintMode, -1), mThumbTintMode);
+            mHasThumbTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.SeekBar_thumbTint)) {
+            mThumbTintList = a.getColorStateList(R.styleable.SeekBar_thumbTint);
+            mHasThumbTint = true;
+        }
+
+        final Drawable tickMark = a.getDrawable(R.styleable.SeekBar_tickMark);
+        setTickMark(tickMark);
+
+        if (a.hasValue(R.styleable.SeekBar_tickMarkTintMode)) {
+            mTickMarkTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.SeekBar_tickMarkTintMode, -1), mTickMarkTintMode);
+            mHasTickMarkTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.SeekBar_tickMarkTint)) {
+            mTickMarkTintList = a.getColorStateList(R.styleable.SeekBar_tickMarkTint);
+            mHasTickMarkTint = true;
+        }
+
+        mSplitTrack = a.getBoolean(R.styleable.SeekBar_splitTrack, false);
+
+        // Guess thumb offset if thumb != null, but allow layout to override.
+        final int thumbOffset = a.getDimensionPixelOffset(
+                R.styleable.SeekBar_thumbOffset, getThumbOffset());
+        setThumbOffset(thumbOffset);
+
+        final boolean useDisabledAlpha = a.getBoolean(R.styleable.SeekBar_useDisabledAlpha, true);
+        a.recycle();
+
+        if (useDisabledAlpha) {
+            final TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.Theme, 0, 0);
+            mDisabledAlpha = ta.getFloat(R.styleable.Theme_disabledAlpha, 0.5f);
+            ta.recycle();
+        } else {
+            mDisabledAlpha = 1.0f;
+        }
+
+        applyThumbTint();
+        applyTickMarkTint();
+
+        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+    }
+
+    /**
+     * Sets the thumb that will be drawn at the end of the progress meter within the SeekBar.
+     * <p>
+     * If the thumb is a valid drawable (i.e. not null), half its width will be
+     * used as the new thumb offset (@see #setThumbOffset(int)).
+     *
+     * @param thumb Drawable representing the thumb
+     */
+    public void setThumb(Drawable thumb) {
+        final boolean needUpdate;
+        // This way, calling setThumb again with the same bitmap will result in
+        // it recalcuating mThumbOffset (if for example it the bounds of the
+        // drawable changed)
+        if (mThumb != null && thumb != mThumb) {
+            mThumb.setCallback(null);
+            needUpdate = true;
+        } else {
+            needUpdate = false;
+        }
+
+        if (thumb != null) {
+            thumb.setCallback(this);
+            if (canResolveLayoutDirection()) {
+                thumb.setLayoutDirection(getLayoutDirection());
+            }
+
+            // Assuming the thumb drawable is symmetric, set the thumb offset
+            // such that the thumb will hang halfway off either edge of the
+            // progress bar.
+            mThumbOffset = thumb.getIntrinsicWidth() / 2;
+
+            // If we're updating get the new states
+            if (needUpdate &&
+                    (thumb.getIntrinsicWidth() != mThumb.getIntrinsicWidth()
+                        || thumb.getIntrinsicHeight() != mThumb.getIntrinsicHeight())) {
+                requestLayout();
+            }
+        }
+
+        mThumb = thumb;
+
+        applyThumbTint();
+        invalidate();
+
+        if (needUpdate) {
+            updateThumbAndTrackPos(getWidth(), getHeight());
+            if (thumb != null && thumb.isStateful()) {
+                // Note that if the states are different this won't work.
+                // For now, let's consider that an app bug.
+                int[] state = getDrawableState();
+                thumb.setState(state);
+            }
+        }
+    }
+
+    /**
+     * Return the drawable used to represent the scroll thumb - the component that
+     * the user can drag back and forth indicating the current value by its position.
+     *
+     * @return The current thumb drawable
+     */
+    public Drawable getThumb() {
+        return mThumb;
+    }
+
+    /**
+     * Applies a tint to the thumb drawable. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setThumb(Drawable)} will automatically
+     * mutate the drawable and apply the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#SeekBar_thumbTint
+     * @see #getThumbTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setThumbTintList(@Nullable ColorStateList tint) {
+        mThumbTintList = tint;
+        mHasThumbTint = true;
+
+        applyThumbTint();
+    }
+
+    /**
+     * Returns the tint applied to the thumb drawable, if specified.
+     *
+     * @return the tint applied to the thumb drawable
+     * @attr ref android.R.styleable#SeekBar_thumbTint
+     * @see #setThumbTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getThumbTintList() {
+        return mThumbTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable. The
+     * default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#SeekBar_thumbTintMode
+     * @see #getThumbTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mThumbTintMode = tintMode;
+        mHasThumbTintMode = true;
+
+        applyThumbTint();
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the thumb drawable,
+     * if specified.
+     *
+     * @return the blending mode used to apply the tint to the thumb drawable
+     * @attr ref android.R.styleable#SeekBar_thumbTintMode
+     * @see #setThumbTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getThumbTintMode() {
+        return mThumbTintMode;
+    }
+
+    private void applyThumbTint() {
+        if (mThumb != null && (mHasThumbTint || mHasThumbTintMode)) {
+            mThumb = mThumb.mutate();
+
+            if (mHasThumbTint) {
+                mThumb.setTintList(mThumbTintList);
+            }
+
+            if (mHasThumbTintMode) {
+                mThumb.setTintMode(mThumbTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mThumb.isStateful()) {
+                mThumb.setState(getDrawableState());
+            }
+        }
+    }
+
+    /**
+     * @see #setThumbOffset(int)
+     */
+    public int getThumbOffset() {
+        return mThumbOffset;
+    }
+
+    /**
+     * Sets the thumb offset that allows the thumb to extend out of the range of
+     * the track.
+     *
+     * @param thumbOffset The offset amount in pixels.
+     */
+    public void setThumbOffset(int thumbOffset) {
+        mThumbOffset = thumbOffset;
+        invalidate();
+    }
+
+    /**
+     * Specifies whether the track should be split by the thumb. When true,
+     * the thumb's optical bounds will be clipped out of the track drawable,
+     * then the thumb will be drawn into the resulting gap.
+     *
+     * @param splitTrack Whether the track should be split by the thumb
+     */
+    public void setSplitTrack(boolean splitTrack) {
+        mSplitTrack = splitTrack;
+        invalidate();
+    }
+
+    /**
+     * Returns whether the track should be split by the thumb.
+     */
+    public boolean getSplitTrack() {
+        return mSplitTrack;
+    }
+
+    /**
+     * Sets the drawable displayed at each progress position, e.g. at each
+     * possible thumb position.
+     *
+     * @param tickMark the drawable to display at each progress position
+     */
+    public void setTickMark(Drawable tickMark) {
+        if (mTickMark != null) {
+            mTickMark.setCallback(null);
+        }
+
+        mTickMark = tickMark;
+
+        if (tickMark != null) {
+            tickMark.setCallback(this);
+            tickMark.setLayoutDirection(getLayoutDirection());
+            if (tickMark.isStateful()) {
+                tickMark.setState(getDrawableState());
+            }
+            applyTickMarkTint();
+        }
+
+        invalidate();
+    }
+
+    /**
+     * @return the drawable displayed at each progress position
+     */
+    public Drawable getTickMark() {
+        return mTickMark;
+    }
+
+    /**
+     * Applies a tint to the tick mark drawable. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setTickMark(Drawable)} will automatically
+     * mutate the drawable and apply the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#SeekBar_tickMarkTint
+     * @see #getTickMarkTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setTickMarkTintList(@Nullable ColorStateList tint) {
+        mTickMarkTintList = tint;
+        mHasTickMarkTint = true;
+
+        applyTickMarkTint();
+    }
+
+    /**
+     * Returns the tint applied to the tick mark drawable, if specified.
+     *
+     * @return the tint applied to the tick mark drawable
+     * @attr ref android.R.styleable#SeekBar_tickMarkTint
+     * @see #setTickMarkTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getTickMarkTintList() {
+        return mTickMarkTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setTickMarkTintList(ColorStateList)}} to the tick mark drawable. The
+     * default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
+     * @see #getTickMarkTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setTickMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mTickMarkTintMode = tintMode;
+        mHasTickMarkTintMode = true;
+
+        applyTickMarkTint();
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the tick mark drawable,
+     * if specified.
+     *
+     * @return the blending mode used to apply the tint to the tick mark drawable
+     * @attr ref android.R.styleable#SeekBar_tickMarkTintMode
+     * @see #setTickMarkTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getTickMarkTintMode() {
+        return mTickMarkTintMode;
+    }
+
+    private void applyTickMarkTint() {
+        if (mTickMark != null && (mHasTickMarkTint || mHasTickMarkTintMode)) {
+            mTickMark = mTickMark.mutate();
+
+            if (mHasTickMarkTint) {
+                mTickMark.setTintList(mTickMarkTintList);
+            }
+
+            if (mHasTickMarkTintMode) {
+                mTickMark.setTintMode(mTickMarkTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mTickMark.isStateful()) {
+                mTickMark.setState(getDrawableState());
+            }
+        }
+    }
+
+    /**
+     * Sets the amount of progress changed via the arrow keys.
+     *
+     * @param increment The amount to increment or decrement when the user
+     *            presses the arrow keys.
+     */
+    public void setKeyProgressIncrement(int increment) {
+        mKeyProgressIncrement = increment < 0 ? -increment : increment;
+    }
+
+    /**
+     * Returns the amount of progress changed via the arrow keys.
+     * <p>
+     * By default, this will be a value that is derived from the progress range.
+     *
+     * @return The amount to increment or decrement when the user presses the
+     *         arrow keys. This will be positive.
+     */
+    public int getKeyProgressIncrement() {
+        return mKeyProgressIncrement;
+    }
+
+    @Override
+    public synchronized void setMin(int min) {
+        super.setMin(min);
+        int range = getMax() - getMin();
+
+        if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
+
+            // It will take the user too long to change this via keys, change it
+            // to something more reasonable
+            setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
+        }
+    }
+
+    @Override
+    public synchronized void setMax(int max) {
+        super.setMax(max);
+        int range = getMax() - getMin();
+
+        if ((mKeyProgressIncrement == 0) || (range / mKeyProgressIncrement > 20)) {
+            // It will take the user too long to change this via keys, change it
+            // to something more reasonable
+            setKeyProgressIncrement(Math.max(1, Math.round((float) range / 20)));
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return who == mThumb || who == mTickMark || super.verifyDrawable(who);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+
+        if (mThumb != null) {
+            mThumb.jumpToCurrentState();
+        }
+
+        if (mTickMark != null) {
+            mTickMark.jumpToCurrentState();
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable progressDrawable = getProgressDrawable();
+        if (progressDrawable != null && mDisabledAlpha < 1.0f) {
+            progressDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+        }
+
+        final Drawable thumb = mThumb;
+        if (thumb != null && thumb.isStateful()
+                && thumb.setState(getDrawableState())) {
+            invalidateDrawable(thumb);
+        }
+
+        final Drawable tickMark = mTickMark;
+        if (tickMark != null && tickMark.isStateful()
+                && tickMark.setState(getDrawableState())) {
+            invalidateDrawable(tickMark);
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mThumb != null) {
+            mThumb.setHotspot(x, y);
+        }
+    }
+
+    @Override
+    void onVisualProgressChanged(int id, float scale) {
+        super.onVisualProgressChanged(id, scale);
+
+        if (id == R.id.progress) {
+            final Drawable thumb = mThumb;
+            if (thumb != null) {
+                setThumbPos(getWidth(), thumb, scale, Integer.MIN_VALUE);
+
+                // Since we draw translated, the drawable's bounds that it signals
+                // for invalidation won't be the actual bounds we want invalidated,
+                // so just invalidate this whole view.
+                invalidate();
+            }
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        updateThumbAndTrackPos(w, h);
+    }
+
+    private void updateThumbAndTrackPos(int w, int h) {
+        final int paddedHeight = h - mPaddingTop - mPaddingBottom;
+        final Drawable track = getCurrentDrawable();
+        final Drawable thumb = mThumb;
+
+        // The max height does not incorporate padding, whereas the height
+        // parameter does.
+        final int trackHeight = Math.min(mMaxHeight, paddedHeight);
+        final int thumbHeight = thumb == null ? 0 : thumb.getIntrinsicHeight();
+
+        // Apply offset to whichever item is taller.
+        final int trackOffset;
+        final int thumbOffset;
+        if (thumbHeight > trackHeight) {
+            final int offsetHeight = (paddedHeight - thumbHeight) / 2;
+            trackOffset = offsetHeight + (thumbHeight - trackHeight) / 2;
+            thumbOffset = offsetHeight;
+        } else {
+            final int offsetHeight = (paddedHeight - trackHeight) / 2;
+            trackOffset = offsetHeight;
+            thumbOffset = offsetHeight + (trackHeight - thumbHeight) / 2;
+        }
+
+        if (track != null) {
+            final int trackWidth = w - mPaddingRight - mPaddingLeft;
+            track.setBounds(0, trackOffset, trackWidth, trackOffset + trackHeight);
+        }
+
+        if (thumb != null) {
+            setThumbPos(w, thumb, getScale(), thumbOffset);
+        }
+    }
+
+    private float getScale() {
+        int min = getMin();
+        int max = getMax();
+        int range = max - min;
+        return range > 0 ? (getProgress() - min) / (float) range : 0;
+    }
+
+    /**
+     * Updates the thumb drawable bounds.
+     *
+     * @param w Width of the view, including padding
+     * @param thumb Drawable used for the thumb
+     * @param scale Current progress between 0 and 1
+     * @param offset Vertical offset for centering. If set to
+     *            {@link Integer#MIN_VALUE}, the current offset will be used.
+     */
+    private void setThumbPos(int w, Drawable thumb, float scale, int offset) {
+        int available = w - mPaddingLeft - mPaddingRight;
+        final int thumbWidth = thumb.getIntrinsicWidth();
+        final int thumbHeight = thumb.getIntrinsicHeight();
+        available -= thumbWidth;
+
+        // The extra space for the thumb to move on the track
+        available += mThumbOffset * 2;
+
+        final int thumbPos = (int) (scale * available + 0.5f);
+
+        final int top, bottom;
+        if (offset == Integer.MIN_VALUE) {
+            final Rect oldBounds = thumb.getBounds();
+            top = oldBounds.top;
+            bottom = oldBounds.bottom;
+        } else {
+            top = offset;
+            bottom = offset + thumbHeight;
+        }
+
+        final int left = (isLayoutRtl() && mMirrorForRtl) ? available - thumbPos : thumbPos;
+        final int right = left + thumbWidth;
+
+        final Drawable background = getBackground();
+        if (background != null) {
+            final int offsetX = mPaddingLeft - mThumbOffset;
+            final int offsetY = mPaddingTop;
+            background.setHotspotBounds(left + offsetX, top + offsetY,
+                    right + offsetX, bottom + offsetY);
+        }
+
+        // Canvas will be translated, so 0,0 is where we start drawing
+        thumb.setBounds(left, top, right, bottom);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onResolveDrawables(int layoutDirection) {
+        super.onResolveDrawables(layoutDirection);
+
+        if (mThumb != null) {
+            mThumb.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    @Override
+    protected synchronized void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+        drawThumb(canvas);
+    }
+
+    @Override
+    void drawTrack(Canvas canvas) {
+        final Drawable thumbDrawable = mThumb;
+        if (thumbDrawable != null && mSplitTrack) {
+            final Insets insets = thumbDrawable.getOpticalInsets();
+            final Rect tempRect = mTempRect;
+            thumbDrawable.copyBounds(tempRect);
+            tempRect.offset(mPaddingLeft - mThumbOffset, mPaddingTop);
+            tempRect.left += insets.left;
+            tempRect.right -= insets.right;
+
+            final int saveCount = canvas.save();
+            canvas.clipRect(tempRect, Op.DIFFERENCE);
+            super.drawTrack(canvas);
+            drawTickMarks(canvas);
+            canvas.restoreToCount(saveCount);
+        } else {
+            super.drawTrack(canvas);
+            drawTickMarks(canvas);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    protected void drawTickMarks(Canvas canvas) {
+        if (mTickMark != null) {
+            final int count = getMax() - getMin();
+            if (count > 1) {
+                final int w = mTickMark.getIntrinsicWidth();
+                final int h = mTickMark.getIntrinsicHeight();
+                final int halfW = w >= 0 ? w / 2 : 1;
+                final int halfH = h >= 0 ? h / 2 : 1;
+                mTickMark.setBounds(-halfW, -halfH, halfW, halfH);
+
+                final float spacing = (getWidth() - mPaddingLeft - mPaddingRight) / (float) count;
+                final int saveCount = canvas.save();
+                canvas.translate(mPaddingLeft, getHeight() / 2);
+                for (int i = 0; i <= count; i++) {
+                    mTickMark.draw(canvas);
+                    canvas.translate(spacing, 0);
+                }
+                canvas.restoreToCount(saveCount);
+            }
+        }
+    }
+
+    /**
+     * Draw the thumb.
+     */
+    void drawThumb(Canvas canvas) {
+        if (mThumb != null) {
+            final int saveCount = canvas.save();
+            // Translate the padding. For the x, we need to allow the thumb to
+            // draw in its extra space
+            canvas.translate(mPaddingLeft - mThumbOffset, mPaddingTop);
+            mThumb.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    @Override
+    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        Drawable d = getCurrentDrawable();
+
+        int thumbHeight = mThumb == null ? 0 : mThumb.getIntrinsicHeight();
+        int dw = 0;
+        int dh = 0;
+        if (d != null) {
+            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+            dh = Math.max(thumbHeight, dh);
+        }
+        dw += mPaddingLeft + mPaddingRight;
+        dh += mPaddingTop + mPaddingBottom;
+
+        setMeasuredDimension(resolveSizeAndState(dw, widthMeasureSpec, 0),
+                resolveSizeAndState(dh, heightMeasureSpec, 0));
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mIsUserSeekable || !isEnabled()) {
+            return false;
+        }
+
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                if (isInScrollingContainer()) {
+                    mTouchDownX = event.getX();
+                } else {
+                    startDrag(event);
+                }
+                break;
+
+            case MotionEvent.ACTION_MOVE:
+                if (mIsDragging) {
+                    trackTouchEvent(event);
+                } else {
+                    final float x = event.getX();
+                    if (Math.abs(x - mTouchDownX) > mScaledTouchSlop) {
+                        startDrag(event);
+                    }
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                if (mIsDragging) {
+                    trackTouchEvent(event);
+                    onStopTrackingTouch();
+                    setPressed(false);
+                } else {
+                    // Touch up when we never crossed the touch slop threshold should
+                    // be interpreted as a tap-seek to that location.
+                    onStartTrackingTouch();
+                    trackTouchEvent(event);
+                    onStopTrackingTouch();
+                }
+                // ProgressBar doesn't know to repaint the thumb drawable
+                // in its inactive state when the touch stops (because the
+                // value has not apparently changed)
+                invalidate();
+                break;
+
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsDragging) {
+                    onStopTrackingTouch();
+                    setPressed(false);
+                }
+                invalidate(); // see above explanation
+                break;
+        }
+        return true;
+    }
+
+    private void startDrag(MotionEvent event) {
+        setPressed(true);
+
+        if (mThumb != null) {
+            // This may be within the padding region.
+            invalidate(mThumb.getBounds());
+        }
+
+        onStartTrackingTouch();
+        trackTouchEvent(event);
+        attemptClaimDrag();
+    }
+
+    private void setHotspot(float x, float y) {
+        final Drawable bg = getBackground();
+        if (bg != null) {
+            bg.setHotspot(x, y);
+        }
+    }
+
+    private void trackTouchEvent(MotionEvent event) {
+        final int x = Math.round(event.getX());
+        final int y = Math.round(event.getY());
+        final int width = getWidth();
+        final int availableWidth = width - mPaddingLeft - mPaddingRight;
+
+        final float scale;
+        float progress = 0.0f;
+        if (isLayoutRtl() && mMirrorForRtl) {
+            if (x > width - mPaddingRight) {
+                scale = 0.0f;
+            } else if (x < mPaddingLeft) {
+                scale = 1.0f;
+            } else {
+                scale = (availableWidth - x + mPaddingLeft) / (float) availableWidth;
+                progress = mTouchProgressOffset;
+            }
+        } else {
+            if (x < mPaddingLeft) {
+                scale = 0.0f;
+            } else if (x > width - mPaddingRight) {
+                scale = 1.0f;
+            } else {
+                scale = (x - mPaddingLeft) / (float) availableWidth;
+                progress = mTouchProgressOffset;
+            }
+        }
+
+        final int range = getMax() - getMin();
+        progress += scale * range;
+
+        setHotspot(x, y);
+        setProgressInternal(Math.round(progress), true, false);
+    }
+
+    /**
+     * Tries to claim the user's drag motion, and requests disallowing any
+     * ancestors from stealing events in the drag.
+     */
+    private void attemptClaimDrag() {
+        if (mParent != null) {
+            mParent.requestDisallowInterceptTouchEvent(true);
+        }
+    }
+
+    /**
+     * This is called when the user has started touching this widget.
+     */
+    void onStartTrackingTouch() {
+        mIsDragging = true;
+    }
+
+    /**
+     * This is called when the user either releases his touch or the touch is
+     * canceled.
+     */
+    void onStopTrackingTouch() {
+        mIsDragging = false;
+    }
+
+    /**
+     * Called when the user changes the seekbar's progress by using a key event.
+     */
+    void onKeyChange() {
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (isEnabled()) {
+            int increment = mKeyProgressIncrement;
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                case KeyEvent.KEYCODE_MINUS:
+                    increment = -increment;
+                    // fallthrough
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                case KeyEvent.KEYCODE_PLUS:
+                case KeyEvent.KEYCODE_EQUALS:
+                    increment = isLayoutRtl() ? -increment : increment;
+
+                    if (setProgressInternal(getProgress() + increment, true, true)) {
+                        onKeyChange();
+                        return true;
+                    }
+                    break;
+            }
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AbsSeekBar.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (isEnabled()) {
+            final int progress = getProgress();
+            if (progress > getMin()) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+            }
+            if (progress < getMax()) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+
+        if (!isEnabled()) {
+            return false;
+        }
+
+        switch (action) {
+            case R.id.accessibilityActionSetProgress: {
+                if (!canUserSetProgress()) {
+                    return false;
+                }
+                if (arguments == null || !arguments.containsKey(
+                        AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE)) {
+                    return false;
+                }
+                float value = arguments.getFloat(
+                        AccessibilityNodeInfo.ACTION_ARGUMENT_PROGRESS_VALUE);
+                return setProgressInternal((int) value, true, true);
+            }
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+                if (!canUserSetProgress()) {
+                    return false;
+                }
+                int range = getMax() - getMin();
+                int increment = Math.max(1, Math.round((float) range / 20));
+                if (action == AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD) {
+                    increment = -increment;
+                }
+
+                // Let progress bar handle clamping values.
+                if (setProgressInternal(getProgress() + increment, true, true)) {
+                    onKeyChange();
+                    return true;
+                }
+                return false;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * @return whether user can change progress on the view
+     */
+    boolean canUserSetProgress() {
+        return !isIndeterminate() && isEnabled();
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        final Drawable thumb = mThumb;
+        if (thumb != null) {
+            setThumbPos(getWidth(), thumb, getScale(), Integer.MIN_VALUE);
+
+            // Since we draw translated, the drawable's bounds that it signals
+            // for invalidation won't be the actual bounds we want invalidated,
+            // so just invalidate this whole view.
+            invalidate();
+        }
+    }
+}
diff --git a/android/widget/AbsSpinner.java b/android/widget/AbsSpinner.java
new file mode 100644
index 0000000..816c949
--- /dev/null
+++ b/android/widget/AbsSpinner.java
@@ -0,0 +1,515 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+/**
+ * An abstract base class for spinner widgets. SDK users will probably not
+ * need to use this class.
+ *
+ * @attr ref android.R.styleable#AbsSpinner_entries
+ */
+public abstract class AbsSpinner extends AdapterView<SpinnerAdapter> {
+    private static final String LOG_TAG = AbsSpinner.class.getSimpleName();
+
+    SpinnerAdapter mAdapter;
+
+    int mHeightMeasureSpec;
+    int mWidthMeasureSpec;
+
+    int mSelectionLeftPadding = 0;
+    int mSelectionTopPadding = 0;
+    int mSelectionRightPadding = 0;
+    int mSelectionBottomPadding = 0;
+    final Rect mSpinnerPadding = new Rect();
+
+    final RecycleBin mRecycler = new RecycleBin();
+    private DataSetObserver mDataSetObserver;
+
+    /** Temporary frame to hold a child View's frame rectangle */
+    private Rect mTouchFrame;
+
+    public AbsSpinner(Context context) {
+        super(context);
+        initAbsSpinner();
+    }
+
+    public AbsSpinner(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AbsSpinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // Spinner is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        initAbsSpinner();
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.AbsSpinner, defStyleAttr, defStyleRes);
+
+        final CharSequence[] entries = a.getTextArray(R.styleable.AbsSpinner_entries);
+        if (entries != null) {
+            final ArrayAdapter<CharSequence> adapter = new ArrayAdapter<CharSequence>(
+                    context, R.layout.simple_spinner_item, entries);
+            adapter.setDropDownViewResource(R.layout.simple_spinner_dropdown_item);
+            setAdapter(adapter);
+        }
+
+        a.recycle();
+    }
+
+    /**
+     * Common code for different constructor flavors
+     */
+    private void initAbsSpinner() {
+        setFocusable(true);
+        setWillNotDraw(false);
+    }
+
+    /**
+     * The Adapter is used to provide the data which backs this Spinner.
+     * It also provides methods to transform spinner items based on their position
+     * relative to the selected item.
+     * @param adapter The SpinnerAdapter to use for this Spinner
+     */
+    @Override
+    public void setAdapter(SpinnerAdapter adapter) {
+        if (null != mAdapter) {
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
+            resetList();
+        }
+
+        mAdapter = adapter;
+
+        mOldSelectedPosition = INVALID_POSITION;
+        mOldSelectedRowId = INVALID_ROW_ID;
+
+        if (mAdapter != null) {
+            mOldItemCount = mItemCount;
+            mItemCount = mAdapter.getCount();
+            checkFocus();
+
+            mDataSetObserver = new AdapterDataSetObserver();
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+
+            int position = mItemCount > 0 ? 0 : INVALID_POSITION;
+
+            setSelectedPositionInt(position);
+            setNextSelectedPositionInt(position);
+
+            if (mItemCount == 0) {
+                // Nothing selected
+                checkSelectionChanged();
+            }
+
+        } else {
+            checkFocus();
+            resetList();
+            // Nothing selected
+            checkSelectionChanged();
+        }
+
+        requestLayout();
+    }
+
+    /**
+     * Clear out all children from the list
+     */
+    void resetList() {
+        mDataChanged = false;
+        mNeedSync = false;
+
+        removeAllViewsInLayout();
+        mOldSelectedPosition = INVALID_POSITION;
+        mOldSelectedRowId = INVALID_ROW_ID;
+
+        setSelectedPositionInt(INVALID_POSITION);
+        setNextSelectedPositionInt(INVALID_POSITION);
+        invalidate();
+    }
+
+    /**
+     * @see android.view.View#measure(int, int)
+     *
+     * Figure out the dimensions of this Spinner. The width comes from
+     * the widthMeasureSpec as Spinnners can't have their width set to
+     * UNSPECIFIED. The height is based on the height of the selected item
+     * plus padding.
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSize;
+        int heightSize;
+
+        mSpinnerPadding.left = mPaddingLeft > mSelectionLeftPadding ? mPaddingLeft
+                : mSelectionLeftPadding;
+        mSpinnerPadding.top = mPaddingTop > mSelectionTopPadding ? mPaddingTop
+                : mSelectionTopPadding;
+        mSpinnerPadding.right = mPaddingRight > mSelectionRightPadding ? mPaddingRight
+                : mSelectionRightPadding;
+        mSpinnerPadding.bottom = mPaddingBottom > mSelectionBottomPadding ? mPaddingBottom
+                : mSelectionBottomPadding;
+
+        if (mDataChanged) {
+            handleDataChanged();
+        }
+
+        int preferredHeight = 0;
+        int preferredWidth = 0;
+        boolean needsMeasuring = true;
+
+        int selectedPosition = getSelectedItemPosition();
+        if (selectedPosition >= 0 && mAdapter != null && selectedPosition < mAdapter.getCount()) {
+            // Try looking in the recycler. (Maybe we were measured once already)
+            View view = mRecycler.get(selectedPosition);
+            if (view == null) {
+                // Make a new one
+                view = mAdapter.getView(selectedPosition, null, this);
+
+                if (view.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+                    view.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+                }
+            }
+
+            if (view != null) {
+                // Put in recycler for re-measuring and/or layout
+                mRecycler.put(selectedPosition, view);
+
+                if (view.getLayoutParams() == null) {
+                    mBlockLayoutRequests = true;
+                    view.setLayoutParams(generateDefaultLayoutParams());
+                    mBlockLayoutRequests = false;
+                }
+                measureChild(view, widthMeasureSpec, heightMeasureSpec);
+
+                preferredHeight = getChildHeight(view) + mSpinnerPadding.top + mSpinnerPadding.bottom;
+                preferredWidth = getChildWidth(view) + mSpinnerPadding.left + mSpinnerPadding.right;
+
+                needsMeasuring = false;
+            }
+        }
+
+        if (needsMeasuring) {
+            // No views -- just use padding
+            preferredHeight = mSpinnerPadding.top + mSpinnerPadding.bottom;
+            if (widthMode == MeasureSpec.UNSPECIFIED) {
+                preferredWidth = mSpinnerPadding.left + mSpinnerPadding.right;
+            }
+        }
+
+        preferredHeight = Math.max(preferredHeight, getSuggestedMinimumHeight());
+        preferredWidth = Math.max(preferredWidth, getSuggestedMinimumWidth());
+
+        heightSize = resolveSizeAndState(preferredHeight, heightMeasureSpec, 0);
+        widthSize = resolveSizeAndState(preferredWidth, widthMeasureSpec, 0);
+
+        setMeasuredDimension(widthSize, heightSize);
+        mHeightMeasureSpec = heightMeasureSpec;
+        mWidthMeasureSpec = widthMeasureSpec;
+    }
+
+    int getChildHeight(View child) {
+        return child.getMeasuredHeight();
+    }
+
+    int getChildWidth(View child) {
+        return child.getMeasuredWidth();
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    void recycleAllViews() {
+        final int childCount = getChildCount();
+        final AbsSpinner.RecycleBin recycleBin = mRecycler;
+        final int position = mFirstPosition;
+
+        // All views go in recycler
+        for (int i = 0; i < childCount; i++) {
+            View v = getChildAt(i);
+            int index = position + i;
+            recycleBin.put(index, v);
+        }
+    }
+
+    /**
+     * Jump directly to a specific item in the adapter data.
+     */
+    public void setSelection(int position, boolean animate) {
+        // Animate only if requested position is already on screen somewhere
+        boolean shouldAnimate = animate && mFirstPosition <= position &&
+                position <= mFirstPosition + getChildCount() - 1;
+        setSelectionInt(position, shouldAnimate);
+    }
+
+    @Override
+    public void setSelection(int position) {
+        setNextSelectedPositionInt(position);
+        requestLayout();
+        invalidate();
+    }
+
+
+    /**
+     * Makes the item at the supplied position selected.
+     *
+     * @param position Position to select
+     * @param animate Should the transition be animated
+     *
+     */
+    void setSelectionInt(int position, boolean animate) {
+        if (position != mOldSelectedPosition) {
+            mBlockLayoutRequests = true;
+            int delta  = position - mSelectedPosition;
+            setNextSelectedPositionInt(position);
+            layout(delta, animate);
+            mBlockLayoutRequests = false;
+        }
+    }
+
+    abstract void layout(int delta, boolean animate);
+
+    @Override
+    public View getSelectedView() {
+        if (mItemCount > 0 && mSelectedPosition >= 0) {
+            return getChildAt(mSelectedPosition - mFirstPosition);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Override to prevent spamming ourselves with layout requests
+     * as we place views
+     *
+     * @see android.view.View#requestLayout()
+     */
+    @Override
+    public void requestLayout() {
+        if (!mBlockLayoutRequests) {
+            super.requestLayout();
+        }
+    }
+
+    @Override
+    public SpinnerAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public int getCount() {
+        return mItemCount;
+    }
+
+    /**
+     * Maps a point to a position in the list.
+     *
+     * @param x X in local coordinate
+     * @param y Y in local coordinate
+     * @return The position of the item which contains the specified point, or
+     *         {@link #INVALID_POSITION} if the point does not intersect an item.
+     */
+    public int pointToPosition(int x, int y) {
+        Rect frame = mTouchFrame;
+        if (frame == null) {
+            mTouchFrame = new Rect();
+            frame = mTouchFrame;
+        }
+
+        final int count = getChildCount();
+        for (int i = count - 1; i >= 0; i--) {
+            View child = getChildAt(i);
+            if (child.getVisibility() == View.VISIBLE) {
+                child.getHitRect(frame);
+                if (frame.contains(x, y)) {
+                    return mFirstPosition + i;
+                }
+            }
+        }
+        return INVALID_POSITION;
+    }
+
+    @Override
+    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+        super.dispatchRestoreInstanceState(container);
+        // Restores the selected position when Spinner gets restored,
+        // rather than wait until the next measure/layout pass to do it.
+        handleDataChanged();
+    }
+
+    static class SavedState extends BaseSavedState {
+        long selectedId;
+        int position;
+
+        /**
+         * Constructor called from {@link AbsSpinner#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        SavedState(Parcel in) {
+            super(in);
+            selectedId = in.readLong();
+            position = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeLong(selectedId);
+            out.writeInt(position);
+        }
+
+        @Override
+        public String toString() {
+            return "AbsSpinner.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " selectedId=" + selectedId
+                    + " position=" + position + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.selectedId = getSelectedItemId();
+        if (ss.selectedId >= 0) {
+            ss.position = getSelectedItemPosition();
+        } else {
+            ss.position = INVALID_POSITION;
+        }
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (ss.selectedId >= 0) {
+            mDataChanged = true;
+            mNeedSync = true;
+            mSyncRowId = ss.selectedId;
+            mSyncPosition = ss.position;
+            mSyncMode = SYNC_SELECTED_POSITION;
+            requestLayout();
+        }
+    }
+
+    class RecycleBin {
+        private final SparseArray<View> mScrapHeap = new SparseArray<View>();
+
+        public void put(int position, View v) {
+            mScrapHeap.put(position, v);
+        }
+
+        View get(int position) {
+            // System.out.print("Looking for " + position);
+            View result = mScrapHeap.get(position);
+            if (result != null) {
+                // System.out.println(" HIT");
+                mScrapHeap.delete(position);
+            } else {
+                // System.out.println(" MISS");
+            }
+            return result;
+        }
+
+        void clear() {
+            final SparseArray<View> scrapHeap = mScrapHeap;
+            final int count = scrapHeap.size();
+            for (int i = 0; i < count; i++) {
+                final View view = scrapHeap.valueAt(i);
+                if (view != null) {
+                    removeDetachedView(view, true);
+                }
+            }
+            scrapHeap.clear();
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AbsSpinner.class.getName();
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!isEnabled()) return;
+
+        if (!value.isList()) {
+            Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+            return;
+        }
+
+        setSelection(value.getListValue());
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
+    }
+
+    @Override
+    public AutofillValue getAutofillValue() {
+        return isEnabled() ? AutofillValue.forList(getSelectedItemPosition()) : null;
+    }
+}
diff --git a/android/widget/AbsoluteLayout.java b/android/widget/AbsoluteLayout.java
new file mode 100644
index 0000000..4ce0d5d
--- /dev/null
+++ b/android/widget/AbsoluteLayout.java
@@ -0,0 +1,228 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * A layout that lets you specify exact locations (x/y coordinates) of its
+ * children. Absolute layouts are less flexible and harder to maintain than
+ * other types of layouts without absolute positioning.
+ *
+ * <p><strong>XML attributes</strong></p> <p> See {@link
+ * android.R.styleable#ViewGroup ViewGroup Attributes}, {@link
+ * android.R.styleable#View View Attributes}</p>
+ * 
+ * @deprecated Use {@link android.widget.FrameLayout}, {@link android.widget.RelativeLayout}
+ *             or a custom layout instead.
+ */
+@Deprecated
+@RemoteView
+public class AbsoluteLayout extends ViewGroup {
+    public AbsoluteLayout(Context context) {
+        this(context, null);
+    }
+
+    public AbsoluteLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AbsoluteLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+
+        // Find out how big everyone wants to be
+        measureChildren(widthMeasureSpec, heightMeasureSpec);
+
+        // Find rightmost and bottom-most child
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                int childRight;
+                int childBottom;
+
+                AbsoluteLayout.LayoutParams lp
+                        = (AbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+                childRight = lp.x + child.getMeasuredWidth();
+                childBottom = lp.y + child.getMeasuredHeight();
+
+                maxWidth = Math.max(maxWidth, childRight);
+                maxHeight = Math.max(maxHeight, childBottom);
+            }
+        }
+
+        // Account for padding too
+        maxWidth += mPaddingLeft + mPaddingRight;
+        maxHeight += mPaddingTop + mPaddingBottom;
+
+        // Check against minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+        
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, 0),
+                resolveSizeAndState(maxHeight, heightMeasureSpec, 0));
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
+     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+     * and with the coordinates (0, 0).
+     */
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 0, 0);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t,
+            int r, int b) {
+        int count = getChildCount();
+
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+
+                AbsoluteLayout.LayoutParams lp =
+                        (AbsoluteLayout.LayoutParams) child.getLayoutParams();
+
+                int childLeft = mPaddingLeft + lp.x;
+                int childTop = mPaddingTop + lp.y;
+                child.layout(childLeft, childTop,
+                        childLeft + child.getMeasuredWidth(),
+                        childTop + child.getMeasuredHeight());
+
+            }
+        }
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new AbsoluteLayout.LayoutParams(getContext(), attrs);
+    }
+
+    // Override to allow type-checking of LayoutParams. 
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof AbsoluteLayout.LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    /**
+     * Per-child layout information associated with AbsoluteLayout.
+     * See
+     * {@link android.R.styleable#AbsoluteLayout_Layout Absolute Layout Attributes}
+     * for a list of all child view attributes that this class supports.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        /**
+         * The horizontal, or X, location of the child within the view group.
+         */
+        public int x;
+        /**
+         * The vertical, or Y, location of the child within the view group.
+         */
+        public int y;
+
+        /**
+         * Creates a new set of layout parameters with the specified width,
+         * height and location.
+         *
+         * @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 x the X location of the child
+         * @param y the Y location of the child
+         */
+        public LayoutParams(int width, int height, int x, int y) {
+            super(width, height);
+            this.x = x;
+            this.y = y;
+        }
+
+        /**
+         * Creates a new set of layout parameters. The values are extracted from
+         * the supplied attributes set and context. The XML attributes mapped
+         * to this set of layout parameters are:
+         *
+         * <ul>
+         *   <li><code>layout_x</code>: the X location of the child</li>
+         *   <li><code>layout_y</code>: the Y location of the child</li>
+         *   <li>All the XML attributes from
+         *   {@link android.view.ViewGroup.LayoutParams}</li>
+         * </ul>
+         *
+         * @param c the application environment
+         * @param attrs the set of attributes from which to extract the layout
+         *              parameters values
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            TypedArray a = c.obtainStyledAttributes(attrs,
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout);
+            x = a.getDimensionPixelOffset(
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_x, 0);
+            y = a.getDimensionPixelOffset(
+                    com.android.internal.R.styleable.AbsoluteLayout_Layout_layout_y, 0);
+            a.recycle();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        @Override
+        public String debug(String output) {
+            return output + "Absolute.LayoutParams={width="
+                    + sizeToString(width) + ", height=" + sizeToString(height)
+                    + " x=" + x + " y=" + y + "}";
+        }
+    }
+}
+
+
diff --git a/android/widget/AccessibilityIterators.java b/android/widget/AccessibilityIterators.java
new file mode 100644
index 0000000..442ffa1
--- /dev/null
+++ b/android/widget/AccessibilityIterators.java
@@ -0,0 +1,197 @@
+/*
+ * 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 android.widget;
+
+import android.graphics.Rect;
+import android.text.Layout;
+import android.text.Spannable;
+import android.view.AccessibilityIterators.AbstractTextSegmentIterator;
+
+/**
+ * This class contains the implementation of text segment iterators
+ * for accessibility support.
+ */
+final class AccessibilityIterators {
+
+    static class LineTextSegmentIterator extends AbstractTextSegmentIterator {
+        private static LineTextSegmentIterator sLineInstance;
+
+        protected static final int DIRECTION_START = -1;
+        protected static final int DIRECTION_END = 1;
+
+        protected Layout mLayout;
+
+        public static LineTextSegmentIterator getInstance() {
+            if (sLineInstance == null) {
+                sLineInstance = new LineTextSegmentIterator();
+            }
+            return sLineInstance;
+        }
+
+        public void initialize(Spannable text, Layout layout) {
+            mText = text.toString();
+            mLayout = layout;
+        }
+
+        @Override
+        public int[] following(int offset) {
+            final int textLegth = mText.length();
+            if (textLegth <= 0) {
+                return null;
+            }
+            if (offset >= mText.length()) {
+                return null;
+            }
+            int nextLine;
+            if (offset < 0) {
+                nextLine = mLayout.getLineForOffset(0);
+            } else {
+                final int currentLine = mLayout.getLineForOffset(offset);
+                if (getLineEdgeIndex(currentLine, DIRECTION_START) == offset) {
+                    nextLine = currentLine;
+                } else {
+                    nextLine = currentLine + 1;
+                }
+            }
+            if (nextLine >= mLayout.getLineCount()) {
+                return null;
+            }
+            final int start = getLineEdgeIndex(nextLine, DIRECTION_START);
+            final int end = getLineEdgeIndex(nextLine, DIRECTION_END) + 1;
+            return getRange(start, end);
+        }
+
+        @Override
+        public int[] preceding(int offset) {
+            final int textLegth = mText.length();
+            if (textLegth <= 0) {
+                return null;
+            }
+            if (offset <= 0) {
+                return null;
+            }
+            int previousLine;
+            if (offset > mText.length()) {
+                previousLine = mLayout.getLineForOffset(mText.length());
+            } else {
+                final int currentLine = mLayout.getLineForOffset(offset);
+                if (getLineEdgeIndex(currentLine, DIRECTION_END) + 1 == offset) {
+                    previousLine = currentLine;
+                } else {
+                    previousLine = currentLine - 1;
+                }
+            }
+            if (previousLine < 0) {
+                return null;
+            }
+            final int start = getLineEdgeIndex(previousLine, DIRECTION_START);
+            final int end = getLineEdgeIndex(previousLine, DIRECTION_END) + 1;
+            return getRange(start, end);
+        }
+
+        protected int getLineEdgeIndex(int lineNumber, int direction) {
+            final int paragraphDirection = mLayout.getParagraphDirection(lineNumber);
+            if (direction * paragraphDirection < 0) {
+                return mLayout.getLineStart(lineNumber);
+            } else {
+                return mLayout.getLineEnd(lineNumber) - 1;
+            }
+        }
+    }
+
+    static class PageTextSegmentIterator extends LineTextSegmentIterator {
+        private static PageTextSegmentIterator sPageInstance;
+
+        private TextView mView;
+
+        private final Rect mTempRect = new Rect();
+
+        public static PageTextSegmentIterator getInstance() {
+            if (sPageInstance == null) {
+                sPageInstance = new PageTextSegmentIterator();
+            }
+            return sPageInstance;
+        }
+
+        public void initialize(TextView view) {
+            super.initialize((Spannable) view.getIterableTextForAccessibility(), view.getLayout());
+            mView = view;
+        }
+
+        @Override
+        public int[] following(int offset) {
+            final int textLength = mText.length();
+            if (textLength <= 0) {
+                return null;
+            }
+            if (offset >= mText.length()) {
+                return null;
+            }
+            if (!mView.getGlobalVisibleRect(mTempRect)) {
+                return null;
+            }
+
+            final int start = Math.max(0, offset);
+
+            final int currentLine = mLayout.getLineForOffset(start);
+            final int currentLineTop = mLayout.getLineTop(currentLine);
+            final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
+                    - mView.getTotalPaddingBottom();
+            final int nextPageStartY = currentLineTop + pageHeight;
+            final int lastLineTop = mLayout.getLineTop(mLayout.getLineCount() - 1);
+            final int currentPageEndLine = (nextPageStartY < lastLineTop)
+                    ? mLayout.getLineForVertical(nextPageStartY) - 1 : mLayout.getLineCount() - 1;
+
+            final int end = getLineEdgeIndex(currentPageEndLine, DIRECTION_END) + 1;
+
+            return getRange(start, end);
+        }
+
+        @Override
+        public int[] preceding(int offset) {
+            final int textLength = mText.length();
+            if (textLength <= 0) {
+                return null;
+            }
+            if (offset <= 0) {
+                return null;
+            }
+            if (!mView.getGlobalVisibleRect(mTempRect)) {
+                return null;
+            }
+
+            final int end = Math.min(mText.length(), offset);
+
+            final int currentLine = mLayout.getLineForOffset(end);
+            final int currentLineTop = mLayout.getLineTop(currentLine);
+            final int pageHeight = mTempRect.height() - mView.getTotalPaddingTop()
+                    - mView.getTotalPaddingBottom();
+            final int previousPageEndY = currentLineTop - pageHeight;
+            int currentPageStartLine = (previousPageEndY > 0) ?
+                     mLayout.getLineForVertical(previousPageEndY) : 0;
+            // If we're at the end of text, we're at the end of the current line rather than the
+            // start of the next line, so we should move up one fewer lines than we would otherwise.
+            if (end == mText.length() && (currentPageStartLine < currentLine)) {
+                currentPageStartLine += 1;
+            }
+
+            final int start = getLineEdgeIndex(currentPageStartLine, DIRECTION_START);
+
+            return getRange(start, end);
+        }
+    }
+}
diff --git a/android/widget/ActionMenuPresenter.java b/android/widget/ActionMenuPresenter.java
new file mode 100644
index 0000000..46269c6
--- /dev/null
+++ b/android/widget/ActionMenuPresenter.java
@@ -0,0 +1,1070 @@
+/*
+ * 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 android.widget;
+
+import android.animation.Animator;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.view.ActionProvider;
+import android.view.Gravity;
+import android.view.MenuItem;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.view.ActionBarPolicy;
+import com.android.internal.view.menu.ActionMenuItemView;
+import com.android.internal.view.menu.BaseMenuPresenter;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.ShowableListMenu;
+import com.android.internal.view.menu.SubMenuBuilder;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * MenuPresenter for building action menus as seen in the action bar and action modes.
+ *
+ * @hide
+ */
+public class ActionMenuPresenter extends BaseMenuPresenter
+        implements ActionProvider.SubUiVisibilityListener {
+    private static final int ITEM_ANIMATION_DURATION = 150;
+    private static final boolean ACTIONBAR_ANIMATIONS_ENABLED = false;
+
+    private OverflowMenuButton mOverflowButton;
+    private Drawable mPendingOverflowIcon;
+    private boolean mPendingOverflowIconSet;
+    private boolean mReserveOverflow;
+    private boolean mReserveOverflowSet;
+    private int mWidthLimit;
+    private int mActionItemWidthLimit;
+    private int mMaxItems;
+    private boolean mMaxItemsSet;
+    private boolean mStrictWidthLimit;
+    private boolean mWidthLimitSet;
+    private boolean mExpandedActionViewsExclusive;
+
+    private int mMinCellSize;
+
+    // Group IDs that have been added as actions - used temporarily, allocated here for reuse.
+    private final SparseBooleanArray mActionButtonGroups = new SparseBooleanArray();
+
+    private OverflowPopup mOverflowPopup;
+    private ActionButtonSubmenu mActionButtonPopup;
+
+    private OpenOverflowRunnable mPostedOpenRunnable;
+    private ActionMenuPopupCallback mPopupCallback;
+
+    final PopupPresenterCallback mPopupPresenterCallback = new PopupPresenterCallback();
+    int mOpenSubMenuId;
+
+    // These collections are used to store pre- and post-layout information for menu items,
+    // which is used to determine appropriate animations to run for changed items.
+    private SparseArray<MenuItemLayoutInfo> mPreLayoutItems = new SparseArray<>();
+    private SparseArray<MenuItemLayoutInfo> mPostLayoutItems = new SparseArray<>();
+
+    // The list of currently running animations on menu items.
+    private List<ItemAnimationInfo> mRunningItemAnimations = new ArrayList<>();
+    private ViewTreeObserver.OnPreDrawListener mItemAnimationPreDrawListener =
+            new ViewTreeObserver.OnPreDrawListener() {
+        @Override
+        public boolean onPreDraw() {
+            computeMenuItemAnimationInfo(false);
+            ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(this);
+            runItemAnimations();
+            return true;
+        }
+    };
+    private View.OnAttachStateChangeListener mAttachStateChangeListener =
+            new View.OnAttachStateChangeListener() {
+        @Override
+        public void onViewAttachedToWindow(View v) {
+        }
+
+        @Override
+        public void onViewDetachedFromWindow(View v) {
+            ((View) mMenuView).getViewTreeObserver().removeOnPreDrawListener(
+                    mItemAnimationPreDrawListener);
+            mPreLayoutItems.clear();
+            mPostLayoutItems.clear();
+        }
+    };
+
+
+    public ActionMenuPresenter(Context context) {
+        super(context, com.android.internal.R.layout.action_menu_layout,
+                com.android.internal.R.layout.action_menu_item_layout);
+    }
+
+    @Override
+    public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+        super.initForMenu(context, menu);
+
+        final Resources res = context.getResources();
+
+        final ActionBarPolicy abp = ActionBarPolicy.get(context);
+        if (!mReserveOverflowSet) {
+            mReserveOverflow = abp.showsOverflowMenuButton();
+        }
+
+        if (!mWidthLimitSet) {
+            mWidthLimit = abp.getEmbeddedMenuWidthLimit();
+        }
+
+        // Measure for initial configuration
+        if (!mMaxItemsSet) {
+            mMaxItems = abp.getMaxActionButtons();
+        }
+
+        int width = mWidthLimit;
+        if (mReserveOverflow) {
+            if (mOverflowButton == null) {
+                mOverflowButton = new OverflowMenuButton(mSystemContext);
+                if (mPendingOverflowIconSet) {
+                    mOverflowButton.setImageDrawable(mPendingOverflowIcon);
+                    mPendingOverflowIcon = null;
+                    mPendingOverflowIconSet = false;
+                }
+                final int spec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+                mOverflowButton.measure(spec, spec);
+            }
+            width -= mOverflowButton.getMeasuredWidth();
+        } else {
+            mOverflowButton = null;
+        }
+
+        mActionItemWidthLimit = width;
+
+        mMinCellSize = (int) (ActionMenuView.MIN_CELL_SIZE * res.getDisplayMetrics().density);
+    }
+
+    public void onConfigurationChanged(Configuration newConfig) {
+        if (!mMaxItemsSet) {
+            mMaxItems = ActionBarPolicy.get(mContext).getMaxActionButtons();
+        }
+        if (mMenu != null) {
+            mMenu.onItemsChanged(true);
+        }
+    }
+
+    public void setWidthLimit(int width, boolean strict) {
+        mWidthLimit = width;
+        mStrictWidthLimit = strict;
+        mWidthLimitSet = true;
+    }
+
+    public void setReserveOverflow(boolean reserveOverflow) {
+        mReserveOverflow = reserveOverflow;
+        mReserveOverflowSet = true;
+    }
+
+    public void setItemLimit(int itemCount) {
+        mMaxItems = itemCount;
+        mMaxItemsSet = true;
+    }
+
+    public void setExpandedActionViewsExclusive(boolean isExclusive) {
+        mExpandedActionViewsExclusive = isExclusive;
+    }
+
+    public void setOverflowIcon(Drawable icon) {
+        if (mOverflowButton != null) {
+            mOverflowButton.setImageDrawable(icon);
+        } else {
+            mPendingOverflowIconSet = true;
+            mPendingOverflowIcon = icon;
+        }
+    }
+
+    public Drawable getOverflowIcon() {
+        if (mOverflowButton != null) {
+            return mOverflowButton.getDrawable();
+        } else if (mPendingOverflowIconSet) {
+            return mPendingOverflowIcon;
+        }
+        return null;
+    }
+
+    @Override
+    public MenuView getMenuView(ViewGroup root) {
+        MenuView oldMenuView = mMenuView;
+        MenuView result = super.getMenuView(root);
+        if (oldMenuView != result) {
+            ((ActionMenuView) result).setPresenter(this);
+            if (oldMenuView != null) {
+                ((View) oldMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
+            }
+            ((View) result).addOnAttachStateChangeListener(mAttachStateChangeListener);
+        }
+        return result;
+    }
+
+    @Override
+    public View getItemView(final MenuItemImpl item, View convertView, ViewGroup parent) {
+        View actionView = item.getActionView();
+        if (actionView == null || item.hasCollapsibleActionView()) {
+            actionView = super.getItemView(item, convertView, parent);
+        }
+        actionView.setVisibility(item.isActionViewExpanded() ? View.GONE : View.VISIBLE);
+
+        final ActionMenuView menuParent = (ActionMenuView) parent;
+        final ViewGroup.LayoutParams lp = actionView.getLayoutParams();
+        if (!menuParent.checkLayoutParams(lp)) {
+            actionView.setLayoutParams(menuParent.generateLayoutParams(lp));
+        }
+        return actionView;
+    }
+
+    @Override
+    public void bindItemView(MenuItemImpl item, MenuView.ItemView itemView) {
+        itemView.initialize(item, 0);
+
+        final ActionMenuView menuView = (ActionMenuView) mMenuView;
+        final ActionMenuItemView actionItemView = (ActionMenuItemView) itemView;
+        actionItemView.setItemInvoker(menuView);
+
+        if (mPopupCallback == null) {
+            mPopupCallback = new ActionMenuPopupCallback();
+        }
+        actionItemView.setPopupCallback(mPopupCallback);
+    }
+
+    @Override
+    public boolean shouldIncludeItem(int childIndex, MenuItemImpl item) {
+        return item.isActionButton();
+    }
+
+    /**
+     * Store layout information about current items in the menu. This is stored for
+     * both pre- and post-layout phases and compared in runItemAnimations() to determine
+     * the animations that need to be run on any item changes.
+     *
+     * @param preLayout Whether this is being called in the pre-layout phase. This is passed
+     * into the MenuItemLayoutInfo structure to store the appropriate position values.
+     */
+    private void computeMenuItemAnimationInfo(boolean preLayout) {
+        final ViewGroup menuView = (ViewGroup) mMenuView;
+        final int count = menuView.getChildCount();
+        SparseArray items = preLayout ? mPreLayoutItems : mPostLayoutItems;
+        for (int i = 0; i < count; ++i) {
+            View child = menuView.getChildAt(i);
+            final int id = child.getId();
+            if (id > 0 && child.getWidth() != 0 && child.getHeight() != 0) {
+                MenuItemLayoutInfo info = new MenuItemLayoutInfo(child, preLayout);
+                items.put(id, info);
+            }
+        }
+    }
+
+    /**
+     * This method is called once both the pre-layout and post-layout steps have
+     * happened. It figures out which views are new (didn't exist prior to layout),
+     * gone (existed pre-layout, but are now gone), or changed (exist in both,
+     * but in a different location) and runs appropriate animations on those views.
+     * Items are tracked by ids, since the underlying views that represent items
+     * pre- and post-layout may be different.
+     */
+    private void runItemAnimations() {
+        for (int i = 0; i < mPreLayoutItems.size(); ++i) {
+            int id = mPreLayoutItems.keyAt(i);
+            final MenuItemLayoutInfo menuItemLayoutInfoPre = mPreLayoutItems.get(id);
+            final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
+            if (postLayoutIndex >= 0) {
+                // item exists pre and post: see if it's changed
+                final MenuItemLayoutInfo menuItemLayoutInfoPost =
+                        mPostLayoutItems.valueAt(postLayoutIndex);
+                PropertyValuesHolder pvhX = null;
+                PropertyValuesHolder pvhY = null;
+                if (menuItemLayoutInfoPre.left != menuItemLayoutInfoPost.left) {
+                    pvhX = PropertyValuesHolder.ofFloat(View.TRANSLATION_X,
+                            (menuItemLayoutInfoPre.left - menuItemLayoutInfoPost.left), 0);
+                }
+                if (menuItemLayoutInfoPre.top != menuItemLayoutInfoPost.top) {
+                    pvhY = PropertyValuesHolder.ofFloat(View.TRANSLATION_Y,
+                            menuItemLayoutInfoPre.top - menuItemLayoutInfoPost.top, 0);
+                }
+                if (pvhX != null || pvhY != null) {
+                    for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                        ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+                        if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.MOVE) {
+                            oldInfo.animator.cancel();
+                        }
+                    }
+                    ObjectAnimator anim;
+                    if (pvhX != null) {
+                        if (pvhY != null) {
+                            anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view,
+                                    pvhX, pvhY);
+                        } else {
+                            anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhX);
+                        }
+                    } else {
+                        anim = ObjectAnimator.ofPropertyValuesHolder(menuItemLayoutInfoPost.view, pvhY);
+                    }
+                    anim.setDuration(ITEM_ANIMATION_DURATION);
+                    anim.start();
+                    ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPost, anim,
+                            ItemAnimationInfo.MOVE);
+                    mRunningItemAnimations.add(info);
+                    anim.addListener(new AnimatorListenerAdapter() {
+                        @Override
+                        public void onAnimationEnd(Animator animation) {
+                            for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                                if (mRunningItemAnimations.get(j).animator == animation) {
+                                    mRunningItemAnimations.remove(j);
+                                    break;
+                                }
+                            }
+                        }
+                    });
+                }
+                mPostLayoutItems.remove(id);
+            } else {
+                // item used to be there, is now gone
+                float oldAlpha = 1;
+                for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                    ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+                    if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_IN) {
+                        oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
+                        oldInfo.animator.cancel();
+                    }
+                }
+                ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfoPre.view, View.ALPHA,
+                        oldAlpha, 0);
+                // Re-using the view from pre-layout assumes no view recycling
+                ((ViewGroup) mMenuView).getOverlay().add(menuItemLayoutInfoPre.view);
+                anim.setDuration(ITEM_ANIMATION_DURATION);
+                anim.start();
+                ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfoPre, anim, ItemAnimationInfo.FADE_OUT);
+                mRunningItemAnimations.add(info);
+                anim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                            if (mRunningItemAnimations.get(j).animator == animation) {
+                                mRunningItemAnimations.remove(j);
+                                break;
+                            }
+                        }
+                        ((ViewGroup) mMenuView).getOverlay().remove(menuItemLayoutInfoPre.view);
+                    }
+                });
+            }
+        }
+        for (int i = 0; i < mPostLayoutItems.size(); ++i) {
+            int id = mPostLayoutItems.keyAt(i);
+            final int postLayoutIndex = mPostLayoutItems.indexOfKey(id);
+            if (postLayoutIndex >= 0) {
+                // item is new
+                final MenuItemLayoutInfo menuItemLayoutInfo =
+                        mPostLayoutItems.valueAt(postLayoutIndex);
+                float oldAlpha = 0;
+                for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                    ItemAnimationInfo oldInfo = mRunningItemAnimations.get(j);
+                    if (oldInfo.id == id && oldInfo.animType == ItemAnimationInfo.FADE_OUT) {
+                        oldAlpha = oldInfo.menuItemLayoutInfo.view.getAlpha();
+                        oldInfo.animator.cancel();
+                    }
+                }
+                ObjectAnimator anim = ObjectAnimator.ofFloat(menuItemLayoutInfo.view, View.ALPHA,
+                        oldAlpha, 1);
+                anim.start();
+                anim.setDuration(ITEM_ANIMATION_DURATION);
+                ItemAnimationInfo info = new ItemAnimationInfo(id, menuItemLayoutInfo, anim, ItemAnimationInfo.FADE_IN);
+                mRunningItemAnimations.add(info);
+                anim.addListener(new AnimatorListenerAdapter() {
+                    @Override
+                    public void onAnimationEnd(Animator animation) {
+                        for (int j = 0; j < mRunningItemAnimations.size(); ++j) {
+                            if (mRunningItemAnimations.get(j).animator == animation) {
+                                mRunningItemAnimations.remove(j);
+                                break;
+                            }
+                        }
+                    }
+                });
+            }
+        }
+        mPreLayoutItems.clear();
+        mPostLayoutItems.clear();
+    }
+
+    /**
+     * Gets position/existence information on menu items before and after layout,
+     * which is then fed into runItemAnimations()
+     */
+    private void setupItemAnimations() {
+        computeMenuItemAnimationInfo(true);
+        ((View) mMenuView).getViewTreeObserver().
+                addOnPreDrawListener(mItemAnimationPreDrawListener);
+    }
+
+    @Override
+    public void updateMenuView(boolean cleared) {
+        final ViewGroup menuViewParent = (ViewGroup) ((View) mMenuView).getParent();
+        if (menuViewParent != null && ACTIONBAR_ANIMATIONS_ENABLED) {
+            setupItemAnimations();
+        }
+        super.updateMenuView(cleared);
+
+        ((View) mMenuView).requestLayout();
+
+        if (mMenu != null) {
+            final ArrayList<MenuItemImpl> actionItems = mMenu.getActionItems();
+            final int count = actionItems.size();
+            for (int i = 0; i < count; i++) {
+                final ActionProvider provider = actionItems.get(i).getActionProvider();
+                if (provider != null) {
+                    provider.setSubUiVisibilityListener(this);
+                }
+            }
+        }
+
+        final ArrayList<MenuItemImpl> nonActionItems = mMenu != null ?
+                mMenu.getNonActionItems() : null;
+
+        boolean hasOverflow = false;
+        if (mReserveOverflow && nonActionItems != null) {
+            final int count = nonActionItems.size();
+            if (count == 1) {
+                hasOverflow = !nonActionItems.get(0).isActionViewExpanded();
+            } else {
+                hasOverflow = count > 0;
+            }
+        }
+
+        if (hasOverflow) {
+            if (mOverflowButton == null) {
+                mOverflowButton = new OverflowMenuButton(mSystemContext);
+            }
+            ViewGroup parent = (ViewGroup) mOverflowButton.getParent();
+            if (parent != mMenuView) {
+                if (parent != null) {
+                    parent.removeView(mOverflowButton);
+                }
+                ActionMenuView menuView = (ActionMenuView) mMenuView;
+                menuView.addView(mOverflowButton, menuView.generateOverflowButtonLayoutParams());
+            }
+        } else if (mOverflowButton != null && mOverflowButton.getParent() == mMenuView) {
+            ((ViewGroup) mMenuView).removeView(mOverflowButton);
+        }
+
+        ((ActionMenuView) mMenuView).setOverflowReserved(mReserveOverflow);
+    }
+
+    @Override
+    public boolean filterLeftoverView(ViewGroup parent, int childIndex) {
+        if (parent.getChildAt(childIndex) == mOverflowButton) return false;
+        return super.filterLeftoverView(parent, childIndex);
+    }
+
+    public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+        if (!subMenu.hasVisibleItems()) return false;
+
+        SubMenuBuilder topSubMenu = subMenu;
+        while (topSubMenu.getParentMenu() != mMenu) {
+            topSubMenu = (SubMenuBuilder) topSubMenu.getParentMenu();
+        }
+        View anchor = findViewForItem(topSubMenu.getItem());
+        if (anchor == null) {
+            // This means the submenu was opened from an overflow menu item, indicating the
+            // MenuPopupHelper will handle opening the submenu via its MenuPopup. Return false to
+            // ensure that the MenuPopup acts as presenter for the submenu, and acts on its
+            // responsibility to display the new submenu.
+            return false;
+        }
+
+        mOpenSubMenuId = subMenu.getItem().getItemId();
+
+        boolean preserveIconSpacing = false;
+        final int count = subMenu.size();
+        for (int i = 0; i < count; i++) {
+            MenuItem childItem = subMenu.getItem(i);
+            if (childItem.isVisible() && childItem.getIcon() != null) {
+                preserveIconSpacing = true;
+                break;
+            }
+        }
+
+        mActionButtonPopup = new ActionButtonSubmenu(mContext, subMenu, anchor);
+        mActionButtonPopup.setForceShowIcon(preserveIconSpacing);
+        mActionButtonPopup.show();
+
+        super.onSubMenuSelected(subMenu);
+        return true;
+    }
+
+    private View findViewForItem(MenuItem item) {
+        final ViewGroup parent = (ViewGroup) mMenuView;
+        if (parent == null) return null;
+
+        final int count = parent.getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = parent.getChildAt(i);
+            if (child instanceof MenuView.ItemView &&
+                    ((MenuView.ItemView) child).getItemData() == item) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Display the overflow menu if one is present.
+     * @return true if the overflow menu was shown, false otherwise.
+     */
+    public boolean showOverflowMenu() {
+        if (mReserveOverflow && !isOverflowMenuShowing() && mMenu != null && mMenuView != null &&
+                mPostedOpenRunnable == null && !mMenu.getNonActionItems().isEmpty()) {
+            OverflowPopup popup = new OverflowPopup(mContext, mMenu, mOverflowButton, true);
+            mPostedOpenRunnable = new OpenOverflowRunnable(popup);
+            // Post this for later; we might still need a layout for the anchor to be right.
+            ((View) mMenuView).post(mPostedOpenRunnable);
+
+            // ActionMenuPresenter uses null as a callback argument here
+            // to indicate overflow is opening.
+            super.onSubMenuSelected(null);
+
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Hide the overflow menu if it is currently showing.
+     *
+     * @return true if the overflow menu was hidden, false otherwise.
+     */
+    public boolean hideOverflowMenu() {
+        if (mPostedOpenRunnable != null && mMenuView != null) {
+            ((View) mMenuView).removeCallbacks(mPostedOpenRunnable);
+            mPostedOpenRunnable = null;
+            return true;
+        }
+
+        MenuPopupHelper popup = mOverflowPopup;
+        if (popup != null) {
+            popup.dismiss();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Dismiss all popup menus - overflow and submenus.
+     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
+     */
+    public boolean dismissPopupMenus() {
+        boolean result = hideOverflowMenu();
+        result |= hideSubMenus();
+        return result;
+    }
+
+    /**
+     * Dismiss all submenu popups.
+     *
+     * @return true if popups were dismissed, false otherwise. (This can be because none were open.)
+     */
+    public boolean hideSubMenus() {
+        if (mActionButtonPopup != null) {
+            mActionButtonPopup.dismiss();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return true if the overflow menu is currently showing
+     */
+    public boolean isOverflowMenuShowing() {
+        return mOverflowPopup != null && mOverflowPopup.isShowing();
+    }
+
+    public boolean isOverflowMenuShowPending() {
+        return mPostedOpenRunnable != null || isOverflowMenuShowing();
+    }
+
+    /**
+     * @return true if space has been reserved in the action menu for an overflow item.
+     */
+    public boolean isOverflowReserved() {
+        return mReserveOverflow;
+    }
+
+    public boolean flagActionItems() {
+        final ArrayList<MenuItemImpl> visibleItems;
+        final int itemsSize;
+        if (mMenu != null) {
+            visibleItems = mMenu.getVisibleItems();
+            itemsSize = visibleItems.size();
+        } else {
+            visibleItems = null;
+            itemsSize = 0;
+        }
+
+        int maxActions = mMaxItems;
+        int widthLimit = mActionItemWidthLimit;
+        final int querySpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+        final ViewGroup parent = (ViewGroup) mMenuView;
+
+        int requiredItems = 0;
+        int requestedItems = 0;
+        int firstActionWidth = 0;
+        boolean hasOverflow = false;
+        for (int i = 0; i < itemsSize; i++) {
+            MenuItemImpl item = visibleItems.get(i);
+            if (item.requiresActionButton()) {
+                requiredItems++;
+            } else if (item.requestsActionButton()) {
+                requestedItems++;
+            } else {
+                hasOverflow = true;
+            }
+            if (mExpandedActionViewsExclusive && item.isActionViewExpanded()) {
+                // Overflow everything if we have an expanded action view and we're
+                // space constrained.
+                maxActions = 0;
+            }
+        }
+
+        // Reserve a spot for the overflow item if needed.
+        if (mReserveOverflow &&
+                (hasOverflow || requiredItems + requestedItems > maxActions)) {
+            maxActions--;
+        }
+        maxActions -= requiredItems;
+
+        final SparseBooleanArray seenGroups = mActionButtonGroups;
+        seenGroups.clear();
+
+        int cellSize = 0;
+        int cellsRemaining = 0;
+        if (mStrictWidthLimit) {
+            cellsRemaining = widthLimit / mMinCellSize;
+            final int cellSizeRemaining = widthLimit % mMinCellSize;
+            cellSize = mMinCellSize + cellSizeRemaining / cellsRemaining;
+        }
+
+        // Flag as many more requested items as will fit.
+        for (int i = 0; i < itemsSize; i++) {
+            MenuItemImpl item = visibleItems.get(i);
+
+            if (item.requiresActionButton()) {
+                View v = getItemView(item, null, parent);
+                if (mStrictWidthLimit) {
+                    cellsRemaining -= ActionMenuView.measureChildForCells(v,
+                            cellSize, cellsRemaining, querySpec, 0);
+                } else {
+                    v.measure(querySpec, querySpec);
+                }
+                final int measuredWidth = v.getMeasuredWidth();
+                widthLimit -= measuredWidth;
+                if (firstActionWidth == 0) {
+                    firstActionWidth = measuredWidth;
+                }
+                final int groupId = item.getGroupId();
+                if (groupId != 0) {
+                    seenGroups.put(groupId, true);
+                }
+                item.setIsActionButton(true);
+            } else if (item.requestsActionButton()) {
+                // Items in a group with other items that already have an action slot
+                // can break the max actions rule, but not the width limit.
+                final int groupId = item.getGroupId();
+                final boolean inGroup = seenGroups.get(groupId);
+                boolean isAction = (maxActions > 0 || inGroup) && widthLimit > 0 &&
+                        (!mStrictWidthLimit || cellsRemaining > 0);
+
+                if (isAction) {
+                    View v = getItemView(item, null, parent);
+                    if (mStrictWidthLimit) {
+                        final int cells = ActionMenuView.measureChildForCells(v,
+                                cellSize, cellsRemaining, querySpec, 0);
+                        cellsRemaining -= cells;
+                        if (cells == 0) {
+                            isAction = false;
+                        }
+                    } else {
+                        v.measure(querySpec, querySpec);
+                    }
+                    final int measuredWidth = v.getMeasuredWidth();
+                    widthLimit -= measuredWidth;
+                    if (firstActionWidth == 0) {
+                        firstActionWidth = measuredWidth;
+                    }
+
+                    if (mStrictWidthLimit) {
+                        isAction &= widthLimit >= 0;
+                    } else {
+                        // Did this push the entire first item past the limit?
+                        isAction &= widthLimit + firstActionWidth > 0;
+                    }
+                }
+
+                if (isAction && groupId != 0) {
+                    seenGroups.put(groupId, true);
+                } else if (inGroup) {
+                    // We broke the width limit. Demote the whole group, they all overflow now.
+                    seenGroups.put(groupId, false);
+                    for (int j = 0; j < i; j++) {
+                        MenuItemImpl areYouMyGroupie = visibleItems.get(j);
+                        if (areYouMyGroupie.getGroupId() == groupId) {
+                            // Give back the action slot
+                            if (areYouMyGroupie.isActionButton()) maxActions++;
+                            areYouMyGroupie.setIsActionButton(false);
+                        }
+                    }
+                }
+
+                if (isAction) maxActions--;
+
+                item.setIsActionButton(isAction);
+            } else {
+                // Neither requires nor requests an action button.
+                item.setIsActionButton(false);
+            }
+        }
+        return true;
+    }
+
+    @Override
+    public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        dismissPopupMenus();
+        super.onCloseMenu(menu, allMenusAreClosing);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        SavedState state = new SavedState();
+        state.openSubMenuId = mOpenSubMenuId;
+        return state;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState saved = (SavedState) state;
+        if (saved.openSubMenuId > 0) {
+            MenuItem item = mMenu.findItem(saved.openSubMenuId);
+            if (item != null) {
+                SubMenuBuilder subMenu = (SubMenuBuilder) item.getSubMenu();
+                onSubMenuSelected(subMenu);
+            }
+        }
+    }
+
+    @Override
+    public void onSubUiVisibilityChanged(boolean isVisible) {
+        if (isVisible) {
+            // Not a submenu, but treat it like one.
+            super.onSubMenuSelected(null);
+        } else if (mMenu != null) {
+            mMenu.close(false /* closeAllMenus */);
+        }
+    }
+
+    public void setMenuView(ActionMenuView menuView) {
+        if (menuView != mMenuView) {
+            if (mMenuView != null) {
+                ((View) mMenuView).removeOnAttachStateChangeListener(mAttachStateChangeListener);
+            }
+            mMenuView = menuView;
+            menuView.initialize(mMenu);
+            menuView.addOnAttachStateChangeListener(mAttachStateChangeListener);
+        }
+    }
+
+    private static class SavedState implements Parcelable {
+        public int openSubMenuId;
+
+        SavedState() {
+        }
+
+        SavedState(Parcel in) {
+            openSubMenuId = in.readInt();
+        }
+
+        @Override
+        public int describeContents() {
+            return 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(openSubMenuId);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    private class OverflowMenuButton extends ImageButton implements ActionMenuView.ActionMenuChildView {
+        public OverflowMenuButton(Context context) {
+            super(context, null, com.android.internal.R.attr.actionOverflowButtonStyle);
+
+            setClickable(true);
+            setFocusable(true);
+            setVisibility(VISIBLE);
+            setEnabled(true);
+
+            setOnTouchListener(new ForwardingListener(this) {
+                @Override
+                public ShowableListMenu getPopup() {
+                    if (mOverflowPopup == null) {
+                        return null;
+                    }
+
+                    return mOverflowPopup.getPopup();
+                }
+
+                @Override
+                public boolean onForwardingStarted() {
+                    showOverflowMenu();
+                    return true;
+                }
+
+                @Override
+                public boolean onForwardingStopped() {
+                    // Displaying the popup occurs asynchronously, so wait for
+                    // the runnable to finish before deciding whether to stop
+                    // forwarding.
+                    if (mPostedOpenRunnable != null) {
+                        return false;
+                    }
+
+                    hideOverflowMenu();
+                    return true;
+                }
+            });
+        }
+
+        @Override
+        public boolean performClick() {
+            if (super.performClick()) {
+                return true;
+            }
+
+            playSoundEffect(SoundEffectConstants.CLICK);
+            showOverflowMenu();
+            return true;
+        }
+
+        @Override
+        public boolean needsDividerBefore() {
+            return false;
+        }
+
+        @Override
+        public boolean needsDividerAfter() {
+            return false;
+        }
+
+    /** @hide */
+        @Override
+        public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfoInternal(info);
+            info.setCanOpenPopup(true);
+        }
+
+        @Override
+        protected boolean setFrame(int l, int t, int r, int b) {
+            final boolean changed = super.setFrame(l, t, r, b);
+
+            // Set up the hotspot bounds to square and centered on the image.
+            final Drawable d = getDrawable();
+            final Drawable bg = getBackground();
+            if (d != null && bg != null) {
+                final int width = getWidth();
+                final int height = getHeight();
+                final int halfEdge = Math.max(width, height) / 2;
+                final int offsetX = getPaddingLeft() - getPaddingRight();
+                final int offsetY = getPaddingTop() - getPaddingBottom();
+                final int centerX = (width + offsetX) / 2;
+                final int centerY = (height + offsetY) / 2;
+                bg.setHotspotBounds(centerX - halfEdge, centerY - halfEdge,
+                        centerX + halfEdge, centerY + halfEdge);
+            }
+
+            return changed;
+        }
+    }
+
+    private class OverflowPopup extends MenuPopupHelper {
+        public OverflowPopup(Context context, MenuBuilder menu, View anchorView,
+                boolean overflowOnly) {
+            super(context, menu, anchorView, overflowOnly,
+                    com.android.internal.R.attr.actionOverflowMenuStyle);
+            setGravity(Gravity.END);
+            setPresenterCallback(mPopupPresenterCallback);
+        }
+
+        @Override
+        protected void onDismiss() {
+            if (mMenu != null) {
+                mMenu.close();
+            }
+            mOverflowPopup = null;
+
+            super.onDismiss();
+        }
+    }
+
+    private class ActionButtonSubmenu extends MenuPopupHelper {
+        public ActionButtonSubmenu(Context context, SubMenuBuilder subMenu, View anchorView) {
+            super(context, subMenu, anchorView, false,
+                    com.android.internal.R.attr.actionOverflowMenuStyle);
+
+            MenuItemImpl item = (MenuItemImpl) subMenu.getItem();
+            if (!item.isActionButton()) {
+                // Give a reasonable anchor to nested submenus.
+                setAnchorView(mOverflowButton == null ? (View) mMenuView : mOverflowButton);
+            }
+
+            setPresenterCallback(mPopupPresenterCallback);
+        }
+
+        @Override
+        protected void onDismiss() {
+            mActionButtonPopup = null;
+            mOpenSubMenuId = 0;
+
+            super.onDismiss();
+        }
+    }
+
+    private class PopupPresenterCallback implements Callback {
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            if (subMenu == null) return false;
+
+            mOpenSubMenuId = ((SubMenuBuilder) subMenu).getItem().getItemId();
+            final Callback cb = getCallback();
+            return cb != null ? cb.onOpenSubMenu(subMenu) : false;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+            if (menu instanceof SubMenuBuilder) {
+                menu.getRootMenu().close(false /* closeAllMenus */);
+            }
+            final Callback cb = getCallback();
+            if (cb != null) {
+                cb.onCloseMenu(menu, allMenusAreClosing);
+            }
+        }
+    }
+
+    private class OpenOverflowRunnable implements Runnable {
+        private OverflowPopup mPopup;
+
+        public OpenOverflowRunnable(OverflowPopup popup) {
+            mPopup = popup;
+        }
+
+        public void run() {
+            if (mMenu != null) {
+                mMenu.changeMenuMode();
+            }
+            final View menuView = (View) mMenuView;
+            if (menuView != null && menuView.getWindowToken() != null && mPopup.tryShow()) {
+                mOverflowPopup = mPopup;
+            }
+            mPostedOpenRunnable = null;
+        }
+    }
+
+    private class ActionMenuPopupCallback extends ActionMenuItemView.PopupCallback {
+        @Override
+        public ShowableListMenu getPopup() {
+            return mActionButtonPopup != null ? mActionButtonPopup.getPopup() : null;
+        }
+    }
+
+    /**
+     * This class holds layout information for a menu item. This is used to determine
+     * pre- and post-layout information about menu items, which will then be used to
+     * determine appropriate item animations.
+     */
+    private static class MenuItemLayoutInfo {
+        View view;
+        int left;
+        int top;
+
+        MenuItemLayoutInfo(View view, boolean preLayout) {
+            left = view.getLeft();
+            top = view.getTop();
+            if (preLayout) {
+                // We track translation for pre-layout because a view might be mid-animation
+                // and we need this information to know where to animate from
+                left += view.getTranslationX();
+                top += view.getTranslationY();
+            }
+            this.view = view;
+        }
+    }
+
+    /**
+     * This class is used to store information about currently-running item animations.
+     * This is used when new animations are scheduled to determine whether any existing
+     * animations need to be canceled, based on whether the running animations overlap
+     * with any new animations. For example, if an item is currently animating from
+     * location A to B and another change dictates that it be animated to C, then the current
+     * A-B animation will be canceled and a new animation to C will be started.
+     */
+    private static class ItemAnimationInfo {
+        int id;
+        MenuItemLayoutInfo menuItemLayoutInfo;
+        Animator animator;
+        int animType;
+        static final int MOVE = 0;
+        static final int FADE_IN = 1;
+        static final int FADE_OUT = 2;
+
+        ItemAnimationInfo(int id, MenuItemLayoutInfo info, Animator anim, int animType) {
+            this.id = id;
+            menuItemLayoutInfo = info;
+            animator = anim;
+            this.animType = animType;
+        }
+    }
+}
diff --git a/android/widget/ActionMenuView.java b/android/widget/ActionMenuView.java
new file mode 100644
index 0000000..c4bbdb0
--- /dev/null
+++ b/android/widget/ActionMenuView.java
@@ -0,0 +1,850 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.view.menu.ActionMenuItemView;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+
+/**
+ * ActionMenuView is a presentation of a series of menu options as a View. It provides
+ * several top level options as action buttons while spilling remaining options over as
+ * items in an overflow menu. This allows applications to present packs of actions inline with
+ * specific or repeating content.
+ */
+public class ActionMenuView extends LinearLayout implements MenuBuilder.ItemInvoker, MenuView {
+    private static final String TAG = "ActionMenuView";
+
+    static final int MIN_CELL_SIZE = 56; // dips
+    static final int GENERATED_ITEM_PADDING = 4; // dips
+
+    private MenuBuilder mMenu;
+
+    /** Context against which to inflate popup menus. */
+    private Context mPopupContext;
+
+    /** Theme resource against which to inflate popup menus. */
+    private int mPopupTheme;
+
+    private boolean mReserveOverflow;
+    private ActionMenuPresenter mPresenter;
+    private MenuPresenter.Callback mActionMenuPresenterCallback;
+    private MenuBuilder.Callback mMenuBuilderCallback;
+    private boolean mFormatItems;
+    private int mFormatItemsWidth;
+    private int mMinCellSize;
+    private int mGeneratedItemPadding;
+
+    private OnMenuItemClickListener mOnMenuItemClickListener;
+
+    public ActionMenuView(Context context) {
+        this(context, null);
+    }
+
+    public ActionMenuView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setBaselineAligned(false);
+        final float density = context.getResources().getDisplayMetrics().density;
+        mMinCellSize = (int) (MIN_CELL_SIZE * density);
+        mGeneratedItemPadding = (int) (GENERATED_ITEM_PADDING * density);
+        mPopupContext = context;
+        mPopupTheme = 0;
+    }
+
+    /**
+     * Specifies the theme to use when inflating popup menus. By default, uses
+     * the same theme as the action menu view itself.
+     *
+     * @param resId theme used to inflate popup menus
+     * @see #getPopupTheme()
+     */
+    public void setPopupTheme(@StyleRes int resId) {
+        if (mPopupTheme != resId) {
+            mPopupTheme = resId;
+            if (resId == 0) {
+                mPopupContext = mContext;
+            } else {
+                mPopupContext = new ContextThemeWrapper(mContext, resId);
+            }
+        }
+    }
+
+    /**
+     * @return resource identifier of the theme used to inflate popup menus, or
+     *         0 if menus are inflated against the action menu view theme
+     * @see #setPopupTheme(int)
+     */
+    public int getPopupTheme() {
+        return mPopupTheme;
+    }
+
+    /**
+     * @param presenter Menu presenter used to display popup menu
+     * @hide
+     */
+    public void setPresenter(ActionMenuPresenter presenter) {
+        mPresenter = presenter;
+        mPresenter.setMenuView(this);
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+
+        if (mPresenter != null) {
+            mPresenter.updateMenuView(false);
+
+            if (mPresenter.isOverflowMenuShowing()) {
+                mPresenter.hideOverflowMenu();
+                mPresenter.showOverflowMenu();
+            }
+        }
+    }
+
+    public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+        mOnMenuItemClickListener = listener;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // If we've been given an exact size to match, apply special formatting during layout.
+        final boolean wasFormatted = mFormatItems;
+        mFormatItems = MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY;
+
+        if (wasFormatted != mFormatItems) {
+            mFormatItemsWidth = 0; // Reset this when switching modes
+        }
+
+        // Special formatting can change whether items can fit as action buttons.
+        // Kick the menu and update presenters when this changes.
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        if (mFormatItems && mMenu != null && widthSize != mFormatItemsWidth) {
+            mFormatItemsWidth = widthSize;
+            mMenu.onItemsChanged(true);
+        }
+
+        final int childCount = getChildCount();
+        if (mFormatItems && childCount > 0) {
+            onMeasureExactFormat(widthMeasureSpec, heightMeasureSpec);
+        } else {
+            // Previous measurement at exact format may have set margins - reset them.
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                lp.leftMargin = lp.rightMargin = 0;
+            }
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    private void onMeasureExactFormat(int widthMeasureSpec, int heightMeasureSpec) {
+        // We already know the width mode is EXACTLY if we're here.
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        final int widthPadding = getPaddingLeft() + getPaddingRight();
+        final int heightPadding = getPaddingTop() + getPaddingBottom();
+
+        final int itemHeightSpec = getChildMeasureSpec(heightMeasureSpec, heightPadding,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+
+        widthSize -= widthPadding;
+
+        // Divide the view into cells.
+        final int cellCount = widthSize / mMinCellSize;
+        final int cellSizeRemaining = widthSize % mMinCellSize;
+
+        if (cellCount == 0) {
+            // Give up, nothing fits.
+            setMeasuredDimension(widthSize, 0);
+            return;
+        }
+
+        final int cellSize = mMinCellSize + cellSizeRemaining / cellCount;
+
+        int cellsRemaining = cellCount;
+        int maxChildHeight = 0;
+        int maxCellsUsed = 0;
+        int expandableItemCount = 0;
+        int visibleItemCount = 0;
+        boolean hasOverflow = false;
+
+        // This is used as a bitfield to locate the smallest items present. Assumes childCount < 64.
+        long smallestItemsAt = 0;
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == GONE) continue;
+
+            final boolean isGeneratedItem = child instanceof ActionMenuItemView;
+            visibleItemCount++;
+
+            if (isGeneratedItem) {
+                // Reset padding for generated menu item views; it may change below
+                // and views are recycled.
+                child.setPadding(mGeneratedItemPadding, 0, mGeneratedItemPadding, 0);
+            }
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            lp.expanded = false;
+            lp.extraPixels = 0;
+            lp.cellsUsed = 0;
+            lp.expandable = false;
+            lp.leftMargin = 0;
+            lp.rightMargin = 0;
+            lp.preventEdgeOffset = isGeneratedItem && ((ActionMenuItemView) child).hasText();
+
+            // Overflow always gets 1 cell. No more, no less.
+            final int cellsAvailable = lp.isOverflowButton ? 1 : cellsRemaining;
+
+            final int cellsUsed = measureChildForCells(child, cellSize, cellsAvailable,
+                    itemHeightSpec, heightPadding);
+
+            maxCellsUsed = Math.max(maxCellsUsed, cellsUsed);
+            if (lp.expandable) expandableItemCount++;
+            if (lp.isOverflowButton) hasOverflow = true;
+
+            cellsRemaining -= cellsUsed;
+            maxChildHeight = Math.max(maxChildHeight, child.getMeasuredHeight());
+            if (cellsUsed == 1) smallestItemsAt |= (1 << i);
+        }
+
+        // When we have overflow and a single expanded (text) item, we want to try centering it
+        // visually in the available space even though overflow consumes some of it.
+        final boolean centerSingleExpandedItem = hasOverflow && visibleItemCount == 2;
+
+        // Divide space for remaining cells if we have items that can expand.
+        // Try distributing whole leftover cells to smaller items first.
+
+        boolean needsExpansion = false;
+        while (expandableItemCount > 0 && cellsRemaining > 0) {
+            int minCells = Integer.MAX_VALUE;
+            long minCellsAt = 0; // Bit locations are indices of relevant child views
+            int minCellsItemCount = 0;
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                // Don't try to expand items that shouldn't.
+                if (!lp.expandable) continue;
+
+                // Mark indices of children that can receive an extra cell.
+                if (lp.cellsUsed < minCells) {
+                    minCells = lp.cellsUsed;
+                    minCellsAt = 1 << i;
+                    minCellsItemCount = 1;
+                } else if (lp.cellsUsed == minCells) {
+                    minCellsAt |= 1 << i;
+                    minCellsItemCount++;
+                }
+            }
+
+            // Items that get expanded will always be in the set of smallest items when we're done.
+            smallestItemsAt |= minCellsAt;
+
+            if (minCellsItemCount > cellsRemaining) break; // Couldn't expand anything evenly. Stop.
+
+            // We have enough cells, all minimum size items will be incremented.
+            minCells++;
+
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if ((minCellsAt & (1 << i)) == 0) {
+                    // If this item is already at our small item count, mark it for later.
+                    if (lp.cellsUsed == minCells) smallestItemsAt |= 1 << i;
+                    continue;
+                }
+
+                if (centerSingleExpandedItem && lp.preventEdgeOffset && cellsRemaining == 1) {
+                    // Add padding to this item such that it centers.
+                    child.setPadding(mGeneratedItemPadding + cellSize, 0, mGeneratedItemPadding, 0);
+                }
+                lp.cellsUsed++;
+                lp.expanded = true;
+                cellsRemaining--;
+            }
+
+            needsExpansion = true;
+        }
+
+        // Divide any space left that wouldn't divide along cell boundaries
+        // evenly among the smallest items
+
+        final boolean singleItem = !hasOverflow && visibleItemCount == 1;
+        if (cellsRemaining > 0 && smallestItemsAt != 0 &&
+                (cellsRemaining < visibleItemCount - 1 || singleItem || maxCellsUsed > 1)) {
+            float expandCount = Long.bitCount(smallestItemsAt);
+
+            if (!singleItem) {
+                // The items at the far edges may only expand by half in order to pin to either side.
+                if ((smallestItemsAt & 1) != 0) {
+                    LayoutParams lp = (LayoutParams) getChildAt(0).getLayoutParams();
+                    if (!lp.preventEdgeOffset) expandCount -= 0.5f;
+                }
+                if ((smallestItemsAt & (1 << (childCount - 1))) != 0) {
+                    LayoutParams lp = ((LayoutParams) getChildAt(childCount - 1).getLayoutParams());
+                    if (!lp.preventEdgeOffset) expandCount -= 0.5f;
+                }
+            }
+
+            final int extraPixels = expandCount > 0 ?
+                    (int) (cellsRemaining * cellSize / expandCount) : 0;
+
+            for (int i = 0; i < childCount; i++) {
+                if ((smallestItemsAt & (1 << i)) == 0) continue;
+
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (child instanceof ActionMenuItemView) {
+                    // If this is one of our views, expand and measure at the larger size.
+                    lp.extraPixels = extraPixels;
+                    lp.expanded = true;
+                    if (i == 0 && !lp.preventEdgeOffset) {
+                        // First item gets part of its new padding pushed out of sight.
+                        // The last item will get this implicitly from layout.
+                        lp.leftMargin = -extraPixels / 2;
+                    }
+                    needsExpansion = true;
+                } else if (lp.isOverflowButton) {
+                    lp.extraPixels = extraPixels;
+                    lp.expanded = true;
+                    lp.rightMargin = -extraPixels / 2;
+                    needsExpansion = true;
+                } else {
+                    // If we don't know what it is, give it some margins instead
+                    // and let it center within its space. We still want to pin
+                    // against the edges.
+                    if (i != 0) {
+                        lp.leftMargin = extraPixels / 2;
+                    }
+                    if (i != childCount - 1) {
+                        lp.rightMargin = extraPixels / 2;
+                    }
+                }
+            }
+
+            cellsRemaining = 0;
+        }
+
+        // Remeasure any items that have had extra space allocated to them.
+        if (needsExpansion) {
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                if (!lp.expanded) continue;
+
+                final int width = lp.cellsUsed * cellSize + lp.extraPixels;
+                child.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                        itemHeightSpec);
+            }
+        }
+
+        if (heightMode != MeasureSpec.EXACTLY) {
+            heightSize = maxChildHeight;
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+    }
+
+    /**
+     * Measure a child view to fit within cell-based formatting. The child's width
+     * will be measured to a whole multiple of cellSize.
+     *
+     * <p>Sets the expandable and cellsUsed fields of LayoutParams.
+     *
+     * @param child Child to measure
+     * @param cellSize Size of one cell
+     * @param cellsRemaining Number of cells remaining that this view can expand to fill
+     * @param parentHeightMeasureSpec MeasureSpec used by the parent view
+     * @param parentHeightPadding Padding present in the parent view
+     * @return Number of cells this child was measured to occupy
+     */
+    static int measureChildForCells(View child, int cellSize, int cellsRemaining,
+            int parentHeightMeasureSpec, int parentHeightPadding) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+        final int childHeightSize = MeasureSpec.getSize(parentHeightMeasureSpec) -
+                parentHeightPadding;
+        final int childHeightMode = MeasureSpec.getMode(parentHeightMeasureSpec);
+        final int childHeightSpec = MeasureSpec.makeMeasureSpec(childHeightSize, childHeightMode);
+
+        final ActionMenuItemView itemView = child instanceof ActionMenuItemView ?
+                (ActionMenuItemView) child : null;
+        final boolean hasText = itemView != null && itemView.hasText();
+
+        int cellsUsed = 0;
+        if (cellsRemaining > 0 && (!hasText || cellsRemaining >= 2)) {
+            final int childWidthSpec = MeasureSpec.makeMeasureSpec(
+                    cellSize * cellsRemaining, MeasureSpec.AT_MOST);
+            child.measure(childWidthSpec, childHeightSpec);
+
+            final int measuredWidth = child.getMeasuredWidth();
+            cellsUsed = measuredWidth / cellSize;
+            if (measuredWidth % cellSize != 0) cellsUsed++;
+            if (hasText && cellsUsed < 2) cellsUsed = 2;
+        }
+
+        final boolean expandable = !lp.isOverflowButton && hasText;
+        lp.expandable = expandable;
+
+        lp.cellsUsed = cellsUsed;
+        final int targetWidth = cellsUsed * cellSize;
+        child.measure(MeasureSpec.makeMeasureSpec(targetWidth, MeasureSpec.EXACTLY),
+                childHeightSpec);
+        return cellsUsed;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (!mFormatItems) {
+            super.onLayout(changed, left, top, right, bottom);
+            return;
+        }
+
+        final int childCount = getChildCount();
+        final int midVertical = (bottom - top) / 2;
+        final int dividerWidth = getDividerWidth();
+        int overflowWidth = 0;
+        int nonOverflowWidth = 0;
+        int nonOverflowCount = 0;
+        int widthRemaining = right - left - getPaddingRight() - getPaddingLeft();
+        boolean hasOverflow = false;
+        final boolean isLayoutRtl = isLayoutRtl();
+        for (int i = 0; i < childCount; i++) {
+            final View v = getChildAt(i);
+            if (v.getVisibility() == GONE) {
+                continue;
+            }
+
+            LayoutParams p = (LayoutParams) v.getLayoutParams();
+            if (p.isOverflowButton) {
+                overflowWidth = v.getMeasuredWidth();
+                if (hasDividerBeforeChildAt(i)) {
+                    overflowWidth += dividerWidth;
+                }
+
+                int height = v.getMeasuredHeight();
+                int r;
+                int l;
+                if (isLayoutRtl) {
+                    l = getPaddingLeft() + p.leftMargin;
+                    r = l + overflowWidth;
+                } else {
+                    r = getWidth() - getPaddingRight() - p.rightMargin;
+                    l = r - overflowWidth;
+                }
+                int t = midVertical - (height / 2);
+                int b = t + height;
+                v.layout(l, t, r, b);
+
+                widthRemaining -= overflowWidth;
+                hasOverflow = true;
+            } else {
+                final int size = v.getMeasuredWidth() + p.leftMargin + p.rightMargin;
+                nonOverflowWidth += size;
+                widthRemaining -= size;
+                if (hasDividerBeforeChildAt(i)) {
+                    nonOverflowWidth += dividerWidth;
+                }
+                nonOverflowCount++;
+            }
+        }
+
+        if (childCount == 1 && !hasOverflow) {
+            // Center a single child
+            final View v = getChildAt(0);
+            final int width = v.getMeasuredWidth();
+            final int height = v.getMeasuredHeight();
+            final int midHorizontal = (right - left) / 2;
+            final int l = midHorizontal - width / 2;
+            final int t = midVertical - height / 2;
+            v.layout(l, t, l + width, t + height);
+            return;
+        }
+
+        final int spacerCount = nonOverflowCount - (hasOverflow ? 0 : 1);
+        final int spacerSize = Math.max(0, spacerCount > 0 ? widthRemaining / spacerCount : 0);
+
+        if (isLayoutRtl) {
+            int startRight = getWidth() - getPaddingRight();
+            for (int i = 0; i < childCount; i++) {
+                final View v = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+                if (v.getVisibility() == GONE || lp.isOverflowButton) {
+                    continue;
+                }
+
+                startRight -= lp.rightMargin;
+                int width = v.getMeasuredWidth();
+                int height = v.getMeasuredHeight();
+                int t = midVertical - height / 2;
+                v.layout(startRight - width, t, startRight, t + height);
+                startRight -= width + lp.leftMargin + spacerSize;
+            }
+        } else {
+            int startLeft = getPaddingLeft();
+            for (int i = 0; i < childCount; i++) {
+                final View v = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+                if (v.getVisibility() == GONE || lp.isOverflowButton) {
+                    continue;
+                }
+
+                startLeft += lp.leftMargin;
+                int width = v.getMeasuredWidth();
+                int height = v.getMeasuredHeight();
+                int t = midVertical - height / 2;
+                v.layout(startLeft, t, startLeft + width, t + height);
+                startLeft += width + lp.rightMargin + spacerSize;
+            }
+        }
+    }
+
+    @Override
+    public void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        dismissPopupMenus();
+    }
+
+    /**
+     * Set the icon to use for the overflow button.
+     *
+     * @param icon Drawable to set, may be null to clear the icon
+     */
+    public void setOverflowIcon(@Nullable Drawable icon) {
+        getMenu();
+        mPresenter.setOverflowIcon(icon);
+    }
+
+    /**
+     * Return the current drawable used as the overflow icon.
+     *
+     * @return The overflow icon drawable
+     */
+    @Nullable
+    public Drawable getOverflowIcon() {
+        getMenu();
+        return mPresenter.getOverflowIcon();
+    }
+
+    /** @hide */
+    public boolean isOverflowReserved() {
+        return mReserveOverflow;
+    }
+
+    /** @hide */
+    public void setOverflowReserved(boolean reserveOverflow) {
+        mReserveOverflow = reserveOverflow;
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        LayoutParams params = new LayoutParams(LayoutParams.WRAP_CONTENT,
+                LayoutParams.WRAP_CONTENT);
+        params.gravity = Gravity.CENTER_VERTICAL;
+        return params;
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (p != null) {
+            final LayoutParams result = p instanceof LayoutParams
+                    ? new LayoutParams((LayoutParams) p)
+                    : new LayoutParams(p);
+            if (result.gravity <= Gravity.NO_GRAVITY) {
+                result.gravity = Gravity.CENTER_VERTICAL;
+            }
+            return result;
+        }
+        return generateDefaultLayoutParams();
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p != null && p instanceof LayoutParams;
+    }
+
+    /** @hide */
+    public LayoutParams generateOverflowButtonLayoutParams() {
+        LayoutParams result = generateDefaultLayoutParams();
+        result.isOverflowButton = true;
+        return result;
+    }
+
+    /** @hide */
+    public boolean invokeItem(MenuItemImpl item) {
+        return mMenu.performItemAction(item, 0);
+    }
+
+    /** @hide */
+    public int getWindowAnimations() {
+        return 0;
+    }
+
+    /** @hide */
+    public void initialize(@Nullable MenuBuilder menu) {
+        mMenu = menu;
+    }
+
+    /**
+     * Returns the Menu object that this ActionMenuView is currently presenting.
+     *
+     * <p>Applications should use this method to obtain the ActionMenuView's Menu object
+     * and inflate or add content to it as necessary.</p>
+     *
+     * @return the Menu presented by this view
+     */
+    public Menu getMenu() {
+        if (mMenu == null) {
+            final Context context = getContext();
+            mMenu = new MenuBuilder(context);
+            mMenu.setCallback(new MenuBuilderCallback());
+            mPresenter = new ActionMenuPresenter(context);
+            mPresenter.setReserveOverflow(true);
+            mPresenter.setCallback(mActionMenuPresenterCallback != null
+                    ? mActionMenuPresenterCallback : new ActionMenuPresenterCallback());
+            mMenu.addMenuPresenter(mPresenter, mPopupContext);
+            mPresenter.setMenuView(this);
+        }
+
+        return mMenu;
+    }
+
+    /**
+     * Must be called before the first call to getMenu()
+     * @hide
+     */
+    public void setMenuCallbacks(MenuPresenter.Callback pcb, MenuBuilder.Callback mcb) {
+        mActionMenuPresenterCallback = pcb;
+        mMenuBuilderCallback = mcb;
+    }
+
+    /**
+     * Returns the current menu or null if one has not yet been configured.
+     * @hide Internal use only for action bar integration
+     */
+    public MenuBuilder peekMenu() {
+        return mMenu;
+    }
+
+    /**
+     * Show the overflow items from the associated menu.
+     *
+     * @return true if the menu was able to be shown, false otherwise
+     */
+    public boolean showOverflowMenu() {
+        return mPresenter != null && mPresenter.showOverflowMenu();
+    }
+
+    /**
+     * Hide the overflow items from the associated menu.
+     *
+     * @return true if the menu was able to be hidden, false otherwise
+     */
+    public boolean hideOverflowMenu() {
+        return mPresenter != null && mPresenter.hideOverflowMenu();
+    }
+
+    /**
+     * Check whether the overflow menu is currently showing. This may not reflect
+     * a pending show operation in progress.
+     *
+     * @return true if the overflow menu is currently showing
+     */
+    public boolean isOverflowMenuShowing() {
+        return mPresenter != null && mPresenter.isOverflowMenuShowing();
+    }
+
+    /** @hide */
+    public boolean isOverflowMenuShowPending() {
+        return mPresenter != null && mPresenter.isOverflowMenuShowPending();
+    }
+
+    /**
+     * Dismiss any popups associated with this menu view.
+     */
+    public void dismissPopupMenus() {
+        if (mPresenter != null) {
+            mPresenter.dismissPopupMenus();
+        }
+    }
+
+    /**
+     * @hide Private LinearLayout (superclass) API. Un-hide if LinearLayout API is made public.
+     */
+    @Override
+    protected boolean hasDividerBeforeChildAt(int childIndex) {
+        if (childIndex == 0) {
+            return false;
+        }
+        final View childBefore = getChildAt(childIndex - 1);
+        final View child = getChildAt(childIndex);
+        boolean result = false;
+        if (childIndex < getChildCount() && childBefore instanceof ActionMenuChildView) {
+            result |= ((ActionMenuChildView) childBefore).needsDividerAfter();
+        }
+        if (childIndex > 0 && child instanceof ActionMenuChildView) {
+            result |= ((ActionMenuChildView) child).needsDividerBefore();
+        }
+        return result;
+    }
+
+    /** @hide */
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        return false;
+    }
+
+    /** @hide */
+    public void setExpandedActionViewsExclusive(boolean exclusive) {
+        mPresenter.setExpandedActionViewsExclusive(exclusive);
+    }
+
+    /**
+     * Interface responsible for receiving menu item click events if the items themselves
+     * do not have individual item click listeners.
+     */
+    public interface OnMenuItemClickListener {
+        /**
+         * This method will be invoked when a menu item is clicked if the item itself did
+         * not already handle the event.
+         *
+         * @param item {@link MenuItem} that was clicked
+         * @return <code>true</code> if the event was handled, <code>false</code> otherwise.
+         */
+        public boolean onMenuItemClick(MenuItem item);
+    }
+
+    private class MenuBuilderCallback implements MenuBuilder.Callback {
+        @Override
+        public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+            return mOnMenuItemClickListener != null &&
+                    mOnMenuItemClickListener.onMenuItemClick(item);
+        }
+
+        @Override
+        public void onMenuModeChange(MenuBuilder menu) {
+            if (mMenuBuilderCallback != null) {
+                mMenuBuilderCallback.onMenuModeChange(menu);
+            }
+        }
+    }
+
+    private class ActionMenuPresenterCallback implements ActionMenuPresenter.Callback {
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        }
+
+        @Override
+        public boolean onOpenSubMenu(MenuBuilder subMenu) {
+            return false;
+        }
+    }
+
+    /** @hide */
+    public interface ActionMenuChildView {
+        public boolean needsDividerBefore();
+        public boolean needsDividerAfter();
+    }
+
+    public static class LayoutParams extends LinearLayout.LayoutParams {
+        /** @hide */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public boolean isOverflowButton;
+
+        /** @hide */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int cellsUsed;
+
+        /** @hide */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int extraPixels;
+
+        /** @hide */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public boolean expandable;
+
+        /** @hide */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public boolean preventEdgeOffset;
+
+        /** @hide */
+        public boolean expanded;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams other) {
+            super(other);
+        }
+
+        public LayoutParams(LayoutParams other) {
+            super((LinearLayout.LayoutParams) other);
+            isOverflowButton = other.isOverflowButton;
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+            isOverflowButton = false;
+        }
+
+        /** @hide */
+        public LayoutParams(int width, int height, boolean isOverflowButton) {
+            super(width, height);
+            this.isOverflowButton = isOverflowButton;
+        }
+
+        /** @hide */
+        @Override
+        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+            super.encodeProperties(encoder);
+
+            encoder.addProperty("layout:overFlowButton", isOverflowButton);
+            encoder.addProperty("layout:cellsUsed", cellsUsed);
+            encoder.addProperty("layout:extraPixels", extraPixels);
+            encoder.addProperty("layout:expandable", expandable);
+            encoder.addProperty("layout:preventEdgeOffset", preventEdgeOffset);
+        }
+    }
+}
diff --git a/android/widget/ActivityChooserModel.java b/android/widget/ActivityChooserModel.java
new file mode 100644
index 0000000..75c857c
--- /dev/null
+++ b/android/widget/ActivityChooserModel.java
@@ -0,0 +1,1131 @@
+/*
+ * 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 android.widget;
+
+import android.app.ActivityManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.database.DataSetObservable;
+import android.os.AsyncTask;
+import android.text.TextUtils;
+import android.util.Log;
+import android.util.Xml;
+
+import com.android.internal.content.PackageMonitor;
+
+import org.xmlpull.v1.XmlPullParser;
+import org.xmlpull.v1.XmlPullParserException;
+import org.xmlpull.v1.XmlSerializer;
+
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * <p>
+ * This class represents a data model for choosing a component for handing a
+ * given {@link Intent}. The model is responsible for querying the system for
+ * activities that can handle the given intent and order found activities
+ * based on historical data of previous choices. The historical data is stored
+ * in an application private file. If a client does not want to have persistent
+ * choice history the file can be omitted, thus the activities will be ordered
+ * based on historical usage for the current session.
+ * <p>
+ * </p>
+ * For each backing history file there is a singleton instance of this class. Thus,
+ * several clients that specify the same history file will share the same model. Note
+ * that if multiple clients are sharing the same model they should implement semantically
+ * equivalent functionality since setting the model intent will change the found
+ * activities and they may be inconsistent with the functionality of some of the clients.
+ * For example, choosing a share activity can be implemented by a single backing
+ * model and two different views for performing the selection. If however, one of the
+ * views is used for sharing but the other for importing, for example, then each
+ * view should be backed by a separate model.
+ * </p>
+ * <p>
+ * The way clients interact with this class is as follows:
+ * </p>
+ * <p>
+ * <pre>
+ * <code>
+ *  // Get a model and set it to a couple of clients with semantically similar function.
+ *  ActivityChooserModel dataModel =
+ *      ActivityChooserModel.get(context, "task_specific_history_file_name.xml");
+ *
+ *  ActivityChooserModelClient modelClient1 = getActivityChooserModelClient1();
+ *  modelClient1.setActivityChooserModel(dataModel);
+ *
+ *  ActivityChooserModelClient modelClient2 = getActivityChooserModelClient2();
+ *  modelClient2.setActivityChooserModel(dataModel);
+ *
+ *  // Set an intent to choose a an activity for.
+ *  dataModel.setIntent(intent);
+ * <pre>
+ * <code>
+ * </p>
+ * <p>
+ * <strong>Note:</strong> This class is thread safe.
+ * </p>
+ *
+ * @hide
+ */
+public class ActivityChooserModel extends DataSetObservable {
+
+    /**
+     * Client that utilizes an {@link ActivityChooserModel}.
+     */
+    public interface ActivityChooserModelClient {
+
+        /**
+         * Sets the {@link ActivityChooserModel}.
+         *
+         * @param dataModel The model.
+         */
+        public void setActivityChooserModel(ActivityChooserModel dataModel);
+    }
+
+    /**
+     * Defines a sorter that is responsible for sorting the activities
+     * based on the provided historical choices and an intent.
+     */
+    public interface ActivitySorter {
+
+        /**
+         * Sorts the <code>activities</code> in descending order of relevance
+         * based on previous history and an intent.
+         *
+         * @param intent The {@link Intent}.
+         * @param activities Activities to be sorted.
+         * @param historicalRecords Historical records.
+         */
+        // This cannot be done by a simple comparator since an Activity weight
+        // is computed from history. Note that Activity implements Comparable.
+        public void sort(Intent intent, List<ActivityResolveInfo> activities,
+                List<HistoricalRecord> historicalRecords);
+    }
+
+    /**
+     * Listener for choosing an activity.
+     */
+    public interface OnChooseActivityListener {
+
+        /**
+         * Called when an activity has been chosen. The client can decide whether
+         * an activity can be chosen and if so the caller of
+         * {@link ActivityChooserModel#chooseActivity(int)} will receive and {@link Intent}
+         * for launching it.
+         * <p>
+         * <strong>Note:</strong> Modifying the intent is not permitted and
+         *     any changes to the latter will be ignored.
+         * </p>
+         *
+         * @param host The listener's host model.
+         * @param intent The intent for launching the chosen activity.
+         * @return Whether the intent is handled and should not be delivered to clients.
+         *
+         * @see ActivityChooserModel#chooseActivity(int)
+         */
+        public boolean onChooseActivity(ActivityChooserModel host, Intent intent);
+    }
+
+    /**
+     * Flag for selecting debug mode.
+     */
+    private static final boolean DEBUG = false;
+
+    /**
+     * Tag used for logging.
+     */
+    private static final String LOG_TAG = ActivityChooserModel.class.getSimpleName();
+
+    /**
+     * The root tag in the history file.
+     */
+    private static final String TAG_HISTORICAL_RECORDS = "historical-records";
+
+    /**
+     * The tag for a record in the history file.
+     */
+    private static final String TAG_HISTORICAL_RECORD = "historical-record";
+
+    /**
+     * Attribute for the activity.
+     */
+    private static final String ATTRIBUTE_ACTIVITY = "activity";
+
+    /**
+     * Attribute for the choice time.
+     */
+    private static final String ATTRIBUTE_TIME = "time";
+
+    /**
+     * Attribute for the choice weight.
+     */
+    private static final String ATTRIBUTE_WEIGHT = "weight";
+
+    /**
+     * The default name of the choice history file.
+     */
+    public static final String DEFAULT_HISTORY_FILE_NAME =
+        "activity_choser_model_history.xml";
+
+    /**
+     * The default maximal length of the choice history.
+     */
+    public static final int DEFAULT_HISTORY_MAX_LENGTH = 50;
+
+    /**
+     * The amount with which to inflate a chosen activity when set as default.
+     */
+    private static final int DEFAULT_ACTIVITY_INFLATION = 5;
+
+    /**
+     * Default weight for a choice record.
+     */
+    private static final float DEFAULT_HISTORICAL_RECORD_WEIGHT = 1.0f;
+
+    /**
+     * The extension of the history file.
+     */
+    private static final String HISTORY_FILE_EXTENSION = ".xml";
+
+    /**
+     * An invalid item index.
+     */
+    private static final int INVALID_INDEX = -1;
+
+    /**
+     * Lock to guard the model registry.
+     */
+    private static final Object sRegistryLock = new Object();
+
+    /**
+     * This the registry for data models.
+     */
+    private static final Map<String, ActivityChooserModel> sDataModelRegistry =
+        new HashMap<String, ActivityChooserModel>();
+
+    /**
+     * Lock for synchronizing on this instance.
+     */
+    private final Object mInstanceLock = new Object();
+
+    /**
+     * List of activities that can handle the current intent.
+     */
+    private final List<ActivityResolveInfo> mActivities = new ArrayList<ActivityResolveInfo>();
+
+    /**
+     * List with historical choice records.
+     */
+    private final List<HistoricalRecord> mHistoricalRecords = new ArrayList<HistoricalRecord>();
+
+    /**
+     * Monitor for added and removed packages.
+     */
+    private final PackageMonitor mPackageMonitor = new DataModelPackageMonitor();
+
+    /**
+     * Context for accessing resources.
+     */
+    private final Context mContext;
+
+    /**
+     * The name of the history file that backs this model.
+     */
+    private final String mHistoryFileName;
+
+    /**
+     * The intent for which a activity is being chosen.
+     */
+    private Intent mIntent;
+
+    /**
+     * The sorter for ordering activities based on intent and past choices.
+     */
+    private ActivitySorter mActivitySorter = new DefaultSorter();
+
+    /**
+     * The maximal length of the choice history.
+     */
+    private int mHistoryMaxSize = DEFAULT_HISTORY_MAX_LENGTH;
+
+    /**
+     * Flag whether choice history can be read. In general many clients can
+     * share the same data model and {@link #readHistoricalDataIfNeeded()} may be called
+     * by arbitrary of them any number of times. Therefore, this class guarantees
+     * that the very first read succeeds and subsequent reads can be performed
+     * only after a call to {@link #persistHistoricalDataIfNeeded()} followed by change
+     * of the share records.
+     */
+    private boolean mCanReadHistoricalData = true;
+
+    /**
+     * Flag whether the choice history was read. This is used to enforce that
+     * before calling {@link #persistHistoricalDataIfNeeded()} a call to
+     * {@link #persistHistoricalDataIfNeeded()} has been made. This aims to avoid a
+     * scenario in which a choice history file exits, it is not read yet and
+     * it is overwritten. Note that always all historical records are read in
+     * full and the file is rewritten. This is necessary since we need to
+     * purge old records that are outside of the sliding window of past choices.
+     */
+    private boolean mReadShareHistoryCalled = false;
+
+    /**
+     * Flag whether the choice records have changed. In general many clients can
+     * share the same data model and {@link #persistHistoricalDataIfNeeded()} may be called
+     * by arbitrary of them any number of times. Therefore, this class guarantees
+     * that choice history will be persisted only if it has changed.
+     */
+    private boolean mHistoricalRecordsChanged = true;
+
+    /**
+     * Flag whether to reload the activities for the current intent.
+     */
+    private boolean mReloadActivities = false;
+
+    /**
+     * Policy for controlling how the model handles chosen activities.
+     */
+    private OnChooseActivityListener mActivityChoserModelPolicy;
+
+    /**
+     * Gets the data model backed by the contents of the provided file with historical data.
+     * Note that only one data model is backed by a given file, thus multiple calls with
+     * the same file name will return the same model instance. If no such instance is present
+     * it is created.
+     * <p>
+     * <strong>Note:</strong> To use the default historical data file clients should explicitly
+     * pass as file name {@link #DEFAULT_HISTORY_FILE_NAME}. If no persistence of the choice
+     * history is desired clients should pass <code>null</code> for the file name. In such
+     * case a new model is returned for each invocation.
+     * </p>
+     *
+     * <p>
+     * <strong>Always use difference historical data files for semantically different actions.
+     * For example, sharing is different from importing.</strong>
+     * </p>
+     *
+     * @param context Context for loading resources.
+     * @param historyFileName File name with choice history, <code>null</code>
+     *        if the model should not be backed by a file. In this case the activities
+     *        will be ordered only by data from the current session.
+     *
+     * @return The model.
+     */
+    public static ActivityChooserModel get(Context context, String historyFileName) {
+        synchronized (sRegistryLock) {
+            ActivityChooserModel dataModel = sDataModelRegistry.get(historyFileName);
+            if (dataModel == null) {
+                dataModel = new ActivityChooserModel(context, historyFileName);
+                sDataModelRegistry.put(historyFileName, dataModel);
+            }
+            return dataModel;
+        }
+    }
+
+    /**
+     * Creates a new instance.
+     *
+     * @param context Context for loading resources.
+     * @param historyFileName The history XML file.
+     */
+    private ActivityChooserModel(Context context, String historyFileName) {
+        mContext = context.getApplicationContext();
+        if (!TextUtils.isEmpty(historyFileName)
+                && !historyFileName.endsWith(HISTORY_FILE_EXTENSION)) {
+            mHistoryFileName = historyFileName + HISTORY_FILE_EXTENSION;
+        } else {
+            mHistoryFileName = historyFileName;
+        }
+        mPackageMonitor.register(mContext, null, true);
+    }
+
+    /**
+     * Sets an intent for which to choose a activity.
+     * <p>
+     * <strong>Note:</strong> Clients must set only semantically similar
+     * intents for each data model.
+     * <p>
+     *
+     * @param intent The intent.
+     */
+    public void setIntent(Intent intent) {
+        synchronized (mInstanceLock) {
+            if (mIntent == intent) {
+                return;
+            }
+            mIntent = intent;
+            mReloadActivities = true;
+            ensureConsistentState();
+        }
+    }
+
+    /**
+     * Gets the intent for which a activity is being chosen.
+     *
+     * @return The intent.
+     */
+    public Intent getIntent() {
+        synchronized (mInstanceLock) {
+            return mIntent;
+        }
+    }
+
+    /**
+     * Gets the number of activities that can handle the intent.
+     *
+     * @return The activity count.
+     *
+     * @see #setIntent(Intent)
+     */
+    public int getActivityCount() {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+            return mActivities.size();
+        }
+    }
+
+    /**
+     * Gets an activity at a given index.
+     *
+     * @return The activity.
+     *
+     * @see ActivityResolveInfo
+     * @see #setIntent(Intent)
+     */
+    public ResolveInfo getActivity(int index) {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+            return mActivities.get(index).resolveInfo;
+        }
+    }
+
+    /**
+     * Gets the index of a the given activity.
+     *
+     * @param activity The activity index.
+     *
+     * @return The index if found, -1 otherwise.
+     */
+    public int getActivityIndex(ResolveInfo activity) {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+            List<ActivityResolveInfo> activities = mActivities;
+            final int activityCount = activities.size();
+            for (int i = 0; i < activityCount; i++) {
+                ActivityResolveInfo currentActivity = activities.get(i);
+                if (currentActivity.resolveInfo == activity) {
+                    return i;
+                }
+            }
+            return INVALID_INDEX;
+        }
+    }
+
+    /**
+     * Chooses a activity to handle the current intent. This will result in
+     * adding a historical record for that action and construct intent with
+     * its component name set such that it can be immediately started by the
+     * client.
+     * <p>
+     * <strong>Note:</strong> By calling this method the client guarantees
+     * that the returned intent will be started. This intent is returned to
+     * the client solely to let additional customization before the start.
+     * </p>
+     *
+     * @return An {@link Intent} for launching the activity or null if the
+     *         policy has consumed the intent or there is not current intent
+     *         set via {@link #setIntent(Intent)}.
+     *
+     * @see HistoricalRecord
+     * @see OnChooseActivityListener
+     */
+    public Intent chooseActivity(int index) {
+        synchronized (mInstanceLock) {
+            if (mIntent == null) {
+                return null;
+            }
+
+            ensureConsistentState();
+
+            ActivityResolveInfo chosenActivity = mActivities.get(index);
+
+            ComponentName chosenName = new ComponentName(
+                    chosenActivity.resolveInfo.activityInfo.packageName,
+                    chosenActivity.resolveInfo.activityInfo.name);
+
+            Intent choiceIntent = new Intent(mIntent);
+            choiceIntent.setComponent(chosenName);
+
+            if (mActivityChoserModelPolicy != null) {
+                // Do not allow the policy to change the intent.
+                Intent choiceIntentCopy = new Intent(choiceIntent);
+                final boolean handled = mActivityChoserModelPolicy.onChooseActivity(this,
+                        choiceIntentCopy);
+                if (handled) {
+                    return null;
+                }
+            }
+
+            HistoricalRecord historicalRecord = new HistoricalRecord(chosenName,
+                    System.currentTimeMillis(), DEFAULT_HISTORICAL_RECORD_WEIGHT);
+            addHisoricalRecord(historicalRecord);
+
+            return choiceIntent;
+        }
+    }
+
+    /**
+     * Sets the listener for choosing an activity.
+     *
+     * @param listener The listener.
+     */
+    public void setOnChooseActivityListener(OnChooseActivityListener listener) {
+        synchronized (mInstanceLock) {
+            mActivityChoserModelPolicy = listener;
+        }
+    }
+
+    /**
+     * Gets the default activity, The default activity is defined as the one
+     * with highest rank i.e. the first one in the list of activities that can
+     * handle the intent.
+     *
+     * @return The default activity, <code>null</code> id not activities.
+     *
+     * @see #getActivity(int)
+     */
+    public ResolveInfo getDefaultActivity() {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+            if (!mActivities.isEmpty()) {
+                return mActivities.get(0).resolveInfo;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Sets the default activity. The default activity is set by adding a
+     * historical record with weight high enough that this activity will
+     * become the highest ranked. Such a strategy guarantees that the default
+     * will eventually change if not used. Also the weight of the record for
+     * setting a default is inflated with a constant amount to guarantee that
+     * it will stay as default for awhile.
+     *
+     * @param index The index of the activity to set as default.
+     */
+    public void setDefaultActivity(int index) {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+
+            ActivityResolveInfo newDefaultActivity = mActivities.get(index);
+            ActivityResolveInfo oldDefaultActivity = mActivities.get(0);
+
+            final float weight;
+            if (oldDefaultActivity != null) {
+                // Add a record with weight enough to boost the chosen at the top.
+                weight = oldDefaultActivity.weight - newDefaultActivity.weight
+                    + DEFAULT_ACTIVITY_INFLATION;
+            } else {
+                weight = DEFAULT_HISTORICAL_RECORD_WEIGHT;
+            }
+
+            ComponentName defaultName = new ComponentName(
+                    newDefaultActivity.resolveInfo.activityInfo.packageName,
+                    newDefaultActivity.resolveInfo.activityInfo.name);
+            HistoricalRecord historicalRecord = new HistoricalRecord(defaultName,
+                    System.currentTimeMillis(), weight);
+            addHisoricalRecord(historicalRecord);
+        }
+    }
+
+    /**
+     * Persists the history data to the backing file if the latter
+     * was provided. Calling this method before a call to {@link #readHistoricalDataIfNeeded()}
+     * throws an exception. Calling this method more than one without choosing an
+     * activity has not effect.
+     *
+     * @throws IllegalStateException If this method is called before a call to
+     *         {@link #readHistoricalDataIfNeeded()}.
+     */
+    private void persistHistoricalDataIfNeeded() {
+        if (!mReadShareHistoryCalled) {
+            throw new IllegalStateException("No preceding call to #readHistoricalData");
+        }
+        if (!mHistoricalRecordsChanged) {
+            return;
+        }
+        mHistoricalRecordsChanged = false;
+        if (!TextUtils.isEmpty(mHistoryFileName)) {
+            new PersistHistoryAsyncTask().executeOnExecutor(AsyncTask.SERIAL_EXECUTOR,
+                    new ArrayList<HistoricalRecord>(mHistoricalRecords), mHistoryFileName);
+        }
+    }
+
+    /**
+     * Sets the sorter for ordering activities based on historical data and an intent.
+     *
+     * @param activitySorter The sorter.
+     *
+     * @see ActivitySorter
+     */
+    public void setActivitySorter(ActivitySorter activitySorter) {
+        synchronized (mInstanceLock) {
+            if (mActivitySorter == activitySorter) {
+                return;
+            }
+            mActivitySorter = activitySorter;
+            if (sortActivitiesIfNeeded()) {
+                notifyChanged();
+            }
+        }
+    }
+
+    /**
+     * Sets the maximal size of the historical data. Defaults to
+     * {@link #DEFAULT_HISTORY_MAX_LENGTH}
+     * <p>
+     *   <strong>Note:</strong> Setting this property will immediately
+     *   enforce the specified max history size by dropping enough old
+     *   historical records to enforce the desired size. Thus, any
+     *   records that exceed the history size will be discarded and
+     *   irreversibly lost.
+     * </p>
+     *
+     * @param historyMaxSize The max history size.
+     */
+    public void setHistoryMaxSize(int historyMaxSize) {
+        synchronized (mInstanceLock) {
+            if (mHistoryMaxSize == historyMaxSize) {
+                return;
+            }
+            mHistoryMaxSize = historyMaxSize;
+            pruneExcessiveHistoricalRecordsIfNeeded();
+            if (sortActivitiesIfNeeded()) {
+                notifyChanged();
+            }
+        }
+    }
+
+    /**
+     * Gets the history max size.
+     *
+     * @return The history max size.
+     */
+    public int getHistoryMaxSize() {
+        synchronized (mInstanceLock) {
+            return mHistoryMaxSize;
+        }
+    }
+
+    /**
+     * Gets the history size.
+     *
+     * @return The history size.
+     */
+    public int getHistorySize() {
+        synchronized (mInstanceLock) {
+            ensureConsistentState();
+            return mHistoricalRecords.size();
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        super.finalize();
+        mPackageMonitor.unregister();
+    }
+
+    /**
+     * Ensures the model is in a consistent state which is the
+     * activities for the current intent have been loaded, the
+     * most recent history has been read, and the activities
+     * are sorted.
+     */
+    private void ensureConsistentState() {
+        boolean stateChanged = loadActivitiesIfNeeded();
+        stateChanged |= readHistoricalDataIfNeeded();
+        pruneExcessiveHistoricalRecordsIfNeeded();
+        if (stateChanged) {
+            sortActivitiesIfNeeded();
+            notifyChanged();
+        }
+    }
+
+    /**
+     * Sorts the activities if necessary which is if there is a
+     * sorter, there are some activities to sort, and there is some
+     * historical data.
+     *
+     * @return Whether sorting was performed.
+     */
+    private boolean sortActivitiesIfNeeded() {
+        if (mActivitySorter != null && mIntent != null
+                && !mActivities.isEmpty() && !mHistoricalRecords.isEmpty()) {
+            mActivitySorter.sort(mIntent, mActivities,
+                    Collections.unmodifiableList(mHistoricalRecords));
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Loads the activities for the current intent if needed which is
+     * if they are not already loaded for the current intent.
+     *
+     * @return Whether loading was performed.
+     */
+    private boolean loadActivitiesIfNeeded() {
+        if (mReloadActivities && mIntent != null) {
+            mReloadActivities = false;
+            mActivities.clear();
+            List<ResolveInfo> resolveInfos = mContext.getPackageManager()
+                    .queryIntentActivities(mIntent, 0);
+            final int resolveInfoCount = resolveInfos.size();
+            for (int i = 0; i < resolveInfoCount; i++) {
+                ResolveInfo resolveInfo = resolveInfos.get(i);
+                ActivityInfo activityInfo = resolveInfo.activityInfo;
+                if (ActivityManager.checkComponentPermission(activityInfo.permission,
+                        android.os.Process.myUid(), activityInfo.applicationInfo.uid,
+                        activityInfo.exported) == PackageManager.PERMISSION_GRANTED) {
+                    mActivities.add(new ActivityResolveInfo(resolveInfo));
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Reads the historical data if necessary which is it has
+     * changed, there is a history file, and there is not persist
+     * in progress.
+     *
+     * @return Whether reading was performed.
+     */
+    private boolean readHistoricalDataIfNeeded() {
+        if (mCanReadHistoricalData && mHistoricalRecordsChanged &&
+                !TextUtils.isEmpty(mHistoryFileName)) {
+            mCanReadHistoricalData = false;
+            mReadShareHistoryCalled = true;
+            readHistoricalDataImpl();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Adds a historical record.
+     *
+     * @param historicalRecord The record to add.
+     * @return True if the record was added.
+     */
+    private boolean addHisoricalRecord(HistoricalRecord historicalRecord) {
+        final boolean added = mHistoricalRecords.add(historicalRecord);
+        if (added) {
+            mHistoricalRecordsChanged = true;
+            pruneExcessiveHistoricalRecordsIfNeeded();
+            persistHistoricalDataIfNeeded();
+            sortActivitiesIfNeeded();
+            notifyChanged();
+        }
+        return added;
+    }
+
+    /**
+     * Prunes older excessive records to guarantee maxHistorySize.
+     */
+    private void pruneExcessiveHistoricalRecordsIfNeeded() {
+        final int pruneCount = mHistoricalRecords.size() - mHistoryMaxSize;
+        if (pruneCount <= 0) {
+            return;
+        }
+        mHistoricalRecordsChanged = true;
+        for (int i = 0; i < pruneCount; i++) {
+            HistoricalRecord prunedRecord = mHistoricalRecords.remove(0);
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Pruned: " + prunedRecord);
+            }
+        }
+    }
+
+    /**
+     * Represents a record in the history.
+     */
+    public final static class HistoricalRecord {
+
+        /**
+         * The activity name.
+         */
+        public final ComponentName activity;
+
+        /**
+         * The choice time.
+         */
+        public final long time;
+
+        /**
+         * The record weight.
+         */
+        public final float weight;
+
+        /**
+         * Creates a new instance.
+         *
+         * @param activityName The activity component name flattened to string.
+         * @param time The time the activity was chosen.
+         * @param weight The weight of the record.
+         */
+        public HistoricalRecord(String activityName, long time, float weight) {
+            this(ComponentName.unflattenFromString(activityName), time, weight);
+        }
+
+        /**
+         * Creates a new instance.
+         *
+         * @param activityName The activity name.
+         * @param time The time the activity was chosen.
+         * @param weight The weight of the record.
+         */
+        public HistoricalRecord(ComponentName activityName, long time, float weight) {
+            this.activity = activityName;
+            this.time = time;
+            this.weight = weight;
+        }
+
+        @Override
+        public int hashCode() {
+            final int prime = 31;
+            int result = 1;
+            result = prime * result + ((activity == null) ? 0 : activity.hashCode());
+            result = prime * result + (int) (time ^ (time >>> 32));
+            result = prime * result + Float.floatToIntBits(weight);
+            return result;
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            HistoricalRecord other = (HistoricalRecord) obj;
+            if (activity == null) {
+                if (other.activity != null) {
+                    return false;
+                }
+            } else if (!activity.equals(other.activity)) {
+                return false;
+            }
+            if (time != other.time) {
+                return false;
+            }
+            if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
+                return false;
+            }
+            return true;
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("[");
+            builder.append("; activity:").append(activity);
+            builder.append("; time:").append(time);
+            builder.append("; weight:").append(new BigDecimal(weight));
+            builder.append("]");
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Represents an activity.
+     */
+    public final class ActivityResolveInfo implements Comparable<ActivityResolveInfo> {
+
+        /**
+         * The {@link ResolveInfo} of the activity.
+         */
+        public final ResolveInfo resolveInfo;
+
+        /**
+         * Weight of the activity. Useful for sorting.
+         */
+        public float weight;
+
+        /**
+         * Creates a new instance.
+         *
+         * @param resolveInfo activity {@link ResolveInfo}.
+         */
+        public ActivityResolveInfo(ResolveInfo resolveInfo) {
+            this.resolveInfo = resolveInfo;
+        }
+
+        @Override
+        public int hashCode() {
+            return 31 + Float.floatToIntBits(weight);
+        }
+
+        @Override
+        public boolean equals(Object obj) {
+            if (this == obj) {
+                return true;
+            }
+            if (obj == null) {
+                return false;
+            }
+            if (getClass() != obj.getClass()) {
+                return false;
+            }
+            ActivityResolveInfo other = (ActivityResolveInfo) obj;
+            if (Float.floatToIntBits(weight) != Float.floatToIntBits(other.weight)) {
+                return false;
+            }
+            return true;
+        }
+
+        public int compareTo(ActivityResolveInfo another) {
+             return  Float.floatToIntBits(another.weight) - Float.floatToIntBits(weight);
+        }
+
+        @Override
+        public String toString() {
+            StringBuilder builder = new StringBuilder();
+            builder.append("[");
+            builder.append("resolveInfo:").append(resolveInfo.toString());
+            builder.append("; weight:").append(new BigDecimal(weight));
+            builder.append("]");
+            return builder.toString();
+        }
+    }
+
+    /**
+     * Default activity sorter implementation.
+     */
+    private final class DefaultSorter implements ActivitySorter {
+        private static final float WEIGHT_DECAY_COEFFICIENT = 0.95f;
+
+        private final Map<ComponentName, ActivityResolveInfo> mPackageNameToActivityMap =
+                new HashMap<ComponentName, ActivityResolveInfo>();
+
+        public void sort(Intent intent, List<ActivityResolveInfo> activities,
+                List<HistoricalRecord> historicalRecords) {
+            Map<ComponentName, ActivityResolveInfo> componentNameToActivityMap =
+                    mPackageNameToActivityMap;
+            componentNameToActivityMap.clear();
+
+            final int activityCount = activities.size();
+            for (int i = 0; i < activityCount; i++) {
+                ActivityResolveInfo activity = activities.get(i);
+                activity.weight = 0.0f;
+                ComponentName componentName = new ComponentName(
+                        activity.resolveInfo.activityInfo.packageName,
+                        activity.resolveInfo.activityInfo.name);
+                componentNameToActivityMap.put(componentName, activity);
+            }
+
+            final int lastShareIndex = historicalRecords.size() - 1;
+            float nextRecordWeight = 1;
+            for (int i = lastShareIndex; i >= 0; i--) {
+                HistoricalRecord historicalRecord = historicalRecords.get(i);
+                ComponentName componentName = historicalRecord.activity;
+                ActivityResolveInfo activity = componentNameToActivityMap.get(componentName);
+                if (activity != null) {
+                    activity.weight += historicalRecord.weight * nextRecordWeight;
+                    nextRecordWeight = nextRecordWeight * WEIGHT_DECAY_COEFFICIENT;
+                }
+            }
+
+            Collections.sort(activities);
+
+            if (DEBUG) {
+                for (int i = 0; i < activityCount; i++) {
+                    Log.i(LOG_TAG, "Sorted: " + activities.get(i));
+                }
+            }
+        }
+    }
+
+    private void readHistoricalDataImpl() {
+        FileInputStream fis = null;
+        try {
+            fis = mContext.openFileInput(mHistoryFileName);
+        } catch (FileNotFoundException fnfe) {
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Could not open historical records file: " + mHistoryFileName);
+            }
+            return;
+        }
+        try {
+            XmlPullParser parser = Xml.newPullParser();
+            parser.setInput(fis, StandardCharsets.UTF_8.name());
+
+            int type = XmlPullParser.START_DOCUMENT;
+            while (type != XmlPullParser.END_DOCUMENT && type != XmlPullParser.START_TAG) {
+                type = parser.next();
+            }
+
+            if (!TAG_HISTORICAL_RECORDS.equals(parser.getName())) {
+                throw new XmlPullParserException("Share records file does not start with "
+                        + TAG_HISTORICAL_RECORDS + " tag.");
+            }
+
+            List<HistoricalRecord> historicalRecords = mHistoricalRecords;
+            historicalRecords.clear();
+
+            while (true) {
+                type = parser.next();
+                if (type == XmlPullParser.END_DOCUMENT) {
+                    break;
+                }
+                if (type == XmlPullParser.END_TAG || type == XmlPullParser.TEXT) {
+                    continue;
+                }
+                String nodeName = parser.getName();
+                if (!TAG_HISTORICAL_RECORD.equals(nodeName)) {
+                    throw new XmlPullParserException("Share records file not well-formed.");
+                }
+
+                String activity = parser.getAttributeValue(null, ATTRIBUTE_ACTIVITY);
+                final long time =
+                    Long.parseLong(parser.getAttributeValue(null, ATTRIBUTE_TIME));
+                final float weight =
+                    Float.parseFloat(parser.getAttributeValue(null, ATTRIBUTE_WEIGHT));
+                 HistoricalRecord readRecord = new HistoricalRecord(activity, time, weight);
+                historicalRecords.add(readRecord);
+
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "Read " + readRecord.toString());
+                }
+            }
+
+            if (DEBUG) {
+                Log.i(LOG_TAG, "Read " + historicalRecords.size() + " historical records.");
+            }
+        } catch (XmlPullParserException xppe) {
+            Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, xppe);
+        } catch (IOException ioe) {
+            Log.e(LOG_TAG, "Error reading historical recrod file: " + mHistoryFileName, ioe);
+        } finally {
+            if (fis != null) {
+                try {
+                    fis.close();
+                } catch (IOException ioe) {
+                    /* ignore */
+                }
+            }
+        }
+    }
+
+    /**
+     * Command for persisting the historical records to a file off the UI thread.
+     */
+    private final class PersistHistoryAsyncTask extends AsyncTask<Object, Void, Void> {
+
+        @Override
+        @SuppressWarnings("unchecked")
+        public Void doInBackground(Object... args) {
+            List<HistoricalRecord> historicalRecords = (List<HistoricalRecord>) args[0];
+            String hostoryFileName = (String) args[1];
+
+            FileOutputStream fos = null;
+
+            try {
+                fos = mContext.openFileOutput(hostoryFileName, Context.MODE_PRIVATE);
+            } catch (FileNotFoundException fnfe) {
+                Log.e(LOG_TAG, "Error writing historical recrod file: " + hostoryFileName, fnfe);
+                return null;
+            }
+
+            XmlSerializer serializer = Xml.newSerializer();
+
+            try {
+                serializer.setOutput(fos, null);
+                serializer.startDocument(StandardCharsets.UTF_8.name(), true);
+                serializer.startTag(null, TAG_HISTORICAL_RECORDS);
+
+                final int recordCount = historicalRecords.size();
+                for (int i = 0; i < recordCount; i++) {
+                    HistoricalRecord record = historicalRecords.remove(0);
+                    serializer.startTag(null, TAG_HISTORICAL_RECORD);
+                    serializer.attribute(null, ATTRIBUTE_ACTIVITY,
+                            record.activity.flattenToString());
+                    serializer.attribute(null, ATTRIBUTE_TIME, String.valueOf(record.time));
+                    serializer.attribute(null, ATTRIBUTE_WEIGHT, String.valueOf(record.weight));
+                    serializer.endTag(null, TAG_HISTORICAL_RECORD);
+                    if (DEBUG) {
+                        Log.i(LOG_TAG, "Wrote " + record.toString());
+                    }
+                }
+
+                serializer.endTag(null, TAG_HISTORICAL_RECORDS);
+                serializer.endDocument();
+
+                if (DEBUG) {
+                    Log.i(LOG_TAG, "Wrote " + recordCount + " historical records.");
+                }
+            } catch (IllegalArgumentException iae) {
+                Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, iae);
+            } catch (IllegalStateException ise) {
+                Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ise);
+            } catch (IOException ioe) {
+                Log.e(LOG_TAG, "Error writing historical recrod file: " + mHistoryFileName, ioe);
+            } finally {
+                mCanReadHistoricalData = true;
+                if (fos != null) {
+                    try {
+                        fos.close();
+                    } catch (IOException e) {
+                        /* ignore */
+                    }
+                }
+            }
+            return null;
+        }
+    }
+
+    /**
+     * Keeps in sync the historical records and activities with the installed applications.
+     */
+    private final class DataModelPackageMonitor extends PackageMonitor {
+
+        @Override
+        public void onSomePackagesChanged() {
+            mReloadActivities = true;
+        }
+    }
+}
diff --git a/android/widget/ActivityChooserView.java b/android/widget/ActivityChooserView.java
new file mode 100644
index 0000000..121a8c5
--- /dev/null
+++ b/android/widget/ActivityChooserView.java
@@ -0,0 +1,864 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.StringRes;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Color;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ActionProvider;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.ActivityChooserModel.ActivityChooserModelClient;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * This class is a view for choosing an activity for handling a given {@link Intent}.
+ * <p>
+ * The view is composed of two adjacent buttons:
+ * <ul>
+ * <li>
+ * The left button is an immediate action and allows one click activity choosing.
+ * Tapping this button immediately executes the intent without requiring any further
+ * user input. Long press on this button shows a popup for changing the default
+ * activity.
+ * </li>
+ * <li>
+ * The right button is an overflow action and provides an optimized menu
+ * of additional activities. Tapping this button shows a popup anchored to this
+ * view, listing the most frequently used activities. This list is initially
+ * limited to a small number of items in frequency used order. The last item,
+ * "Show all..." serves as an affordance to display all available activities.
+ * </li>
+ * </ul>
+ * </p>
+ *
+ * @hide
+ */
+public class ActivityChooserView extends ViewGroup implements ActivityChooserModelClient {
+
+    private static final String LOG_TAG = "ActivityChooserView";
+
+    /**
+     * An adapter for displaying the activities in an {@link AdapterView}.
+     */
+    private final ActivityChooserViewAdapter mAdapter;
+
+    /**
+     * Implementation of various interfaces to avoid publishing them in the APIs.
+     */
+    private final Callbacks mCallbacks;
+
+    /**
+     * The content of this view.
+     */
+    private final LinearLayout mActivityChooserContent;
+
+    /**
+     * Stores the background drawable to allow hiding and latter showing.
+     */
+    private final Drawable mActivityChooserContentBackground;
+
+    /**
+     * The expand activities action button;
+     */
+    private final FrameLayout mExpandActivityOverflowButton;
+
+    /**
+     * The image for the expand activities action button;
+     */
+    private final ImageView mExpandActivityOverflowButtonImage;
+
+    /**
+     * The default activities action button;
+     */
+    private final FrameLayout mDefaultActivityButton;
+
+    /**
+     * The image for the default activities action button;
+     */
+    private final ImageView mDefaultActivityButtonImage;
+
+    /**
+     * The maximal width of the list popup.
+     */
+    private final int mListPopupMaxWidth;
+
+    /**
+     * The ActionProvider hosting this view, if applicable.
+     */
+    ActionProvider mProvider;
+
+    /**
+     * Observer for the model data.
+     */
+    private final DataSetObserver mModelDataSetOberver = new DataSetObserver() {
+
+        @Override
+        public void onChanged() {
+            super.onChanged();
+            mAdapter.notifyDataSetChanged();
+        }
+        @Override
+        public void onInvalidated() {
+            super.onInvalidated();
+            mAdapter.notifyDataSetInvalidated();
+        }
+    };
+
+    private final OnGlobalLayoutListener mOnGlobalLayoutListener = new OnGlobalLayoutListener() {
+        @Override
+        public void onGlobalLayout() {
+            if (isShowingPopup()) {
+                if (!isShown()) {
+                    getListPopupWindow().dismiss();
+                } else {
+                    getListPopupWindow().show();
+                    if (mProvider != null) {
+                        mProvider.subUiVisibilityChanged(true);
+                    }
+                }
+            }
+        }
+    };
+
+    /**
+     * Popup window for showing the activity overflow list.
+     */
+    private ListPopupWindow mListPopupWindow;
+
+    /**
+     * Listener for the dismissal of the popup/alert.
+     */
+    private PopupWindow.OnDismissListener mOnDismissListener;
+
+    /**
+     * Flag whether a default activity currently being selected.
+     */
+    private boolean mIsSelectingDefaultActivity;
+
+    /**
+     * The count of activities in the popup.
+     */
+    private int mInitialActivityCount = ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT;
+
+    /**
+     * Flag whether this view is attached to a window.
+     */
+    private boolean mIsAttachedToWindow;
+
+    /**
+     * String resource for formatting content description of the default target.
+     */
+    private int mDefaultActionButtonContentDescription;
+
+    /**
+     * Create a new instance.
+     *
+     * @param context The application environment.
+     */
+    public ActivityChooserView(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a new instance.
+     *
+     * @param context The application environment.
+     * @param attrs A collection of attributes.
+     */
+    public ActivityChooserView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Create a new instance.
+     *
+     * @param context The application environment.
+     * @param attrs A collection of attributes.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     */
+    public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * Create a new instance.
+     *
+     * @param context The application environment.
+     * @param attrs A collection of attributes.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *        supplies default values for the view, used only if
+     *        defStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public ActivityChooserView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+                R.styleable.ActivityChooserView, defStyleAttr, defStyleRes);
+
+        mInitialActivityCount = attributesArray.getInt(
+                R.styleable.ActivityChooserView_initialActivityCount,
+                ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_DEFAULT);
+
+        Drawable expandActivityOverflowButtonDrawable = attributesArray.getDrawable(
+                R.styleable.ActivityChooserView_expandActivityOverflowButtonDrawable);
+
+        attributesArray.recycle();
+
+        LayoutInflater inflater = LayoutInflater.from(mContext);
+        inflater.inflate(R.layout.activity_chooser_view, this, true);
+
+        mCallbacks = new Callbacks();
+
+        mActivityChooserContent = (LinearLayout) findViewById(R.id.activity_chooser_view_content);
+        mActivityChooserContentBackground = mActivityChooserContent.getBackground();
+
+        mDefaultActivityButton = (FrameLayout) findViewById(R.id.default_activity_button);
+        mDefaultActivityButton.setOnClickListener(mCallbacks);
+        mDefaultActivityButton.setOnLongClickListener(mCallbacks);
+        mDefaultActivityButtonImage = mDefaultActivityButton.findViewById(R.id.image);
+
+        final FrameLayout expandButton = (FrameLayout) findViewById(R.id.expand_activities_button);
+        expandButton.setOnClickListener(mCallbacks);
+        expandButton.setAccessibilityDelegate(new AccessibilityDelegate() {
+            @Override
+            public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+                super.onInitializeAccessibilityNodeInfo(host, info);
+                info.setCanOpenPopup(true);
+            }
+        });
+        expandButton.setOnTouchListener(new ForwardingListener(expandButton) {
+            @Override
+            public ShowableListMenu getPopup() {
+                return getListPopupWindow();
+            }
+
+            @Override
+            protected boolean onForwardingStarted() {
+                showPopup();
+                return true;
+            }
+
+            @Override
+            protected boolean onForwardingStopped() {
+                dismissPopup();
+                return true;
+            }
+        });
+        mExpandActivityOverflowButton = expandButton;
+
+        mExpandActivityOverflowButtonImage =
+            expandButton.findViewById(R.id.image);
+        mExpandActivityOverflowButtonImage.setImageDrawable(expandActivityOverflowButtonDrawable);
+
+        mAdapter = new ActivityChooserViewAdapter();
+        mAdapter.registerDataSetObserver(new DataSetObserver() {
+            @Override
+            public void onChanged() {
+                super.onChanged();
+                updateAppearance();
+            }
+        });
+
+        Resources resources = context.getResources();
+        mListPopupMaxWidth = Math.max(resources.getDisplayMetrics().widthPixels / 2,
+              resources.getDimensionPixelSize(com.android.internal.R.dimen.config_prefDialogWidth));
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public void setActivityChooserModel(ActivityChooserModel dataModel) {
+        mAdapter.setDataModel(dataModel);
+        if (isShowingPopup()) {
+            dismissPopup();
+            showPopup();
+        }
+    }
+
+    /**
+     * Sets the background for the button that expands the activity
+     * overflow list.
+     *
+     * <strong>Note:</strong> Clients would like to set this drawable
+     * as a clue about the action the chosen activity will perform. For
+     * example, if a share activity is to be chosen the drawable should
+     * give a clue that sharing is to be performed.
+     *
+     * @param drawable The drawable.
+     */
+    public void setExpandActivityOverflowButtonDrawable(Drawable drawable) {
+        mExpandActivityOverflowButtonImage.setImageDrawable(drawable);
+    }
+
+    /**
+     * Sets the content description for the button that expands the activity
+     * overflow list.
+     *
+     * description as a clue about the action performed by the button.
+     * For example, if a share activity is to be chosen the content
+     * description should be something like "Share with".
+     *
+     * @param resourceId The content description resource id.
+     */
+    public void setExpandActivityOverflowButtonContentDescription(@StringRes int resourceId) {
+        CharSequence contentDescription = mContext.getString(resourceId);
+        mExpandActivityOverflowButtonImage.setContentDescription(contentDescription);
+    }
+
+    /**
+     * Set the provider hosting this view, if applicable.
+     * @hide Internal use only
+     */
+    public void setProvider(ActionProvider provider) {
+        mProvider = provider;
+    }
+
+    /**
+     * Shows the popup window with activities.
+     *
+     * @return True if the popup was shown, false if already showing.
+     */
+    public boolean showPopup() {
+        if (isShowingPopup() || !mIsAttachedToWindow) {
+            return false;
+        }
+        mIsSelectingDefaultActivity = false;
+        showPopupUnchecked(mInitialActivityCount);
+        return true;
+    }
+
+    /**
+     * Shows the popup no matter if it was already showing.
+     *
+     * @param maxActivityCount The max number of activities to display.
+     */
+    private void showPopupUnchecked(int maxActivityCount) {
+        if (mAdapter.getDataModel() == null) {
+            throw new IllegalStateException("No data model. Did you call #setDataModel?");
+        }
+
+        getViewTreeObserver().addOnGlobalLayoutListener(mOnGlobalLayoutListener);
+
+        final boolean defaultActivityButtonShown =
+            mDefaultActivityButton.getVisibility() == VISIBLE;
+
+        final int activityCount = mAdapter.getActivityCount();
+        final int maxActivityCountOffset = defaultActivityButtonShown ? 1 : 0;
+        if (maxActivityCount != ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED
+                && activityCount > maxActivityCount + maxActivityCountOffset) {
+            mAdapter.setShowFooterView(true);
+            mAdapter.setMaxActivityCount(maxActivityCount - 1);
+        } else {
+            mAdapter.setShowFooterView(false);
+            mAdapter.setMaxActivityCount(maxActivityCount);
+        }
+
+        ListPopupWindow popupWindow = getListPopupWindow();
+        if (!popupWindow.isShowing()) {
+            if (mIsSelectingDefaultActivity || !defaultActivityButtonShown) {
+                mAdapter.setShowDefaultActivity(true, defaultActivityButtonShown);
+            } else {
+                mAdapter.setShowDefaultActivity(false, false);
+            }
+            final int contentWidth = Math.min(mAdapter.measureContentWidth(), mListPopupMaxWidth);
+            popupWindow.setContentWidth(contentWidth);
+            popupWindow.show();
+            if (mProvider != null) {
+                mProvider.subUiVisibilityChanged(true);
+            }
+            popupWindow.getListView().setContentDescription(mContext.getString(
+                    R.string.activitychooserview_choose_application));
+            popupWindow.getListView().setSelector(new ColorDrawable(Color.TRANSPARENT));
+        }
+    }
+
+    /**
+     * Dismisses the popup window with activities.
+     *
+     * @return True if dismissed, false if already dismissed.
+     */
+    public boolean dismissPopup() {
+        if (isShowingPopup()) {
+            getListPopupWindow().dismiss();
+            ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+            if (viewTreeObserver.isAlive()) {
+                viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Gets whether the popup window with activities is shown.
+     *
+     * @return True if the popup is shown.
+     */
+    public boolean isShowingPopup() {
+        return getListPopupWindow().isShowing();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        ActivityChooserModel dataModel = mAdapter.getDataModel();
+        if (dataModel != null) {
+            dataModel.registerObserver(mModelDataSetOberver);
+        }
+        mIsAttachedToWindow = true;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        ActivityChooserModel dataModel = mAdapter.getDataModel();
+        if (dataModel != null) {
+            dataModel.unregisterObserver(mModelDataSetOberver);
+        }
+        ViewTreeObserver viewTreeObserver = getViewTreeObserver();
+        if (viewTreeObserver.isAlive()) {
+            viewTreeObserver.removeOnGlobalLayoutListener(mOnGlobalLayoutListener);
+        }
+        if (isShowingPopup()) {
+            dismissPopup();
+        }
+        mIsAttachedToWindow = false;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        View child = mActivityChooserContent;
+        // If the default action is not visible we want to be as tall as the
+        // ActionBar so if this widget is used in the latter it will look as
+        // a normal action button.
+        if (mDefaultActivityButton.getVisibility() != VISIBLE) {
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
+                    MeasureSpec.EXACTLY);
+        }
+        measureChild(child, widthMeasureSpec, heightMeasureSpec);
+        setMeasuredDimension(child.getMeasuredWidth(), child.getMeasuredHeight());
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mActivityChooserContent.layout(0, 0, right - left, bottom - top);
+        if (!isShowingPopup()) {
+            dismissPopup();
+        }
+    }
+
+    public ActivityChooserModel getDataModel() {
+        return mAdapter.getDataModel();
+    }
+
+    /**
+     * Sets a listener to receive a callback when the popup is dismissed.
+     *
+     * @param listener The listener to be notified.
+     */
+    public void setOnDismissListener(PopupWindow.OnDismissListener listener) {
+        mOnDismissListener = listener;
+    }
+
+    /**
+     * Sets the initial count of items shown in the activities popup
+     * i.e. the items before the popup is expanded. This is an upper
+     * bound since it is not guaranteed that such number of intent
+     * handlers exist.
+     *
+     * @param itemCount The initial popup item count.
+     */
+    public void setInitialActivityCount(int itemCount) {
+        mInitialActivityCount = itemCount;
+    }
+
+    /**
+     * Sets a content description of the default action button. This
+     * resource should be a string taking one formatting argument and
+     * will be used for formatting the content description of the button
+     * dynamically as the default target changes. For example, a resource
+     * pointing to the string "share with %1$s" will result in a content
+     * description "share with Bluetooth" for the Bluetooth activity.
+     *
+     * @param resourceId The resource id.
+     */
+    public void setDefaultActionButtonContentDescription(@StringRes int resourceId) {
+        mDefaultActionButtonContentDescription = resourceId;
+    }
+
+    /**
+     * Gets the list popup window which is lazily initialized.
+     *
+     * @return The popup.
+     */
+    private ListPopupWindow getListPopupWindow() {
+        if (mListPopupWindow == null) {
+            mListPopupWindow = new ListPopupWindow(getContext());
+            mListPopupWindow.setAdapter(mAdapter);
+            mListPopupWindow.setAnchorView(ActivityChooserView.this);
+            mListPopupWindow.setModal(true);
+            mListPopupWindow.setOnItemClickListener(mCallbacks);
+            mListPopupWindow.setOnDismissListener(mCallbacks);
+        }
+        return mListPopupWindow;
+    }
+
+    /**
+     * Updates the buttons state.
+     */
+    private void updateAppearance() {
+        // Expand overflow button.
+        if (mAdapter.getCount() > 0) {
+            mExpandActivityOverflowButton.setEnabled(true);
+        } else {
+            mExpandActivityOverflowButton.setEnabled(false);
+        }
+        // Default activity button.
+        final int activityCount = mAdapter.getActivityCount();
+        final int historySize = mAdapter.getHistorySize();
+        if (activityCount==1 || activityCount > 1 && historySize > 0) {
+            mDefaultActivityButton.setVisibility(VISIBLE);
+            ResolveInfo activity = mAdapter.getDefaultActivity();
+            PackageManager packageManager = mContext.getPackageManager();
+            mDefaultActivityButtonImage.setImageDrawable(activity.loadIcon(packageManager));
+            if (mDefaultActionButtonContentDescription != 0) {
+                CharSequence label = activity.loadLabel(packageManager);
+                String contentDescription = mContext.getString(
+                        mDefaultActionButtonContentDescription, label);
+                mDefaultActivityButton.setContentDescription(contentDescription);
+            }
+        } else {
+            mDefaultActivityButton.setVisibility(View.GONE);
+        }
+        // Activity chooser content.
+        if (mDefaultActivityButton.getVisibility() == VISIBLE) {
+            mActivityChooserContent.setBackground(mActivityChooserContentBackground);
+        } else {
+            mActivityChooserContent.setBackground(null);
+        }
+    }
+
+    /**
+     * Interface implementation to avoid publishing them in the APIs.
+     */
+    private class Callbacks implements AdapterView.OnItemClickListener,
+            View.OnClickListener, View.OnLongClickListener, PopupWindow.OnDismissListener {
+
+        // AdapterView#OnItemClickListener
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            ActivityChooserViewAdapter adapter = (ActivityChooserViewAdapter) parent.getAdapter();
+            final int itemViewType = adapter.getItemViewType(position);
+            switch (itemViewType) {
+                case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_FOOTER: {
+                    showPopupUnchecked(ActivityChooserViewAdapter.MAX_ACTIVITY_COUNT_UNLIMITED);
+                } break;
+                case ActivityChooserViewAdapter.ITEM_VIEW_TYPE_ACTIVITY: {
+                    dismissPopup();
+                    if (mIsSelectingDefaultActivity) {
+                        // The item at position zero is the default already.
+                        if (position > 0) {
+                            mAdapter.getDataModel().setDefaultActivity(position);
+                        }
+                    } else {
+                        // If the default target is not shown in the list, the first
+                        // item in the model is default action => adjust index
+                        position = mAdapter.getShowDefaultActivity() ? position : position + 1;
+                        Intent launchIntent = mAdapter.getDataModel().chooseActivity(position);
+                        if (launchIntent != null) {
+                            launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+                            ResolveInfo resolveInfo = mAdapter.getDataModel().getActivity(position);
+                            startActivity(launchIntent, resolveInfo);
+                        }
+                    }
+                } break;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        // View.OnClickListener
+        public void onClick(View view) {
+            if (view == mDefaultActivityButton) {
+                dismissPopup();
+                ResolveInfo defaultActivity = mAdapter.getDefaultActivity();
+                final int index = mAdapter.getDataModel().getActivityIndex(defaultActivity);
+                Intent launchIntent = mAdapter.getDataModel().chooseActivity(index);
+                if (launchIntent != null) {
+                    launchIntent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET);
+                    startActivity(launchIntent, defaultActivity);
+                }
+            } else if (view == mExpandActivityOverflowButton) {
+                mIsSelectingDefaultActivity = false;
+                showPopupUnchecked(mInitialActivityCount);
+            } else {
+                throw new IllegalArgumentException();
+            }
+        }
+
+        // OnLongClickListener#onLongClick
+        @Override
+        public boolean onLongClick(View view) {
+            if (view == mDefaultActivityButton) {
+                if (mAdapter.getCount() > 0) {
+                    mIsSelectingDefaultActivity = true;
+                    showPopupUnchecked(mInitialActivityCount);
+                }
+            } else {
+                throw new IllegalArgumentException();
+            }
+            return true;
+        }
+
+        // PopUpWindow.OnDismissListener#onDismiss
+        public void onDismiss() {
+            notifyOnDismissListener();
+            if (mProvider != null) {
+                mProvider.subUiVisibilityChanged(false);
+            }
+        }
+
+        private void notifyOnDismissListener() {
+            if (mOnDismissListener != null) {
+                mOnDismissListener.onDismiss();
+            }
+        }
+
+        private void startActivity(Intent intent, ResolveInfo resolveInfo) {
+            try {
+                mContext.startActivity(intent);
+            } catch (RuntimeException re) {
+                CharSequence appLabel = resolveInfo.loadLabel(mContext.getPackageManager());
+                String message = mContext.getString(
+                        R.string.activitychooserview_choose_application_error, appLabel);
+                Log.e(LOG_TAG, message);
+                Toast.makeText(mContext, message, Toast.LENGTH_SHORT).show();
+            }
+        }
+    }
+
+    /**
+     * Adapter for backing the list of activities shown in the popup.
+     */
+    private class ActivityChooserViewAdapter extends BaseAdapter {
+
+        public static final int MAX_ACTIVITY_COUNT_UNLIMITED = Integer.MAX_VALUE;
+
+        public static final int MAX_ACTIVITY_COUNT_DEFAULT = 4;
+
+        private static final int ITEM_VIEW_TYPE_ACTIVITY = 0;
+
+        private static final int ITEM_VIEW_TYPE_FOOTER = 1;
+
+        private static final int ITEM_VIEW_TYPE_COUNT = 3;
+
+        private ActivityChooserModel mDataModel;
+
+        private int mMaxActivityCount = MAX_ACTIVITY_COUNT_DEFAULT;
+
+        private boolean mShowDefaultActivity;
+
+        private boolean mHighlightDefaultActivity;
+
+        private boolean mShowFooterView;
+
+        public void setDataModel(ActivityChooserModel dataModel) {
+            ActivityChooserModel oldDataModel = mAdapter.getDataModel();
+            if (oldDataModel != null && isShown()) {
+                oldDataModel.unregisterObserver(mModelDataSetOberver);
+            }
+            mDataModel = dataModel;
+            if (dataModel != null && isShown()) {
+                dataModel.registerObserver(mModelDataSetOberver);
+            }
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            if (mShowFooterView && position == getCount() - 1) {
+                return ITEM_VIEW_TYPE_FOOTER;
+            } else {
+                return ITEM_VIEW_TYPE_ACTIVITY;
+            }
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return ITEM_VIEW_TYPE_COUNT;
+        }
+
+        public int getCount() {
+            int count = 0;
+            int activityCount = mDataModel.getActivityCount();
+            if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
+                activityCount--;
+            }
+            count = Math.min(activityCount, mMaxActivityCount);
+            if (mShowFooterView) {
+                count++;
+            }
+            return count;
+        }
+
+        public Object getItem(int position) {
+            final int itemViewType = getItemViewType(position);
+            switch (itemViewType) {
+                case ITEM_VIEW_TYPE_FOOTER:
+                    return null;
+                case ITEM_VIEW_TYPE_ACTIVITY:
+                    if (!mShowDefaultActivity && mDataModel.getDefaultActivity() != null) {
+                        position++;
+                    }
+                    return mDataModel.getActivity(position);
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        public long getItemId(int position) {
+            return position;
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final int itemViewType = getItemViewType(position);
+            switch (itemViewType) {
+                case ITEM_VIEW_TYPE_FOOTER:
+                    if (convertView == null || convertView.getId() != ITEM_VIEW_TYPE_FOOTER) {
+                        convertView = LayoutInflater.from(getContext()).inflate(
+                                R.layout.activity_chooser_view_list_item, parent, false);
+                        convertView.setId(ITEM_VIEW_TYPE_FOOTER);
+                        TextView titleView = convertView.findViewById(R.id.title);
+                        titleView.setText(mContext.getString(
+                                R.string.activity_chooser_view_see_all));
+                    }
+                    return convertView;
+                case ITEM_VIEW_TYPE_ACTIVITY:
+                    if (convertView == null || convertView.getId() != R.id.list_item) {
+                        convertView = LayoutInflater.from(getContext()).inflate(
+                                R.layout.activity_chooser_view_list_item, parent, false);
+                    }
+                    PackageManager packageManager = mContext.getPackageManager();
+                    // Set the icon
+                    ImageView iconView = convertView.findViewById(R.id.icon);
+                    ResolveInfo activity = (ResolveInfo) getItem(position);
+                    iconView.setImageDrawable(activity.loadIcon(packageManager));
+                    // Set the title.
+                    TextView titleView = convertView.findViewById(R.id.title);
+                    titleView.setText(activity.loadLabel(packageManager));
+                    // Highlight the default.
+                    if (mShowDefaultActivity && position == 0 && mHighlightDefaultActivity) {
+                        convertView.setActivated(true);
+                    } else {
+                        convertView.setActivated(false);
+                    }
+                    return convertView;
+                default:
+                    throw new IllegalArgumentException();
+            }
+        }
+
+        public int measureContentWidth() {
+            // The user may have specified some of the target not to be shown but we
+            // want to measure all of them since after expansion they should fit.
+            final int oldMaxActivityCount = mMaxActivityCount;
+            mMaxActivityCount = MAX_ACTIVITY_COUNT_UNLIMITED;
+
+            int contentWidth = 0;
+            View itemView = null;
+
+            final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+            final int heightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+            final int count = getCount();
+
+            for (int i = 0; i < count; i++) {
+                itemView = getView(i, itemView, null);
+                itemView.measure(widthMeasureSpec, heightMeasureSpec);
+                contentWidth = Math.max(contentWidth, itemView.getMeasuredWidth());
+            }
+
+            mMaxActivityCount = oldMaxActivityCount;
+
+            return contentWidth;
+        }
+
+        public void setMaxActivityCount(int maxActivityCount) {
+            if (mMaxActivityCount != maxActivityCount) {
+                mMaxActivityCount = maxActivityCount;
+                notifyDataSetChanged();
+            }
+        }
+
+        public ResolveInfo getDefaultActivity() {
+            return mDataModel.getDefaultActivity();
+        }
+
+        public void setShowFooterView(boolean showFooterView) {
+            if (mShowFooterView != showFooterView) {
+                mShowFooterView = showFooterView;
+                notifyDataSetChanged();
+            }
+        }
+
+        public int getActivityCount() {
+            return mDataModel.getActivityCount();
+        }
+
+        public int getHistorySize() {
+            return mDataModel.getHistorySize();
+        }
+
+        public ActivityChooserModel getDataModel() {
+            return mDataModel;
+        }
+
+        public void setShowDefaultActivity(boolean showDefaultActivity,
+                boolean highlightDefaultActivity) {
+            if (mShowDefaultActivity != showDefaultActivity
+                    || mHighlightDefaultActivity != highlightDefaultActivity) {
+                mShowDefaultActivity = showDefaultActivity;
+                mHighlightDefaultActivity = highlightDefaultActivity;
+                notifyDataSetChanged();
+            }
+        }
+
+        public boolean getShowDefaultActivity() {
+            return mShowDefaultActivity;
+        }
+    }
+}
diff --git a/android/widget/Adapter.java b/android/widget/Adapter.java
new file mode 100644
index 0000000..4e463dd
--- /dev/null
+++ b/android/widget/Adapter.java
@@ -0,0 +1,168 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.Nullable;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An Adapter object acts as a bridge between an {@link AdapterView} and the
+ * underlying data for that view. The Adapter provides access to the data items.
+ * The Adapter is also responsible for making a {@link android.view.View} for
+ * each item in the data set.
+ * 
+ * @see android.widget.ArrayAdapter
+ * @see android.widget.CursorAdapter
+ * @see android.widget.SimpleCursorAdapter
+ */
+public interface Adapter {
+    /**
+     * Register an observer that is called when changes happen to the data used by this adapter.
+     *
+     * @param observer the object that gets notified when the data set changes.
+     */
+    void registerDataSetObserver(DataSetObserver observer);
+
+    /**
+     * Unregister an observer that has previously been registered with this
+     * adapter via {@link #registerDataSetObserver}.
+     *
+     * @param observer the object to unregister.
+     */
+    void unregisterDataSetObserver(DataSetObserver observer);
+
+    /**
+     * How many items are in the data set represented by this Adapter.
+     * 
+     * @return Count of items.
+     */
+    int getCount();   
+    
+    /**
+     * Get the data item associated with the specified position in the data set.
+     * 
+     * @param position Position of the item whose data we want within the adapter's 
+     * data set.
+     * @return The data at the specified position.
+     */
+    Object getItem(int position);
+    
+    /**
+     * Get the row id associated with the specified position in the list.
+     * 
+     * @param position The position of the item within the adapter's data set whose row id we want.
+     * @return The id of the item at the specified position.
+     */
+    long getItemId(int position);
+    
+    /**
+     * Indicates whether the item ids are stable across changes to the
+     * underlying data.
+     * 
+     * @return True if the same id always refers to the same object.
+     */
+    boolean hasStableIds();
+    
+    /**
+     * Get a View that displays the data at the specified position in the data set. You can either
+     * create a View manually or inflate it from an XML layout file. When the View is inflated, the
+     * parent View (GridView, ListView...) will apply default layout parameters unless you use
+     * {@link android.view.LayoutInflater#inflate(int, android.view.ViewGroup, boolean)}
+     * to specify a root view and to prevent attachment to the root.
+     * 
+     * @param position The position of the item within the adapter's data set of the item whose view
+     *        we want.
+     * @param convertView The old view to reuse, if possible. Note: You should check that this view
+     *        is non-null and of an appropriate type before using. If it is not possible to convert
+     *        this view to display the correct data, this method can create a new view.
+     *        Heterogeneous lists can specify their number of view types, so that this View is
+     *        always of the right type (see {@link #getViewTypeCount()} and
+     *        {@link #getItemViewType(int)}).
+     * @param parent The parent that this view will eventually be attached to
+     * @return A View corresponding to the data at the specified position.
+     */
+    View getView(int position, View convertView, ViewGroup parent);
+
+    /**
+     * An item view type that causes the {@link AdapterView} to ignore the item
+     * view. For example, this can be used if the client does not want a
+     * particular view to be given for conversion in
+     * {@link #getView(int, View, ViewGroup)}.
+     * 
+     * @see #getItemViewType(int)
+     * @see #getViewTypeCount()
+     */
+    static final int IGNORE_ITEM_VIEW_TYPE = AdapterView.ITEM_VIEW_TYPE_IGNORE;
+    
+    /**
+     * Get the type of View that will be created by {@link #getView} for the specified item.
+     * 
+     * @param position The position of the item within the adapter's data set whose view type we
+     *        want.
+     * @return An integer representing the type of View. Two views should share the same type if one
+     *         can be converted to the other in {@link #getView}. Note: Integers must be in the
+     *         range 0 to {@link #getViewTypeCount} - 1. {@link #IGNORE_ITEM_VIEW_TYPE} can
+     *         also be returned.
+     * @see #IGNORE_ITEM_VIEW_TYPE
+     */
+    int getItemViewType(int position);
+    
+    /**
+     * <p>
+     * Returns the number of types of Views that will be created by
+     * {@link #getView}. Each type represents a set of views that can be
+     * converted in {@link #getView}. If the adapter always returns the same
+     * type of View for all items, this method should return 1.
+     * </p>
+     * <p>
+     * This method will only be called when the adapter is set on the {@link AdapterView}.
+     * </p>
+     * 
+     * @return The number of types of Views that will be created by this adapter
+     */
+    int getViewTypeCount();
+    
+    static final int NO_SELECTION = Integer.MIN_VALUE;
+ 
+     /**
+      * @return true if this adapter doesn't contain any data.  This is used to determine
+      * whether the empty view should be displayed.  A typical implementation will return
+      * getCount() == 0 but since getCount() includes the headers and footers, specialized
+      * adapters might want a different behavior.
+      */
+     boolean isEmpty();
+
+    /**
+     * Gets a string representation of the adapter data that can help
+     * {@link android.service.autofill.AutofillService} autofill the view backed by the adapter.
+     *
+     * <p>
+     * It should only be set (i.e., non-{@code null} if the values do not represent PII
+     * (Personally Identifiable Information - sensitive data such as email addresses,
+     * credit card numbers, passwords, etc...). For
+     * example, it's ok to return a list of month names, but not a list of usernames. A good rule of
+     * thumb is that if the adapter data comes from static resources, such data is not PII - see
+     * {@link android.view.ViewStructure#setDataIsSensitive(boolean)} for more info.
+     *
+     * @return {@code null} by default, unless implementations override it.
+     */
+    default @Nullable CharSequence[] getAutofillOptions() {
+        return null;
+    }
+}
diff --git a/android/widget/AdapterView.java b/android/widget/AdapterView.java
new file mode 100644
index 0000000..dd01251
--- /dev/null
+++ b/android/widget/AdapterView.java
@@ -0,0 +1,1307 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.database.DataSetObserver;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillManager;
+
+/**
+ * An AdapterView is a view whose children are determined by an {@link Adapter}.
+ *
+ * <p>
+ * See {@link ListView}, {@link GridView}, {@link Spinner} and
+ *      {@link Gallery} for commonly used subclasses of AdapterView.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For more information about using AdapterView, read the
+ * <a href="{@docRoot}guide/topics/ui/binding.html">Binding to Data with AdapterView</a>
+ * developer guide.</p></div>
+ */
+public abstract class AdapterView<T extends Adapter> extends ViewGroup {
+
+    /**
+     * The item view type returned by {@link Adapter#getItemViewType(int)} when
+     * the adapter does not want the item's view recycled.
+     */
+    public static final int ITEM_VIEW_TYPE_IGNORE = -1;
+
+    /**
+     * The item view type returned by {@link Adapter#getItemViewType(int)} when
+     * the item is a header or footer.
+     */
+    public static final int ITEM_VIEW_TYPE_HEADER_OR_FOOTER = -2;
+
+    /**
+     * The position of the first child displayed
+     */
+    @ViewDebug.ExportedProperty(category = "scrolling")
+    int mFirstPosition = 0;
+
+    /**
+     * The offset in pixels from the top of the AdapterView to the top
+     * of the view to select during the next layout.
+     */
+    int mSpecificTop;
+
+    /**
+     * Position from which to start looking for mSyncRowId
+     */
+    int mSyncPosition;
+
+    /**
+     * Row id to look for when data has changed
+     */
+    long mSyncRowId = INVALID_ROW_ID;
+
+    /**
+     * Height of the view when mSyncPosition and mSyncRowId where set
+     */
+    long mSyncHeight;
+
+    /**
+     * True if we need to sync to mSyncRowId
+     */
+    boolean mNeedSync = false;
+
+    /**
+     * Indicates whether to sync based on the selection or position. Possible
+     * values are {@link #SYNC_SELECTED_POSITION} or
+     * {@link #SYNC_FIRST_POSITION}.
+     */
+    int mSyncMode;
+
+    /**
+     * Our height after the last layout
+     */
+    private int mLayoutHeight;
+
+    /**
+     * Sync based on the selected child
+     */
+    static final int SYNC_SELECTED_POSITION = 0;
+
+    /**
+     * Sync based on the first child displayed
+     */
+    static final int SYNC_FIRST_POSITION = 1;
+
+    /**
+     * Maximum amount of time to spend in {@link #findSyncPosition()}
+     */
+    static final int SYNC_MAX_DURATION_MILLIS = 100;
+
+    /**
+     * Indicates that this view is currently being laid out.
+     */
+    boolean mInLayout = false;
+
+    /**
+     * The listener that receives notifications when an item is selected.
+     */
+    OnItemSelectedListener mOnItemSelectedListener;
+
+    /**
+     * The listener that receives notifications when an item is clicked.
+     */
+    OnItemClickListener mOnItemClickListener;
+
+    /**
+     * The listener that receives notifications when an item is long clicked.
+     */
+    OnItemLongClickListener mOnItemLongClickListener;
+
+    /**
+     * True if the data has changed since the last layout
+     */
+    boolean mDataChanged;
+
+    /**
+     * The position within the adapter's data set of the item to select
+     * during the next layout.
+     */
+    @ViewDebug.ExportedProperty(category = "list")
+    int mNextSelectedPosition = INVALID_POSITION;
+
+    /**
+     * The item id of the item to select during the next layout.
+     */
+    long mNextSelectedRowId = INVALID_ROW_ID;
+
+    /**
+     * The position within the adapter's data set of the currently selected item.
+     */
+    @ViewDebug.ExportedProperty(category = "list")
+    int mSelectedPosition = INVALID_POSITION;
+
+    /**
+     * The item id of the currently selected item.
+     */
+    long mSelectedRowId = INVALID_ROW_ID;
+
+    /**
+     * View to show if there are no items to show.
+     */
+    private View mEmptyView;
+
+    /**
+     * The number of items in the current adapter.
+     */
+    @ViewDebug.ExportedProperty(category = "list")
+    int mItemCount;
+
+    /**
+     * The number of items in the adapter before a data changed event occurred.
+     */
+    int mOldItemCount;
+
+    /**
+     * Represents an invalid position. All valid positions are in the range 0 to 1 less than the
+     * number of items in the current adapter.
+     */
+    public static final int INVALID_POSITION = -1;
+
+    /**
+     * Represents an empty or invalid row id
+     */
+    public static final long INVALID_ROW_ID = Long.MIN_VALUE;
+
+    /**
+     * The last selected position we used when notifying
+     */
+    int mOldSelectedPosition = INVALID_POSITION;
+    
+    /**
+     * The id of the last selected position we used when notifying
+     */
+    long mOldSelectedRowId = INVALID_ROW_ID;
+
+    /**
+     * Indicates what focusable state is requested when calling setFocusable().
+     * In addition to this, this view has other criteria for actually
+     * determining the focusable state (such as whether its empty or the text
+     * filter is shown).
+     *
+     * @see #setFocusable(boolean)
+     * @see #checkFocus()
+     */
+    private int mDesiredFocusableState = FOCUSABLE_AUTO;
+    private boolean mDesiredFocusableInTouchModeState;
+
+    /** Lazily-constructed runnable for dispatching selection events. */
+    private SelectionNotifier mSelectionNotifier;
+
+    /** Selection notifier that's waiting for the next layout pass. */
+    private SelectionNotifier mPendingSelectionNotifier;
+
+    /**
+     * When set to true, calls to requestLayout() will not propagate up the parent hierarchy.
+     * This is used to layout the children during a layout pass.
+     */
+    boolean mBlockLayoutRequests = false;
+
+    public AdapterView(Context context) {
+        this(context, null);
+    }
+
+    public AdapterView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AdapterView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AdapterView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // If not explicitly specified this view is important for accessibility.
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        mDesiredFocusableState = getFocusable();
+        if (mDesiredFocusableState == FOCUSABLE_AUTO) {
+            // Starts off without an adapter, so NOT_FOCUSABLE by default.
+            super.setFocusable(NOT_FOCUSABLE);
+        }
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when an item in this
+     * AdapterView has been clicked.
+     */
+    public interface OnItemClickListener {
+
+        /**
+         * Callback method to be invoked when an item in this AdapterView has
+         * been clicked.
+         * <p>
+         * Implementers can call getItemAtPosition(position) if they need
+         * to access the data associated with the selected item.
+         *
+         * @param parent The AdapterView where the click happened.
+         * @param view The view within the AdapterView that was clicked (this
+         *            will be a view provided by the adapter)
+         * @param position The position of the view in the adapter.
+         * @param id The row id of the item that was clicked.
+         */
+        void onItemClick(AdapterView<?> parent, View view, int position, long id);
+    }
+
+    /**
+     * Register a callback to be invoked when an item in this AdapterView has
+     * been clicked.
+     *
+     * @param listener The callback that will be invoked.
+     */
+    public void setOnItemClickListener(@Nullable OnItemClickListener listener) {
+        mOnItemClickListener = listener;
+    }
+
+    /**
+     * @return The callback to be invoked with an item in this AdapterView has
+     *         been clicked, or null id no callback has been set.
+     */
+    @Nullable
+    public final OnItemClickListener getOnItemClickListener() {
+        return mOnItemClickListener;
+    }
+
+    /**
+     * Call the OnItemClickListener, if it is defined. Performs all normal
+     * actions associated with clicking: reporting accessibility event, playing
+     * a sound, etc.
+     *
+     * @param view The view within the AdapterView that was clicked.
+     * @param position The position of the view in the adapter.
+     * @param id The row id of the item that was clicked.
+     * @return True if there was an assigned OnItemClickListener that was
+     *         called, false otherwise is returned.
+     */
+    public boolean performItemClick(View view, int position, long id) {
+        final boolean result;
+        if (mOnItemClickListener != null) {
+            playSoundEffect(SoundEffectConstants.CLICK);
+            mOnItemClickListener.onItemClick(this, view, position, id);
+            result = true;
+        } else {
+            result = false;
+        }
+
+        if (view != null) {
+            view.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_CLICKED);
+        }
+        return result;
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when an item in this
+     * view has been clicked and held.
+     */
+    public interface OnItemLongClickListener {
+        /**
+         * Callback method to be invoked when an item in this view has been
+         * clicked and held.
+         *
+         * Implementers can call getItemAtPosition(position) if they need to access
+         * the data associated with the selected item.
+         *
+         * @param parent The AbsListView where the click happened
+         * @param view The view within the AbsListView that was clicked
+         * @param position The position of the view in the list
+         * @param id The row id of the item that was clicked
+         *
+         * @return true if the callback consumed the long click, false otherwise
+         */
+        boolean onItemLongClick(AdapterView<?> parent, View view, int position, long id);
+    }
+
+
+    /**
+     * Register a callback to be invoked when an item in this AdapterView has
+     * been clicked and held
+     *
+     * @param listener The callback that will run
+     */
+    public void setOnItemLongClickListener(OnItemLongClickListener listener) {
+        if (!isLongClickable()) {
+            setLongClickable(true);
+        }
+        mOnItemLongClickListener = listener;
+    }
+
+    /**
+     * @return The callback to be invoked with an item in this AdapterView has
+     *         been clicked and held, or null id no callback as been set.
+     */
+    public final OnItemLongClickListener getOnItemLongClickListener() {
+        return mOnItemLongClickListener;
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when
+     * an item in this view has been selected.
+     */
+    public interface OnItemSelectedListener {
+        /**
+         * <p>Callback method to be invoked when an item in this view has been
+         * selected. This callback is invoked only when the newly selected
+         * position is different from the previously selected position or if
+         * there was no selected item.</p>
+         *
+         * Impelmenters can call getItemAtPosition(position) if they need to access the
+         * data associated with the selected item.
+         *
+         * @param parent The AdapterView where the selection happened
+         * @param view The view within the AdapterView that was clicked
+         * @param position The position of the view in the adapter
+         * @param id The row id of the item that is selected
+         */
+        void onItemSelected(AdapterView<?> parent, View view, int position, long id);
+
+        /**
+         * Callback method to be invoked when the selection disappears from this
+         * view. The selection can disappear for instance when touch is activated
+         * or when the adapter becomes empty.
+         *
+         * @param parent The AdapterView that now contains no selected item.
+         */
+        void onNothingSelected(AdapterView<?> parent);
+    }
+
+
+    /**
+     * Register a callback to be invoked when an item in this AdapterView has
+     * been selected.
+     *
+     * @param listener The callback that will run
+     */
+    public void setOnItemSelectedListener(@Nullable OnItemSelectedListener listener) {
+        mOnItemSelectedListener = listener;
+    }
+
+    @Nullable
+    public final OnItemSelectedListener getOnItemSelectedListener() {
+        return mOnItemSelectedListener;
+    }
+
+    /**
+     * Extra menu information provided to the
+     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
+     * callback when a context menu is brought up for this AdapterView.
+     *
+     */
+    public static class AdapterContextMenuInfo implements ContextMenu.ContextMenuInfo {
+
+        public AdapterContextMenuInfo(View targetView, int position, long id) {
+            this.targetView = targetView;
+            this.position = position;
+            this.id = id;
+        }
+
+        /**
+         * The child view for which the context menu is being displayed. This
+         * will be one of the children of this AdapterView.
+         */
+        public View targetView;
+
+        /**
+         * The position in the adapter for which the context menu is being
+         * displayed.
+         */
+        public int position;
+
+        /**
+         * The row id of the item for which the context menu is being displayed.
+         */
+        public long id;
+    }
+
+    /**
+     * Returns the adapter currently associated with this widget.
+     *
+     * @return The adapter used to provide this view's content.
+     */
+    public abstract T getAdapter();
+
+    /**
+     * Sets the adapter that provides the data and the views to represent the data
+     * in this widget.
+     *
+     * @param adapter The adapter to use to create this view's content.
+     */
+    public abstract void setAdapter(T adapter);
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param child Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void addView(View child) {
+        throw new UnsupportedOperationException("addView(View) is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param child Ignored.
+     * @param index Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void addView(View child, int index) {
+        throw new UnsupportedOperationException("addView(View, int) is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param child Ignored.
+     * @param params Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void addView(View child, LayoutParams params) {
+        throw new UnsupportedOperationException("addView(View, LayoutParams) "
+                + "is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param child Ignored.
+     * @param index Ignored.
+     * @param params Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void addView(View child, int index, LayoutParams params) {
+        throw new UnsupportedOperationException("addView(View, int, LayoutParams) "
+                + "is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param child Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void removeView(View child) {
+        throw new UnsupportedOperationException("removeView(View) is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @param index Ignored.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void removeViewAt(int index) {
+        throw new UnsupportedOperationException("removeViewAt(int) is not supported in AdapterView");
+    }
+
+    /**
+     * This method is not supported and throws an UnsupportedOperationException when called.
+     *
+     * @throws UnsupportedOperationException Every time this method is invoked.
+     */
+    @Override
+    public void removeAllViews() {
+        throw new UnsupportedOperationException("removeAllViews() is not supported in AdapterView");
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        mLayoutHeight = getHeight();
+    }
+
+    /**
+     * Return the position of the currently selected item within the adapter's data set
+     *
+     * @return int Position (starting at 0), or {@link #INVALID_POSITION} if there is nothing selected.
+     */
+    @ViewDebug.CapturedViewProperty
+    public int getSelectedItemPosition() {
+        return mNextSelectedPosition;
+    }
+
+    /**
+     * @return The id corresponding to the currently selected item, or {@link #INVALID_ROW_ID}
+     * if nothing is selected.
+     */
+    @ViewDebug.CapturedViewProperty
+    public long getSelectedItemId() {
+        return mNextSelectedRowId;
+    }
+
+    /**
+     * @return The view corresponding to the currently selected item, or null
+     * if nothing is selected
+     */
+    public abstract View getSelectedView();
+
+    /**
+     * @return The data corresponding to the currently selected item, or
+     * null if there is nothing selected.
+     */
+    public Object getSelectedItem() {
+        T adapter = getAdapter();
+        int selection = getSelectedItemPosition();
+        if (adapter != null && adapter.getCount() > 0 && selection >= 0) {
+            return adapter.getItem(selection);
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @return The number of items owned by the Adapter associated with this
+     *         AdapterView. (This is the number of data items, which may be
+     *         larger than the number of visible views.)
+     */
+    @ViewDebug.CapturedViewProperty
+    public int getCount() {
+        return mItemCount;
+    }
+
+    /**
+     * Returns the position within the adapter's data set for the view, where
+     * view is a an adapter item or a descendant of an adapter item.
+     * <p>
+     * <strong>Note:</strong> The result of this method only reflects the
+     * position of the data bound to <var>view</var> during the most recent
+     * layout pass. If the adapter's data set has changed without a subsequent
+     * layout pass, the position returned by this method may not match the
+     * current position of the data within the adapter.
+     *
+     * @param view an adapter item, or a descendant of an adapter item. This
+     *             must be visible in this AdapterView at the time of the call.
+     * @return the position within the adapter's data set of the view, or
+     *         {@link #INVALID_POSITION} if the view does not correspond to a
+     *         list item (or it is not currently visible)
+     */
+    public int getPositionForView(View view) {
+        View listItem = view;
+        try {
+            View v;
+            while ((v = (View) listItem.getParent()) != null && !v.equals(this)) {
+                listItem = v;
+            }
+        } catch (ClassCastException e) {
+            // We made it up to the window without find this list view
+            return INVALID_POSITION;
+        }
+
+        if (listItem != null) {
+            // Search the children for the list item
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                if (getChildAt(i).equals(listItem)) {
+                    return mFirstPosition + i;
+                }
+            }
+        }
+
+        // Child not found!
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Returns the position within the adapter's data set for the first item
+     * displayed on screen.
+     *
+     * @return The position within the adapter's data set
+     */
+    public int getFirstVisiblePosition() {
+        return mFirstPosition;
+    }
+
+    /**
+     * Returns the position within the adapter's data set for the last item
+     * displayed on screen.
+     *
+     * @return The position within the adapter's data set
+     */
+    public int getLastVisiblePosition() {
+        return mFirstPosition + getChildCount() - 1;
+    }
+
+    /**
+     * Sets the currently selected item. To support accessibility subclasses that
+     * override this method must invoke the overriden super method first.
+     *
+     * @param position Index (starting at 0) of the data item to be selected.
+     */
+    public abstract void setSelection(int position);
+
+    /**
+     * Sets the view to show if the adapter is empty
+     */
+    @android.view.RemotableViewMethod
+    public void setEmptyView(View emptyView) {
+        mEmptyView = emptyView;
+
+        // If not explicitly specified this view is important for accessibility.
+        if (emptyView != null
+                && emptyView.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            emptyView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        final T adapter = getAdapter();
+        final boolean empty = ((adapter == null) || adapter.isEmpty());
+        updateEmptyStatus(empty);
+    }
+
+    /**
+     * When the current adapter is empty, the AdapterView can display a special view
+     * called the empty view. The empty view is used to provide feedback to the user
+     * that no data is available in this AdapterView.
+     *
+     * @return The view to show if the adapter is empty.
+     */
+    public View getEmptyView() {
+        return mEmptyView;
+    }
+
+    /**
+     * Indicates whether this view is in filter mode. Filter mode can for instance
+     * be enabled by a user when typing on the keyboard.
+     *
+     * @return True if the view is in filter mode, false otherwise.
+     */
+    boolean isInFilterMode() {
+        return false;
+    }
+
+    @Override
+    public void setFocusable(@Focusable int focusable) {
+        final T adapter = getAdapter();
+        final boolean empty = adapter == null || adapter.getCount() == 0;
+
+        mDesiredFocusableState = focusable;
+        if ((focusable & (FOCUSABLE_AUTO | FOCUSABLE)) == 0) {
+            mDesiredFocusableInTouchModeState = false;
+        }
+
+        super.setFocusable((!empty || isInFilterMode()) ? focusable : NOT_FOCUSABLE);
+    }
+
+    @Override
+    public void setFocusableInTouchMode(boolean focusable) {
+        final T adapter = getAdapter();
+        final boolean empty = adapter == null || adapter.getCount() == 0;
+
+        mDesiredFocusableInTouchModeState = focusable;
+        if (focusable) {
+            mDesiredFocusableState = FOCUSABLE;
+        }
+
+        super.setFocusableInTouchMode(focusable && (!empty || isInFilterMode()));
+    }
+
+    void checkFocus() {
+        final T adapter = getAdapter();
+        final boolean empty = adapter == null || adapter.getCount() == 0;
+        final boolean focusable = !empty || isInFilterMode();
+        // The order in which we set focusable in touch mode/focusable may matter
+        // for the client, see View.setFocusableInTouchMode() comments for more
+        // details
+        super.setFocusableInTouchMode(focusable && mDesiredFocusableInTouchModeState);
+        super.setFocusable(focusable ? mDesiredFocusableState : NOT_FOCUSABLE);
+        if (mEmptyView != null) {
+            updateEmptyStatus((adapter == null) || adapter.isEmpty());
+        }
+    }
+
+    /**
+     * Update the status of the list based on the empty parameter.  If empty is true and
+     * we have an empty view, display it.  In all the other cases, make sure that the listview
+     * is VISIBLE and that the empty view is GONE (if it's not null).
+     */
+    private void updateEmptyStatus(boolean empty) {
+        if (isInFilterMode()) {
+            empty = false;
+        }
+
+        if (empty) {
+            if (mEmptyView != null) {
+                mEmptyView.setVisibility(View.VISIBLE);
+                setVisibility(View.GONE);
+            } else {
+                // If the caller just removed our empty view, make sure the list view is visible
+                setVisibility(View.VISIBLE);
+            }
+
+            // We are now GONE, so pending layouts will not be dispatched.
+            // Force one here to make sure that the state of the list matches
+            // the state of the adapter.
+            if (mDataChanged) {           
+                this.onLayout(false, mLeft, mTop, mRight, mBottom); 
+            }
+        } else {
+            if (mEmptyView != null) mEmptyView.setVisibility(View.GONE);
+            setVisibility(View.VISIBLE);
+        }
+    }
+
+    /**
+     * Gets the data associated with the specified position in the list.
+     *
+     * @param position Which data to get
+     * @return The data associated with the specified position in the list
+     */
+    public Object getItemAtPosition(int position) {
+        T adapter = getAdapter();
+        return (adapter == null || position < 0) ? null : adapter.getItem(position);
+    }
+
+    public long getItemIdAtPosition(int position) {
+        T adapter = getAdapter();
+        return (adapter == null || position < 0) ? INVALID_ROW_ID : adapter.getItemId(position);
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener l) {
+        throw new RuntimeException("Don't call setOnClickListener for an AdapterView. "
+                + "You probably want setOnItemClickListener instead");
+    }
+
+    /**
+     * Override to prevent freezing of any views created by the adapter.
+     */
+    @Override
+    protected void dispatchSaveInstanceState(SparseArray<Parcelable> container) {
+        dispatchFreezeSelfOnly(container);
+    }
+
+    /**
+     * Override to prevent thawing of any views created by the adapter.
+     */
+    @Override
+    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+        dispatchThawSelfOnly(container);
+    }
+
+    class AdapterDataSetObserver extends DataSetObserver {
+
+        private Parcelable mInstanceState = null;
+
+        @Override
+        public void onChanged() {
+            mDataChanged = true;
+            mOldItemCount = mItemCount;
+            mItemCount = getAdapter().getCount();
+
+            // Detect the case where a cursor that was previously invalidated has
+            // been repopulated with new data.
+            if (AdapterView.this.getAdapter().hasStableIds() && mInstanceState != null
+                    && mOldItemCount == 0 && mItemCount > 0) {
+                AdapterView.this.onRestoreInstanceState(mInstanceState);
+                mInstanceState = null;
+            } else {
+                rememberSyncState();
+            }
+            checkFocus();
+            requestLayout();
+        }
+
+        @Override
+        public void onInvalidated() {
+            mDataChanged = true;
+
+            if (AdapterView.this.getAdapter().hasStableIds()) {
+                // Remember the current state for the case where our hosting activity is being
+                // stopped and later restarted
+                mInstanceState = AdapterView.this.onSaveInstanceState();
+            }
+
+            // Data is invalid so we should reset our state
+            mOldItemCount = mItemCount;
+            mItemCount = 0;
+            mSelectedPosition = INVALID_POSITION;
+            mSelectedRowId = INVALID_ROW_ID;
+            mNextSelectedPosition = INVALID_POSITION;
+            mNextSelectedRowId = INVALID_ROW_ID;
+            mNeedSync = false;
+
+            checkFocus();
+            requestLayout();
+        }
+
+        public void clearSavedState() {
+            mInstanceState = null;
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        removeCallbacks(mSelectionNotifier);
+    }
+
+    private class SelectionNotifier implements Runnable {
+        public void run() {
+            mPendingSelectionNotifier = null;
+
+            if (mDataChanged && getViewRootImpl() != null
+                    && getViewRootImpl().isLayoutRequested()) {
+                // Data has changed between when this SelectionNotifier was
+                // posted and now. Postpone the notification until the next
+                // layout is complete and we run checkSelectionChanged().
+                if (getAdapter() != null) {
+                    mPendingSelectionNotifier = this;
+                }
+            } else {
+                dispatchOnItemSelected();
+            }
+        }
+    }
+
+    void selectionChanged() {
+        // We're about to post or run the selection notifier, so we don't need
+        // a pending notifier.
+        mPendingSelectionNotifier = null;
+
+        if (mOnItemSelectedListener != null
+                || AccessibilityManager.getInstance(mContext).isEnabled()) {
+            if (mInLayout || mBlockLayoutRequests) {
+                // If we are in a layout traversal, defer notification
+                // by posting. This ensures that the view tree is
+                // in a consistent state and is able to accommodate
+                // new layout or invalidate requests.
+                if (mSelectionNotifier == null) {
+                    mSelectionNotifier = new SelectionNotifier();
+                } else {
+                    removeCallbacks(mSelectionNotifier);
+                }
+                post(mSelectionNotifier);
+            } else {
+                dispatchOnItemSelected();
+            }
+        }
+        // Always notify AutoFillManager - it will return right away if autofill is disabled.
+        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+        if (afm != null) {
+            afm.notifyValueChanged(this);
+        }
+    }
+
+    private void dispatchOnItemSelected() {
+        fireOnSelected();
+        performAccessibilityActionsOnSelected();
+    }
+
+    private void fireOnSelected() {
+        if (mOnItemSelectedListener == null) {
+            return;
+        }
+        final int selection = getSelectedItemPosition();
+        if (selection >= 0) {
+            View v = getSelectedView();
+            mOnItemSelectedListener.onItemSelected(this, v, selection,
+                    getAdapter().getItemId(selection));
+        } else {
+            mOnItemSelectedListener.onNothingSelected(this);
+        }
+    }
+
+    private void performAccessibilityActionsOnSelected() {
+        if (!AccessibilityManager.getInstance(mContext).isEnabled()) {
+            return;
+        }
+        final int position = getSelectedItemPosition();
+        if (position >= 0) {
+            // we fire selection events here not in View
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        View selectedView = getSelectedView();
+        if (selectedView != null && selectedView.getVisibility() == VISIBLE
+                && selectedView.dispatchPopulateAccessibilityEvent(event)) {
+            return true;
+        }
+        return false;
+    }
+
+    /** @hide */
+    @Override
+    public boolean onRequestSendAccessibilityEventInternal(View child, AccessibilityEvent event) {
+        if (super.onRequestSendAccessibilityEventInternal(child, event)) {
+            // Add a record for ourselves as well.
+            AccessibilityEvent record = AccessibilityEvent.obtain();
+            onInitializeAccessibilityEvent(record);
+            // Populate with the text of the requesting child.
+            child.dispatchPopulateAccessibilityEvent(record);
+            event.appendRecord(record);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AdapterView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setScrollable(isScrollableForAccessibility());
+        View selectedView = getSelectedView();
+        if (selectedView != null) {
+            info.setEnabled(selectedView.isEnabled());
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setScrollable(isScrollableForAccessibility());
+        View selectedView = getSelectedView();
+        if (selectedView != null) {
+            event.setEnabled(selectedView.isEnabled());
+        }
+        event.setCurrentItemIndex(getSelectedItemPosition());
+        event.setFromIndex(getFirstVisiblePosition());
+        event.setToIndex(getLastVisiblePosition());
+        event.setItemCount(getCount());
+    }
+
+    private boolean isScrollableForAccessibility() {
+        T adapter = getAdapter();
+        if (adapter != null) {
+            final int itemCount = adapter.getCount();
+            return itemCount > 0
+                && (getFirstVisiblePosition() > 0 || getLastVisiblePosition() < itemCount - 1);
+        }
+        return false;
+    }
+
+    @Override
+    protected boolean canAnimate() {
+        return super.canAnimate() && mItemCount > 0;
+    }
+
+    void handleDataChanged() {
+        final int count = mItemCount;
+        boolean found = false;
+
+        if (count > 0) {
+
+            int newPos;
+
+            // Find the row we are supposed to sync to
+            if (mNeedSync) {
+                // Update this first, since setNextSelectedPositionInt inspects
+                // it
+                mNeedSync = false;
+
+                // See if we can find a position in the new data with the same
+                // id as the old selection
+                newPos = findSyncPosition();
+                if (newPos >= 0) {
+                    // Verify that new selection is selectable
+                    int selectablePos = lookForSelectablePosition(newPos, true);
+                    if (selectablePos == newPos) {
+                        // Same row id is selected
+                        setNextSelectedPositionInt(newPos);
+                        found = true;
+                    }
+                }
+            }
+            if (!found) {
+                // Try to use the same position if we can't find matching data
+                newPos = getSelectedItemPosition();
+
+                // Pin position to the available range
+                if (newPos >= count) {
+                    newPos = count - 1;
+                }
+                if (newPos < 0) {
+                    newPos = 0;
+                }
+
+                // Make sure we select something selectable -- first look down
+                int selectablePos = lookForSelectablePosition(newPos, true);
+                if (selectablePos < 0) {
+                    // Looking down didn't work -- try looking up
+                    selectablePos = lookForSelectablePosition(newPos, false);
+                }
+                if (selectablePos >= 0) {
+                    setNextSelectedPositionInt(selectablePos);
+                    checkSelectionChanged();
+                    found = true;
+                }
+            }
+        }
+        if (!found) {
+            // Nothing is selected
+            mSelectedPosition = INVALID_POSITION;
+            mSelectedRowId = INVALID_ROW_ID;
+            mNextSelectedPosition = INVALID_POSITION;
+            mNextSelectedRowId = INVALID_ROW_ID;
+            mNeedSync = false;
+            checkSelectionChanged();
+        }
+
+        notifySubtreeAccessibilityStateChangedIfNeeded();
+    }
+
+    /**
+     * Called after layout to determine whether the selection position needs to
+     * be updated. Also used to fire any pending selection events.
+     */
+    void checkSelectionChanged() {
+        if ((mSelectedPosition != mOldSelectedPosition) || (mSelectedRowId != mOldSelectedRowId)) {
+            selectionChanged();
+            mOldSelectedPosition = mSelectedPosition;
+            mOldSelectedRowId = mSelectedRowId;
+        }
+
+        // If we have a pending selection notification -- and we won't if we
+        // just fired one in selectionChanged() -- run it now.
+        if (mPendingSelectionNotifier != null) {
+            mPendingSelectionNotifier.run();
+        }
+    }
+
+    /**
+     * Searches the adapter for a position matching mSyncRowId. The search starts at mSyncPosition
+     * and then alternates between moving up and moving down until 1) we find the right position, or
+     * 2) we run out of time, or 3) we have looked at every position
+     *
+     * @return Position of the row that matches mSyncRowId, or {@link #INVALID_POSITION} if it can't
+     *         be found
+     */
+    int findSyncPosition() {
+        int count = mItemCount;
+
+        if (count == 0) {
+            return INVALID_POSITION;
+        }
+
+        long idToMatch = mSyncRowId;
+        int seed = mSyncPosition;
+
+        // If there isn't a selection don't hunt for it
+        if (idToMatch == INVALID_ROW_ID) {
+            return INVALID_POSITION;
+        }
+
+        // Pin seed to reasonable values
+        seed = Math.max(0, seed);
+        seed = Math.min(count - 1, seed);
+
+        long endTime = SystemClock.uptimeMillis() + SYNC_MAX_DURATION_MILLIS;
+
+        long rowId;
+
+        // first position scanned so far
+        int first = seed;
+
+        // last position scanned so far
+        int last = seed;
+
+        // True if we should move down on the next iteration
+        boolean next = false;
+
+        // True when we have looked at the first item in the data
+        boolean hitFirst;
+
+        // True when we have looked at the last item in the data
+        boolean hitLast;
+
+        // Get the item ID locally (instead of getItemIdAtPosition), so
+        // we need the adapter
+        T adapter = getAdapter();
+        if (adapter == null) {
+            return INVALID_POSITION;
+        }
+
+        while (SystemClock.uptimeMillis() <= endTime) {
+            rowId = adapter.getItemId(seed);
+            if (rowId == idToMatch) {
+                // Found it!
+                return seed;
+            }
+
+            hitLast = last == count - 1;
+            hitFirst = first == 0;
+
+            if (hitLast && hitFirst) {
+                // Looked at everything
+                break;
+            }
+
+            if (hitFirst || (next && !hitLast)) {
+                // Either we hit the top, or we are trying to move down
+                last++;
+                seed = last;
+                // Try going up next time
+                next = false;
+            } else if (hitLast || (!next && !hitFirst)) {
+                // Either we hit the bottom, or we are trying to move up
+                first--;
+                seed = first;
+                // Try going down next time
+                next = true;
+            }
+
+        }
+
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Find a position that can be selected (i.e., is not a separator).
+     *
+     * @param position The starting position to look at.
+     * @param lookDown Whether to look down for other positions.
+     * @return The next selectable position starting at position and then searching either up or
+     *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
+     */
+    int lookForSelectablePosition(int position, boolean lookDown) {
+        return position;
+    }
+
+    /**
+     * Utility to keep mSelectedPosition and mSelectedRowId in sync
+     * @param position Our current position
+     */
+    void setSelectedPositionInt(int position) {
+        mSelectedPosition = position;
+        mSelectedRowId = getItemIdAtPosition(position);
+    }
+
+    /**
+     * Utility to keep mNextSelectedPosition and mNextSelectedRowId in sync
+     * @param position Intended value for mSelectedPosition the next time we go
+     * through layout
+     */
+    void setNextSelectedPositionInt(int position) {
+        mNextSelectedPosition = position;
+        mNextSelectedRowId = getItemIdAtPosition(position);
+        // If we are trying to sync to the selection, update that too
+        if (mNeedSync && mSyncMode == SYNC_SELECTED_POSITION && position >= 0) {
+            mSyncPosition = position;
+            mSyncRowId = mNextSelectedRowId;
+        }
+    }
+
+    /**
+     * Remember enough information to restore the screen state when the data has
+     * changed.
+     *
+     */
+    void rememberSyncState() {
+        if (getChildCount() > 0) {
+            mNeedSync = true;
+            mSyncHeight = mLayoutHeight;
+            if (mSelectedPosition >= 0) {
+                // Sync the selection state
+                View v = getChildAt(mSelectedPosition - mFirstPosition);
+                mSyncRowId = mNextSelectedRowId;
+                mSyncPosition = mNextSelectedPosition;
+                if (v != null) {
+                    mSpecificTop = v.getTop();
+                }
+                mSyncMode = SYNC_SELECTED_POSITION;
+            } else {
+                // Sync the based on the offset of the first view
+                View v = getChildAt(0);
+                T adapter = getAdapter();
+                if (mFirstPosition >= 0 && mFirstPosition < adapter.getCount()) {
+                    mSyncRowId = adapter.getItemId(mFirstPosition);
+                } else {
+                    mSyncRowId = NO_ID;
+                }
+                mSyncPosition = mFirstPosition;
+                if (v != null) {
+                    mSpecificTop = v.getTop();
+                }
+                mSyncMode = SYNC_FIRST_POSITION;
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+
+        encoder.addProperty("scrolling:firstPosition", mFirstPosition);
+        encoder.addProperty("list:nextSelectedPosition", mNextSelectedPosition);
+        encoder.addProperty("list:nextSelectedRowId", mNextSelectedRowId);
+        encoder.addProperty("list:selectedPosition", mSelectedPosition);
+        encoder.addProperty("list:itemCount", mItemCount);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>It also sets the autofill options in the structure; when overridden, it should set it as
+     * well, either explicitly by calling {@link ViewStructure#setAutofillOptions(CharSequence[])}
+     * or implicitly by calling {@code super.onProvideAutofillStructure(structure, flags)}.
+     */
+    @Override
+    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+        super.onProvideAutofillStructure(structure, flags);
+
+        final Adapter adapter = getAdapter();
+        if (adapter == null) return;
+
+        final CharSequence[] options = adapter.getAutofillOptions();
+        if (options != null) {
+            structure.setAutofillOptions(options);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/widget/AdapterViewAnimator.java b/android/widget/AdapterViewAnimator.java
new file mode 100644
index 0000000..6f29368
--- /dev/null
+++ b/android/widget/AdapterViewAnimator.java
@@ -0,0 +1,1099 @@
+/*
+ * 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 android.widget;
+
+import android.animation.AnimatorInflater;
+import android.animation.ObjectAnimator;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.os.Handler;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.OnClickHandler;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/**
+ * Base class for a {@link AdapterView} that will perform animations
+ * when switching between its views.
+ *
+ * @attr ref android.R.styleable#AdapterViewAnimator_inAnimation
+ * @attr ref android.R.styleable#AdapterViewAnimator_outAnimation
+ * @attr ref android.R.styleable#AdapterViewAnimator_animateFirstView
+ * @attr ref android.R.styleable#AdapterViewAnimator_loopViews
+ */
+public abstract class AdapterViewAnimator extends AdapterView<Adapter>
+        implements RemoteViewsAdapter.RemoteAdapterConnectionCallback, Advanceable {
+    private static final String TAG = "RemoteViewAnimator";
+
+    /**
+     * The index of the current child, which appears anywhere from the beginning
+     * to the end of the current set of children, as specified by {@link #mActiveOffset}
+     */
+    int mWhichChild = 0;
+
+    /**
+     * The index of the child to restore after the asynchronous connection from the
+     * RemoteViewsAdapter has been.
+     */
+    private int mRestoreWhichChild = -1;
+
+    /**
+     * Whether or not the first view(s) should be animated in
+     */
+    boolean mAnimateFirstTime = true;
+
+    /**
+     *  Represents where the in the current window of
+     *  views the current <code>mDisplayedChild</code> sits
+     */
+    int mActiveOffset = 0;
+
+    /**
+     * The number of views that the {@link AdapterViewAnimator} keeps as children at any
+     * given time (not counting views that are pending removal, see {@link #mPreviousViews}).
+     */
+    int mMaxNumActiveViews = 1;
+
+    /**
+     * Map of the children of the {@link AdapterViewAnimator}.
+     */
+    HashMap<Integer, ViewAndMetaData> mViewsMap = new HashMap<Integer, ViewAndMetaData>();
+
+    /**
+     * List of views pending removal from the {@link AdapterViewAnimator}
+     */
+    ArrayList<Integer> mPreviousViews;
+
+    /**
+     * The index, relative to the adapter, of the beginning of the window of views
+     */
+    int mCurrentWindowStart = 0;
+
+    /**
+     * The index, relative to the adapter, of the end of the window of views
+     */
+    int mCurrentWindowEnd = -1;
+
+    /**
+     * The same as {@link #mCurrentWindowStart}, except when the we have bounded
+     * {@link #mCurrentWindowStart} to be non-negative
+     */
+    int mCurrentWindowStartUnbounded = 0;
+
+    /**
+     * Listens for data changes from the adapter
+     */
+    AdapterDataSetObserver mDataSetObserver;
+
+    /**
+     * The {@link Adapter} for this {@link AdapterViewAnimator}
+     */
+    Adapter mAdapter;
+
+    /**
+     * The {@link RemoteViewsAdapter} for this {@link AdapterViewAnimator}
+     */
+    RemoteViewsAdapter mRemoteViewsAdapter;
+
+    /**
+     * The remote adapter containing the data to be displayed by this view to be set
+     */
+    boolean mDeferNotifyDataSetChanged = false;
+
+    /**
+     * Specifies whether this is the first time the animator is showing views
+     */
+    boolean mFirstTime = true;
+
+    /**
+     * Specifies if the animator should wrap from 0 to the end and vice versa
+     * or have hard boundaries at the beginning and end
+     */
+    boolean mLoopViews = true;
+
+    /**
+     * The width and height of some child, used as a size reference in-case our
+     * dimensions are unspecified by the parent.
+     */
+    int mReferenceChildWidth = -1;
+    int mReferenceChildHeight = -1;
+
+    /**
+     * In and out animations.
+     */
+    ObjectAnimator mInAnimation;
+    ObjectAnimator mOutAnimation;
+
+    /**
+     * Current touch state.
+     */
+    private int mTouchMode = TOUCH_MODE_NONE;
+
+    /**
+     * Private touch states.
+     */
+    static final int TOUCH_MODE_NONE = 0;
+    static final int TOUCH_MODE_DOWN_IN_CURRENT_VIEW = 1;
+    static final int TOUCH_MODE_HANDLED = 2;
+
+    private Runnable mPendingCheckForTap;
+
+    private static final int DEFAULT_ANIMATION_DURATION = 200;
+
+    public AdapterViewAnimator(Context context) {
+        this(context, null);
+    }
+
+    public AdapterViewAnimator(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AdapterViewAnimator(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AdapterViewAnimator(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.AdapterViewAnimator, defStyleAttr, defStyleRes);
+        int resource = a.getResourceId(
+                com.android.internal.R.styleable.AdapterViewAnimator_inAnimation, 0);
+        if (resource > 0) {
+            setInAnimation(context, resource);
+        } else {
+            setInAnimation(getDefaultInAnimation());
+        }
+
+        resource = a.getResourceId(com.android.internal.R.styleable.AdapterViewAnimator_outAnimation, 0);
+        if (resource > 0) {
+            setOutAnimation(context, resource);
+        } else {
+            setOutAnimation(getDefaultOutAnimation());
+        }
+
+        boolean flag = a.getBoolean(
+                com.android.internal.R.styleable.AdapterViewAnimator_animateFirstView, true);
+        setAnimateFirstView(flag);
+
+        mLoopViews = a.getBoolean(
+                com.android.internal.R.styleable.AdapterViewAnimator_loopViews, false);
+
+        a.recycle();
+
+        initViewAnimator();
+    }
+
+    /**
+     * Initialize this {@link AdapterViewAnimator}
+     */
+    private void initViewAnimator() {
+        mPreviousViews = new ArrayList<Integer>();
+    }
+
+    class ViewAndMetaData {
+        View view;
+        int relativeIndex;
+        int adapterPosition;
+        long itemId;
+
+        ViewAndMetaData(View view, int relativeIndex, int adapterPosition, long itemId) {
+            this.view = view;
+            this.relativeIndex = relativeIndex;
+            this.adapterPosition = adapterPosition;
+            this.itemId = itemId;
+        }
+    }
+
+    /**
+     * This method is used by subclasses to configure the animator to display the
+     * desired number of views, and specify the offset
+     *
+     * @param numVisibleViews The number of views the animator keeps in the {@link ViewGroup}
+     * @param activeOffset This parameter specifies where the current index ({@link #mWhichChild})
+     *        sits within the window. For example if activeOffset is 1, and numVisibleViews is 3,
+     *        and {@link #setDisplayedChild(int)} is called with 10, then the effective window will
+     *        be the indexes 9, 10, and 11. In the same example, if activeOffset were 0, then the
+     *        window would instead contain indexes 10, 11 and 12.
+     * @param shouldLoop If the animator is show view 0, and setPrevious() is called, do we
+     *        we loop back to the end, or do we do nothing
+     */
+     void configureViewAnimator(int numVisibleViews, int activeOffset) {
+        if (activeOffset > numVisibleViews - 1) {
+            // Throw an exception here.
+        }
+        mMaxNumActiveViews = numVisibleViews;
+        mActiveOffset = activeOffset;
+        mPreviousViews.clear();
+        mViewsMap.clear();
+        removeAllViewsInLayout();
+        mCurrentWindowStart = 0;
+        mCurrentWindowEnd = -1;
+    }
+
+    /**
+     * This class should be overridden by subclasses to customize view transitions within
+     * the set of visible views
+     *
+     * @param fromIndex The relative index within the window that the view was in, -1 if it wasn't
+     *        in the window
+     * @param toIndex The relative index within the window that the view is going to, -1 if it is
+     *        being removed
+     * @param view The view that is being animated
+     */
+    void transformViewForTransition(int fromIndex, int toIndex, View view, boolean animate) {
+        if (fromIndex == -1) {
+            mInAnimation.setTarget(view);
+            mInAnimation.start();
+        } else if (toIndex == -1) {
+            mOutAnimation.setTarget(view);
+            mOutAnimation.start();
+        }
+    }
+
+    ObjectAnimator getDefaultInAnimation() {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 0.0f, 1.0f);
+        anim.setDuration(DEFAULT_ANIMATION_DURATION);
+        return anim;
+    }
+
+    ObjectAnimator getDefaultOutAnimation() {
+        ObjectAnimator anim = ObjectAnimator.ofFloat(null, "alpha", 1.0f, 0.0f);
+        anim.setDuration(DEFAULT_ANIMATION_DURATION);
+        return anim;
+    }
+
+    /**
+     * Sets which child view will be displayed.
+     *
+     * @param whichChild the index of the child view to display
+     */
+    @android.view.RemotableViewMethod
+    public void setDisplayedChild(int whichChild) {
+        setDisplayedChild(whichChild, true);
+    }
+
+    private void setDisplayedChild(int whichChild, boolean animate) {
+        if (mAdapter != null) {
+            mWhichChild = whichChild;
+            if (whichChild >= getWindowSize()) {
+                mWhichChild = mLoopViews ? 0 : getWindowSize() - 1;
+            } else if (whichChild < 0) {
+                mWhichChild = mLoopViews ? getWindowSize() - 1 : 0;
+            }
+
+            boolean hasFocus = getFocusedChild() != null;
+            // This will clear old focus if we had it
+            showOnly(mWhichChild, animate);
+            if (hasFocus) {
+                // Try to retake focus if we had it
+                requestFocus(FOCUS_FORWARD);
+            }
+        }
+    }
+
+    /**
+     * To be overridden by subclasses. This method applies a view / index specific
+     * transform to the child view.
+     *
+     * @param child
+     * @param relativeIndex
+     */
+    void applyTransformForChildAtIndex(View child, int relativeIndex) {
+    }
+
+    /**
+     * Returns the index of the currently displayed child view.
+     */
+    public int getDisplayedChild() {
+        return mWhichChild;
+    }
+
+    /**
+     * Manually shows the next child.
+     */
+    public void showNext() {
+        setDisplayedChild(mWhichChild + 1);
+    }
+
+    /**
+     * Manually shows the previous child.
+     */
+    public void showPrevious() {
+        setDisplayedChild(mWhichChild - 1);
+    }
+
+    int modulo(int pos, int size) {
+        if (size > 0) {
+            return (size + (pos % size)) % size;
+        } else {
+            return 0;
+        }
+    }
+
+    /**
+     * Get the view at this index relative to the current window's start
+     *
+     * @param relativeIndex Position relative to the current window's start
+     * @return View at this index, null if the index is outside the bounds
+     */
+    View getViewAtRelativeIndex(int relativeIndex) {
+        if (relativeIndex >= 0 && relativeIndex <= getNumActiveViews() - 1 && mAdapter != null) {
+            int i = modulo(mCurrentWindowStartUnbounded + relativeIndex, getWindowSize());
+            if (mViewsMap.get(i) != null) {
+                return mViewsMap.get(i).view;
+            }
+        }
+        return null;
+    }
+
+    int getNumActiveViews() {
+        if (mAdapter != null) {
+            return Math.min(getCount() + 1, mMaxNumActiveViews);
+        } else {
+            return mMaxNumActiveViews;
+        }
+    }
+
+    int getWindowSize() {
+        if (mAdapter != null) {
+            int adapterCount = getCount();
+            if (adapterCount <= getNumActiveViews() && mLoopViews) {
+                return adapterCount*mMaxNumActiveViews;
+            } else {
+                return adapterCount;
+            }
+        } else {
+            return 0;
+        }
+    }
+
+    private ViewAndMetaData getMetaDataForChild(View child) {
+        for (ViewAndMetaData vm: mViewsMap.values()) {
+            if (vm.view == child) {
+                return vm;
+            }
+        }
+        return null;
+     }
+
+    LayoutParams createOrReuseLayoutParams(View v) {
+        final LayoutParams currentLp = v.getLayoutParams();
+        if (currentLp != null) {
+            return currentLp;
+        }
+        return new LayoutParams(0, 0);
+    }
+
+    void refreshChildren() {
+        if (mAdapter == null) return;
+        for (int i = mCurrentWindowStart; i <= mCurrentWindowEnd; i++) {
+            int index = modulo(i, getWindowSize());
+
+            int adapterCount = getCount();
+            // get the fresh child from the adapter
+            final View updatedChild = mAdapter.getView(modulo(i, adapterCount), null, this);
+
+            if (updatedChild.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+                updatedChild.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+            }
+
+            if (mViewsMap.containsKey(index)) {
+                final FrameLayout fl = (FrameLayout) mViewsMap.get(index).view;
+                // add the new child to the frame, if it exists
+                if (updatedChild != null) {
+                    // flush out the old child
+                    fl.removeAllViewsInLayout();
+                    fl.addView(updatedChild);
+                }
+            }
+        }
+    }
+
+    /**
+     * This method can be overridden so that subclasses can provide a custom frame in which their
+     * children can live. For example, StackView adds padding to its childrens' frames so as to
+     * accomodate for the highlight effect.
+     *
+     * @return The FrameLayout into which children can be placed.
+     */
+    FrameLayout getFrameForChild() {
+        return new FrameLayout(mContext);
+    }
+
+    /**
+     * Shows only the specified child. The other displays Views exit the screen,
+     * optionally with the with the {@link #getOutAnimation() out animation} and
+     * the specified child enters the screen, optionally with the
+     * {@link #getInAnimation() in animation}.
+     *
+     * @param childIndex The index of the child to be shown.
+     * @param animate Whether or not to use the in and out animations, defaults
+     *            to true.
+     */
+    void showOnly(int childIndex, boolean animate) {
+        if (mAdapter == null) return;
+        final int adapterCount = getCount();
+        if (adapterCount == 0) return;
+
+        for (int i = 0; i < mPreviousViews.size(); i++) {
+            View viewToRemove = mViewsMap.get(mPreviousViews.get(i)).view;
+            mViewsMap.remove(mPreviousViews.get(i));
+            viewToRemove.clearAnimation();
+            if (viewToRemove instanceof ViewGroup) {
+                ViewGroup vg = (ViewGroup) viewToRemove;
+                vg.removeAllViewsInLayout();
+            }
+            // applyTransformForChildAtIndex here just allows for any cleanup
+            // associated with this view that may need to be done by a subclass
+            applyTransformForChildAtIndex(viewToRemove, -1);
+
+            removeViewInLayout(viewToRemove);
+        }
+        mPreviousViews.clear();
+        int newWindowStartUnbounded = childIndex - mActiveOffset;
+        int newWindowEndUnbounded = newWindowStartUnbounded + getNumActiveViews() - 1;
+        int newWindowStart = Math.max(0, newWindowStartUnbounded);
+        int newWindowEnd = Math.min(adapterCount - 1, newWindowEndUnbounded);
+
+        if (mLoopViews) {
+            newWindowStart = newWindowStartUnbounded;
+            newWindowEnd = newWindowEndUnbounded;
+        }
+        int rangeStart = modulo(newWindowStart, getWindowSize());
+        int rangeEnd = modulo(newWindowEnd, getWindowSize());
+
+        boolean wrap = false;
+        if (rangeStart > rangeEnd) {
+            wrap = true;
+        }
+
+        // This section clears out any items that are in our active views list
+        // but are outside the effective bounds of our window (this is becomes an issue
+        // at the extremities of the list, eg. where newWindowStartUnbounded < 0 or
+        // newWindowEndUnbounded > adapterCount - 1
+        for (Integer index : mViewsMap.keySet()) {
+            boolean remove = false;
+            if (!wrap && (index < rangeStart || index > rangeEnd)) {
+                remove = true;
+            } else if (wrap && (index > rangeEnd && index < rangeStart)) {
+                remove = true;
+            }
+
+            if (remove) {
+                View previousView = mViewsMap.get(index).view;
+                int oldRelativeIndex = mViewsMap.get(index).relativeIndex;
+
+                mPreviousViews.add(index);
+                transformViewForTransition(oldRelativeIndex, -1, previousView, animate);
+            }
+        }
+
+        // If the window has changed
+        if (!(newWindowStart == mCurrentWindowStart && newWindowEnd == mCurrentWindowEnd &&
+              newWindowStartUnbounded == mCurrentWindowStartUnbounded)) {
+            // Run through the indices in the new range
+            for (int i = newWindowStart; i <= newWindowEnd; i++) {
+
+                int index = modulo(i, getWindowSize());
+                int oldRelativeIndex;
+                if (mViewsMap.containsKey(index)) {
+                    oldRelativeIndex = mViewsMap.get(index).relativeIndex;
+                } else {
+                    oldRelativeIndex = -1;
+                }
+                int newRelativeIndex = i - newWindowStartUnbounded;
+
+                // If this item is in the current window, great, we just need to apply
+                // the transform for it's new relative position in the window, and animate
+                // between it's current and new relative positions
+                boolean inOldRange = mViewsMap.containsKey(index) && !mPreviousViews.contains(index);
+
+                if (inOldRange) {
+                    View view = mViewsMap.get(index).view;
+                    mViewsMap.get(index).relativeIndex = newRelativeIndex;
+                    applyTransformForChildAtIndex(view, newRelativeIndex);
+                    transformViewForTransition(oldRelativeIndex, newRelativeIndex, view, animate);
+
+                // Otherwise this view is new to the window
+                } else {
+                    // Get the new view from the adapter, add it and apply any transform / animation
+                    final int adapterPosition = modulo(i, adapterCount);
+                    View newView = mAdapter.getView(adapterPosition, null, this);
+                    long itemId = mAdapter.getItemId(adapterPosition);
+
+                    // We wrap the new view in a FrameLayout so as to respect the contract
+                    // with the adapter, that is, that we don't modify this view directly
+                    FrameLayout fl = getFrameForChild();
+
+                    // If the view from the adapter is null, we still keep an empty frame in place
+                    if (newView != null) {
+                       fl.addView(newView);
+                    }
+                    mViewsMap.put(index, new ViewAndMetaData(fl, newRelativeIndex,
+                            adapterPosition, itemId));
+                    addChild(fl);
+                    applyTransformForChildAtIndex(fl, newRelativeIndex);
+                    transformViewForTransition(-1, newRelativeIndex, fl, animate);
+                }
+                mViewsMap.get(index).view.bringToFront();
+            }
+            mCurrentWindowStart = newWindowStart;
+            mCurrentWindowEnd = newWindowEnd;
+            mCurrentWindowStartUnbounded = newWindowStartUnbounded;
+            if (mRemoteViewsAdapter != null) {
+                int adapterStart = modulo(mCurrentWindowStart, adapterCount);
+                int adapterEnd = modulo(mCurrentWindowEnd, adapterCount);
+                mRemoteViewsAdapter.setVisibleRangeHint(adapterStart, adapterEnd);
+            }
+        }
+        requestLayout();
+        invalidate();
+    }
+
+    private void addChild(View child) {
+        addViewInLayout(child, -1, createOrReuseLayoutParams(child));
+
+        // This code is used to obtain a reference width and height of a child in case we need
+        // to decide our own size. TODO: Do we want to update the size of the child that we're
+        // using for reference size? If so, when?
+        if (mReferenceChildWidth == -1 || mReferenceChildHeight == -1) {
+            int measureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+            child.measure(measureSpec, measureSpec);
+            mReferenceChildWidth = child.getMeasuredWidth();
+            mReferenceChildHeight = child.getMeasuredHeight();
+        }
+    }
+
+    void showTapFeedback(View v) {
+        v.setPressed(true);
+    }
+
+    void hideTapFeedback(View v) {
+        v.setPressed(false);
+    }
+
+    void cancelHandleClick() {
+        View v = getCurrentView();
+        if (v != null) {
+            hideTapFeedback(v);
+        }
+        mTouchMode = TOUCH_MODE_NONE;
+    }
+
+    final class CheckForTap implements Runnable {
+        public void run() {
+            if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) {
+                View v = getCurrentView();
+                showTapFeedback(v);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        int action = ev.getAction();
+        boolean handled = false;
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                View v = getCurrentView();
+                if (v != null) {
+                    if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
+                        if (mPendingCheckForTap == null) {
+                            mPendingCheckForTap = new CheckForTap();
+                        }
+                        mTouchMode = TOUCH_MODE_DOWN_IN_CURRENT_VIEW;
+                        postDelayed(mPendingCheckForTap, ViewConfiguration.getTapTimeout());
+                    }
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: break;
+            case MotionEvent.ACTION_POINTER_UP: break;
+            case MotionEvent.ACTION_UP: {
+                if (mTouchMode == TOUCH_MODE_DOWN_IN_CURRENT_VIEW) {
+                    final View v = getCurrentView();
+                    final ViewAndMetaData viewData = getMetaDataForChild(v);
+                    if (v != null) {
+                        if (isTransformedTouchPointInView(ev.getX(), ev.getY(), v, null)) {
+                            final Handler handler = getHandler();
+                            if (handler != null) {
+                                handler.removeCallbacks(mPendingCheckForTap);
+                            }
+                            showTapFeedback(v);
+                            postDelayed(new Runnable() {
+                                public void run() {
+                                    hideTapFeedback(v);
+                                    post(new Runnable() {
+                                        public void run() {
+                                            if (viewData != null) {
+                                                performItemClick(v, viewData.adapterPosition,
+                                                        viewData.itemId);
+                                            } else {
+                                                performItemClick(v, 0, 0);
+                                            }
+                                        }
+                                    });
+                                }
+                            }, ViewConfiguration.getPressedStateDuration());
+                            handled = true;
+                        }
+                    }
+                }
+                mTouchMode = TOUCH_MODE_NONE;
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL: {
+                View v = getCurrentView();
+                if (v != null) {
+                    hideTapFeedback(v);
+                }
+                mTouchMode = TOUCH_MODE_NONE;
+            }
+        }
+        return handled;
+    }
+
+    private void measureChildren() {
+        final int count = getChildCount();
+        final int childWidth = getMeasuredWidth() - mPaddingLeft - mPaddingRight;
+        final int childHeight = getMeasuredHeight() - mPaddingTop - mPaddingBottom;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.EXACTLY));
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
+
+        // We need to deal with the case where our parent hasn't told us how
+        // big we should be. In this case we try to use the desired size of the first
+        // child added.
+        if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
+            heightSpecSize = haveChildRefSize ? mReferenceChildHeight + mPaddingTop +
+                    mPaddingBottom : 0;
+        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+            if (haveChildRefSize) {
+                int height = mReferenceChildHeight + mPaddingTop + mPaddingBottom;
+                if (height > heightSpecSize) {
+                    heightSpecSize |= MEASURED_STATE_TOO_SMALL;
+                } else {
+                    heightSpecSize = height;
+                }
+            }
+        }
+
+        if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
+            widthSpecSize = haveChildRefSize ? mReferenceChildWidth + mPaddingLeft +
+                    mPaddingRight : 0;
+        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+            if (haveChildRefSize) {
+                int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
+                if (width > widthSpecSize) {
+                    widthSpecSize |= MEASURED_STATE_TOO_SMALL;
+                } else {
+                    widthSpecSize = width;
+                }
+            }
+        }
+
+        setMeasuredDimension(widthSpecSize, heightSpecSize);
+        measureChildren();
+    }
+
+    void checkForAndHandleDataChanged() {
+        boolean dataChanged = mDataChanged;
+        if (dataChanged) {
+            post(new Runnable() {
+                public void run() {
+                    handleDataChanged();
+                    // if the data changes, mWhichChild might be out of the bounds of the adapter
+                    // in this case, we reset mWhichChild to the beginning
+                    if (mWhichChild >= getWindowSize()) {
+                        mWhichChild = 0;
+
+                        showOnly(mWhichChild, false);
+                    } else if (mOldItemCount != getCount()) {
+                        showOnly(mWhichChild, false);
+                    }
+                    refreshChildren();
+                    requestLayout();
+                }
+            });
+        }
+        mDataChanged = false;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        checkForAndHandleDataChanged();
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+
+            int childRight = mPaddingLeft + child.getMeasuredWidth();
+            int childBottom = mPaddingTop + child.getMeasuredHeight();
+
+            child.layout(mPaddingLeft, mPaddingTop, childRight, childBottom);
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        int whichChild;
+
+        /**
+         * Constructor called from {@link AdapterViewAnimator#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState, int whichChild) {
+            super(superState);
+            this.whichChild = whichChild;
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            this.whichChild = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(this.whichChild);
+        }
+
+        @Override
+        public String toString() {
+            return "AdapterViewAnimator.SavedState{ whichChild = " + this.whichChild + " }";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        if (mRemoteViewsAdapter != null) {
+            mRemoteViewsAdapter.saveRemoteViewsCache();
+        }
+        return new SavedState(superState, mWhichChild);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        // Here we set mWhichChild in addition to setDisplayedChild
+        // We do the former in case mAdapter is null, and hence setDisplayedChild won't
+        // set mWhichChild
+        mWhichChild = ss.whichChild;
+
+        // When using RemoteAdapters, the async connection process can lead to
+        // onRestoreInstanceState to be called before setAdapter(), so we need to save the previous
+        // values to restore the list position after we connect, and can skip setting the displayed
+        // child until then.
+        if (mRemoteViewsAdapter != null && mAdapter == null) {
+            mRestoreWhichChild = mWhichChild;
+        } else {
+            setDisplayedChild(mWhichChild, false);
+        }
+    }
+
+    /**
+     * Returns the View corresponding to the currently displayed child.
+     *
+     * @return The View currently displayed.
+     *
+     * @see #getDisplayedChild()
+     */
+    public View getCurrentView() {
+        return getViewAtRelativeIndex(mActiveOffset);
+    }
+
+    /**
+     * Returns the current animation used to animate a View that enters the screen.
+     *
+     * @return An Animation or null if none is set.
+     *
+     * @see #setInAnimation(android.animation.ObjectAnimator)
+     * @see #setInAnimation(android.content.Context, int)
+     */
+    public ObjectAnimator getInAnimation() {
+        return mInAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that enters the screen.
+     *
+     * @param inAnimation The animation started when a View enters the screen.
+     *
+     * @see #getInAnimation()
+     * @see #setInAnimation(android.content.Context, int)
+     */
+    public void setInAnimation(ObjectAnimator inAnimation) {
+        mInAnimation = inAnimation;
+    }
+
+    /**
+     * Returns the current animation used to animate a View that exits the screen.
+     *
+     * @return An Animation or null if none is set.
+     *
+     * @see #setOutAnimation(android.animation.ObjectAnimator)
+     * @see #setOutAnimation(android.content.Context, int)
+     */
+    public ObjectAnimator getOutAnimation() {
+        return mOutAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that exit the screen.
+     *
+     * @param outAnimation The animation started when a View exit the screen.
+     *
+     * @see #getOutAnimation()
+     * @see #setOutAnimation(android.content.Context, int)
+     */
+    public void setOutAnimation(ObjectAnimator outAnimation) {
+        mOutAnimation = outAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that enters the screen.
+     *
+     * @param context The application's environment.
+     * @param resourceID The resource id of the animation.
+     *
+     * @see #getInAnimation()
+     * @see #setInAnimation(android.animation.ObjectAnimator)
+     */
+    public void setInAnimation(Context context, int resourceID) {
+        setInAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(context, resourceID));
+    }
+
+    /**
+     * Specifies the animation used to animate a View that exit the screen.
+     *
+     * @param context The application's environment.
+     * @param resourceID The resource id of the animation.
+     *
+     * @see #getOutAnimation()
+     * @see #setOutAnimation(android.animation.ObjectAnimator)
+     */
+    public void setOutAnimation(Context context, int resourceID) {
+        setOutAnimation((ObjectAnimator) AnimatorInflater.loadAnimator(context, resourceID));
+    }
+
+    /**
+     * Indicates whether the current View should be animated the first time
+     * the ViewAnimation is displayed.
+     *
+     * @param animate True to animate the current View the first time it is displayed,
+     *                false otherwise.
+     */
+    public void setAnimateFirstView(boolean animate) {
+        mAnimateFirstTime = animate;
+    }
+
+    @Override
+    public int getBaseline() {
+        return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline();
+    }
+
+    @Override
+    public Adapter getAdapter() {
+        return mAdapter;
+    }
+
+    @Override
+    public void setAdapter(Adapter adapter) {
+        if (mAdapter != null && mDataSetObserver != null) {
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
+        }
+
+        mAdapter = adapter;
+        checkFocus();
+
+        if (mAdapter != null) {
+            mDataSetObserver = new AdapterDataSetObserver();
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+            mItemCount = mAdapter.getCount();
+        }
+        setFocusable(true);
+        mWhichChild = 0;
+        showOnly(mWhichChild, false);
+    }
+
+    /**
+     * Sets up this AdapterViewAnimator to use a remote views adapter which connects to a
+     * RemoteViewsService through the specified intent.
+     *
+     * @param intent the intent used to identify the RemoteViewsService for the adapter to
+     *        connect to.
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+    public void setRemoteViewsAdapter(Intent intent) {
+        setRemoteViewsAdapter(intent, false);
+    }
+
+    /** @hide **/
+    public Runnable setRemoteViewsAdapterAsync(final Intent intent) {
+        return new RemoteViewsAdapter.AsyncRemoteAdapterAction(this, intent);
+    }
+
+    /** @hide **/
+    @Override
+    public void setRemoteViewsAdapter(Intent intent, boolean isAsync) {
+        // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+        // service handling the specified intent.
+        if (mRemoteViewsAdapter != null) {
+            Intent.FilterComparison fcNew = new Intent.FilterComparison(intent);
+            Intent.FilterComparison fcOld = new Intent.FilterComparison(
+                    mRemoteViewsAdapter.getRemoteViewsServiceIntent());
+            if (fcNew.equals(fcOld)) {
+                return;
+            }
+        }
+        mDeferNotifyDataSetChanged = false;
+        // Otherwise, create a new RemoteViewsAdapter for binding
+        mRemoteViewsAdapter = new RemoteViewsAdapter(getContext(), intent, this, isAsync);
+        if (mRemoteViewsAdapter.isDataReady()) {
+            setAdapter(mRemoteViewsAdapter);
+        }
+    }
+
+    /**
+     * Sets up the onClickHandler to be used by the RemoteViewsAdapter when inflating RemoteViews
+     * 
+     * @param handler The OnClickHandler to use when inflating RemoteViews.
+     * 
+     * @hide
+     */
+    public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+        // Ensure that we don't already have a RemoteViewsAdapter that is bound to an existing
+        // service handling the specified intent.
+        if (mRemoteViewsAdapter != null) {
+            mRemoteViewsAdapter.setRemoteViewsOnClickHandler(handler);
+        }
+    }
+
+    @Override
+    public void setSelection(int position) {
+        setDisplayedChild(position);
+    }
+
+    @Override
+    public View getSelectedView() {
+        return getViewAtRelativeIndex(mActiveOffset);
+    }
+
+    /**
+     * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+     * connected yet.
+     */
+    public void deferNotifyDataSetChanged() {
+        mDeferNotifyDataSetChanged = true;
+    }
+
+    /**
+     * Called back when the adapter connects to the RemoteViewsService.
+     */
+    public boolean onRemoteAdapterConnected() {
+        if (mRemoteViewsAdapter != mAdapter) {
+            setAdapter(mRemoteViewsAdapter);
+
+            if (mDeferNotifyDataSetChanged) {
+                mRemoteViewsAdapter.notifyDataSetChanged();
+                mDeferNotifyDataSetChanged = false;
+            }
+
+            // Restore the previous position (see onRestoreInstanceState)
+            if (mRestoreWhichChild > -1) {
+                setDisplayedChild(mRestoreWhichChild, false);
+                mRestoreWhichChild = -1;
+            }
+            return false;
+        } else if (mRemoteViewsAdapter != null) {
+            mRemoteViewsAdapter.superNotifyDataSetChanged();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Called back when the adapter disconnects from the RemoteViewsService.
+     */
+    public void onRemoteAdapterDisconnected() {
+        // If the remote adapter disconnects, we keep it around
+        // since the currently displayed items are still cached.
+        // Further, we want the service to eventually reconnect
+        // when necessary, as triggered by this view requesting
+        // items from the Adapter.
+    }
+
+    /**
+     * Called by an {@link android.appwidget.AppWidgetHost} in order to advance the current view when
+     * it is being used within an app widget.
+     */
+    public void advance() {
+        showNext();
+    }
+
+    /**
+     * Called by an {@link android.appwidget.AppWidgetHost} to indicate that it will be
+     * automatically advancing the views of this {@link AdapterViewAnimator} by calling
+     * {@link AdapterViewAnimator#advance()} at some point in the future. This allows subclasses to
+     * perform any required setup, for example, to stop automatically advancing their children.
+     */
+    public void fyiWillBeAdvancedByHostKThx() {
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AdapterViewAnimator.class.getName();
+    }
+}
diff --git a/android/widget/AdapterViewFlipper.java b/android/widget/AdapterViewFlipper.java
new file mode 100644
index 0000000..18d7470
--- /dev/null
+++ b/android/widget/AdapterViewFlipper.java
@@ -0,0 +1,301 @@
+/*
+ * 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 android.widget;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.RemotableViewMethod;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * Simple {@link ViewAnimator} that will animate between two or more views
+ * that have been added to it.  Only one child is shown at a time.  If
+ * requested, can automatically flip between each child at a regular interval.
+ *
+ * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+ * @attr ref android.R.styleable#AdapterViewFlipper_autoStart
+ */
+@RemoteView
+public class AdapterViewFlipper extends AdapterViewAnimator {
+    private static final String TAG = "ViewFlipper";
+    private static final boolean LOGD = false;
+
+    private static final int DEFAULT_INTERVAL = 10000;
+
+    private int mFlipInterval = DEFAULT_INTERVAL;
+    private boolean mAutoStart = false;
+
+    private boolean mRunning = false;
+    private boolean mStarted = false;
+    private boolean mVisible = false;
+    private boolean mUserPresent = true;
+    private boolean mAdvancedByHost = false;
+
+    public AdapterViewFlipper(Context context) {
+        super(context);
+    }
+
+    public AdapterViewFlipper(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AdapterViewFlipper(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AdapterViewFlipper(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.AdapterViewFlipper, defStyleAttr, defStyleRes);
+        mFlipInterval = a.getInt(
+                com.android.internal.R.styleable.AdapterViewFlipper_flipInterval, DEFAULT_INTERVAL);
+        mAutoStart = a.getBoolean(
+                com.android.internal.R.styleable.AdapterViewFlipper_autoStart, false);
+
+        // A view flipper should cycle through the views
+        mLoopViews = true;
+
+        a.recycle();
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                mUserPresent = false;
+                updateRunning();
+            } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+                mUserPresent = true;
+                updateRunning(false);
+            }
+        }
+    };
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Listen for broadcasts related to user-presence
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(Intent.ACTION_USER_PRESENT);
+
+        // OK, this is gross but needed. This class is supported by the
+        // remote views machanism and as a part of that the remote views
+        // can be inflated by a context for another user without the app
+        // having interact users permission - just for loading resources.
+        // For exmaple, when adding widgets from a user profile to the
+        // home screen. Therefore, we register the receiver as the current
+        // user not the one the context is for.
+        getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
+                filter, null, getHandler());
+
+
+        if (mAutoStart) {
+            // Automatically start when requested
+            startFlipping();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mVisible = false;
+
+        getContext().unregisterReceiver(mReceiver);
+        updateRunning();
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mVisible = (visibility == VISIBLE);
+        updateRunning(false);
+    }
+
+    @Override
+    public void setAdapter(Adapter adapter) {
+        super.setAdapter(adapter);
+        updateRunning();
+    }
+
+    /**
+     * Returns the flip interval, in milliseconds.
+     *
+     * @return the flip interval in milliseconds
+     *
+     * @see #setFlipInterval(int)
+     *
+     * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+     */
+    public int getFlipInterval() {
+        return mFlipInterval;
+    }
+
+    /**
+     * How long to wait before flipping to the next view.
+     *
+     * @param flipInterval flip interval in milliseconds
+     *
+     * @see #getFlipInterval()
+     *
+     * @attr ref android.R.styleable#AdapterViewFlipper_flipInterval
+     */
+    public void setFlipInterval(int flipInterval) {
+        mFlipInterval = flipInterval;
+    }
+
+    /**
+     * Start a timer to cycle through child views
+     */
+    public void startFlipping() {
+        mStarted = true;
+        updateRunning();
+    }
+
+    /**
+     * No more flips
+     */
+    public void stopFlipping() {
+        mStarted = false;
+        updateRunning();
+    }
+
+    /**
+    * {@inheritDoc}
+    */
+   @Override
+   @RemotableViewMethod
+   public void showNext() {
+       // if the flipper is currently flipping automatically, and showNext() is called
+       // we should we should make sure to reset the timer
+       if (mRunning) {
+           removeCallbacks(mFlipRunnable);
+           postDelayed(mFlipRunnable, mFlipInterval);
+       }
+       super.showNext();
+   }
+
+   /**
+    * {@inheritDoc}
+    */
+   @Override
+   @RemotableViewMethod
+   public void showPrevious() {
+       // if the flipper is currently flipping automatically, and showPrevious() is called
+       // we should we should make sure to reset the timer
+       if (mRunning) {
+           removeCallbacks(mFlipRunnable);
+           postDelayed(mFlipRunnable, mFlipInterval);
+       }
+       super.showPrevious();
+   }
+
+    /**
+     * Internal method to start or stop dispatching flip {@link Message} based
+     * on {@link #mRunning} and {@link #mVisible} state.
+     */
+    private void updateRunning() {
+        // by default when we update running, we want the
+        // current view to animate in
+        updateRunning(true);
+    }
+
+    /**
+     * Internal method to start or stop dispatching flip {@link Message} based
+     * on {@link #mRunning} and {@link #mVisible} state.
+     *
+     * @param flipNow Determines whether or not to execute the animation now, in
+     *            addition to queuing future flips. If omitted, defaults to
+     *            true.
+     */
+    private void updateRunning(boolean flipNow) {
+        boolean running = !mAdvancedByHost && mVisible && mStarted && mUserPresent
+                && mAdapter != null;
+        if (running != mRunning) {
+            if (running) {
+                showOnly(mWhichChild, flipNow);
+                postDelayed(mFlipRunnable, mFlipInterval);
+            } else {
+                removeCallbacks(mFlipRunnable);
+            }
+            mRunning = running;
+        }
+        if (LOGD) {
+            Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+                    + ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
+        }
+    }
+
+    /**
+     * Returns true if the child views are flipping.
+     */
+    public boolean isFlipping() {
+        return mStarted;
+    }
+
+    /**
+     * Set if this view automatically calls {@link #startFlipping()} when it
+     * becomes attached to a window.
+     */
+    public void setAutoStart(boolean autoStart) {
+        mAutoStart = autoStart;
+    }
+
+    /**
+     * Returns true if this view automatically calls {@link #startFlipping()}
+     * when it becomes attached to a window.
+     */
+    public boolean isAutoStart() {
+        return mAutoStart;
+    }
+
+    private final Runnable mFlipRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mRunning) {
+                showNext();
+            }
+        }
+    };
+
+    /**
+     * Called by an {@link android.appwidget.AppWidgetHost} to indicate that it will be
+     * automatically advancing the views of this {@link AdapterViewFlipper} by calling
+     * {@link AdapterViewFlipper#advance()} at some point in the future. This allows
+     * {@link AdapterViewFlipper} to prepare by no longer Advancing its children.
+     */
+    @Override
+    public void fyiWillBeAdvancedByHostKThx() {
+        mAdvancedByHost = true;
+        updateRunning(false);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return AdapterViewFlipper.class.getName();
+    }
+}
diff --git a/android/widget/Advanceable.java b/android/widget/Advanceable.java
new file mode 100644
index 0000000..ad8e101
--- /dev/null
+++ b/android/widget/Advanceable.java
@@ -0,0 +1,37 @@
+/*
+ * 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 android.widget;
+
+/**
+ * This interface can be implemented by any collection-type view which has a notion of
+ * progressing through its set of children. The interface exists to give AppWidgetHosts a way of
+ * taking responsibility for automatically advancing such collections.
+ *
+ */
+public interface Advanceable {
+
+    /**
+     * Advances this collection, eg. shows the next view.
+     */
+    public void advance();
+
+    /**
+     * Called by the AppWidgetHost once before it begins to call advance(), allowing the
+     * collection to do any required setup.
+     */
+    public void fyiWillBeAdvancedByHostKThx();
+}
diff --git a/android/widget/AlphabetIndexer.java b/android/widget/AlphabetIndexer.java
new file mode 100644
index 0000000..59b2c2a
--- /dev/null
+++ b/android/widget/AlphabetIndexer.java
@@ -0,0 +1,289 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.util.SparseIntArray;
+
+/**
+ * A helper class for adapters that implement the SectionIndexer interface.
+ * If the items in the adapter are sorted by simple alphabet-based sorting, then
+ * this class provides a way to do fast indexing of large lists using binary search.
+ * It caches the indices that have been determined through the binary search and also
+ * invalidates the cache if changes occur in the cursor.
+ * <p/>
+ * Your adapter is responsible for updating the cursor by calling {@link #setCursor} if the
+ * cursor changes. {@link #getPositionForSection} method does the binary search for the starting
+ * index of a given section (alphabet).
+ */
+public class AlphabetIndexer extends DataSetObserver implements SectionIndexer {
+
+    /**
+     * Cursor that is used by the adapter of the list view.
+     */
+    protected Cursor mDataCursor;
+
+    /**
+     * The index of the cursor column that this list is sorted on.
+     */
+    protected int mColumnIndex;
+
+    /**
+     * The string of characters that make up the indexing sections.
+     */
+    protected CharSequence mAlphabet;
+
+    /**
+     * Cached length of the alphabet array.
+     */
+    private int mAlphabetLength;
+
+    /**
+     * This contains a cache of the computed indices so far. It will get reset whenever
+     * the dataset changes or the cursor changes.
+     */
+    private SparseIntArray mAlphaMap;
+
+    /**
+     * Use a collator to compare strings in a localized manner.
+     */
+    private java.text.Collator mCollator;
+
+    /**
+     * The section array converted from the alphabet string.
+     */
+    private String[] mAlphabetArray;
+
+    /**
+     * Constructs the indexer.
+     * @param cursor the cursor containing the data set
+     * @param sortedColumnIndex the column number in the cursor that is sorted
+     *        alphabetically
+     * @param alphabet string containing the alphabet, with space as the first character.
+     *        For example, use the string " ABCDEFGHIJKLMNOPQRSTUVWXYZ" for English indexing.
+     *        The characters must be uppercase and be sorted in ascii/unicode order. Basically
+     *        characters in the alphabet will show up as preview letters.
+     */
+    public AlphabetIndexer(Cursor cursor, int sortedColumnIndex, CharSequence alphabet) {
+        mDataCursor = cursor;
+        mColumnIndex = sortedColumnIndex;
+        mAlphabet = alphabet;
+        mAlphabetLength = alphabet.length();
+        mAlphabetArray = new String[mAlphabetLength];
+        for (int i = 0; i < mAlphabetLength; i++) {
+            mAlphabetArray[i] = Character.toString(mAlphabet.charAt(i));
+        }
+        mAlphaMap = new SparseIntArray(mAlphabetLength);
+        if (cursor != null) {
+            cursor.registerDataSetObserver(this);
+        }
+        // Get a Collator for the current locale for string comparisons.
+        mCollator = java.text.Collator.getInstance();
+        mCollator.setStrength(java.text.Collator.PRIMARY);
+    }
+
+    /**
+     * Returns the section array constructed from the alphabet provided in the constructor.
+     * @return the section array
+     */
+    public Object[] getSections() {
+        return mAlphabetArray;
+    }
+
+    /**
+     * Sets a new cursor as the data set and resets the cache of indices.
+     * @param cursor the new cursor to use as the data set
+     */
+    public void setCursor(Cursor cursor) {
+        if (mDataCursor != null) {
+            mDataCursor.unregisterDataSetObserver(this);
+        }
+        mDataCursor = cursor;
+        if (cursor != null) {
+            mDataCursor.registerDataSetObserver(this);
+        }
+        mAlphaMap.clear();
+    }
+
+    /**
+     * Default implementation compares the first character of word with letter.
+     */
+    protected int compare(String word, String letter) {
+        final String firstLetter;
+        if (word.length() == 0) {
+            firstLetter = " ";
+        } else {
+            firstLetter = word.substring(0, 1);
+        }
+
+        return mCollator.compare(firstLetter, letter);
+    }
+
+    /**
+     * Performs a binary search or cache lookup to find the first row that
+     * matches a given section's starting letter.
+     * @param sectionIndex the section to search for
+     * @return the row index of the first occurrence, or the nearest next letter.
+     * For instance, if searching for "T" and no "T" is found, then the first
+     * row starting with "U" or any higher letter is returned. If there is no
+     * data following "T" at all, then the list size is returned.
+     */
+    public int getPositionForSection(int sectionIndex) {
+        final SparseIntArray alphaMap = mAlphaMap;
+        final Cursor cursor = mDataCursor;
+
+        if (cursor == null || mAlphabet == null) {
+            return 0;
+        }
+
+        // Check bounds
+        if (sectionIndex <= 0) {
+            return 0;
+        }
+        if (sectionIndex >= mAlphabetLength) {
+            sectionIndex = mAlphabetLength - 1;
+        }
+
+        int savedCursorPos = cursor.getPosition();
+
+        int count = cursor.getCount();
+        int start = 0;
+        int end = count;
+        int pos;
+
+        char letter = mAlphabet.charAt(sectionIndex);
+        String targetLetter = Character.toString(letter);
+        int key = letter;
+        // Check map
+        if (Integer.MIN_VALUE != (pos = alphaMap.get(key, Integer.MIN_VALUE))) {
+            // Is it approximate? Using negative value to indicate that it's
+            // an approximation and positive value when it is the accurate
+            // position.
+            if (pos < 0) {
+                pos = -pos;
+                end = pos;
+            } else {
+                // Not approximate, this is the confirmed start of section, return it
+                return pos;
+            }
+        }
+
+        // Do we have the position of the previous section?
+        if (sectionIndex > 0) {
+            int prevLetter =
+                    mAlphabet.charAt(sectionIndex - 1);
+            int prevLetterPos = alphaMap.get(prevLetter, Integer.MIN_VALUE);
+            if (prevLetterPos != Integer.MIN_VALUE) {
+                start = Math.abs(prevLetterPos);
+            }
+        }
+
+        // Now that we have a possibly optimized start and end, let's binary search
+
+        pos = (end + start) / 2;
+
+        while (pos < end) {
+            // Get letter at pos
+            cursor.moveToPosition(pos);
+            String curName = cursor.getString(mColumnIndex);
+            if (curName == null) {
+                if (pos == 0) {
+                    break;
+                } else {
+                    pos--;
+                    continue;
+                }
+            }
+            int diff = compare(curName, targetLetter);
+            if (diff != 0) {
+                // TODO: Commenting out approximation code because it doesn't work for certain
+                // lists with custom comparators
+                // Enter approximation in hash if a better solution doesn't exist
+                // String startingLetter = Character.toString(getFirstLetter(curName));
+                // int startingLetterKey = startingLetter.charAt(0);
+                // int curPos = alphaMap.get(startingLetterKey, Integer.MIN_VALUE);
+                // if (curPos == Integer.MIN_VALUE || Math.abs(curPos) > pos) {
+                //     Negative pos indicates that it is an approximation
+                //     alphaMap.put(startingLetterKey, -pos);
+                // }
+                // if (mCollator.compare(startingLetter, targetLetter) < 0) {
+                if (diff < 0) {
+                    start = pos + 1;
+                    if (start >= count) {
+                        pos = count;
+                        break;
+                    }
+                } else {
+                    end = pos;
+                }
+            } else {
+                // They're the same, but that doesn't mean it's the start
+                if (start == pos) {
+                    // This is it
+                    break;
+                } else {
+                    // Need to go further lower to find the starting row
+                    end = pos;
+                }
+            }
+            pos = (start + end) / 2;
+        }
+        alphaMap.put(key, pos);
+        cursor.moveToPosition(savedCursorPos);
+        return pos;
+    }
+
+    /**
+     * Returns the section index for a given position in the list by querying the item
+     * and comparing it with all items in the section array.
+     */
+    public int getSectionForPosition(int position) {
+        int savedCursorPos = mDataCursor.getPosition();
+        mDataCursor.moveToPosition(position);
+        String curName = mDataCursor.getString(mColumnIndex);
+        mDataCursor.moveToPosition(savedCursorPos);
+        // Linear search, as there are only a few items in the section index
+        // Could speed this up later if it actually gets used.
+        for (int i = 0; i < mAlphabetLength; i++) {
+            char letter = mAlphabet.charAt(i);
+            String targetLetter = Character.toString(letter);
+            if (compare(curName, targetLetter) == 0) {
+                return i;
+            }
+        }
+        return 0; // Don't recognize the letter - falls under zero'th section
+    }
+
+    /*
+     * @hide
+     */
+    @Override
+    public void onChanged() {
+        super.onChanged();
+        mAlphaMap.clear();
+    }
+
+    /*
+     * @hide
+     */
+    @Override
+    public void onInvalidated() {
+        super.onInvalidated();
+        mAlphaMap.clear();
+    }
+}
diff --git a/android/widget/AnalogClock.java b/android/widget/AnalogClock.java
new file mode 100644
index 0000000..bde5f7f
--- /dev/null
+++ b/android/widget/AnalogClock.java
@@ -0,0 +1,271 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.text.format.DateUtils;
+import android.text.format.Time;
+import android.util.AttributeSet;
+import android.view.View;
+import android.widget.RemoteViews.RemoteView;
+
+import java.util.TimeZone;
+
+/**
+ * This widget display an analogic clock with two hands for hours and
+ * minutes.
+ *
+ * @attr ref android.R.styleable#AnalogClock_dial
+ * @attr ref android.R.styleable#AnalogClock_hand_hour
+ * @attr ref android.R.styleable#AnalogClock_hand_minute
+ * @deprecated This widget is no longer supported.
+ */
+@RemoteView
+@Deprecated
+public class AnalogClock extends View {
+    private Time mCalendar;
+
+    private Drawable mHourHand;
+    private Drawable mMinuteHand;
+    private Drawable mDial;
+
+    private int mDialWidth;
+    private int mDialHeight;
+
+    private boolean mAttached;
+
+    private float mMinutes;
+    private float mHour;
+    private boolean mChanged;
+
+    public AnalogClock(Context context) {
+        this(context, null);
+    }
+
+    public AnalogClock(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public AnalogClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final Resources r = context.getResources();
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.AnalogClock, defStyleAttr, defStyleRes);
+
+        mDial = a.getDrawable(com.android.internal.R.styleable.AnalogClock_dial);
+        if (mDial == null) {
+            mDial = context.getDrawable(com.android.internal.R.drawable.clock_dial);
+        }
+
+        mHourHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_hour);
+        if (mHourHand == null) {
+            mHourHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_hour);
+        }
+
+        mMinuteHand = a.getDrawable(com.android.internal.R.styleable.AnalogClock_hand_minute);
+        if (mMinuteHand == null) {
+            mMinuteHand = context.getDrawable(com.android.internal.R.drawable.clock_hand_minute);
+        }
+
+        mCalendar = new Time();
+
+        mDialWidth = mDial.getIntrinsicWidth();
+        mDialHeight = mDial.getIntrinsicHeight();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (!mAttached) {
+            mAttached = true;
+            IntentFilter filter = new IntentFilter();
+
+            filter.addAction(Intent.ACTION_TIME_TICK);
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+
+            // OK, this is gross but needed. This class is supported by the
+            // remote views machanism and as a part of that the remote views
+            // can be inflated by a context for another user without the app
+            // having interact users permission - just for loading resources.
+            // For exmaple, when adding widgets from a user profile to the
+            // home screen. Therefore, we register the receiver as the current
+            // user not the one the context is for.
+            getContext().registerReceiverAsUser(mIntentReceiver,
+                    android.os.Process.myUserHandle(), filter, null, getHandler());
+        }
+
+        // NOTE: It's safe to do these after registering the receiver since the receiver always runs
+        // in the main thread, therefore the receiver can't run before this method returns.
+
+        // The time zone may have changed while the receiver wasn't registered, so update the Time
+        mCalendar = new Time();
+
+        // Make sure we update to the current time
+        onTimeChanged();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        if (mAttached) {
+            getContext().unregisterReceiver(mIntentReceiver);
+            mAttached = false;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSize =  MeasureSpec.getSize(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int heightSize =  MeasureSpec.getSize(heightMeasureSpec);
+
+        float hScale = 1.0f;
+        float vScale = 1.0f;
+
+        if (widthMode != MeasureSpec.UNSPECIFIED && widthSize < mDialWidth) {
+            hScale = (float) widthSize / (float) mDialWidth;
+        }
+
+        if (heightMode != MeasureSpec.UNSPECIFIED && heightSize < mDialHeight) {
+            vScale = (float )heightSize / (float) mDialHeight;
+        }
+
+        float scale = Math.min(hScale, vScale);
+
+        setMeasuredDimension(resolveSizeAndState((int) (mDialWidth * scale), widthMeasureSpec, 0),
+                resolveSizeAndState((int) (mDialHeight * scale), heightMeasureSpec, 0));
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+        mChanged = true;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        boolean changed = mChanged;
+        if (changed) {
+            mChanged = false;
+        }
+
+        int availableWidth = mRight - mLeft;
+        int availableHeight = mBottom - mTop;
+
+        int x = availableWidth / 2;
+        int y = availableHeight / 2;
+
+        final Drawable dial = mDial;
+        int w = dial.getIntrinsicWidth();
+        int h = dial.getIntrinsicHeight();
+
+        boolean scaled = false;
+
+        if (availableWidth < w || availableHeight < h) {
+            scaled = true;
+            float scale = Math.min((float) availableWidth / (float) w,
+                                   (float) availableHeight / (float) h);
+            canvas.save();
+            canvas.scale(scale, scale, x, y);
+        }
+
+        if (changed) {
+            dial.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+        }
+        dial.draw(canvas);
+
+        canvas.save();
+        canvas.rotate(mHour / 12.0f * 360.0f, x, y);
+        final Drawable hourHand = mHourHand;
+        if (changed) {
+            w = hourHand.getIntrinsicWidth();
+            h = hourHand.getIntrinsicHeight();
+            hourHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+        }
+        hourHand.draw(canvas);
+        canvas.restore();
+
+        canvas.save();
+        canvas.rotate(mMinutes / 60.0f * 360.0f, x, y);
+
+        final Drawable minuteHand = mMinuteHand;
+        if (changed) {
+            w = minuteHand.getIntrinsicWidth();
+            h = minuteHand.getIntrinsicHeight();
+            minuteHand.setBounds(x - (w / 2), y - (h / 2), x + (w / 2), y + (h / 2));
+        }
+        minuteHand.draw(canvas);
+        canvas.restore();
+
+        if (scaled) {
+            canvas.restore();
+        }
+    }
+
+    private void onTimeChanged() {
+        mCalendar.setToNow();
+
+        int hour = mCalendar.hour;
+        int minute = mCalendar.minute;
+        int second = mCalendar.second;
+
+        mMinutes = minute + second / 60.0f;
+        mHour = hour + mMinutes / 60.0f;
+        mChanged = true;
+
+        updateContentDescription(mCalendar);
+    }
+
+    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (intent.getAction().equals(Intent.ACTION_TIMEZONE_CHANGED)) {
+                String tz = intent.getStringExtra("time-zone");
+                mCalendar = new Time(TimeZone.getTimeZone(tz).getID());
+            }
+
+            onTimeChanged();
+
+            invalidate();
+        }
+    };
+
+    private void updateContentDescription(Time time) {
+        final int flags = DateUtils.FORMAT_SHOW_TIME | DateUtils.FORMAT_24HOUR;
+        String contentDescription = DateUtils.formatDateTime(mContext,
+                time.toMillis(false), flags);
+        setContentDescription(contentDescription);
+    }
+}
diff --git a/android/widget/AppSecurityPermissions.java b/android/widget/AppSecurityPermissions.java
new file mode 100644
index 0000000..6df76fa
--- /dev/null
+++ b/android/widget/AppSecurityPermissions.java
@@ -0,0 +1,639 @@
+/*
+**
+** Copyright 2007, 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 android.widget;
+
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.pm.PermissionGroupInfo;
+import android.content.pm.PermissionInfo;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.UserHandle;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * This class contains the SecurityPermissions view implementation.
+ * Initially the package's advanced or dangerous security permissions
+ * are displayed under categorized
+ * groups. Clicking on the additional permissions presents
+ * extended information consisting of all groups and permissions.
+ * To use this view define a LinearLayout or any ViewGroup and add this
+ * view by instantiating AppSecurityPermissions and invoking getPermissionsView.
+ *
+ * {@hide}
+ */
+public class AppSecurityPermissions {
+
+    public static final int WHICH_NEW = 1<<2;
+    public static final int WHICH_ALL = 0xffff;
+
+    private final static String TAG = "AppSecurityPermissions";
+    private final static boolean localLOGV = false;
+    private final Context mContext;
+    private final LayoutInflater mInflater;
+    private final PackageManager mPm;
+    private final Map<String, MyPermissionGroupInfo> mPermGroups
+            = new HashMap<String, MyPermissionGroupInfo>();
+    private final List<MyPermissionGroupInfo> mPermGroupsList
+            = new ArrayList<MyPermissionGroupInfo>();
+    private final PermissionGroupInfoComparator mPermGroupComparator =
+            new PermissionGroupInfoComparator();
+    private final PermissionInfoComparator mPermComparator = new PermissionInfoComparator();
+    private final List<MyPermissionInfo> mPermsList = new ArrayList<MyPermissionInfo>();
+    private final CharSequence mNewPermPrefix;
+    private String mPackageName;
+
+    /** @hide */
+    static class MyPermissionGroupInfo extends PermissionGroupInfo {
+        CharSequence mLabel;
+
+        final ArrayList<MyPermissionInfo> mNewPermissions = new ArrayList<MyPermissionInfo>();
+        final ArrayList<MyPermissionInfo> mAllPermissions = new ArrayList<MyPermissionInfo>();
+
+        MyPermissionGroupInfo(PermissionInfo perm) {
+            name = perm.packageName;
+            packageName = perm.packageName;
+        }
+
+        MyPermissionGroupInfo(PermissionGroupInfo info) {
+            super(info);
+        }
+
+        public Drawable loadGroupIcon(Context context, PackageManager pm) {
+            if (icon != 0) {
+                return loadUnbadgedIcon(pm);
+            } else {
+                return context.getDrawable(R.drawable.ic_perm_device_info);
+            }
+        }
+    }
+
+    /** @hide */
+    private static class MyPermissionInfo extends PermissionInfo {
+        CharSequence mLabel;
+
+        /**
+         * PackageInfo.requestedPermissionsFlags for the new package being installed.
+         */
+        int mNewReqFlags;
+
+        /**
+         * PackageInfo.requestedPermissionsFlags for the currently installed
+         * package, if it is installed.
+         */
+        int mExistingReqFlags;
+
+        /**
+         * True if this should be considered a new permission.
+         */
+        boolean mNew;
+
+        MyPermissionInfo(PermissionInfo info) {
+            super(info);
+        }
+    }
+
+    /** @hide */
+    public static class PermissionItemView extends LinearLayout implements View.OnClickListener {
+        MyPermissionGroupInfo mGroup;
+        MyPermissionInfo mPerm;
+        AlertDialog mDialog;
+        private boolean mShowRevokeUI = false;
+        private String mPackageName;
+
+        public PermissionItemView(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            setClickable(true);
+        }
+
+        public void setPermission(MyPermissionGroupInfo grp, MyPermissionInfo perm,
+                boolean first, CharSequence newPermPrefix, String packageName,
+                boolean showRevokeUI) {
+            mGroup = grp;
+            mPerm = perm;
+            mShowRevokeUI = showRevokeUI;
+            mPackageName = packageName;
+
+            ImageView permGrpIcon = findViewById(R.id.perm_icon);
+            TextView permNameView = findViewById(R.id.perm_name);
+
+            PackageManager pm = getContext().getPackageManager();
+            Drawable icon = null;
+            if (first) {
+                icon = grp.loadGroupIcon(getContext(), pm);
+            }
+            CharSequence label = perm.mLabel;
+            if (perm.mNew && newPermPrefix != null) {
+                // If this is a new permission, format it appropriately.
+                SpannableStringBuilder builder = new SpannableStringBuilder();
+                Parcel parcel = Parcel.obtain();
+                TextUtils.writeToParcel(newPermPrefix, parcel, 0);
+                parcel.setDataPosition(0);
+                CharSequence newStr = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(parcel);
+                parcel.recycle();
+                builder.append(newStr);
+                builder.append(label);
+                label = builder;
+            }
+
+            permGrpIcon.setImageDrawable(icon);
+            permNameView.setText(label);
+            setOnClickListener(this);
+            if (localLOGV) Log.i(TAG, "Made perm item " + perm.name
+                    + ": " + label + " in group " + grp.name);
+        }
+
+        @Override
+        public void onClick(View v) {
+            if (mGroup != null && mPerm != null) {
+                if (mDialog != null) {
+                    mDialog.dismiss();
+                }
+                PackageManager pm = getContext().getPackageManager();
+                AlertDialog.Builder builder = new AlertDialog.Builder(getContext());
+                builder.setTitle(mGroup.mLabel);
+                if (mPerm.descriptionRes != 0) {
+                    builder.setMessage(mPerm.loadDescription(pm));
+                } else {
+                    CharSequence appName;
+                    try {
+                        ApplicationInfo app = pm.getApplicationInfo(mPerm.packageName, 0);
+                        appName = app.loadLabel(pm);
+                    } catch (NameNotFoundException e) {
+                        appName = mPerm.packageName;
+                    }
+                    StringBuilder sbuilder = new StringBuilder(128);
+                    sbuilder.append(getContext().getString(
+                            R.string.perms_description_app, appName));
+                    sbuilder.append("\n\n");
+                    sbuilder.append(mPerm.name);
+                    builder.setMessage(sbuilder.toString());
+                }
+                builder.setCancelable(true);
+                builder.setIcon(mGroup.loadGroupIcon(getContext(), pm));
+                addRevokeUIIfNecessary(builder);
+                mDialog = builder.show();
+                mDialog.setCanceledOnTouchOutside(true);
+            }
+        }
+
+        @Override
+        protected void onDetachedFromWindow() {
+            super.onDetachedFromWindow();
+            if (mDialog != null) {
+                mDialog.dismiss();
+            }
+        }
+
+        private void addRevokeUIIfNecessary(AlertDialog.Builder builder) {
+            if (!mShowRevokeUI) {
+                return;
+            }
+
+            final boolean isRequired =
+                    ((mPerm.mExistingReqFlags & PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0);
+
+            if (isRequired) {
+                return;
+            }
+
+            DialogInterface.OnClickListener ocl = new DialogInterface.OnClickListener() {
+                @Override
+                public void onClick(DialogInterface dialog, int which) {
+                    PackageManager pm = getContext().getPackageManager();
+                    pm.revokeRuntimePermission(mPackageName, mPerm.name,
+                            new UserHandle(mContext.getUserId()));
+                    PermissionItemView.this.setVisibility(View.GONE);
+                }
+            };
+            builder.setNegativeButton(R.string.revoke, ocl);
+            builder.setPositiveButton(R.string.ok, null);
+        }
+    }
+
+    private AppSecurityPermissions(Context context) {
+        mContext = context;
+        mInflater = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mPm = mContext.getPackageManager();
+        // Pick up from framework resources instead.
+        mNewPermPrefix = mContext.getText(R.string.perms_new_perm_prefix);
+    }
+
+    public AppSecurityPermissions(Context context, String packageName) {
+        this(context);
+        mPackageName = packageName;
+        Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>();
+        PackageInfo pkgInfo;
+        try {
+            pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Couldn't retrieve permissions for package:"+packageName);
+            return;
+        }
+        // Extract all user permissions
+        if((pkgInfo.applicationInfo != null) && (pkgInfo.applicationInfo.uid != -1)) {
+            getAllUsedPermissions(pkgInfo.applicationInfo.uid, permSet);
+        }
+        mPermsList.addAll(permSet);
+        setPermissions(mPermsList);
+    }
+
+    public AppSecurityPermissions(Context context, PackageInfo info) {
+        this(context);
+        Set<MyPermissionInfo> permSet = new HashSet<MyPermissionInfo>();
+        if(info == null) {
+            return;
+        }
+        mPackageName = info.packageName;
+
+        // Convert to a PackageInfo
+        PackageInfo installedPkgInfo = null;
+        // Get requested permissions
+        if (info.requestedPermissions != null) {
+            try {
+                installedPkgInfo = mPm.getPackageInfo(info.packageName,
+                        PackageManager.GET_PERMISSIONS);
+            } catch (NameNotFoundException e) {
+            }
+            extractPerms(info, permSet, installedPkgInfo);
+        }
+        // Get permissions related to shared user if any
+        if (info.sharedUserId != null) {
+            int sharedUid;
+            try {
+                sharedUid = mPm.getUidForSharedUser(info.sharedUserId);
+                getAllUsedPermissions(sharedUid, permSet);
+            } catch (NameNotFoundException e) {
+                Log.w(TAG, "Couldn't retrieve shared user id for: " + info.packageName);
+            }
+        }
+        // Retrieve list of permissions
+        mPermsList.addAll(permSet);
+        setPermissions(mPermsList);
+    }
+
+    /**
+     * Utility to retrieve a view displaying a single permission.  This provides
+     * the old UI layout for permissions; it is only here for the device admin
+     * settings to continue to use.
+     */
+    public static View getPermissionItemView(Context context,
+            CharSequence grpName, CharSequence description, boolean dangerous) {
+        LayoutInflater inflater = (LayoutInflater)context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        Drawable icon = context.getDrawable(dangerous
+                ? R.drawable.ic_bullet_key_permission : R.drawable.ic_text_dot);
+        return getPermissionItemViewOld(context, inflater, grpName,
+                description, dangerous, icon);
+    }
+
+    private void getAllUsedPermissions(int sharedUid, Set<MyPermissionInfo> permSet) {
+        String sharedPkgList[] = mPm.getPackagesForUid(sharedUid);
+        if(sharedPkgList == null || (sharedPkgList.length == 0)) {
+            return;
+        }
+        for(String sharedPkg : sharedPkgList) {
+            getPermissionsForPackage(sharedPkg, permSet);
+        }
+    }
+
+    private void getPermissionsForPackage(String packageName, Set<MyPermissionInfo> permSet) {
+        try {
+            PackageInfo pkgInfo = mPm.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS);
+            extractPerms(pkgInfo, permSet, pkgInfo);
+        } catch (NameNotFoundException e) {
+            Log.w(TAG, "Couldn't retrieve permissions for package: " + packageName);
+        }
+    }
+
+    private void extractPerms(PackageInfo info, Set<MyPermissionInfo> permSet,
+            PackageInfo installedPkgInfo) {
+        String[] strList = info.requestedPermissions;
+        int[] flagsList = info.requestedPermissionsFlags;
+        if ((strList == null) || (strList.length == 0)) {
+            return;
+        }
+        for (int i=0; i<strList.length; i++) {
+            String permName = strList[i];
+            try {
+                PermissionInfo tmpPermInfo = mPm.getPermissionInfo(permName, 0);
+                if (tmpPermInfo == null) {
+                    continue;
+                }
+                int existingIndex = -1;
+                if (installedPkgInfo != null
+                        && installedPkgInfo.requestedPermissions != null) {
+                    for (int j=0; j<installedPkgInfo.requestedPermissions.length; j++) {
+                        if (permName.equals(installedPkgInfo.requestedPermissions[j])) {
+                            existingIndex = j;
+                            break;
+                        }
+                    }
+                }
+                final int existingFlags = existingIndex >= 0 ?
+                        installedPkgInfo.requestedPermissionsFlags[existingIndex] : 0;
+                if (!isDisplayablePermission(tmpPermInfo, flagsList[i], existingFlags)) {
+                    // This is not a permission that is interesting for the user
+                    // to see, so skip it.
+                    continue;
+                }
+                final String origGroupName = tmpPermInfo.group;
+                String groupName = origGroupName;
+                if (groupName == null) {
+                    groupName = tmpPermInfo.packageName;
+                    tmpPermInfo.group = groupName;
+                }
+                MyPermissionGroupInfo group = mPermGroups.get(groupName);
+                if (group == null) {
+                    PermissionGroupInfo grp = null;
+                    if (origGroupName != null) {
+                        grp = mPm.getPermissionGroupInfo(origGroupName, 0);
+                    }
+                    if (grp != null) {
+                        group = new MyPermissionGroupInfo(grp);
+                    } else {
+                        // We could be here either because the permission
+                        // didn't originally specify a group or the group it
+                        // gave couldn't be found.  In either case, we consider
+                        // its group to be the permission's package name.
+                        tmpPermInfo.group = tmpPermInfo.packageName;
+                        group = mPermGroups.get(tmpPermInfo.group);
+                        if (group == null) {
+                            group = new MyPermissionGroupInfo(tmpPermInfo);
+                        }
+                        group = new MyPermissionGroupInfo(tmpPermInfo);
+                    }
+                    mPermGroups.put(tmpPermInfo.group, group);
+                }
+                final boolean newPerm = installedPkgInfo != null
+                        && (existingFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) == 0;
+                MyPermissionInfo myPerm = new MyPermissionInfo(tmpPermInfo);
+                myPerm.mNewReqFlags = flagsList[i];
+                myPerm.mExistingReqFlags = existingFlags;
+                // This is a new permission if the app is already installed and
+                // doesn't currently hold this permission.
+                myPerm.mNew = newPerm;
+                permSet.add(myPerm);
+            } catch (NameNotFoundException e) {
+                Log.i(TAG, "Ignoring unknown permission:"+permName);
+            }
+        }
+    }
+
+    public int getPermissionCount() {
+        return getPermissionCount(WHICH_ALL);
+    }
+
+    private List<MyPermissionInfo> getPermissionList(MyPermissionGroupInfo grp, int which) {
+        if (which == WHICH_NEW) {
+            return grp.mNewPermissions;
+        } else {
+            return grp.mAllPermissions;
+        }
+    }
+
+    public int getPermissionCount(int which) {
+        int N = 0;
+        for (int i=0; i<mPermGroupsList.size(); i++) {
+            N += getPermissionList(mPermGroupsList.get(i), which).size();
+        }
+        return N;
+    }
+
+    public View getPermissionsView() {
+        return getPermissionsView(WHICH_ALL, false);
+    }
+
+    public View getPermissionsViewWithRevokeButtons() {
+        return getPermissionsView(WHICH_ALL, true);
+    }
+
+    public View getPermissionsView(int which) {
+        return getPermissionsView(which, false);
+    }
+
+    private View getPermissionsView(int which, boolean showRevokeUI) {
+        LinearLayout permsView = (LinearLayout) mInflater.inflate(R.layout.app_perms_summary, null);
+        LinearLayout displayList = permsView.findViewById(R.id.perms_list);
+        View noPermsView = permsView.findViewById(R.id.no_permissions);
+
+        displayPermissions(mPermGroupsList, displayList, which, showRevokeUI);
+        if (displayList.getChildCount() <= 0) {
+            noPermsView.setVisibility(View.VISIBLE);
+        }
+
+        return permsView;
+    }
+
+    /**
+     * Utility method that displays permissions from a map containing group name and
+     * list of permission descriptions.
+     */
+    private void displayPermissions(List<MyPermissionGroupInfo> groups,
+            LinearLayout permListView, int which, boolean showRevokeUI) {
+        permListView.removeAllViews();
+
+        int spacing = (int)(8*mContext.getResources().getDisplayMetrics().density);
+
+        for (int i=0; i<groups.size(); i++) {
+            MyPermissionGroupInfo grp = groups.get(i);
+            final List<MyPermissionInfo> perms = getPermissionList(grp, which);
+            for (int j=0; j<perms.size(); j++) {
+                MyPermissionInfo perm = perms.get(j);
+                View view = getPermissionItemView(grp, perm, j == 0,
+                        which != WHICH_NEW ? mNewPermPrefix : null, showRevokeUI);
+                LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT);
+                if (j == 0) {
+                    lp.topMargin = spacing;
+                }
+                if (j == grp.mAllPermissions.size()-1) {
+                    lp.bottomMargin = spacing;
+                }
+                if (permListView.getChildCount() == 0) {
+                    lp.topMargin *= 2;
+                }
+                permListView.addView(view, lp);
+            }
+        }
+    }
+
+    private PermissionItemView getPermissionItemView(MyPermissionGroupInfo grp,
+            MyPermissionInfo perm, boolean first, CharSequence newPermPrefix, boolean showRevokeUI) {
+        return getPermissionItemView(mContext, mInflater, grp, perm, first, newPermPrefix,
+                mPackageName, showRevokeUI);
+    }
+
+    private static PermissionItemView getPermissionItemView(Context context, LayoutInflater inflater,
+            MyPermissionGroupInfo grp, MyPermissionInfo perm, boolean first,
+            CharSequence newPermPrefix, String packageName, boolean showRevokeUI) {
+            PermissionItemView permView = (PermissionItemView)inflater.inflate(
+                (perm.flags & PermissionInfo.FLAG_COSTS_MONEY) != 0
+                        ? R.layout.app_permission_item_money : R.layout.app_permission_item,
+                null);
+        permView.setPermission(grp, perm, first, newPermPrefix, packageName, showRevokeUI);
+        return permView;
+    }
+
+    private static View getPermissionItemViewOld(Context context, LayoutInflater inflater,
+            CharSequence grpName, CharSequence permList, boolean dangerous, Drawable icon) {
+        View permView = inflater.inflate(R.layout.app_permission_item_old, null);
+
+        TextView permGrpView = permView.findViewById(R.id.permission_group);
+        TextView permDescView = permView.findViewById(R.id.permission_list);
+
+        ImageView imgView = (ImageView)permView.findViewById(R.id.perm_icon);
+        imgView.setImageDrawable(icon);
+        if(grpName != null) {
+            permGrpView.setText(grpName);
+            permDescView.setText(permList);
+        } else {
+            permGrpView.setText(permList);
+            permDescView.setVisibility(View.GONE);
+        }
+        return permView;
+    }
+
+    private boolean isDisplayablePermission(PermissionInfo pInfo, int newReqFlags,
+            int existingReqFlags) {
+        final int base = pInfo.protectionLevel & PermissionInfo.PROTECTION_MASK_BASE;
+        final boolean isNormal = (base == PermissionInfo.PROTECTION_NORMAL);
+
+        // We do not show normal permissions in the UI.
+        if (isNormal) {
+            return false;
+        }
+
+        final boolean isDangerous = (base == PermissionInfo.PROTECTION_DANGEROUS)
+                || ((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_PRE23) != 0);
+        final boolean isRequired =
+                ((newReqFlags&PackageInfo.REQUESTED_PERMISSION_REQUIRED) != 0);
+        final boolean isDevelopment =
+                ((pInfo.protectionLevel&PermissionInfo.PROTECTION_FLAG_DEVELOPMENT) != 0);
+        final boolean wasGranted =
+                ((existingReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
+        final boolean isGranted =
+                ((newReqFlags&PackageInfo.REQUESTED_PERMISSION_GRANTED) != 0);
+
+        // Dangerous and normal permissions are always shown to the user if the permission
+        // is required, or it was previously granted
+        if (isDangerous && (isRequired || wasGranted || isGranted)) {
+            return true;
+        }
+
+        // Development permissions are only shown to the user if they are already
+        // granted to the app -- if we are installing an app and they are not
+        // already granted, they will not be granted as part of the install.
+        if (isDevelopment && wasGranted) {
+            if (localLOGV) Log.i(TAG, "Special perm " + pInfo.name
+                    + ": protlevel=0x" + Integer.toHexString(pInfo.protectionLevel));
+            return true;
+        }
+        return false;
+    }
+
+    private static class PermissionGroupInfoComparator implements Comparator<MyPermissionGroupInfo> {
+        private final Collator sCollator = Collator.getInstance();
+        @Override
+        public final int compare(MyPermissionGroupInfo a, MyPermissionGroupInfo b) {
+            return sCollator.compare(a.mLabel, b.mLabel);
+        }
+    }
+
+    private static class PermissionInfoComparator implements Comparator<MyPermissionInfo> {
+        private final Collator sCollator = Collator.getInstance();
+        PermissionInfoComparator() {
+        }
+        public final int compare(MyPermissionInfo a, MyPermissionInfo b) {
+            return sCollator.compare(a.mLabel, b.mLabel);
+        }
+    }
+
+    private void addPermToList(List<MyPermissionInfo> permList,
+            MyPermissionInfo pInfo) {
+        if (pInfo.mLabel == null) {
+            pInfo.mLabel = pInfo.loadLabel(mPm);
+        }
+        int idx = Collections.binarySearch(permList, pInfo, mPermComparator);
+        if(localLOGV) Log.i(TAG, "idx="+idx+", list.size="+permList.size());
+        if (idx < 0) {
+            idx = -idx-1;
+            permList.add(idx, pInfo);
+        }
+    }
+
+    private void setPermissions(List<MyPermissionInfo> permList) {
+        if (permList != null) {
+            // First pass to group permissions
+            for (MyPermissionInfo pInfo : permList) {
+                if(localLOGV) Log.i(TAG, "Processing permission:"+pInfo.name);
+                if(!isDisplayablePermission(pInfo, pInfo.mNewReqFlags, pInfo.mExistingReqFlags)) {
+                    if(localLOGV) Log.i(TAG, "Permission:"+pInfo.name+" is not displayable");
+                    continue;
+                }
+                MyPermissionGroupInfo group = mPermGroups.get(pInfo.group);
+                if (group != null) {
+                    pInfo.mLabel = pInfo.loadLabel(mPm);
+                    addPermToList(group.mAllPermissions, pInfo);
+                    if (pInfo.mNew) {
+                        addPermToList(group.mNewPermissions, pInfo);
+                    }
+                }
+            }
+        }
+
+        for (MyPermissionGroupInfo pgrp : mPermGroups.values()) {
+            if (pgrp.labelRes != 0 || pgrp.nonLocalizedLabel != null) {
+                pgrp.mLabel = pgrp.loadLabel(mPm);
+            } else {
+                ApplicationInfo app;
+                try {
+                    app = mPm.getApplicationInfo(pgrp.packageName, 0);
+                    pgrp.mLabel = app.loadLabel(mPm);
+                } catch (NameNotFoundException e) {
+                    pgrp.mLabel = pgrp.loadLabel(mPm);
+                }
+            }
+            mPermGroupsList.add(pgrp);
+        }
+        Collections.sort(mPermGroupsList, mPermGroupComparator);
+    }
+}
diff --git a/android/widget/ArrayAdapter.java b/android/widget/ArrayAdapter.java
new file mode 100644
index 0000000..f18f217
--- /dev/null
+++ b/android/widget/ArrayAdapter.java
@@ -0,0 +1,623 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.ArrayRes;
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+/**
+ * You can use this adapter to provide views for an {@link AdapterView},
+ * Returns a view for each object in a collection of data objects you
+ * provide, and can be used with list-based user interface widgets such as
+ * {@link ListView} or {@link Spinner}.
+ * <p>
+ * By default, the array adapter creates a view by calling {@link Object#toString()} on each
+ * data object in the collection you provide, and places the result in a TextView.
+ * You may also customize what type of view is used for the data object in the collection.
+ * To customize what type of view is used for the data object,
+ * override {@link #getView(int, View, ViewGroup)}
+ * and inflate a view resource.
+ * For a code example, see
+ * the <a href="https://developer.android.com/samples/CustomChoiceList/index.html">
+ * CustomChoiceList</a> sample.
+ * </p>
+ * <p>
+ * For an example of using an array adapter with a ListView, see the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews">
+ * Adapter Views</a> guide.
+ * </p>
+ * <p>
+ * For an example of using an array adapter with a Spinner, see the
+ * <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.
+ * </p>
+ * <p class="note"><strong>Note:</strong>
+ * If you are considering using array adapter with a ListView, consider using
+ * {@link android.support.v7.widget.RecyclerView} instead.
+ * RecyclerView offers similar features with better performance and more flexibility than
+ * ListView provides.
+ * See the
+ * <a href="https://developer.android.com/guide/topics/ui/layout/recyclerview.html">
+ * Recycler View</a> guide.</p>
+ */
+public class ArrayAdapter<T> extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
+    /**
+     * Lock used to modify the content of {@link #mObjects}. Any write operation
+     * performed on the array should be synchronized on this lock. This lock is also
+     * used by the filter (see {@link #getFilter()} to make a synchronized copy of
+     * the original array of data.
+     */
+    private final Object mLock = new Object();
+
+    private final LayoutInflater mInflater;
+
+    private final Context mContext;
+
+    /**
+     * The resource indicating what views to inflate to display the content of this
+     * array adapter.
+     */
+    private final int mResource;
+
+    /**
+     * The resource indicating what views to inflate to display the content of this
+     * array adapter in a drop down widget.
+     */
+    private int mDropDownResource;
+
+    /**
+     * Contains the list of objects that represent the data of this ArrayAdapter.
+     * The content of this list is referred to as "the array" in the documentation.
+     */
+    private List<T> mObjects;
+
+    /**
+     * Indicates whether the contents of {@link #mObjects} came from static resources.
+     */
+    private boolean mObjectsFromResources;
+
+    /**
+     * If the inflated resource is not a TextView, {@code mFieldId} is used to find
+     * a TextView inside the inflated views hierarchy. This field must contain the
+     * identifier that matches the one defined in the resource file.
+     */
+    private int mFieldId = 0;
+
+    /**
+     * Indicates whether or not {@link #notifyDataSetChanged()} must be called whenever
+     * {@link #mObjects} is modified.
+     */
+    private boolean mNotifyOnChange = true;
+
+    // A copy of the original mObjects array, initialized from and then used instead as soon as
+    // the mFilter ArrayFilter is used. mObjects will then only contain the filtered values.
+    private ArrayList<T> mOriginalValues;
+    private ArrayFilter mFilter;
+
+    /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
+    private LayoutInflater mDropDownInflater;
+
+    /**
+     * Constructor
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a TextView to use when
+     *                 instantiating views.
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource) {
+        this(context, resource, 0, new ArrayList<>());
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a layout to use when
+     *                 instantiating views.
+     * @param textViewResourceId The id of the TextView within the layout resource to be populated
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+            @IdRes int textViewResourceId) {
+        this(context, resource, textViewResourceId, new ArrayList<>());
+    }
+
+    /**
+     * Constructor. This constructor will result in the underlying data collection being
+     * immutable, so methods such as {@link #clear()} will throw an exception.
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a TextView to use when
+     *                 instantiating views.
+     * @param objects The objects to represent in the ListView.
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource, @NonNull T[] objects) {
+        this(context, resource, 0, Arrays.asList(objects));
+    }
+
+    /**
+     * Constructor. This constructor will result in the underlying data collection being
+     * immutable, so methods such as {@link #clear()} will throw an exception.
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a layout to use when
+     *                 instantiating views.
+     * @param textViewResourceId The id of the TextView within the layout resource to be populated
+     * @param objects The objects to represent in the ListView.
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+            @IdRes int textViewResourceId, @NonNull T[] objects) {
+        this(context, resource, textViewResourceId, Arrays.asList(objects));
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a TextView to use when
+     *                 instantiating views.
+     * @param objects The objects to represent in the ListView.
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+            @NonNull List<T> objects) {
+        this(context, resource, 0, objects);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context The current context.
+     * @param resource The resource ID for a layout file containing a layout to use when
+     *                 instantiating views.
+     * @param textViewResourceId The id of the TextView within the layout resource to be populated
+     * @param objects The objects to represent in the ListView.
+     */
+    public ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+            @IdRes int textViewResourceId, @NonNull List<T> objects) {
+        this(context, resource, textViewResourceId, objects, false);
+    }
+
+    private ArrayAdapter(@NonNull Context context, @LayoutRes int resource,
+            @IdRes int textViewResourceId, @NonNull List<T> objects, boolean objsFromResources) {
+        mContext = context;
+        mInflater = LayoutInflater.from(context);
+        mResource = mDropDownResource = resource;
+        mObjects = objects;
+        mObjectsFromResources = objsFromResources;
+        mFieldId = textViewResourceId;
+    }
+
+    /**
+     * Adds the specified object at the end of the array.
+     *
+     * @param object The object to add at the end of the array.
+     * @throws UnsupportedOperationException if the underlying data collection is immutable
+     */
+    public void add(@Nullable T object) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                mOriginalValues.add(object);
+            } else {
+                mObjects.add(object);
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Adds the specified Collection at the end of the array.
+     *
+     * @param collection The Collection to add at the end of the array.
+     * @throws UnsupportedOperationException if the <tt>addAll</tt> operation
+     *         is not supported by this list
+     * @throws ClassCastException if the class of an element of the specified
+     *         collection prevents it from being added to this list
+     * @throws NullPointerException if the specified collection contains one
+     *         or more null elements and this list does not permit null
+     *         elements, or if the specified collection is null
+     * @throws IllegalArgumentException if some property of an element of the
+     *         specified collection prevents it from being added to this list
+     */
+    public void addAll(@NonNull Collection<? extends T> collection) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                mOriginalValues.addAll(collection);
+            } else {
+                mObjects.addAll(collection);
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Adds the specified items at the end of the array.
+     *
+     * @param items The items to add at the end of the array.
+     * @throws UnsupportedOperationException if the underlying data collection is immutable
+     */
+    public void addAll(T ... items) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                Collections.addAll(mOriginalValues, items);
+            } else {
+                Collections.addAll(mObjects, items);
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Inserts the specified object at the specified index in the array.
+     *
+     * @param object The object to insert into the array.
+     * @param index The index at which the object must be inserted.
+     * @throws UnsupportedOperationException if the underlying data collection is immutable
+     */
+    public void insert(@Nullable T object, int index) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                mOriginalValues.add(index, object);
+            } else {
+                mObjects.add(index, object);
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Removes the specified object from the array.
+     *
+     * @param object The object to remove.
+     * @throws UnsupportedOperationException if the underlying data collection is immutable
+     */
+    public void remove(@Nullable T object) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                mOriginalValues.remove(object);
+            } else {
+                mObjects.remove(object);
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Remove all elements from the list.
+     *
+     * @throws UnsupportedOperationException if the underlying data collection is immutable
+     */
+    public void clear() {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                mOriginalValues.clear();
+            } else {
+                mObjects.clear();
+            }
+            mObjectsFromResources = false;
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    /**
+     * Sorts the content of this adapter using the specified comparator.
+     *
+     * @param comparator The comparator used to sort the objects contained
+     *        in this adapter.
+     */
+    public void sort(@NonNull Comparator<? super T> comparator) {
+        synchronized (mLock) {
+            if (mOriginalValues != null) {
+                Collections.sort(mOriginalValues, comparator);
+            } else {
+                Collections.sort(mObjects, comparator);
+            }
+        }
+        if (mNotifyOnChange) notifyDataSetChanged();
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        super.notifyDataSetChanged();
+        mNotifyOnChange = true;
+    }
+
+    /**
+     * Control whether methods that change the list ({@link #add}, {@link #addAll(Collection)},
+     * {@link #addAll(Object[])}, {@link #insert}, {@link #remove}, {@link #clear},
+     * {@link #sort(Comparator)}) automatically call {@link #notifyDataSetChanged}.  If set to
+     * false, caller must manually call notifyDataSetChanged() to have the changes
+     * reflected in the attached view.
+     *
+     * The default is true, and calling notifyDataSetChanged()
+     * resets the flag to true.
+     *
+     * @param notifyOnChange if true, modifications to the list will
+     *                       automatically call {@link
+     *                       #notifyDataSetChanged}
+     */
+    public void setNotifyOnChange(boolean notifyOnChange) {
+        mNotifyOnChange = notifyOnChange;
+    }
+
+    /**
+     * Returns the context associated with this array adapter. The context is used
+     * to create views from the resource passed to the constructor.
+     *
+     * @return The Context associated with this adapter.
+     */
+    public @NonNull Context getContext() {
+        return mContext;
+    }
+
+    @Override
+    public int getCount() {
+        return mObjects.size();
+    }
+
+    @Override
+    public @Nullable T getItem(int position) {
+        return mObjects.get(position);
+    }
+
+    /**
+     * Returns the position of the specified item in the array.
+     *
+     * @param item The item to retrieve the position of.
+     *
+     * @return The position of the specified item.
+     */
+    public int getPosition(@Nullable T item) {
+        return mObjects.indexOf(item);
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public @NonNull View getView(int position, @Nullable View convertView,
+            @NonNull ViewGroup parent) {
+        return createViewFromResource(mInflater, position, convertView, parent, mResource);
+    }
+
+    private @NonNull View createViewFromResource(@NonNull LayoutInflater inflater, int position,
+            @Nullable View convertView, @NonNull ViewGroup parent, int resource) {
+        final View view;
+        final TextView text;
+
+        if (convertView == null) {
+            view = inflater.inflate(resource, parent, false);
+        } else {
+            view = convertView;
+        }
+
+        try {
+            if (mFieldId == 0) {
+                //  If no custom field is assigned, assume the whole resource is a TextView
+                text = (TextView) view;
+            } else {
+                //  Otherwise, find the TextView field within the layout
+                text = view.findViewById(mFieldId);
+
+                if (text == null) {
+                    throw new RuntimeException("Failed to find view with ID "
+                            + mContext.getResources().getResourceName(mFieldId)
+                            + " in item layout");
+                }
+            }
+        } catch (ClassCastException e) {
+            Log.e("ArrayAdapter", "You must supply a resource ID for a TextView");
+            throw new IllegalStateException(
+                    "ArrayAdapter requires the resource ID to be a TextView", e);
+        }
+
+        final T item = getItem(position);
+        if (item instanceof CharSequence) {
+            text.setText((CharSequence) item);
+        } else {
+            text.setText(item.toString());
+        }
+
+        return view;
+    }
+
+    /**
+     * <p>Sets the layout resource to create the drop down views.</p>
+     *
+     * @param resource the layout resource defining the drop down views
+     * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
+     */
+    public void setDropDownViewResource(@LayoutRes int resource) {
+        this.mDropDownResource = resource;
+    }
+
+    /**
+     * Sets the {@link Resources.Theme} against which drop-down views are
+     * inflated.
+     * <p>
+     * By default, drop-down views are inflated against the theme of the
+     * {@link Context} passed to the adapter's constructor.
+     *
+     * @param theme the theme against which to inflate drop-down views or
+     *              {@code null} to use the theme from the adapter's context
+     * @see #getDropDownView(int, View, ViewGroup)
+     */
+    @Override
+    public void setDropDownViewTheme(@Nullable Resources.Theme theme) {
+        if (theme == null) {
+            mDropDownInflater = null;
+        } else if (theme == mInflater.getContext().getTheme()) {
+            mDropDownInflater = mInflater;
+        } else {
+            final Context context = new ContextThemeWrapper(mContext, theme);
+            mDropDownInflater = LayoutInflater.from(context);
+        }
+    }
+
+    @Override
+    public @Nullable Resources.Theme getDropDownViewTheme() {
+        return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
+    }
+
+    @Override
+    public View getDropDownView(int position, @Nullable View convertView,
+            @NonNull ViewGroup parent) {
+        final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
+        return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
+    }
+
+    /**
+     * Creates a new ArrayAdapter from external resources. The content of the array is
+     * obtained through {@link android.content.res.Resources#getTextArray(int)}.
+     *
+     * @param context The application's environment.
+     * @param textArrayResId The identifier of the array to use as the data source.
+     * @param textViewResId The identifier of the layout used to create views.
+     *
+     * @return An ArrayAdapter<CharSequence>.
+     */
+    public static @NonNull ArrayAdapter<CharSequence> createFromResource(@NonNull Context context,
+            @ArrayRes int textArrayResId, @LayoutRes int textViewResId) {
+        final CharSequence[] strings = context.getResources().getTextArray(textArrayResId);
+        return new ArrayAdapter<>(context, textViewResId, 0, Arrays.asList(strings), true);
+    }
+
+    @Override
+    public @NonNull Filter getFilter() {
+        if (mFilter == null) {
+            mFilter = new ArrayFilter();
+        }
+        return mFilter;
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @return values from the string array used by {@link #createFromResource(Context, int, int)},
+     * or {@code null} if object was created otherwsie or if contents were dynamically changed after
+     * creation.
+     */
+    @Override
+    public CharSequence[] getAutofillOptions() {
+        // First check if app developer explicitly set them.
+        final CharSequence[] explicitOptions = super.getAutofillOptions();
+        if (explicitOptions != null) {
+            return explicitOptions;
+        }
+
+        // Otherwise, only return options that came from static resources.
+        if (!mObjectsFromResources || mObjects == null || mObjects.isEmpty()) {
+            return null;
+        }
+        final int size = mObjects.size();
+        final CharSequence[] options = new CharSequence[size];
+        mObjects.toArray(options);
+        return options;
+    }
+
+    /**
+     * <p>An array filter constrains the content of the array adapter with
+     * a prefix. Each item that does not start with the supplied prefix
+     * is removed from the list.</p>
+     */
+    private class ArrayFilter extends Filter {
+        @Override
+        protected FilterResults performFiltering(CharSequence prefix) {
+            final FilterResults results = new FilterResults();
+
+            if (mOriginalValues == null) {
+                synchronized (mLock) {
+                    mOriginalValues = new ArrayList<>(mObjects);
+                }
+            }
+
+            if (prefix == null || prefix.length() == 0) {
+                final ArrayList<T> list;
+                synchronized (mLock) {
+                    list = new ArrayList<>(mOriginalValues);
+                }
+                results.values = list;
+                results.count = list.size();
+            } else {
+                final String prefixString = prefix.toString().toLowerCase();
+
+                final ArrayList<T> values;
+                synchronized (mLock) {
+                    values = new ArrayList<>(mOriginalValues);
+                }
+
+                final int count = values.size();
+                final ArrayList<T> newValues = new ArrayList<>();
+
+                for (int i = 0; i < count; i++) {
+                    final T value = values.get(i);
+                    final String valueText = value.toString().toLowerCase();
+
+                    // First match against the whole, non-splitted value
+                    if (valueText.startsWith(prefixString)) {
+                        newValues.add(value);
+                    } else {
+                        final String[] words = valueText.split(" ");
+                        for (String word : words) {
+                            if (word.startsWith(prefixString)) {
+                                newValues.add(value);
+                                break;
+                            }
+                        }
+                    }
+                }
+
+                results.values = newValues;
+                results.count = newValues.size();
+            }
+
+            return results;
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+            //noinspection unchecked
+            mObjects = (List<T>) results.values;
+            if (results.count > 0) {
+                notifyDataSetChanged();
+            } else {
+                notifyDataSetInvalidated();
+            }
+        }
+    }
+}
diff --git a/android/widget/AutoCompleteTextView.java b/android/widget/AutoCompleteTextView.java
new file mode 100644
index 0000000..49741d4
--- /dev/null
+++ b/android/widget/AutoCompleteTextView.java
@@ -0,0 +1,1415 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.content.Context;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.WindowManager;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import java.lang.ref.WeakReference;
+
+/**
+ * <p>An editable text view that shows completion suggestions automatically
+ * while the user is typing. The list of suggestions is displayed in a drop
+ * down menu from which the user can choose an item to replace the content
+ * of the edit box with.</p>
+ *
+ * <p>The drop down can be dismissed at any time by pressing the back key or,
+ * if no item is selected in the drop down, by pressing the enter/dpad center
+ * key.</p>
+ *
+ * <p>The list of suggestions is obtained from a data adapter and appears
+ * only after a given number of characters defined by
+ * {@link #getThreshold() the threshold}.</p>
+ *
+ * <p>The following code snippet shows how to create a text view which suggests
+ * various countries names while the user is typing:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CountriesActivity extends Activity {
+ *     protected void onCreate(Bundle icicle) {
+ *         super.onCreate(icicle);
+ *         setContentView(R.layout.countries);
+ *
+ *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
+ *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ *         AutoCompleteTextView textView = (AutoCompleteTextView)
+ *                 findViewById(R.id.countries_list);
+ *         textView.setAdapter(adapter);
+ *     }
+ *
+ *     private static final String[] COUNTRIES = new String[] {
+ *         "Belgium", "France", "Italy", "Germany", "Spain"
+ *     };
+ * }
+ * </pre>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
+ * guide.</p>
+ *
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+ * @attr ref android.R.styleable#AutoCompleteTextView_completionHintView
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownSelector
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+ * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+public class AutoCompleteTextView extends EditText implements Filter.FilterListener {
+    static final boolean DEBUG = false;
+    static final String TAG = "AutoCompleteTextView";
+
+    static final int EXPAND_MAX = 3;
+
+    /** Context used to inflate the popup window or dialog. */
+    private final Context mPopupContext;
+
+    private final ListPopupWindow mPopup;
+    private final PassThroughClickListener mPassThroughClickListener;
+
+    private CharSequence mHintText;
+    private TextView mHintView;
+    private int mHintResource;
+
+    private ListAdapter mAdapter;
+    private Filter mFilter;
+    private int mThreshold;
+
+    private int mDropDownAnchorId;
+
+    private AdapterView.OnItemClickListener mItemClickListener;
+    private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+    private boolean mDropDownDismissedOnCompletion = true;
+
+    private int mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+    private boolean mOpenBefore;
+
+    private Validator mValidator = null;
+
+    // Set to true when text is set directly and no filtering shall be performed
+    private boolean mBlockCompletion;
+
+    // When set, an update in the underlying adapter will update the result list popup.
+    // Set to false when the list is hidden to prevent asynchronous updates to popup the list again.
+    private boolean mPopupCanBeUpdated = true;
+
+    private PopupDataSetObserver mObserver;
+
+    /**
+     * Constructs a new auto-complete text view with the given context's theme.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     */
+    public AutoCompleteTextView(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Constructs a new auto-complete text view with the given context's theme
+     * and the supplied attribute set.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     */
+    public AutoCompleteTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.autoCompleteTextViewStyle);
+    }
+
+    /**
+     * Constructs a new auto-complete text view with the given context's theme,
+     * the supplied attribute set, and default style attribute.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     */
+    public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * Constructs a new auto-complete text view with the given context's theme,
+     * the supplied attribute set, and default styles.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *                    supplies default values for the view, used only if
+     *                    defStyleAttr is 0 or can not be found in the theme.
+     *                    Can be 0 to not look for defaults.
+     */
+    public AutoCompleteTextView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        this(context, attrs, defStyleAttr, defStyleRes, null);
+    }
+
+    /**
+     * Constructs a new auto-complete text view with the given context, the
+     * supplied attribute set, default styles, and the theme against which the
+     * completion popup should be inflated.
+     *
+     * @param context The context against which the view is inflated, which
+     *                provides access to the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *                    supplies default values for the view, used only if
+     *                    defStyleAttr is 0 or can not be found in the theme.
+     *                    Can be 0 to not look for defaults.
+     * @param popupTheme The theme against which the completion popup window
+     *                   should be inflated. May be {@code null} to use the
+     *                   view theme. If set, this will override any value
+     *                   specified by
+     *                   {@link android.R.styleable#AutoCompleteTextView_popupTheme}.
+     */
+    public AutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes, Theme popupTheme) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes);
+
+        if (popupTheme != null) {
+            mPopupContext = new ContextThemeWrapper(context, popupTheme);
+        } else {
+            final int popupThemeResId = a.getResourceId(
+                    R.styleable.AutoCompleteTextView_popupTheme, 0);
+            if (popupThemeResId != 0) {
+                mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
+            } else {
+                mPopupContext = context;
+            }
+        }
+
+        // Load attributes used within the popup against the popup context.
+        final TypedArray pa;
+        if (mPopupContext != context) {
+            pa = mPopupContext.obtainStyledAttributes(
+                    attrs, R.styleable.AutoCompleteTextView, defStyleAttr, defStyleRes);
+        } else {
+            pa = a;
+        }
+
+        final Drawable popupListSelector = pa.getDrawable(
+                R.styleable.AutoCompleteTextView_dropDownSelector);
+        final int popupWidth = pa.getLayoutDimension(
+                R.styleable.AutoCompleteTextView_dropDownWidth, LayoutParams.WRAP_CONTENT);
+        final int popupHeight = pa.getLayoutDimension(
+                R.styleable.AutoCompleteTextView_dropDownHeight, LayoutParams.WRAP_CONTENT);
+        final int popupHintLayoutResId = pa.getResourceId(
+                R.styleable.AutoCompleteTextView_completionHintView, R.layout.simple_dropdown_hint);
+        final CharSequence popupHintText = pa.getText(
+                R.styleable.AutoCompleteTextView_completionHint);
+
+        if (pa != a) {
+            pa.recycle();
+        }
+
+        mPopup = new ListPopupWindow(mPopupContext, attrs, defStyleAttr, defStyleRes);
+        mPopup.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_ADJUST_RESIZE);
+        mPopup.setPromptPosition(ListPopupWindow.POSITION_PROMPT_BELOW);
+        mPopup.setListSelector(popupListSelector);
+        mPopup.setOnItemClickListener(new DropDownItemClickListener());
+
+        // For dropdown width, the developer can specify a specific width, or
+        // MATCH_PARENT (for full screen width), or WRAP_CONTENT (to match the
+        // width of the anchored view).
+        mPopup.setWidth(popupWidth);
+        mPopup.setHeight(popupHeight);
+
+        // Completion hint must be set after specifying hint layout.
+        mHintResource = popupHintLayoutResId;
+        setCompletionHint(popupHintText);
+
+        // Get the anchor's id now, but the view won't be ready, so wait to
+        // actually get the view and store it in mDropDownAnchorView lazily in
+        // getDropDownAnchorView later. Defaults to NO_ID, in which case the
+        // getDropDownAnchorView method will simply return this TextView, as a
+        // default anchoring point.
+        mDropDownAnchorId = a.getResourceId(
+                R.styleable.AutoCompleteTextView_dropDownAnchor, View.NO_ID);
+
+        mThreshold = a.getInt(R.styleable.AutoCompleteTextView_completionThreshold, 2);
+
+        a.recycle();
+
+        // Always turn on the auto complete input type flag, since it
+        // makes no sense to use this widget without it.
+        int inputType = getInputType();
+        if ((inputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+            inputType |= EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+            setRawInputType(inputType);
+        }
+
+        setFocusable(true);
+
+        addTextChangedListener(new MyWatcher());
+
+        mPassThroughClickListener = new PassThroughClickListener();
+        super.setOnClickListener(mPassThroughClickListener);
+    }
+
+    @Override
+    public void setOnClickListener(OnClickListener listener) {
+        mPassThroughClickListener.mWrapped = listener;
+    }
+
+    /**
+     * Private hook into the on click event, dispatched from {@link PassThroughClickListener}
+     */
+    private void onClickImpl() {
+        // If the dropdown is showing, bring the keyboard to the front
+        // when the user touches the text field.
+        if (isPopupShowing()) {
+            ensureImeVisible(true);
+        }
+    }
+
+    /**
+     * <p>Sets the optional hint text that is displayed at the bottom of the
+     * the matching list.  This can be used as a cue to the user on how to
+     * best use the list, or to provide extra information.</p>
+     *
+     * @param hint the text to be displayed to the user
+     *
+     * @see #getCompletionHint()
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+     */
+    public void setCompletionHint(CharSequence hint) {
+        mHintText = hint;
+        if (hint != null) {
+            if (mHintView == null) {
+                final TextView hintView = (TextView) LayoutInflater.from(mPopupContext).inflate(
+                        mHintResource, null).findViewById(R.id.text1);
+                hintView.setText(mHintText);
+                mHintView = hintView;
+                mPopup.setPromptView(hintView);
+            } else {
+                mHintView.setText(hint);
+            }
+        } else {
+            mPopup.setPromptView(null);
+            mHintView = null;
+        }
+    }
+
+    /**
+     * Gets the optional hint text displayed at the bottom of the the matching list.
+     *
+     * @return The hint text, if any
+     *
+     * @see #setCompletionHint(CharSequence)
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_completionHint
+     */
+    public CharSequence getCompletionHint() {
+        return mHintText;
+    }
+
+    /**
+     * <p>Returns the current width for the auto-complete drop down list. This can
+     * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+     * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
+     *
+     * @return the width for the drop down list
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+     */
+    public int getDropDownWidth() {
+        return mPopup.getWidth();
+    }
+
+    /**
+     * <p>Sets the current width for the auto-complete drop down list. This can
+     * be a fixed width, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill the screen, or
+     * {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the width of its anchor view.</p>
+     *
+     * @param width the width to use
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownWidth
+     */
+    public void setDropDownWidth(int width) {
+        mPopup.setWidth(width);
+    }
+
+    /**
+     * <p>Returns the current height for the auto-complete drop down list. This can
+     * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+     * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+     * of the drop down's content.</p>
+     *
+     * @return the height for the drop down list
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+     */
+    public int getDropDownHeight() {
+        return mPopup.getHeight();
+    }
+
+    /**
+     * <p>Sets the current height for the auto-complete drop down list. This can
+     * be a fixed height, or {@link ViewGroup.LayoutParams#MATCH_PARENT} to fill
+     * the screen, or {@link ViewGroup.LayoutParams#WRAP_CONTENT} to fit the height
+     * of the drop down's content.</p>
+     *
+     * @param height the height to use
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownHeight
+     */
+    public void setDropDownHeight(int height) {
+        mPopup.setHeight(height);
+    }
+
+    /**
+     * <p>Returns the id for the view that the auto-complete drop down list is anchored to.</p>
+     *
+     * @return the view's id, or {@link View#NO_ID} if none specified
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+     */
+    public int getDropDownAnchor() {
+        return mDropDownAnchorId;
+    }
+
+    /**
+     * <p>Sets the view to which the auto-complete drop down list should anchor. The view
+     * corresponding to this id will not be loaded until the next time it is needed to avoid
+     * loading a view which is not yet instantiated.</p>
+     *
+     * @param id the id to anchor the drop down list view to
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_dropDownAnchor
+     */
+    public void setDropDownAnchor(int id) {
+        mDropDownAnchorId = id;
+        mPopup.setAnchorView(null);
+    }
+
+    /**
+     * <p>Gets the background of the auto-complete drop-down list.</p>
+     *
+     * @return the background drawable
+     *
+     * @attr ref android.R.styleable#PopupWindow_popupBackground
+     */
+    public Drawable getDropDownBackground() {
+        return mPopup.getBackground();
+    }
+
+    /**
+     * <p>Sets the background of the auto-complete drop-down list.</p>
+     *
+     * @param d the drawable to set as the background
+     *
+     * @attr ref android.R.styleable#PopupWindow_popupBackground
+     */
+    public void setDropDownBackgroundDrawable(Drawable d) {
+        mPopup.setBackgroundDrawable(d);
+    }
+
+    /**
+     * <p>Sets the background of the auto-complete drop-down list.</p>
+     *
+     * @param id the id of the drawable to set as the background
+     *
+     * @attr ref android.R.styleable#PopupWindow_popupBackground
+     */
+    public void setDropDownBackgroundResource(@DrawableRes int id) {
+        mPopup.setBackgroundDrawable(getContext().getDrawable(id));
+    }
+
+    /**
+     * <p>Sets the vertical offset used for the auto-complete drop-down list.</p>
+     *
+     * @param offset the vertical offset
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+     */
+    public void setDropDownVerticalOffset(int offset) {
+        mPopup.setVerticalOffset(offset);
+    }
+
+    /**
+     * <p>Gets the vertical offset used for the auto-complete drop-down list.</p>
+     *
+     * @return the vertical offset
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+     */
+    public int getDropDownVerticalOffset() {
+        return mPopup.getVerticalOffset();
+    }
+
+    /**
+     * <p>Sets the horizontal offset used for the auto-complete drop-down list.</p>
+     *
+     * @param offset the horizontal offset
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+     */
+    public void setDropDownHorizontalOffset(int offset) {
+        mPopup.setHorizontalOffset(offset);
+    }
+
+    /**
+     * <p>Gets the horizontal offset used for the auto-complete drop-down list.</p>
+     *
+     * @return the horizontal offset
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+     */
+    public int getDropDownHorizontalOffset() {
+        return mPopup.getHorizontalOffset();
+    }
+
+     /**
+     * <p>Sets the animation style of the auto-complete drop-down list.</p>
+     *
+     * <p>If the drop-down is showing, calling this method will take effect only
+     * the next time the drop-down is shown.</p>
+     *
+     * @param animationStyle animation style to use when the drop-down appears
+     *      and disappears.  Set to -1 for the default animation, 0 for no
+     *      animation, or a resource identifier for an explicit animation.
+     *
+     * @hide Pending API council approval
+     */
+    public void setDropDownAnimationStyle(int animationStyle) {
+        mPopup.setAnimationStyle(animationStyle);
+    }
+
+    /**
+     * <p>Returns the animation style that is used when the drop-down list appears and disappears
+     * </p>
+     *
+     * @return the animation style that is used when the drop-down list appears and disappears
+     *
+     * @hide Pending API council approval
+     */
+    public int getDropDownAnimationStyle() {
+        return mPopup.getAnimationStyle();
+    }
+
+    /**
+     * @return Whether the drop-down is visible as long as there is {@link #enoughToFilter()}
+     *
+     * @hide Pending API council approval
+     */
+    public boolean isDropDownAlwaysVisible() {
+        return mPopup.isDropDownAlwaysVisible();
+    }
+
+    /**
+     * Sets whether the drop-down should remain visible as long as there is there is
+     * {@link #enoughToFilter()}.  This is useful if an unknown number of results are expected
+     * to show up in the adapter sometime in the future.
+     *
+     * The drop-down will occupy the entire screen below {@link #getDropDownAnchor} regardless
+     * of the size or content of the list.  {@link #getDropDownBackground()} will fill any space
+     * that is not used by the list.
+     *
+     * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+     *
+     * @hide Pending API council approval
+     */
+    public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+        mPopup.setDropDownAlwaysVisible(dropDownAlwaysVisible);
+    }
+
+    /**
+     * Checks whether the drop-down is dismissed when a suggestion is clicked.
+     *
+     * @hide Pending API council approval
+     */
+    public boolean isDropDownDismissedOnCompletion() {
+        return mDropDownDismissedOnCompletion;
+    }
+
+    /**
+     * Sets whether the drop-down is dismissed when a suggestion is clicked. This is
+     * true by default.
+     *
+     * @param dropDownDismissedOnCompletion Whether to dismiss the drop-down.
+     *
+     * @hide Pending API council approval
+     */
+    public void setDropDownDismissedOnCompletion(boolean dropDownDismissedOnCompletion) {
+        mDropDownDismissedOnCompletion = dropDownDismissedOnCompletion;
+    }
+
+    /**
+     * <p>Returns the number of characters the user must type before the drop
+     * down list is shown.</p>
+     *
+     * @return the minimum number of characters to type to show the drop down
+     *
+     * @see #setThreshold(int)
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+     */
+    public int getThreshold() {
+        return mThreshold;
+    }
+
+    /**
+     * <p>Specifies the minimum number of characters the user has to type in the
+     * edit box before the drop down list is shown.</p>
+     *
+     * <p>When <code>threshold</code> is less than or equals 0, a threshold of
+     * 1 is applied.</p>
+     *
+     * @param threshold the number of characters to type before the drop down
+     *                  is shown
+     *
+     * @see #getThreshold()
+     *
+     * @attr ref android.R.styleable#AutoCompleteTextView_completionThreshold
+     */
+    public void setThreshold(int threshold) {
+        if (threshold <= 0) {
+            threshold = 1;
+        }
+
+        mThreshold = threshold;
+    }
+
+    /**
+     * <p>Sets the listener that will be notified when the user clicks an item
+     * in the drop down list.</p>
+     *
+     * @param l the item click listener
+     */
+    public void setOnItemClickListener(AdapterView.OnItemClickListener l) {
+        mItemClickListener = l;
+    }
+
+    /**
+     * <p>Sets the listener that will be notified when the user selects an item
+     * in the drop down list.</p>
+     *
+     * @param l the item selected listener
+     */
+    public void setOnItemSelectedListener(AdapterView.OnItemSelectedListener l) {
+        mItemSelectedListener = l;
+    }
+
+    /**
+     * <p>Returns the listener that is notified whenever the user clicks an item
+     * in the drop down list.</p>
+     *
+     * @return the item click listener
+     *
+     * @deprecated Use {@link #getOnItemClickListener()} intead
+     */
+    @Deprecated
+    public AdapterView.OnItemClickListener getItemClickListener() {
+        return mItemClickListener;
+    }
+
+    /**
+     * <p>Returns the listener that is notified whenever the user selects an
+     * item in the drop down list.</p>
+     *
+     * @return the item selected listener
+     *
+     * @deprecated Use {@link #getOnItemSelectedListener()} intead
+     */
+    @Deprecated
+    public AdapterView.OnItemSelectedListener getItemSelectedListener() {
+        return mItemSelectedListener;
+    }
+
+    /**
+     * <p>Returns the listener that is notified whenever the user clicks an item
+     * in the drop down list.</p>
+     *
+     * @return the item click listener
+     */
+    public AdapterView.OnItemClickListener getOnItemClickListener() {
+        return mItemClickListener;
+    }
+
+    /**
+     * <p>Returns the listener that is notified whenever the user selects an
+     * item in the drop down list.</p>
+     *
+     * @return the item selected listener
+     */
+    public AdapterView.OnItemSelectedListener getOnItemSelectedListener() {
+        return mItemSelectedListener;
+    }
+
+    /**
+     * Set a listener that will be invoked whenever the AutoCompleteTextView's
+     * list of completions is dismissed.
+     * @param dismissListener Listener to invoke when completions are dismissed
+     */
+    public void setOnDismissListener(final OnDismissListener dismissListener) {
+        PopupWindow.OnDismissListener wrappedListener = null;
+        if (dismissListener != null) {
+            wrappedListener = new PopupWindow.OnDismissListener() {
+                @Override public void onDismiss() {
+                    dismissListener.onDismiss();
+                }
+            };
+        }
+        mPopup.setOnDismissListener(wrappedListener);
+    }
+
+    /**
+     * <p>Returns a filterable list adapter used for auto completion.</p>
+     *
+     * @return a data adapter used for auto completion
+     */
+    public ListAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    /**
+     * <p>Changes the list of data used for auto completion. The provided list
+     * must be a filterable list adapter.</p>
+     *
+     * <p>The caller is still responsible for managing any resources used by the adapter.
+     * Notably, when the AutoCompleteTextView is closed or released, the adapter is not notified.
+     * A common case is the use of {@link android.widget.CursorAdapter}, which
+     * contains a {@link android.database.Cursor} that must be closed.  This can be done
+     * automatically (see
+     * {@link android.app.Activity#startManagingCursor(android.database.Cursor)
+     * startManagingCursor()}),
+     * or by manually closing the cursor when the AutoCompleteTextView is dismissed.</p>
+     *
+     * @param adapter the adapter holding the auto completion data
+     *
+     * @see #getAdapter()
+     * @see android.widget.Filterable
+     * @see android.widget.ListAdapter
+     */
+    public <T extends ListAdapter & Filterable> void setAdapter(T adapter) {
+        if (mObserver == null) {
+            mObserver = new PopupDataSetObserver(this);
+        } else if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+        }
+        mAdapter = adapter;
+        if (mAdapter != null) {
+            //noinspection unchecked
+            mFilter = ((Filterable) mAdapter).getFilter();
+            adapter.registerDataSetObserver(mObserver);
+        } else {
+            mFilter = null;
+        }
+
+        mPopup.setAdapter(mAdapter);
+    }
+
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && isPopupShowing()
+                && !mPopup.isDropDownAlwaysVisible()) {
+            // special case for the back key, we do not even try to send it
+            // to the drop down list but instead, consume it immediately
+            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                KeyEvent.DispatcherState state = getKeyDispatcherState();
+                if (state != null) {
+                    state.startTracking(event, this);
+                }
+                return true;
+            } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                KeyEvent.DispatcherState state = getKeyDispatcherState();
+                if (state != null) {
+                    state.handleUpEvent(event);
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    dismissDropDown();
+                    return true;
+                }
+            }
+        }
+        return super.onKeyPreIme(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        boolean consumed = mPopup.onKeyUp(keyCode, event);
+        if (consumed) {
+            switch (keyCode) {
+            // if the list accepts the key events and the key event
+            // was a click, the text view gets the selected item
+            // from the drop down as its content
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers()) {
+                    performCompletion();
+                }
+                return true;
+            }
+        }
+
+        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+            performCompletion();
+            return true;
+        }
+
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mPopup.onKeyDown(keyCode, event)) {
+            return true;
+        }
+
+        if (!isPopupShowing()) {
+            switch(keyCode) {
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (event.hasNoModifiers()) {
+                    performValidation();
+                }
+            }
+        }
+
+        if (isPopupShowing() && keyCode == KeyEvent.KEYCODE_TAB && event.hasNoModifiers()) {
+            return true;
+        }
+
+        mLastKeyCode = keyCode;
+        boolean handled = super.onKeyDown(keyCode, event);
+        mLastKeyCode = KeyEvent.KEYCODE_UNKNOWN;
+
+        if (handled && isPopupShowing()) {
+            clearListSelection();
+        }
+
+        return handled;
+    }
+
+    /**
+     * Returns <code>true</code> if the amount of text in the field meets
+     * or exceeds the {@link #getThreshold} requirement.  You can override
+     * this to impose a different standard for when filtering will be
+     * triggered.
+     */
+    public boolean enoughToFilter() {
+        if (DEBUG) Log.v(TAG, "Enough to filter: len=" + getText().length()
+                + " threshold=" + mThreshold);
+        return getText().length() >= mThreshold;
+    }
+
+    /**
+     * This is used to watch for edits to the text view.  Note that we call
+     * to methods on the auto complete text view class so that we can access
+     * private vars without going through thunks.
+     */
+    private class MyWatcher implements TextWatcher {
+        public void afterTextChanged(Editable s) {
+            doAfterTextChanged();
+        }
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            doBeforeTextChanged();
+        }
+        public void onTextChanged(CharSequence s, int start, int before, int count) {
+        }
+    }
+
+    void doBeforeTextChanged() {
+        if (mBlockCompletion) return;
+
+        // when text is changed, inserted or deleted, we attempt to show
+        // the drop down
+        mOpenBefore = isPopupShowing();
+        if (DEBUG) Log.v(TAG, "before text changed: open=" + mOpenBefore);
+    }
+
+    void doAfterTextChanged() {
+        if (mBlockCompletion) return;
+
+        // if the list was open before the keystroke, but closed afterwards,
+        // then something in the keystroke processing (an input filter perhaps)
+        // called performCompletion() and we shouldn't do any more processing.
+        if (DEBUG) Log.v(TAG, "after text changed: openBefore=" + mOpenBefore
+                + " open=" + isPopupShowing());
+        if (mOpenBefore && !isPopupShowing()) {
+            return;
+        }
+
+        // the drop down is shown only when a minimum number of characters
+        // was typed in the text view
+        if (enoughToFilter()) {
+            if (mFilter != null) {
+                mPopupCanBeUpdated = true;
+                performFiltering(getText(), mLastKeyCode);
+            }
+        } else {
+            // drop down is automatically dismissed when enough characters
+            // are deleted from the text view
+            if (!mPopup.isDropDownAlwaysVisible()) {
+                dismissDropDown();
+            }
+            if (mFilter != null) {
+                mFilter.filter(null);
+            }
+        }
+    }
+
+    /**
+     * <p>Indicates whether the popup menu is showing.</p>
+     *
+     * @return true if the popup menu is showing, false otherwise
+     */
+    public boolean isPopupShowing() {
+        return mPopup.isShowing();
+    }
+
+    /**
+     * <p>Converts the selected item from the drop down list into a sequence
+     * of character that can be used in the edit box.</p>
+     *
+     * @param selectedItem the item selected by the user for completion
+     *
+     * @return a sequence of characters representing the selected suggestion
+     */
+    protected CharSequence convertSelectionToString(Object selectedItem) {
+        return mFilter.convertResultToString(selectedItem);
+    }
+
+    /**
+     * <p>Clear the list selection.  This may only be temporary, as user input will often bring
+     * it back.
+     */
+    public void clearListSelection() {
+        mPopup.clearListSelection();
+    }
+
+    /**
+     * Set the position of the dropdown view selection.
+     *
+     * @param position The position to move the selector to.
+     */
+    public void setListSelection(int position) {
+        mPopup.setSelection(position);
+    }
+
+    /**
+     * Get the position of the dropdown view selection, if there is one.  Returns
+     * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if there is no dropdown or if
+     * there is no selection.
+     *
+     * @return the position of the current selection, if there is one, or
+     * {@link ListView#INVALID_POSITION ListView.INVALID_POSITION} if not.
+     *
+     * @see ListView#getSelectedItemPosition()
+     */
+    public int getListSelection() {
+        return mPopup.getSelectedItemPosition();
+    }
+
+    /**
+     * <p>Starts filtering the content of the drop down list. The filtering
+     * pattern is the content of the edit box. Subclasses should override this
+     * method to filter with a different pattern, for instance a substring of
+     * <code>text</code>.</p>
+     *
+     * @param text the filtering pattern
+     * @param keyCode the last character inserted in the edit box; beware that
+     * this will be null when text is being added through a soft input method.
+     */
+    @SuppressWarnings({ "UnusedDeclaration" })
+    protected void performFiltering(CharSequence text, int keyCode) {
+        mFilter.filter(text, this);
+    }
+
+    /**
+     * <p>Performs the text completion by converting the selected item from
+     * the drop down list into a string, replacing the text box's content with
+     * this string and finally dismissing the drop down menu.</p>
+     */
+    public void performCompletion() {
+        performCompletion(null, -1, -1);
+    }
+
+    @Override
+    public void onCommitCompletion(CompletionInfo completion) {
+        if (isPopupShowing()) {
+            mPopup.performItemClick(completion.getPosition());
+        }
+    }
+
+    private void performCompletion(View selectedView, int position, long id) {
+        if (isPopupShowing()) {
+            Object selectedItem;
+            if (position < 0) {
+                selectedItem = mPopup.getSelectedItem();
+            } else {
+                selectedItem = mAdapter.getItem(position);
+            }
+            if (selectedItem == null) {
+                Log.w(TAG, "performCompletion: no selected item");
+                return;
+            }
+
+            mBlockCompletion = true;
+            replaceText(convertSelectionToString(selectedItem));
+            mBlockCompletion = false;
+
+            if (mItemClickListener != null) {
+                final ListPopupWindow list = mPopup;
+
+                if (selectedView == null || position < 0) {
+                    selectedView = list.getSelectedView();
+                    position = list.getSelectedItemPosition();
+                    id = list.getSelectedItemId();
+                }
+                mItemClickListener.onItemClick(list.getListView(), selectedView, position, id);
+            }
+        }
+
+        if (mDropDownDismissedOnCompletion && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+    }
+
+    /**
+     * Identifies whether the view is currently performing a text completion, so subclasses
+     * can decide whether to respond to text changed events.
+     */
+    public boolean isPerformingCompletion() {
+        return mBlockCompletion;
+    }
+
+    /**
+     * Like {@link #setText(CharSequence)}, except that it can disable filtering.
+     *
+     * @param filter If <code>false</code>, no filtering will be performed
+     *        as a result of this call.
+     */
+    public void setText(CharSequence text, boolean filter) {
+        if (filter) {
+            setText(text);
+        } else {
+            mBlockCompletion = true;
+            setText(text);
+            mBlockCompletion = false;
+        }
+    }
+
+    /**
+     * <p>Performs the text completion by replacing the current text by the
+     * selected item. Subclasses should override this method to avoid replacing
+     * the whole content of the edit box.</p>
+     *
+     * @param text the selected suggestion in the drop down list
+     */
+    protected void replaceText(CharSequence text) {
+        clearComposingText();
+
+        setText(text);
+        // make sure we keep the caret at the end of the text view
+        Editable spannable = getText();
+        Selection.setSelection(spannable, spannable.length());
+    }
+
+    /** {@inheritDoc} */
+    public void onFilterComplete(int count) {
+        updateDropDownForFilter(count);
+    }
+
+    private void updateDropDownForFilter(int count) {
+        // Not attached to window, don't update drop-down
+        if (getWindowVisibility() == View.GONE) return;
+
+        /*
+         * This checks enoughToFilter() again because filtering requests
+         * are asynchronous, so the result may come back after enough text
+         * has since been deleted to make it no longer appropriate
+         * to filter.
+         */
+
+        final boolean dropDownAlwaysVisible = mPopup.isDropDownAlwaysVisible();
+        final boolean enoughToFilter = enoughToFilter();
+        if ((count > 0 || dropDownAlwaysVisible) && enoughToFilter) {
+            if (hasFocus() && hasWindowFocus() && mPopupCanBeUpdated) {
+                showDropDown();
+            }
+        } else if (!dropDownAlwaysVisible && isPopupShowing()) {
+            dismissDropDown();
+            // When the filter text is changed, the first update from the adapter may show an empty
+            // count (when the query is being performed on the network). Future updates when some
+            // content has been retrieved should still be able to update the list.
+            mPopupCanBeUpdated = true;
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+        if (!hasWindowFocus && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+    }
+
+    @Override
+    protected void onDisplayHint(int hint) {
+        super.onDisplayHint(hint);
+        switch (hint) {
+            case INVISIBLE:
+                if (!mPopup.isDropDownAlwaysVisible()) {
+                    dismissDropDown();
+                }
+                break;
+        }
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+        if (isTemporarilyDetached()) {
+            // If we are temporarily in the detach state, then do nothing.
+            return;
+        }
+
+        // Perform validation if the view is losing focus.
+        if (!focused) {
+            performValidation();
+        }
+        if (!focused && !mPopup.isDropDownAlwaysVisible()) {
+            dismissDropDown();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        dismissDropDown();
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * <p>Closes the drop down if present on screen.</p>
+     */
+    public void dismissDropDown() {
+        InputMethodManager imm = InputMethodManager.peekInstance();
+        if (imm != null) {
+            imm.displayCompletions(this, null);
+        }
+        mPopup.dismiss();
+        mPopupCanBeUpdated = false;
+    }
+
+    @Override
+    protected boolean setFrame(final int l, int t, final int r, int b) {
+        boolean result = super.setFrame(l, t, r, b);
+
+        if (isPopupShowing()) {
+            showDropDown();
+        }
+
+        return result;
+    }
+
+    /**
+     * Issues a runnable to show the dropdown as soon as possible.
+     *
+     * @hide internal used only by SearchDialog
+     */
+    public void showDropDownAfterLayout() {
+        mPopup.postShow();
+    }
+
+    /**
+     * Ensures that the drop down is not obscuring the IME.
+     * @param visible whether the ime should be in front. If false, the ime is pushed to
+     * the background.
+     * @hide internal used only here and SearchDialog
+     */
+    public void ensureImeVisible(boolean visible) {
+        mPopup.setInputMethodMode(visible
+                ? ListPopupWindow.INPUT_METHOD_NEEDED : ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+        if (mPopup.isDropDownAlwaysVisible() || (mFilter != null && enoughToFilter())) {
+            showDropDown();
+        }
+    }
+
+    /**
+     * @hide internal used only here and SearchDialog
+     */
+    public boolean isInputMethodNotNeeded() {
+        return mPopup.getInputMethodMode() == ListPopupWindow.INPUT_METHOD_NOT_NEEDED;
+    }
+
+    /**
+     * <p>Displays the drop down on screen.</p>
+     */
+    public void showDropDown() {
+        buildImeCompletions();
+
+        if (mPopup.getAnchorView() == null) {
+            if (mDropDownAnchorId != View.NO_ID) {
+                mPopup.setAnchorView(getRootView().findViewById(mDropDownAnchorId));
+            } else {
+                mPopup.setAnchorView(this);
+            }
+        }
+        if (!isPopupShowing()) {
+            // Make sure the list does not obscure the IME when shown for the first time.
+            mPopup.setInputMethodMode(ListPopupWindow.INPUT_METHOD_NEEDED);
+            mPopup.setListItemExpandMax(EXPAND_MAX);
+        }
+        mPopup.show();
+        mPopup.getListView().setOverScrollMode(View.OVER_SCROLL_ALWAYS);
+    }
+
+    /**
+     * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+     * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+     * ignore outside touch even when the drop down is not set to always visible.
+     *
+     * @hide used only by SearchDialog
+     */
+    public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+        mPopup.setForceIgnoreOutsideTouch(forceIgnoreOutsideTouch);
+    }
+
+    private void buildImeCompletions() {
+        final ListAdapter adapter = mAdapter;
+        if (adapter != null) {
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) {
+                final int count = Math.min(adapter.getCount(), 20);
+                CompletionInfo[] completions = new CompletionInfo[count];
+                int realCount = 0;
+
+                for (int i = 0; i < count; i++) {
+                    if (adapter.isEnabled(i)) {
+                        Object item = adapter.getItem(i);
+                        long id = adapter.getItemId(i);
+                        completions[realCount] = new CompletionInfo(id, realCount,
+                                convertSelectionToString(item));
+                        realCount++;
+                    }
+                }
+
+                if (realCount != count) {
+                    CompletionInfo[] tmp = new CompletionInfo[realCount];
+                    System.arraycopy(completions, 0, tmp, 0, realCount);
+                    completions = tmp;
+                }
+
+                imm.displayCompletions(this, completions);
+            }
+        }
+    }
+
+    /**
+     * Sets the validator used to perform text validation.
+     *
+     * @param validator The validator used to validate the text entered in this widget.
+     *
+     * @see #getValidator()
+     * @see #performValidation()
+     */
+    public void setValidator(Validator validator) {
+        mValidator = validator;
+    }
+
+    /**
+     * Returns the Validator set with {@link #setValidator},
+     * or <code>null</code> if it was not set.
+     *
+     * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
+     * @see #performValidation()
+     */
+    public Validator getValidator() {
+        return mValidator;
+    }
+
+    /**
+     * If a validator was set on this view and the current string is not valid,
+     * ask the validator to fix it.
+     *
+     * @see #getValidator()
+     * @see #setValidator(android.widget.AutoCompleteTextView.Validator)
+     */
+    public void performValidation() {
+        if (mValidator == null) return;
+
+        CharSequence text = getText();
+
+        if (!TextUtils.isEmpty(text) && !mValidator.isValid(text)) {
+            setText(mValidator.fixText(text));
+        }
+    }
+
+    /**
+     * Returns the Filter obtained from {@link Filterable#getFilter},
+     * or <code>null</code> if {@link #setAdapter} was not called with
+     * a Filterable.
+     */
+    protected Filter getFilter() {
+        return mFilter;
+    }
+
+    private class DropDownItemClickListener implements AdapterView.OnItemClickListener {
+        public void onItemClick(AdapterView parent, View v, int position, long id) {
+            performCompletion(v, position, id);
+        }
+    }
+
+    /**
+     * This interface is used to make sure that the text entered in this TextView complies to
+     * a certain format.  Since there is no foolproof way to prevent the user from leaving
+     * this View with an incorrect value in it, all we can do is try to fix it ourselves
+     * when this happens.
+     */
+    public interface Validator {
+        /**
+         * Validates the specified text.
+         *
+         * @return true If the text currently in the text editor is valid.
+         *
+         * @see #fixText(CharSequence)
+         */
+        boolean isValid(CharSequence text);
+
+        /**
+         * Corrects the specified text to make it valid.
+         *
+         * @param invalidText A string that doesn't pass validation: isValid(invalidText)
+         *        returns false
+         *
+         * @return A string based on invalidText such as invoking isValid() on it returns true.
+         *
+         * @see #isValid(CharSequence)
+         */
+        CharSequence fixText(CharSequence invalidText);
+    }
+
+    /**
+     * Listener to respond to the AutoCompleteTextView's completion list being dismissed.
+     * @see AutoCompleteTextView#setOnDismissListener(OnDismissListener)
+     */
+    public interface OnDismissListener {
+        /**
+         * This method will be invoked whenever the AutoCompleteTextView's list
+         * of completion options has been dismissed and is no longer available
+         * for user interaction.
+         */
+        void onDismiss();
+    }
+
+    /**
+     * Allows us a private hook into the on click event without preventing users from setting
+     * their own click listener.
+     */
+    private class PassThroughClickListener implements OnClickListener {
+
+        private View.OnClickListener mWrapped;
+
+        /** {@inheritDoc} */
+        public void onClick(View v) {
+            onClickImpl();
+
+            if (mWrapped != null) mWrapped.onClick(v);
+        }
+    }
+
+    /**
+     * Static inner listener that keeps a WeakReference to the actual AutoCompleteTextView.
+     * <p>
+     * This way, if adapter has a longer life span than the View, we won't leak the View, instead
+     * we will just leak a small Observer with 1 field.
+     */
+    private static class PopupDataSetObserver extends DataSetObserver {
+        private final WeakReference<AutoCompleteTextView> mViewReference;
+
+        private PopupDataSetObserver(AutoCompleteTextView view) {
+            mViewReference = new WeakReference<AutoCompleteTextView>(view);
+        }
+
+        @Override
+        public void onChanged() {
+            final AutoCompleteTextView textView = mViewReference.get();
+            if (textView != null && textView.mAdapter != null) {
+                // If the popup is not showing already, showing it will cause
+                // the list of data set observers attached to the adapter to
+                // change. We can't do it from here, because we are in the middle
+                // of iterating through the list of observers.
+                textView.post(updateRunnable);
+            }
+        }
+
+        private final Runnable updateRunnable = new Runnable() {
+            @Override
+            public void run() {
+                final AutoCompleteTextView textView = mViewReference.get();
+                if (textView == null) {
+                    return;
+                }
+                final ListAdapter adapter = textView.mAdapter;
+                if (adapter == null) {
+                    return;
+                }
+                textView.updateDropDownForFilter(adapter.getCount());
+            }
+        };
+    }
+}
diff --git a/android/widget/BaseAdapter.java b/android/widget/BaseAdapter.java
new file mode 100644
index 0000000..5838cc1
--- /dev/null
+++ b/android/widget/BaseAdapter.java
@@ -0,0 +1,99 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.Nullable;
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Common base class of common implementation for an {@link Adapter} that can be
+ * used in both {@link ListView} (by implementing the specialized
+ * {@link ListAdapter} interface) and {@link Spinner} (by implementing the
+ * specialized {@link SpinnerAdapter} interface).
+ */
+public abstract class BaseAdapter implements ListAdapter, SpinnerAdapter {
+    private final DataSetObservable mDataSetObservable = new DataSetObservable();
+    private CharSequence[] mAutofillOptions;
+
+    public boolean hasStableIds() {
+        return false;
+    }
+    
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.registerObserver(observer);
+    }
+
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.unregisterObserver(observer);
+    }
+    
+    /**
+     * Notifies the attached observers that the underlying data has been changed
+     * and any View reflecting the data set should refresh itself.
+     */
+    public void notifyDataSetChanged() {
+        mDataSetObservable.notifyChanged();
+    }
+
+    /**
+     * Notifies the attached observers that the underlying data is no longer valid
+     * or available. Once invoked this adapter is no longer valid and should
+     * not report further data set changes.
+     */
+    public void notifyDataSetInvalidated() {
+        mDataSetObservable.notifyInvalidated();
+    }
+
+    public boolean areAllItemsEnabled() {
+        return true;
+    }
+
+    public boolean isEnabled(int position) {
+        return true;
+    }
+
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        return getView(position, convertView, parent);
+    }
+
+    public int getItemViewType(int position) {
+        return 0;
+    }
+
+    public int getViewTypeCount() {
+        return 1;
+    }
+    
+    public boolean isEmpty() {
+        return getCount() == 0;
+    }
+
+    @Override
+    public CharSequence[] getAutofillOptions() {
+        return mAutofillOptions;
+    }
+
+    /**
+     * Sets the value returned by {@link #getAutofillOptions()}
+     */
+    public void setAutofillOptions(@Nullable CharSequence... options) {
+        mAutofillOptions = options;
+    }
+}
diff --git a/android/widget/BaseExpandableListAdapter.java b/android/widget/BaseExpandableListAdapter.java
new file mode 100644
index 0000000..b4d6ad7
--- /dev/null
+++ b/android/widget/BaseExpandableListAdapter.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.database.DataSetObservable;
+import android.database.DataSetObserver;
+
+/**
+ * Base class for a {@link ExpandableListAdapter} used to provide data and Views
+ * from some data to an expandable list view.
+ * <p>
+ * Adapters inheriting this class should verify that the base implementations of
+ * {@link #getCombinedChildId(long, long)} and {@link #getCombinedGroupId(long)}
+ * are correct in generating unique IDs from the group/children IDs.
+ * <p>
+ * @see SimpleExpandableListAdapter
+ * @see SimpleCursorTreeAdapter
+ */
+public abstract class BaseExpandableListAdapter implements ExpandableListAdapter, 
+        HeterogeneousExpandableList {
+    private final DataSetObservable mDataSetObservable = new DataSetObservable();
+    
+    public void registerDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.registerObserver(observer);
+    }
+
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        mDataSetObservable.unregisterObserver(observer);
+    }
+    
+    /**
+     * @see DataSetObservable#notifyInvalidated()
+     */
+    public void notifyDataSetInvalidated() {
+        mDataSetObservable.notifyInvalidated();
+    }
+    
+    /**
+     * @see DataSetObservable#notifyChanged()
+     */
+    public void notifyDataSetChanged() {
+        mDataSetObservable.notifyChanged();
+    }
+
+    public boolean areAllItemsEnabled() {
+        return true;
+    }
+
+    public void onGroupCollapsed(int groupPosition) {
+    }
+
+    public void onGroupExpanded(int groupPosition) {
+    }
+
+    /**
+     * Override this method if you foresee a clash in IDs based on this scheme:
+     * <p>
+     * Base implementation returns a long:
+     * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method
+     *             this bit will be 1.
+     * <li> bit 1-31: Lower 31 bits of the groupId
+     * <li> bit 32-63: Lower 32 bits of the childId.
+     * <p> 
+     * {@inheritDoc}
+     */
+    public long getCombinedChildId(long groupId, long childId) {
+        return 0x8000000000000000L | ((groupId & 0x7FFFFFFF) << 32) | (childId & 0xFFFFFFFF);
+    }
+
+    /**
+     * Override this method if you foresee a clash in IDs based on this scheme:
+     * <p>
+     * Base implementation returns a long:
+     * <li> bit 0: Whether this ID points to a child (unset) or group (set), so for this method
+     *             this bit will be 0.
+     * <li> bit 1-31: Lower 31 bits of the groupId
+     * <li> bit 32-63: Lower 32 bits of the childId.
+     * <p> 
+     * {@inheritDoc}
+     */
+    public long getCombinedGroupId(long groupId) {
+        return (groupId & 0x7FFFFFFF) << 32;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public boolean isEmpty() {
+        return getGroupCount() == 0;
+    }
+
+
+    /**
+     * {@inheritDoc}
+     * @return 0 for any group or child position, since only one child type count is declared.
+     */
+    public int getChildType(int groupPosition, int childPosition) {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return 1 as a default value in BaseExpandableListAdapter.
+     */
+    public int getChildTypeCount() {
+        return 1;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return 0 for any groupPosition, since only one group type count is declared.
+     */
+    public int getGroupType(int groupPosition) {
+        return 0;
+    }
+
+    /**
+     * {@inheritDoc}
+     * @return 1 as a default value in BaseExpandableListAdapter.
+     */
+    public int getGroupTypeCount() {
+        return 1;
+    }
+}
diff --git a/android/widget/Button.java b/android/widget/Button.java
new file mode 100644
index 0000000..634cbe3
--- /dev/null
+++ b/android/widget/Button.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.widget.RemoteViews.RemoteView;
+
+
+/**
+ * A user interface element the user can tap or click to perform an action.
+ *
+ * <p>To display a button in an activity, add a button to the activity's layout XML file:</p>
+ *
+ * <pre>
+ * &lt;Button
+ *     android:id="@+id/button_id"
+ *     android:layout_height="wrap_content"
+ *     android:layout_width="wrap_content"
+ *     android:text="@string/self_destruct" /&gt;</pre>
+ *
+ * <p>To specify an action when the button is pressed, set a click
+ * listener on the button object in the corresponding activity code:</p>
+ *
+ * <pre>
+ * public class MyActivity extends Activity {
+ *     protected void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *
+ *         setContentView(R.layout.content_layout_id);
+ *
+ *         final Button button = findViewById(R.id.button_id);
+ *         button.setOnClickListener(new View.OnClickListener() {
+ *             public void onClick(View v) {
+ *                 // Code here executes on main thread after user presses button
+ *             }
+ *         });
+ *     }
+ * }</pre>
+ *
+ * <p>The above snippet creates an instance of {@link android.view.View.OnClickListener} and wires
+ * the listener to the button using
+ * {@link #setOnClickListener setOnClickListener(View.OnClickListener)}.
+ * As a result, the system executes the code you write in {@code onClick(View)} after the
+ * user presses the button.</p>
+ *
+ * <p class="note">The system executes the code in {@code onClick} on the
+ * <a href="{@docRoot}guide/components/processes-and-threads.html#Threads">main thread</a>.
+ * This means your onClick code must execute quickly to avoid delaying your app's response
+ * to further user actions.  See
+ * <a href="{@docRoot}training/articles/perf-anr.html">Keeping Your App Responsive</a>
+ * for more details.</p>
+ *
+ * <p>Every button is styled using the system's default button background, which is often
+ * different from one version of the platform to another. If you are not satisfied with the
+ * default button style, you can customize it. For more details and code samples, see the
+ * <a href="{@docRoot}guide/topics/ui/controls/button.html#Style">Styling Your Button</a>
+ * guide.</p>
+ *
+ * <p>For all XML style attributes available on Button see
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}.  See the
+ * <a href="{@docRoot}guide/topics/ui/themes.html#ApplyingStyles">Styles and Themes</a>
+ * guide to learn how to implement and organize overrides to style-related attributes.</p>
+ */
+@RemoteView
+public class Button extends TextView {
+
+    /**
+     * Simple constructor to use when creating a button from code.
+     *
+     * @param context The Context the Button is running in, through which it can
+     *        access the current theme, resources, etc.
+     *
+     * @see #Button(Context, AttributeSet)
+     */
+    public Button(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * {@link LayoutInflater} calls this constructor when inflating a Button from XML.
+     * The attributes defined by the current theme's
+     * {@link android.R.attr#buttonStyle android:buttonStyle}
+     * override base view attributes.
+     *
+     * You typically do not call this constructor to create your own button instance in code.
+     * However, you must override this constructor when
+     * <a href="{@docRoot}training/custom-views/index.html">creating custom views</a>.
+     *
+     * @param context The Context the view is running in, through which it can
+     *        access the current theme, resources, etc.
+     * @param attrs The attributes of the XML Button tag being used to inflate the view.
+     *
+     * @see #Button(Context, AttributeSet, int)
+     * @see android.view.View#View(Context, AttributeSet)
+     */
+    public Button(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.buttonStyle);
+    }
+
+    /**
+     * This constructor allows a Button subclass to use its own class-specific base style from a
+     * theme attribute when inflating. The attributes defined by the current theme's
+     * {@code defStyleAttr} override base view attributes.
+     *
+     * <p>For Button's base view attributes see
+     * {@link android.R.styleable#Button Button Attributes},
+     * {@link android.R.styleable#TextView TextView Attributes},
+     * {@link android.R.styleable#View View Attributes}.
+     *
+     * @param context The Context the Button is running in, through which it can
+     *        access the current theme, resources, etc.
+     * @param attrs The attributes of the XML Button tag that is inflating the view.
+     * @param defStyleAttr The resource identifier of an attribute in the current theme
+     *        whose value is the the resource id of a style. The specified style’s
+     *        attribute values serve as default values for the button. Set this parameter
+     *        to 0 to avoid use of default values.
+     * @see #Button(Context, AttributeSet, int, int)
+     * @see android.view.View#View(Context, AttributeSet, int)
+     */
+    public Button(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * This constructor allows a Button subclass to use its own class-specific base style from
+     * either a theme attribute or style resource when inflating. To see how the final value of a
+     * particular attribute is resolved based on your inputs to this constructor, see
+     * {@link android.view.View#View(Context, AttributeSet, int, int)}.
+     *
+     * @param context The Context the Button is running in, through which it can
+     *        access the current theme, resources, etc.
+     * @param attrs The attributes of the XML Button tag that is inflating the view.
+     * @param defStyleAttr The resource identifier of an attribute in the current theme
+     *        whose value is the the resource id of a style. The specified style’s
+     *        attribute values serve as default values for the button. Set this parameter
+     *        to 0 to avoid use of default values.
+     * @param defStyleRes The identifier of a style resource that
+     *        supplies default values for the button, used only if
+     *        defStyleAttr is 0 or cannot be found in the theme.
+     *        Set this parameter to 0 to avoid use of default values.
+     *
+     * @see #Button(Context, AttributeSet, int)
+     * @see android.view.View#View(Context, AttributeSet, int, int)
+     */
+    public Button(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Button.class.getName();
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+}
diff --git a/android/widget/CalendarView.java b/android/widget/CalendarView.java
new file mode 100644
index 0000000..db50e34
--- /dev/null
+++ b/android/widget/CalendarView.java
@@ -0,0 +1,787 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.icu.util.Calendar;
+import android.icu.util.TimeZone;
+import android.util.AttributeSet;
+import android.util.Log;
+
+import com.android.internal.R;
+
+import java.text.DateFormat;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.Locale;
+
+/**
+ * This class is a calendar widget for displaying and selecting dates. The
+ * range of dates supported by this calendar is configurable.
+ * <p>
+ * The exact appearance and interaction model of this widget may vary between
+ * OS versions and themes (e.g. Holo versus Material), but in general a user
+ * can select a date by tapping on it and can scroll or fling the calendar to a
+ * desired date.
+ *
+ * @attr ref android.R.styleable#CalendarView_showWeekNumber
+ * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+ * @attr ref android.R.styleable#CalendarView_minDate
+ * @attr ref android.R.styleable#CalendarView_maxDate
+ * @attr ref android.R.styleable#CalendarView_shownWeekCount
+ * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+ * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+ * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+ * @attr ref android.R.styleable#CalendarView_weekNumberColor
+ * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+ * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+ * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+ * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+ */
+@Widget
+public class CalendarView extends FrameLayout {
+    private static final String LOG_TAG = "CalendarView";
+
+    private static final int MODE_HOLO = 0;
+    private static final int MODE_MATERIAL = 1;
+
+    private final CalendarViewDelegate mDelegate;
+
+    /**
+     * The callback used to indicate the user changes the date.
+     */
+    public interface OnDateChangeListener {
+
+        /**
+         * Called upon change of the selected day.
+         *
+         * @param view The view associated with this listener.
+         * @param year The year that was set.
+         * @param month The month that was set [0-11].
+         * @param dayOfMonth The day of the month that was set.
+         */
+        void onSelectedDayChange(@NonNull CalendarView view, int year, int month, int dayOfMonth);
+    }
+
+    public CalendarView(@NonNull Context context) {
+        this(context, null);
+    }
+
+    public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, R.attr.calendarViewStyle);
+    }
+
+    public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CalendarView(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.CalendarView, defStyleAttr, defStyleRes);
+        final int mode = a.getInt(R.styleable.CalendarView_calendarViewMode, MODE_HOLO);
+        a.recycle();
+
+        switch (mode) {
+            case MODE_HOLO:
+                mDelegate = new CalendarViewLegacyDelegate(
+                        this, context, attrs, defStyleAttr, defStyleRes);
+                break;
+            case MODE_MATERIAL:
+                mDelegate = new CalendarViewMaterialDelegate(
+                        this, context, attrs, defStyleAttr, defStyleRes);
+                break;
+            default:
+                throw new IllegalArgumentException("invalid calendarViewMode attribute");
+        }
+    }
+
+    /**
+     * Sets the number of weeks to be shown.
+     *
+     * @param count The shown week count.
+     *
+     * @attr ref android.R.styleable#CalendarView_shownWeekCount
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setShownWeekCount(int count) {
+        mDelegate.setShownWeekCount(count);
+    }
+
+    /**
+     * Gets the number of weeks to be shown.
+     *
+     * @return The shown week count.
+     *
+     * @attr ref android.R.styleable#CalendarView_shownWeekCount
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public int getShownWeekCount() {
+        return mDelegate.getShownWeekCount();
+    }
+
+    /**
+     * Sets the background color for the selected week.
+     *
+     * @param color The week background color.
+     *
+     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setSelectedWeekBackgroundColor(@ColorInt int color) {
+        mDelegate.setSelectedWeekBackgroundColor(color);
+    }
+
+    /**
+     * Gets the background color for the selected week.
+     *
+     * @return The week background color.
+     *
+     * @attr ref android.R.styleable#CalendarView_selectedWeekBackgroundColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @ColorInt
+    @Deprecated
+    public int getSelectedWeekBackgroundColor() {
+        return mDelegate.getSelectedWeekBackgroundColor();
+    }
+
+    /**
+     * Sets the color for the dates of the focused month.
+     *
+     * @param color The focused month date color.
+     *
+     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setFocusedMonthDateColor(@ColorInt int color) {
+        mDelegate.setFocusedMonthDateColor(color);
+    }
+
+    /**
+     * Gets the color for the dates in the focused month.
+     *
+     * @return The focused month date color.
+     *
+     * @attr ref android.R.styleable#CalendarView_focusedMonthDateColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @ColorInt
+    @Deprecated
+    public int getFocusedMonthDateColor() {
+        return mDelegate.getFocusedMonthDateColor();
+    }
+
+    /**
+     * Sets the color for the dates of a not focused month.
+     *
+     * @param color A not focused month date color.
+     *
+     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setUnfocusedMonthDateColor(@ColorInt int color) {
+        mDelegate.setUnfocusedMonthDateColor(color);
+    }
+
+    /**
+     * Gets the color for the dates in a not focused month.
+     *
+     * @return A not focused month date color.
+     *
+     * @attr ref android.R.styleable#CalendarView_unfocusedMonthDateColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @ColorInt
+    @Deprecated
+    public int getUnfocusedMonthDateColor() {
+        return mDelegate.getUnfocusedMonthDateColor();
+    }
+
+    /**
+     * Sets the color for the week numbers.
+     *
+     * @param color The week number color.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekNumberColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setWeekNumberColor(@ColorInt int color) {
+        mDelegate.setWeekNumberColor(color);
+    }
+
+    /**
+     * Gets the color for the week numbers.
+     *
+     * @return The week number color.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekNumberColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @ColorInt
+    @Deprecated
+    public int getWeekNumberColor() {
+        return mDelegate.getWeekNumberColor();
+    }
+
+    /**
+     * Sets the color for the separator line between weeks.
+     *
+     * @param color The week separator color.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setWeekSeparatorLineColor(@ColorInt int color) {
+        mDelegate.setWeekSeparatorLineColor(color);
+    }
+
+    /**
+     * Gets the color for the separator line between weeks.
+     *
+     * @return The week separator color.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekSeparatorLineColor
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @ColorInt
+    @Deprecated
+    public int getWeekSeparatorLineColor() {
+        return mDelegate.getWeekSeparatorLineColor();
+    }
+
+    /**
+     * Sets the drawable for the vertical bar shown at the beginning and at
+     * the end of the selected date.
+     *
+     * @param resourceId The vertical bar drawable resource id.
+     *
+     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setSelectedDateVerticalBar(@DrawableRes int resourceId) {
+        mDelegate.setSelectedDateVerticalBar(resourceId);
+    }
+
+    /**
+     * Sets the drawable for the vertical bar shown at the beginning and at
+     * the end of the selected date.
+     *
+     * @param drawable The vertical bar drawable.
+     *
+     * @attr ref android.R.styleable#CalendarView_selectedDateVerticalBar
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public void setSelectedDateVerticalBar(Drawable drawable) {
+        mDelegate.setSelectedDateVerticalBar(drawable);
+    }
+
+    /**
+     * Gets the drawable for the vertical bar shown at the beginning and at
+     * the end of the selected date.
+     *
+     * @return The vertical bar drawable.
+     * @deprecated No longer used by Material-style CalendarView.
+     */
+    @Deprecated
+    public Drawable getSelectedDateVerticalBar() {
+        return mDelegate.getSelectedDateVerticalBar();
+    }
+
+    /**
+     * Sets the text appearance for the week day abbreviation of the calendar header.
+     *
+     * @param resourceId The text appearance resource id.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+     */
+    public void setWeekDayTextAppearance(@StyleRes int resourceId) {
+        mDelegate.setWeekDayTextAppearance(resourceId);
+    }
+
+    /**
+     * Gets the text appearance for the week day abbreviation of the calendar header.
+     *
+     * @return The text appearance resource id.
+     *
+     * @attr ref android.R.styleable#CalendarView_weekDayTextAppearance
+     */
+    public @StyleRes int getWeekDayTextAppearance() {
+        return mDelegate.getWeekDayTextAppearance();
+    }
+
+    /**
+     * Sets the text appearance for the calendar dates.
+     *
+     * @param resourceId The text appearance resource id.
+     *
+     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+     */
+    public void setDateTextAppearance(@StyleRes int resourceId) {
+        mDelegate.setDateTextAppearance(resourceId);
+    }
+
+    /**
+     * Gets the text appearance for the calendar dates.
+     *
+     * @return The text appearance resource id.
+     *
+     * @attr ref android.R.styleable#CalendarView_dateTextAppearance
+     */
+    public @StyleRes int getDateTextAppearance() {
+        return mDelegate.getDateTextAppearance();
+    }
+
+    /**
+     * Gets the minimal date supported by this {@link CalendarView} in milliseconds
+     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+     * zone.
+     * <p>
+     * Note: The default minimal date is 01/01/1900.
+     * <p>
+     *
+     * @return The minimal supported date.
+     *
+     * @attr ref android.R.styleable#CalendarView_minDate
+     */
+    public long getMinDate() {
+        return mDelegate.getMinDate();
+    }
+
+    /**
+     * Sets the minimal date supported by this {@link CalendarView} in milliseconds
+     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+     * zone.
+     *
+     * @param minDate The minimal supported date.
+     *
+     * @attr ref android.R.styleable#CalendarView_minDate
+     */
+    public void setMinDate(long minDate) {
+        mDelegate.setMinDate(minDate);
+    }
+
+    /**
+     * Gets the maximal date supported by this {@link CalendarView} in milliseconds
+     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+     * zone.
+     * <p>
+     * Note: The default maximal date is 01/01/2100.
+     * <p>
+     *
+     * @return The maximal supported date.
+     *
+     * @attr ref android.R.styleable#CalendarView_maxDate
+     */
+    public long getMaxDate() {
+        return mDelegate.getMaxDate();
+    }
+
+    /**
+     * Sets the maximal date supported by this {@link CalendarView} in milliseconds
+     * since January 1, 1970 00:00:00 in {@link TimeZone#getDefault()} time
+     * zone.
+     *
+     * @param maxDate The maximal supported date.
+     *
+     * @attr ref android.R.styleable#CalendarView_maxDate
+     */
+    public void setMaxDate(long maxDate) {
+        mDelegate.setMaxDate(maxDate);
+    }
+
+    /**
+     * Sets whether to show the week number.
+     *
+     * @param showWeekNumber True to show the week number.
+     * @deprecated No longer used by Material-style CalendarView.
+     *
+     * @attr ref android.R.styleable#CalendarView_showWeekNumber
+     */
+    @Deprecated
+    public void setShowWeekNumber(boolean showWeekNumber) {
+        mDelegate.setShowWeekNumber(showWeekNumber);
+    }
+
+    /**
+     * Gets whether to show the week number.
+     *
+     * @return True if showing the week number.
+     * @deprecated No longer used by Material-style CalendarView.
+     *
+     * @attr ref android.R.styleable#CalendarView_showWeekNumber
+     */
+    @Deprecated
+    public boolean getShowWeekNumber() {
+        return mDelegate.getShowWeekNumber();
+    }
+
+    /**
+     * Gets the first day of week.
+     *
+     * @return The first day of the week conforming to the {@link CalendarView}
+     *         APIs.
+     * @see Calendar#MONDAY
+     * @see Calendar#TUESDAY
+     * @see Calendar#WEDNESDAY
+     * @see Calendar#THURSDAY
+     * @see Calendar#FRIDAY
+     * @see Calendar#SATURDAY
+     * @see Calendar#SUNDAY
+     *
+     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+     */
+    public int getFirstDayOfWeek() {
+        return mDelegate.getFirstDayOfWeek();
+    }
+
+    /**
+     * Sets the first day of week.
+     *
+     * @param firstDayOfWeek The first day of the week conforming to the
+     *            {@link CalendarView} APIs.
+     * @see Calendar#MONDAY
+     * @see Calendar#TUESDAY
+     * @see Calendar#WEDNESDAY
+     * @see Calendar#THURSDAY
+     * @see Calendar#FRIDAY
+     * @see Calendar#SATURDAY
+     * @see Calendar#SUNDAY
+     *
+     * @attr ref android.R.styleable#CalendarView_firstDayOfWeek
+     */
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        mDelegate.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    /**
+     * Sets the listener to be notified upon selected date change.
+     *
+     * @param listener The listener to be notified.
+     */
+    public void setOnDateChangeListener(OnDateChangeListener listener) {
+        mDelegate.setOnDateChangeListener(listener);
+    }
+
+    /**
+     * Gets the selected date in milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     *
+     * @return The selected date.
+     */
+    public long getDate() {
+        return mDelegate.getDate();
+    }
+
+    /**
+     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     *
+     * @param date The selected date.
+     *
+     * @throws IllegalArgumentException of the provided date is before the
+     *        minimal or after the maximal date.
+     *
+     * @see #setDate(long, boolean, boolean)
+     * @see #setMinDate(long)
+     * @see #setMaxDate(long)
+     */
+    public void setDate(long date) {
+        mDelegate.setDate(date);
+    }
+
+    /**
+     * Sets the selected date in milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     *
+     * @param date The date.
+     * @param animate Whether to animate the scroll to the current date.
+     * @param center Whether to center the current date even if it is already visible.
+     *
+     * @throws IllegalArgumentException of the provided date is before the
+     *        minimal or after the maximal date.
+     *
+     * @see #setMinDate(long)
+     * @see #setMaxDate(long)
+     */
+    public void setDate(long date, boolean animate, boolean center) {
+        mDelegate.setDate(date, animate, center);
+    }
+
+    /**
+     * Retrieves the screen bounds for the specific date in the coordinate system of this
+     * view. If the passed date is being currently displayed, this method returns true and
+     * the caller can query the fields of the passed {@link Rect} object. Otherwise the
+     * method returns false and does not touch the passed {@link Rect} object.
+     *
+     * @hide
+     */
+    @TestApi
+    public boolean getBoundsForDate(long date, Rect outBounds) {
+        return mDelegate.getBoundsForDate(date, outBounds);
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDelegate.onConfigurationChanged(newConfig);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return CalendarView.class.getName();
+    }
+
+    /**
+     * A delegate interface that defined the public API of the CalendarView. Allows different
+     * CalendarView implementations. This would need to be implemented by the CalendarView delegates
+     * for the real behavior.
+     */
+    private interface CalendarViewDelegate {
+        void setShownWeekCount(int count);
+        int getShownWeekCount();
+
+        void setSelectedWeekBackgroundColor(@ColorInt int color);
+        @ColorInt int getSelectedWeekBackgroundColor();
+
+        void setFocusedMonthDateColor(@ColorInt int color);
+        @ColorInt int getFocusedMonthDateColor();
+
+        void setUnfocusedMonthDateColor(@ColorInt int color);
+        @ColorInt int getUnfocusedMonthDateColor();
+
+        void setWeekNumberColor(@ColorInt int color);
+        @ColorInt int getWeekNumberColor();
+
+        void setWeekSeparatorLineColor(@ColorInt int color);
+        @ColorInt int getWeekSeparatorLineColor();
+
+        void setSelectedDateVerticalBar(@DrawableRes int resourceId);
+        void setSelectedDateVerticalBar(Drawable drawable);
+        Drawable getSelectedDateVerticalBar();
+
+        void setWeekDayTextAppearance(@StyleRes int resourceId);
+        @StyleRes int getWeekDayTextAppearance();
+
+        void setDateTextAppearance(@StyleRes int resourceId);
+        @StyleRes int getDateTextAppearance();
+
+        void setMinDate(long minDate);
+        long getMinDate();
+
+        void setMaxDate(long maxDate);
+        long getMaxDate();
+
+        void setShowWeekNumber(boolean showWeekNumber);
+        boolean getShowWeekNumber();
+
+        void setFirstDayOfWeek(int firstDayOfWeek);
+        int getFirstDayOfWeek();
+
+        void setDate(long date);
+        void setDate(long date, boolean animate, boolean center);
+        long getDate();
+
+        boolean getBoundsForDate(long date, Rect outBounds);
+
+        void setOnDateChangeListener(OnDateChangeListener listener);
+
+        void onConfigurationChanged(Configuration newConfig);
+    }
+
+    /**
+     * An abstract class which can be used as a start for CalendarView implementations
+     */
+    abstract static class AbstractCalendarViewDelegate implements CalendarViewDelegate {
+        /** The default minimal date. */
+        protected static final String DEFAULT_MIN_DATE = "01/01/1900";
+
+        /** The default maximal date. */
+        protected static final String DEFAULT_MAX_DATE = "01/01/2100";
+
+        protected CalendarView mDelegator;
+        protected Context mContext;
+        protected Locale mCurrentLocale;
+
+        AbstractCalendarViewDelegate(CalendarView delegator, Context context) {
+            mDelegator = delegator;
+            mContext = context;
+
+            // Initialization based on locale
+            setCurrentLocale(Locale.getDefault());
+        }
+
+        protected void setCurrentLocale(Locale locale) {
+            if (locale.equals(mCurrentLocale)) {
+                return;
+            }
+            mCurrentLocale = locale;
+        }
+
+        @Override
+        public void setShownWeekCount(int count) {
+            // Deprecated.
+        }
+
+        @Override
+        public int getShownWeekCount() {
+            // Deprecated.
+            return 0;
+        }
+
+        @Override
+        public void setSelectedWeekBackgroundColor(@ColorInt int color) {
+            // Deprecated.
+        }
+
+        @ColorInt
+        @Override
+        public int getSelectedWeekBackgroundColor() {
+            return 0;
+        }
+
+        @Override
+        public void setFocusedMonthDateColor(@ColorInt int color) {
+            // Deprecated.
+        }
+
+        @ColorInt
+        @Override
+        public int getFocusedMonthDateColor() {
+            return 0;
+        }
+
+        @Override
+        public void setUnfocusedMonthDateColor(@ColorInt int color) {
+            // Deprecated.
+        }
+
+        @ColorInt
+        @Override
+        public int getUnfocusedMonthDateColor() {
+            return 0;
+        }
+
+        @Override
+        public void setWeekNumberColor(@ColorInt int color) {
+            // Deprecated.
+        }
+
+        @ColorInt
+        @Override
+        public int getWeekNumberColor() {
+            // Deprecated.
+            return 0;
+        }
+
+        @Override
+        public void setWeekSeparatorLineColor(@ColorInt int color) {
+            // Deprecated.
+        }
+
+        @ColorInt
+        @Override
+        public int getWeekSeparatorLineColor() {
+            // Deprecated.
+            return 0;
+        }
+
+        @Override
+        public void setSelectedDateVerticalBar(@DrawableRes int resId) {
+            // Deprecated.
+        }
+
+        @Override
+        public void setSelectedDateVerticalBar(Drawable drawable) {
+            // Deprecated.
+        }
+
+        @Override
+        public Drawable getSelectedDateVerticalBar() {
+            // Deprecated.
+            return null;
+        }
+
+        @Override
+        public void setShowWeekNumber(boolean showWeekNumber) {
+            // Deprecated.
+        }
+
+        @Override
+        public boolean getShowWeekNumber() {
+            // Deprecated.
+            return false;
+        }
+
+        @Override
+        public void onConfigurationChanged(Configuration newConfig) {
+            // Nothing to do here, configuration changes are already propagated
+            // by ViewGroup.
+        }
+    }
+
+    /** String for parsing dates. */
+    private static final String DATE_FORMAT = "MM/dd/yyyy";
+
+    /** Date format for parsing dates. */
+    private static final DateFormat DATE_FORMATTER = new SimpleDateFormat(DATE_FORMAT);
+
+    /**
+     * Utility method for the date format used by CalendarView's min/max date.
+     *
+     * @hide Use only as directed. For internal use only.
+     */
+    public static boolean parseDate(String date, Calendar outDate) {
+        if (date == null || date.isEmpty()) {
+            return false;
+        }
+
+        try {
+            final Date parsedDate = DATE_FORMATTER.parse(date);
+            outDate.setTime(parsedDate);
+            return true;
+        } catch (ParseException e) {
+            Log.w(LOG_TAG, "Date: " + date + " not in format: " + DATE_FORMAT);
+            return false;
+        }
+    }
+}
diff --git a/android/widget/CalendarViewLegacyDelegate.java b/android/widget/CalendarViewLegacyDelegate.java
new file mode 100644
index 0000000..1b899db
--- /dev/null
+++ b/android/widget/CalendarViewLegacyDelegate.java
@@ -0,0 +1,1578 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.app.Service;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.icu.util.Calendar;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.TypedValue;
+import android.view.GestureDetector;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Locale;
+
+/**
+ * A delegate implementing the legacy CalendarView
+ */
+class CalendarViewLegacyDelegate extends CalendarView.AbstractCalendarViewDelegate {
+    /**
+     * Default value whether to show week number.
+     */
+    private static final boolean DEFAULT_SHOW_WEEK_NUMBER = true;
+
+    /**
+     * The number of milliseconds in a day.e
+     */
+    private static final long MILLIS_IN_DAY = 86400000L;
+
+    /**
+     * The number of day in a week.
+     */
+    private static final int DAYS_PER_WEEK = 7;
+
+    /**
+     * The number of milliseconds in a week.
+     */
+    private static final long MILLIS_IN_WEEK = DAYS_PER_WEEK * MILLIS_IN_DAY;
+
+    /**
+     * Affects when the month selection will change while scrolling upe
+     */
+    private static final int SCROLL_HYST_WEEKS = 2;
+
+    /**
+     * How long the GoTo fling animation should last.
+     */
+    private static final int GOTO_SCROLL_DURATION = 1000;
+
+    /**
+     * The duration of the adjustment upon a user scroll in milliseconds.
+     */
+    private static final int ADJUSTMENT_SCROLL_DURATION = 500;
+
+    /**
+     * How long to wait after receiving an onScrollStateChanged notification
+     * before acting on it.
+     */
+    private static final int SCROLL_CHANGE_DELAY = 40;
+
+    private static final int DEFAULT_SHOWN_WEEK_COUNT = 6;
+
+    private static final int DEFAULT_DATE_TEXT_SIZE = 14;
+
+    private static final int UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH = 6;
+
+    private static final int UNSCALED_WEEK_MIN_VISIBLE_HEIGHT = 12;
+
+    private static final int UNSCALED_LIST_SCROLL_TOP_OFFSET = 2;
+
+    private static final int UNSCALED_BOTTOM_BUFFER = 20;
+
+    private static final int UNSCALED_WEEK_SEPARATOR_LINE_WIDTH = 1;
+
+    private static final int DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID = -1;
+
+    private final int mWeekSeparatorLineWidth;
+
+    private int mDateTextSize;
+
+    private Drawable mSelectedDateVerticalBar;
+
+    private final int mSelectedDateVerticalBarWidth;
+
+    private int mSelectedWeekBackgroundColor;
+
+    private int mFocusedMonthDateColor;
+
+    private int mUnfocusedMonthDateColor;
+
+    private int mWeekSeparatorLineColor;
+
+    private int mWeekNumberColor;
+
+    private int mWeekDayTextAppearanceResId;
+
+    private int mDateTextAppearanceResId;
+
+    /**
+     * The top offset of the weeks list.
+     */
+    private int mListScrollTopOffset = 2;
+
+    /**
+     * The visible height of a week view.
+     */
+    private int mWeekMinVisibleHeight = 12;
+
+    /**
+     * The visible height of a week view.
+     */
+    private int mBottomBuffer = 20;
+
+    /**
+     * The number of shown weeks.
+     */
+    private int mShownWeekCount;
+
+    /**
+     * Flag whether to show the week number.
+     */
+    private boolean mShowWeekNumber;
+
+    /**
+     * The number of day per week to be shown.
+     */
+    private int mDaysPerWeek = 7;
+
+    /**
+     * The friction of the week list while flinging.
+     */
+    private float mFriction = .05f;
+
+    /**
+     * Scale for adjusting velocity of the week list while flinging.
+     */
+    private float mVelocityScale = 0.333f;
+
+    /**
+     * The adapter for the weeks list.
+     */
+    private WeeksAdapter mAdapter;
+
+    /**
+     * The weeks list.
+     */
+    private ListView mListView;
+
+    /**
+     * The name of the month to display.
+     */
+    private TextView mMonthName;
+
+    /**
+     * The header with week day names.
+     */
+    private ViewGroup mDayNamesHeader;
+
+    /**
+     * Cached abbreviations for day of week names.
+     */
+    private String[] mDayNamesShort;
+
+    /**
+     * Cached full-length day of week names.
+     */
+    private String[] mDayNamesLong;
+
+    /**
+     * The first day of the week.
+     */
+    private int mFirstDayOfWeek;
+
+    /**
+     * Which month should be displayed/highlighted [0-11].
+     */
+    private int mCurrentMonthDisplayed = -1;
+
+    /**
+     * Used for tracking during a scroll.
+     */
+    private long mPreviousScrollPosition;
+
+    /**
+     * Used for tracking which direction the view is scrolling.
+     */
+    private boolean mIsScrollingUp = false;
+
+    /**
+     * The previous scroll state of the weeks ListView.
+     */
+    private int mPreviousScrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
+
+    /**
+     * The current scroll state of the weeks ListView.
+     */
+    private int mCurrentScrollState = AbsListView.OnScrollListener.SCROLL_STATE_IDLE;
+
+    /**
+     * Listener for changes in the selected day.
+     */
+    private CalendarView.OnDateChangeListener mOnDateChangeListener;
+
+    /**
+     * Command for adjusting the position after a scroll/fling.
+     */
+    private ScrollStateRunnable mScrollStateChangedRunnable = new ScrollStateRunnable();
+
+    /**
+     * Temporary instance to avoid multiple instantiations.
+     */
+    private Calendar mTempDate;
+
+    /**
+     * The first day of the focused month.
+     */
+    private Calendar mFirstDayOfMonth;
+
+    /**
+     * The start date of the range supported by this picker.
+     */
+    private Calendar mMinDate;
+
+    /**
+     * The end date of the range supported by this picker.
+     */
+    private Calendar mMaxDate;
+
+    CalendarViewLegacyDelegate(CalendarView delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.CalendarView, defStyleAttr, defStyleRes);
+        mShowWeekNumber = a.getBoolean(R.styleable.CalendarView_showWeekNumber,
+                DEFAULT_SHOW_WEEK_NUMBER);
+        mFirstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
+                LocaleData.get(Locale.getDefault()).firstDayOfWeek);
+        final String minDate = a.getString(R.styleable.CalendarView_minDate);
+        if (!CalendarView.parseDate(minDate, mMinDate)) {
+            CalendarView.parseDate(DEFAULT_MIN_DATE, mMinDate);
+        }
+        final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
+        if (!CalendarView.parseDate(maxDate, mMaxDate)) {
+            CalendarView.parseDate(DEFAULT_MAX_DATE, mMaxDate);
+        }
+        if (mMaxDate.before(mMinDate)) {
+            throw new IllegalArgumentException("Max date cannot be before min date.");
+        }
+        mShownWeekCount = a.getInt(R.styleable.CalendarView_shownWeekCount,
+                DEFAULT_SHOWN_WEEK_COUNT);
+        mSelectedWeekBackgroundColor = a.getColor(
+                R.styleable.CalendarView_selectedWeekBackgroundColor, 0);
+        mFocusedMonthDateColor = a.getColor(
+                R.styleable.CalendarView_focusedMonthDateColor, 0);
+        mUnfocusedMonthDateColor = a.getColor(
+                R.styleable.CalendarView_unfocusedMonthDateColor, 0);
+        mWeekSeparatorLineColor = a.getColor(
+                R.styleable.CalendarView_weekSeparatorLineColor, 0);
+        mWeekNumberColor = a.getColor(R.styleable.CalendarView_weekNumberColor, 0);
+        mSelectedDateVerticalBar = a.getDrawable(
+                R.styleable.CalendarView_selectedDateVerticalBar);
+
+        mDateTextAppearanceResId = a.getResourceId(
+                R.styleable.CalendarView_dateTextAppearance, R.style.TextAppearance_Small);
+        updateDateTextSize();
+
+        mWeekDayTextAppearanceResId = a.getResourceId(
+                R.styleable.CalendarView_weekDayTextAppearance,
+                DEFAULT_WEEK_DAY_TEXT_APPEARANCE_RES_ID);
+        a.recycle();
+
+        DisplayMetrics displayMetrics = mDelegator.getResources().getDisplayMetrics();
+        mWeekMinVisibleHeight = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                UNSCALED_WEEK_MIN_VISIBLE_HEIGHT, displayMetrics);
+        mListScrollTopOffset = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                UNSCALED_LIST_SCROLL_TOP_OFFSET, displayMetrics);
+        mBottomBuffer = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                UNSCALED_BOTTOM_BUFFER, displayMetrics);
+        mSelectedDateVerticalBarWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                UNSCALED_SELECTED_DATE_VERTICAL_BAR_WIDTH, displayMetrics);
+        mWeekSeparatorLineWidth = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                UNSCALED_WEEK_SEPARATOR_LINE_WIDTH, displayMetrics);
+
+        LayoutInflater layoutInflater = (LayoutInflater) mContext
+                .getSystemService(Service.LAYOUT_INFLATER_SERVICE);
+        View content = layoutInflater.inflate(R.layout.calendar_view, null, false);
+        mDelegator.addView(content);
+
+        mListView = mDelegator.findViewById(R.id.list);
+        mDayNamesHeader = content.findViewById(R.id.day_names);
+        mMonthName = content.findViewById(R.id.month_name);
+
+        setUpHeader();
+        setUpListView();
+        setUpAdapter();
+
+        // go to today or whichever is close to today min or max date
+        mTempDate.setTimeInMillis(System.currentTimeMillis());
+        if (mTempDate.before(mMinDate)) {
+            goTo(mMinDate, false, true, true);
+        } else if (mMaxDate.before(mTempDate)) {
+            goTo(mMaxDate, false, true, true);
+        } else {
+            goTo(mTempDate, false, true, true);
+        }
+
+        mDelegator.invalidate();
+    }
+
+    @Override
+    public void setShownWeekCount(int count) {
+        if (mShownWeekCount != count) {
+            mShownWeekCount = count;
+            mDelegator.invalidate();
+        }
+    }
+
+    @Override
+    public int getShownWeekCount() {
+        return mShownWeekCount;
+    }
+
+    @Override
+    public void setSelectedWeekBackgroundColor(int color) {
+        if (mSelectedWeekBackgroundColor != color) {
+            mSelectedWeekBackgroundColor = color;
+            final int childCount = mListView.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                WeekView weekView = (WeekView) mListView.getChildAt(i);
+                if (weekView.mHasSelectedDay) {
+                    weekView.invalidate();
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getSelectedWeekBackgroundColor() {
+        return mSelectedWeekBackgroundColor;
+    }
+
+    @Override
+    public void setFocusedMonthDateColor(int color) {
+        if (mFocusedMonthDateColor != color) {
+            mFocusedMonthDateColor = color;
+            final int childCount = mListView.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                WeekView weekView = (WeekView) mListView.getChildAt(i);
+                if (weekView.mHasFocusedDay) {
+                    weekView.invalidate();
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getFocusedMonthDateColor() {
+        return mFocusedMonthDateColor;
+    }
+
+    @Override
+    public void setUnfocusedMonthDateColor(int color) {
+        if (mUnfocusedMonthDateColor != color) {
+            mUnfocusedMonthDateColor = color;
+            final int childCount = mListView.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                WeekView weekView = (WeekView) mListView.getChildAt(i);
+                if (weekView.mHasUnfocusedDay) {
+                    weekView.invalidate();
+                }
+            }
+        }
+    }
+
+    @Override
+    public int getUnfocusedMonthDateColor() {
+        return mUnfocusedMonthDateColor;
+    }
+
+    @Override
+    public void setWeekNumberColor(int color) {
+        if (mWeekNumberColor != color) {
+            mWeekNumberColor = color;
+            if (mShowWeekNumber) {
+                invalidateAllWeekViews();
+            }
+        }
+    }
+
+    @Override
+    public int getWeekNumberColor() {
+        return mWeekNumberColor;
+    }
+
+    @Override
+    public void setWeekSeparatorLineColor(int color) {
+        if (mWeekSeparatorLineColor != color) {
+            mWeekSeparatorLineColor = color;
+            invalidateAllWeekViews();
+        }
+    }
+
+    @Override
+    public int getWeekSeparatorLineColor() {
+        return mWeekSeparatorLineColor;
+    }
+
+    @Override
+    public void setSelectedDateVerticalBar(int resourceId) {
+        Drawable drawable = mDelegator.getContext().getDrawable(resourceId);
+        setSelectedDateVerticalBar(drawable);
+    }
+
+    @Override
+    public void setSelectedDateVerticalBar(Drawable drawable) {
+        if (mSelectedDateVerticalBar != drawable) {
+            mSelectedDateVerticalBar = drawable;
+            final int childCount = mListView.getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                WeekView weekView = (WeekView) mListView.getChildAt(i);
+                if (weekView.mHasSelectedDay) {
+                    weekView.invalidate();
+                }
+            }
+        }
+    }
+
+    @Override
+    public Drawable getSelectedDateVerticalBar() {
+        return mSelectedDateVerticalBar;
+    }
+
+    @Override
+    public void setWeekDayTextAppearance(int resourceId) {
+        if (mWeekDayTextAppearanceResId != resourceId) {
+            mWeekDayTextAppearanceResId = resourceId;
+            setUpHeader();
+        }
+    }
+
+    @Override
+    public int getWeekDayTextAppearance() {
+        return mWeekDayTextAppearanceResId;
+    }
+
+    @Override
+    public void setDateTextAppearance(int resourceId) {
+        if (mDateTextAppearanceResId != resourceId) {
+            mDateTextAppearanceResId = resourceId;
+            updateDateTextSize();
+            invalidateAllWeekViews();
+        }
+    }
+
+    @Override
+    public int getDateTextAppearance() {
+        return mDateTextAppearanceResId;
+    }
+
+    @Override
+    public void setMinDate(long minDate) {
+        mTempDate.setTimeInMillis(minDate);
+        if (isSameDate(mTempDate, mMinDate)) {
+            return;
+        }
+        mMinDate.setTimeInMillis(minDate);
+        // make sure the current date is not earlier than
+        // the new min date since the latter is used for
+        // calculating the indices in the adapter thus
+        // avoiding out of bounds error
+        Calendar date = mAdapter.mSelectedDate;
+        if (date.before(mMinDate)) {
+            mAdapter.setSelectedDay(mMinDate);
+        }
+        // reinitialize the adapter since its range depends on min date
+        mAdapter.init();
+        if (date.before(mMinDate)) {
+            setDate(mTempDate.getTimeInMillis());
+        } else {
+            // we go to the current date to force the ListView to query its
+            // adapter for the shown views since we have changed the adapter
+            // range and the base from which the later calculates item indices
+            // note that calling setDate will not work since the date is the same
+            goTo(date, false, true, false);
+        }
+    }
+
+    @Override
+    public long getMinDate() {
+        return mMinDate.getTimeInMillis();
+    }
+
+    @Override
+    public void setMaxDate(long maxDate) {
+        mTempDate.setTimeInMillis(maxDate);
+        if (isSameDate(mTempDate, mMaxDate)) {
+            return;
+        }
+        mMaxDate.setTimeInMillis(maxDate);
+        // reinitialize the adapter since its range depends on max date
+        mAdapter.init();
+        Calendar date = mAdapter.mSelectedDate;
+        if (date.after(mMaxDate)) {
+            setDate(mMaxDate.getTimeInMillis());
+        } else {
+            // we go to the current date to force the ListView to query its
+            // adapter for the shown views since we have changed the adapter
+            // range and the base from which the later calculates item indices
+            // note that calling setDate will not work since the date is the same
+            goTo(date, false, true, false);
+        }
+    }
+
+    @Override
+    public long getMaxDate() {
+        return mMaxDate.getTimeInMillis();
+    }
+
+    @Override
+    public void setShowWeekNumber(boolean showWeekNumber) {
+        if (mShowWeekNumber == showWeekNumber) {
+            return;
+        }
+        mShowWeekNumber = showWeekNumber;
+        mAdapter.notifyDataSetChanged();
+        setUpHeader();
+    }
+
+    @Override
+    public boolean getShowWeekNumber() {
+        return mShowWeekNumber;
+    }
+
+    @Override
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        if (mFirstDayOfWeek == firstDayOfWeek) {
+            return;
+        }
+        mFirstDayOfWeek = firstDayOfWeek;
+        mAdapter.init();
+        mAdapter.notifyDataSetChanged();
+        setUpHeader();
+    }
+
+    @Override
+    public int getFirstDayOfWeek() {
+        return mFirstDayOfWeek;
+    }
+
+    @Override
+    public void setDate(long date) {
+        setDate(date, false, false);
+    }
+
+    @Override
+    public void setDate(long date, boolean animate, boolean center) {
+        mTempDate.setTimeInMillis(date);
+        if (isSameDate(mTempDate, mAdapter.mSelectedDate)) {
+            return;
+        }
+        goTo(mTempDate, animate, true, center);
+    }
+
+    @Override
+    public long getDate() {
+        return mAdapter.mSelectedDate.getTimeInMillis();
+    }
+
+    @Override
+    public void setOnDateChangeListener(CalendarView.OnDateChangeListener listener) {
+        mOnDateChangeListener = listener;
+    }
+
+    @Override
+    public boolean getBoundsForDate(long date, Rect outBounds) {
+        Calendar calendarDate = Calendar.getInstance();
+        calendarDate.setTimeInMillis(date);
+        int listViewEntryCount = mListView.getCount();
+        for (int i = 0; i < listViewEntryCount; i++) {
+            WeekView currWeekView = (WeekView) mListView.getChildAt(i);
+            if (currWeekView.getBoundsForDate(calendarDate, outBounds)) {
+                // Found the date in this week. Now need to offset vertically to return correct
+                // bounds in the coordinate system of the entire layout
+                final int[] weekViewPositionOnScreen = new int[2];
+                final int[] delegatorPositionOnScreen = new int[2];
+                currWeekView.getLocationOnScreen(weekViewPositionOnScreen);
+                mDelegator.getLocationOnScreen(delegatorPositionOnScreen);
+                final int extraVerticalOffset =
+                        weekViewPositionOnScreen[1] - delegatorPositionOnScreen[1];
+                outBounds.top += extraVerticalOffset;
+                outBounds.bottom += extraVerticalOffset;
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        setCurrentLocale(newConfig.locale);
+    }
+
+    /**
+     * Sets the current locale.
+     *
+     * @param locale The current locale.
+     */
+    @Override
+    protected void setCurrentLocale(Locale locale) {
+        super.setCurrentLocale(locale);
+
+        mTempDate = getCalendarForLocale(mTempDate, locale);
+        mFirstDayOfMonth = getCalendarForLocale(mFirstDayOfMonth, locale);
+        mMinDate = getCalendarForLocale(mMinDate, locale);
+        mMaxDate = getCalendarForLocale(mMaxDate, locale);
+    }
+    private void updateDateTextSize() {
+        TypedArray dateTextAppearance = mDelegator.getContext().obtainStyledAttributes(
+                mDateTextAppearanceResId, R.styleable.TextAppearance);
+        mDateTextSize = dateTextAppearance.getDimensionPixelSize(
+                R.styleable.TextAppearance_textSize, DEFAULT_DATE_TEXT_SIZE);
+        dateTextAppearance.recycle();
+    }
+
+    /**
+     * Invalidates all week views.
+     */
+    private void invalidateAllWeekViews() {
+        final int childCount = mListView.getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            View view = mListView.getChildAt(i);
+            view.invalidate();
+        }
+    }
+
+    /**
+     * Gets a calendar for locale bootstrapped with the value of a given calendar.
+     *
+     * @param oldCalendar The old calendar.
+     * @param locale The locale.
+     */
+    private static Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+        if (oldCalendar == null) {
+            return Calendar.getInstance(locale);
+        } else {
+            final long currentTimeMillis = oldCalendar.getTimeInMillis();
+            Calendar newCalendar = Calendar.getInstance(locale);
+            newCalendar.setTimeInMillis(currentTimeMillis);
+            return newCalendar;
+        }
+    }
+
+    /**
+     * @return True if the <code>firstDate</code> is the same as the <code>
+     * secondDate</code>.
+     */
+    private static boolean isSameDate(Calendar firstDate, Calendar secondDate) {
+        return (firstDate.get(Calendar.DAY_OF_YEAR) == secondDate.get(Calendar.DAY_OF_YEAR)
+                && firstDate.get(Calendar.YEAR) == secondDate.get(Calendar.YEAR));
+    }
+
+    /**
+     * Creates a new adapter if necessary and sets up its parameters.
+     */
+    private void setUpAdapter() {
+        if (mAdapter == null) {
+            mAdapter = new WeeksAdapter(mContext);
+            mAdapter.registerDataSetObserver(new DataSetObserver() {
+                @Override
+                public void onChanged() {
+                    if (mOnDateChangeListener != null) {
+                        Calendar selectedDay = mAdapter.getSelectedDay();
+                        mOnDateChangeListener.onSelectedDayChange(mDelegator,
+                                selectedDay.get(Calendar.YEAR),
+                                selectedDay.get(Calendar.MONTH),
+                                selectedDay.get(Calendar.DAY_OF_MONTH));
+                    }
+                }
+            });
+            mListView.setAdapter(mAdapter);
+        }
+
+        // refresh the view with the new parameters
+        mAdapter.notifyDataSetChanged();
+    }
+
+    /**
+     * Sets up the strings to be used by the header.
+     */
+    private void setUpHeader() {
+        mDayNamesShort = new String[mDaysPerWeek];
+        mDayNamesLong = new String[mDaysPerWeek];
+        for (int i = mFirstDayOfWeek, count = mFirstDayOfWeek + mDaysPerWeek; i < count; i++) {
+            int calendarDay = (i > Calendar.SATURDAY) ? i - Calendar.SATURDAY : i;
+            mDayNamesShort[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
+                    DateUtils.LENGTH_SHORTEST);
+            mDayNamesLong[i - mFirstDayOfWeek] = DateUtils.getDayOfWeekString(calendarDay,
+                    DateUtils.LENGTH_LONG);
+        }
+
+        TextView label = (TextView) mDayNamesHeader.getChildAt(0);
+        if (mShowWeekNumber) {
+            label.setVisibility(View.VISIBLE);
+        } else {
+            label.setVisibility(View.GONE);
+        }
+        for (int i = 1, count = mDayNamesHeader.getChildCount(); i < count; i++) {
+            label = (TextView) mDayNamesHeader.getChildAt(i);
+            if (mWeekDayTextAppearanceResId > -1) {
+                label.setTextAppearance(mWeekDayTextAppearanceResId);
+            }
+            if (i < mDaysPerWeek + 1) {
+                label.setText(mDayNamesShort[i - 1]);
+                label.setContentDescription(mDayNamesLong[i - 1]);
+                label.setVisibility(View.VISIBLE);
+            } else {
+                label.setVisibility(View.GONE);
+            }
+        }
+        mDayNamesHeader.invalidate();
+    }
+
+    /**
+     * Sets all the required fields for the list view.
+     */
+    private void setUpListView() {
+        // Configure the listview
+        mListView.setDivider(null);
+        mListView.setItemsCanFocus(true);
+        mListView.setVerticalScrollBarEnabled(false);
+        mListView.setOnScrollListener(new AbsListView.OnScrollListener() {
+            public void onScrollStateChanged(AbsListView view, int scrollState) {
+                CalendarViewLegacyDelegate.this.onScrollStateChanged(view, scrollState);
+            }
+
+            public void onScroll(
+                    AbsListView view, int firstVisibleItem, int visibleItemCount,
+                    int totalItemCount) {
+                CalendarViewLegacyDelegate.this.onScroll(view, firstVisibleItem,
+                        visibleItemCount, totalItemCount);
+            }
+        });
+        // Make the scrolling behavior nicer
+        mListView.setFriction(mFriction);
+        mListView.setVelocityScale(mVelocityScale);
+    }
+
+    /**
+     * This moves to the specified time in the view. If the time is not already
+     * in range it will move the list so that the first of the month containing
+     * the time is at the top of the view. If the new time is already in view
+     * the list will not be scrolled unless forceScroll is true. This time may
+     * optionally be highlighted as selected as well.
+     *
+     * @param date The time to move to.
+     * @param animate Whether to scroll to the given time or just redraw at the
+     *            new location.
+     * @param setSelected Whether to set the given time as selected.
+     * @param forceScroll Whether to recenter even if the time is already
+     *            visible.
+     *
+     * @throws IllegalArgumentException if the provided date is before the
+     *         range start or after the range end.
+     */
+    private void goTo(Calendar date, boolean animate, boolean setSelected,
+            boolean forceScroll) {
+        if (date.before(mMinDate) || date.after(mMaxDate)) {
+            throw new IllegalArgumentException("timeInMillis must be between the values of "
+                    + "getMinDate() and getMaxDate()");
+        }
+        // Find the first and last entirely visible weeks
+        int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
+        View firstChild = mListView.getChildAt(0);
+        if (firstChild != null && firstChild.getTop() < 0) {
+            firstFullyVisiblePosition++;
+        }
+        int lastFullyVisiblePosition = firstFullyVisiblePosition + mShownWeekCount - 1;
+        if (firstChild != null && firstChild.getTop() > mBottomBuffer) {
+            lastFullyVisiblePosition--;
+        }
+        if (setSelected) {
+            mAdapter.setSelectedDay(date);
+        }
+        // Get the week we're going to
+        int position = getWeeksSinceMinDate(date);
+
+        // Check if the selected day is now outside of our visible range
+        // and if so scroll to the month that contains it
+        if (position < firstFullyVisiblePosition || position > lastFullyVisiblePosition
+                || forceScroll) {
+            mFirstDayOfMonth.setTimeInMillis(date.getTimeInMillis());
+            mFirstDayOfMonth.set(Calendar.DAY_OF_MONTH, 1);
+
+            setMonthDisplayed(mFirstDayOfMonth);
+
+            // the earliest time we can scroll to is the min date
+            if (mFirstDayOfMonth.before(mMinDate)) {
+                position = 0;
+            } else {
+                position = getWeeksSinceMinDate(mFirstDayOfMonth);
+            }
+
+            mPreviousScrollState = AbsListView.OnScrollListener.SCROLL_STATE_FLING;
+            if (animate) {
+                mListView.smoothScrollToPositionFromTop(position, mListScrollTopOffset,
+                        GOTO_SCROLL_DURATION);
+            } else {
+                mListView.setSelectionFromTop(position, mListScrollTopOffset);
+                // Perform any after scroll operations that are needed
+                onScrollStateChanged(mListView, AbsListView.OnScrollListener.SCROLL_STATE_IDLE);
+            }
+        } else if (setSelected) {
+            // Otherwise just set the selection
+            setMonthDisplayed(date);
+        }
+    }
+
+    /**
+     * Called when a <code>view</code> transitions to a new <code>scrollState
+     * </code>.
+     */
+    private void onScrollStateChanged(AbsListView view, int scrollState) {
+        mScrollStateChangedRunnable.doScrollStateChange(view, scrollState);
+    }
+
+    /**
+     * Updates the title and selected month if the <code>view</code> has moved to a new
+     * month.
+     */
+    private void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                          int totalItemCount) {
+        WeekView child = (WeekView) view.getChildAt(0);
+        if (child == null) {
+            return;
+        }
+
+        // Figure out where we are
+        long currScroll =
+                view.getFirstVisiblePosition() * child.getHeight() - child.getBottom();
+
+        // If we have moved since our last call update the direction
+        if (currScroll < mPreviousScrollPosition) {
+            mIsScrollingUp = true;
+        } else if (currScroll > mPreviousScrollPosition) {
+            mIsScrollingUp = false;
+        } else {
+            return;
+        }
+
+        // Use some hysteresis for checking which month to highlight. This
+        // causes the month to transition when two full weeks of a month are
+        // visible when scrolling up, and when the first day in a month reaches
+        // the top of the screen when scrolling down.
+        int offset = child.getBottom() < mWeekMinVisibleHeight ? 1 : 0;
+        if (mIsScrollingUp) {
+            child = (WeekView) view.getChildAt(SCROLL_HYST_WEEKS + offset);
+        } else if (offset != 0) {
+            child = (WeekView) view.getChildAt(offset);
+        }
+
+        if (child != null) {
+            // Find out which month we're moving into
+            int month;
+            if (mIsScrollingUp) {
+                month = child.getMonthOfFirstWeekDay();
+            } else {
+                month = child.getMonthOfLastWeekDay();
+            }
+
+            // And how it relates to our current highlighted month
+            int monthDiff;
+            if (mCurrentMonthDisplayed == 11 && month == 0) {
+                monthDiff = 1;
+            } else if (mCurrentMonthDisplayed == 0 && month == 11) {
+                monthDiff = -1;
+            } else {
+                monthDiff = month - mCurrentMonthDisplayed;
+            }
+
+            // Only switch months if we're scrolling away from the currently
+            // selected month
+            if ((!mIsScrollingUp && monthDiff > 0) || (mIsScrollingUp && monthDiff < 0)) {
+                Calendar firstDay = child.getFirstDay();
+                if (mIsScrollingUp) {
+                    firstDay.add(Calendar.DAY_OF_MONTH, -DAYS_PER_WEEK);
+                } else {
+                    firstDay.add(Calendar.DAY_OF_MONTH, DAYS_PER_WEEK);
+                }
+                setMonthDisplayed(firstDay);
+            }
+        }
+        mPreviousScrollPosition = currScroll;
+        mPreviousScrollState = mCurrentScrollState;
+    }
+
+    /**
+     * Sets the month displayed at the top of this view based on time. Override
+     * to add custom events when the title is changed.
+     *
+     * @param calendar A day in the new focus month.
+     */
+    private void setMonthDisplayed(Calendar calendar) {
+        mCurrentMonthDisplayed = calendar.get(Calendar.MONTH);
+        mAdapter.setFocusMonth(mCurrentMonthDisplayed);
+        final int flags = DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_NO_MONTH_DAY
+                | DateUtils.FORMAT_SHOW_YEAR;
+        final long millis = calendar.getTimeInMillis();
+        String newMonthName = DateUtils.formatDateRange(mContext, millis, millis, flags);
+        mMonthName.setText(newMonthName);
+        mMonthName.invalidate();
+    }
+
+    /**
+     * @return Returns the number of weeks between the current <code>date</code>
+     *         and the <code>mMinDate</code>.
+     */
+    private int getWeeksSinceMinDate(Calendar date) {
+        if (date.before(mMinDate)) {
+            throw new IllegalArgumentException("fromDate: " + mMinDate.getTime()
+                    + " does not precede toDate: " + date.getTime());
+        }
+        long endTimeMillis = date.getTimeInMillis()
+                + date.getTimeZone().getOffset(date.getTimeInMillis());
+        long startTimeMillis = mMinDate.getTimeInMillis()
+                + mMinDate.getTimeZone().getOffset(mMinDate.getTimeInMillis());
+        long dayOffsetMillis = (mMinDate.get(Calendar.DAY_OF_WEEK) - mFirstDayOfWeek)
+                * MILLIS_IN_DAY;
+        return (int) ((endTimeMillis - startTimeMillis + dayOffsetMillis) / MILLIS_IN_WEEK);
+    }
+
+    /**
+     * Command responsible for acting upon scroll state changes.
+     */
+    private class ScrollStateRunnable implements Runnable {
+        private AbsListView mView;
+
+        private int mNewState;
+
+        /**
+         * Sets up the runnable with a short delay in case the scroll state
+         * immediately changes again.
+         *
+         * @param view The list view that changed state
+         * @param scrollState The new state it changed to
+         */
+        public void doScrollStateChange(AbsListView view, int scrollState) {
+            mView = view;
+            mNewState = scrollState;
+            mDelegator.removeCallbacks(this);
+            mDelegator.postDelayed(this, SCROLL_CHANGE_DELAY);
+        }
+
+        public void run() {
+            mCurrentScrollState = mNewState;
+            // Fix the position after a scroll or a fling ends
+            if (mNewState == AbsListView.OnScrollListener.SCROLL_STATE_IDLE
+                    && mPreviousScrollState != AbsListView.OnScrollListener.SCROLL_STATE_IDLE) {
+                View child = mView.getChildAt(0);
+                if (child == null) {
+                    // The view is no longer visible, just return
+                    return;
+                }
+                int dist = child.getBottom() - mListScrollTopOffset;
+                if (dist > mListScrollTopOffset) {
+                    if (mIsScrollingUp) {
+                        mView.smoothScrollBy(dist - child.getHeight(),
+                                ADJUSTMENT_SCROLL_DURATION);
+                    } else {
+                        mView.smoothScrollBy(dist, ADJUSTMENT_SCROLL_DURATION);
+                    }
+                }
+            }
+            mPreviousScrollState = mNewState;
+        }
+    }
+
+    /**
+     * <p>
+     * This is a specialized adapter for creating a list of weeks with
+     * selectable days. It can be configured to display the week number, start
+     * the week on a given day, show a reduced number of days, or display an
+     * arbitrary number of weeks at a time.
+     * </p>
+     */
+    private class WeeksAdapter extends BaseAdapter implements View.OnTouchListener {
+
+        private int mSelectedWeek;
+
+        private GestureDetector mGestureDetector;
+
+        private int mFocusedMonth;
+
+        private final Calendar mSelectedDate = Calendar.getInstance();
+
+        private int mTotalWeekCount;
+
+        public WeeksAdapter(Context context) {
+            mContext = context;
+            mGestureDetector = new GestureDetector(mContext, new WeeksAdapter.CalendarGestureListener());
+            init();
+        }
+
+        /**
+         * Set up the gesture detector and selected time
+         */
+        private void init() {
+            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
+            mTotalWeekCount = getWeeksSinceMinDate(mMaxDate);
+            if (mMinDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek
+                    || mMaxDate.get(Calendar.DAY_OF_WEEK) != mFirstDayOfWeek) {
+                mTotalWeekCount++;
+            }
+            notifyDataSetChanged();
+        }
+
+        /**
+         * Updates the selected day and related parameters.
+         *
+         * @param selectedDay The time to highlight
+         */
+        public void setSelectedDay(Calendar selectedDay) {
+            if (selectedDay.get(Calendar.DAY_OF_YEAR) == mSelectedDate.get(Calendar.DAY_OF_YEAR)
+                    && selectedDay.get(Calendar.YEAR) == mSelectedDate.get(Calendar.YEAR)) {
+                return;
+            }
+            mSelectedDate.setTimeInMillis(selectedDay.getTimeInMillis());
+            mSelectedWeek = getWeeksSinceMinDate(mSelectedDate);
+            mFocusedMonth = mSelectedDate.get(Calendar.MONTH);
+            notifyDataSetChanged();
+        }
+
+        /**
+         * @return The selected day of month.
+         */
+        public Calendar getSelectedDay() {
+            return mSelectedDate;
+        }
+
+        @Override
+        public int getCount() {
+            return mTotalWeekCount;
+        }
+
+        @Override
+        public Object getItem(int position) {
+            return null;
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return position;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            WeekView weekView = null;
+            if (convertView != null) {
+                weekView = (WeekView) convertView;
+            } else {
+                weekView = new WeekView(mContext);
+                AbsListView.LayoutParams params =
+                        new AbsListView.LayoutParams(FrameLayout.LayoutParams.WRAP_CONTENT,
+                                FrameLayout.LayoutParams.WRAP_CONTENT);
+                weekView.setLayoutParams(params);
+                weekView.setClickable(true);
+                weekView.setOnTouchListener(this);
+            }
+
+            int selectedWeekDay = (mSelectedWeek == position) ? mSelectedDate.get(
+                    Calendar.DAY_OF_WEEK) : -1;
+            weekView.init(position, selectedWeekDay, mFocusedMonth);
+
+            return weekView;
+        }
+
+        /**
+         * Changes which month is in focus and updates the view.
+         *
+         * @param month The month to show as in focus [0-11]
+         */
+        public void setFocusMonth(int month) {
+            if (mFocusedMonth == month) {
+                return;
+            }
+            mFocusedMonth = month;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (mListView.isEnabled() && mGestureDetector.onTouchEvent(event)) {
+                WeekView weekView = (WeekView) v;
+                // if we cannot find a day for the given location we are done
+                if (!weekView.getDayFromLocation(event.getX(), mTempDate)) {
+                    return true;
+                }
+                // it is possible that the touched day is outside the valid range
+                // we draw whole weeks but range end can fall not on the week end
+                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
+                    return true;
+                }
+                onDateTapped(mTempDate);
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Maintains the same hour/min/sec but moves the day to the tapped day.
+         *
+         * @param day The day that was tapped
+         */
+        private void onDateTapped(Calendar day) {
+            setSelectedDay(day);
+            setMonthDisplayed(day);
+        }
+
+        /**
+         * This is here so we can identify single tap events and set the
+         * selected day correctly
+         */
+        class CalendarGestureListener extends GestureDetector.SimpleOnGestureListener {
+            @Override
+            public boolean onSingleTapUp(MotionEvent e) {
+                return true;
+            }
+        }
+    }
+
+    /**
+     * <p>
+     * This is a dynamic view for drawing a single week. It can be configured to
+     * display the week number, start the week on a given day, or show a reduced
+     * number of days. It is intended for use as a single view within a
+     * ListView. See {@link WeeksAdapter} for usage.
+     * </p>
+     */
+    private class WeekView extends View {
+
+        private final Rect mTempRect = new Rect();
+
+        private final Paint mDrawPaint = new Paint();
+
+        private final Paint mMonthNumDrawPaint = new Paint();
+
+        // Cache the number strings so we don't have to recompute them each time
+        private String[] mDayNumbers;
+
+        // Quick lookup for checking which days are in the focus month
+        private boolean[] mFocusDay;
+
+        // Whether this view has a focused day.
+        private boolean mHasFocusedDay;
+
+        // Whether this view has only focused days.
+        private boolean mHasUnfocusedDay;
+
+        // The first day displayed by this item
+        private Calendar mFirstDay;
+
+        // The month of the first day in this week
+        private int mMonthOfFirstWeekDay = -1;
+
+        // The month of the last day in this week
+        private int mLastWeekDayMonth = -1;
+
+        // The position of this week, equivalent to weeks since the week of Jan
+        // 1st, 1900
+        private int mWeek = -1;
+
+        // Quick reference to the width of this view, matches parent
+        private int mWidth;
+
+        // The height this view should draw at in pixels, set by height param
+        private int mHeight;
+
+        // If this view contains the selected day
+        private boolean mHasSelectedDay = false;
+
+        // Which day is selected [0-6] or -1 if no day is selected
+        private int mSelectedDay = -1;
+
+        // The number of days + a spot for week number if it is displayed
+        private int mNumCells;
+
+        // The left edge of the selected day
+        private int mSelectedLeft = -1;
+
+        // The right edge of the selected day
+        private int mSelectedRight = -1;
+
+        public WeekView(Context context) {
+            super(context);
+
+            // Sets up any standard paints that will be used
+            initializePaints();
+        }
+
+        /**
+         * Initializes this week view.
+         *
+         * @param weekNumber The number of the week this view represents. The
+         *            week number is a zero based index of the weeks since
+         *            {@link android.widget.CalendarView#getMinDate()}.
+         * @param selectedWeekDay The selected day of the week from 0 to 6, -1 if no
+         *            selected day.
+         * @param focusedMonth The month that is currently in focus i.e.
+         *            highlighted.
+         */
+        public void init(int weekNumber, int selectedWeekDay, int focusedMonth) {
+            mSelectedDay = selectedWeekDay;
+            mHasSelectedDay = mSelectedDay != -1;
+            mNumCells = mShowWeekNumber ? mDaysPerWeek + 1 : mDaysPerWeek;
+            mWeek = weekNumber;
+            mTempDate.setTimeInMillis(mMinDate.getTimeInMillis());
+
+            mTempDate.add(Calendar.WEEK_OF_YEAR, mWeek);
+            mTempDate.setFirstDayOfWeek(mFirstDayOfWeek);
+
+            // Allocate space for caching the day numbers and focus values
+            mDayNumbers = new String[mNumCells];
+            mFocusDay = new boolean[mNumCells];
+
+            // If we're showing the week number calculate it based on Monday
+            int i = 0;
+            if (mShowWeekNumber) {
+                mDayNumbers[0] = String.format(Locale.getDefault(), "%d",
+                        mTempDate.get(Calendar.WEEK_OF_YEAR));
+                i++;
+            }
+
+            // Now adjust our starting day based on the start day of the week
+            int diff = mFirstDayOfWeek - mTempDate.get(Calendar.DAY_OF_WEEK);
+            mTempDate.add(Calendar.DAY_OF_MONTH, diff);
+
+            mFirstDay = (Calendar) mTempDate.clone();
+            mMonthOfFirstWeekDay = mTempDate.get(Calendar.MONTH);
+
+            mHasUnfocusedDay = true;
+            for (; i < mNumCells; i++) {
+                final boolean isFocusedDay = (mTempDate.get(Calendar.MONTH) == focusedMonth);
+                mFocusDay[i] = isFocusedDay;
+                mHasFocusedDay |= isFocusedDay;
+                mHasUnfocusedDay &= !isFocusedDay;
+                // do not draw dates outside the valid range to avoid user confusion
+                if (mTempDate.before(mMinDate) || mTempDate.after(mMaxDate)) {
+                    mDayNumbers[i] = "";
+                } else {
+                    mDayNumbers[i] = String.format(Locale.getDefault(), "%d",
+                            mTempDate.get(Calendar.DAY_OF_MONTH));
+                }
+                mTempDate.add(Calendar.DAY_OF_MONTH, 1);
+            }
+            // We do one extra add at the end of the loop, if that pushed us to
+            // new month undo it
+            if (mTempDate.get(Calendar.DAY_OF_MONTH) == 1) {
+                mTempDate.add(Calendar.DAY_OF_MONTH, -1);
+            }
+            mLastWeekDayMonth = mTempDate.get(Calendar.MONTH);
+
+            updateSelectionPositions();
+        }
+
+        /**
+         * Initialize the paint instances.
+         */
+        private void initializePaints() {
+            mDrawPaint.setFakeBoldText(false);
+            mDrawPaint.setAntiAlias(true);
+            mDrawPaint.setStyle(Paint.Style.FILL);
+
+            mMonthNumDrawPaint.setFakeBoldText(true);
+            mMonthNumDrawPaint.setAntiAlias(true);
+            mMonthNumDrawPaint.setStyle(Paint.Style.FILL);
+            mMonthNumDrawPaint.setTextAlign(Paint.Align.CENTER);
+            mMonthNumDrawPaint.setTextSize(mDateTextSize);
+        }
+
+        /**
+         * Returns the month of the first day in this week.
+         *
+         * @return The month the first day of this view is in.
+         */
+        public int getMonthOfFirstWeekDay() {
+            return mMonthOfFirstWeekDay;
+        }
+
+        /**
+         * Returns the month of the last day in this week
+         *
+         * @return The month the last day of this view is in
+         */
+        public int getMonthOfLastWeekDay() {
+            return mLastWeekDayMonth;
+        }
+
+        /**
+         * Returns the first day in this view.
+         *
+         * @return The first day in the view.
+         */
+        public Calendar getFirstDay() {
+            return mFirstDay;
+        }
+
+        /**
+         * Calculates the day that the given x position is in, accounting for
+         * week number.
+         *
+         * @param x The x position of the touch event.
+         * @return True if a day was found for the given location.
+         */
+        public boolean getDayFromLocation(float x, Calendar outCalendar) {
+            final boolean isLayoutRtl = isLayoutRtl();
+
+            int start;
+            int end;
+
+            if (isLayoutRtl) {
+                start = 0;
+                end = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+            } else {
+                start = mShowWeekNumber ? mWidth / mNumCells : 0;
+                end = mWidth;
+            }
+
+            if (x < start || x > end) {
+                outCalendar.clear();
+                return false;
+            }
+
+            // Selection is (x - start) / (pixels/day) which is (x - start) * day / pixels
+            int dayPosition = (int) ((x - start) * mDaysPerWeek / (end - start));
+
+            if (isLayoutRtl) {
+                dayPosition = mDaysPerWeek - 1 - dayPosition;
+            }
+
+            outCalendar.setTimeInMillis(mFirstDay.getTimeInMillis());
+            outCalendar.add(Calendar.DAY_OF_MONTH, dayPosition);
+
+            return true;
+        }
+
+        public boolean getBoundsForDate(Calendar date, Rect outBounds) {
+            Calendar currDay = Calendar.getInstance();
+            currDay.setTime(mFirstDay.getTime());
+            for (int i = 0; i < mDaysPerWeek; i++) {
+                if ((date.get(Calendar.YEAR) == currDay.get(Calendar.YEAR))
+                    && (date.get(Calendar.MONTH) == currDay.get(Calendar.MONTH))
+                    && (date.get(Calendar.DAY_OF_MONTH) == currDay.get(Calendar.DAY_OF_MONTH))) {
+                    // We found the matching date. Follow the logic in the draw pass that divides
+                    // the available horizontal space equally between all the entries in this week.
+                    // Note that if we're showing week number, the start entry will be that number.
+                    int cellSize = mWidth / mNumCells;
+                    if (isLayoutRtl()) {
+                        outBounds.left = cellSize *
+                                (mShowWeekNumber ? (mNumCells - i - 2) : (mNumCells - i - 1));
+                    } else {
+                        outBounds.left = cellSize * (mShowWeekNumber ? i + 1 : i);
+                    }
+                    outBounds.top = 0;
+                    outBounds.right = outBounds.left + cellSize;
+                    outBounds.bottom = getHeight();
+                    return true;
+                }
+                // Add one day
+                currDay.add(Calendar.DAY_OF_MONTH, 1);
+            }
+            return false;
+        }
+
+        @Override
+        protected void onDraw(Canvas canvas) {
+            drawBackground(canvas);
+            drawWeekNumbersAndDates(canvas);
+            drawWeekSeparators(canvas);
+            drawSelectedDateVerticalBars(canvas);
+        }
+
+        /**
+         * This draws the selection highlight if a day is selected in this week.
+         *
+         * @param canvas The canvas to draw on
+         */
+        private void drawBackground(Canvas canvas) {
+            if (!mHasSelectedDay) {
+                return;
+            }
+            mDrawPaint.setColor(mSelectedWeekBackgroundColor);
+
+            mTempRect.top = mWeekSeparatorLineWidth;
+            mTempRect.bottom = mHeight;
+
+            final boolean isLayoutRtl = isLayoutRtl();
+
+            if (isLayoutRtl) {
+                mTempRect.left = 0;
+                mTempRect.right = mSelectedLeft - 2;
+            } else {
+                mTempRect.left = mShowWeekNumber ? mWidth / mNumCells : 0;
+                mTempRect.right = mSelectedLeft - 2;
+            }
+            canvas.drawRect(mTempRect, mDrawPaint);
+
+            if (isLayoutRtl) {
+                mTempRect.left = mSelectedRight + 3;
+                mTempRect.right = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+            } else {
+                mTempRect.left = mSelectedRight + 3;
+                mTempRect.right = mWidth;
+            }
+            canvas.drawRect(mTempRect, mDrawPaint);
+        }
+
+        /**
+         * Draws the week and month day numbers for this week.
+         *
+         * @param canvas The canvas to draw on
+         */
+        private void drawWeekNumbersAndDates(Canvas canvas) {
+            final float textHeight = mDrawPaint.getTextSize();
+            final int y = (int) ((mHeight + textHeight) / 2) - mWeekSeparatorLineWidth;
+            final int nDays = mNumCells;
+            final int divisor = 2 * nDays;
+
+            mDrawPaint.setTextAlign(Paint.Align.CENTER);
+            mDrawPaint.setTextSize(mDateTextSize);
+
+            int i = 0;
+
+            if (isLayoutRtl()) {
+                for (; i < nDays - 1; i++) {
+                    mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
+                            : mUnfocusedMonthDateColor);
+                    int x = (2 * i + 1) * mWidth / divisor;
+                    canvas.drawText(mDayNumbers[nDays - 1 - i], x, y, mMonthNumDrawPaint);
+                }
+                if (mShowWeekNumber) {
+                    mDrawPaint.setColor(mWeekNumberColor);
+                    int x = mWidth - mWidth / divisor;
+                    canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
+                }
+            } else {
+                if (mShowWeekNumber) {
+                    mDrawPaint.setColor(mWeekNumberColor);
+                    int x = mWidth / divisor;
+                    canvas.drawText(mDayNumbers[0], x, y, mDrawPaint);
+                    i++;
+                }
+                for (; i < nDays; i++) {
+                    mMonthNumDrawPaint.setColor(mFocusDay[i] ? mFocusedMonthDateColor
+                            : mUnfocusedMonthDateColor);
+                    int x = (2 * i + 1) * mWidth / divisor;
+                    canvas.drawText(mDayNumbers[i], x, y, mMonthNumDrawPaint);
+                }
+            }
+        }
+
+        /**
+         * Draws a horizontal line for separating the weeks.
+         *
+         * @param canvas The canvas to draw on.
+         */
+        private void drawWeekSeparators(Canvas canvas) {
+            // If it is the topmost fully visible child do not draw separator line
+            int firstFullyVisiblePosition = mListView.getFirstVisiblePosition();
+            if (mListView.getChildAt(0).getTop() < 0) {
+                firstFullyVisiblePosition++;
+            }
+            if (firstFullyVisiblePosition == mWeek) {
+                return;
+            }
+            mDrawPaint.setColor(mWeekSeparatorLineColor);
+            mDrawPaint.setStrokeWidth(mWeekSeparatorLineWidth);
+            float startX;
+            float stopX;
+            if (isLayoutRtl()) {
+                startX = 0;
+                stopX = mShowWeekNumber ? mWidth - mWidth / mNumCells : mWidth;
+            } else {
+                startX = mShowWeekNumber ? mWidth / mNumCells : 0;
+                stopX = mWidth;
+            }
+            canvas.drawLine(startX, 0, stopX, 0, mDrawPaint);
+        }
+
+        /**
+         * Draws the selected date bars if this week has a selected day.
+         *
+         * @param canvas The canvas to draw on
+         */
+        private void drawSelectedDateVerticalBars(Canvas canvas) {
+            if (!mHasSelectedDay) {
+                return;
+            }
+            mSelectedDateVerticalBar.setBounds(
+                    mSelectedLeft - mSelectedDateVerticalBarWidth / 2,
+                    mWeekSeparatorLineWidth,
+                    mSelectedLeft + mSelectedDateVerticalBarWidth / 2,
+                    mHeight);
+            mSelectedDateVerticalBar.draw(canvas);
+            mSelectedDateVerticalBar.setBounds(
+                    mSelectedRight - mSelectedDateVerticalBarWidth / 2,
+                    mWeekSeparatorLineWidth,
+                    mSelectedRight + mSelectedDateVerticalBarWidth / 2,
+                    mHeight);
+            mSelectedDateVerticalBar.draw(canvas);
+        }
+
+        @Override
+        protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+            mWidth = w;
+            updateSelectionPositions();
+        }
+
+        /**
+         * This calculates the positions for the selected day lines.
+         */
+        private void updateSelectionPositions() {
+            if (mHasSelectedDay) {
+                final boolean isLayoutRtl = isLayoutRtl();
+                int selectedPosition = mSelectedDay - mFirstDayOfWeek;
+                if (selectedPosition < 0) {
+                    selectedPosition += 7;
+                }
+                if (mShowWeekNumber && !isLayoutRtl) {
+                    selectedPosition++;
+                }
+                if (isLayoutRtl) {
+                    mSelectedLeft = (mDaysPerWeek - 1 - selectedPosition) * mWidth / mNumCells;
+
+                } else {
+                    mSelectedLeft = selectedPosition * mWidth / mNumCells;
+                }
+                mSelectedRight = mSelectedLeft + mWidth / mNumCells;
+            }
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            mHeight = (mListView.getHeight() - mListView.getPaddingTop() - mListView
+                    .getPaddingBottom()) / mShownWeekCount;
+            setMeasuredDimension(MeasureSpec.getSize(widthMeasureSpec), mHeight);
+        }
+    }
+
+}
diff --git a/android/widget/CalendarViewMaterialDelegate.java b/android/widget/CalendarViewMaterialDelegate.java
new file mode 100644
index 0000000..b752eb6
--- /dev/null
+++ b/android/widget/CalendarViewMaterialDelegate.java
@@ -0,0 +1,143 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.widget.DayPickerView.OnDaySelectedListener;
+
+class CalendarViewMaterialDelegate extends CalendarView.AbstractCalendarViewDelegate {
+    private final DayPickerView mDayPickerView;
+
+    private CalendarView.OnDateChangeListener mOnDateChangeListener;
+
+    public CalendarViewMaterialDelegate(CalendarView delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        mDayPickerView = new DayPickerView(context, attrs, defStyleAttr, defStyleRes);
+        mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
+
+        delegator.addView(mDayPickerView);
+    }
+
+    @Override
+    public void setWeekDayTextAppearance(@StyleRes int resId) {
+        mDayPickerView.setDayOfWeekTextAppearance(resId);
+    }
+
+    @StyleRes
+    @Override
+    public int getWeekDayTextAppearance() {
+        return mDayPickerView.getDayOfWeekTextAppearance();
+    }
+
+    @Override
+    public void setDateTextAppearance(@StyleRes int resId) {
+        mDayPickerView.setDayTextAppearance(resId);
+    }
+
+    @StyleRes
+    @Override
+    public int getDateTextAppearance() {
+        return mDayPickerView.getDayTextAppearance();
+    }
+
+    @Override
+    public void setMinDate(long minDate) {
+        mDayPickerView.setMinDate(minDate);
+    }
+
+    @Override
+    public long getMinDate() {
+        return mDayPickerView.getMinDate();
+    }
+
+    @Override
+    public void setMaxDate(long maxDate) {
+        mDayPickerView.setMaxDate(maxDate);
+    }
+
+    @Override
+    public long getMaxDate() {
+        return mDayPickerView.getMaxDate();
+    }
+
+    @Override
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    @Override
+    public int getFirstDayOfWeek() {
+        return mDayPickerView.getFirstDayOfWeek();
+    }
+
+    @Override
+    public void setDate(long date) {
+        mDayPickerView.setDate(date, true);
+    }
+
+    @Override
+    public void setDate(long date, boolean animate, boolean center) {
+        mDayPickerView.setDate(date, animate);
+    }
+
+    @Override
+    public long getDate() {
+        return mDayPickerView.getDate();
+    }
+
+    @Override
+    public void setOnDateChangeListener(CalendarView.OnDateChangeListener listener) {
+        mOnDateChangeListener = listener;
+    }
+
+    @Override
+    public boolean getBoundsForDate(long date, Rect outBounds) {
+        boolean result = mDayPickerView.getBoundsForDate(date, outBounds);
+        if (result) {
+            // Found the date in the current picker. Now need to offset vertically to return correct
+            // bounds in the coordinate system of the entire layout
+            final int[] dayPickerPositionOnScreen = new int[2];
+            final int[] delegatorPositionOnScreen = new int[2];
+            mDayPickerView.getLocationOnScreen(dayPickerPositionOnScreen);
+            mDelegator.getLocationOnScreen(delegatorPositionOnScreen);
+            final int extraVerticalOffset =
+                    dayPickerPositionOnScreen[1] - delegatorPositionOnScreen[1];
+            outBounds.top += extraVerticalOffset;
+            outBounds.bottom += extraVerticalOffset;
+            return true;
+        }
+        return false;
+    }
+
+    private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
+        @Override
+        public void onDaySelected(DayPickerView view, Calendar day) {
+            if (mOnDateChangeListener != null) {
+                final int year = day.get(Calendar.YEAR);
+                final int month = day.get(Calendar.MONTH);
+                final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+                mOnDateChangeListener.onSelectedDayChange(mDelegator, year, month, dayOfMonth);
+            }
+        }
+    };
+}
diff --git a/android/widget/CheckBox.java b/android/widget/CheckBox.java
new file mode 100644
index 0000000..046f75f
--- /dev/null
+++ b/android/widget/CheckBox.java
@@ -0,0 +1,76 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+/**
+ * <p>
+ * A checkbox is a specific type of two-states button that can be either
+ * checked or unchecked. A example usage of a checkbox inside your activity
+ * would be the following:
+ * </p>
+ *
+ * <pre class="prettyprint">
+ * public class MyActivity extends Activity {
+ *     protected void onCreate(Bundle icicle) {
+ *         super.onCreate(icicle);
+ *
+ *         setContentView(R.layout.content_layout_id);
+ *
+ *         final CheckBox checkBox = (CheckBox) findViewById(R.id.checkbox_id);
+ *         if (checkBox.isChecked()) {
+ *             checkBox.setChecked(false);
+ *         }
+ *     }
+ * }
+ * </pre>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/checkbox.html">Checkboxes</a>
+ * guide.</p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#CompoundButton CompoundButton Attributes},
+ * {@link android.R.styleable#Button Button Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class CheckBox extends CompoundButton {
+    public CheckBox(Context context) {
+        this(context, null);
+    }
+
+    public CheckBox(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.checkboxStyle);
+    }
+
+    public CheckBox(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CheckBox(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return CheckBox.class.getName();
+    }
+}
diff --git a/android/widget/Checkable.java b/android/widget/Checkable.java
new file mode 100644
index 0000000..eb97b4a
--- /dev/null
+++ b/android/widget/Checkable.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+/**
+ * Defines an extension for views that make them checkable.
+ *
+ */
+public interface Checkable {
+    
+    /**
+     * Change the checked state of the view
+     * 
+     * @param checked The new checked state
+     */
+    void setChecked(boolean checked);
+        
+    /**
+     * @return The current checked state of the view
+     */
+    boolean isChecked();
+    
+    /**
+     * Change the checked state of the view to the inverse of its current state
+     *
+     */
+    void toggle();
+}
diff --git a/android/widget/CheckedTextView.java b/android/widget/CheckedTextView.java
new file mode 100644
index 0000000..92bfd56
--- /dev/null
+++ b/android/widget/CheckedTextView.java
@@ -0,0 +1,538 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * An extension to {@link TextView} that supports the {@link Checkable}
+ * interface and displays.
+ * <p>
+ * This is useful when used in a {@link android.widget.ListView ListView} where
+ * the {@link android.widget.ListView#setChoiceMode(int) setChoiceMode} has
+ * been set to something other than
+ * {@link android.widget.ListView#CHOICE_MODE_NONE CHOICE_MODE_NONE}.
+ *
+ * @attr ref android.R.styleable#CheckedTextView_checked
+ * @attr ref android.R.styleable#CheckedTextView_checkMark
+ */
+public class CheckedTextView extends TextView implements Checkable {
+    private boolean mChecked;
+
+    private int mCheckMarkResource;
+    private Drawable mCheckMarkDrawable;
+    private ColorStateList mCheckMarkTintList = null;
+    private PorterDuff.Mode mCheckMarkTintMode = null;
+    private boolean mHasCheckMarkTint = false;
+    private boolean mHasCheckMarkTintMode = false;
+
+    private int mBasePadding;
+    private int mCheckMarkWidth;
+    private int mCheckMarkGravity = Gravity.END;
+
+    private boolean mNeedRequestlayout;
+
+    private static final int[] CHECKED_STATE_SET = {
+        R.attr.state_checked
+    };
+
+    public CheckedTextView(Context context) {
+        this(context, null);
+    }
+
+    public CheckedTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.checkedTextViewStyle);
+    }
+
+    public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CheckedTextView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.CheckedTextView, defStyleAttr, defStyleRes);
+
+        final Drawable d = a.getDrawable(R.styleable.CheckedTextView_checkMark);
+        if (d != null) {
+            setCheckMarkDrawable(d);
+        }
+
+        if (a.hasValue(R.styleable.CheckedTextView_checkMarkTintMode)) {
+            mCheckMarkTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.CheckedTextView_checkMarkTintMode, -1), mCheckMarkTintMode);
+            mHasCheckMarkTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.CheckedTextView_checkMarkTint)) {
+            mCheckMarkTintList = a.getColorStateList(R.styleable.CheckedTextView_checkMarkTint);
+            mHasCheckMarkTint = true;
+        }
+
+        mCheckMarkGravity = a.getInt(R.styleable.CheckedTextView_checkMarkGravity, Gravity.END);
+
+        final boolean checked = a.getBoolean(R.styleable.CheckedTextView_checked, false);
+        setChecked(checked);
+
+        a.recycle();
+
+        applyCheckMarkTint();
+    }
+
+    public void toggle() {
+        setChecked(!mChecked);
+    }
+
+    @ViewDebug.ExportedProperty
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    /**
+     * Sets the checked state of this view.
+     *
+     * @param checked {@code true} set the state to checked, {@code false} to
+     *                uncheck
+     */
+    public void setChecked(boolean checked) {
+        if (mChecked != checked) {
+            mChecked = checked;
+            refreshDrawableState();
+            notifyViewAccessibilityStateChangedIfNeeded(
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+        }
+    }
+
+    /**
+     * Sets the check mark to the drawable with the specified resource ID.
+     * <p>
+     * When this view is checked, the drawable's state set will include
+     * {@link android.R.attr#state_checked}.
+     *
+     * @param resId the resource identifier of drawable to use as the check
+     *              mark
+     * @attr ref android.R.styleable#CheckedTextView_checkMark
+     * @see #setCheckMarkDrawable(Drawable)
+     * @see #getCheckMarkDrawable()
+     */
+    public void setCheckMarkDrawable(@DrawableRes int resId) {
+        if (resId != 0 && resId == mCheckMarkResource) {
+            return;
+        }
+
+        final Drawable d = resId != 0 ? getContext().getDrawable(resId) : null;
+        setCheckMarkDrawableInternal(d, resId);
+    }
+
+    /**
+     * Set the check mark to the specified drawable.
+     * <p>
+     * When this view is checked, the drawable's state set will include
+     * {@link android.R.attr#state_checked}.
+     *
+     * @param d the drawable to use for the check mark
+     * @attr ref android.R.styleable#CheckedTextView_checkMark
+     * @see #setCheckMarkDrawable(int)
+     * @see #getCheckMarkDrawable()
+     */
+    public void setCheckMarkDrawable(@Nullable Drawable d) {
+        setCheckMarkDrawableInternal(d, 0);
+    }
+
+    private void setCheckMarkDrawableInternal(@Nullable Drawable d, @DrawableRes int resId) {
+        if (mCheckMarkDrawable != null) {
+            mCheckMarkDrawable.setCallback(null);
+            unscheduleDrawable(mCheckMarkDrawable);
+        }
+
+        mNeedRequestlayout = (d != mCheckMarkDrawable);
+
+        if (d != null) {
+            d.setCallback(this);
+            d.setVisible(getVisibility() == VISIBLE, false);
+            d.setState(CHECKED_STATE_SET);
+
+            // Record the intrinsic dimensions when in "checked" state.
+            setMinHeight(d.getIntrinsicHeight());
+            mCheckMarkWidth = d.getIntrinsicWidth();
+
+            d.setState(getDrawableState());
+        } else {
+            mCheckMarkWidth = 0;
+        }
+
+        mCheckMarkDrawable = d;
+        mCheckMarkResource = resId;
+
+        applyCheckMarkTint();
+
+        // Do padding resolution. This will call internalSetPadding() and do a
+        // requestLayout() if needed.
+        resolvePadding();
+    }
+
+    /**
+     * Applies a tint to the check mark drawable. Does not modify the
+     * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setCheckMarkDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and
+     * tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
+     * @see #getCheckMarkTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setCheckMarkTintList(@Nullable ColorStateList tint) {
+        mCheckMarkTintList = tint;
+        mHasCheckMarkTint = true;
+
+        applyCheckMarkTint();
+    }
+
+    /**
+     * Returns the tint applied to the check mark drawable, if specified.
+     *
+     * @return the tint applied to the check mark drawable
+     * @attr ref android.R.styleable#CheckedTextView_checkMarkTint
+     * @see #setCheckMarkTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getCheckMarkTintList() {
+        return mCheckMarkTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setCheckMarkTintList(ColorStateList)} to the check mark
+     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
+     * @see #setCheckMarkTintList(ColorStateList)
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setCheckMarkTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mCheckMarkTintMode = tintMode;
+        mHasCheckMarkTintMode = true;
+
+        applyCheckMarkTint();
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the check mark
+     * drawable, if specified.
+     *
+     * @return the blending mode used to apply the tint to the check mark
+     *         drawable
+     * @attr ref android.R.styleable#CheckedTextView_checkMarkTintMode
+     * @see #setCheckMarkTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getCheckMarkTintMode() {
+        return mCheckMarkTintMode;
+    }
+
+    private void applyCheckMarkTint() {
+        if (mCheckMarkDrawable != null && (mHasCheckMarkTint || mHasCheckMarkTintMode)) {
+            mCheckMarkDrawable = mCheckMarkDrawable.mutate();
+
+            if (mHasCheckMarkTint) {
+                mCheckMarkDrawable.setTintList(mCheckMarkTintList);
+            }
+
+            if (mHasCheckMarkTintMode) {
+                mCheckMarkDrawable.setTintMode(mCheckMarkTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mCheckMarkDrawable.isStateful()) {
+                mCheckMarkDrawable.setState(getDrawableState());
+            }
+        }
+    }
+
+    @RemotableViewMethod
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+
+        if (mCheckMarkDrawable != null) {
+            mCheckMarkDrawable.setVisible(visibility == VISIBLE, false);
+        }
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+
+        if (mCheckMarkDrawable != null) {
+            mCheckMarkDrawable.jumpToCurrentState();
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return who == mCheckMarkDrawable || super.verifyDrawable(who);
+    }
+
+    /**
+     * Gets the checkmark drawable
+     *
+     * @return The drawable use to represent the checkmark, if any.
+     *
+     * @see #setCheckMarkDrawable(Drawable)
+     * @see #setCheckMarkDrawable(int)
+     *
+     * @attr ref android.R.styleable#CheckedTextView_checkMark
+     */
+    public Drawable getCheckMarkDrawable() {
+        return mCheckMarkDrawable;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected void internalSetPadding(int left, int top, int right, int bottom) {
+        super.internalSetPadding(left, top, right, bottom);
+        setBasePadding(isCheckMarkAtStart());
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        updatePadding();
+    }
+
+    private void updatePadding() {
+        resetPaddingToInitialValues();
+        int newPadding = (mCheckMarkDrawable != null) ?
+                mCheckMarkWidth + mBasePadding : mBasePadding;
+        if (isCheckMarkAtStart()) {
+            mNeedRequestlayout |= (mPaddingLeft != newPadding);
+            mPaddingLeft = newPadding;
+        } else {
+            mNeedRequestlayout |= (mPaddingRight != newPadding);
+            mPaddingRight = newPadding;
+        }
+        if (mNeedRequestlayout) {
+            requestLayout();
+            mNeedRequestlayout = false;
+        }
+    }
+
+    private void setBasePadding(boolean checkmarkAtStart) {
+        if (checkmarkAtStart) {
+            mBasePadding = mPaddingLeft;
+        } else {
+            mBasePadding = mPaddingRight;
+        }
+    }
+
+    private boolean isCheckMarkAtStart() {
+        final int gravity = Gravity.getAbsoluteGravity(mCheckMarkGravity, getLayoutDirection());
+        final int hgrav = gravity & Gravity.HORIZONTAL_GRAVITY_MASK;
+        return hgrav == Gravity.LEFT;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        final Drawable checkMarkDrawable = mCheckMarkDrawable;
+        if (checkMarkDrawable != null) {
+            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+            final int height = checkMarkDrawable.getIntrinsicHeight();
+
+            int y = 0;
+
+            switch (verticalGravity) {
+                case Gravity.BOTTOM:
+                    y = getHeight() - height;
+                    break;
+                case Gravity.CENTER_VERTICAL:
+                    y = (getHeight() - height) / 2;
+                    break;
+            }
+
+            final boolean checkMarkAtStart = isCheckMarkAtStart();
+            final int width = getWidth();
+            final int top = y;
+            final int bottom = top + height;
+            final int left;
+            final int right;
+            if (checkMarkAtStart) {
+                left = mBasePadding;
+                right = left + mCheckMarkWidth;
+            } else {
+                right = width - mBasePadding;
+                left = right - mCheckMarkWidth;
+            }
+            checkMarkDrawable.setBounds(mScrollX + left, top, mScrollX + right, bottom);
+            checkMarkDrawable.draw(canvas);
+
+            final Drawable background = getBackground();
+            if (background != null) {
+                background.setHotspotBounds(mScrollX + left, top, mScrollX + right, bottom);
+            }
+        }
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (isChecked()) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable checkMarkDrawable = mCheckMarkDrawable;
+        if (checkMarkDrawable != null && checkMarkDrawable.isStateful()
+                && checkMarkDrawable.setState(getDrawableState())) {
+            invalidateDrawable(checkMarkDrawable);
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mCheckMarkDrawable != null) {
+            mCheckMarkDrawable.setHotspot(x, y);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return CheckedTextView.class.getName();
+    }
+
+    static class SavedState extends BaseSavedState {
+        boolean checked;
+
+        /**
+         * Constructor called from {@link CheckedTextView#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            checked = (Boolean)in.readValue(null);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeValue(checked);
+        }
+
+        @Override
+        public String toString() {
+            return "CheckedTextView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " checked=" + checked + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+
+        SavedState ss = new SavedState(superState);
+
+        ss.checked = isChecked();
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+        setChecked(ss.checked);
+        requestLayout();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setChecked(mChecked);
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setCheckable(true);
+        info.setChecked(mChecked);
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+        stream.addProperty("text:checked", isChecked());
+    }
+}
diff --git a/android/widget/Chronometer.java b/android/widget/Chronometer.java
new file mode 100644
index 0000000..d11c03a
--- /dev/null
+++ b/android/widget/Chronometer.java
@@ -0,0 +1,394 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.icu.text.MeasureFormat;
+import android.icu.text.MeasureFormat.FormatWidth;
+import android.icu.util.Measure;
+import android.icu.util.MeasureUnit;
+import android.net.Uri;
+import android.os.SystemClock;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.Formatter;
+import java.util.IllegalFormatException;
+import java.util.Locale;
+
+/**
+ * Class that implements a simple timer.
+ * <p>
+ * You can give it a start time in the {@link SystemClock#elapsedRealtime} timebase,
+ * and it counts up from that, or if you don't give it a base time, it will use the
+ * time at which you call {@link #start}.
+ *
+ * <p>The timer can also count downward towards the base time by
+ * setting {@link #setCountDown(boolean)} to true.
+ *
+ *  <p>By default it will display the current
+ * timer value in the form "MM:SS" or "H:MM:SS", or you can use {@link #setFormat}
+ * to format the timer value into an arbitrary string.
+ *
+ * @attr ref android.R.styleable#Chronometer_format
+ * @attr ref android.R.styleable#Chronometer_countDown
+ */
+@RemoteView
+public class Chronometer extends TextView {
+    private static final String TAG = "Chronometer";
+
+    /**
+     * A callback that notifies when the chronometer has incremented on its own.
+     */
+    public interface OnChronometerTickListener {
+
+        /**
+         * Notification that the chronometer has changed.
+         */
+        void onChronometerTick(Chronometer chronometer);
+
+    }
+
+    private long mBase;
+    private long mNow; // the currently displayed time
+    private boolean mVisible;
+    private boolean mStarted;
+    private boolean mRunning;
+    private boolean mLogged;
+    private String mFormat;
+    private Formatter mFormatter;
+    private Locale mFormatterLocale;
+    private Object[] mFormatterArgs = new Object[1];
+    private StringBuilder mFormatBuilder;
+    private OnChronometerTickListener mOnChronometerTickListener;
+    private StringBuilder mRecycle = new StringBuilder(8);
+    private boolean mCountDown;
+
+    /**
+     * Initialize this Chronometer object.
+     * Sets the base to the current time.
+     */
+    public Chronometer(Context context) {
+        this(context, null, 0);
+    }
+
+    /**
+     * Initialize with standard view layout information.
+     * Sets the base to the current time.
+     */
+    public Chronometer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Initialize with standard view layout information and style.
+     * Sets the base to the current time.
+     */
+    public Chronometer(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public Chronometer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.Chronometer, defStyleAttr, defStyleRes);
+        setFormat(a.getString(R.styleable.Chronometer_format));
+        setCountDown(a.getBoolean(R.styleable.Chronometer_countDown, false));
+        a.recycle();
+
+        init();
+    }
+
+    private void init() {
+        mBase = SystemClock.elapsedRealtime();
+        updateText(mBase);
+    }
+
+    /**
+     * Set this view to count down to the base instead of counting up from it.
+     *
+     * @param countDown whether this view should count down
+     *
+     * @see #setBase(long)
+     */
+    @android.view.RemotableViewMethod
+    public void setCountDown(boolean countDown) {
+        mCountDown = countDown;
+        updateText(SystemClock.elapsedRealtime());
+    }
+
+    /**
+     * @return whether this view counts down
+     *
+     * @see #setCountDown(boolean)
+     */
+    public boolean isCountDown() {
+        return mCountDown;
+    }
+
+    /**
+     * @return whether this is the final countdown
+     */
+    public boolean isTheFinalCountDown() {
+        try {
+            getContext().startActivity(
+                    new Intent(Intent.ACTION_VIEW, Uri.parse("https://youtu.be/9jK-NcRmVcw"))
+                            .addCategory(Intent.CATEGORY_BROWSABLE)
+                            .addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT
+                                    | Intent.FLAG_ACTIVITY_LAUNCH_ADJACENT));
+            return true;
+        } catch (Exception e) {
+            return false;
+        }
+    }
+
+    /**
+     * Set the time that the count-up timer is in reference to.
+     *
+     * @param base Use the {@link SystemClock#elapsedRealtime} time base.
+     */
+    @android.view.RemotableViewMethod
+    public void setBase(long base) {
+        mBase = base;
+        dispatchChronometerTick();
+        updateText(SystemClock.elapsedRealtime());
+    }
+
+    /**
+     * Return the base time as set through {@link #setBase}.
+     */
+    public long getBase() {
+        return mBase;
+    }
+
+    /**
+     * Sets the format string used for display.  The Chronometer will display
+     * this string, with the first "%s" replaced by the current timer value in
+     * "MM:SS" or "H:MM:SS" form.
+     *
+     * If the format string is null, or if you never call setFormat(), the
+     * Chronometer will simply display the timer value in "MM:SS" or "H:MM:SS"
+     * form.
+     *
+     * @param format the format string.
+     */
+    @android.view.RemotableViewMethod
+    public void setFormat(String format) {
+        mFormat = format;
+        if (format != null && mFormatBuilder == null) {
+            mFormatBuilder = new StringBuilder(format.length() * 2);
+        }
+    }
+
+    /**
+     * Returns the current format string as set through {@link #setFormat}.
+     */
+    public String getFormat() {
+        return mFormat;
+    }
+
+    /**
+     * Sets the listener to be called when the chronometer changes.
+     *
+     * @param listener The listener.
+     */
+    public void setOnChronometerTickListener(OnChronometerTickListener listener) {
+        mOnChronometerTickListener = listener;
+    }
+
+    /**
+     * @return The listener (may be null) that is listening for chronometer change
+     *         events.
+     */
+    public OnChronometerTickListener getOnChronometerTickListener() {
+        return mOnChronometerTickListener;
+    }
+
+    /**
+     * Start counting up.  This does not affect the base as set from {@link #setBase}, just
+     * the view display.
+     *
+     * Chronometer works by regularly scheduling messages to the handler, even when the
+     * Widget is not visible.  To make sure resource leaks do not occur, the user should
+     * make sure that each start() call has a reciprocal call to {@link #stop}.
+     */
+    public void start() {
+        mStarted = true;
+        updateRunning();
+    }
+
+    /**
+     * Stop counting up.  This does not affect the base as set from {@link #setBase}, just
+     * the view display.
+     *
+     * This stops the messages to the handler, effectively releasing resources that would
+     * be held as the chronometer is running, via {@link #start}.
+     */
+    public void stop() {
+        mStarted = false;
+        updateRunning();
+    }
+
+    /**
+     * The same as calling {@link #start} or {@link #stop}.
+     * @hide pending API council approval
+     */
+    @android.view.RemotableViewMethod
+    public void setStarted(boolean started) {
+        mStarted = started;
+        updateRunning();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mVisible = false;
+        updateRunning();
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mVisible = visibility == VISIBLE;
+        updateRunning();
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        updateRunning();
+    }
+
+    private synchronized void updateText(long now) {
+        mNow = now;
+        long seconds = mCountDown ? mBase - now : now - mBase;
+        seconds /= 1000;
+        boolean negative = false;
+        if (seconds < 0) {
+            seconds = -seconds;
+            negative = true;
+        }
+        String text = DateUtils.formatElapsedTime(mRecycle, seconds);
+        if (negative) {
+            text = getResources().getString(R.string.negative_duration, text);
+        }
+
+        if (mFormat != null) {
+            Locale loc = Locale.getDefault();
+            if (mFormatter == null || !loc.equals(mFormatterLocale)) {
+                mFormatterLocale = loc;
+                mFormatter = new Formatter(mFormatBuilder, loc);
+            }
+            mFormatBuilder.setLength(0);
+            mFormatterArgs[0] = text;
+            try {
+                mFormatter.format(mFormat, mFormatterArgs);
+                text = mFormatBuilder.toString();
+            } catch (IllegalFormatException ex) {
+                if (!mLogged) {
+                    Log.w(TAG, "Illegal format string: " + mFormat);
+                    mLogged = true;
+                }
+            }
+        }
+        setText(text);
+    }
+
+    private void updateRunning() {
+        boolean running = mVisible && mStarted && isShown();
+        if (running != mRunning) {
+            if (running) {
+                updateText(SystemClock.elapsedRealtime());
+                dispatchChronometerTick();
+                postDelayed(mTickRunnable, 1000);
+            } else {
+                removeCallbacks(mTickRunnable);
+            }
+            mRunning = running;
+        }
+    }
+
+    private final Runnable mTickRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mRunning) {
+                updateText(SystemClock.elapsedRealtime());
+                dispatchChronometerTick();
+                postDelayed(mTickRunnable, 1000);
+            }
+        }
+    };
+
+    void dispatchChronometerTick() {
+        if (mOnChronometerTickListener != null) {
+            mOnChronometerTickListener.onChronometerTick(this);
+        }
+    }
+
+    private static final int MIN_IN_SEC = 60;
+    private static final int HOUR_IN_SEC = MIN_IN_SEC*60;
+    private static String formatDuration(long ms) {
+        int duration = (int) (ms / DateUtils.SECOND_IN_MILLIS);
+        if (duration < 0) {
+            duration = -duration;
+        }
+
+        int h = 0;
+        int m = 0;
+
+        if (duration >= HOUR_IN_SEC) {
+            h = duration / HOUR_IN_SEC;
+            duration -= h * HOUR_IN_SEC;
+        }
+        if (duration >= MIN_IN_SEC) {
+            m = duration / MIN_IN_SEC;
+            duration -= m * MIN_IN_SEC;
+        }
+        final int s = duration;
+
+        final ArrayList<Measure> measures = new ArrayList<Measure>();
+        if (h > 0) {
+            measures.add(new Measure(h, MeasureUnit.HOUR));
+        }
+        if (m > 0) {
+            measures.add(new Measure(m, MeasureUnit.MINUTE));
+        }
+        measures.add(new Measure(s, MeasureUnit.SECOND));
+
+        return MeasureFormat.getInstance(Locale.getDefault(), FormatWidth.WIDE)
+                    .formatMeasures(measures.toArray(new Measure[measures.size()]));
+    }
+
+    @Override
+    public CharSequence getContentDescription() {
+        return formatDuration(mNow - mBase);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Chronometer.class.getName();
+    }
+}
diff --git a/android/widget/CompoundButton.java b/android/widget/CompoundButton.java
new file mode 100644
index 0000000..0762b15
--- /dev/null
+++ b/android/widget/CompoundButton.java
@@ -0,0 +1,605 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.SoundEffectConstants;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+/**
+ * <p>
+ * A button with two states, checked and unchecked. When the button is pressed
+ * or clicked, the state changes automatically.
+ * </p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#CompoundButton
+ * CompoundButton Attributes}, {@link android.R.styleable#Button Button
+ * Attributes}, {@link android.R.styleable#TextView TextView Attributes}, {@link
+ * android.R.styleable#View View Attributes}
+ * </p>
+ */
+public abstract class CompoundButton extends Button implements Checkable {
+    private static final String LOG_TAG = CompoundButton.class.getSimpleName();
+
+    private boolean mChecked;
+    private boolean mBroadcasting;
+
+    private Drawable mButtonDrawable;
+    private ColorStateList mButtonTintList = null;
+    private PorterDuff.Mode mButtonTintMode = null;
+    private boolean mHasButtonTint = false;
+    private boolean mHasButtonTintMode = false;
+
+    private OnCheckedChangeListener mOnCheckedChangeListener;
+    private OnCheckedChangeListener mOnCheckedChangeWidgetListener;
+
+    // Indicates whether the toggle state was set from resources or dynamically, so it can be used
+    // to sanitize autofill requests.
+    private boolean mCheckedFromResource = false;
+
+    private static final int[] CHECKED_STATE_SET = {
+        R.attr.state_checked
+    };
+
+    public CompoundButton(Context context) {
+        this(context, null);
+    }
+
+    public CompoundButton(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public CompoundButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.CompoundButton, defStyleAttr, defStyleRes);
+
+        final Drawable d = a.getDrawable(com.android.internal.R.styleable.CompoundButton_button);
+        if (d != null) {
+            setButtonDrawable(d);
+        }
+
+        if (a.hasValue(R.styleable.CompoundButton_buttonTintMode)) {
+            mButtonTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.CompoundButton_buttonTintMode, -1), mButtonTintMode);
+            mHasButtonTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.CompoundButton_buttonTint)) {
+            mButtonTintList = a.getColorStateList(R.styleable.CompoundButton_buttonTint);
+            mHasButtonTint = true;
+        }
+
+        final boolean checked = a.getBoolean(
+                com.android.internal.R.styleable.CompoundButton_checked, false);
+        setChecked(checked);
+        mCheckedFromResource = true;
+
+        a.recycle();
+
+        applyButtonTint();
+    }
+
+    @Override
+    public void toggle() {
+        setChecked(!mChecked);
+    }
+
+    @Override
+    public boolean performClick() {
+        toggle();
+
+        final boolean handled = super.performClick();
+        if (!handled) {
+            // View only makes a sound effect if the onClickListener was
+            // called, so we'll need to make one here instead.
+            playSoundEffect(SoundEffectConstants.CLICK);
+        }
+
+        return handled;
+    }
+
+    @ViewDebug.ExportedProperty
+    @Override
+    public boolean isChecked() {
+        return mChecked;
+    }
+
+    /**
+     * <p>Changes the checked state of this button.</p>
+     *
+     * @param checked true to check the button, false to uncheck it
+     */
+    @Override
+    public void setChecked(boolean checked) {
+        if (mChecked != checked) {
+            mCheckedFromResource = false;
+            mChecked = checked;
+            refreshDrawableState();
+            notifyViewAccessibilityStateChangedIfNeeded(
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+
+            // Avoid infinite recursions if setChecked() is called from a listener
+            if (mBroadcasting) {
+                return;
+            }
+
+            mBroadcasting = true;
+            if (mOnCheckedChangeListener != null) {
+                mOnCheckedChangeListener.onCheckedChanged(this, mChecked);
+            }
+            if (mOnCheckedChangeWidgetListener != null) {
+                mOnCheckedChangeWidgetListener.onCheckedChanged(this, mChecked);
+            }
+            final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+            if (afm != null) {
+                afm.notifyValueChanged(this);
+            }
+
+            mBroadcasting = false;
+        }
+    }
+
+    /**
+     * Register a callback to be invoked when the checked state of this button
+     * changes.
+     *
+     * @param listener the callback to call on checked state change
+     */
+    public void setOnCheckedChangeListener(@Nullable OnCheckedChangeListener listener) {
+        mOnCheckedChangeListener = listener;
+    }
+
+    /**
+     * Register a callback to be invoked when the checked state of this button
+     * changes. This callback is used for internal purpose only.
+     *
+     * @param listener the callback to call on checked state change
+     * @hide
+     */
+    void setOnCheckedChangeWidgetListener(OnCheckedChangeListener listener) {
+        mOnCheckedChangeWidgetListener = listener;
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when the checked state
+     * of a compound button changed.
+     */
+    public static interface OnCheckedChangeListener {
+        /**
+         * Called when the checked state of a compound button has changed.
+         *
+         * @param buttonView The compound button view whose state has changed.
+         * @param isChecked  The new checked state of buttonView.
+         */
+        void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
+    }
+
+    /**
+     * Sets a drawable as the compound button image given its resource
+     * identifier.
+     *
+     * @param resId the resource identifier of the drawable
+     * @attr ref android.R.styleable#CompoundButton_button
+     */
+    public void setButtonDrawable(@DrawableRes int resId) {
+        final Drawable d;
+        if (resId != 0) {
+            d = getContext().getDrawable(resId);
+        } else {
+            d = null;
+        }
+        setButtonDrawable(d);
+    }
+
+    /**
+     * Sets a drawable as the compound button image.
+     *
+     * @param drawable the drawable to set
+     * @attr ref android.R.styleable#CompoundButton_button
+     */
+    public void setButtonDrawable(@Nullable Drawable drawable) {
+        if (mButtonDrawable != drawable) {
+            if (mButtonDrawable != null) {
+                mButtonDrawable.setCallback(null);
+                unscheduleDrawable(mButtonDrawable);
+            }
+
+            mButtonDrawable = drawable;
+
+            if (drawable != null) {
+                drawable.setCallback(this);
+                drawable.setLayoutDirection(getLayoutDirection());
+                if (drawable.isStateful()) {
+                    drawable.setState(getDrawableState());
+                }
+                drawable.setVisible(getVisibility() == VISIBLE, false);
+                setMinHeight(drawable.getIntrinsicHeight());
+                applyButtonTint();
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
+        super.onResolveDrawables(layoutDirection);
+        if (mButtonDrawable != null) {
+            mButtonDrawable.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    /**
+     * @return the drawable used as the compound button image
+     * @see #setButtonDrawable(Drawable)
+     * @see #setButtonDrawable(int)
+     */
+    @Nullable
+    public Drawable getButtonDrawable() {
+        return mButtonDrawable;
+    }
+
+    /**
+     * Applies a tint to the button drawable. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setButtonDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and tint
+     * mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#CompoundButton_buttonTint
+     * @see #setButtonTintList(ColorStateList)
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setButtonTintList(@Nullable ColorStateList tint) {
+        mButtonTintList = tint;
+        mHasButtonTint = true;
+
+        applyButtonTint();
+    }
+
+    /**
+     * @return the tint applied to the button drawable
+     * @attr ref android.R.styleable#CompoundButton_buttonTint
+     * @see #setButtonTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getButtonTintList() {
+        return mButtonTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setButtonTintList(ColorStateList)}} to the button drawable. The
+     * default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#CompoundButton_buttonTintMode
+     * @see #getButtonTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setButtonTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mButtonTintMode = tintMode;
+        mHasButtonTintMode = true;
+
+        applyButtonTint();
+    }
+
+    /**
+     * @return the blending mode used to apply the tint to the button drawable
+     * @attr ref android.R.styleable#CompoundButton_buttonTintMode
+     * @see #setButtonTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getButtonTintMode() {
+        return mButtonTintMode;
+    }
+
+    private void applyButtonTint() {
+        if (mButtonDrawable != null && (mHasButtonTint || mHasButtonTintMode)) {
+            mButtonDrawable = mButtonDrawable.mutate();
+
+            if (mHasButtonTint) {
+                mButtonDrawable.setTintList(mButtonTintList);
+            }
+
+            if (mHasButtonTintMode) {
+                mButtonDrawable.setTintMode(mButtonTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mButtonDrawable.isStateful()) {
+                mButtonDrawable.setState(getDrawableState());
+            }
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return CompoundButton.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setChecked(mChecked);
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setCheckable(true);
+        info.setChecked(mChecked);
+    }
+
+    @Override
+    public int getCompoundPaddingLeft() {
+        int padding = super.getCompoundPaddingLeft();
+        if (!isLayoutRtl()) {
+            final Drawable buttonDrawable = mButtonDrawable;
+            if (buttonDrawable != null) {
+                padding += buttonDrawable.getIntrinsicWidth();
+            }
+        }
+        return padding;
+    }
+
+    @Override
+    public int getCompoundPaddingRight() {
+        int padding = super.getCompoundPaddingRight();
+        if (isLayoutRtl()) {
+            final Drawable buttonDrawable = mButtonDrawable;
+            if (buttonDrawable != null) {
+                padding += buttonDrawable.getIntrinsicWidth();
+            }
+        }
+        return padding;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int getHorizontalOffsetForDrawables() {
+        final Drawable buttonDrawable = mButtonDrawable;
+        return (buttonDrawable != null) ? buttonDrawable.getIntrinsicWidth() : 0;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        final Drawable buttonDrawable = mButtonDrawable;
+        if (buttonDrawable != null) {
+            final int verticalGravity = getGravity() & Gravity.VERTICAL_GRAVITY_MASK;
+            final int drawableHeight = buttonDrawable.getIntrinsicHeight();
+            final int drawableWidth = buttonDrawable.getIntrinsicWidth();
+
+            final int top;
+            switch (verticalGravity) {
+                case Gravity.BOTTOM:
+                    top = getHeight() - drawableHeight;
+                    break;
+                case Gravity.CENTER_VERTICAL:
+                    top = (getHeight() - drawableHeight) / 2;
+                    break;
+                default:
+                    top = 0;
+            }
+            final int bottom = top + drawableHeight;
+            final int left = isLayoutRtl() ? getWidth() - drawableWidth : 0;
+            final int right = isLayoutRtl() ? getWidth() : drawableWidth;
+
+            buttonDrawable.setBounds(left, top, right, bottom);
+
+            final Drawable background = getBackground();
+            if (background != null) {
+                background.setHotspotBounds(left, top, right, bottom);
+            }
+        }
+
+        super.onDraw(canvas);
+
+        if (buttonDrawable != null) {
+            final int scrollX = mScrollX;
+            final int scrollY = mScrollY;
+            if (scrollX == 0 && scrollY == 0) {
+                buttonDrawable.draw(canvas);
+            } else {
+                canvas.translate(scrollX, scrollY);
+                buttonDrawable.draw(canvas);
+                canvas.translate(-scrollX, -scrollY);
+            }
+        }
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (isChecked()) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable buttonDrawable = mButtonDrawable;
+        if (buttonDrawable != null && buttonDrawable.isStateful()
+                && buttonDrawable.setState(getDrawableState())) {
+            invalidateDrawable(buttonDrawable);
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mButtonDrawable != null) {
+            mButtonDrawable.setHotspot(x, y);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return super.verifyDrawable(who) || who == mButtonDrawable;
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mButtonDrawable != null) mButtonDrawable.jumpToCurrentState();
+    }
+
+    static class SavedState extends BaseSavedState {
+        boolean checked;
+
+        /**
+         * Constructor called from {@link CompoundButton#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            checked = (Boolean)in.readValue(null);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeValue(checked);
+        }
+
+        @Override
+        public String toString() {
+            return "CompoundButton.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " checked=" + checked + "}";
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            @Override
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+
+        SavedState ss = new SavedState(superState);
+
+        ss.checked = isChecked();
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+        setChecked(ss.checked);
+        requestLayout();
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+        stream.addProperty("checked", isChecked());
+    }
+
+    @Override
+    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+        super.onProvideAutofillStructure(structure, flags);
+
+        structure.setDataIsSensitive(!mCheckedFromResource);
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!isEnabled()) return;
+
+        if (!value.isToggle()) {
+            Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+            return;
+        }
+
+        setChecked(value.getToggleValue());
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isEnabled() ? AUTOFILL_TYPE_TOGGLE : AUTOFILL_TYPE_NONE;
+    }
+
+    @Override
+    public AutofillValue getAutofillValue() {
+        return isEnabled() ? AutofillValue.forToggle(isChecked()) : null;
+    }
+}
diff --git a/android/widget/CursorAdapter.java b/android/widget/CursorAdapter.java
new file mode 100644
index 0000000..9fb98db
--- /dev/null
+++ b/android/widget/CursorAdapter.java
@@ -0,0 +1,518 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.WorkerThread;
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Adapter that exposes data from a {@link android.database.Cursor Cursor} to a
+ * {@link android.widget.ListView ListView} widget.
+ * <p>
+ * The Cursor must include a column named "_id" or this class will not work.
+ * Additionally, using {@link android.database.MergeCursor} with this class will
+ * not work if the merged Cursors have overlapping values in their "_id"
+ * columns.
+ */
+public abstract class CursorAdapter extends BaseAdapter implements Filterable,
+        CursorFilter.CursorFilterClient, ThemedSpinnerAdapter {
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected boolean mDataValid;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected boolean mAutoRequery;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected Cursor mCursor;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected Context mContext;
+    /**
+     * Context used for {@link #getDropDownView(int, View, ViewGroup)}.
+     * {@hide}
+     */
+    protected Context mDropDownContext;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected int mRowIDColumn;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected ChangeObserver mChangeObserver;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected DataSetObserver mDataSetObserver;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected CursorFilter mCursorFilter;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected FilterQueryProvider mFilterQueryProvider;
+
+    /**
+     * If set the adapter will call requery() on the cursor whenever a content change
+     * notification is delivered. Implies {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
+     *
+     * @deprecated This option is discouraged, as it results in Cursor queries
+     * being performed on the application's UI thread and thus can cause poor
+     * responsiveness or even Application Not Responding errors.  As an alternative,
+     * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+     */
+    @Deprecated
+    public static final int FLAG_AUTO_REQUERY = 0x01;
+
+    /**
+     * If set the adapter will register a content observer on the cursor and will call
+     * {@link #onContentChanged()} when a notification comes in.  Be careful when
+     * using this flag: you will need to unset the current Cursor from the adapter
+     * to avoid leaks due to its registered observers.  This flag is not needed
+     * when using a CursorAdapter with a
+     * {@link android.content.CursorLoader}.
+     */
+    public static final int FLAG_REGISTER_CONTENT_OBSERVER = 0x02;
+
+    /**
+     * Constructor that always enables auto-requery.
+     *
+     * @deprecated This option is discouraged, as it results in Cursor queries
+     * being performed on the application's UI thread and thus can cause poor
+     * responsiveness or even Application Not Responding errors.  As an alternative,
+     * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+     *
+     * @param c The cursor from which to get the data.
+     * @param context The context
+     */
+    @Deprecated
+    public CursorAdapter(Context context, Cursor c) {
+        init(context, c, FLAG_AUTO_REQUERY);
+    }
+
+    /**
+     * Constructor that allows control over auto-requery.  It is recommended
+     * you not use this, but instead {@link #CursorAdapter(Context, Cursor, int)}.
+     * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+     * will always be set.
+     *
+     * @param c The cursor from which to get the data.
+     * @param context The context
+     * @param autoRequery If true the adapter will call requery() on the
+     *                    cursor whenever it changes so the most recent
+     *                    data is always displayed.  Using true here is discouraged.
+     */
+    public CursorAdapter(Context context, Cursor c, boolean autoRequery) {
+        init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+    }
+
+    /**
+     * Recommended constructor.
+     *
+     * @param c The cursor from which to get the data.
+     * @param context The context
+     * @param flags Flags used to determine the behavior of the adapter; may
+     * be any combination of {@link #FLAG_AUTO_REQUERY} and
+     * {@link #FLAG_REGISTER_CONTENT_OBSERVER}.
+     */
+    public CursorAdapter(Context context, Cursor c, int flags) {
+        init(context, c, flags);
+    }
+
+    /**
+     * @deprecated Don't use this, use the normal constructor.  This will
+     * be removed in the future.
+     */
+    @Deprecated
+    protected void init(Context context, Cursor c, boolean autoRequery) {
+        init(context, c, autoRequery ? FLAG_AUTO_REQUERY : FLAG_REGISTER_CONTENT_OBSERVER);
+    }
+
+    void init(Context context, Cursor c, int flags) {
+        if ((flags & FLAG_AUTO_REQUERY) == FLAG_AUTO_REQUERY) {
+            flags |= FLAG_REGISTER_CONTENT_OBSERVER;
+            mAutoRequery = true;
+        } else {
+            mAutoRequery = false;
+        }
+        boolean cursorPresent = c != null;
+        mCursor = c;
+        mDataValid = cursorPresent;
+        mContext = context;
+        mRowIDColumn = cursorPresent ? c.getColumnIndexOrThrow("_id") : -1;
+        if ((flags & FLAG_REGISTER_CONTENT_OBSERVER) == FLAG_REGISTER_CONTENT_OBSERVER) {
+            mChangeObserver = new ChangeObserver();
+            mDataSetObserver = new MyDataSetObserver();
+        } else {
+            mChangeObserver = null;
+            mDataSetObserver = null;
+        }
+
+        if (cursorPresent) {
+            if (mChangeObserver != null) c.registerContentObserver(mChangeObserver);
+            if (mDataSetObserver != null) c.registerDataSetObserver(mDataSetObserver);
+        }
+    }
+
+    /**
+     * Sets the {@link Resources.Theme} against which drop-down views are
+     * inflated.
+     * <p>
+     * By default, drop-down views are inflated against the theme of the
+     * {@link Context} passed to the adapter's constructor.
+     *
+     * @param theme the theme against which to inflate drop-down views or
+     *              {@code null} to use the theme from the adapter's context
+     * @see #newDropDownView(Context, Cursor, ViewGroup)
+     */
+    @Override
+    public void setDropDownViewTheme(Resources.Theme theme) {
+        if (theme == null) {
+            mDropDownContext = null;
+        } else if (theme == mContext.getTheme()) {
+            mDropDownContext = mContext;
+        } else {
+            mDropDownContext = new ContextThemeWrapper(mContext, theme);
+        }
+    }
+
+    @Override
+    public Resources.Theme getDropDownViewTheme() {
+        return mDropDownContext == null ? null : mDropDownContext.getTheme();
+    }
+
+    /**
+     * Returns the cursor.
+     * @return the cursor.
+     */
+    public Cursor getCursor() {
+        return mCursor;
+    }
+
+    /**
+     * @see android.widget.ListAdapter#getCount()
+     */
+    public int getCount() {
+        if (mDataValid && mCursor != null) {
+            return mCursor.getCount();
+        } else {
+            return 0;
+        }
+    }
+    
+    /**
+     * @see android.widget.ListAdapter#getItem(int)
+     */
+    public Object getItem(int position) {
+        if (mDataValid && mCursor != null) {
+            mCursor.moveToPosition(position);
+            return mCursor;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * @see android.widget.ListAdapter#getItemId(int)
+     */
+    public long getItemId(int position) {
+        if (mDataValid && mCursor != null) {
+            if (mCursor.moveToPosition(position)) {
+                return mCursor.getLong(mRowIDColumn);
+            } else {
+                return 0;
+            }
+        } else {
+            return 0;
+        }
+    }
+    
+    @Override
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    /**
+     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+     */
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (!mDataValid) {
+            throw new IllegalStateException("this should only be called when the cursor is valid");
+        }
+        if (!mCursor.moveToPosition(position)) {
+            throw new IllegalStateException("couldn't move cursor to position " + position);
+        }
+        View v;
+        if (convertView == null) {
+            v = newView(mContext, mCursor, parent);
+        } else {
+            v = convertView;
+        }
+        bindView(v, mContext, mCursor);
+        return v;
+    }
+
+    @Override
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        if (mDataValid) {
+            final Context context = mDropDownContext == null ? mContext : mDropDownContext;
+            mCursor.moveToPosition(position);
+            final View v;
+            if (convertView == null) {
+                v = newDropDownView(context, mCursor, parent);
+            } else {
+                v = convertView;
+            }
+            bindView(v, context, mCursor);
+            return v;
+        } else {
+            return null;
+        }
+    }
+
+    /**
+     * Makes a new view to hold the data pointed to by cursor.
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is already
+     * moved to the correct position.
+     * @param parent The parent to which the new view is attached to
+     * @return the newly created view.
+     */
+    public abstract View newView(Context context, Cursor cursor, ViewGroup parent);
+
+    /**
+     * Makes a new drop down view to hold the data pointed to by cursor.
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is already
+     * moved to the correct position.
+     * @param parent The parent to which the new view is attached to
+     * @return the newly created view.
+     */
+    public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
+        return newView(context, cursor, parent);
+    }
+
+    /**
+     * Bind an existing view to the data pointed to by cursor
+     * @param view Existing view, returned earlier by newView
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is already
+     * moved to the correct position.
+     */
+    public abstract void bindView(View view, Context context, Cursor cursor);
+    
+    /**
+     * Change the underlying cursor to a new cursor. If there is an existing cursor it will be
+     * closed.
+     * 
+     * @param cursor The new cursor to be used
+     */
+    public void changeCursor(Cursor cursor) {
+        Cursor old = swapCursor(cursor);
+        if (old != null) {
+            old.close();
+        }
+    }
+
+    /**
+     * Swap in a new Cursor, returning the old Cursor.  Unlike
+     * {@link #changeCursor(Cursor)}, the returned old Cursor is <em>not</em>
+     * closed.
+     *
+     * @param newCursor The new cursor to be used.
+     * @return Returns the previously set Cursor, or null if there wasa not one.
+     * If the given new Cursor is the same instance is the previously set
+     * Cursor, null is also returned.
+     */
+    public Cursor swapCursor(Cursor newCursor) {
+        if (newCursor == mCursor) {
+            return null;
+        }
+        Cursor oldCursor = mCursor;
+        if (oldCursor != null) {
+            if (mChangeObserver != null) oldCursor.unregisterContentObserver(mChangeObserver);
+            if (mDataSetObserver != null) oldCursor.unregisterDataSetObserver(mDataSetObserver);
+        }
+        mCursor = newCursor;
+        if (newCursor != null) {
+            if (mChangeObserver != null) newCursor.registerContentObserver(mChangeObserver);
+            if (mDataSetObserver != null) newCursor.registerDataSetObserver(mDataSetObserver);
+            mRowIDColumn = newCursor.getColumnIndexOrThrow("_id");
+            mDataValid = true;
+            // notify the observers about the new cursor
+            notifyDataSetChanged();
+        } else {
+            mRowIDColumn = -1;
+            mDataValid = false;
+            // notify the observers about the lack of a data set
+            notifyDataSetInvalidated();
+        }
+        return oldCursor;
+    }
+
+    /**
+     * <p>Converts the cursor into a CharSequence. Subclasses should override this
+     * method to convert their results. The default implementation returns an
+     * empty String for null values or the default String representation of
+     * the value.</p>
+     *
+     * @param cursor the cursor to convert to a CharSequence
+     * @return a CharSequence representing the value
+     */
+    public CharSequence convertToString(Cursor cursor) {
+        return cursor == null ? "" : cursor.toString();
+    }
+
+    /**
+     * Runs a query with the specified constraint. This query is requested
+     * by the filter attached to this adapter.
+     *
+     * The query is provided by a
+     * {@link android.widget.FilterQueryProvider}.
+     * If no provider is specified, the current cursor is not filtered and returned.
+     *
+     * After this method returns the resulting cursor is passed to {@link #changeCursor(Cursor)}
+     * and the previous cursor is closed.
+     *
+     * This method is always executed on a background thread, not on the
+     * application's main thread (or UI thread.)
+     * 
+     * Contract: when constraint is null or empty, the original results,
+     * prior to any filtering, must be returned.
+     *
+     * @param constraint the constraint with which the query must be filtered
+     *
+     * @return a Cursor representing the results of the new query
+     *
+     * @see #getFilter()
+     * @see #getFilterQueryProvider()
+     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
+     */
+    @WorkerThread
+    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+        if (mFilterQueryProvider != null) {
+            return mFilterQueryProvider.runQuery(constraint);
+        }
+
+        return mCursor;
+    }
+
+    public Filter getFilter() {
+        if (mCursorFilter == null) {
+            mCursorFilter = new CursorFilter(this);
+        }
+        return mCursorFilter;
+    }
+
+    /**
+     * Returns the query filter provider used for filtering. When the
+     * provider is null, no filtering occurs.
+     *
+     * @return the current filter query provider or null if it does not exist
+     *
+     * @see #setFilterQueryProvider(android.widget.FilterQueryProvider)
+     * @see #runQueryOnBackgroundThread(CharSequence)
+     */
+    public FilterQueryProvider getFilterQueryProvider() {
+        return mFilterQueryProvider;
+    }
+
+    /**
+     * Sets the query filter provider used to filter the current Cursor.
+     * The provider's
+     * {@link android.widget.FilterQueryProvider#runQuery(CharSequence)}
+     * method is invoked when filtering is requested by a client of
+     * this adapter.
+     *
+     * @param filterQueryProvider the filter query provider or null to remove it
+     *
+     * @see #getFilterQueryProvider()
+     * @see #runQueryOnBackgroundThread(CharSequence)
+     */
+    public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
+        mFilterQueryProvider = filterQueryProvider;
+    }
+
+    /**
+     * Called when the {@link ContentObserver} on the cursor receives a change notification.
+     * The default implementation provides the auto-requery logic, but may be overridden by
+     * sub classes.
+     * 
+     * @see ContentObserver#onChange(boolean)
+     */
+    protected void onContentChanged() {
+        if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
+            if (false) Log.v("Cursor", "Auto requerying " + mCursor + " due to update");
+            mDataValid = mCursor.requery();
+        }
+    }
+
+    private class ChangeObserver extends ContentObserver {
+        public ChangeObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public boolean deliverSelfNotifications() {
+            return true;
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            onContentChanged();
+        }
+    }
+
+    private class MyDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            mDataValid = true;
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            mDataValid = false;
+            notifyDataSetInvalidated();
+        }
+    }
+
+}
diff --git a/android/widget/CursorFilter.java b/android/widget/CursorFilter.java
new file mode 100644
index 0000000..dbded69
--- /dev/null
+++ b/android/widget/CursorFilter.java
@@ -0,0 +1,70 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.database.Cursor;
+
+/**
+ * <p>The CursorFilter delegates most of the work to the CursorAdapter.
+ * Subclasses should override these delegate methods to run the queries
+ * and convert the results into String that can be used by auto-completion
+ * widgets.</p>
+ */
+class CursorFilter extends Filter {
+    
+    CursorFilterClient mClient;
+    
+    interface CursorFilterClient {
+        CharSequence convertToString(Cursor cursor);
+        Cursor runQueryOnBackgroundThread(CharSequence constraint);
+        Cursor getCursor();
+        void changeCursor(Cursor cursor);
+    }
+
+    CursorFilter(CursorFilterClient client) {
+        mClient = client;
+    }
+    
+    @Override
+    public CharSequence convertResultToString(Object resultValue) {
+        return mClient.convertToString((Cursor) resultValue);
+    }
+
+    @Override
+    protected FilterResults performFiltering(CharSequence constraint) {
+        Cursor cursor = mClient.runQueryOnBackgroundThread(constraint);
+
+        FilterResults results = new FilterResults();
+        if (cursor != null) {
+            results.count = cursor.getCount();
+            results.values = cursor;
+        } else {
+            results.count = 0;
+            results.values = null;
+        }
+        return results;
+    }
+
+    @Override
+    protected void publishResults(CharSequence constraint, FilterResults results) {
+        Cursor oldCursor = mClient.getCursor();
+        
+        if (results.values != null && results.values != oldCursor) {
+            mClient.changeCursor((Cursor) results.values);
+        }
+    }
+}
diff --git a/android/widget/CursorTreeAdapter.java b/android/widget/CursorTreeAdapter.java
new file mode 100644
index 0000000..405e45a
--- /dev/null
+++ b/android/widget/CursorTreeAdapter.java
@@ -0,0 +1,522 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.app.Activity;
+import android.content.Context;
+import android.database.ContentObserver;
+import android.database.Cursor;
+import android.database.DataSetObserver;
+import android.os.Handler;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An adapter that exposes data from a series of {@link Cursor}s to an
+ * {@link ExpandableListView} widget. The top-level {@link Cursor} (that is
+ * given in the constructor) exposes the groups, while subsequent {@link Cursor}s
+ * returned from {@link #getChildrenCursor(Cursor)} expose children within a
+ * particular group. The Cursors must include a column named "_id" or this class
+ * will not work.
+ */
+public abstract class CursorTreeAdapter extends BaseExpandableListAdapter implements Filterable,
+        CursorFilter.CursorFilterClient {
+    private Context mContext;
+    private Handler mHandler;
+    private boolean mAutoRequery;
+
+    /** The cursor helper that is used to get the groups */
+    MyCursorHelper mGroupCursorHelper;
+    
+    /**
+     * The map of a group position to the group's children cursor helper (the
+     * cursor helper that is used to get the children for that group)
+     */
+    SparseArray<MyCursorHelper> mChildrenCursorHelpers;
+
+    // Filter related
+    CursorFilter mCursorFilter;
+    FilterQueryProvider mFilterQueryProvider;
+    
+    /**
+     * Constructor. The adapter will call {@link Cursor#requery()} on the cursor whenever
+     * it changes so that the most recent data is always displayed.
+     *
+     * @param cursor The cursor from which to get the data for the groups.
+     */
+    public CursorTreeAdapter(Cursor cursor, Context context) {
+        init(cursor, context, true);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param cursor The cursor from which to get the data for the groups.
+     * @param context The context
+     * @param autoRequery If true the adapter will call {@link Cursor#requery()}
+     *        on the cursor whenever it changes so the most recent data is
+     *        always displayed.
+     */
+    public CursorTreeAdapter(Cursor cursor, Context context, boolean autoRequery) {
+        init(cursor, context, autoRequery);
+    }
+    
+    private void init(Cursor cursor, Context context, boolean autoRequery) {
+        mContext = context;
+        mHandler = new Handler();
+        mAutoRequery = autoRequery;
+        
+        mGroupCursorHelper = new MyCursorHelper(cursor);
+        mChildrenCursorHelpers = new SparseArray<MyCursorHelper>();
+    }
+
+    /**
+     * Gets the cursor helper for the children in the given group.
+     * 
+     * @param groupPosition The group whose children will be returned
+     * @param requestCursor Whether to request a Cursor via
+     *            {@link #getChildrenCursor(Cursor)} (true), or to assume a call
+     *            to {@link #setChildrenCursor(int, Cursor)} will happen shortly
+     *            (false).
+     * @return The cursor helper for the children of the given group
+     */
+    synchronized MyCursorHelper getChildrenCursorHelper(int groupPosition, boolean requestCursor) {
+        MyCursorHelper cursorHelper = mChildrenCursorHelpers.get(groupPosition);
+        
+        if (cursorHelper == null) {
+            if (mGroupCursorHelper.moveTo(groupPosition) == null) return null;
+            
+            final Cursor cursor = getChildrenCursor(mGroupCursorHelper.getCursor());
+            cursorHelper = new MyCursorHelper(cursor);
+            mChildrenCursorHelpers.put(groupPosition, cursorHelper);
+        }
+        
+        return cursorHelper;
+    }
+
+    /**
+     * Gets the Cursor for the children at the given group. Subclasses must
+     * implement this method to return the children data for a particular group.
+     * <p>
+     * If you want to asynchronously query a provider to prevent blocking the
+     * UI, it is possible to return null and at a later time call
+     * {@link #setChildrenCursor(int, Cursor)}.
+     * <p>
+     * It is your responsibility to manage this Cursor through the Activity
+     * lifecycle. It is a good idea to use {@link Activity#managedQuery} which
+     * will handle this for you. In some situations, the adapter will deactivate
+     * the Cursor on its own, but this will not always be the case, so please
+     * ensure the Cursor is properly managed.
+     * 
+     * @param groupCursor The cursor pointing to the group whose children cursor
+     *            should be returned
+     * @return The cursor for the children of a particular group, or null.
+     */
+    abstract protected Cursor getChildrenCursor(Cursor groupCursor);
+    
+    /**
+     * Sets the group Cursor.
+     * 
+     * @param cursor The Cursor to set for the group. If there is an existing cursor 
+     * it will be closed.
+     */
+    public void setGroupCursor(Cursor cursor) {
+        mGroupCursorHelper.changeCursor(cursor, false);
+    }
+    
+    /**
+     * Sets the children Cursor for a particular group. If there is an existing cursor
+     * it will be closed.
+     * <p>
+     * This is useful when asynchronously querying to prevent blocking the UI.
+     * 
+     * @param groupPosition The group whose children are being set via this Cursor.
+     * @param childrenCursor The Cursor that contains the children of the group.
+     */
+    public void setChildrenCursor(int groupPosition, Cursor childrenCursor) {
+        
+        /*
+         * Don't request a cursor from the subclass, instead we will be setting
+         * the cursor ourselves.
+         */
+        MyCursorHelper childrenCursorHelper = getChildrenCursorHelper(groupPosition, false);
+
+        /*
+         * Don't release any cursor since we know exactly what data is changing
+         * (this cursor, which is still valid).
+         */
+        childrenCursorHelper.changeCursor(childrenCursor, false);
+    }
+    
+    public Cursor getChild(int groupPosition, int childPosition) {
+        // Return this group's children Cursor pointing to the particular child
+        return getChildrenCursorHelper(groupPosition, true).moveTo(childPosition);
+    }
+
+    public long getChildId(int groupPosition, int childPosition) {
+        return getChildrenCursorHelper(groupPosition, true).getId(childPosition);
+    }
+
+    public int getChildrenCount(int groupPosition) {
+        MyCursorHelper helper = getChildrenCursorHelper(groupPosition, true);
+        return (mGroupCursorHelper.isValid() && helper != null) ? helper.getCount() : 0;
+    }
+
+    public Cursor getGroup(int groupPosition) {
+        // Return the group Cursor pointing to the given group
+        return mGroupCursorHelper.moveTo(groupPosition);
+    }
+
+    public int getGroupCount() {
+        return mGroupCursorHelper.getCount();
+    }
+
+    public long getGroupId(int groupPosition) {
+        return mGroupCursorHelper.getId(groupPosition);
+    }
+
+    public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+            ViewGroup parent) {
+        Cursor cursor = mGroupCursorHelper.moveTo(groupPosition);
+        if (cursor == null) {
+            throw new IllegalStateException("this should only be called when the cursor is valid");
+        }
+        
+        View v;
+        if (convertView == null) {
+            v = newGroupView(mContext, cursor, isExpanded, parent);
+        } else {
+            v = convertView;
+        }
+        bindGroupView(v, mContext, cursor, isExpanded);
+        return v;
+    }
+
+    /**
+     * Makes a new group view to hold the group data pointed to by cursor.
+     * 
+     * @param context Interface to application's global information
+     * @param cursor The group cursor from which to get the data. The cursor is
+     *            already moved to the correct position.
+     * @param isExpanded Whether the group is expanded.
+     * @param parent The parent to which the new view is attached to
+     * @return The newly created view.
+     */
+    protected abstract View newGroupView(Context context, Cursor cursor, boolean isExpanded,
+            ViewGroup parent);
+
+    /**
+     * Bind an existing view to the group data pointed to by cursor.
+     * 
+     * @param view Existing view, returned earlier by newGroupView.
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is
+     *            already moved to the correct position.
+     * @param isExpanded Whether the group is expanded.
+     */
+    protected abstract void bindGroupView(View view, Context context, Cursor cursor,
+            boolean isExpanded);
+
+    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+            View convertView, ViewGroup parent) {
+        MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
+        
+        Cursor cursor = cursorHelper.moveTo(childPosition);
+        if (cursor == null) {
+            throw new IllegalStateException("this should only be called when the cursor is valid");
+        }
+        
+        View v;
+        if (convertView == null) {
+            v = newChildView(mContext, cursor, isLastChild, parent);
+        } else {
+            v = convertView;
+        }
+        bindChildView(v, mContext, cursor, isLastChild);
+        return v;
+    }
+
+    /**
+     * Makes a new child view to hold the data pointed to by cursor.
+     * 
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is
+     *            already moved to the correct position.
+     * @param isLastChild Whether the child is the last child within its group.
+     * @param parent The parent to which the new view is attached to
+     * @return the newly created view.
+     */
+    protected abstract View newChildView(Context context, Cursor cursor, boolean isLastChild,
+            ViewGroup parent);
+
+    /**
+     * Bind an existing view to the child data pointed to by cursor
+     * 
+     * @param view Existing view, returned earlier by newChildView
+     * @param context Interface to application's global information
+     * @param cursor The cursor from which to get the data. The cursor is
+     *            already moved to the correct position.
+     * @param isLastChild Whether the child is the last child within its group.
+     */
+    protected abstract void bindChildView(View view, Context context, Cursor cursor,
+            boolean isLastChild);
+    
+    public boolean isChildSelectable(int groupPosition, int childPosition) {
+        return true;
+    }
+
+    public boolean hasStableIds() {
+        return true;
+    }
+
+    private synchronized void releaseCursorHelpers() {
+        for (int pos = mChildrenCursorHelpers.size() - 1; pos >= 0; pos--) {
+            mChildrenCursorHelpers.valueAt(pos).deactivate();
+        }
+        
+        mChildrenCursorHelpers.clear();
+    }
+    
+    @Override
+    public void notifyDataSetChanged() {
+        notifyDataSetChanged(true);
+    }
+
+    /**
+     * Notifies a data set change, but with the option of not releasing any
+     * cached cursors.
+     * 
+     * @param releaseCursors Whether to release and deactivate any cached
+     *            cursors.
+     */
+    public void notifyDataSetChanged(boolean releaseCursors) {
+        
+        if (releaseCursors) {
+            releaseCursorHelpers();
+        }
+        
+        super.notifyDataSetChanged();
+    }
+    
+    @Override
+    public void notifyDataSetInvalidated() {
+        releaseCursorHelpers();
+        super.notifyDataSetInvalidated();
+    }
+
+    @Override
+    public void onGroupCollapsed(int groupPosition) {
+        deactivateChildrenCursorHelper(groupPosition);
+    }
+
+    /**
+     * Deactivates the Cursor and removes the helper from cache.
+     * 
+     * @param groupPosition The group whose children Cursor and helper should be
+     *            deactivated.
+     */
+    synchronized void deactivateChildrenCursorHelper(int groupPosition) {
+        MyCursorHelper cursorHelper = getChildrenCursorHelper(groupPosition, true);
+        mChildrenCursorHelpers.remove(groupPosition);
+        cursorHelper.deactivate();
+    }
+
+    /**
+     * @see CursorAdapter#convertToString(Cursor)
+     */
+    public String convertToString(Cursor cursor) {
+        return cursor == null ? "" : cursor.toString();
+    }
+
+    /**
+     * @see CursorAdapter#runQueryOnBackgroundThread(CharSequence)
+     */
+    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+        if (mFilterQueryProvider != null) {
+            return mFilterQueryProvider.runQuery(constraint);
+        }
+
+        return mGroupCursorHelper.getCursor();
+    }
+    
+    public Filter getFilter() {
+        if (mCursorFilter == null) {
+            mCursorFilter = new CursorFilter(this);
+        }
+        return mCursorFilter;
+    }
+
+    /**
+     * @see CursorAdapter#getFilterQueryProvider()
+     */
+    public FilterQueryProvider getFilterQueryProvider() {
+        return mFilterQueryProvider;
+    }
+
+    /**
+     * @see CursorAdapter#setFilterQueryProvider(FilterQueryProvider)
+     */
+    public void setFilterQueryProvider(FilterQueryProvider filterQueryProvider) {
+        mFilterQueryProvider = filterQueryProvider;
+    }
+    
+    /**
+     * @see CursorAdapter#changeCursor(Cursor)
+     */
+    public void changeCursor(Cursor cursor) {
+        mGroupCursorHelper.changeCursor(cursor, true);
+    }
+
+    /**
+     * @see CursorAdapter#getCursor()
+     */
+    public Cursor getCursor() {
+        return mGroupCursorHelper.getCursor();
+    }
+
+    /**
+     * Helper class for Cursor management:
+     * <li> Data validity
+     * <li> Funneling the content and data set observers from a Cursor to a
+     *      single data set observer for widgets
+     * <li> ID from the Cursor for use in adapter IDs
+     * <li> Swapping cursors but maintaining other metadata
+     */
+    class MyCursorHelper {
+        private Cursor mCursor;
+        private boolean mDataValid;
+        private int mRowIDColumn;
+        private MyContentObserver mContentObserver;
+        private MyDataSetObserver mDataSetObserver;
+        
+        MyCursorHelper(Cursor cursor) {
+            final boolean cursorPresent = cursor != null;
+            mCursor = cursor;
+            mDataValid = cursorPresent;
+            mRowIDColumn = cursorPresent ? cursor.getColumnIndex("_id") : -1;
+            mContentObserver = new MyContentObserver();
+            mDataSetObserver = new MyDataSetObserver();
+            if (cursorPresent) {
+                cursor.registerContentObserver(mContentObserver);
+                cursor.registerDataSetObserver(mDataSetObserver);
+            }
+        }
+        
+        Cursor getCursor() {
+            return mCursor;
+        }
+
+        int getCount() {
+            if (mDataValid && mCursor != null) {
+                return mCursor.getCount();
+            } else {
+                return 0;
+            }
+        }
+        
+        long getId(int position) {
+            if (mDataValid && mCursor != null) {
+                if (mCursor.moveToPosition(position)) {
+                    return mCursor.getLong(mRowIDColumn);
+                } else {
+                    return 0;
+                }
+            } else {
+                return 0;
+            }
+        }
+        
+        Cursor moveTo(int position) {
+            if (mDataValid && (mCursor != null) && mCursor.moveToPosition(position)) {
+                return mCursor;
+            } else {
+                return null;
+            }
+        }
+        
+        void changeCursor(Cursor cursor, boolean releaseCursors) {
+            if (cursor == mCursor) return;
+
+            deactivate();
+            mCursor = cursor;
+            if (cursor != null) {
+                cursor.registerContentObserver(mContentObserver);
+                cursor.registerDataSetObserver(mDataSetObserver);
+                mRowIDColumn = cursor.getColumnIndex("_id");
+                mDataValid = true;
+                // notify the observers about the new cursor
+                notifyDataSetChanged(releaseCursors);
+            } else {
+                mRowIDColumn = -1;
+                mDataValid = false;
+                // notify the observers about the lack of a data set
+                notifyDataSetInvalidated();
+            }
+        }
+
+        void deactivate() {
+            if (mCursor == null) {
+                return;
+            }
+            
+            mCursor.unregisterContentObserver(mContentObserver);
+            mCursor.unregisterDataSetObserver(mDataSetObserver);
+            mCursor.close();
+            mCursor = null;
+        }
+        
+        boolean isValid() {
+            return mDataValid && mCursor != null;
+        }
+        
+        private class MyContentObserver extends ContentObserver {
+            public MyContentObserver() {
+                super(mHandler);
+            }
+
+            @Override
+            public boolean deliverSelfNotifications() {
+                return true;
+            }
+
+            @Override
+            public void onChange(boolean selfChange) {
+                if (mAutoRequery && mCursor != null && !mCursor.isClosed()) {
+                    if (false) Log.v("Cursor", "Auto requerying " + mCursor +
+                            " due to update");
+                    mDataValid = mCursor.requery();
+                }
+            }
+        }
+
+        private class MyDataSetObserver extends DataSetObserver {
+            @Override
+            public void onChanged() {
+                mDataValid = true;
+                notifyDataSetChanged();
+            }
+
+            @Override
+            public void onInvalidated() {
+                mDataValid = false;
+                notifyDataSetInvalidated();
+            }
+        }
+    }
+}
diff --git a/android/widget/DatePicker.java b/android/widget/DatePicker.java
new file mode 100644
index 0000000..dfb3642
--- /dev/null
+++ b/android/widget/DatePicker.java
@@ -0,0 +1,821 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.icu.util.Calendar;
+import android.icu.util.TimeZone;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * Provides a widget for selecting a date.
+ * <p>
+ * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
+ * set to {@code spinner}, the date can be selected using year, month, and day
+ * spinners or a {@link CalendarView}. The set of spinners and the calendar
+ * view are automatically synchronized. The client can customize whether only
+ * the spinners, or only the calendar view, or both to be displayed.
+ * </p>
+ * <p>
+ * When the {@link android.R.styleable#DatePicker_datePickerMode} attribute is
+ * set to {@code calendar}, the month and day can be selected using a
+ * calendar-style view while the year can be selected separately using a list.
+ * </p>
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide.
+ * </p>
+ * <p>
+ * For a dialog using this view, see {@link android.app.DatePickerDialog}.
+ * </p>
+ *
+ * @attr ref android.R.styleable#DatePicker_startYear
+ * @attr ref android.R.styleable#DatePicker_endYear
+ * @attr ref android.R.styleable#DatePicker_maxDate
+ * @attr ref android.R.styleable#DatePicker_minDate
+ * @attr ref android.R.styleable#DatePicker_spinnersShown
+ * @attr ref android.R.styleable#DatePicker_calendarViewShown
+ * @attr ref android.R.styleable#DatePicker_dayOfWeekBackground
+ * @attr ref android.R.styleable#DatePicker_dayOfWeekTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerBackground
+ * @attr ref android.R.styleable#DatePicker_headerMonthTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerDayOfMonthTextAppearance
+ * @attr ref android.R.styleable#DatePicker_headerYearTextAppearance
+ * @attr ref android.R.styleable#DatePicker_yearListItemTextAppearance
+ * @attr ref android.R.styleable#DatePicker_yearListSelectorColor
+ * @attr ref android.R.styleable#DatePicker_calendarTextColor
+ * @attr ref android.R.styleable#DatePicker_datePickerMode
+ */
+@Widget
+public class DatePicker extends FrameLayout {
+    private static final String LOG_TAG = DatePicker.class.getSimpleName();
+
+    /**
+     * Presentation mode for the Holo-style date picker that uses a set of
+     * {@link android.widget.NumberPicker}s.
+     *
+     * @see #getMode()
+     * @hide Visible for testing only.
+     */
+    @TestApi
+    public static final int MODE_SPINNER = 1;
+
+    /**
+     * Presentation mode for the Material-style date picker that uses a
+     * calendar.
+     *
+     * @see #getMode()
+     * @hide Visible for testing only.
+     */
+    @TestApi
+    public static final int MODE_CALENDAR = 2;
+
+    /** @hide */
+    @IntDef({MODE_SPINNER, MODE_CALENDAR})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DatePickerMode {}
+
+    private final DatePickerDelegate mDelegate;
+
+    @DatePickerMode
+    private final int mMode;
+
+    /**
+     * The callback used to indicate the user changed the date.
+     */
+    public interface OnDateChangedListener {
+
+        /**
+         * Called upon a date change.
+         *
+         * @param view The view associated with this listener.
+         * @param year The year that was set.
+         * @param monthOfYear The month that was set (0-11) for compatibility
+         *            with {@link java.util.Calendar}.
+         * @param dayOfMonth The day of the month that was set.
+         */
+        void onDateChanged(DatePicker view, int year, int monthOfYear, int dayOfMonth);
+    }
+
+    public DatePicker(Context context) {
+        this(context, null);
+    }
+
+    public DatePicker(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.datePickerStyle);
+    }
+
+    public DatePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DatePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // DatePicker is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.DatePicker,
+                defStyleAttr, defStyleRes);
+        final boolean isDialogMode = a.getBoolean(R.styleable.DatePicker_dialogMode, false);
+        final int requestedMode = a.getInt(R.styleable.DatePicker_datePickerMode, MODE_SPINNER);
+        final int firstDayOfWeek = a.getInt(R.styleable.DatePicker_firstDayOfWeek, 0);
+        a.recycle();
+
+        if (requestedMode == MODE_CALENDAR && isDialogMode) {
+            // You want MODE_CALENDAR? YOU CAN'T HANDLE MODE_CALENDAR! Well,
+            // maybe you can depending on your screen size. Let's check...
+            mMode = context.getResources().getInteger(R.integer.date_picker_mode);
+        } else {
+            mMode = requestedMode;
+        }
+
+        switch (mMode) {
+            case MODE_CALENDAR:
+                mDelegate = createCalendarUIDelegate(context, attrs, defStyleAttr, defStyleRes);
+                break;
+            case MODE_SPINNER:
+            default:
+                mDelegate = createSpinnerUIDelegate(context, attrs, defStyleAttr, defStyleRes);
+                break;
+        }
+
+        if (firstDayOfWeek != 0) {
+            setFirstDayOfWeek(firstDayOfWeek);
+        }
+
+        mDelegate.setAutoFillChangeListener((v, y, m, d) -> {
+            final AutofillManager afm = context.getSystemService(AutofillManager.class);
+            if (afm != null) {
+                afm.notifyValueChanged(this);
+            }
+        });
+    }
+
+    private DatePickerDelegate createSpinnerUIDelegate(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        return new DatePickerSpinnerDelegate(this, context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    private DatePickerDelegate createCalendarUIDelegate(Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        return new DatePickerCalendarDelegate(this, context, attrs, defStyleAttr,
+                defStyleRes);
+    }
+
+    /**
+     * @return the picker's presentation mode, one of {@link #MODE_CALENDAR} or
+     *         {@link #MODE_SPINNER}
+     * @attr ref android.R.styleable#DatePicker_datePickerMode
+     * @hide Visible for testing only.
+     */
+    @DatePickerMode
+    @TestApi
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Initialize the state. If the provided values designate an inconsistent
+     * date the values are normalized before updating the spinners.
+     *
+     * @param year The initial year.
+     * @param monthOfYear The initial month <strong>starting from zero</strong>.
+     * @param dayOfMonth The initial day of the month.
+     * @param onDateChangedListener How user is notified date is changed by
+     *            user, can be null.
+     */
+    public void init(int year, int monthOfYear, int dayOfMonth,
+                     OnDateChangedListener onDateChangedListener) {
+        mDelegate.init(year, monthOfYear, dayOfMonth, onDateChangedListener);
+    }
+
+    /**
+     * Set the callback that indicates the date has been adjusted by the user.
+     *
+     * @param onDateChangedListener How user is notified date is changed by
+     *            user, can be null.
+     */
+    public void setOnDateChangedListener(OnDateChangedListener onDateChangedListener) {
+        mDelegate.setOnDateChangedListener(onDateChangedListener);
+    }
+
+    /**
+     * Update the current date.
+     *
+     * @param year The year.
+     * @param month The month which is <strong>starting from zero</strong>.
+     * @param dayOfMonth The day of the month.
+     */
+    public void updateDate(int year, int month, int dayOfMonth) {
+        mDelegate.updateDate(year, month, dayOfMonth);
+    }
+
+    /**
+     * @return The selected year.
+     */
+    public int getYear() {
+        return mDelegate.getYear();
+    }
+
+    /**
+     * @return The selected month.
+     */
+    public int getMonth() {
+        return mDelegate.getMonth();
+    }
+
+    /**
+     * @return The selected day of month.
+     */
+    public int getDayOfMonth() {
+        return mDelegate.getDayOfMonth();
+    }
+
+    /**
+     * Gets the minimal date supported by this {@link DatePicker} in
+     * milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     * <p>
+     * Note: The default minimal date is 01/01/1900.
+     * <p>
+     *
+     * @return The minimal supported date.
+     */
+    public long getMinDate() {
+        return mDelegate.getMinDate().getTimeInMillis();
+    }
+
+    /**
+     * Sets the minimal date supported by this {@link NumberPicker} in
+     * milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     *
+     * @param minDate The minimal supported date.
+     */
+    public void setMinDate(long minDate) {
+        mDelegate.setMinDate(minDate);
+    }
+
+    /**
+     * Gets the maximal date supported by this {@link DatePicker} in
+     * milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     * <p>
+     * Note: The default maximal date is 12/31/2100.
+     * <p>
+     *
+     * @return The maximal supported date.
+     */
+    public long getMaxDate() {
+        return mDelegate.getMaxDate().getTimeInMillis();
+    }
+
+    /**
+     * Sets the maximal date supported by this {@link DatePicker} in
+     * milliseconds since January 1, 1970 00:00:00 in
+     * {@link TimeZone#getDefault()} time zone.
+     *
+     * @param maxDate The maximal supported date.
+     */
+    public void setMaxDate(long maxDate) {
+        mDelegate.setMaxDate(maxDate);
+    }
+
+    /**
+     * Sets the callback that indicates the current date is valid.
+     *
+     * @param callback the callback, may be null
+     * @hide
+     */
+    public void setValidationCallback(@Nullable ValidationCallback callback) {
+        mDelegate.setValidationCallback(callback);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (mDelegate.isEnabled() == enabled) {
+            return;
+        }
+        super.setEnabled(enabled);
+        mDelegate.setEnabled(enabled);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mDelegate.isEnabled();
+    }
+
+    /** @hide */
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        return mDelegate.dispatchPopulateAccessibilityEvent(event);
+    }
+
+    /** @hide */
+    @Override
+    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEventInternal(event);
+        mDelegate.onPopulateAccessibilityEvent(event);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return DatePicker.class.getName();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        mDelegate.onConfigurationChanged(newConfig);
+    }
+
+    /**
+     * Sets the first day of week.
+     *
+     * @param firstDayOfWeek The first day of the week conforming to the
+     *            {@link CalendarView} APIs.
+     * @see Calendar#SUNDAY
+     * @see Calendar#MONDAY
+     * @see Calendar#TUESDAY
+     * @see Calendar#WEDNESDAY
+     * @see Calendar#THURSDAY
+     * @see Calendar#FRIDAY
+     * @see Calendar#SATURDAY
+     *
+     * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
+     */
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        if (firstDayOfWeek < Calendar.SUNDAY || firstDayOfWeek > Calendar.SATURDAY) {
+            throw new IllegalArgumentException("firstDayOfWeek must be between 1 and 7");
+        }
+        mDelegate.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    /**
+     * Gets the first day of week.
+     *
+     * @return The first day of the week conforming to the {@link CalendarView}
+     *         APIs.
+     * @see Calendar#SUNDAY
+     * @see Calendar#MONDAY
+     * @see Calendar#TUESDAY
+     * @see Calendar#WEDNESDAY
+     * @see Calendar#THURSDAY
+     * @see Calendar#FRIDAY
+     * @see Calendar#SATURDAY
+     *
+     * @attr ref android.R.styleable#DatePicker_firstDayOfWeek
+     */
+    public int getFirstDayOfWeek() {
+        return mDelegate.getFirstDayOfWeek();
+    }
+
+    /**
+     * Returns whether the {@link CalendarView} is shown.
+     * <p>
+     * <strong>Note:</strong> This method returns {@code false} when the
+     * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+     * to {@code calendar}.
+     *
+     * @return {@code true} if the calendar view is shown
+     * @see #getCalendarView()
+     * @deprecated Not supported by Material-style {@code calendar} mode
+     */
+    @Deprecated
+    public boolean getCalendarViewShown() {
+        return mDelegate.getCalendarViewShown();
+    }
+
+    /**
+     * Returns the {@link CalendarView} used by this picker.
+     * <p>
+     * <strong>Note:</strong> This method throws an
+     * {@link UnsupportedOperationException} when the
+     * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+     * to {@code calendar}.
+     *
+     * @return the calendar view
+     * @see #getCalendarViewShown()
+     * @deprecated Not supported by Material-style {@code calendar} mode
+     * @throws UnsupportedOperationException if called when the picker is
+     *         displayed in {@code calendar} mode
+     */
+    @Deprecated
+    public CalendarView getCalendarView() {
+        return mDelegate.getCalendarView();
+    }
+
+    /**
+     * Sets whether the {@link CalendarView} is shown.
+     * <p>
+     * <strong>Note:</strong> Calling this method has no effect when the
+     * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+     * to {@code calendar}.
+     *
+     * @param shown {@code true} to show the calendar view, {@code false} to
+     *              hide it
+     * @deprecated Not supported by Material-style {@code calendar} mode
+     */
+    @Deprecated
+    public void setCalendarViewShown(boolean shown) {
+        mDelegate.setCalendarViewShown(shown);
+    }
+
+    /**
+     * Returns whether the spinners are shown.
+     * <p>
+     * <strong>Note:</strong> his method returns {@code false} when the
+     * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+     * to {@code calendar}.
+     *
+     * @return {@code true} if the spinners are shown
+     * @deprecated Not supported by Material-style {@code calendar} mode
+     */
+    @Deprecated
+    public boolean getSpinnersShown() {
+        return mDelegate.getSpinnersShown();
+    }
+
+    /**
+     * Sets whether the spinners are shown.
+     * <p>
+     * Calling this method has no effect when the
+     * {@link android.R.styleable#DatePicker_datePickerMode} attribute is set
+     * to {@code calendar}.
+     *
+     * @param shown {@code true} to show the spinners, {@code false} to hide
+     *              them
+     * @deprecated Not supported by Material-style {@code calendar} mode
+     */
+    @Deprecated
+    public void setSpinnersShown(boolean shown) {
+        mDelegate.setSpinnersShown(shown);
+    }
+
+    @Override
+    protected void dispatchRestoreInstanceState(SparseArray<Parcelable> container) {
+        dispatchThawSelfOnly(container);
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        return mDelegate.onSaveInstanceState(superState);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        BaseSavedState ss = (BaseSavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mDelegate.onRestoreInstanceState(ss);
+    }
+
+    /**
+     * A delegate interface that defined the public API of the DatePicker. Allows different
+     * DatePicker implementations. This would need to be implemented by the DatePicker delegates
+     * for the real behavior.
+     *
+     * @hide
+     */
+    interface DatePickerDelegate {
+        void init(int year, int monthOfYear, int dayOfMonth,
+                  OnDateChangedListener onDateChangedListener);
+
+        void setOnDateChangedListener(OnDateChangedListener onDateChangedListener);
+        void setAutoFillChangeListener(OnDateChangedListener onDateChangedListener);
+
+        void updateDate(int year, int month, int dayOfMonth);
+
+        int getYear();
+        int getMonth();
+        int getDayOfMonth();
+
+        void autofill(AutofillValue value);
+        AutofillValue getAutofillValue();
+
+        void setFirstDayOfWeek(int firstDayOfWeek);
+        int getFirstDayOfWeek();
+
+        void setMinDate(long minDate);
+        Calendar getMinDate();
+
+        void setMaxDate(long maxDate);
+        Calendar getMaxDate();
+
+        void setEnabled(boolean enabled);
+        boolean isEnabled();
+
+        CalendarView getCalendarView();
+
+        void setCalendarViewShown(boolean shown);
+        boolean getCalendarViewShown();
+
+        void setSpinnersShown(boolean shown);
+        boolean getSpinnersShown();
+
+        void setValidationCallback(ValidationCallback callback);
+
+        void onConfigurationChanged(Configuration newConfig);
+
+        Parcelable onSaveInstanceState(Parcelable superState);
+        void onRestoreInstanceState(Parcelable state);
+
+        boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+        void onPopulateAccessibilityEvent(AccessibilityEvent event);
+    }
+
+    /**
+     * An abstract class which can be used as a start for DatePicker implementations
+     */
+    abstract static class AbstractDatePickerDelegate implements DatePickerDelegate {
+        // The delegator
+        protected DatePicker mDelegator;
+
+        // The context
+        protected Context mContext;
+
+        // NOTE: when subclasses change this variable, they must call resetAutofilledValue().
+        protected Calendar mCurrentDate;
+
+        // The current locale
+        protected Locale mCurrentLocale;
+
+        // Callbacks
+        protected OnDateChangedListener mOnDateChangedListener;
+        protected OnDateChangedListener mAutoFillChangeListener;
+        protected ValidationCallback mValidationCallback;
+
+        // The value that was passed to autofill() - it must be stored because it getAutofillValue()
+        // must return the exact same value that was autofilled, otherwise the widget will not be
+        // properly highlighted after autofill().
+        private long mAutofilledValue;
+
+        public AbstractDatePickerDelegate(DatePicker delegator, Context context) {
+            mDelegator = delegator;
+            mContext = context;
+
+            setCurrentLocale(Locale.getDefault());
+        }
+
+        protected void setCurrentLocale(Locale locale) {
+            if (!locale.equals(mCurrentLocale)) {
+                mCurrentLocale = locale;
+                onLocaleChanged(locale);
+            }
+        }
+
+        @Override
+        public void setOnDateChangedListener(OnDateChangedListener callback) {
+            mOnDateChangedListener = callback;
+        }
+
+        @Override
+        public void setAutoFillChangeListener(OnDateChangedListener callback) {
+            mAutoFillChangeListener = callback;
+        }
+
+        @Override
+        public void setValidationCallback(ValidationCallback callback) {
+            mValidationCallback = callback;
+        }
+
+        @Override
+        public final void autofill(AutofillValue value) {
+            if (value == null || !value.isDate()) {
+                Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+                return;
+            }
+
+            final long time = value.getDateValue();
+
+            final Calendar cal = Calendar.getInstance(mCurrentLocale);
+            cal.setTimeInMillis(time);
+            updateDate(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH),
+                    cal.get(Calendar.DAY_OF_MONTH));
+
+            // Must set mAutofilledValue *after* calling subclass method to make sure the value
+            // returned by getAutofillValue() matches it.
+            mAutofilledValue = time;
+        }
+
+        @Override
+        public final AutofillValue getAutofillValue() {
+            final long time = mAutofilledValue != 0
+                    ? mAutofilledValue
+                    : mCurrentDate.getTimeInMillis();
+            return AutofillValue.forDate(time);
+        }
+
+        /**
+         * This method must be called every time the value of the year, month, and/or day is
+         * changed by a subclass method.
+         */
+        protected void resetAutofilledValue() {
+            mAutofilledValue = 0;
+        }
+
+        protected void onValidationChanged(boolean valid) {
+            if (mValidationCallback != null) {
+                mValidationCallback.onValidationChanged(valid);
+            }
+        }
+
+        protected void onLocaleChanged(Locale locale) {
+            // Stub.
+        }
+
+        @Override
+        public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+            event.getText().add(getFormattedCurrentDate());
+        }
+
+        protected String getFormattedCurrentDate() {
+           return DateUtils.formatDateTime(mContext, mCurrentDate.getTimeInMillis(),
+                   DateUtils.FORMAT_SHOW_DATE | DateUtils.FORMAT_SHOW_YEAR
+                           | DateUtils.FORMAT_SHOW_WEEKDAY);
+        }
+
+        /**
+         * Class for managing state storing/restoring.
+         */
+        static class SavedState extends View.BaseSavedState {
+            private final int mSelectedYear;
+            private final int mSelectedMonth;
+            private final int mSelectedDay;
+            private final long mMinDate;
+            private final long mMaxDate;
+            private final int mCurrentView;
+            private final int mListPosition;
+            private final int mListPositionOffset;
+
+            public SavedState(Parcelable superState, int year, int month, int day, long minDate,
+                    long maxDate) {
+                this(superState, year, month, day, minDate, maxDate, 0, 0, 0);
+            }
+
+            /**
+             * Constructor called from {@link DatePicker#onSaveInstanceState()}
+             */
+            public SavedState(Parcelable superState, int year, int month, int day, long minDate,
+                    long maxDate, int currentView, int listPosition, int listPositionOffset) {
+                super(superState);
+                mSelectedYear = year;
+                mSelectedMonth = month;
+                mSelectedDay = day;
+                mMinDate = minDate;
+                mMaxDate = maxDate;
+                mCurrentView = currentView;
+                mListPosition = listPosition;
+                mListPositionOffset = listPositionOffset;
+            }
+
+            /**
+             * Constructor called from {@link #CREATOR}
+             */
+            private SavedState(Parcel in) {
+                super(in);
+                mSelectedYear = in.readInt();
+                mSelectedMonth = in.readInt();
+                mSelectedDay = in.readInt();
+                mMinDate = in.readLong();
+                mMaxDate = in.readLong();
+                mCurrentView = in.readInt();
+                mListPosition = in.readInt();
+                mListPositionOffset = in.readInt();
+            }
+
+            @Override
+            public void writeToParcel(Parcel dest, int flags) {
+                super.writeToParcel(dest, flags);
+                dest.writeInt(mSelectedYear);
+                dest.writeInt(mSelectedMonth);
+                dest.writeInt(mSelectedDay);
+                dest.writeLong(mMinDate);
+                dest.writeLong(mMaxDate);
+                dest.writeInt(mCurrentView);
+                dest.writeInt(mListPosition);
+                dest.writeInt(mListPositionOffset);
+            }
+
+            public int getSelectedDay() {
+                return mSelectedDay;
+            }
+
+            public int getSelectedMonth() {
+                return mSelectedMonth;
+            }
+
+            public int getSelectedYear() {
+                return mSelectedYear;
+            }
+
+            public long getMinDate() {
+                return mMinDate;
+            }
+
+            public long getMaxDate() {
+                return mMaxDate;
+            }
+
+            public int getCurrentView() {
+                return mCurrentView;
+            }
+
+            public int getListPosition() {
+                return mListPosition;
+            }
+
+            public int getListPositionOffset() {
+                return mListPositionOffset;
+            }
+
+            @SuppressWarnings("all")
+            // suppress unused and hiding
+            public static final Parcelable.Creator<SavedState> CREATOR = new Creator<SavedState>() {
+
+                public SavedState createFromParcel(Parcel in) {
+                    return new SavedState(in);
+                }
+
+                public SavedState[] newArray(int size) {
+                    return new SavedState[size];
+                }
+            };
+        }
+    }
+
+    /**
+     * A callback interface for updating input validity when the date picker
+     * when included into a dialog.
+     *
+     * @hide
+     */
+    public interface ValidationCallback {
+        void onValidationChanged(boolean valid);
+    }
+
+    @Override
+    public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
+        // This view is self-sufficient for autofill, so it needs to call
+        // onProvideAutoFillStructure() to fill itself, but it does not need to call
+        // dispatchProvideAutoFillStructure() to fill its children.
+        structure.setAutofillId(getAutofillId());
+        onProvideAutofillStructure(structure, flags);
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!isEnabled()) return;
+
+        mDelegate.autofill(value);
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
+    }
+
+    @Override
+    public AutofillValue getAutofillValue() {
+        return isEnabled() ? mDelegate.getAutofillValue() : null;
+    }
+}
diff --git a/android/widget/DatePickerCalendarDelegate.java b/android/widget/DatePickerCalendarDelegate.java
new file mode 100644
index 0000000..60b4757
--- /dev/null
+++ b/android/widget/DatePickerCalendarDelegate.java
@@ -0,0 +1,617 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.icu.text.DisplayContext;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.os.Parcelable;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.DayPickerView.OnDaySelectedListener;
+import android.widget.YearPickerView.OnYearSelectedListener;
+
+import com.android.internal.R;
+
+import java.util.Locale;
+
+/**
+ * A delegate for picking up a date (day / month / year).
+ */
+class DatePickerCalendarDelegate extends DatePicker.AbstractDatePickerDelegate {
+    private static final int USE_LOCALE = 0;
+
+    private static final int UNINITIALIZED = -1;
+    private static final int VIEW_MONTH_DAY = 0;
+    private static final int VIEW_YEAR = 1;
+
+    private static final int DEFAULT_START_YEAR = 1900;
+    private static final int DEFAULT_END_YEAR = 2100;
+
+    private static final int ANIMATION_DURATION = 300;
+
+    private static final int[] ATTRS_TEXT_COLOR = new int[] {
+            com.android.internal.R.attr.textColor};
+    private static final int[] ATTRS_DISABLED_ALPHA = new int[] {
+            com.android.internal.R.attr.disabledAlpha};
+
+    private SimpleDateFormat mYearFormat;
+    private SimpleDateFormat mMonthDayFormat;
+
+    // Top-level container.
+    private ViewGroup mContainer;
+
+    // Header views.
+    private TextView mHeaderYear;
+    private TextView mHeaderMonthDay;
+
+    // Picker views.
+    private ViewAnimator mAnimator;
+    private DayPickerView mDayPickerView;
+    private YearPickerView mYearPickerView;
+
+    // Accessibility strings.
+    private String mSelectDay;
+    private String mSelectYear;
+
+    private int mCurrentView = UNINITIALIZED;
+
+    private final Calendar mTempDate;
+    private final Calendar mMinDate;
+    private final Calendar mMaxDate;
+
+    private int mFirstDayOfWeek = USE_LOCALE;
+
+    public DatePickerCalendarDelegate(DatePicker delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        final Locale locale = mCurrentLocale;
+        mCurrentDate = Calendar.getInstance(locale);
+        mTempDate = Calendar.getInstance(locale);
+        mMinDate = Calendar.getInstance(locale);
+        mMaxDate = Calendar.getInstance(locale);
+
+        mMinDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
+        mMaxDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
+
+        final Resources res = mDelegator.getResources();
+        final TypedArray a = mContext.obtainStyledAttributes(attrs,
+                R.styleable.DatePicker, defStyleAttr, defStyleRes);
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        final int layoutResourceId = a.getResourceId(
+                R.styleable.DatePicker_internalLayout, R.layout.date_picker_material);
+
+        // Set up and attach container.
+        mContainer = (ViewGroup) inflater.inflate(layoutResourceId, mDelegator, false);
+        mContainer.setSaveFromParentEnabled(false);
+        mDelegator.addView(mContainer);
+
+        // Set up header views.
+        final ViewGroup header = mContainer.findViewById(R.id.date_picker_header);
+        mHeaderYear = header.findViewById(R.id.date_picker_header_year);
+        mHeaderYear.setOnClickListener(mOnHeaderClickListener);
+        mHeaderMonthDay = header.findViewById(R.id.date_picker_header_date);
+        mHeaderMonthDay.setOnClickListener(mOnHeaderClickListener);
+
+        // For the sake of backwards compatibility, attempt to extract the text
+        // color from the header month text appearance. If it's set, we'll let
+        // that override the "real" header text color.
+        ColorStateList headerTextColor = null;
+
+        @SuppressWarnings("deprecation")
+        final int monthHeaderTextAppearance = a.getResourceId(
+                R.styleable.DatePicker_headerMonthTextAppearance, 0);
+        if (monthHeaderTextAppearance != 0) {
+            final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
+                    ATTRS_TEXT_COLOR, 0, monthHeaderTextAppearance);
+            final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
+            headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
+            textAppearance.recycle();
+        }
+
+        if (headerTextColor == null) {
+            headerTextColor = a.getColorStateList(R.styleable.DatePicker_headerTextColor);
+        }
+
+        if (headerTextColor != null) {
+            mHeaderYear.setTextColor(headerTextColor);
+            mHeaderMonthDay.setTextColor(headerTextColor);
+        }
+
+        // Set up header background, if available.
+        if (a.hasValueOrEmpty(R.styleable.DatePicker_headerBackground)) {
+            header.setBackground(a.getDrawable(R.styleable.DatePicker_headerBackground));
+        }
+
+        a.recycle();
+
+        // Set up picker container.
+        mAnimator = mContainer.findViewById(R.id.animator);
+
+        // Set up day picker view.
+        mDayPickerView = mAnimator.findViewById(R.id.date_picker_day_picker);
+        mDayPickerView.setFirstDayOfWeek(mFirstDayOfWeek);
+        mDayPickerView.setMinDate(mMinDate.getTimeInMillis());
+        mDayPickerView.setMaxDate(mMaxDate.getTimeInMillis());
+        mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+        mDayPickerView.setOnDaySelectedListener(mOnDaySelectedListener);
+
+        // Set up year picker view.
+        mYearPickerView = mAnimator.findViewById(R.id.date_picker_year_picker);
+        mYearPickerView.setRange(mMinDate, mMaxDate);
+        mYearPickerView.setYear(mCurrentDate.get(Calendar.YEAR));
+        mYearPickerView.setOnYearSelectedListener(mOnYearSelectedListener);
+
+        // Set up content descriptions.
+        mSelectDay = res.getString(R.string.select_day);
+        mSelectYear = res.getString(R.string.select_year);
+
+        // Initialize for current locale. This also initializes the date, so no
+        // need to call onDateChanged.
+        onLocaleChanged(mCurrentLocale);
+
+        setCurrentView(VIEW_MONTH_DAY);
+    }
+
+    /**
+     * The legacy text color might have been poorly defined. Ensures that it
+     * has an appropriate activated state, using the selected state if one
+     * exists or modifying the default text color otherwise.
+     *
+     * @param color a legacy text color, or {@code null}
+     * @return a color state list with an appropriate activated state, or
+     *         {@code null} if a valid activated state could not be generated
+     */
+    @Nullable
+    private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
+        if (color == null || color.hasState(R.attr.state_activated)) {
+            return color;
+        }
+
+        final int activatedColor;
+        final int defaultColor;
+        if (color.hasState(R.attr.state_selected)) {
+            activatedColor = color.getColorForState(StateSet.get(
+                    StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
+            defaultColor = color.getColorForState(StateSet.get(
+                    StateSet.VIEW_STATE_ENABLED), 0);
+        } else {
+            activatedColor = color.getDefaultColor();
+
+            // Generate a non-activated color using the disabled alpha.
+            final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
+            final float disabledAlpha = ta.getFloat(0, 0.30f);
+            defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
+        }
+
+        if (activatedColor == 0 || defaultColor == 0) {
+            // We somehow failed to obtain the colors.
+            return null;
+        }
+
+        final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
+        final int[] colors = new int[] { activatedColor, defaultColor };
+        return new ColorStateList(stateSet, colors);
+    }
+
+    private int multiplyAlphaComponent(int color, float alphaMod) {
+        final int srcRgb = color & 0xFFFFFF;
+        final int srcAlpha = (color >> 24) & 0xFF;
+        final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
+        return srcRgb | (dstAlpha << 24);
+    }
+
+    /**
+     * Listener called when the user selects a day in the day picker view.
+     */
+    private final OnDaySelectedListener mOnDaySelectedListener = new OnDaySelectedListener() {
+        @Override
+        public void onDaySelected(DayPickerView view, Calendar day) {
+            mCurrentDate.setTimeInMillis(day.getTimeInMillis());
+            onDateChanged(true, true);
+        }
+    };
+
+    /**
+     * Listener called when the user selects a year in the year picker view.
+     */
+    private final OnYearSelectedListener mOnYearSelectedListener = new OnYearSelectedListener() {
+        @Override
+        public void onYearChanged(YearPickerView view, int year) {
+            // If the newly selected month / year does not contain the
+            // currently selected day number, change the selected day number
+            // to the last day of the selected month or year.
+            // e.g. Switching from Mar to Apr when Mar 31 is selected -> Apr 30
+            // e.g. Switching from 2012 to 2013 when Feb 29, 2012 is selected -> Feb 28, 2013
+            final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+            final int month = mCurrentDate.get(Calendar.MONTH);
+            final int daysInMonth = getDaysInMonth(month, year);
+            if (day > daysInMonth) {
+                mCurrentDate.set(Calendar.DAY_OF_MONTH, daysInMonth);
+            }
+
+            mCurrentDate.set(Calendar.YEAR, year);
+            onDateChanged(true, true);
+
+            // Automatically switch to day picker.
+            setCurrentView(VIEW_MONTH_DAY);
+
+            // Switch focus back to the year text.
+            mHeaderYear.requestFocus();
+        }
+    };
+
+    /**
+     * Listener called when the user clicks on a header item.
+     */
+    private final OnClickListener mOnHeaderClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            tryVibrate();
+
+            switch (v.getId()) {
+                case R.id.date_picker_header_year:
+                    setCurrentView(VIEW_YEAR);
+                    break;
+                case R.id.date_picker_header_date:
+                    setCurrentView(VIEW_MONTH_DAY);
+                    break;
+            }
+        }
+    };
+
+    @Override
+    protected void onLocaleChanged(Locale locale) {
+        final TextView headerYear = mHeaderYear;
+        if (headerYear == null) {
+            // Abort, we haven't initialized yet. This method will get called
+            // again later after everything has been set up.
+            return;
+        }
+
+        // Update the date formatter.
+        final String datePattern = DateFormat.getBestDateTimePattern(locale, "EMMMd");
+        mMonthDayFormat = new SimpleDateFormat(datePattern, locale);
+        mMonthDayFormat.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+        mYearFormat = new SimpleDateFormat("y", locale);
+
+        // Update the header text.
+        onCurrentDateChanged(false);
+    }
+
+    private void onCurrentDateChanged(boolean announce) {
+        if (mHeaderYear == null) {
+            // Abort, we haven't initialized yet. This method will get called
+            // again later after everything has been set up.
+            return;
+        }
+
+        final String year = mYearFormat.format(mCurrentDate.getTime());
+        mHeaderYear.setText(year);
+
+        final String monthDay = mMonthDayFormat.format(mCurrentDate.getTime());
+        mHeaderMonthDay.setText(monthDay);
+
+        // TODO: This should use live regions.
+        if (announce) {
+            mAnimator.announceForAccessibility(getFormattedCurrentDate());
+        }
+    }
+
+    private void setCurrentView(final int viewIndex) {
+        switch (viewIndex) {
+            case VIEW_MONTH_DAY:
+                mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+
+                if (mCurrentView != viewIndex) {
+                    mHeaderMonthDay.setActivated(true);
+                    mHeaderYear.setActivated(false);
+                    mAnimator.setDisplayedChild(VIEW_MONTH_DAY);
+                    mCurrentView = viewIndex;
+                }
+
+                mAnimator.announceForAccessibility(mSelectDay);
+                break;
+            case VIEW_YEAR:
+                final int year = mCurrentDate.get(Calendar.YEAR);
+                mYearPickerView.setYear(year);
+                mYearPickerView.post(new Runnable() {
+                    @Override
+                    public void run() {
+                        mYearPickerView.requestFocus();
+                        final View selected = mYearPickerView.getSelectedView();
+                        if (selected != null) {
+                            selected.requestFocus();
+                        }
+                    }
+                });
+
+                if (mCurrentView != viewIndex) {
+                    mHeaderMonthDay.setActivated(false);
+                    mHeaderYear.setActivated(true);
+                    mAnimator.setDisplayedChild(VIEW_YEAR);
+                    mCurrentView = viewIndex;
+                }
+
+                mAnimator.announceForAccessibility(mSelectYear);
+                break;
+        }
+    }
+
+    @Override
+    public void init(int year, int month, int dayOfMonth,
+            DatePicker.OnDateChangedListener callBack) {
+        setDate(year, month, dayOfMonth);
+        onDateChanged(false, false);
+
+        mOnDateChangedListener = callBack;
+    }
+
+    @Override
+    public void updateDate(int year, int month, int dayOfMonth) {
+        setDate(year, month, dayOfMonth);
+        onDateChanged(false, true);
+    }
+
+    private void setDate(int year, int month, int dayOfMonth) {
+        mCurrentDate.set(Calendar.YEAR, year);
+        mCurrentDate.set(Calendar.MONTH, month);
+        mCurrentDate.set(Calendar.DAY_OF_MONTH, dayOfMonth);
+        resetAutofilledValue();
+    }
+
+    private void onDateChanged(boolean fromUser, boolean callbackToClient) {
+        final int year = mCurrentDate.get(Calendar.YEAR);
+
+        if (callbackToClient
+                && (mOnDateChangedListener != null || mAutoFillChangeListener != null)) {
+            final int monthOfYear = mCurrentDate.get(Calendar.MONTH);
+            final int dayOfMonth = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+            if (mOnDateChangedListener != null) {
+                mOnDateChangedListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
+            }
+            if (mAutoFillChangeListener != null) {
+                mAutoFillChangeListener.onDateChanged(mDelegator, year, monthOfYear, dayOfMonth);
+            }
+        }
+
+        mDayPickerView.setDate(mCurrentDate.getTimeInMillis());
+        mYearPickerView.setYear(year);
+
+        onCurrentDateChanged(fromUser);
+
+        if (fromUser) {
+            tryVibrate();
+        }
+    }
+
+    @Override
+    public int getYear() {
+        return mCurrentDate.get(Calendar.YEAR);
+    }
+
+    @Override
+    public int getMonth() {
+        return mCurrentDate.get(Calendar.MONTH);
+    }
+
+    @Override
+    public int getDayOfMonth() {
+        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
+    }
+
+    @Override
+    public void setMinDate(long minDate) {
+        mTempDate.setTimeInMillis(minDate);
+        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
+                && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
+            // Same day, no-op.
+            return;
+        }
+        if (mCurrentDate.before(mTempDate)) {
+            mCurrentDate.setTimeInMillis(minDate);
+            onDateChanged(false, true);
+        }
+        mMinDate.setTimeInMillis(minDate);
+        mDayPickerView.setMinDate(minDate);
+        mYearPickerView.setRange(mMinDate, mMaxDate);
+    }
+
+    @Override
+    public Calendar getMinDate() {
+        return mMinDate;
+    }
+
+    @Override
+    public void setMaxDate(long maxDate) {
+        mTempDate.setTimeInMillis(maxDate);
+        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
+                && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
+            // Same day, no-op.
+            return;
+        }
+        if (mCurrentDate.after(mTempDate)) {
+            mCurrentDate.setTimeInMillis(maxDate);
+            onDateChanged(false, true);
+        }
+        mMaxDate.setTimeInMillis(maxDate);
+        mDayPickerView.setMaxDate(maxDate);
+        mYearPickerView.setRange(mMinDate, mMaxDate);
+    }
+
+    @Override
+    public Calendar getMaxDate() {
+        return mMaxDate;
+    }
+
+    @Override
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        mFirstDayOfWeek = firstDayOfWeek;
+
+        mDayPickerView.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    @Override
+    public int getFirstDayOfWeek() {
+        if (mFirstDayOfWeek != USE_LOCALE) {
+            return mFirstDayOfWeek;
+        }
+        return mCurrentDate.getFirstDayOfWeek();
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mContainer.setEnabled(enabled);
+        mDayPickerView.setEnabled(enabled);
+        mYearPickerView.setEnabled(enabled);
+        mHeaderYear.setEnabled(enabled);
+        mHeaderMonthDay.setEnabled(enabled);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mContainer.isEnabled();
+    }
+
+    @Override
+    public CalendarView getCalendarView() {
+        throw new UnsupportedOperationException("Not supported by calendar-mode DatePicker");
+    }
+
+    @Override
+    public void setCalendarViewShown(boolean shown) {
+        // No-op for compatibility with the old DatePicker.
+    }
+
+    @Override
+    public boolean getCalendarViewShown() {
+        return false;
+    }
+
+    @Override
+    public void setSpinnersShown(boolean shown) {
+        // No-op for compatibility with the old DatePicker.
+    }
+
+    @Override
+    public boolean getSpinnersShown() {
+        return false;
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        setCurrentLocale(newConfig.locale);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        final int year = mCurrentDate.get(Calendar.YEAR);
+        final int month = mCurrentDate.get(Calendar.MONTH);
+        final int day = mCurrentDate.get(Calendar.DAY_OF_MONTH);
+
+        int listPosition = -1;
+        int listPositionOffset = -1;
+
+        if (mCurrentView == VIEW_MONTH_DAY) {
+            listPosition = mDayPickerView.getMostVisiblePosition();
+        } else if (mCurrentView == VIEW_YEAR) {
+            listPosition = mYearPickerView.getFirstVisiblePosition();
+            listPositionOffset = mYearPickerView.getFirstPositionOffset();
+        }
+
+        return new SavedState(superState, year, month, day, mMinDate.getTimeInMillis(),
+                mMaxDate.getTimeInMillis(), mCurrentView, listPosition, listPositionOffset);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            final SavedState ss = (SavedState) state;
+
+            // TODO: Move instance state into DayPickerView, YearPickerView.
+            mCurrentDate.set(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
+            mMinDate.setTimeInMillis(ss.getMinDate());
+            mMaxDate.setTimeInMillis(ss.getMaxDate());
+
+            onCurrentDateChanged(false);
+
+            final int currentView = ss.getCurrentView();
+            setCurrentView(currentView);
+
+            final int listPosition = ss.getListPosition();
+            if (listPosition != -1) {
+                if (currentView == VIEW_MONTH_DAY) {
+                    mDayPickerView.setPosition(listPosition);
+                } else if (currentView == VIEW_YEAR) {
+                    final int listPositionOffset = ss.getListPositionOffset();
+                    mYearPickerView.setSelectionFromTop(listPosition, listPositionOffset);
+                }
+            }
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    public CharSequence getAccessibilityClassName() {
+        return DatePicker.class.getName();
+    }
+
+    public static int getDaysInMonth(int month, int year) {
+        switch (month) {
+            case Calendar.JANUARY:
+            case Calendar.MARCH:
+            case Calendar.MAY:
+            case Calendar.JULY:
+            case Calendar.AUGUST:
+            case Calendar.OCTOBER:
+            case Calendar.DECEMBER:
+                return 31;
+            case Calendar.APRIL:
+            case Calendar.JUNE:
+            case Calendar.SEPTEMBER:
+            case Calendar.NOVEMBER:
+                return 30;
+            case Calendar.FEBRUARY:
+                return (year % 4 == 0) ? 29 : 28;
+            default:
+                throw new IllegalArgumentException("Invalid Month");
+        }
+    }
+
+    private void tryVibrate() {
+        mDelegator.performHapticFeedback(HapticFeedbackConstants.CALENDAR_DATE);
+    }
+}
diff --git a/android/widget/DatePickerController.java b/android/widget/DatePickerController.java
new file mode 100644
index 0000000..8f809ba
--- /dev/null
+++ b/android/widget/DatePickerController.java
@@ -0,0 +1,35 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import java.util.Calendar;
+
+/**
+ * Controller class to communicate among the various components of the date picker dialog.
+ *
+ * @hide
+ */
+interface DatePickerController {
+
+    void onYearSelected(int year);
+
+    void registerOnDateChangedListener(OnDateChangedListener listener);
+
+    Calendar getSelectedDay();
+
+    void tryVibrate();
+}
diff --git a/android/widget/DatePickerSpinnerDelegate.java b/android/widget/DatePickerSpinnerDelegate.java
new file mode 100644
index 0000000..dba74b1
--- /dev/null
+++ b/android/widget/DatePickerSpinnerDelegate.java
@@ -0,0 +1,650 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.icu.util.Calendar;
+import android.os.Parcelable;
+import android.text.InputType;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.DatePicker.AbstractDatePickerDelegate;
+import android.widget.NumberPicker.OnValueChangeListener;
+
+import libcore.icu.ICU;
+
+import java.text.DateFormatSymbols;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * A delegate implementing the basic DatePicker
+ */
+class DatePickerSpinnerDelegate extends AbstractDatePickerDelegate {
+
+    private static final String DATE_FORMAT = "MM/dd/yyyy";
+
+    private static final int DEFAULT_START_YEAR = 1900;
+
+    private static final int DEFAULT_END_YEAR = 2100;
+
+    private static final boolean DEFAULT_CALENDAR_VIEW_SHOWN = true;
+
+    private static final boolean DEFAULT_SPINNERS_SHOWN = true;
+
+    private static final boolean DEFAULT_ENABLED_STATE = true;
+
+    private final LinearLayout mSpinners;
+
+    private final NumberPicker mDaySpinner;
+
+    private final NumberPicker mMonthSpinner;
+
+    private final NumberPicker mYearSpinner;
+
+    private final EditText mDaySpinnerInput;
+
+    private final EditText mMonthSpinnerInput;
+
+    private final EditText mYearSpinnerInput;
+
+    private final CalendarView mCalendarView;
+
+    private String[] mShortMonths;
+
+    private final java.text.DateFormat mDateFormat = new SimpleDateFormat(DATE_FORMAT);
+
+    private int mNumberOfMonths;
+
+    private Calendar mTempDate;
+
+    private Calendar mMinDate;
+
+    private Calendar mMaxDate;
+
+    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+
+    DatePickerSpinnerDelegate(DatePicker delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        mDelegator = delegator;
+        mContext = context;
+
+        // initialization based on locale
+        setCurrentLocale(Locale.getDefault());
+
+        final TypedArray attributesArray = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.DatePicker, defStyleAttr, defStyleRes);
+        boolean spinnersShown = attributesArray.getBoolean(com.android.internal.R.styleable.DatePicker_spinnersShown,
+                DEFAULT_SPINNERS_SHOWN);
+        boolean calendarViewShown = attributesArray.getBoolean(
+                com.android.internal.R.styleable.DatePicker_calendarViewShown, DEFAULT_CALENDAR_VIEW_SHOWN);
+        int startYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_startYear,
+                DEFAULT_START_YEAR);
+        int endYear = attributesArray.getInt(com.android.internal.R.styleable.DatePicker_endYear, DEFAULT_END_YEAR);
+        String minDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_minDate);
+        String maxDate = attributesArray.getString(com.android.internal.R.styleable.DatePicker_maxDate);
+        int layoutResourceId = attributesArray.getResourceId(
+                com.android.internal.R.styleable.DatePicker_legacyLayout, com.android.internal.R.layout.date_picker_legacy);
+        attributesArray.recycle();
+
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final View view = inflater.inflate(layoutResourceId, mDelegator, true);
+        view.setSaveFromParentEnabled(false);
+
+        OnValueChangeListener onChangeListener = new OnValueChangeListener() {
+            public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
+                updateInputState();
+                mTempDate.setTimeInMillis(mCurrentDate.getTimeInMillis());
+                // take care of wrapping of days and months to update greater fields
+                if (picker == mDaySpinner) {
+                    int maxDayOfMonth = mTempDate.getActualMaximum(Calendar.DAY_OF_MONTH);
+                    if (oldVal == maxDayOfMonth && newVal == 1) {
+                        mTempDate.add(Calendar.DAY_OF_MONTH, 1);
+                    } else if (oldVal == 1 && newVal == maxDayOfMonth) {
+                        mTempDate.add(Calendar.DAY_OF_MONTH, -1);
+                    } else {
+                        mTempDate.add(Calendar.DAY_OF_MONTH, newVal - oldVal);
+                    }
+                } else if (picker == mMonthSpinner) {
+                    if (oldVal == 11 && newVal == 0) {
+                        mTempDate.add(Calendar.MONTH, 1);
+                    } else if (oldVal == 0 && newVal == 11) {
+                        mTempDate.add(Calendar.MONTH, -1);
+                    } else {
+                        mTempDate.add(Calendar.MONTH, newVal - oldVal);
+                    }
+                } else if (picker == mYearSpinner) {
+                    mTempDate.set(Calendar.YEAR, newVal);
+                } else {
+                    throw new IllegalArgumentException();
+                }
+                // now set the date to the adjusted one
+                setDate(mTempDate.get(Calendar.YEAR), mTempDate.get(Calendar.MONTH),
+                        mTempDate.get(Calendar.DAY_OF_MONTH));
+                updateSpinners();
+                updateCalendarView();
+                notifyDateChanged();
+            }
+        };
+
+        mSpinners = (LinearLayout) mDelegator.findViewById(com.android.internal.R.id.pickers);
+
+        // calendar view day-picker
+        mCalendarView = (CalendarView) mDelegator.findViewById(com.android.internal.R.id.calendar_view);
+        mCalendarView.setOnDateChangeListener(new CalendarView.OnDateChangeListener() {
+            public void onSelectedDayChange(CalendarView view, int year, int month, int monthDay) {
+                setDate(year, month, monthDay);
+                updateSpinners();
+                notifyDateChanged();
+            }
+        });
+
+        // day
+        mDaySpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.day);
+        mDaySpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
+        mDaySpinner.setOnLongPressUpdateInterval(100);
+        mDaySpinner.setOnValueChangedListener(onChangeListener);
+        mDaySpinnerInput = (EditText) mDaySpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+        // month
+        mMonthSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.month);
+        mMonthSpinner.setMinValue(0);
+        mMonthSpinner.setMaxValue(mNumberOfMonths - 1);
+        mMonthSpinner.setDisplayedValues(mShortMonths);
+        mMonthSpinner.setOnLongPressUpdateInterval(200);
+        mMonthSpinner.setOnValueChangedListener(onChangeListener);
+        mMonthSpinnerInput = (EditText) mMonthSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+        // year
+        mYearSpinner = (NumberPicker) mDelegator.findViewById(com.android.internal.R.id.year);
+        mYearSpinner.setOnLongPressUpdateInterval(100);
+        mYearSpinner.setOnValueChangedListener(onChangeListener);
+        mYearSpinnerInput = (EditText) mYearSpinner.findViewById(com.android.internal.R.id.numberpicker_input);
+
+        // show only what the user required but make sure we
+        // show something and the spinners have higher priority
+        if (!spinnersShown && !calendarViewShown) {
+            setSpinnersShown(true);
+        } else {
+            setSpinnersShown(spinnersShown);
+            setCalendarViewShown(calendarViewShown);
+        }
+
+        // set the min date giving priority of the minDate over startYear
+        mTempDate.clear();
+        if (!TextUtils.isEmpty(minDate)) {
+            if (!parseDate(minDate, mTempDate)) {
+                mTempDate.set(startYear, 0, 1);
+            }
+        } else {
+            mTempDate.set(startYear, 0, 1);
+        }
+        setMinDate(mTempDate.getTimeInMillis());
+
+        // set the max date giving priority of the maxDate over endYear
+        mTempDate.clear();
+        if (!TextUtils.isEmpty(maxDate)) {
+            if (!parseDate(maxDate, mTempDate)) {
+                mTempDate.set(endYear, 11, 31);
+            }
+        } else {
+            mTempDate.set(endYear, 11, 31);
+        }
+        setMaxDate(mTempDate.getTimeInMillis());
+
+        // initialize to current date
+        mCurrentDate.setTimeInMillis(System.currentTimeMillis());
+        init(mCurrentDate.get(Calendar.YEAR), mCurrentDate.get(Calendar.MONTH), mCurrentDate
+                .get(Calendar.DAY_OF_MONTH), null);
+
+        // re-order the number spinners to match the current date format
+        reorderSpinners();
+
+        // accessibility
+        setContentDescriptions();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (mDelegator.getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            mDelegator.setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    @Override
+    public void init(int year, int monthOfYear, int dayOfMonth,
+                     DatePicker.OnDateChangedListener onDateChangedListener) {
+        setDate(year, monthOfYear, dayOfMonth);
+        updateSpinners();
+        updateCalendarView();
+
+        mOnDateChangedListener = onDateChangedListener;
+    }
+
+    @Override
+    public void updateDate(int year, int month, int dayOfMonth) {
+        if (!isNewDate(year, month, dayOfMonth)) {
+            return;
+        }
+        setDate(year, month, dayOfMonth);
+        updateSpinners();
+        updateCalendarView();
+        notifyDateChanged();
+    }
+
+    @Override
+    public int getYear() {
+        return mCurrentDate.get(Calendar.YEAR);
+    }
+
+    @Override
+    public int getMonth() {
+        return mCurrentDate.get(Calendar.MONTH);
+    }
+
+    @Override
+    public int getDayOfMonth() {
+        return mCurrentDate.get(Calendar.DAY_OF_MONTH);
+    }
+
+    @Override
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        mCalendarView.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    @Override
+    public int getFirstDayOfWeek() {
+        return mCalendarView.getFirstDayOfWeek();
+    }
+
+    @Override
+    public void setMinDate(long minDate) {
+        mTempDate.setTimeInMillis(minDate);
+        if (mTempDate.get(Calendar.YEAR) == mMinDate.get(Calendar.YEAR)
+                && mTempDate.get(Calendar.DAY_OF_YEAR) == mMinDate.get(Calendar.DAY_OF_YEAR)) {
+            // Same day, no-op.
+            return;
+        }
+        mMinDate.setTimeInMillis(minDate);
+        mCalendarView.setMinDate(minDate);
+        if (mCurrentDate.before(mMinDate)) {
+            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
+            updateCalendarView();
+        }
+        updateSpinners();
+    }
+
+    @Override
+    public Calendar getMinDate() {
+        final Calendar minDate = Calendar.getInstance();
+        minDate.setTimeInMillis(mCalendarView.getMinDate());
+        return minDate;
+    }
+
+    @Override
+    public void setMaxDate(long maxDate) {
+        mTempDate.setTimeInMillis(maxDate);
+        if (mTempDate.get(Calendar.YEAR) == mMaxDate.get(Calendar.YEAR)
+                && mTempDate.get(Calendar.DAY_OF_YEAR) == mMaxDate.get(Calendar.DAY_OF_YEAR)) {
+            // Same day, no-op.
+            return;
+        }
+        mMaxDate.setTimeInMillis(maxDate);
+        mCalendarView.setMaxDate(maxDate);
+        if (mCurrentDate.after(mMaxDate)) {
+            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
+            updateCalendarView();
+        }
+        updateSpinners();
+    }
+
+    @Override
+    public Calendar getMaxDate() {
+        final Calendar maxDate = Calendar.getInstance();
+        maxDate.setTimeInMillis(mCalendarView.getMaxDate());
+        return maxDate;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mDaySpinner.setEnabled(enabled);
+        mMonthSpinner.setEnabled(enabled);
+        mYearSpinner.setEnabled(enabled);
+        mCalendarView.setEnabled(enabled);
+        mIsEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    @Override
+    public CalendarView getCalendarView() {
+        return mCalendarView;
+    }
+
+    @Override
+    public void setCalendarViewShown(boolean shown) {
+        mCalendarView.setVisibility(shown ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public boolean getCalendarViewShown() {
+        return (mCalendarView.getVisibility() == View.VISIBLE);
+    }
+
+    @Override
+    public void setSpinnersShown(boolean shown) {
+        mSpinners.setVisibility(shown ? View.VISIBLE : View.GONE);
+    }
+
+    @Override
+    public boolean getSpinnersShown() {
+        return mSpinners.isShown();
+    }
+
+    @Override
+    public void onConfigurationChanged(Configuration newConfig) {
+        setCurrentLocale(newConfig.locale);
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        return new SavedState(superState, getYear(), getMonth(), getDayOfMonth(),
+                getMinDate().getTimeInMillis(), getMaxDate().getTimeInMillis());
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            final SavedState ss = (SavedState) state;
+            setDate(ss.getSelectedYear(), ss.getSelectedMonth(), ss.getSelectedDay());
+            updateSpinners();
+            updateCalendarView();
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    /**
+     * Sets the current locale.
+     *
+     * @param locale The current locale.
+     */
+    @Override
+    protected void setCurrentLocale(Locale locale) {
+        super.setCurrentLocale(locale);
+
+        mTempDate = getCalendarForLocale(mTempDate, locale);
+        mMinDate = getCalendarForLocale(mMinDate, locale);
+        mMaxDate = getCalendarForLocale(mMaxDate, locale);
+        mCurrentDate = getCalendarForLocale(mCurrentDate, locale);
+
+        mNumberOfMonths = mTempDate.getActualMaximum(Calendar.MONTH) + 1;
+        mShortMonths = new DateFormatSymbols().getShortMonths();
+
+        if (usingNumericMonths()) {
+            // We're in a locale where a date should either be all-numeric, or all-text.
+            // All-text would require custom NumberPicker formatters for day and year.
+            mShortMonths = new String[mNumberOfMonths];
+            for (int i = 0; i < mNumberOfMonths; ++i) {
+                mShortMonths[i] = String.format("%d", i + 1);
+            }
+        }
+    }
+
+    /**
+     * Tests whether the current locale is one where there are no real month names,
+     * such as Chinese, Japanese, or Korean locales.
+     */
+    private boolean usingNumericMonths() {
+        return Character.isDigit(mShortMonths[Calendar.JANUARY].charAt(0));
+    }
+
+    /**
+     * Gets a calendar for locale bootstrapped with the value of a given calendar.
+     *
+     * @param oldCalendar The old calendar.
+     * @param locale The locale.
+     */
+    private Calendar getCalendarForLocale(Calendar oldCalendar, Locale locale) {
+        if (oldCalendar == null) {
+            return Calendar.getInstance(locale);
+        } else {
+            final long currentTimeMillis = oldCalendar.getTimeInMillis();
+            Calendar newCalendar = Calendar.getInstance(locale);
+            newCalendar.setTimeInMillis(currentTimeMillis);
+            return newCalendar;
+        }
+    }
+
+    /**
+     * Reorders the spinners according to the date format that is
+     * explicitly set by the user and if no such is set fall back
+     * to the current locale's default format.
+     */
+    private void reorderSpinners() {
+        mSpinners.removeAllViews();
+        // We use numeric spinners for year and day, but textual months. Ask icu4c what
+        // order the user's locale uses for that combination. http://b/7207103.
+        String pattern = DateFormat.getBestDateTimePattern(Locale.getDefault(), "yyyyMMMdd");
+        char[] order = ICU.getDateFormatOrder(pattern);
+        final int spinnerCount = order.length;
+        for (int i = 0; i < spinnerCount; i++) {
+            switch (order[i]) {
+                case 'd':
+                    mSpinners.addView(mDaySpinner);
+                    setImeOptions(mDaySpinner, spinnerCount, i);
+                    break;
+                case 'M':
+                    mSpinners.addView(mMonthSpinner);
+                    setImeOptions(mMonthSpinner, spinnerCount, i);
+                    break;
+                case 'y':
+                    mSpinners.addView(mYearSpinner);
+                    setImeOptions(mYearSpinner, spinnerCount, i);
+                    break;
+                default:
+                    throw new IllegalArgumentException(Arrays.toString(order));
+            }
+        }
+    }
+
+    /**
+     * Parses the given <code>date</code> and in case of success sets the result
+     * to the <code>outDate</code>.
+     *
+     * @return True if the date was parsed.
+     */
+    private boolean parseDate(String date, Calendar outDate) {
+        try {
+            outDate.setTime(mDateFormat.parse(date));
+            return true;
+        } catch (ParseException e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+
+    private boolean isNewDate(int year, int month, int dayOfMonth) {
+        return (mCurrentDate.get(Calendar.YEAR) != year
+                || mCurrentDate.get(Calendar.MONTH) != month
+                || mCurrentDate.get(Calendar.DAY_OF_MONTH) != dayOfMonth);
+    }
+
+    private void setDate(int year, int month, int dayOfMonth) {
+        mCurrentDate.set(year, month, dayOfMonth);
+        resetAutofilledValue();
+        if (mCurrentDate.before(mMinDate)) {
+            mCurrentDate.setTimeInMillis(mMinDate.getTimeInMillis());
+        } else if (mCurrentDate.after(mMaxDate)) {
+            mCurrentDate.setTimeInMillis(mMaxDate.getTimeInMillis());
+        }
+    }
+
+    private void updateSpinners() {
+        // set the spinner ranges respecting the min and max dates
+        if (mCurrentDate.equals(mMinDate)) {
+            mDaySpinner.setMinValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+            mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+            mDaySpinner.setWrapSelectorWheel(false);
+            mMonthSpinner.setDisplayedValues(null);
+            mMonthSpinner.setMinValue(mCurrentDate.get(Calendar.MONTH));
+            mMonthSpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.MONTH));
+            mMonthSpinner.setWrapSelectorWheel(false);
+        } else if (mCurrentDate.equals(mMaxDate)) {
+            mDaySpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.DAY_OF_MONTH));
+            mDaySpinner.setMaxValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+            mDaySpinner.setWrapSelectorWheel(false);
+            mMonthSpinner.setDisplayedValues(null);
+            mMonthSpinner.setMinValue(mCurrentDate.getActualMinimum(Calendar.MONTH));
+            mMonthSpinner.setMaxValue(mCurrentDate.get(Calendar.MONTH));
+            mMonthSpinner.setWrapSelectorWheel(false);
+        } else {
+            mDaySpinner.setMinValue(1);
+            mDaySpinner.setMaxValue(mCurrentDate.getActualMaximum(Calendar.DAY_OF_MONTH));
+            mDaySpinner.setWrapSelectorWheel(true);
+            mMonthSpinner.setDisplayedValues(null);
+            mMonthSpinner.setMinValue(0);
+            mMonthSpinner.setMaxValue(11);
+            mMonthSpinner.setWrapSelectorWheel(true);
+        }
+
+        // make sure the month names are a zero based array
+        // with the months in the month spinner
+        String[] displayedValues = Arrays.copyOfRange(mShortMonths,
+                mMonthSpinner.getMinValue(), mMonthSpinner.getMaxValue() + 1);
+        mMonthSpinner.setDisplayedValues(displayedValues);
+
+        // year spinner range does not change based on the current date
+        mYearSpinner.setMinValue(mMinDate.get(Calendar.YEAR));
+        mYearSpinner.setMaxValue(mMaxDate.get(Calendar.YEAR));
+        mYearSpinner.setWrapSelectorWheel(false);
+
+        // set the spinner values
+        mYearSpinner.setValue(mCurrentDate.get(Calendar.YEAR));
+        mMonthSpinner.setValue(mCurrentDate.get(Calendar.MONTH));
+        mDaySpinner.setValue(mCurrentDate.get(Calendar.DAY_OF_MONTH));
+
+        if (usingNumericMonths()) {
+            mMonthSpinnerInput.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+        }
+    }
+
+    /**
+     * Updates the calendar view with the current date.
+     */
+    private void updateCalendarView() {
+        mCalendarView.setDate(mCurrentDate.getTimeInMillis(), false, false);
+    }
+
+
+    /**
+     * Notifies the listener, if such, for a change in the selected date.
+     */
+    private void notifyDateChanged() {
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        if (mOnDateChangedListener != null) {
+            mOnDateChangedListener.onDateChanged(mDelegator, getYear(), getMonth(),
+                    getDayOfMonth());
+        }
+        if (mAutoFillChangeListener != null) {
+            mAutoFillChangeListener.onDateChanged(mDelegator, getYear(), getMonth(),
+                    getDayOfMonth());
+        }
+    }
+
+    /**
+     * Sets the IME options for a spinner based on its ordering.
+     *
+     * @param spinner The spinner.
+     * @param spinnerCount The total spinner count.
+     * @param spinnerIndex The index of the given spinner.
+     */
+    private void setImeOptions(NumberPicker spinner, int spinnerCount, int spinnerIndex) {
+        final int imeOptions;
+        if (spinnerIndex < spinnerCount - 1) {
+            imeOptions = EditorInfo.IME_ACTION_NEXT;
+        } else {
+            imeOptions = EditorInfo.IME_ACTION_DONE;
+        }
+        TextView input = (TextView) spinner.findViewById(com.android.internal.R.id.numberpicker_input);
+        input.setImeOptions(imeOptions);
+    }
+
+    private void setContentDescriptions() {
+        // Day
+        trySetContentDescription(mDaySpinner, com.android.internal.R.id.increment,
+                com.android.internal.R.string.date_picker_increment_day_button);
+        trySetContentDescription(mDaySpinner, com.android.internal.R.id.decrement,
+                com.android.internal.R.string.date_picker_decrement_day_button);
+        // Month
+        trySetContentDescription(mMonthSpinner, com.android.internal.R.id.increment,
+                com.android.internal.R.string.date_picker_increment_month_button);
+        trySetContentDescription(mMonthSpinner, com.android.internal.R.id.decrement,
+                com.android.internal.R.string.date_picker_decrement_month_button);
+        // Year
+        trySetContentDescription(mYearSpinner, com.android.internal.R.id.increment,
+                com.android.internal.R.string.date_picker_increment_year_button);
+        trySetContentDescription(mYearSpinner, com.android.internal.R.id.decrement,
+                com.android.internal.R.string.date_picker_decrement_year_button);
+    }
+
+    private void trySetContentDescription(View root, int viewId, int contDescResId) {
+        View target = root.findViewById(viewId);
+        if (target != null) {
+            target.setContentDescription(mContext.getString(contDescResId));
+        }
+    }
+
+    private void updateInputState() {
+        // Make sure that if the user changes the value and the IME is active
+        // for one of the inputs if this widget, the IME is closed. If the user
+        // changed the value via the IME and there is a next input the IME will
+        // be shown, otherwise the user chose another means of changing the
+        // value and having the IME up makes no sense.
+        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+        if (inputMethodManager != null) {
+            if (inputMethodManager.isActive(mYearSpinnerInput)) {
+                mYearSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mMonthSpinnerInput)) {
+                mMonthSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mDaySpinnerInput)) {
+                mDaySpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            }
+        }
+    }
+}
diff --git a/android/widget/DateTimeView.java b/android/widget/DateTimeView.java
new file mode 100644
index 0000000..4db3607
--- /dev/null
+++ b/android/widget/DateTimeView.java
@@ -0,0 +1,504 @@
+/*
+ * 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 android.widget;
+
+import static android.text.format.DateUtils.DAY_IN_MILLIS;
+import static android.text.format.DateUtils.HOUR_IN_MILLIS;
+import static android.text.format.DateUtils.MINUTE_IN_MILLIS;
+import static android.text.format.DateUtils.YEAR_IN_MILLIS;
+import static android.text.format.Time.getJulianDay;
+
+import android.app.ActivityThread;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.icu.util.Calendar;
+import android.os.Handler;
+import android.text.format.Time;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.text.DateFormat;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.TimeZone;
+
+//
+// TODO
+// - listen for the next threshold time to update the view.
+// - listen for date format pref changed
+// - put the AM/PM in a smaller font
+//
+
+/**
+ * Displays a given time in a convenient human-readable foramt.
+ *
+ * @hide
+ */
+@RemoteView
+public class DateTimeView extends TextView {
+    private static final int SHOW_TIME = 0;
+    private static final int SHOW_MONTH_DAY_YEAR = 1;
+
+    Date mTime;
+    long mTimeMillis;
+
+    int mLastDisplay = -1;
+    DateFormat mLastFormat;
+
+    private long mUpdateTimeMillis;
+    private static final ThreadLocal<ReceiverInfo> sReceiverInfo = new ThreadLocal<ReceiverInfo>();
+    private String mNowText;
+    private boolean mShowRelativeTime;
+
+    public DateTimeView(Context context) {
+        this(context, null);
+    }
+
+    public DateTimeView(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.DateTimeView, 0,
+                0);
+
+        final int N = a.getIndexCount();
+        for (int i = 0; i < N; i++) {
+            int attr = a.getIndex(i);
+            switch (attr) {
+                case R.styleable.DateTimeView_showRelative:
+                    boolean relative = a.getBoolean(i, false);
+                    setShowRelativeTime(relative);
+                    break;
+            }
+        }
+        a.recycle();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        ReceiverInfo ri = sReceiverInfo.get();
+        if (ri == null) {
+            ri = new ReceiverInfo();
+            sReceiverInfo.set(ri);
+        }
+        ri.addView(this);
+    }
+        
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        final ReceiverInfo ri = sReceiverInfo.get();
+        if (ri != null) {
+            ri.removeView(this);
+        }
+    }
+
+    @android.view.RemotableViewMethod
+    public void setTime(long time) {
+        Time t = new Time();
+        t.set(time);
+        mTimeMillis = t.toMillis(false);
+        mTime = new Date(t.year-1900, t.month, t.monthDay, t.hour, t.minute, 0);
+        update();
+    }
+
+    @android.view.RemotableViewMethod
+    public void setShowRelativeTime(boolean showRelativeTime) {
+        mShowRelativeTime = showRelativeTime;
+        updateNowText();
+        update();
+    }
+
+    @Override
+    @android.view.RemotableViewMethod
+    public void setVisibility(@Visibility int visibility) {
+        boolean gotVisible = visibility != GONE && getVisibility() == GONE;
+        super.setVisibility(visibility);
+        if (gotVisible) {
+            update();
+        }
+    }
+
+    void update() {
+        if (mTime == null || getVisibility() == GONE) {
+            return;
+        }
+        if (mShowRelativeTime) {
+            updateRelativeTime();
+            return;
+        }
+
+        int display;
+        Date time = mTime;
+
+        Time t = new Time();
+        t.set(mTimeMillis);
+        t.second = 0;
+
+        t.hour -= 12;
+        long twelveHoursBefore = t.toMillis(false);
+        t.hour += 12;
+        long twelveHoursAfter = t.toMillis(false);
+        t.hour = 0;
+        t.minute = 0;
+        long midnightBefore = t.toMillis(false);
+        t.monthDay++;
+        long midnightAfter = t.toMillis(false);
+
+        long nowMillis = System.currentTimeMillis();
+        t.set(nowMillis);
+        t.second = 0;
+        nowMillis = t.normalize(false);
+
+        // Choose the display mode
+        choose_display: {
+            if ((nowMillis >= midnightBefore && nowMillis < midnightAfter)
+                    || (nowMillis >= twelveHoursBefore && nowMillis < twelveHoursAfter)) {
+                display = SHOW_TIME;
+                break choose_display;
+            }
+            // Else, show month day and year.
+            display = SHOW_MONTH_DAY_YEAR;
+            break choose_display;
+        }
+
+        // Choose the format
+        DateFormat format;
+        if (display == mLastDisplay && mLastFormat != null) {
+            // use cached format
+            format = mLastFormat;
+        } else {
+            switch (display) {
+                case SHOW_TIME:
+                    format = getTimeFormat();
+                    break;
+                case SHOW_MONTH_DAY_YEAR:
+                    format = DateFormat.getDateInstance(DateFormat.SHORT);
+                    break;
+                default:
+                    throw new RuntimeException("unknown display value: " + display);
+            }
+            mLastFormat = format;
+        }
+
+        // Set the text
+        String text = format.format(mTime);
+        setText(text);
+
+        // Schedule the next update
+        if (display == SHOW_TIME) {
+            // Currently showing the time, update at the later of twelve hours after or midnight.
+            mUpdateTimeMillis = twelveHoursAfter > midnightAfter ? twelveHoursAfter : midnightAfter;
+        } else {
+            // Currently showing the date
+            if (mTimeMillis < nowMillis) {
+                // If the time is in the past, don't schedule an update
+                mUpdateTimeMillis = 0;
+            } else {
+                // If hte time is in the future, schedule one at the earlier of twelve hours
+                // before or midnight before.
+                mUpdateTimeMillis = twelveHoursBefore < midnightBefore
+                        ? twelveHoursBefore : midnightBefore;
+            }
+        }
+    }
+
+    private void updateRelativeTime() {
+        long now = System.currentTimeMillis();
+        long duration = Math.abs(now - mTimeMillis);
+        int count;
+        long millisIncrease;
+        boolean past = (now >= mTimeMillis);
+        String result;
+        if (duration < MINUTE_IN_MILLIS) {
+            setText(mNowText);
+            mUpdateTimeMillis = mTimeMillis + MINUTE_IN_MILLIS + 1;
+            return;
+        } else if (duration < HOUR_IN_MILLIS) {
+            count = (int)(duration / MINUTE_IN_MILLIS);
+            result = String.format(getContext().getResources().getQuantityString(past
+                            ? com.android.internal.R.plurals.duration_minutes_shortest
+                            : com.android.internal.R.plurals.duration_minutes_shortest_future,
+                            count),
+                    count);
+            millisIncrease = MINUTE_IN_MILLIS;
+        } else if (duration < DAY_IN_MILLIS) {
+            count = (int)(duration / HOUR_IN_MILLIS);
+            result = String.format(getContext().getResources().getQuantityString(past
+                            ? com.android.internal.R.plurals.duration_hours_shortest
+                            : com.android.internal.R.plurals.duration_hours_shortest_future,
+                            count),
+                    count);
+            millisIncrease = HOUR_IN_MILLIS;
+        } else if (duration < YEAR_IN_MILLIS) {
+            // In weird cases it can become 0 because of daylight savings
+            TimeZone timeZone = TimeZone.getDefault();
+            count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
+            result = String.format(getContext().getResources().getQuantityString(past
+                            ? com.android.internal.R.plurals.duration_days_shortest
+                            : com.android.internal.R.plurals.duration_days_shortest_future,
+                            count),
+                    count);
+            if (past || count != 1) {
+                mUpdateTimeMillis = computeNextMidnight(timeZone);
+                millisIncrease = -1;
+            } else {
+                millisIncrease = DAY_IN_MILLIS;
+            }
+
+        } else {
+            count = (int)(duration / YEAR_IN_MILLIS);
+            result = String.format(getContext().getResources().getQuantityString(past
+                            ? com.android.internal.R.plurals.duration_years_shortest
+                            : com.android.internal.R.plurals.duration_years_shortest_future,
+                            count),
+                    count);
+            millisIncrease = YEAR_IN_MILLIS;
+        }
+        if (millisIncrease != -1) {
+            if (past) {
+                mUpdateTimeMillis = mTimeMillis + millisIncrease * (count + 1) + 1;
+            } else {
+                mUpdateTimeMillis = mTimeMillis - millisIncrease * count + 1;
+            }
+        }
+        setText(result);
+    }
+
+    /**
+     * @param timeZone the timezone we are in
+     * @return the timepoint in millis at UTC at midnight in the current timezone
+     */
+    private long computeNextMidnight(TimeZone timeZone) {
+        Calendar c = Calendar.getInstance();
+        c.setTimeZone(libcore.icu.DateUtilsBridge.icuTimeZone(timeZone));
+        c.add(Calendar.DAY_OF_MONTH, 1);
+        c.set(Calendar.HOUR_OF_DAY, 0);
+        c.set(Calendar.MINUTE, 0);
+        c.set(Calendar.SECOND, 0);
+        c.set(Calendar.MILLISECOND, 0);
+        return c.getTimeInMillis();
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        updateNowText();
+        update();
+    }
+
+    private void updateNowText() {
+        if (!mShowRelativeTime) {
+            return;
+        }
+        mNowText = getContext().getResources().getString(
+                com.android.internal.R.string.now_string_shortest);
+    }
+
+    // Return the date difference for the two times in a given timezone.
+    private static int dayDistance(TimeZone timeZone, long startTime,
+            long endTime) {
+        return getJulianDay(endTime, timeZone.getOffset(endTime) / 1000)
+                - getJulianDay(startTime, timeZone.getOffset(startTime) / 1000);
+    }
+
+    private DateFormat getTimeFormat() {
+        return android.text.format.DateFormat.getTimeFormat(getContext());
+    }
+
+    void clearFormatAndUpdate() {
+        mLastFormat = null;
+        update();
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (mShowRelativeTime) {
+            // The short version of the time might not be completely understandable and for
+            // accessibility we rather have a longer version.
+            long now = System.currentTimeMillis();
+            long duration = Math.abs(now - mTimeMillis);
+            int count;
+            boolean past = (now >= mTimeMillis);
+            String result;
+            if (duration < MINUTE_IN_MILLIS) {
+                result = mNowText;
+            } else if (duration < HOUR_IN_MILLIS) {
+                count = (int)(duration / MINUTE_IN_MILLIS);
+                result = String.format(getContext().getResources().getQuantityString(past
+                                ? com.android.internal.
+                                        R.plurals.duration_minutes_relative
+                                : com.android.internal.
+                                        R.plurals.duration_minutes_relative_future,
+                        count),
+                        count);
+            } else if (duration < DAY_IN_MILLIS) {
+                count = (int)(duration / HOUR_IN_MILLIS);
+                result = String.format(getContext().getResources().getQuantityString(past
+                                ? com.android.internal.
+                                        R.plurals.duration_hours_relative
+                                : com.android.internal.
+                                        R.plurals.duration_hours_relative_future,
+                        count),
+                        count);
+            } else if (duration < YEAR_IN_MILLIS) {
+                // In weird cases it can become 0 because of daylight savings
+                TimeZone timeZone = TimeZone.getDefault();
+                count = Math.max(Math.abs(dayDistance(timeZone, mTimeMillis, now)), 1);
+                result = String.format(getContext().getResources().getQuantityString(past
+                                ? com.android.internal.
+                                        R.plurals.duration_days_relative
+                                : com.android.internal.
+                                        R.plurals.duration_days_relative_future,
+                        count),
+                        count);
+
+            } else {
+                count = (int)(duration / YEAR_IN_MILLIS);
+                result = String.format(getContext().getResources().getQuantityString(past
+                                ? com.android.internal.
+                                        R.plurals.duration_years_relative
+                                : com.android.internal.
+                                        R.plurals.duration_years_relative_future,
+                        count),
+                        count);
+            }
+            info.setText(result);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static void setReceiverHandler(Handler handler) {
+        ReceiverInfo ri = sReceiverInfo.get();
+        if (ri == null) {
+            ri = new ReceiverInfo();
+            sReceiverInfo.set(ri);
+        }
+        ri.setHandler(handler);
+    }
+
+    private static class ReceiverInfo {
+        private final ArrayList<DateTimeView> mAttachedViews = new ArrayList<DateTimeView>();
+        private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+            @Override
+            public void onReceive(Context context, Intent intent) {
+                String action = intent.getAction();
+                if (Intent.ACTION_TIME_TICK.equals(action)) {
+                    if (System.currentTimeMillis() < getSoonestUpdateTime()) {
+                        // The update() function takes a few milliseconds to run because of
+                        // all of the time conversions it needs to do, so we can't do that
+                        // every minute.
+                        return;
+                    }
+                }
+                // ACTION_TIME_CHANGED can also signal a change of 12/24 hr. format.
+                updateAll();
+            }
+        };
+
+        private final ContentObserver mObserver = new ContentObserver(new Handler()) {
+            @Override
+            public void onChange(boolean selfChange) {
+                updateAll();
+            }
+        };
+
+        private Handler mHandler = new Handler();
+
+        public void addView(DateTimeView v) {
+            synchronized (mAttachedViews) {
+                final boolean register = mAttachedViews.isEmpty();
+                mAttachedViews.add(v);
+                if (register) {
+                    register(getApplicationContextIfAvailable(v.getContext()));
+                }
+            }
+        }
+
+        public void removeView(DateTimeView v) {
+            synchronized (mAttachedViews) {
+                mAttachedViews.remove(v);
+                if (mAttachedViews.isEmpty()) {
+                    unregister(getApplicationContextIfAvailable(v.getContext()));
+                }
+            }
+        }
+
+        void updateAll() {
+            synchronized (mAttachedViews) {
+                final int count = mAttachedViews.size();
+                for (int i = 0; i < count; i++) {
+                    DateTimeView view = mAttachedViews.get(i);
+                    view.post(() -> view.clearFormatAndUpdate());
+                }
+            }
+        }
+
+        long getSoonestUpdateTime() {
+            long result = Long.MAX_VALUE;
+            synchronized (mAttachedViews) {
+                final int count = mAttachedViews.size();
+                for (int i = 0; i < count; i++) {
+                    final long time = mAttachedViews.get(i).mUpdateTimeMillis;
+                    if (time < result) {
+                        result = time;
+                    }
+                }
+            }
+            return result;
+        }
+
+        static final Context getApplicationContextIfAvailable(Context context) {
+            final Context ac = context.getApplicationContext();
+            return ac != null ? ac : ActivityThread.currentApplication().getApplicationContext();
+        }
+
+        void register(Context context) {
+            final IntentFilter filter = new IntentFilter();
+            filter.addAction(Intent.ACTION_TIME_TICK);
+            filter.addAction(Intent.ACTION_TIME_CHANGED);
+            filter.addAction(Intent.ACTION_CONFIGURATION_CHANGED);
+            filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+            context.registerReceiver(mReceiver, filter, null, mHandler);
+        }
+
+        void unregister(Context context) {
+            context.unregisterReceiver(mReceiver);
+        }
+
+        public void setHandler(Handler handler) {
+            mHandler = handler;
+            synchronized (mAttachedViews) {
+                if (!mAttachedViews.isEmpty()) {
+                    unregister(mAttachedViews.get(0).getContext());
+                    register(mAttachedViews.get(0).getContext());
+                }
+            }
+        }
+    }
+}
diff --git a/android/widget/DayPickerPagerAdapter.java b/android/widget/DayPickerPagerAdapter.java
new file mode 100644
index 0000000..63621e1
--- /dev/null
+++ b/android/widget/DayPickerPagerAdapter.java
@@ -0,0 +1,342 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.SparseArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.SimpleMonthView.OnDayClickListener;
+
+import com.android.internal.widget.PagerAdapter;
+
+/**
+ * An adapter for a list of {@link android.widget.SimpleMonthView} items.
+ */
+class DayPickerPagerAdapter extends PagerAdapter {
+    private static final int MONTHS_IN_YEAR = 12;
+
+    private final Calendar mMinDate = Calendar.getInstance();
+    private final Calendar mMaxDate = Calendar.getInstance();
+
+    private final SparseArray<ViewHolder> mItems = new SparseArray<>();
+
+    private final LayoutInflater mInflater;
+    private final int mLayoutResId;
+    private final int mCalendarViewId;
+
+    private Calendar mSelectedDay = null;
+
+    private int mMonthTextAppearance;
+    private int mDayOfWeekTextAppearance;
+    private int mDayTextAppearance;
+
+    private ColorStateList mCalendarTextColor;
+    private ColorStateList mDaySelectorColor;
+    private ColorStateList mDayHighlightColor;
+
+    private OnDaySelectedListener mOnDaySelectedListener;
+
+    private int mCount;
+    private int mFirstDayOfWeek;
+
+    public DayPickerPagerAdapter(@NonNull Context context, @LayoutRes int layoutResId,
+            @IdRes int calendarViewId) {
+        mInflater = LayoutInflater.from(context);
+        mLayoutResId = layoutResId;
+        mCalendarViewId = calendarViewId;
+
+        final TypedArray ta = context.obtainStyledAttributes(new int[] {
+                com.android.internal.R.attr.colorControlHighlight});
+        mDayHighlightColor = ta.getColorStateList(0);
+        ta.recycle();
+    }
+
+    public void setRange(@NonNull Calendar min, @NonNull Calendar max) {
+        mMinDate.setTimeInMillis(min.getTimeInMillis());
+        mMaxDate.setTimeInMillis(max.getTimeInMillis());
+
+        final int diffYear = mMaxDate.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR);
+        final int diffMonth = mMaxDate.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH);
+        mCount = diffMonth + MONTHS_IN_YEAR * diffYear + 1;
+
+        // Positions are now invalid, clear everything and start over.
+        notifyDataSetChanged();
+    }
+
+    /**
+     * Sets the first day of the week.
+     *
+     * @param weekStart which day the week should start on, valid values are
+     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+     */
+    public void setFirstDayOfWeek(int weekStart) {
+        mFirstDayOfWeek = weekStart;
+
+        // Update displayed views.
+        final int count = mItems.size();
+        for (int i = 0; i < count; i++) {
+            final SimpleMonthView monthView = mItems.valueAt(i).calendar;
+            monthView.setFirstDayOfWeek(weekStart);
+        }
+    }
+
+    public int getFirstDayOfWeek() {
+        return mFirstDayOfWeek;
+    }
+
+    public boolean getBoundsForDate(Calendar day, Rect outBounds) {
+        final int position = getPositionForDay(day);
+        final ViewHolder monthView = mItems.get(position, null);
+        if (monthView == null) {
+            return false;
+        } else {
+            final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+            return monthView.calendar.getBoundsForDay(dayOfMonth, outBounds);
+        }
+    }
+
+    /**
+     * Sets the selected day.
+     *
+     * @param day the selected day
+     */
+    public void setSelectedDay(@Nullable Calendar day) {
+        final int oldPosition = getPositionForDay(mSelectedDay);
+        final int newPosition = getPositionForDay(day);
+
+        // Clear the old position if necessary.
+        if (oldPosition != newPosition && oldPosition >= 0) {
+            final ViewHolder oldMonthView = mItems.get(oldPosition, null);
+            if (oldMonthView != null) {
+                oldMonthView.calendar.setSelectedDay(-1);
+            }
+        }
+
+        // Set the new position.
+        if (newPosition >= 0) {
+            final ViewHolder newMonthView = mItems.get(newPosition, null);
+            if (newMonthView != null) {
+                final int dayOfMonth = day.get(Calendar.DAY_OF_MONTH);
+                newMonthView.calendar.setSelectedDay(dayOfMonth);
+            }
+        }
+
+        mSelectedDay = day;
+    }
+
+    /**
+     * Sets the listener to call when the user selects a day.
+     *
+     * @param listener The listener to call.
+     */
+    public void setOnDaySelectedListener(OnDaySelectedListener listener) {
+        mOnDaySelectedListener = listener;
+    }
+
+    void setCalendarTextColor(ColorStateList calendarTextColor) {
+        mCalendarTextColor = calendarTextColor;
+        notifyDataSetChanged();
+    }
+
+    void setDaySelectorColor(ColorStateList selectorColor) {
+        mDaySelectorColor = selectorColor;
+        notifyDataSetChanged();
+    }
+
+    void setMonthTextAppearance(int resId) {
+        mMonthTextAppearance = resId;
+        notifyDataSetChanged();
+    }
+
+    void setDayOfWeekTextAppearance(int resId) {
+        mDayOfWeekTextAppearance = resId;
+        notifyDataSetChanged();
+    }
+
+    int getDayOfWeekTextAppearance() {
+        return mDayOfWeekTextAppearance;
+    }
+
+    void setDayTextAppearance(int resId) {
+        mDayTextAppearance = resId;
+        notifyDataSetChanged();
+    }
+
+    int getDayTextAppearance() {
+        return mDayTextAppearance;
+    }
+
+    @Override
+    public int getCount() {
+        return mCount;
+    }
+
+    @Override
+    public boolean isViewFromObject(View view, Object object) {
+        final ViewHolder holder = (ViewHolder) object;
+        return view == holder.container;
+    }
+
+    private int getMonthForPosition(int position) {
+        return (position + mMinDate.get(Calendar.MONTH)) % MONTHS_IN_YEAR;
+    }
+
+    private int getYearForPosition(int position) {
+        final int yearOffset = (position + mMinDate.get(Calendar.MONTH)) / MONTHS_IN_YEAR;
+        return yearOffset + mMinDate.get(Calendar.YEAR);
+    }
+
+    private int getPositionForDay(@Nullable Calendar day) {
+        if (day == null) {
+            return -1;
+        }
+
+        final int yearOffset = day.get(Calendar.YEAR) - mMinDate.get(Calendar.YEAR);
+        final int monthOffset = day.get(Calendar.MONTH) - mMinDate.get(Calendar.MONTH);
+        final int position = yearOffset * MONTHS_IN_YEAR + monthOffset;
+        return position;
+    }
+
+    @Override
+    public Object instantiateItem(ViewGroup container, int position) {
+        final View itemView = mInflater.inflate(mLayoutResId, container, false);
+
+        final SimpleMonthView v = itemView.findViewById(mCalendarViewId);
+        v.setOnDayClickListener(mOnDayClickListener);
+        v.setMonthTextAppearance(mMonthTextAppearance);
+        v.setDayOfWeekTextAppearance(mDayOfWeekTextAppearance);
+        v.setDayTextAppearance(mDayTextAppearance);
+
+        if (mDaySelectorColor != null) {
+            v.setDaySelectorColor(mDaySelectorColor);
+        }
+
+        if (mDayHighlightColor != null) {
+            v.setDayHighlightColor(mDayHighlightColor);
+        }
+
+        if (mCalendarTextColor != null) {
+            v.setMonthTextColor(mCalendarTextColor);
+            v.setDayOfWeekTextColor(mCalendarTextColor);
+            v.setDayTextColor(mCalendarTextColor);
+        }
+
+        final int month = getMonthForPosition(position);
+        final int year = getYearForPosition(position);
+
+        final int selectedDay;
+        if (mSelectedDay != null && mSelectedDay.get(Calendar.MONTH) == month) {
+            selectedDay = mSelectedDay.get(Calendar.DAY_OF_MONTH);
+        } else {
+            selectedDay = -1;
+        }
+
+        final int enabledDayRangeStart;
+        if (mMinDate.get(Calendar.MONTH) == month && mMinDate.get(Calendar.YEAR) == year) {
+            enabledDayRangeStart = mMinDate.get(Calendar.DAY_OF_MONTH);
+        } else {
+            enabledDayRangeStart = 1;
+        }
+
+        final int enabledDayRangeEnd;
+        if (mMaxDate.get(Calendar.MONTH) == month && mMaxDate.get(Calendar.YEAR) == year) {
+            enabledDayRangeEnd = mMaxDate.get(Calendar.DAY_OF_MONTH);
+        } else {
+            enabledDayRangeEnd = 31;
+        }
+
+        v.setMonthParams(selectedDay, month, year, mFirstDayOfWeek,
+                enabledDayRangeStart, enabledDayRangeEnd);
+
+        final ViewHolder holder = new ViewHolder(position, itemView, v);
+        mItems.put(position, holder);
+
+        container.addView(itemView);
+
+        return holder;
+    }
+
+    @Override
+    public void destroyItem(ViewGroup container, int position, Object object) {
+        final ViewHolder holder = (ViewHolder) object;
+        container.removeView(holder.container);
+
+        mItems.remove(position);
+    }
+
+    @Override
+    public int getItemPosition(Object object) {
+        final ViewHolder holder = (ViewHolder) object;
+        return holder.position;
+    }
+
+    @Override
+    public CharSequence getPageTitle(int position) {
+        final SimpleMonthView v = mItems.get(position).calendar;
+        if (v != null) {
+            return v.getMonthYearLabel();
+        }
+        return null;
+    }
+
+    SimpleMonthView getView(Object object) {
+        if (object == null) {
+            return null;
+        }
+        final ViewHolder holder = (ViewHolder) object;
+        return holder.calendar;
+    }
+
+    private final OnDayClickListener mOnDayClickListener = new OnDayClickListener() {
+        @Override
+        public void onDayClick(SimpleMonthView view, Calendar day) {
+            if (day != null) {
+                setSelectedDay(day);
+
+                if (mOnDaySelectedListener != null) {
+                    mOnDaySelectedListener.onDaySelected(DayPickerPagerAdapter.this, day);
+                }
+            }
+        }
+    };
+
+    private static class ViewHolder {
+        public final int position;
+        public final View container;
+        public final SimpleMonthView calendar;
+
+        public ViewHolder(int position, View container, SimpleMonthView calendar) {
+            this.position = position;
+            this.container = container;
+            this.calendar = calendar;
+        }
+    }
+
+    public interface OnDaySelectedListener {
+        public void onDaySelected(DayPickerPagerAdapter view, Calendar day);
+    }
+}
diff --git a/android/widget/DayPickerView.java b/android/widget/DayPickerView.java
new file mode 100644
index 0000000..f712d5f
--- /dev/null
+++ b/android/widget/DayPickerView.java
@@ -0,0 +1,455 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.internal.R;
+import com.android.internal.widget.ViewPager;
+import com.android.internal.widget.ViewPager.OnPageChangeListener;
+
+import libcore.icu.LocaleData;
+
+import java.util.Locale;
+
+class DayPickerView extends ViewGroup {
+    private static final int DEFAULT_LAYOUT = R.layout.day_picker_content_material;
+    private static final int DEFAULT_START_YEAR = 1900;
+    private static final int DEFAULT_END_YEAR = 2100;
+
+    private static final int[] ATTRS_TEXT_COLOR = new int[] { R.attr.textColor };
+
+    private final Calendar mSelectedDay = Calendar.getInstance();
+    private final Calendar mMinDate = Calendar.getInstance();
+    private final Calendar mMaxDate = Calendar.getInstance();
+
+    private final AccessibilityManager mAccessibilityManager;
+
+    private final ViewPager mViewPager;
+    private final ImageButton mPrevButton;
+    private final ImageButton mNextButton;
+
+    private final DayPickerPagerAdapter mAdapter;
+
+    /** Temporary calendar used for date calculations. */
+    private Calendar mTempCalendar;
+
+    private OnDaySelectedListener mOnDaySelectedListener;
+
+    public DayPickerView(Context context) {
+        this(context, null);
+    }
+
+    public DayPickerView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, R.attr.calendarViewStyle);
+    }
+
+    public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DayPickerView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mAccessibilityManager = (AccessibilityManager) context.getSystemService(
+                Context.ACCESSIBILITY_SERVICE);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                R.styleable.CalendarView, defStyleAttr, defStyleRes);
+
+        final int firstDayOfWeek = a.getInt(R.styleable.CalendarView_firstDayOfWeek,
+                LocaleData.get(Locale.getDefault()).firstDayOfWeek);
+
+        final String minDate = a.getString(R.styleable.CalendarView_minDate);
+        final String maxDate = a.getString(R.styleable.CalendarView_maxDate);
+
+        final int monthTextAppearanceResId = a.getResourceId(
+                R.styleable.CalendarView_monthTextAppearance,
+                R.style.TextAppearance_Material_Widget_Calendar_Month);
+        final int dayOfWeekTextAppearanceResId = a.getResourceId(
+                R.styleable.CalendarView_weekDayTextAppearance,
+                R.style.TextAppearance_Material_Widget_Calendar_DayOfWeek);
+        final int dayTextAppearanceResId = a.getResourceId(
+                R.styleable.CalendarView_dateTextAppearance,
+                R.style.TextAppearance_Material_Widget_Calendar_Day);
+
+        final ColorStateList daySelectorColor = a.getColorStateList(
+                R.styleable.CalendarView_daySelectorColor);
+
+        a.recycle();
+
+        // Set up adapter.
+        mAdapter = new DayPickerPagerAdapter(context,
+                R.layout.date_picker_month_item_material, R.id.month_view);
+        mAdapter.setMonthTextAppearance(monthTextAppearanceResId);
+        mAdapter.setDayOfWeekTextAppearance(dayOfWeekTextAppearanceResId);
+        mAdapter.setDayTextAppearance(dayTextAppearanceResId);
+        mAdapter.setDaySelectorColor(daySelectorColor);
+
+        final LayoutInflater inflater = LayoutInflater.from(context);
+        final ViewGroup content = (ViewGroup) inflater.inflate(DEFAULT_LAYOUT, this, false);
+
+        // Transfer all children from content to here.
+        while (content.getChildCount() > 0) {
+            final View child = content.getChildAt(0);
+            content.removeViewAt(0);
+            addView(child);
+        }
+
+        mPrevButton = findViewById(R.id.prev);
+        mPrevButton.setOnClickListener(mOnClickListener);
+
+        mNextButton = findViewById(R.id.next);
+        mNextButton.setOnClickListener(mOnClickListener);
+
+        mViewPager = findViewById(R.id.day_picker_view_pager);
+        mViewPager.setAdapter(mAdapter);
+        mViewPager.setOnPageChangeListener(mOnPageChangedListener);
+
+        // Proxy the month text color into the previous and next buttons.
+        if (monthTextAppearanceResId != 0) {
+            final TypedArray ta = mContext.obtainStyledAttributes(null,
+                    ATTRS_TEXT_COLOR, 0, monthTextAppearanceResId);
+            final ColorStateList monthColor = ta.getColorStateList(0);
+            if (monthColor != null) {
+                mPrevButton.setImageTintList(monthColor);
+                mNextButton.setImageTintList(monthColor);
+            }
+            ta.recycle();
+        }
+
+        // Set up min and max dates.
+        final Calendar tempDate = Calendar.getInstance();
+        if (!CalendarView.parseDate(minDate, tempDate)) {
+            tempDate.set(DEFAULT_START_YEAR, Calendar.JANUARY, 1);
+        }
+        final long minDateMillis = tempDate.getTimeInMillis();
+
+        if (!CalendarView.parseDate(maxDate, tempDate)) {
+            tempDate.set(DEFAULT_END_YEAR, Calendar.DECEMBER, 31);
+        }
+        final long maxDateMillis = tempDate.getTimeInMillis();
+
+        if (maxDateMillis < minDateMillis) {
+            throw new IllegalArgumentException("maxDate must be >= minDate");
+        }
+
+        final long setDateMillis = MathUtils.constrain(
+                System.currentTimeMillis(), minDateMillis, maxDateMillis);
+
+        setFirstDayOfWeek(firstDayOfWeek);
+        setMinDate(minDateMillis);
+        setMaxDate(maxDateMillis);
+        setDate(setDateMillis, false);
+
+        // Proxy selection callbacks to our own listener.
+        mAdapter.setOnDaySelectedListener(new DayPickerPagerAdapter.OnDaySelectedListener() {
+            @Override
+            public void onDaySelected(DayPickerPagerAdapter adapter, Calendar day) {
+                if (mOnDaySelectedListener != null) {
+                    mOnDaySelectedListener.onDaySelected(DayPickerView.this, day);
+                }
+            }
+        });
+    }
+
+    private void updateButtonVisibility(int position) {
+        final boolean hasPrev = position > 0;
+        final boolean hasNext = position < (mAdapter.getCount() - 1);
+        mPrevButton.setVisibility(hasPrev ? View.VISIBLE : View.INVISIBLE);
+        mNextButton.setVisibility(hasNext ? View.VISIBLE : View.INVISIBLE);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final ViewPager viewPager = mViewPager;
+        measureChild(viewPager, widthMeasureSpec, heightMeasureSpec);
+
+        final int measuredWidthAndState = viewPager.getMeasuredWidthAndState();
+        final int measuredHeightAndState = viewPager.getMeasuredHeightAndState();
+        setMeasuredDimension(measuredWidthAndState, measuredHeightAndState);
+
+        final int pagerWidth = viewPager.getMeasuredWidth();
+        final int pagerHeight = viewPager.getMeasuredHeight();
+        final int buttonWidthSpec = MeasureSpec.makeMeasureSpec(pagerWidth, MeasureSpec.AT_MOST);
+        final int buttonHeightSpec = MeasureSpec.makeMeasureSpec(pagerHeight, MeasureSpec.AT_MOST);
+        mPrevButton.measure(buttonWidthSpec, buttonHeightSpec);
+        mNextButton.measure(buttonWidthSpec, buttonHeightSpec);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        requestLayout();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        final ImageButton leftButton;
+        final ImageButton rightButton;
+        if (isLayoutRtl()) {
+            leftButton = mNextButton;
+            rightButton = mPrevButton;
+        } else {
+            leftButton = mPrevButton;
+            rightButton = mNextButton;
+        }
+
+        final int width = right - left;
+        final int height = bottom - top;
+        mViewPager.layout(0, 0, width, height);
+
+        final SimpleMonthView monthView = (SimpleMonthView) mViewPager.getChildAt(0);
+        final int monthHeight = monthView.getMonthHeight();
+        final int cellWidth = monthView.getCellWidth();
+
+        // Vertically center the previous/next buttons within the month
+        // header, horizontally center within the day cell.
+        final int leftDW = leftButton.getMeasuredWidth();
+        final int leftDH = leftButton.getMeasuredHeight();
+        final int leftIconTop = monthView.getPaddingTop() + (monthHeight - leftDH) / 2;
+        final int leftIconLeft = monthView.getPaddingLeft() + (cellWidth - leftDW) / 2;
+        leftButton.layout(leftIconLeft, leftIconTop, leftIconLeft + leftDW, leftIconTop + leftDH);
+
+        final int rightDW = rightButton.getMeasuredWidth();
+        final int rightDH = rightButton.getMeasuredHeight();
+        final int rightIconTop = monthView.getPaddingTop() + (monthHeight - rightDH) / 2;
+        final int rightIconRight = width - monthView.getPaddingRight() - (cellWidth - rightDW) / 2;
+        rightButton.layout(rightIconRight - rightDW, rightIconTop,
+                rightIconRight, rightIconTop + rightDH);
+    }
+
+    public void setDayOfWeekTextAppearance(int resId) {
+        mAdapter.setDayOfWeekTextAppearance(resId);
+    }
+
+    public int getDayOfWeekTextAppearance() {
+        return mAdapter.getDayOfWeekTextAppearance();
+    }
+
+    public void setDayTextAppearance(int resId) {
+        mAdapter.setDayTextAppearance(resId);
+    }
+
+    public int getDayTextAppearance() {
+        return mAdapter.getDayTextAppearance();
+    }
+
+    /**
+     * Sets the currently selected date to the specified timestamp. Jumps
+     * immediately to the new date. To animate to the new date, use
+     * {@link #setDate(long, boolean)}.
+     *
+     * @param timeInMillis the target day in milliseconds
+     */
+    public void setDate(long timeInMillis) {
+        setDate(timeInMillis, false);
+    }
+
+    /**
+     * Sets the currently selected date to the specified timestamp. Jumps
+     * immediately to the new date, optionally animating the transition.
+     *
+     * @param timeInMillis the target day in milliseconds
+     * @param animate whether to smooth scroll to the new position
+     */
+    public void setDate(long timeInMillis, boolean animate) {
+        setDate(timeInMillis, animate, true);
+    }
+
+    /**
+     * Moves to the month containing the specified day, optionally setting the
+     * day as selected.
+     *
+     * @param timeInMillis the target day in milliseconds
+     * @param animate whether to smooth scroll to the new position
+     * @param setSelected whether to set the specified day as selected
+     */
+    private void setDate(long timeInMillis, boolean animate, boolean setSelected) {
+        boolean dateClamped = false;
+        // Clamp the target day in milliseconds to the min or max if outside the range.
+        if (timeInMillis < mMinDate.getTimeInMillis()) {
+            timeInMillis = mMinDate.getTimeInMillis();
+            dateClamped = true;
+        } else if (timeInMillis > mMaxDate.getTimeInMillis()) {
+            timeInMillis = mMaxDate.getTimeInMillis();
+            dateClamped = true;
+        }
+
+        getTempCalendarForTime(timeInMillis);
+
+        if (setSelected || dateClamped) {
+            mSelectedDay.setTimeInMillis(timeInMillis);
+        }
+
+        final int position = getPositionFromDay(timeInMillis);
+        if (position != mViewPager.getCurrentItem()) {
+            mViewPager.setCurrentItem(position, animate);
+        }
+
+        mAdapter.setSelectedDay(mTempCalendar);
+    }
+
+    public long getDate() {
+        return mSelectedDay.getTimeInMillis();
+    }
+
+    public boolean getBoundsForDate(long timeInMillis, Rect outBounds) {
+        final int position = getPositionFromDay(timeInMillis);
+        if (position != mViewPager.getCurrentItem()) {
+            return false;
+        }
+
+        mTempCalendar.setTimeInMillis(timeInMillis);
+        return mAdapter.getBoundsForDate(mTempCalendar, outBounds);
+    }
+
+    public void setFirstDayOfWeek(int firstDayOfWeek) {
+        mAdapter.setFirstDayOfWeek(firstDayOfWeek);
+    }
+
+    public int getFirstDayOfWeek() {
+        return mAdapter.getFirstDayOfWeek();
+    }
+
+    public void setMinDate(long timeInMillis) {
+        mMinDate.setTimeInMillis(timeInMillis);
+        onRangeChanged();
+    }
+
+    public long getMinDate() {
+        return mMinDate.getTimeInMillis();
+    }
+
+    public void setMaxDate(long timeInMillis) {
+        mMaxDate.setTimeInMillis(timeInMillis);
+        onRangeChanged();
+    }
+
+    public long getMaxDate() {
+        return mMaxDate.getTimeInMillis();
+    }
+
+    /**
+     * Handles changes to date range.
+     */
+    public void onRangeChanged() {
+        mAdapter.setRange(mMinDate, mMaxDate);
+
+        // Changing the min/max date changes the selection position since we
+        // don't really have stable IDs. Jumps immediately to the new position.
+        setDate(mSelectedDay.getTimeInMillis(), false, false);
+
+        updateButtonVisibility(mViewPager.getCurrentItem());
+    }
+
+    /**
+     * Sets the listener to call when the user selects a day.
+     *
+     * @param listener The listener to call.
+     */
+    public void setOnDaySelectedListener(OnDaySelectedListener listener) {
+        mOnDaySelectedListener = listener;
+    }
+
+    private int getDiffMonths(Calendar start, Calendar end) {
+        final int diffYears = end.get(Calendar.YEAR) - start.get(Calendar.YEAR);
+        return end.get(Calendar.MONTH) - start.get(Calendar.MONTH) + 12 * diffYears;
+    }
+
+    private int getPositionFromDay(long timeInMillis) {
+        final int diffMonthMax = getDiffMonths(mMinDate, mMaxDate);
+        final int diffMonth = getDiffMonths(mMinDate, getTempCalendarForTime(timeInMillis));
+        return MathUtils.constrain(diffMonth, 0, diffMonthMax);
+    }
+
+    private Calendar getTempCalendarForTime(long timeInMillis) {
+        if (mTempCalendar == null) {
+            mTempCalendar = Calendar.getInstance();
+        }
+        mTempCalendar.setTimeInMillis(timeInMillis);
+        return mTempCalendar;
+    }
+
+    /**
+     * Gets the position of the view that is most prominently displayed within the list view.
+     */
+    public int getMostVisiblePosition() {
+        return mViewPager.getCurrentItem();
+    }
+
+    public void setPosition(int position) {
+        mViewPager.setCurrentItem(position, false);
+    }
+
+    private final OnPageChangeListener mOnPageChangedListener = new OnPageChangeListener() {
+        @Override
+        public void onPageScrolled(int position, float positionOffset, int positionOffsetPixels) {
+            final float alpha = Math.abs(0.5f - positionOffset) * 2.0f;
+            mPrevButton.setAlpha(alpha);
+            mNextButton.setAlpha(alpha);
+        }
+
+        @Override
+        public void onPageScrollStateChanged(int state) {}
+
+        @Override
+        public void onPageSelected(int position) {
+            updateButtonVisibility(position);
+        }
+    };
+
+    private final OnClickListener mOnClickListener = new OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            final int direction;
+            if (v == mPrevButton) {
+                direction = -1;
+            } else if (v == mNextButton) {
+                direction = 1;
+            } else {
+                return;
+            }
+
+            // Animation is expensive for accessibility services since it sends
+            // lots of scroll and content change events.
+            final boolean animate = !mAccessibilityManager.isEnabled();
+
+            // ViewPager clamps input values, so we don't need to worry
+            // about passing invalid indices.
+            final int nextItem = mViewPager.getCurrentItem() + direction;
+            mViewPager.setCurrentItem(nextItem, animate);
+        }
+    };
+
+    public interface OnDaySelectedListener {
+        void onDaySelected(DayPickerView view, Calendar day);
+    }
+}
diff --git a/android/widget/DayPickerViewPager.java b/android/widget/DayPickerViewPager.java
new file mode 100644
index 0000000..1704ed7
--- /dev/null
+++ b/android/widget/DayPickerViewPager.java
@@ -0,0 +1,172 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.View;
+
+import com.android.internal.widget.ViewPager;
+
+import java.util.ArrayList;
+import java.util.function.Predicate;
+
+/**
+ * This displays a list of months in a calendar format with selectable days.
+ */
+class DayPickerViewPager extends ViewPager {
+    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+    public DayPickerViewPager(Context context) {
+        this(context, null);
+    }
+
+    public DayPickerViewPager(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public DayPickerViewPager(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public DayPickerViewPager(Context context, AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        populate();
+
+        // Everything below is mostly copied from FrameLayout.
+        int count = getChildCount();
+
+        final boolean measureMatchParentChildren =
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+                        MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                measureChild(child, widthMeasureSpec, heightMeasureSpec);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                maxWidth = Math.max(maxWidth, child.getMeasuredWidth());
+                maxHeight = Math.max(maxHeight, child.getMeasuredHeight());
+                childState = combineMeasuredStates(childState, child.getMeasuredState());
+                if (measureMatchParentChildren) {
+                    if (lp.width == LayoutParams.MATCH_PARENT ||
+                            lp.height == LayoutParams.MATCH_PARENT) {
+                        mMatchParentChildren.add(child);
+                    }
+                }
+            }
+        }
+
+        // Account for padding too
+        maxWidth += getPaddingLeft() + getPaddingRight();
+        maxHeight += getPaddingTop() + getPaddingBottom();
+
+        // Check against our minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        // Check against our foreground's minimum height and width
+        final Drawable drawable = getForeground();
+        if (drawable != null) {
+            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+        }
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+        count = mMatchParentChildren.size();
+        if (count > 1) {
+            for (int i = 0; i < count; i++) {
+                final View child = mMatchParentChildren.get(i);
+
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                final int childWidthMeasureSpec;
+                final int childHeightMeasureSpec;
+
+                if (lp.width == LayoutParams.MATCH_PARENT) {
+                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            getMeasuredWidth() - getPaddingLeft() - getPaddingRight(),
+                            MeasureSpec.EXACTLY);
+                } else {
+                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                            getPaddingLeft() + getPaddingRight(),
+                            lp.width);
+                }
+
+                if (lp.height == LayoutParams.MATCH_PARENT) {
+                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            getMeasuredHeight() - getPaddingTop() - getPaddingBottom(),
+                            MeasureSpec.EXACTLY);
+                } else {
+                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                            getPaddingTop() + getPaddingBottom(),
+                            lp.height);
+                }
+
+                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            }
+        }
+
+        mMatchParentChildren.clear();
+    }
+
+    @Override
+    protected <T extends View> T findViewByPredicateTraversal(Predicate<View> predicate,
+            View childToSkip) {
+        if (predicate.test(this)) {
+            return (T) this;
+        }
+
+        // Always try the selected view first.
+        final DayPickerPagerAdapter adapter = (DayPickerPagerAdapter) getAdapter();
+        final SimpleMonthView current = adapter.getView(getCurrent());
+        if (current != childToSkip && current != null) {
+            final View v = current.findViewByPredicate(predicate);
+            if (v != null) {
+                return (T) v;
+            }
+        }
+
+        final int len = getChildCount();
+        for (int i = 0; i < len; i++) {
+            final View child = getChildAt(i);
+
+            if (child != childToSkip && child != current) {
+                final View v = child.findViewByPredicate(predicate);
+
+                if (v != null) {
+                    return (T) v;
+                }
+            }
+        }
+
+        return null;
+    }
+
+}
diff --git a/android/widget/DialerFilter.java b/android/widget/DialerFilter.java
new file mode 100644
index 0000000..8f9c96c
--- /dev/null
+++ b/android/widget/DialerFilter.java
@@ -0,0 +1,422 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.graphics.Rect;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.Spanned;
+import android.text.TextWatcher;
+import android.text.method.DialerKeyListener;
+import android.text.method.KeyListener;
+import android.text.method.TextKeyListener;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.View;
+
+/**
+ * This widget is a layout that contains several specifically-named child views that
+ * handle keyboard entry interpreted as standard phone dialpad digits.
+ *
+ * @deprecated Use a custom view or layout to handle this functionality instead
+ */
+@Deprecated
+public class DialerFilter extends RelativeLayout
+{
+    public DialerFilter(Context context) {
+        super(context);
+    }
+
+    public DialerFilter(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        // Setup the filter view
+        mInputFilters = new InputFilter[] { new InputFilter.AllCaps() };
+
+        mHint = (EditText) findViewById(com.android.internal.R.id.hint);
+        if (mHint == null) {
+            throw new IllegalStateException("DialerFilter must have a child EditText named hint");
+        }
+        mHint.setFilters(mInputFilters);
+
+        mLetters = mHint;
+        mLetters.setKeyListener(TextKeyListener.getInstance());
+        mLetters.setMovementMethod(null);
+        mLetters.setFocusable(false);
+
+        // Setup the digits view
+        mPrimary = (EditText) findViewById(com.android.internal.R.id.primary);
+        if (mPrimary == null) {
+            throw new IllegalStateException("DialerFilter must have a child EditText named primary");
+        }
+        mPrimary.setFilters(mInputFilters);
+
+        mDigits = mPrimary;
+        mDigits.setKeyListener(DialerKeyListener.getInstance());
+        mDigits.setMovementMethod(null);
+        mDigits.setFocusable(false);
+
+        // Look for an icon
+        mIcon = (ImageView) findViewById(com.android.internal.R.id.icon);
+
+        // Setup focus & highlight for this view
+        setFocusable(true);
+
+        // XXX Force the mode to QWERTY for now, since 12-key isn't supported
+        mIsQwerty = true;
+        setMode(DIGITS_AND_LETTERS);
+    }
+
+    /**
+     * Only show the icon view when focused, if there is one.
+     */
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+
+        if (mIcon != null) {
+            mIcon.setVisibility(focused ? View.VISIBLE : View.GONE);
+        }
+    }
+
+
+    public boolean isQwertyKeyboard() {
+        return mIsQwerty;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        boolean handled = false;
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                break;
+
+            case KeyEvent.KEYCODE_DEL:
+                switch (mMode) {
+                    case DIGITS_AND_LETTERS:
+                        handled = mDigits.onKeyDown(keyCode, event);
+                        handled &= mLetters.onKeyDown(keyCode, event);
+                        break;
+
+                    case DIGITS_AND_LETTERS_NO_DIGITS:
+                        handled = mLetters.onKeyDown(keyCode, event);
+                        if (mLetters.getText().length() == mDigits.getText().length()) {
+                            setMode(DIGITS_AND_LETTERS);
+                        }
+                        break;
+
+                    case DIGITS_AND_LETTERS_NO_LETTERS:
+                        if (mDigits.getText().length() == mLetters.getText().length()) {
+                            mLetters.onKeyDown(keyCode, event);
+                            setMode(DIGITS_AND_LETTERS);
+                        }
+                        handled = mDigits.onKeyDown(keyCode, event);
+                        break;
+
+                    case DIGITS_ONLY:
+                        handled = mDigits.onKeyDown(keyCode, event);
+                        break;
+
+                    case LETTERS_ONLY:
+                        handled = mLetters.onKeyDown(keyCode, event);
+                        break;
+                }
+                break;
+
+            default:
+                //mIsQwerty = msg.getKeyIsQwertyKeyboard();
+
+                switch (mMode) {
+                    case DIGITS_AND_LETTERS:
+                        handled = mLetters.onKeyDown(keyCode, event);
+
+                        // pass this throw so the shift state is correct (for example,
+                        // on a standard QWERTY keyboard, * and 8 are on the same key)
+                        if (KeyEvent.isModifierKey(keyCode)) {
+                            mDigits.onKeyDown(keyCode, event);
+                            handled = true;
+                            break;
+                        }
+
+                        // Only check to see if the digit is valid if the key is a printing key
+                        // in the TextKeyListener. This prevents us from hiding the digits
+                        // line when keys like UP and DOWN are hit.
+                        // XXX note that KEYCODE_TAB is special-cased here for
+                        // devices that share tab and 0 on a single key.
+                        boolean isPrint = event.isPrintingKey();
+                        if (isPrint || keyCode == KeyEvent.KEYCODE_SPACE
+                                || keyCode == KeyEvent.KEYCODE_TAB) {
+                            char c = event.getMatch(DialerKeyListener.CHARACTERS);
+                            if (c != 0) {
+                                handled &= mDigits.onKeyDown(keyCode, event);
+                            } else {
+                                setMode(DIGITS_AND_LETTERS_NO_DIGITS);
+                            }
+                        }
+                        break;
+
+                    case DIGITS_AND_LETTERS_NO_LETTERS:
+                    case DIGITS_ONLY:
+                        handled = mDigits.onKeyDown(keyCode, event);
+                        break;
+
+                    case DIGITS_AND_LETTERS_NO_DIGITS:
+                    case LETTERS_ONLY:
+                        handled = mLetters.onKeyDown(keyCode, event);
+                        break;
+                }
+        }
+
+        if (!handled) {
+            return super.onKeyDown(keyCode, event);
+        } else {
+            return true;
+        }
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        boolean a = mLetters.onKeyUp(keyCode, event);
+        boolean b = mDigits.onKeyUp(keyCode, event);
+        return a || b;
+    }
+
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Change the mode of the widget.
+     *
+     * @param newMode The mode to switch to.
+     */
+    public void setMode(int newMode) {
+        switch (newMode) {
+            case DIGITS_AND_LETTERS:
+                makeDigitsPrimary();
+                mLetters.setVisibility(View.VISIBLE);
+                mDigits.setVisibility(View.VISIBLE);
+                break;
+
+            case DIGITS_ONLY:
+                makeDigitsPrimary();
+                mLetters.setVisibility(View.GONE);
+                mDigits.setVisibility(View.VISIBLE);
+                break;
+
+            case LETTERS_ONLY:
+                makeLettersPrimary();
+                mLetters.setVisibility(View.VISIBLE);
+                mDigits.setVisibility(View.GONE);
+                break;
+
+            case DIGITS_AND_LETTERS_NO_LETTERS:
+                makeDigitsPrimary();
+                mLetters.setVisibility(View.INVISIBLE);
+                mDigits.setVisibility(View.VISIBLE);
+                break;
+
+            case DIGITS_AND_LETTERS_NO_DIGITS:
+                makeLettersPrimary();
+                mLetters.setVisibility(View.VISIBLE);
+                mDigits.setVisibility(View.INVISIBLE);
+                break;
+
+        }
+        int oldMode = mMode;
+        mMode = newMode;
+        onModeChange(oldMode, newMode);
+    }
+
+    private void makeLettersPrimary() {
+        if (mPrimary == mDigits) {
+            swapPrimaryAndHint(true);
+        }
+    }
+
+    private void makeDigitsPrimary() {
+        if (mPrimary == mLetters) {
+            swapPrimaryAndHint(false);
+        }
+    }
+
+    private void swapPrimaryAndHint(boolean makeLettersPrimary) {
+        Editable lettersText = mLetters.getText();
+        Editable digitsText = mDigits.getText();
+        KeyListener lettersInput = mLetters.getKeyListener();
+        KeyListener digitsInput = mDigits.getKeyListener();
+
+        if (makeLettersPrimary) {
+            mLetters = mPrimary;
+            mDigits = mHint;
+        } else {
+            mLetters = mHint;
+            mDigits = mPrimary;
+        }
+
+        mLetters.setKeyListener(lettersInput);
+        mLetters.setText(lettersText);
+        lettersText = mLetters.getText();
+        Selection.setSelection(lettersText, lettersText.length());
+
+        mDigits.setKeyListener(digitsInput);
+        mDigits.setText(digitsText);
+        digitsText = mDigits.getText();
+        Selection.setSelection(digitsText, digitsText.length());
+
+        // Reset the filters
+        mPrimary.setFilters(mInputFilters);
+        mHint.setFilters(mInputFilters);
+    }
+
+
+    public CharSequence getLetters() {
+        if (mLetters.getVisibility() == View.VISIBLE) {
+            return mLetters.getText();
+        } else {
+            return "";
+        }
+    }
+
+    public CharSequence getDigits() {
+        if (mDigits.getVisibility() == View.VISIBLE) {
+            return mDigits.getText();
+        } else {
+            return "";
+        }
+    }
+
+    public CharSequence getFilterText() {
+        if (mMode != DIGITS_ONLY) {
+            return getLetters();
+        } else {
+            return getDigits();
+        }
+    }
+
+    public void append(String text) {
+        switch (mMode) {
+            case DIGITS_AND_LETTERS:
+                mDigits.getText().append(text);
+                mLetters.getText().append(text);
+                break;
+
+            case DIGITS_AND_LETTERS_NO_LETTERS:
+            case DIGITS_ONLY:
+                mDigits.getText().append(text);
+                break;
+
+            case DIGITS_AND_LETTERS_NO_DIGITS:
+            case LETTERS_ONLY:
+                mLetters.getText().append(text);
+                break;
+        }
+    }
+
+    /**
+     * Clears both the digits and the filter text.
+     */
+    public void clearText() {
+        Editable text;
+
+        text = mLetters.getText();
+        text.clear();
+
+        text = mDigits.getText();
+        text.clear();
+
+        // Reset the mode based on the hardware type
+        if (mIsQwerty) {
+            setMode(DIGITS_AND_LETTERS);
+        } else {
+            setMode(DIGITS_ONLY);
+        }
+    }
+
+    public void setLettersWatcher(TextWatcher watcher) {
+        CharSequence text = mLetters.getText();
+        Spannable span = (Spannable)text;
+        span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+    }
+
+    public void setDigitsWatcher(TextWatcher watcher) {
+        CharSequence text = mDigits.getText();
+        Spannable span = (Spannable)text;
+        span.setSpan(watcher, 0, text.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+    }
+
+    public void setFilterWatcher(TextWatcher watcher) {
+        if (mMode != DIGITS_ONLY) {
+            setLettersWatcher(watcher);
+        } else {
+            setDigitsWatcher(watcher);
+        }
+    }
+
+    public void removeFilterWatcher(TextWatcher watcher) {
+        Spannable text;
+        if (mMode != DIGITS_ONLY) {
+            text = mLetters.getText();
+        } else {
+            text = mDigits.getText();
+        }
+        text.removeSpan(watcher);
+    }
+
+    /**
+     * Called right after the mode changes to give subclasses the option to
+     * restyle, etc.
+     */
+    protected void onModeChange(int oldMode, int newMode) {
+    }
+
+    /** This mode has both lines */
+    public static final int DIGITS_AND_LETTERS = 1;
+    /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter
+     *  has removed all possibility of the digits matching, leaving only the letters line */
+    public static final int DIGITS_AND_LETTERS_NO_DIGITS = 2;
+    /** This mode is when after starting in {@link #DIGITS_AND_LETTERS} mode the filter
+     *  has removed all possibility of the letters matching, leaving only the digits line */
+    public static final int DIGITS_AND_LETTERS_NO_LETTERS = 3;
+    /** This mode has only the digits line */
+    public static final int DIGITS_ONLY = 4;
+    /** This mode has only the letters line */
+    public static final int LETTERS_ONLY = 5;
+
+    EditText mLetters;
+    EditText mDigits;
+    EditText mPrimary;
+    EditText mHint;
+    InputFilter mInputFilters[];
+    ImageView mIcon;
+    int mMode;
+    private boolean mIsQwerty;
+}
diff --git a/android/widget/DigitalClock.java b/android/widget/DigitalClock.java
new file mode 100644
index 0000000..c503ef2
--- /dev/null
+++ b/android/widget/DigitalClock.java
@@ -0,0 +1,123 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.database.ContentObserver;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+
+import java.util.Calendar;
+
+/**
+ * Like AnalogClock, but digital.  Shows seconds.
+ *
+ * @deprecated It is recommended you use {@link TextClock} instead.
+ */
+@Deprecated
+public class DigitalClock extends TextView {
+    // FIXME: implement separate views for hours/minutes/seconds, so
+    // proportional fonts don't shake rendering
+
+    Calendar mCalendar;
+    @SuppressWarnings("FieldCanBeLocal") // We must keep a reference to this observer
+    private FormatChangeObserver mFormatChangeObserver;
+
+    private Runnable mTicker;
+    private Handler mHandler;
+
+    private boolean mTickerStopped = false;
+
+    String mFormat;
+
+    public DigitalClock(Context context) {
+        super(context);
+        initClock();
+    }
+
+    public DigitalClock(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initClock();
+    }
+
+    private void initClock() {
+        if (mCalendar == null) {
+            mCalendar = Calendar.getInstance();
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        mTickerStopped = false;
+        super.onAttachedToWindow();
+
+        mFormatChangeObserver = new FormatChangeObserver();
+        getContext().getContentResolver().registerContentObserver(
+                Settings.System.CONTENT_URI, true, mFormatChangeObserver);
+        setFormat();
+
+        mHandler = new Handler();
+
+        /**
+         * requests a tick on the next hard-second boundary
+         */
+        mTicker = new Runnable() {
+            public void run() {
+                if (mTickerStopped) return;
+                mCalendar.setTimeInMillis(System.currentTimeMillis());
+                setText(DateFormat.format(mFormat, mCalendar));
+                invalidate();
+                long now = SystemClock.uptimeMillis();
+                long next = now + (1000 - now % 1000);
+                mHandler.postAtTime(mTicker, next);
+            }
+        };
+        mTicker.run();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mTickerStopped = true;
+        getContext().getContentResolver().unregisterContentObserver(
+                mFormatChangeObserver);
+    }
+
+    private void setFormat() {
+        mFormat = DateFormat.getTimeFormatString(getContext());
+    }
+
+    private class FormatChangeObserver extends ContentObserver {
+        public FormatChangeObserver() {
+            super(new Handler());
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            setFormat();
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        //noinspection deprecation
+        return DigitalClock.class.getName();
+    }
+}
diff --git a/android/widget/DoubleDigitManager.java b/android/widget/DoubleDigitManager.java
new file mode 100644
index 0000000..1eea1fb
--- /dev/null
+++ b/android/widget/DoubleDigitManager.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.os.Handler;
+
+/**
+ * Provides callbacks indicating the steps in two digit pressing within a
+ * timeout.
+ *
+ * Package private: only relevant in helping {@link TimeSpinnerHelper}.
+ */
+class DoubleDigitManager {
+
+    private final long timeoutInMillis;
+    private final CallBack mCallBack;
+
+    private Integer intermediateDigit;
+
+    /**
+     * @param timeoutInMillis How long after the first digit is pressed does
+     *   the user have to press the second digit?
+     * @param callBack The callback to indicate what's going on with the user.
+     */
+    public DoubleDigitManager(long timeoutInMillis, CallBack callBack) {
+        this.timeoutInMillis = timeoutInMillis;
+        mCallBack = callBack;
+    }
+
+    /**
+     * Report to this manager that a digit was pressed.
+     * @param digit
+     */
+    public void reportDigit(int digit) {
+        if (intermediateDigit == null) {
+            intermediateDigit = digit;
+
+            new Handler().postDelayed(new Runnable() {
+                public void run() {
+                    if (intermediateDigit != null) {
+                        mCallBack.singleDigitFinal(intermediateDigit);
+                        intermediateDigit = null;
+                    }
+                }
+            }, timeoutInMillis);
+
+            if (!mCallBack.singleDigitIntermediate(digit)) {
+
+                // this wasn't a good candidate for the intermediate digit,
+                // make it the final digit (since there is no opportunity to
+                // reject the final digit).
+                intermediateDigit = null;
+                mCallBack.singleDigitFinal(digit);
+            }
+        } else if (mCallBack.twoDigitsFinal(intermediateDigit, digit)) {
+             intermediateDigit = null;
+        }
+    }
+
+    /**
+     * The callback to indicate what is going on with the digits pressed.
+     */
+    static interface CallBack {
+
+        /**
+         * A digit was pressed, and there are no intermediate digits.
+         * @param digit The digit pressed.
+         * @return Whether the digit was accepted; how the user of this manager
+         *   tells us that the intermediate digit is acceptable as an
+         *   intermediate digit.
+         */
+        boolean singleDigitIntermediate(int digit);
+
+        /**
+         * A single digit was pressed, and it is 'the final answer'.
+         * - a single digit pressed, and the timeout expires.
+         * - a single digit pressed, and {@link #singleDigitIntermediate}
+         *   returned false.
+         * @param digit The digit.
+         */
+        void singleDigitFinal(int digit);
+
+        /**
+         * The user pressed digit1, then digit2 within the timeout.
+         * @param digit1
+         * @param digit2
+         */
+        boolean twoDigitsFinal(int digit1, int digit2);
+    }
+
+}
diff --git a/android/widget/DropDownListView.java b/android/widget/DropDownListView.java
new file mode 100644
index 0000000..e9c4728
--- /dev/null
+++ b/android/widget/DropDownListView.java
@@ -0,0 +1,370 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.widget.AutoScrollHelper.AbsListViewAutoScroller;
+
+/**
+ * Wrapper class for a ListView. This wrapper can hijack the focus to
+ * make sure the list uses the appropriate drawables and states when
+ * displayed on screen within a drop down. The focus is never actually
+ * passed to the drop down in this mode; the list only looks focused.
+ *
+ * @hide
+ */
+public class DropDownListView extends ListView {
+    /*
+     * WARNING: This is a workaround for a touch mode issue.
+     *
+     * Touch mode is propagated lazily to windows. This causes problems in
+     * the following scenario:
+     * - Type something in the AutoCompleteTextView and get some results
+     * - Move down with the d-pad to select an item in the list
+     * - Move up with the d-pad until the selection disappears
+     * - Type more text in the AutoCompleteTextView *using the soft keyboard*
+     *   and get new results; you are now in touch mode
+     * - The selection comes back on the first item in the list, even though
+     *   the list is supposed to be in touch mode
+     *
+     * Using the soft keyboard triggers the touch mode change but that change
+     * is propagated to our window only after the first list layout, therefore
+     * after the list attempts to resurrect the selection.
+     *
+     * The trick to work around this issue is to pretend the list is in touch
+     * mode when we know that the selection should not appear, that is when
+     * we know the user moved the selection away from the list.
+     *
+     * This boolean is set to true whenever we explicitly hide the list's
+     * selection and reset to false whenever we know the user moved the
+     * selection back to the list.
+     *
+     * When this boolean is true, isInTouchMode() returns true, otherwise it
+     * returns super.isInTouchMode().
+     */
+    private boolean mListSelectionHidden;
+
+    /**
+     * True if this wrapper should fake focus.
+     */
+    private boolean mHijackFocus;
+
+    /** Whether to force drawing of the pressed state selector. */
+    private boolean mDrawsInPressedState;
+
+    /** Helper for drag-to-open auto scrolling. */
+    private AbsListViewAutoScroller mScrollHelper;
+
+    /**
+     * Runnable posted when we are awaiting hover event resolution. When set,
+     * drawable state changes are postponed.
+     */
+    private ResolveHoverRunnable mResolveHoverRunnable;
+
+    /**
+     * Creates a new list view wrapper.
+     *
+     * @param context this view's context
+     */
+    public DropDownListView(@NonNull Context context, boolean hijackFocus) {
+        this(context, hijackFocus, com.android.internal.R.attr.dropDownListViewStyle);
+    }
+
+    /**
+     * Creates a new list view wrapper.
+     *
+     * @param context this view's context
+     */
+    public DropDownListView(@NonNull Context context, boolean hijackFocus, int defStyleAttr) {
+        super(context, null, defStyleAttr);
+        mHijackFocus = hijackFocus;
+        // TODO: Add an API to control this
+        setCacheColorHint(0); // Transparent, since the background drawable could be anything.
+    }
+
+    @Override
+    boolean shouldShowSelector() {
+        return isHovered() || super.shouldShowSelector();
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (mResolveHoverRunnable != null) {
+            // Resolved hover event as hover => touch transition.
+            mResolveHoverRunnable.cancel();
+        }
+
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onHoverEvent(@NonNull MotionEvent ev) {
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_HOVER_EXIT && mResolveHoverRunnable == null) {
+            // This may be transitioning to TOUCH_DOWN. Postpone drawable state
+            // updates until either the next frame or the next touch event.
+            mResolveHoverRunnable = new ResolveHoverRunnable();
+            mResolveHoverRunnable.post();
+        }
+
+        // Allow the super class to handle hover state management first.
+        final boolean handled = super.onHoverEvent(ev);
+
+        if (action == MotionEvent.ACTION_HOVER_ENTER
+                || action == MotionEvent.ACTION_HOVER_MOVE) {
+            final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
+            if (position != INVALID_POSITION && position != mSelectedPosition) {
+                final View hoveredItem = getChildAt(position - getFirstVisiblePosition());
+                if (hoveredItem.isEnabled()) {
+                    // Force a focus so that the proper selector state gets
+                    // used when we update.
+                    requestFocus();
+
+                    positionSelector(position, hoveredItem);
+                    setSelectedPositionInt(position);
+                    setNextSelectedPositionInt(position);
+                }
+                updateSelectorState();
+            }
+        } else {
+            // Do not cancel the selected position if the selection is visible
+            // by other means.
+            if (!super.shouldShowSelector()) {
+                setSelectedPositionInt(INVALID_POSITION);
+                setNextSelectedPositionInt(INVALID_POSITION);
+            }
+        }
+
+        return handled;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        if (mResolveHoverRunnable == null) {
+            super.drawableStateChanged();
+        }
+    }
+
+    /**
+     * Handles forwarded events.
+     *
+     * @param activePointerId id of the pointer that activated forwarding
+     * @return whether the event was handled
+     */
+    public boolean onForwardedEvent(@NonNull MotionEvent event, int activePointerId) {
+        boolean handledEvent = true;
+        boolean clearPressedItem = false;
+
+        final int actionMasked = event.getActionMasked();
+        switch (actionMasked) {
+            case MotionEvent.ACTION_CANCEL:
+                handledEvent = false;
+                break;
+            case MotionEvent.ACTION_UP:
+                handledEvent = false;
+                // $FALL-THROUGH$
+            case MotionEvent.ACTION_MOVE:
+                final int activeIndex = event.findPointerIndex(activePointerId);
+                if (activeIndex < 0) {
+                    handledEvent = false;
+                    break;
+                }
+
+                final int x = (int) event.getX(activeIndex);
+                final int y = (int) event.getY(activeIndex);
+                final int position = pointToPosition(x, y);
+                if (position == INVALID_POSITION) {
+                    clearPressedItem = true;
+                    break;
+                }
+
+                final View child = getChildAt(position - getFirstVisiblePosition());
+                setPressedItem(child, position, x, y);
+                handledEvent = true;
+
+                if (actionMasked == MotionEvent.ACTION_UP) {
+                    final long id = getItemIdAtPosition(position);
+                    performItemClick(child, position, id);
+                }
+                break;
+        }
+
+        // Failure to handle the event cancels forwarding.
+        if (!handledEvent || clearPressedItem) {
+            clearPressedItem();
+        }
+
+        // Manage automatic scrolling.
+        if (handledEvent) {
+            if (mScrollHelper == null) {
+                mScrollHelper = new AbsListViewAutoScroller(this);
+            }
+            mScrollHelper.setEnabled(true);
+            mScrollHelper.onTouch(this, event);
+        } else if (mScrollHelper != null) {
+            mScrollHelper.setEnabled(false);
+        }
+
+        return handledEvent;
+    }
+
+    /**
+     * Sets whether the list selection is hidden, as part of a workaround for a
+     * touch mode issue (see the declaration for mListSelectionHidden).
+     *
+     * @param hideListSelection {@code true} to hide list selection,
+     *                          {@code false} to show
+     */
+    public void setListSelectionHidden(boolean hideListSelection) {
+        mListSelectionHidden = hideListSelection;
+    }
+
+    private void clearPressedItem() {
+        mDrawsInPressedState = false;
+        setPressed(false);
+        updateSelectorState();
+
+        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+        if (motionView != null) {
+            motionView.setPressed(false);
+        }
+    }
+
+    private void setPressedItem(@NonNull View child, int position, float x, float y) {
+        mDrawsInPressedState = true;
+
+        // Ordering is essential. First, update the container's pressed state.
+        drawableHotspotChanged(x, y);
+        if (!isPressed()) {
+            setPressed(true);
+        }
+
+        // Next, run layout if we need to stabilize child positions.
+        if (mDataChanged) {
+            layoutChildren();
+        }
+
+        // Manage the pressed view based on motion position. This allows us to
+        // play nicely with actual touch and scroll events.
+        final View motionView = getChildAt(mMotionPosition - mFirstPosition);
+        if (motionView != null && motionView != child && motionView.isPressed()) {
+            motionView.setPressed(false);
+        }
+        mMotionPosition = position;
+
+        // Offset for child coordinates.
+        final float childX = x - child.getLeft();
+        final float childY = y - child.getTop();
+        child.drawableHotspotChanged(childX, childY);
+        if (!child.isPressed()) {
+            child.setPressed(true);
+        }
+
+        // Ensure that keyboard focus starts from the last touched position.
+        setSelectedPositionInt(position);
+        positionSelectorLikeTouch(position, child, x, y);
+
+        // Refresh the drawable state to reflect the new pressed state,
+        // which will also update the selector state.
+        refreshDrawableState();
+    }
+
+    @Override
+    boolean touchModeDrawsInPressedState() {
+        return mDrawsInPressedState || super.touchModeDrawsInPressedState();
+    }
+
+    /**
+     * Avoids jarring scrolling effect by ensuring that list elements
+     * made of a text view fit on a single line.
+     *
+     * @param position the item index in the list to get a view for
+     * @return the view for the specified item
+     */
+    @Override
+    View obtainView(int position, boolean[] isScrap) {
+        View view = super.obtainView(position, isScrap);
+
+        if (view instanceof TextView) {
+            ((TextView) view).setHorizontallyScrolling(true);
+        }
+
+        return view;
+    }
+
+    @Override
+    public boolean isInTouchMode() {
+        // WARNING: Please read the comment where mListSelectionHidden is declared
+        return (mHijackFocus && mListSelectionHidden) || super.isInTouchMode();
+    }
+
+    /**
+     * Returns the focus state in the drop down.
+     *
+     * @return true always if hijacking focus
+     */
+    @Override
+    public boolean hasWindowFocus() {
+        return mHijackFocus || super.hasWindowFocus();
+    }
+
+    /**
+     * Returns the focus state in the drop down.
+     *
+     * @return true always if hijacking focus
+     */
+    @Override
+    public boolean isFocused() {
+        return mHijackFocus || super.isFocused();
+    }
+
+    /**
+     * Returns the focus state in the drop down.
+     *
+     * @return true always if hijacking focus
+     */
+    @Override
+    public boolean hasFocus() {
+        return mHijackFocus || super.hasFocus();
+    }
+
+    /**
+     * Runnable that forces hover event resolution and updates drawable state.
+     */
+    private class ResolveHoverRunnable implements Runnable {
+        @Override
+        public void run() {
+            // Resolved hover event as standard hover exit.
+            mResolveHoverRunnable = null;
+            drawableStateChanged();
+        }
+
+        public void cancel() {
+            mResolveHoverRunnable = null;
+            removeCallbacks(this);
+        }
+
+        public void post() {
+            DropDownListView.this.post(this);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/widget/EdgeEffect.java b/android/widget/EdgeEffect.java
new file mode 100644
index 0000000..f9f5901
--- /dev/null
+++ b/android/widget/EdgeEffect.java
@@ -0,0 +1,402 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.ColorInt;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+
+/**
+ * This class performs the graphical effect used at the edges of scrollable widgets
+ * when the user scrolls beyond the content bounds in 2D space.
+ *
+ * <p>EdgeEffect is stateful. Custom widgets using EdgeEffect should create an
+ * instance for each edge that should show the effect, feed it input data using
+ * the methods {@link #onAbsorb(int)}, {@link #onPull(float)}, and {@link #onRelease()},
+ * and draw the effect using {@link #draw(Canvas)} in the widget's overridden
+ * {@link android.view.View#draw(Canvas)} method. If {@link #isFinished()} returns
+ * false after drawing, the edge effect's animation is not yet complete and the widget
+ * should schedule another drawing pass to continue the animation.</p>
+ *
+ * <p>When drawing, widgets should draw their main content and child views first,
+ * usually by invoking <code>super.draw(canvas)</code> from an overridden <code>draw</code>
+ * method. (This will invoke onDraw and dispatch drawing to child views as needed.)
+ * The edge effect may then be drawn on top of the view's content using the
+ * {@link #draw(Canvas)} method.</p>
+ */
+public class EdgeEffect {
+    @SuppressWarnings("UnusedDeclaration")
+    private static final String TAG = "EdgeEffect";
+
+    // Time it will take the effect to fully recede in ms
+    private static final int RECEDE_TIME = 600;
+
+    // Time it will take before a pulled glow begins receding in ms
+    private static final int PULL_TIME = 167;
+
+    // Time it will take in ms for a pulled glow to decay to partial strength before release
+    private static final int PULL_DECAY_TIME = 2000;
+
+    private static final float MAX_ALPHA = 0.15f;
+    private static final float GLOW_ALPHA_START = .09f;
+
+    private static final float MAX_GLOW_SCALE = 2.f;
+
+    private static final float PULL_GLOW_BEGIN = 0.f;
+
+    // Minimum velocity that will be absorbed
+    private static final int MIN_VELOCITY = 100;
+    // Maximum velocity, clamps at this value
+    private static final int MAX_VELOCITY = 10000;
+
+    private static final float EPSILON = 0.001f;
+
+    private static final double ANGLE = Math.PI / 6;
+    private static final float SIN = (float) Math.sin(ANGLE);
+    private static final float COS = (float) Math.cos(ANGLE);
+    private static final float RADIUS_FACTOR = 0.6f;
+
+    private float mGlowAlpha;
+    private float mGlowScaleY;
+
+    private float mGlowAlphaStart;
+    private float mGlowAlphaFinish;
+    private float mGlowScaleYStart;
+    private float mGlowScaleYFinish;
+
+    private long mStartTime;
+    private float mDuration;
+
+    private final Interpolator mInterpolator;
+
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PULL = 1;
+    private static final int STATE_ABSORB = 2;
+    private static final int STATE_RECEDE = 3;
+    private static final int STATE_PULL_DECAY = 4;
+
+    private static final float PULL_DISTANCE_ALPHA_GLOW_FACTOR = 0.8f;
+
+    private static final int VELOCITY_GLOW_FACTOR = 6;
+
+    private int mState = STATE_IDLE;
+
+    private float mPullDistance;
+
+    private final Rect mBounds = new Rect();
+    private final Paint mPaint = new Paint();
+    private float mRadius;
+    private float mBaseGlowScale;
+    private float mDisplacement = 0.5f;
+    private float mTargetDisplacement = 0.5f;
+
+    /**
+     * Construct a new EdgeEffect with a theme appropriate for the provided context.
+     * @param context Context used to provide theming and resource information for the EdgeEffect
+     */
+    public EdgeEffect(Context context) {
+        mPaint.setAntiAlias(true);
+        final TypedArray a = context.obtainStyledAttributes(
+                com.android.internal.R.styleable.EdgeEffect);
+        final int themeColor = a.getColor(
+                com.android.internal.R.styleable.EdgeEffect_colorEdgeEffect, 0xff666666);
+        a.recycle();
+        mPaint.setColor((themeColor & 0xffffff) | 0x33000000);
+        mPaint.setStyle(Paint.Style.FILL);
+        mPaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC_ATOP));
+        mInterpolator = new DecelerateInterpolator();
+    }
+
+    /**
+     * Set the size of this edge effect in pixels.
+     *
+     * @param width Effect width in pixels
+     * @param height Effect height in pixels
+     */
+    public void setSize(int width, int height) {
+        final float r = width * RADIUS_FACTOR / SIN;
+        final float y = COS * r;
+        final float h = r - y;
+        final float or = height * RADIUS_FACTOR / SIN;
+        final float oy = COS * or;
+        final float oh = or - oy;
+
+        mRadius = r;
+        mBaseGlowScale = h > 0 ? Math.min(oh / h, 1.f) : 1.f;
+
+        mBounds.set(mBounds.left, mBounds.top, width, (int) Math.min(height, h));
+    }
+
+    /**
+     * Reports if this EdgeEffect's animation is finished. If this method returns false
+     * after a call to {@link #draw(Canvas)} the host widget should schedule another
+     * drawing pass to continue the animation.
+     *
+     * @return true if animation is finished, false if drawing should continue on the next frame.
+     */
+    public boolean isFinished() {
+        return mState == STATE_IDLE;
+    }
+
+    /**
+     * Immediately finish the current animation.
+     * After this call {@link #isFinished()} will return true.
+     */
+    public void finish() {
+        mState = STATE_IDLE;
+    }
+
+    /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly.
+     *
+     * <p>Views using EdgeEffect should favor {@link #onPull(float, float)} when the displacement
+     * of the pull point is known.</p>
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     */
+    public void onPull(float deltaDistance) {
+        onPull(deltaDistance, 0.5f);
+    }
+
+    /**
+     * A view should call this when content is pulled away from an edge by the user.
+     * This will update the state of the current visual effect and its associated animation.
+     * The host view should always {@link android.view.View#invalidate()} after this
+     * and draw the results accordingly.
+     *
+     * @param deltaDistance Change in distance since the last call. Values may be 0 (no change) to
+     *                      1.f (full length of the view) or negative values to express change
+     *                      back toward the edge reached to initiate the effect.
+     * @param displacement The displacement from the starting side of the effect of the point
+     *                     initiating the pull. In the case of touch this is the finger position.
+     *                     Values may be from 0-1.
+     */
+    public void onPull(float deltaDistance, float displacement) {
+        final long now = AnimationUtils.currentAnimationTimeMillis();
+        mTargetDisplacement = displacement;
+        if (mState == STATE_PULL_DECAY && now - mStartTime < mDuration) {
+            return;
+        }
+        if (mState != STATE_PULL) {
+            mGlowScaleY = Math.max(PULL_GLOW_BEGIN, mGlowScaleY);
+        }
+        mState = STATE_PULL;
+
+        mStartTime = now;
+        mDuration = PULL_TIME;
+
+        mPullDistance += deltaDistance;
+
+        final float absdd = Math.abs(deltaDistance);
+        mGlowAlpha = mGlowAlphaStart = Math.min(MAX_ALPHA,
+                mGlowAlpha + (absdd * PULL_DISTANCE_ALPHA_GLOW_FACTOR));
+
+        if (mPullDistance == 0) {
+            mGlowScaleY = mGlowScaleYStart = 0;
+        } else {
+            final float scale = (float) (Math.max(0, 1 - 1 /
+                    Math.sqrt(Math.abs(mPullDistance) * mBounds.height()) - 0.3d) / 0.7d);
+
+            mGlowScaleY = mGlowScaleYStart = scale;
+        }
+
+        mGlowAlphaFinish = mGlowAlpha;
+        mGlowScaleYFinish = mGlowScaleY;
+    }
+
+    /**
+     * Call when the object is released after being pulled.
+     * This will begin the "decay" phase of the effect. After calling this method
+     * the host view should {@link android.view.View#invalidate()} and thereby
+     * draw the results accordingly.
+     */
+    public void onRelease() {
+        mPullDistance = 0;
+
+        if (mState != STATE_PULL && mState != STATE_PULL_DECAY) {
+            return;
+        }
+
+        mState = STATE_RECEDE;
+        mGlowAlphaStart = mGlowAlpha;
+        mGlowScaleYStart = mGlowScaleY;
+
+        mGlowAlphaFinish = 0.f;
+        mGlowScaleYFinish = 0.f;
+
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mDuration = RECEDE_TIME;
+    }
+
+    /**
+     * Call when the effect absorbs an impact at the given velocity.
+     * Used when a fling reaches the scroll boundary.
+     *
+     * <p>When using a {@link android.widget.Scroller} or {@link android.widget.OverScroller},
+     * the method <code>getCurrVelocity</code> will provide a reasonable approximation
+     * to use here.</p>
+     *
+     * @param velocity Velocity at impact in pixels per second.
+     */
+    public void onAbsorb(int velocity) {
+        mState = STATE_ABSORB;
+        velocity = Math.min(Math.max(MIN_VELOCITY, Math.abs(velocity)), MAX_VELOCITY);
+
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mDuration = 0.15f + (velocity * 0.02f);
+
+        // The glow depends more on the velocity, and therefore starts out
+        // nearly invisible.
+        mGlowAlphaStart = GLOW_ALPHA_START;
+        mGlowScaleYStart = Math.max(mGlowScaleY, 0.f);
+
+
+        // Growth for the size of the glow should be quadratic to properly
+        // respond
+        // to a user's scrolling speed. The faster the scrolling speed, the more
+        // intense the effect should be for both the size and the saturation.
+        mGlowScaleYFinish = Math.min(0.025f + (velocity * (velocity / 100) * 0.00015f) / 2, 1.f);
+        // Alpha should change for the glow as well as size.
+        mGlowAlphaFinish = Math.max(
+                mGlowAlphaStart, Math.min(velocity * VELOCITY_GLOW_FACTOR * .00001f, MAX_ALPHA));
+        mTargetDisplacement = 0.5f;
+    }
+
+    /**
+     * Set the color of this edge effect in argb.
+     *
+     * @param color Color in argb
+     */
+    public void setColor(@ColorInt int color) {
+        mPaint.setColor(color);
+    }
+
+    /**
+     * Return the color of this edge effect in argb.
+     * @return The color of this edge effect in argb
+     */
+    @ColorInt
+    public int getColor() {
+        return mPaint.getColor();
+    }
+
+    /**
+     * Draw into the provided canvas. Assumes that the canvas has been rotated
+     * accordingly and the size has been set. The effect will be drawn the full
+     * width of X=0 to X=width, beginning from Y=0 and extending to some factor <
+     * 1.f of height.
+     *
+     * @param canvas Canvas to draw into
+     * @return true if drawing should continue beyond this frame to continue the
+     *         animation
+     */
+    public boolean draw(Canvas canvas) {
+        update();
+
+        final int count = canvas.save();
+
+        final float centerX = mBounds.centerX();
+        final float centerY = mBounds.height() - mRadius;
+
+        canvas.scale(1.f, Math.min(mGlowScaleY, 1.f) * mBaseGlowScale, centerX, 0);
+
+        final float displacement = Math.max(0, Math.min(mDisplacement, 1.f)) - 0.5f;
+        float translateX = mBounds.width() * displacement / 2;
+
+        canvas.clipRect(mBounds);
+        canvas.translate(translateX, 0);
+        mPaint.setAlpha((int) (0xff * mGlowAlpha));
+        canvas.drawCircle(centerX, centerY, mRadius, mPaint);
+        canvas.restoreToCount(count);
+
+        boolean oneLastFrame = false;
+        if (mState == STATE_RECEDE && mGlowScaleY == 0) {
+            mState = STATE_IDLE;
+            oneLastFrame = true;
+        }
+
+        return mState != STATE_IDLE || oneLastFrame;
+    }
+
+    /**
+     * Return the maximum height that the edge effect will be drawn at given the original
+     * {@link #setSize(int, int) input size}.
+     * @return The maximum height of the edge effect
+     */
+    public int getMaxHeight() {
+        return (int) (mBounds.height() * MAX_GLOW_SCALE + 0.5f);
+    }
+
+    private void update() {
+        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final float t = Math.min((time - mStartTime) / mDuration, 1.f);
+
+        final float interp = mInterpolator.getInterpolation(t);
+
+        mGlowAlpha = mGlowAlphaStart + (mGlowAlphaFinish - mGlowAlphaStart) * interp;
+        mGlowScaleY = mGlowScaleYStart + (mGlowScaleYFinish - mGlowScaleYStart) * interp;
+        mDisplacement = (mDisplacement + mTargetDisplacement) / 2;
+
+        if (t >= 1.f - EPSILON) {
+            switch (mState) {
+                case STATE_ABSORB:
+                    mState = STATE_RECEDE;
+                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mDuration = RECEDE_TIME;
+
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After absorb, the glow should fade to nothing.
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL:
+                    mState = STATE_PULL_DECAY;
+                    mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                    mDuration = PULL_DECAY_TIME;
+
+                    mGlowAlphaStart = mGlowAlpha;
+                    mGlowScaleYStart = mGlowScaleY;
+
+                    // After pull, the glow should fade to nothing.
+                    mGlowAlphaFinish = 0.f;
+                    mGlowScaleYFinish = 0.f;
+                    break;
+                case STATE_PULL_DECAY:
+                    mState = STATE_RECEDE;
+                    break;
+                case STATE_RECEDE:
+                    mState = STATE_IDLE;
+                    break;
+            }
+        }
+    }
+}
diff --git a/android/widget/EditText.java b/android/widget/EditText.java
new file mode 100644
index 0000000..56c3e4a
--- /dev/null
+++ b/android/widget/EditText.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.method.MovementMethod;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+/*
+ * This is supposed to be a *very* thin veneer over TextView.
+ * Do not make any changes here that do anything that a TextView
+ * with a key listener and a movement method wouldn't do!
+ */
+
+/**
+ * A user interface element for entering and modifying text.
+ * When you define an edit text widget, you must specify the
+ * {@link android.R.styleable#TextView_inputType}
+ * attribute. For example, for plain text input set inputType to "text":
+ * <p>
+ * <pre>
+ * &lt;EditText
+ *     android:id="@+id/plain_text_input"
+ *     android:layout_height="wrap_content"
+ *     android:layout_width="match_parent"
+ *     android:inputType="text"/&gt;</pre>
+ *
+ * Choosing the input type configures the keyboard type that is shown, acceptable characters,
+ * and appearance of the edit text.
+ * For example, if you want to accept a secret number, like a unique pin or serial number,
+ * you can set inputType to "numericPassword".
+ * An inputType of "numericPassword" results in an edit text that accepts numbers only,
+ * shows a numeric keyboard when focused, and masks the text that is entered for privacy.
+ * <p>
+ * See the <a href="{@docRoot}guide/topics/ui/controls/text.html">Text Fields</a>
+ * guide for examples of other
+ * {@link android.R.styleable#TextView_inputType} settings.
+ * </p>
+ * <p>You also can receive callbacks as a user changes text by
+ * adding a {@link android.text.TextWatcher} to the edit text.
+ * This is useful when you want to add auto-save functionality as changes are made,
+ * or validate the format of user input, for example.
+ * You add a text watcher using the {@link TextView#addTextChangedListener} method.
+ * </p>
+ * <p>
+ * This widget does not support auto-sizing text.
+ * <p>
+ * <b>XML attributes</b>
+ * <p>
+ * See {@link android.R.styleable#EditText EditText Attributes},
+ * {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ */
+public class EditText extends TextView {
+    public EditText(Context context) {
+        this(context, null);
+    }
+
+    public EditText(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.editTextStyle);
+    }
+
+    public EditText(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public EditText(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    public boolean getFreezesText() {
+        return true;
+    }
+
+    @Override
+    protected boolean getDefaultEditable() {
+        return true;
+    }
+
+    @Override
+    protected MovementMethod getDefaultMovementMethod() {
+        return ArrowKeyMovementMethod.getInstance();
+    }
+
+    @Override
+    public Editable getText() {
+        return (Editable) super.getText();
+    }
+
+    @Override
+    public void setText(CharSequence text, BufferType type) {
+        super.setText(text, BufferType.EDITABLE);
+    }
+
+    /**
+     * Convenience for {@link Selection#setSelection(Spannable, int, int)}.
+     */
+    public void setSelection(int start, int stop) {
+        Selection.setSelection(getText(), start, stop);
+    }
+
+    /**
+     * Convenience for {@link Selection#setSelection(Spannable, int)}.
+     */
+    public void setSelection(int index) {
+        Selection.setSelection(getText(), index);
+    }
+
+    /**
+     * Convenience for {@link Selection#selectAll}.
+     */
+    public void selectAll() {
+        Selection.selectAll(getText());
+    }
+
+    /**
+     * Convenience for {@link Selection#extendSelection}.
+     */
+    public void extendSelection(int index) {
+        Selection.extendSelection(getText(), index);
+    }
+
+    /**
+     * Causes words in the text that are longer than the view's width to be ellipsized instead of
+     * broken in the middle. {@link TextUtils.TruncateAt#MARQUEE
+     * TextUtils.TruncateAt#MARQUEE} is not supported.
+     *
+     * @param ellipsis Type of ellipsis to be applied.
+     * @throws IllegalArgumentException When the value of <code>ellipsis</code> parameter is
+     *      {@link TextUtils.TruncateAt#MARQUEE}.
+     * @see TextView#setEllipsize(TextUtils.TruncateAt)
+     */
+    @Override
+    public void setEllipsize(TextUtils.TruncateAt ellipsis) {
+        if (ellipsis == TextUtils.TruncateAt.MARQUEE) {
+            throw new IllegalArgumentException("EditText cannot use the ellipsize mode "
+                    + "TextUtils.TruncateAt.MARQUEE");
+        }
+        super.setEllipsize(ellipsis);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return EditText.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    protected boolean supportsAutoSizeText() {
+        return false;
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (isEnabled()) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+        }
+    }
+}
diff --git a/android/widget/EditTextBackspacePerfTest.java b/android/widget/EditTextBackspacePerfTest.java
new file mode 100644
index 0000000..40b56f4
--- /dev/null
+++ b/android/widget/EditTextBackspacePerfTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.text.Selection;
+import android.view.KeyEvent;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextBackspacePerfTest {
+
+    private static final String BOY = "\uD83D\uDC66";  // U+1F466
+    private static final String US_FLAG = "\uD83C\uDDFA\uD83C\uDDF8";  // U+1F1FA U+1F1F8
+    private static final String FAMILY =
+            // U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467
+            "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67";
+    private static final String EMOJI_MODIFIER = "\uD83C\uDFFD";  // U+1F3FD
+    private static final String KEYCAP = "\u20E3";
+    private static final String COLOR_COPYRIGHT = "\u00A9\uFE0F";
+
+    @Parameters(name = "{0}")
+    public static Collection cases() {
+        return Arrays.asList(new Object[][] {
+            { "Latin", "aaa", 1 },
+            { "Flags", US_FLAG + US_FLAG + US_FLAG, 4 },
+            { "EmojiModifier",
+                BOY + EMOJI_MODIFIER + BOY + EMOJI_MODIFIER + BOY + EMOJI_MODIFIER, 4 },
+            { "KeyCap", "1" + KEYCAP + "1" + KEYCAP + "1" + KEYCAP, 2 },
+            { "ZwjSequence", FAMILY + FAMILY + FAMILY, 11 },
+            { "VariationSelector", COLOR_COPYRIGHT + COLOR_COPYRIGHT + COLOR_COPYRIGHT, 2 },
+        });
+    }
+
+    private final String mMetricKey;
+    private final String mText;
+    private final int mCursorPos;
+
+    private static final KeyEvent BACKSPACE_KEY_EVENT =
+            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL);
+    private static final KeyEvent RIGHT_ARROW_KEY_EVENT =
+            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
+
+    public EditTextBackspacePerfTest(String metricKey, String text, int cursorPos) {
+        mMetricKey = metricKey;
+        mText = text;
+        mCursorPos = cursorPos;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    private void prepareTextForBackspace(EditText editText) {
+        editText.setText(mText, TextView.BufferType.EDITABLE);
+        Selection.setSelection(editText.getText(), 0, 0);
+
+        // Do layout it here since the cursor movement requires layout information but it
+        // happens asynchronously even if the view is attached to an Activity.
+        editText.setLayoutParams(new ViewGroup.LayoutParams(
+                ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT));
+        editText.invalidate();
+        editText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+        editText.layout(0, 0, 1024, 768);
+
+        // mText contains three grapheme clusters. Move the cursor to the 2nd grapheme
+        // cluster by forwarding right arrow key event.
+        editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+        Assert.assertEquals(mCursorPos, Selection.getSelectionStart(editText.getText()));
+    }
+
+    @Test
+    public void testBackspace() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> {
+            EditText editText = new EditText(mActivityRule.getActivity());
+
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+            while (state.keepRunning()) {
+                // Prepare the test data for this iteration with pausing timer.
+                state.pauseTiming();
+                prepareTextForBackspace(editText);
+                state.resumeTiming();
+
+                editText.onKeyDown(BACKSPACE_KEY_EVENT.getKeyCode(), BACKSPACE_KEY_EVENT);
+            }
+        });
+    }
+}
diff --git a/android/widget/EditTextCursorMovementPerfTest.java b/android/widget/EditTextCursorMovementPerfTest.java
new file mode 100644
index 0000000..b100acb
--- /dev/null
+++ b/android/widget/EditTextCursorMovementPerfTest.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.text.Selection;
+import android.view.KeyEvent;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.runner.AndroidJUnit4;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+
+import org.junit.Assert;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runners.Parameterized;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextCursorMovementPerfTest {
+
+    private static final String BOY = "\uD83D\uDC66";  // U+1F466
+    private static final String US_FLAG = "\uD83C\uDDFA\uD83C\uDDF8";  // U+1F1FA U+1F1F8
+    private static final String FAMILY =
+            // U+1F469 U+200D U+1F469 U+200D U+1F467 U+200D U+1F467
+            "\uD83D\uDC69\u200D\uD83D\uDC69\u200D\uD83D\uDC67\u200D\uD83D\uDC67";
+
+    @Parameters(name = "{0}")
+    public static Collection cases() {
+        return Arrays.asList(new Object[][] {
+            { "Latin", "aaa", 1 },
+            { "Emoji", BOY + BOY + BOY, 2 },
+            { "Flags", US_FLAG + US_FLAG + US_FLAG, 4 },
+            { "ZwjSequence", FAMILY + FAMILY + FAMILY, 11 },
+        });
+    }
+
+    private final String mMetricKey;
+    private final String mText;
+    private final int mCursorPos;
+
+    private static final KeyEvent LEFT_ARROW_KEY_EVENT =
+            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_LEFT);
+    private static final KeyEvent RIGHT_ARROW_KEY_EVENT =
+            new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DPAD_RIGHT);
+
+    public EditTextCursorMovementPerfTest(String metricKey, String text, int cursorPos) {
+        mMetricKey = metricKey;
+        mText = text;
+        mCursorPos = cursorPos;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testCursorMovement() {
+        InstrumentationRegistry.getInstrumentation().runOnMainSync(new Runnable() {
+            @Override
+            public void run() {
+                EditText editText = new EditText(mActivityRule.getActivity());
+
+                editText.setText(mText, TextView.BufferType.EDITABLE);
+                Selection.setSelection(editText.getText(), 0, 0);
+
+                // Layout it here since the cursor movement requires layout information but it
+                // happens asynchronously even if the view is attached to an Activity.
+                editText.setLayoutParams(new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.WRAP_CONTENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT));
+                editText.invalidate();
+                editText.measure(MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                                 MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                editText.layout(0, 0, 1024, 768);
+
+                // mText contains three grapheme clusters. Move the cursor to the 2nd grapheme
+                // cluster by forwarding right arrow key event.
+                editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+                Assert.assertEquals(mCursorPos, Selection.getSelectionStart(editText.getText()));
+
+                BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+                while (state.keepRunning()) {
+                    editText.onKeyDown(RIGHT_ARROW_KEY_EVENT.getKeyCode(), RIGHT_ARROW_KEY_EVENT);
+                    editText.onKeyDown(LEFT_ARROW_KEY_EVENT.getKeyCode(), LEFT_ARROW_KEY_EVENT);
+                }
+            }
+        });
+    }
+}
diff --git a/android/widget/EditTextLongTextPerfTest.java b/android/widget/EditTextLongTextPerfTest.java
new file mode 100644
index 0000000..ce0c357
--- /dev/null
+++ b/android/widget/EditTextLongTextPerfTest.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Locale;
+import java.util.Random;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import android.app.Activity;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.util.Log;
+import android.view.KeyEvent;
+import android.view.RenderNodeAnimator;
+import android.view.ViewGroup;
+import android.view.View.MeasureSpec;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class EditTextLongTextPerfTest {
+    @Parameters(name = "{0}")
+    public static Collection cases() {
+        return Arrays.asList(new Object[][] {
+            { "10x30K", 10, 30000 },
+            { "300x1K", 300, 1000 },
+        });
+    }
+
+    private final String mMetricKey;
+    private final int mChars;
+    private final int mLines;
+
+    public EditTextLongTextPerfTest(String metricKey, int chars, int lines) {
+        mMetricKey = metricKey;
+        mChars = chars;
+        mLines = lines;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    private EditText setupEditText() {
+        final EditText editText = new EditText(mActivityRule.getActivity());
+
+        String alphabet = "abcdefghijklmnopqrstuvwxyz";
+        final long seed = 1234567890;
+        Random r = new Random(seed);
+        StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < mLines; i++) {
+            for (int j = 0; j < mChars; j++) {
+                char c = alphabet.charAt(r.nextInt(alphabet.length()));
+                sb.append(c);
+            }
+            sb.append('\n');
+        }
+
+        final int height = 1000;
+        final int width = 1000;
+        editText.setHeight(height);
+        editText.setWidth(width);
+        editText.setLayoutParams(new ViewGroup.LayoutParams(width, height));
+
+        Activity activity = mActivityRule.getActivity();
+        activity.setContentView(editText);
+
+        editText.setText(sb.toString(), TextView.BufferType.EDITABLE);
+        editText.invalidate();
+        editText.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                         MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+        editText.layout(0, 0, height, width);
+
+        return editText;
+    }
+
+    @Test
+    public void testEditText() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+            final EditText editText = setupEditText();
+            final KeyEvent keyEvent = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER);
+            final int steps = 100;
+            while (state.keepRunning()) {
+                for (int i = 0; i < steps; i++) {
+                    int offset = (editText.getText().length() * i) / steps;
+                    editText.setSelection(offset);
+                    editText.bringPointIntoView(offset);
+                    editText.onKeyDown(keyEvent.getKeyCode(), keyEvent);
+                    editText.updateDisplayListIfDirty();
+                }
+            }
+        });
+    }
+}
diff --git a/android/widget/Editor.java b/android/widget/Editor.java
new file mode 100644
index 0000000..d23dfe4
--- /dev/null
+++ b/android/widget/Editor.java
@@ -0,0 +1,6494 @@
+/*
+ * 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 android.widget;
+
+import android.R;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.PendingIntent.CanceledException;
+import android.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UndoManager;
+import android.content.UndoOperation;
+import android.content.UndoOwner;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.drawable.ColorDrawable;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableParcel;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.StaticLayout;
+import android.text.TextUtils;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.WordIterator;
+import android.text.style.EasyEditSpan;
+import android.text.style.SuggestionRangeSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.TextAppearanceSpan;
+import android.text.style.URLSpan;
+import android.util.ArraySet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.SparseArray;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.ContextMenu;
+import android.view.ContextThemeWrapper;
+import android.view.DisplayListCanvas;
+import android.view.DragAndDropPermissions;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.RenderNode;
+import android.view.SubMenu;
+import android.view.View;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.textclassifier.TextClassification;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView.Drawables;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+import com.android.internal.util.Preconditions;
+import com.android.internal.widget.EditableInputConnection;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+
+/**
+ * Helper class used by TextView to handle editable text views.
+ *
+ * @hide
+ */
+public class Editor {
+    private static final String TAG = "Editor";
+    private static final boolean DEBUG_UNDO = false;
+
+    static final int BLINK = 500;
+    private static final int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+    private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
+    private static final int UNSET_X_VALUE = -1;
+    private static final int UNSET_LINE = -1;
+    // Tag used when the Editor maintains its own separate UndoManager.
+    private static final String UNDO_OWNER_TAG = "Editor";
+
+    // Ordering constants used to place the Action Mode or context menu items in their menu.
+    private static final int MENU_ITEM_ORDER_ASSIST = 0;
+    private static final int MENU_ITEM_ORDER_UNDO = 2;
+    private static final int MENU_ITEM_ORDER_REDO = 3;
+    private static final int MENU_ITEM_ORDER_CUT = 4;
+    private static final int MENU_ITEM_ORDER_COPY = 5;
+    private static final int MENU_ITEM_ORDER_PASTE = 6;
+    private static final int MENU_ITEM_ORDER_SHARE = 7;
+    private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
+    private static final int MENU_ITEM_ORDER_REPLACE = 9;
+    private static final int MENU_ITEM_ORDER_AUTOFILL = 10;
+    private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 11;
+    private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 100;
+
+    // Each Editor manages its own undo stack.
+    private final UndoManager mUndoManager = new UndoManager();
+    private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+    final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
+    boolean mAllowUndo = true;
+
+    private final MetricsLogger mMetricsLogger = new MetricsLogger();
+
+    // Cursor Controllers.
+    private InsertionPointCursorController mInsertionPointCursorController;
+    SelectionModifierCursorController mSelectionModifierCursorController;
+    // Action mode used when text is selected or when actions on an insertion cursor are triggered.
+    private ActionMode mTextActionMode;
+    private boolean mInsertionControllerEnabled;
+    private boolean mSelectionControllerEnabled;
+
+    private final boolean mHapticTextHandleEnabled;
+
+    // Used to highlight a word when it is corrected by the IME
+    private CorrectionHighlighter mCorrectionHighlighter;
+
+    InputContentType mInputContentType;
+    InputMethodState mInputMethodState;
+
+    private static class TextRenderNode {
+        // Render node has 3 recording states:
+        // 1. Recorded operations are valid.
+        // #needsRecord() returns false, but needsToBeShifted is false.
+        // 2. Recorded operations are not valid, but just the position needed to be updated.
+        // #needsRecord() returns false, but needsToBeShifted is true.
+        // 3. Recorded operations are not valid. Need to record operations. #needsRecord() returns
+        // true.
+        RenderNode renderNode;
+        boolean isDirty;
+        // Becomes true when recorded operations can be reused, but the position has to be updated.
+        boolean needsToBeShifted;
+        public TextRenderNode(String name) {
+            renderNode = RenderNode.create(name, null);
+            isDirty = true;
+            needsToBeShifted = true;
+        }
+        boolean needsRecord() {
+            return isDirty || !renderNode.isValid();
+        }
+    }
+    private TextRenderNode[] mTextRenderNodes;
+
+    boolean mFrozenWithFocus;
+    boolean mSelectionMoved;
+    boolean mTouchFocusSelected;
+
+    KeyListener mKeyListener;
+    int mInputType = EditorInfo.TYPE_NULL;
+
+    boolean mDiscardNextActionUp;
+    boolean mIgnoreActionUpEvent;
+
+    long mShowCursor;
+    private Blink mBlink;
+
+    boolean mCursorVisible = true;
+    boolean mSelectAllOnFocus;
+    boolean mTextIsSelectable;
+
+    CharSequence mError;
+    boolean mErrorWasChanged;
+    private ErrorPopup mErrorPopup;
+
+    /**
+     * This flag is set if the TextView tries to display an error before it
+     * is attached to the window (so its position is still unknown).
+     * It causes the error to be shown later, when onAttachedToWindow()
+     * is called.
+     */
+    private boolean mShowErrorAfterAttach;
+
+    boolean mInBatchEditControllers;
+    boolean mShowSoftInputOnFocus = true;
+    private boolean mPreserveSelection;
+    private boolean mRestartActionModeOnNextRefresh;
+
+    private SelectionActionModeHelper mSelectionActionModeHelper;
+
+    boolean mIsBeingLongClicked;
+
+    private SuggestionsPopupWindow mSuggestionsPopupWindow;
+    SuggestionRangeSpan mSuggestionRangeSpan;
+    private Runnable mShowSuggestionRunnable;
+
+    Drawable mCursorDrawable = null;
+
+    private Drawable mSelectHandleLeft;
+    private Drawable mSelectHandleRight;
+    private Drawable mSelectHandleCenter;
+
+    // Global listener that detects changes in the global position of the TextView
+    private PositionListener mPositionListener;
+
+    private float mLastDownPositionX, mLastDownPositionY;
+    private float mLastUpPositionX, mLastUpPositionY;
+    private float mContextMenuAnchorX, mContextMenuAnchorY;
+    Callback mCustomSelectionActionModeCallback;
+    Callback mCustomInsertionActionModeCallback;
+
+    // Set when this TextView gained focus with some text selected. Will start selection mode.
+    boolean mCreatedWithASelection;
+
+    // Indicates the current tap state (first tap, double tap, or triple click).
+    private int mTapState = TAP_STATE_INITIAL;
+    private long mLastTouchUpTime = 0;
+    private static final int TAP_STATE_INITIAL = 0;
+    private static final int TAP_STATE_FIRST_TAP = 1;
+    private static final int TAP_STATE_DOUBLE_TAP = 2;
+    // Only for mouse input.
+    private static final int TAP_STATE_TRIPLE_CLICK = 3;
+
+    // The button state as of the last time #onTouchEvent is called.
+    private int mLastButtonState;
+
+    private Runnable mInsertionActionModeRunnable;
+
+    // The span controller helps monitoring the changes to which the Editor needs to react:
+    // - EasyEditSpans, for which we have some UI to display on attach and on hide
+    // - SelectionSpans, for which we need to call updateSelection if an IME is attached
+    private SpanController mSpanController;
+
+    private WordIterator mWordIterator;
+    SpellChecker mSpellChecker;
+
+    // This word iterator is set with text and used to determine word boundaries
+    // when a user is selecting text.
+    private WordIterator mWordIteratorWithText;
+    // Indicate that the text in the word iterator needs to be updated.
+    private boolean mUpdateWordIteratorText;
+
+    private Rect mTempRect;
+
+    private final TextView mTextView;
+
+    final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
+
+    private final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier =
+            new CursorAnchorInfoNotifier();
+
+    private final Runnable mShowFloatingToolbar = new Runnable() {
+        @Override
+        public void run() {
+            if (mTextActionMode != null) {
+                mTextActionMode.hide(0);  // hide off.
+            }
+        }
+    };
+
+    boolean mIsInsertionActionModeStartPending = false;
+
+    private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
+
+    Editor(TextView textView) {
+        mTextView = textView;
+        // Synchronize the filter list, which places the undo input filter at the end.
+        mTextView.setFilters(mTextView.getFilters());
+        mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
+        mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
+                com.android.internal.R.bool.config_enableHapticTextHandle);
+    }
+
+    ParcelableParcel saveInstanceState() {
+        ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
+        Parcel parcel = state.getParcel();
+        mUndoManager.saveInstanceState(parcel);
+        mUndoInputFilter.saveInstanceState(parcel);
+        return state;
+    }
+
+    void restoreInstanceState(ParcelableParcel state) {
+        Parcel parcel = state.getParcel();
+        mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
+        mUndoInputFilter.restoreInstanceState(parcel);
+        // Re-associate this object as the owner of undo state.
+        mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
+    }
+
+    /**
+     * Forgets all undo and redo operations for this Editor.
+     */
+    void forgetUndoRedo() {
+        UndoOwner[] owners = { mUndoOwner };
+        mUndoManager.forgetUndos(owners, -1 /* all */);
+        mUndoManager.forgetRedos(owners, -1 /* all */);
+    }
+
+    boolean canUndo() {
+        UndoOwner[] owners = { mUndoOwner };
+        return mAllowUndo && mUndoManager.countUndos(owners) > 0;
+    }
+
+    boolean canRedo() {
+        UndoOwner[] owners = { mUndoOwner };
+        return mAllowUndo && mUndoManager.countRedos(owners) > 0;
+    }
+
+    void undo() {
+        if (!mAllowUndo) {
+            return;
+        }
+        UndoOwner[] owners = { mUndoOwner };
+        mUndoManager.undo(owners, 1);  // Undo 1 action.
+    }
+
+    void redo() {
+        if (!mAllowUndo) {
+            return;
+        }
+        UndoOwner[] owners = { mUndoOwner };
+        mUndoManager.redo(owners, 1);  // Redo 1 action.
+    }
+
+    void replace() {
+        if (mSuggestionsPopupWindow == null) {
+            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+        }
+        hideCursorAndSpanControllers();
+        mSuggestionsPopupWindow.show();
+
+        int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+        Selection.setSelection((Spannable) mTextView.getText(), middle);
+    }
+
+    void onAttachedToWindow() {
+        if (mShowErrorAfterAttach) {
+            showError();
+            mShowErrorAfterAttach = false;
+        }
+
+        final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+        // No need to create the controller.
+        // The get method will add the listener on controller creation.
+        if (mInsertionPointCursorController != null) {
+            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+        }
+        if (mSelectionModifierCursorController != null) {
+            mSelectionModifierCursorController.resetTouchOffsets();
+            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+        }
+        updateSpellCheckSpans(0, mTextView.getText().length(),
+                true /* create the spell checker if needed */);
+
+        if (mTextView.hasSelection()) {
+            refreshTextActionMode();
+        }
+
+        getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
+        resumeBlink();
+    }
+
+    void onDetachedFromWindow() {
+        getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
+
+        if (mError != null) {
+            hideError();
+        }
+
+        suspendBlink();
+
+        if (mInsertionPointCursorController != null) {
+            mInsertionPointCursorController.onDetached();
+        }
+
+        if (mSelectionModifierCursorController != null) {
+            mSelectionModifierCursorController.onDetached();
+        }
+
+        if (mShowSuggestionRunnable != null) {
+            mTextView.removeCallbacks(mShowSuggestionRunnable);
+        }
+
+        // Cancel the single tap delayed runnable.
+        if (mInsertionActionModeRunnable != null) {
+            mTextView.removeCallbacks(mInsertionActionModeRunnable);
+        }
+
+        mTextView.removeCallbacks(mShowFloatingToolbar);
+
+        discardTextDisplayLists();
+
+        if (mSpellChecker != null) {
+            mSpellChecker.closeSession();
+            // Forces the creation of a new SpellChecker next time this window is created.
+            // Will handle the cases where the settings has been changed in the meantime.
+            mSpellChecker = null;
+        }
+
+        hideCursorAndSpanControllers();
+        stopTextActionModeWithPreservingSelection();
+    }
+
+    private void discardTextDisplayLists() {
+        if (mTextRenderNodes != null) {
+            for (int i = 0; i < mTextRenderNodes.length; i++) {
+                RenderNode displayList = mTextRenderNodes[i] != null
+                        ? mTextRenderNodes[i].renderNode : null;
+                if (displayList != null && displayList.isValid()) {
+                    displayList.discardDisplayList();
+                }
+            }
+        }
+    }
+
+    private void showError() {
+        if (mTextView.getWindowToken() == null) {
+            mShowErrorAfterAttach = true;
+            return;
+        }
+
+        if (mErrorPopup == null) {
+            LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
+            final TextView err = (TextView) inflater.inflate(
+                    com.android.internal.R.layout.textview_hint, null);
+
+            final float scale = mTextView.getResources().getDisplayMetrics().density;
+            mErrorPopup =
+                    new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
+            mErrorPopup.setFocusable(false);
+            // The user is entering text, so the input method is needed.  We
+            // don't want the popup to be displayed on top of it.
+            mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+        }
+
+        TextView tv = (TextView) mErrorPopup.getContentView();
+        chooseSize(mErrorPopup, mError, tv);
+        tv.setText(mError);
+
+        mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
+        mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
+    }
+
+    public void setError(CharSequence error, Drawable icon) {
+        mError = TextUtils.stringOrSpannedString(error);
+        mErrorWasChanged = true;
+
+        if (mError == null) {
+            setErrorIcon(null);
+            if (mErrorPopup != null) {
+                if (mErrorPopup.isShowing()) {
+                    mErrorPopup.dismiss();
+                }
+
+                mErrorPopup = null;
+            }
+            mShowErrorAfterAttach = false;
+        } else {
+            setErrorIcon(icon);
+            if (mTextView.isFocused()) {
+                showError();
+            }
+        }
+    }
+
+    private void setErrorIcon(Drawable icon) {
+        Drawables dr = mTextView.mDrawables;
+        if (dr == null) {
+            mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
+        }
+        dr.setErrorDrawable(icon, mTextView);
+
+        mTextView.resetResolvedDrawables();
+        mTextView.invalidate();
+        mTextView.requestLayout();
+    }
+
+    private void hideError() {
+        if (mErrorPopup != null) {
+            if (mErrorPopup.isShowing()) {
+                mErrorPopup.dismiss();
+            }
+        }
+
+        mShowErrorAfterAttach = false;
+    }
+
+    /**
+     * Returns the X offset to make the pointy top of the error point
+     * at the middle of the error icon.
+     */
+    private int getErrorX() {
+        /*
+         * The "25" is the distance between the point and the right edge
+         * of the background
+         */
+        final float scale = mTextView.getResources().getDisplayMetrics().density;
+
+        final Drawables dr = mTextView.mDrawables;
+
+        final int layoutDirection = mTextView.getLayoutDirection();
+        int errorX;
+        int offset;
+        switch (layoutDirection) {
+            default:
+            case View.LAYOUT_DIRECTION_LTR:
+                offset = -(dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
+                errorX = mTextView.getWidth() - mErrorPopup.getWidth()
+                        - mTextView.getPaddingRight() + offset;
+                break;
+            case View.LAYOUT_DIRECTION_RTL:
+                offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
+                errorX = mTextView.getPaddingLeft() + offset;
+                break;
+        }
+        return errorX;
+    }
+
+    /**
+     * Returns the Y offset to make the pointy top of the error point
+     * at the bottom of the error icon.
+     */
+    private int getErrorY() {
+        /*
+         * Compound, not extended, because the icon is not clipped
+         * if the text height is smaller.
+         */
+        final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
+        int vspace = mTextView.getBottom() - mTextView.getTop()
+                - mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
+
+        final Drawables dr = mTextView.mDrawables;
+
+        final int layoutDirection = mTextView.getLayoutDirection();
+        int height;
+        switch (layoutDirection) {
+            default:
+            case View.LAYOUT_DIRECTION_LTR:
+                height = (dr != null ? dr.mDrawableHeightRight : 0);
+                break;
+            case View.LAYOUT_DIRECTION_RTL:
+                height = (dr != null ? dr.mDrawableHeightLeft : 0);
+                break;
+        }
+
+        int icontop = compoundPaddingTop + (vspace - height) / 2;
+
+        /*
+         * The "2" is the distance between the point and the top edge
+         * of the background.
+         */
+        final float scale = mTextView.getResources().getDisplayMetrics().density;
+        return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
+    }
+
+    void createInputContentTypeIfNeeded() {
+        if (mInputContentType == null) {
+            mInputContentType = new InputContentType();
+        }
+    }
+
+    void createInputMethodStateIfNeeded() {
+        if (mInputMethodState == null) {
+            mInputMethodState = new InputMethodState();
+        }
+    }
+
+    boolean isCursorVisible() {
+        // The default value is true, even when there is no associated Editor
+        return mCursorVisible && mTextView.isTextEditable();
+    }
+
+    void prepareCursorControllers() {
+        boolean windowSupportsHandles = false;
+
+        ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
+        if (params instanceof WindowManager.LayoutParams) {
+            WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
+            windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
+                    || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
+        }
+
+        boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
+        mInsertionControllerEnabled = enabled && isCursorVisible();
+        mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
+
+        if (!mInsertionControllerEnabled) {
+            hideInsertionPointCursorController();
+            if (mInsertionPointCursorController != null) {
+                mInsertionPointCursorController.onDetached();
+                mInsertionPointCursorController = null;
+            }
+        }
+
+        if (!mSelectionControllerEnabled) {
+            stopTextActionMode();
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.onDetached();
+                mSelectionModifierCursorController = null;
+            }
+        }
+    }
+
+    void hideInsertionPointCursorController() {
+        if (mInsertionPointCursorController != null) {
+            mInsertionPointCursorController.hide();
+        }
+    }
+
+    /**
+     * Hides the insertion and span controllers.
+     */
+    void hideCursorAndSpanControllers() {
+        hideCursorControllers();
+        hideSpanControllers();
+    }
+
+    private void hideSpanControllers() {
+        if (mSpanController != null) {
+            mSpanController.hide();
+        }
+    }
+
+    private void hideCursorControllers() {
+        // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
+        // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
+        // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
+        // to distinguish one from the other.
+        if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode())
+                || !mSuggestionsPopupWindow.isShowingUp())) {
+            // Should be done before hide insertion point controller since it triggers a show of it
+            mSuggestionsPopupWindow.hide();
+        }
+        hideInsertionPointCursorController();
+    }
+
+    /**
+     * Create new SpellCheckSpans on the modified region.
+     */
+    private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
+        // Remove spans whose adjacent characters are text not punctuation
+        mTextView.removeAdjacentSuggestionSpans(start);
+        mTextView.removeAdjacentSuggestionSpans(end);
+
+        if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled()
+                && !(mTextView.isInExtractedMode())) {
+            if (mSpellChecker == null && createSpellChecker) {
+                mSpellChecker = new SpellChecker(mTextView);
+            }
+            if (mSpellChecker != null) {
+                mSpellChecker.spellCheck(start, end);
+            }
+        }
+    }
+
+    void onScreenStateChanged(int screenState) {
+        switch (screenState) {
+            case View.SCREEN_STATE_ON:
+                resumeBlink();
+                break;
+            case View.SCREEN_STATE_OFF:
+                suspendBlink();
+                break;
+        }
+    }
+
+    private void suspendBlink() {
+        if (mBlink != null) {
+            mBlink.cancel();
+        }
+    }
+
+    private void resumeBlink() {
+        if (mBlink != null) {
+            mBlink.uncancel();
+            makeBlink();
+        }
+    }
+
+    void adjustInputType(boolean password, boolean passwordInputType,
+            boolean webPasswordInputType, boolean numberPasswordInputType) {
+        // mInputType has been set from inputType, possibly modified by mInputMethod.
+        // Specialize mInputType to [web]password if we have a text class and the original input
+        // type was a password.
+        if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
+            if (password || passwordInputType) {
+                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+                        | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+            }
+            if (webPasswordInputType) {
+                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+                        | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
+            }
+        } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
+            if (numberPasswordInputType) {
+                mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
+                        | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+            }
+        }
+    }
+
+    private void chooseSize(@NonNull PopupWindow pop, @NonNull CharSequence text,
+            @NonNull TextView tv) {
+        final int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+        final int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+        final int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.textview_error_popup_default_width);
+        final StaticLayout l = StaticLayout.Builder.obtain(text, 0, text.length(), tv.getPaint(),
+                defaultWidthInPixels)
+                .setUseLineSpacingFromFallbacks(tv.mUseFallbackLineSpacing)
+                .build();
+
+        float max = 0;
+        for (int i = 0; i < l.getLineCount(); i++) {
+            max = Math.max(max, l.getLineWidth(i));
+        }
+
+        /*
+         * Now set the popup size to be big enough for the text plus the border capped
+         * to DEFAULT_MAX_POPUP_WIDTH
+         */
+        pop.setWidth(wid + (int) Math.ceil(max));
+        pop.setHeight(ht + l.getHeight());
+    }
+
+    void setFrame() {
+        if (mErrorPopup != null) {
+            TextView tv = (TextView) mErrorPopup.getContentView();
+            chooseSize(mErrorPopup, mError, tv);
+            mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
+                    mErrorPopup.getWidth(), mErrorPopup.getHeight());
+        }
+    }
+
+    private int getWordStart(int offset) {
+        // FIXME - For this and similar methods we're not doing anything to check if there's
+        // a LocaleSpan in the text, this may be something we should try handling or checking for.
+        int retOffset = getWordIteratorWithText().prevBoundary(offset);
+        if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
+            // On punctuation boundary or within group of punctuation, find punctuation start.
+            retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
+        } else {
+            // Not on a punctuation boundary, find the word start.
+            retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
+        }
+        if (retOffset == BreakIterator.DONE) {
+            return offset;
+        }
+        return retOffset;
+    }
+
+    private int getWordEnd(int offset) {
+        int retOffset = getWordIteratorWithText().nextBoundary(offset);
+        if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
+            // On punctuation boundary or within group of punctuation, find punctuation end.
+            retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
+        } else {
+            // Not on a punctuation boundary, find the word end.
+            retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
+        }
+        if (retOffset == BreakIterator.DONE) {
+            return offset;
+        }
+        return retOffset;
+    }
+
+    private boolean needsToSelectAllToSelectWordOrParagraph() {
+        if (mTextView.hasPasswordTransformationMethod()) {
+            // Always select all on a password field.
+            // Cut/copy menu entries are not available for passwords, but being able to select all
+            // is however useful to delete or paste to replace the entire content.
+            return true;
+        }
+
+        int inputType = mTextView.getInputType();
+        int klass = inputType & InputType.TYPE_MASK_CLASS;
+        int variation = inputType & InputType.TYPE_MASK_VARIATION;
+
+        // Specific text field types: select the entire text for these
+        if (klass == InputType.TYPE_CLASS_NUMBER
+                || klass == InputType.TYPE_CLASS_PHONE
+                || klass == InputType.TYPE_CLASS_DATETIME
+                || variation == InputType.TYPE_TEXT_VARIATION_URI
+                || variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+                || variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS
+                || variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Adjusts selection to the word under last touch offset. Return true if the operation was
+     * successfully performed.
+     */
+    boolean selectCurrentWord() {
+        if (!mTextView.canSelectText()) {
+            return false;
+        }
+
+        if (needsToSelectAllToSelectWordOrParagraph()) {
+            return mTextView.selectAllText();
+        }
+
+        long lastTouchOffsets = getLastTouchOffsets();
+        final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+        final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+        // Safety check in case standard touch event handling has been bypassed
+        if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
+        if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
+
+        int selectionStart, selectionEnd;
+
+        // If a URLSpan (web address, email, phone...) is found at that position, select it.
+        URLSpan[] urlSpans =
+                ((Spanned) mTextView.getText()).getSpans(minOffset, maxOffset, URLSpan.class);
+        if (urlSpans.length >= 1) {
+            URLSpan urlSpan = urlSpans[0];
+            selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
+            selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
+        } else {
+            // FIXME - We should check if there's a LocaleSpan in the text, this may be
+            // something we should try handling or checking for.
+            final WordIterator wordIterator = getWordIterator();
+            wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
+
+            selectionStart = wordIterator.getBeginning(minOffset);
+            selectionEnd = wordIterator.getEnd(maxOffset);
+
+            if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE
+                    || selectionStart == selectionEnd) {
+                // Possible when the word iterator does not properly handle the text's language
+                long range = getCharClusterRange(minOffset);
+                selectionStart = TextUtils.unpackRangeStartFromLong(range);
+                selectionEnd = TextUtils.unpackRangeEndFromLong(range);
+            }
+        }
+
+        Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+        return selectionEnd > selectionStart;
+    }
+
+    /**
+     * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
+     * successfully performed.
+     */
+    private boolean selectCurrentParagraph() {
+        if (!mTextView.canSelectText()) {
+            return false;
+        }
+
+        if (needsToSelectAllToSelectWordOrParagraph()) {
+            return mTextView.selectAllText();
+        }
+
+        long lastTouchOffsets = getLastTouchOffsets();
+        final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
+        final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
+
+        final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
+        final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+        final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+        if (start < end) {
+            Selection.setSelection((Spannable) mTextView.getText(), start, end);
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Get the minimum range of paragraphs that contains startOffset and endOffset.
+     */
+    private long getParagraphsRange(int startOffset, int endOffset) {
+        final Layout layout = mTextView.getLayout();
+        if (layout == null) {
+            return TextUtils.packRangeInLong(-1, -1);
+        }
+        final CharSequence text = mTextView.getText();
+        int minLine = layout.getLineForOffset(startOffset);
+        // Search paragraph start.
+        while (minLine > 0) {
+            final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
+            if (text.charAt(prevLineEndOffset - 1) == '\n') {
+                break;
+            }
+            minLine--;
+        }
+        int maxLine = layout.getLineForOffset(endOffset);
+        // Search paragraph end.
+        while (maxLine < layout.getLineCount() - 1) {
+            final int lineEndOffset = layout.getLineEnd(maxLine);
+            if (text.charAt(lineEndOffset - 1) == '\n') {
+                break;
+            }
+            maxLine++;
+        }
+        return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
+    }
+
+    void onLocaleChanged() {
+        // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
+        // proper new locale
+        mWordIterator = null;
+        mWordIteratorWithText = null;
+    }
+
+    public WordIterator getWordIterator() {
+        if (mWordIterator == null) {
+            mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
+        }
+        return mWordIterator;
+    }
+
+    private WordIterator getWordIteratorWithText() {
+        if (mWordIteratorWithText == null) {
+            mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
+            mUpdateWordIteratorText = true;
+        }
+        if (mUpdateWordIteratorText) {
+            // FIXME - Shouldn't copy all of the text as only the area of the text relevant
+            // to the user's selection is needed. A possible solution would be to
+            // copy some number N of characters near the selection and then when the
+            // user approaches N then we'd do another copy of the next N characters.
+            CharSequence text = mTextView.getText();
+            mWordIteratorWithText.setCharSequence(text, 0, text.length());
+            mUpdateWordIteratorText = false;
+        }
+        return mWordIteratorWithText;
+    }
+
+    private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
+        final Layout layout = mTextView.getLayout();
+        if (layout == null) return offset;
+        return findAfterGivenOffset == layout.isRtlCharAt(offset)
+                ? layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
+    }
+
+    private long getCharClusterRange(int offset) {
+        final int textLength = mTextView.getText().length();
+        if (offset < textLength) {
+            final int clusterEndOffset = getNextCursorOffset(offset, true);
+            return TextUtils.packRangeInLong(
+                    getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
+        }
+        if (offset - 1 >= 0) {
+            final int clusterStartOffset = getNextCursorOffset(offset, false);
+            return TextUtils.packRangeInLong(clusterStartOffset,
+                    getNextCursorOffset(clusterStartOffset, true));
+        }
+        return TextUtils.packRangeInLong(offset, offset);
+    }
+
+    private boolean touchPositionIsInSelection() {
+        int selectionStart = mTextView.getSelectionStart();
+        int selectionEnd = mTextView.getSelectionEnd();
+
+        if (selectionStart == selectionEnd) {
+            return false;
+        }
+
+        if (selectionStart > selectionEnd) {
+            int tmp = selectionStart;
+            selectionStart = selectionEnd;
+            selectionEnd = tmp;
+            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+        }
+
+        SelectionModifierCursorController selectionController = getSelectionController();
+        int minOffset = selectionController.getMinTouchOffset();
+        int maxOffset = selectionController.getMaxTouchOffset();
+
+        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
+    }
+
+    private PositionListener getPositionListener() {
+        if (mPositionListener == null) {
+            mPositionListener = new PositionListener();
+        }
+        return mPositionListener;
+    }
+
+    private interface TextViewPositionListener {
+        public void updatePosition(int parentPositionX, int parentPositionY,
+                boolean parentPositionChanged, boolean parentScrolled);
+    }
+
+    private boolean isOffsetVisible(int offset) {
+        Layout layout = mTextView.getLayout();
+        if (layout == null) return false;
+
+        final int line = layout.getLineForOffset(offset);
+        final int lineBottom = layout.getLineBottom(line);
+        final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
+        return mTextView.isPositionVisible(
+                primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
+                lineBottom + mTextView.viewportToContentVerticalOffset());
+    }
+
+    /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
+     * in the view. Returns false when the position is in the empty space of left/right of text.
+     */
+    private boolean isPositionOnText(float x, float y) {
+        Layout layout = mTextView.getLayout();
+        if (layout == null) return false;
+
+        final int line = mTextView.getLineAtCoordinate(y);
+        x = mTextView.convertToLocalHorizontalCoordinate(x);
+
+        if (x < layout.getLineLeft(line)) return false;
+        if (x > layout.getLineRight(line)) return false;
+        return true;
+    }
+
+    private void startDragAndDrop() {
+        getSelectionActionModeHelper().onSelectionDrag();
+
+        // TODO: Fix drag and drop in full screen extracted mode.
+        if (mTextView.isInExtractedMode()) {
+            return;
+        }
+        final int start = mTextView.getSelectionStart();
+        final int end = mTextView.getSelectionEnd();
+        CharSequence selectedText = mTextView.getTransformedText(start, end);
+        ClipData data = ClipData.newPlainText(null, selectedText);
+        DragLocalState localState = new DragLocalState(mTextView, start, end);
+        mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
+                View.DRAG_FLAG_GLOBAL);
+        stopTextActionMode();
+        if (hasSelectionController()) {
+            getSelectionController().resetTouchOffsets();
+        }
+    }
+
+    public boolean performLongClick(boolean handled) {
+        // Long press in empty space moves cursor and starts the insertion action mode.
+        if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY)
+                && mInsertionControllerEnabled) {
+            final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
+                    mLastDownPositionY);
+            Selection.setSelection((Spannable) mTextView.getText(), offset);
+            getInsertionController().show();
+            mIsInsertionActionModeStartPending = true;
+            handled = true;
+            MetricsLogger.action(
+                    mTextView.getContext(),
+                    MetricsEvent.TEXT_LONGPRESS,
+                    TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
+        }
+
+        if (!handled && mTextActionMode != null) {
+            if (touchPositionIsInSelection()) {
+                startDragAndDrop();
+                MetricsLogger.action(
+                        mTextView.getContext(),
+                        MetricsEvent.TEXT_LONGPRESS,
+                        TextViewMetrics.SUBTYPE_LONG_PRESS_DRAG_AND_DROP);
+            } else {
+                stopTextActionMode();
+                selectCurrentWordAndStartDrag();
+                MetricsLogger.action(
+                        mTextView.getContext(),
+                        MetricsEvent.TEXT_LONGPRESS,
+                        TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
+            }
+            handled = true;
+        }
+
+        // Start a new selection
+        if (!handled) {
+            handled = selectCurrentWordAndStartDrag();
+            if (handled) {
+                MetricsLogger.action(
+                        mTextView.getContext(),
+                        MetricsEvent.TEXT_LONGPRESS,
+                        TextViewMetrics.SUBTYPE_LONG_PRESS_SELECTION);
+            }
+        }
+
+        return handled;
+    }
+
+    float getLastUpPositionX() {
+        return mLastUpPositionX;
+    }
+
+    float getLastUpPositionY() {
+        return mLastUpPositionY;
+    }
+
+    private long getLastTouchOffsets() {
+        SelectionModifierCursorController selectionController = getSelectionController();
+        final int minOffset = selectionController.getMinTouchOffset();
+        final int maxOffset = selectionController.getMaxTouchOffset();
+        return TextUtils.packRangeInLong(minOffset, maxOffset);
+    }
+
+    void onFocusChanged(boolean focused, int direction) {
+        mShowCursor = SystemClock.uptimeMillis();
+        ensureEndedBatchEdit();
+
+        if (focused) {
+            int selStart = mTextView.getSelectionStart();
+            int selEnd = mTextView.getSelectionEnd();
+
+            // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
+            // mode for these, unless there was a specific selection already started.
+            final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0
+                    && selEnd == mTextView.getText().length();
+
+            mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection()
+                    && !isFocusHighlighted;
+
+            if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
+                // If a tap was used to give focus to that view, move cursor at tap position.
+                // Has to be done before onTakeFocus, which can be overloaded.
+                final int lastTapPosition = getLastTapPosition();
+                if (lastTapPosition >= 0) {
+                    Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
+                }
+
+                // Note this may have to be moved out of the Editor class
+                MovementMethod mMovement = mTextView.getMovementMethod();
+                if (mMovement != null) {
+                    mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
+                }
+
+                // The DecorView does not have focus when the 'Done' ExtractEditText button is
+                // pressed. Since it is the ViewAncestor's mView, it requests focus before
+                // ExtractEditText clears focus, which gives focus to the ExtractEditText.
+                // This special case ensure that we keep current selection in that case.
+                // It would be better to know why the DecorView does not have focus at that time.
+                if (((mTextView.isInExtractedMode()) || mSelectionMoved)
+                        && selStart >= 0 && selEnd >= 0) {
+                    /*
+                     * Someone intentionally set the selection, so let them
+                     * do whatever it is that they wanted to do instead of
+                     * the default on-focus behavior.  We reset the selection
+                     * here instead of just skipping the onTakeFocus() call
+                     * because some movement methods do something other than
+                     * just setting the selection in theirs and we still
+                     * need to go through that path.
+                     */
+                    Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+                }
+
+                if (mSelectAllOnFocus) {
+                    mTextView.selectAllText();
+                }
+
+                mTouchFocusSelected = true;
+            }
+
+            mFrozenWithFocus = false;
+            mSelectionMoved = false;
+
+            if (mError != null) {
+                showError();
+            }
+
+            makeBlink();
+        } else {
+            if (mError != null) {
+                hideError();
+            }
+            // Don't leave us in the middle of a batch edit.
+            mTextView.onEndBatchEdit();
+
+            if (mTextView.isInExtractedMode()) {
+                hideCursorAndSpanControllers();
+                stopTextActionModeWithPreservingSelection();
+            } else {
+                hideCursorAndSpanControllers();
+                if (mTextView.isTemporarilyDetached()) {
+                    stopTextActionModeWithPreservingSelection();
+                } else {
+                    stopTextActionMode();
+                }
+                downgradeEasyCorrectionSpans();
+            }
+            // No need to create the controller
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.resetTouchOffsets();
+            }
+        }
+    }
+
+    /**
+     * Downgrades to simple suggestions all the easy correction spans that are not a spell check
+     * span.
+     */
+    private void downgradeEasyCorrectionSpans() {
+        CharSequence text = mTextView.getText();
+        if (text instanceof Spannable) {
+            Spannable spannable = (Spannable) text;
+            SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
+                    spannable.length(), SuggestionSpan.class);
+            for (int i = 0; i < suggestionSpans.length; i++) {
+                int flags = suggestionSpans[i].getFlags();
+                if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+                        && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
+                    flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+                    suggestionSpans[i].setFlags(flags);
+                }
+            }
+        }
+    }
+
+    void sendOnTextChanged(int start, int before, int after) {
+        getSelectionActionModeHelper().onTextChanged(start, start + before);
+        updateSpellCheckSpans(start, start + after, false);
+
+        // Flip flag to indicate the word iterator needs to have the text reset.
+        mUpdateWordIteratorText = true;
+
+        // Hide the controllers as soon as text is modified (typing, procedural...)
+        // We do not hide the span controllers, since they can be added when a new text is
+        // inserted into the text view (voice IME).
+        hideCursorControllers();
+        // Reset drag accelerator.
+        if (mSelectionModifierCursorController != null) {
+            mSelectionModifierCursorController.resetTouchOffsets();
+        }
+        stopTextActionMode();
+    }
+
+    private int getLastTapPosition() {
+        // No need to create the controller at that point, no last tap position saved
+        if (mSelectionModifierCursorController != null) {
+            int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
+            if (lastTapPosition >= 0) {
+                // Safety check, should not be possible.
+                if (lastTapPosition > mTextView.getText().length()) {
+                    lastTapPosition = mTextView.getText().length();
+                }
+                return lastTapPosition;
+            }
+        }
+
+        return -1;
+    }
+
+    void onWindowFocusChanged(boolean hasWindowFocus) {
+        if (hasWindowFocus) {
+            if (mBlink != null) {
+                mBlink.uncancel();
+                makeBlink();
+            }
+            if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
+                refreshTextActionMode();
+            }
+        } else {
+            if (mBlink != null) {
+                mBlink.cancel();
+            }
+            if (mInputContentType != null) {
+                mInputContentType.enterDown = false;
+            }
+            // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
+            hideCursorAndSpanControllers();
+            stopTextActionModeWithPreservingSelection();
+            if (mSuggestionsPopupWindow != null) {
+                mSuggestionsPopupWindow.onParentLostFocus();
+            }
+
+            // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
+            ensureEndedBatchEdit();
+        }
+    }
+
+    private void updateTapState(MotionEvent event) {
+        final int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            // Detect double tap and triple click.
+            if (((mTapState == TAP_STATE_FIRST_TAP)
+                    || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
+                            && (SystemClock.uptimeMillis() - mLastTouchUpTime)
+                                    <= ViewConfiguration.getDoubleTapTimeout()) {
+                if (mTapState == TAP_STATE_FIRST_TAP) {
+                    mTapState = TAP_STATE_DOUBLE_TAP;
+                } else {
+                    mTapState = TAP_STATE_TRIPLE_CLICK;
+                }
+            } else {
+                mTapState = TAP_STATE_FIRST_TAP;
+            }
+        }
+        if (action == MotionEvent.ACTION_UP) {
+            mLastTouchUpTime = SystemClock.uptimeMillis();
+        }
+    }
+
+    private boolean shouldFilterOutTouchEvent(MotionEvent event) {
+        if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
+            return false;
+        }
+        final boolean primaryButtonStateChanged =
+                ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
+        final int action = event.getActionMasked();
+        if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
+                && !primaryButtonStateChanged) {
+            return true;
+        }
+        if (action == MotionEvent.ACTION_MOVE
+                && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
+            return true;
+        }
+        return false;
+    }
+
+    void onTouchEvent(MotionEvent event) {
+        final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
+        mLastButtonState = event.getButtonState();
+        if (filterOutEvent) {
+            if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+                mDiscardNextActionUp = true;
+            }
+            return;
+        }
+        updateTapState(event);
+        updateFloatingToolbarVisibility(event);
+
+        if (hasSelectionController()) {
+            getSelectionController().onTouchEvent(event);
+        }
+
+        if (mShowSuggestionRunnable != null) {
+            mTextView.removeCallbacks(mShowSuggestionRunnable);
+            mShowSuggestionRunnable = null;
+        }
+
+        if (event.getActionMasked() == MotionEvent.ACTION_UP) {
+            mLastUpPositionX = event.getX();
+            mLastUpPositionY = event.getY();
+        }
+
+        if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+            mLastDownPositionX = event.getX();
+            mLastDownPositionY = event.getY();
+
+            // Reset this state; it will be re-set if super.onTouchEvent
+            // causes focus to move to the view.
+            mTouchFocusSelected = false;
+            mIgnoreActionUpEvent = false;
+        }
+    }
+
+    private void updateFloatingToolbarVisibility(MotionEvent event) {
+        if (mTextActionMode != null) {
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_MOVE:
+                    hideFloatingToolbar(ActionMode.DEFAULT_HIDE_DURATION);
+                    break;
+                case MotionEvent.ACTION_UP:  // fall through
+                case MotionEvent.ACTION_CANCEL:
+                    showFloatingToolbar();
+            }
+        }
+    }
+
+    void hideFloatingToolbar(int duration) {
+        if (mTextActionMode != null) {
+            mTextView.removeCallbacks(mShowFloatingToolbar);
+            mTextActionMode.hide(duration);
+        }
+    }
+
+    private void showFloatingToolbar() {
+        if (mTextActionMode != null) {
+            // Delay "show" so it doesn't interfere with click confirmations
+            // or double-clicks that could "dismiss" the floating toolbar.
+            int delay = ViewConfiguration.getDoubleTapTimeout();
+            mTextView.postDelayed(mShowFloatingToolbar, delay);
+
+            // This classifies the text and most likely returns before the toolbar is actually
+            // shown. If not, it will update the toolbar with the result when classification
+            // returns. We would rather not wait for a long running classification process.
+            invalidateActionModeAsync();
+        }
+    }
+
+    public void beginBatchEdit() {
+        mInBatchEditControllers = true;
+        final InputMethodState ims = mInputMethodState;
+        if (ims != null) {
+            int nesting = ++ims.mBatchEditNesting;
+            if (nesting == 1) {
+                ims.mCursorChanged = false;
+                ims.mChangedDelta = 0;
+                if (ims.mContentChanged) {
+                    // We already have a pending change from somewhere else,
+                    // so turn this into a full update.
+                    ims.mChangedStart = 0;
+                    ims.mChangedEnd = mTextView.getText().length();
+                } else {
+                    ims.mChangedStart = EXTRACT_UNKNOWN;
+                    ims.mChangedEnd = EXTRACT_UNKNOWN;
+                    ims.mContentChanged = false;
+                }
+                mUndoInputFilter.beginBatchEdit();
+                mTextView.onBeginBatchEdit();
+            }
+        }
+    }
+
+    public void endBatchEdit() {
+        mInBatchEditControllers = false;
+        final InputMethodState ims = mInputMethodState;
+        if (ims != null) {
+            int nesting = --ims.mBatchEditNesting;
+            if (nesting == 0) {
+                finishBatchEdit(ims);
+            }
+        }
+    }
+
+    void ensureEndedBatchEdit() {
+        final InputMethodState ims = mInputMethodState;
+        if (ims != null && ims.mBatchEditNesting != 0) {
+            ims.mBatchEditNesting = 0;
+            finishBatchEdit(ims);
+        }
+    }
+
+    void finishBatchEdit(final InputMethodState ims) {
+        mTextView.onEndBatchEdit();
+        mUndoInputFilter.endBatchEdit();
+
+        if (ims.mContentChanged || ims.mSelectionModeChanged) {
+            mTextView.updateAfterEdit();
+            reportExtractedText();
+        } else if (ims.mCursorChanged) {
+            // Cheesy way to get us to report the current cursor location.
+            mTextView.invalidateCursor();
+        }
+        // sendUpdateSelection knows to avoid sending if the selection did
+        // not actually change.
+        sendUpdateSelection();
+
+        // Show drag handles if they were blocked by batch edit mode.
+        if (mTextActionMode != null) {
+            final CursorController cursorController = mTextView.hasSelection()
+                    ? getSelectionController() : getInsertionController();
+            if (cursorController != null && !cursorController.isActive()
+                    && !cursorController.isCursorBeingModified()) {
+                cursorController.show();
+            }
+        }
+    }
+
+    static final int EXTRACT_NOTHING = -2;
+    static final int EXTRACT_UNKNOWN = -1;
+
+    boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
+                EXTRACT_UNKNOWN, outText);
+    }
+
+    private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
+            int partialStartOffset, int partialEndOffset, int delta,
+            @Nullable ExtractedText outText) {
+        if (request == null || outText == null) {
+            return false;
+        }
+
+        final CharSequence content = mTextView.getText();
+        if (content == null) {
+            return false;
+        }
+
+        if (partialStartOffset != EXTRACT_NOTHING) {
+            final int N = content.length();
+            if (partialStartOffset < 0) {
+                outText.partialStartOffset = outText.partialEndOffset = -1;
+                partialStartOffset = 0;
+                partialEndOffset = N;
+            } else {
+                // Now use the delta to determine the actual amount of text
+                // we need.
+                partialEndOffset += delta;
+                // Adjust offsets to ensure we contain full spans.
+                if (content instanceof Spanned) {
+                    Spanned spanned = (Spanned) content;
+                    Object[] spans = spanned.getSpans(partialStartOffset,
+                            partialEndOffset, ParcelableSpan.class);
+                    int i = spans.length;
+                    while (i > 0) {
+                        i--;
+                        int j = spanned.getSpanStart(spans[i]);
+                        if (j < partialStartOffset) partialStartOffset = j;
+                        j = spanned.getSpanEnd(spans[i]);
+                        if (j > partialEndOffset) partialEndOffset = j;
+                    }
+                }
+                outText.partialStartOffset = partialStartOffset;
+                outText.partialEndOffset = partialEndOffset - delta;
+
+                if (partialStartOffset > N) {
+                    partialStartOffset = N;
+                } else if (partialStartOffset < 0) {
+                    partialStartOffset = 0;
+                }
+                if (partialEndOffset > N) {
+                    partialEndOffset = N;
+                } else if (partialEndOffset < 0) {
+                    partialEndOffset = 0;
+                }
+            }
+            if ((request.flags & InputConnection.GET_TEXT_WITH_STYLES) != 0) {
+                outText.text = content.subSequence(partialStartOffset,
+                        partialEndOffset);
+            } else {
+                outText.text = TextUtils.substring(content, partialStartOffset,
+                        partialEndOffset);
+            }
+        } else {
+            outText.partialStartOffset = 0;
+            outText.partialEndOffset = 0;
+            outText.text = "";
+        }
+        outText.flags = 0;
+        if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
+            outText.flags |= ExtractedText.FLAG_SELECTING;
+        }
+        if (mTextView.isSingleLine()) {
+            outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
+        }
+        outText.startOffset = 0;
+        outText.selectionStart = mTextView.getSelectionStart();
+        outText.selectionEnd = mTextView.getSelectionEnd();
+        return true;
+    }
+
+    boolean reportExtractedText() {
+        final Editor.InputMethodState ims = mInputMethodState;
+        if (ims != null) {
+            final boolean contentChanged = ims.mContentChanged;
+            if (contentChanged || ims.mSelectionModeChanged) {
+                ims.mContentChanged = false;
+                ims.mSelectionModeChanged = false;
+                final ExtractedTextRequest req = ims.mExtractedTextRequest;
+                if (req != null) {
+                    InputMethodManager imm = InputMethodManager.peekInstance();
+                    if (imm != null) {
+                        if (TextView.DEBUG_EXTRACT) {
+                            Log.v(TextView.LOG_TAG, "Retrieving extracted start="
+                                    + ims.mChangedStart
+                                    + " end=" + ims.mChangedEnd
+                                    + " delta=" + ims.mChangedDelta);
+                        }
+                        if (ims.mChangedStart < 0 && !contentChanged) {
+                            ims.mChangedStart = EXTRACT_NOTHING;
+                        }
+                        if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
+                                ims.mChangedDelta, ims.mExtractedText)) {
+                            if (TextView.DEBUG_EXTRACT) {
+                                Log.v(TextView.LOG_TAG,
+                                        "Reporting extracted start="
+                                                + ims.mExtractedText.partialStartOffset
+                                                + " end=" + ims.mExtractedText.partialEndOffset
+                                                + ": " + ims.mExtractedText.text);
+                            }
+
+                            imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
+                            ims.mChangedStart = EXTRACT_UNKNOWN;
+                            ims.mChangedEnd = EXTRACT_UNKNOWN;
+                            ims.mChangedDelta = 0;
+                            ims.mContentChanged = false;
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    private void sendUpdateSelection() {
+        if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            if (null != imm) {
+                final int selectionStart = mTextView.getSelectionStart();
+                final int selectionEnd = mTextView.getSelectionEnd();
+                int candStart = -1;
+                int candEnd = -1;
+                if (mTextView.getText() instanceof Spannable) {
+                    final Spannable sp = (Spannable) mTextView.getText();
+                    candStart = EditableInputConnection.getComposingSpanStart(sp);
+                    candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+                }
+                // InputMethodManager#updateSelection skips sending the message if
+                // none of the parameters have changed since the last time we called it.
+                imm.updateSelection(mTextView,
+                        selectionStart, selectionEnd, candStart, candEnd);
+            }
+        }
+    }
+
+    void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
+            int cursorOffsetVertical) {
+        final int selectionStart = mTextView.getSelectionStart();
+        final int selectionEnd = mTextView.getSelectionEnd();
+
+        final InputMethodState ims = mInputMethodState;
+        if (ims != null && ims.mBatchEditNesting == 0) {
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) {
+                if (imm.isActive(mTextView)) {
+                    if (ims.mContentChanged || ims.mSelectionModeChanged) {
+                        // We are in extract mode and the content has changed
+                        // in some way... just report complete new text to the
+                        // input method.
+                        reportExtractedText();
+                    }
+                }
+            }
+        }
+
+        if (mCorrectionHighlighter != null) {
+            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
+        }
+
+        if (highlight != null && selectionStart == selectionEnd && mCursorDrawable != null) {
+            drawCursor(canvas, cursorOffsetVertical);
+            // Rely on the drawable entirely, do not draw the cursor line.
+            // Has to be done after the IMM related code above which relies on the highlight.
+            highlight = null;
+        }
+
+        if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
+            drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
+                    cursorOffsetVertical);
+        } else {
+            layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
+        }
+
+        if (mSelectionActionModeHelper != null) {
+            mSelectionActionModeHelper.onDraw(canvas);
+        }
+    }
+
+    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+            Paint highlightPaint, int cursorOffsetVertical) {
+        final long lineRange = layout.getLineRangeForDraw(canvas);
+        int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
+        int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
+        if (lastLine < 0) return;
+
+        layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
+                firstLine, lastLine);
+
+        if (layout instanceof DynamicLayout) {
+            if (mTextRenderNodes == null) {
+                mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
+            }
+
+            DynamicLayout dynamicLayout = (DynamicLayout) layout;
+            int[] blockEndLines = dynamicLayout.getBlockEndLines();
+            int[] blockIndices = dynamicLayout.getBlockIndices();
+            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+            final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
+
+            final ArraySet<Integer> blockSet = dynamicLayout.getBlocksAlwaysNeedToBeRedrawn();
+            if (blockSet != null) {
+                for (int i = 0; i < blockSet.size(); i++) {
+                    final int blockIndex = dynamicLayout.getBlockIndex(blockSet.valueAt(i));
+                    if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+                            && mTextRenderNodes[blockIndex] != null) {
+                        mTextRenderNodes[blockIndex].needsToBeShifted = true;
+                    }
+                }
+            }
+
+            int startBlock = Arrays.binarySearch(blockEndLines, 0, numberOfBlocks, firstLine);
+            if (startBlock < 0) {
+                startBlock = -(startBlock + 1);
+            }
+            startBlock = Math.min(indexFirstChangedBlock, startBlock);
+
+            int startIndexToFindAvailableRenderNode = 0;
+            int lastIndex = numberOfBlocks;
+
+            for (int i = startBlock; i < numberOfBlocks; i++) {
+                final int blockIndex = blockIndices[i];
+                if (i >= indexFirstChangedBlock
+                        && blockIndex != DynamicLayout.INVALID_BLOCK_INDEX
+                        && mTextRenderNodes[blockIndex] != null) {
+                    mTextRenderNodes[blockIndex].needsToBeShifted = true;
+                }
+                if (blockEndLines[i] < firstLine) {
+                    // Blocks in [indexFirstChangedBlock, firstLine) are not redrawn here. They will
+                    // be redrawn after they get scrolled into drawing range.
+                    continue;
+                }
+                startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas, layout,
+                        highlight, highlightPaint, cursorOffsetVertical, blockEndLines,
+                        blockIndices, i, numberOfBlocks, startIndexToFindAvailableRenderNode);
+                if (blockEndLines[i] >= lastLine) {
+                    lastIndex = Math.max(indexFirstChangedBlock, i + 1);
+                    break;
+                }
+            }
+            if (blockSet != null) {
+                for (int i = 0; i < blockSet.size(); i++) {
+                    final int block = blockSet.valueAt(i);
+                    final int blockIndex = dynamicLayout.getBlockIndex(block);
+                    if (blockIndex == DynamicLayout.INVALID_BLOCK_INDEX
+                            || mTextRenderNodes[blockIndex] == null
+                            || mTextRenderNodes[blockIndex].needsToBeShifted) {
+                        startIndexToFindAvailableRenderNode = drawHardwareAcceleratedInner(canvas,
+                                layout, highlight, highlightPaint, cursorOffsetVertical,
+                                blockEndLines, blockIndices, block, numberOfBlocks,
+                                startIndexToFindAvailableRenderNode);
+                    }
+                }
+            }
+
+            dynamicLayout.setIndexFirstChangedBlock(lastIndex);
+        } else {
+            // Boring layout is used for empty and hint text
+            layout.drawText(canvas, firstLine, lastLine);
+        }
+    }
+
+    private int drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight,
+            Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines,
+            int[] blockIndices, int blockInfoIndex, int numberOfBlocks,
+            int startIndexToFindAvailableRenderNode) {
+        final int blockEndLine = blockEndLines[blockInfoIndex];
+        int blockIndex = blockIndices[blockInfoIndex];
+
+        final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+        if (blockIsInvalid) {
+            blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+                    startIndexToFindAvailableRenderNode);
+            // Note how dynamic layout's internal block indices get updated from Editor
+            blockIndices[blockInfoIndex] = blockIndex;
+            if (mTextRenderNodes[blockIndex] != null) {
+                mTextRenderNodes[blockIndex].isDirty = true;
+            }
+            startIndexToFindAvailableRenderNode = blockIndex + 1;
+        }
+
+        if (mTextRenderNodes[blockIndex] == null) {
+            mTextRenderNodes[blockIndex] = new TextRenderNode("Text " + blockIndex);
+        }
+
+        final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
+        RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
+        if (mTextRenderNodes[blockIndex].needsToBeShifted || blockDisplayListIsInvalid) {
+            final int blockBeginLine = blockInfoIndex == 0 ?
+                    0 : blockEndLines[blockInfoIndex - 1] + 1;
+            final int top = layout.getLineTop(blockBeginLine);
+            final int bottom = layout.getLineBottom(blockEndLine);
+            int left = 0;
+            int right = mTextView.getWidth();
+            if (mTextView.getHorizontallyScrolling()) {
+                float min = Float.MAX_VALUE;
+                float max = Float.MIN_VALUE;
+                for (int line = blockBeginLine; line <= blockEndLine; line++) {
+                    min = Math.min(min, layout.getLineLeft(line));
+                    max = Math.max(max, layout.getLineRight(line));
+                }
+                left = (int) min;
+                right = (int) (max + 0.5f);
+            }
+
+            // Rebuild display list if it is invalid
+            if (blockDisplayListIsInvalid) {
+                final DisplayListCanvas displayListCanvas = blockDisplayList.start(
+                        right - left, bottom - top);
+                try {
+                    // drawText is always relative to TextView's origin, this translation
+                    // brings this range of text back to the top left corner of the viewport
+                    displayListCanvas.translate(-left, -top);
+                    layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
+                    mTextRenderNodes[blockIndex].isDirty = false;
+                    // No need to untranslate, previous context is popped after
+                    // drawDisplayList
+                } finally {
+                    blockDisplayList.end(displayListCanvas);
+                    // Same as drawDisplayList below, handled by our TextView's parent
+                    blockDisplayList.setClipToBounds(false);
+                }
+            }
+
+            // Valid display list only needs to update its drawing location.
+            blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
+            mTextRenderNodes[blockIndex].needsToBeShifted = false;
+        }
+        ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
+        return startIndexToFindAvailableRenderNode;
+    }
+
+    private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
+            int searchStartIndex) {
+        int length = mTextRenderNodes.length;
+        for (int i = searchStartIndex; i < length; i++) {
+            boolean blockIndexFound = false;
+            for (int j = 0; j < numberOfBlocks; j++) {
+                if (blockIndices[j] == i) {
+                    blockIndexFound = true;
+                    break;
+                }
+            }
+            if (blockIndexFound) continue;
+            return i;
+        }
+
+        // No available index found, the pool has to grow
+        mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
+        return length;
+    }
+
+    private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
+        final boolean translate = cursorOffsetVertical != 0;
+        if (translate) canvas.translate(0, cursorOffsetVertical);
+        if (mCursorDrawable != null) {
+            mCursorDrawable.draw(canvas);
+        }
+        if (translate) canvas.translate(0, -cursorOffsetVertical);
+    }
+
+    void invalidateHandlesAndActionMode() {
+        if (mSelectionModifierCursorController != null) {
+            mSelectionModifierCursorController.invalidateHandles();
+        }
+        if (mInsertionPointCursorController != null) {
+            mInsertionPointCursorController.invalidateHandle();
+        }
+        if (mTextActionMode != null) {
+            invalidateActionMode();
+        }
+    }
+
+    /**
+     * Invalidates all the sub-display lists that overlap the specified character range
+     */
+    void invalidateTextDisplayList(Layout layout, int start, int end) {
+        if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
+            final int firstLine = layout.getLineForOffset(start);
+            final int lastLine = layout.getLineForOffset(end);
+
+            DynamicLayout dynamicLayout = (DynamicLayout) layout;
+            int[] blockEndLines = dynamicLayout.getBlockEndLines();
+            int[] blockIndices = dynamicLayout.getBlockIndices();
+            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+
+            int i = 0;
+            // Skip the blocks before firstLine
+            while (i < numberOfBlocks) {
+                if (blockEndLines[i] >= firstLine) break;
+                i++;
+            }
+
+            // Invalidate all subsequent blocks until lastLine is passed
+            while (i < numberOfBlocks) {
+                final int blockIndex = blockIndices[i];
+                if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
+                    mTextRenderNodes[blockIndex].isDirty = true;
+                }
+                if (blockEndLines[i] >= lastLine) break;
+                i++;
+            }
+        }
+    }
+
+    void invalidateTextDisplayList() {
+        if (mTextRenderNodes != null) {
+            for (int i = 0; i < mTextRenderNodes.length; i++) {
+                if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
+            }
+        }
+    }
+
+    void updateCursorPosition() {
+        if (mTextView.mCursorDrawableRes == 0) {
+            mCursorDrawable = null;
+            return;
+        }
+
+        final Layout layout = mTextView.getLayout();
+        final int offset = mTextView.getSelectionStart();
+        final int line = layout.getLineForOffset(offset);
+        final int top = layout.getLineTop(line);
+        final int bottom = layout.getLineBottomWithoutSpacing(line);
+
+        final boolean clamped = layout.shouldClampCursor(line);
+        updateCursorPosition(top, bottom, layout.getPrimaryHorizontal(offset, clamped));
+    }
+
+    void refreshTextActionMode() {
+        if (extractedTextModeWillBeStarted()) {
+            mRestartActionModeOnNextRefresh = false;
+            return;
+        }
+        final boolean hasSelection = mTextView.hasSelection();
+        final SelectionModifierCursorController selectionController = getSelectionController();
+        final InsertionPointCursorController insertionController = getInsertionController();
+        if ((selectionController != null && selectionController.isCursorBeingModified())
+                || (insertionController != null && insertionController.isCursorBeingModified())) {
+            // ActionMode should be managed by the currently active cursor controller.
+            mRestartActionModeOnNextRefresh = false;
+            return;
+        }
+        if (hasSelection) {
+            hideInsertionPointCursorController();
+            if (mTextActionMode == null) {
+                if (mRestartActionModeOnNextRefresh) {
+                    // To avoid distraction, newly start action mode only when selection action
+                    // mode is being restarted.
+                    startSelectionActionModeAsync(false);
+                }
+            } else if (selectionController == null || !selectionController.isActive()) {
+                // Insertion action mode is active. Avoid dismissing the selection.
+                stopTextActionModeWithPreservingSelection();
+                startSelectionActionModeAsync(false);
+            } else {
+                mTextActionMode.invalidateContentRect();
+            }
+        } else {
+            // Insertion action mode is started only when insertion controller is explicitly
+            // activated.
+            if (insertionController == null || !insertionController.isActive()) {
+                stopTextActionMode();
+            } else if (mTextActionMode != null) {
+                mTextActionMode.invalidateContentRect();
+            }
+        }
+        mRestartActionModeOnNextRefresh = false;
+    }
+
+    /**
+     * Start an Insertion action mode.
+     */
+    void startInsertionActionMode() {
+        if (mInsertionActionModeRunnable != null) {
+            mTextView.removeCallbacks(mInsertionActionModeRunnable);
+        }
+        if (extractedTextModeWillBeStarted()) {
+            return;
+        }
+        stopTextActionMode();
+
+        ActionMode.Callback actionModeCallback =
+                new TextActionModeCallback(false /* hasSelection */);
+        mTextActionMode = mTextView.startActionMode(
+                actionModeCallback, ActionMode.TYPE_FLOATING);
+        if (mTextActionMode != null && getInsertionController() != null) {
+            getInsertionController().show();
+        }
+    }
+
+    @NonNull
+    TextView getTextView() {
+        return mTextView;
+    }
+
+    @Nullable
+    ActionMode getTextActionMode() {
+        return mTextActionMode;
+    }
+
+    void setRestartActionModeOnNextRefresh(boolean value) {
+        mRestartActionModeOnNextRefresh = value;
+    }
+
+    /**
+     * Asynchronously starts a selection action mode using the TextClassifier.
+     */
+    void startSelectionActionModeAsync(boolean adjustSelection) {
+        getSelectionActionModeHelper().startActionModeAsync(adjustSelection);
+    }
+
+    /**
+     * Asynchronously invalidates an action mode using the TextClassifier.
+     */
+    void invalidateActionModeAsync() {
+        getSelectionActionModeHelper().invalidateActionModeAsync();
+    }
+
+    /**
+     * Synchronously invalidates an action mode without the TextClassifier.
+     */
+    private void invalidateActionMode() {
+        if (mTextActionMode != null) {
+            mTextActionMode.invalidate();
+        }
+    }
+
+    private SelectionActionModeHelper getSelectionActionModeHelper() {
+        if (mSelectionActionModeHelper == null) {
+            mSelectionActionModeHelper = new SelectionActionModeHelper(this);
+        }
+        return mSelectionActionModeHelper;
+    }
+
+    /**
+     * If the TextView allows text selection, selects the current word when no existing selection
+     * was available and starts a drag.
+     *
+     * @return true if the drag was started.
+     */
+    private boolean selectCurrentWordAndStartDrag() {
+        if (mInsertionActionModeRunnable != null) {
+            mTextView.removeCallbacks(mInsertionActionModeRunnable);
+        }
+        if (extractedTextModeWillBeStarted()) {
+            return false;
+        }
+        if (!checkField()) {
+            return false;
+        }
+        if (!mTextView.hasSelection() && !selectCurrentWord()) {
+            // No selection and cannot select a word.
+            return false;
+        }
+        stopTextActionModeWithPreservingSelection();
+        getSelectionController().enterDrag(
+                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
+        return true;
+    }
+
+    /**
+     * Checks whether a selection can be performed on the current TextView.
+     *
+     * @return true if a selection can be performed
+     */
+    boolean checkField() {
+        if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
+            Log.w(TextView.LOG_TAG,
+                    "TextView does not support text selection. Selection cancelled.");
+            return false;
+        }
+        return true;
+    }
+
+    boolean startSelectionActionModeInternal() {
+        if (extractedTextModeWillBeStarted()) {
+            return false;
+        }
+        if (mTextActionMode != null) {
+            // Text action mode is already started
+            invalidateActionMode();
+            return false;
+        }
+
+        if (!checkField() || !mTextView.hasSelection()) {
+            return false;
+        }
+
+        ActionMode.Callback actionModeCallback =
+                new TextActionModeCallback(true /* hasSelection */);
+        mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
+
+        final boolean selectionStarted = mTextActionMode != null;
+        if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
+            // Show the IME to be able to replace text, except when selecting non editable text.
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) {
+                imm.showSoftInput(mTextView, 0, null);
+            }
+        }
+        return selectionStarted;
+    }
+
+    private boolean extractedTextModeWillBeStarted() {
+        if (!(mTextView.isInExtractedMode())) {
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            return  imm != null && imm.isFullscreenMode();
+        }
+        return false;
+    }
+
+    /**
+     * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
+     * the current cursor position or selection range. This method is consistent with the
+     * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
+     */
+    private boolean shouldOfferToShowSuggestions() {
+        CharSequence text = mTextView.getText();
+        if (!(text instanceof Spannable)) return false;
+
+        final Spannable spannable = (Spannable) text;
+        final int selectionStart = mTextView.getSelectionStart();
+        final int selectionEnd = mTextView.getSelectionEnd();
+        final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
+                SuggestionSpan.class);
+        if (suggestionSpans.length == 0) {
+            return false;
+        }
+        if (selectionStart == selectionEnd) {
+            // Spans overlap the cursor.
+            for (int i = 0; i < suggestionSpans.length; i++) {
+                if (suggestionSpans[i].getSuggestions().length > 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+        int minSpanStart = mTextView.getText().length();
+        int maxSpanEnd = 0;
+        int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
+        int unionOfSpansCoveringSelectionStartEnd = 0;
+        boolean hasValidSuggestions = false;
+        for (int i = 0; i < suggestionSpans.length; i++) {
+            final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
+            final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
+            minSpanStart = Math.min(minSpanStart, spanStart);
+            maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
+            if (selectionStart < spanStart || selectionStart > spanEnd) {
+                // The span doesn't cover the current selection start point.
+                continue;
+            }
+            hasValidSuggestions =
+                    hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
+            unionOfSpansCoveringSelectionStartStart =
+                    Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
+            unionOfSpansCoveringSelectionStartEnd =
+                    Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
+        }
+        if (!hasValidSuggestions) {
+            return false;
+        }
+        if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
+            // No spans cover the selection start point.
+            return false;
+        }
+        if (minSpanStart < unionOfSpansCoveringSelectionStartStart
+                || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
+            // There is a span that is not covered by the union. In this case, we soouldn't offer
+            // to show suggestions as it's confusing.
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
+     * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
+     */
+    private boolean isCursorInsideEasyCorrectionSpan() {
+        Spannable spannable = (Spannable) mTextView.getText();
+        SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
+                mTextView.getSelectionEnd(), SuggestionSpan.class);
+        for (int i = 0; i < suggestionSpans.length; i++) {
+            if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    void onTouchUpEvent(MotionEvent event) {
+        if (getSelectionActionModeHelper().resetSelection(
+                getTextView().getOffsetForPosition(event.getX(), event.getY()))) {
+            return;
+        }
+
+        boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
+        hideCursorAndSpanControllers();
+        stopTextActionMode();
+        CharSequence text = mTextView.getText();
+        if (!selectAllGotFocus && text.length() > 0) {
+            // Move cursor
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+            Selection.setSelection((Spannable) text, offset);
+            if (mSpellChecker != null) {
+                // When the cursor moves, the word that was typed may need spell check
+                mSpellChecker.onSelectionChanged();
+            }
+
+            if (!extractedTextModeWillBeStarted()) {
+                if (isCursorInsideEasyCorrectionSpan()) {
+                    // Cancel the single tap delayed runnable.
+                    if (mInsertionActionModeRunnable != null) {
+                        mTextView.removeCallbacks(mInsertionActionModeRunnable);
+                    }
+
+                    mShowSuggestionRunnable = new Runnable() {
+                        public void run() {
+                            replace();
+                        }
+                    };
+                    // removeCallbacks is performed on every touch
+                    mTextView.postDelayed(mShowSuggestionRunnable,
+                            ViewConfiguration.getDoubleTapTimeout());
+                } else if (hasInsertionController()) {
+                    getInsertionController().show();
+                }
+            }
+        }
+    }
+
+    protected void stopTextActionMode() {
+        if (mTextActionMode != null) {
+            // This will hide the mSelectionModifierCursorController
+            mTextActionMode.finish();
+        }
+    }
+
+    private void stopTextActionModeWithPreservingSelection() {
+        if (mTextActionMode != null) {
+            mRestartActionModeOnNextRefresh = true;
+        }
+        mPreserveSelection = true;
+        stopTextActionMode();
+        mPreserveSelection = false;
+    }
+
+    /**
+     * @return True if this view supports insertion handles.
+     */
+    boolean hasInsertionController() {
+        return mInsertionControllerEnabled;
+    }
+
+    /**
+     * @return True if this view supports selection handles.
+     */
+    boolean hasSelectionController() {
+        return mSelectionControllerEnabled;
+    }
+
+    private InsertionPointCursorController getInsertionController() {
+        if (!mInsertionControllerEnabled) {
+            return null;
+        }
+
+        if (mInsertionPointCursorController == null) {
+            mInsertionPointCursorController = new InsertionPointCursorController();
+
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+        }
+
+        return mInsertionPointCursorController;
+    }
+
+    @Nullable
+    SelectionModifierCursorController getSelectionController() {
+        if (!mSelectionControllerEnabled) {
+            return null;
+        }
+
+        if (mSelectionModifierCursorController == null) {
+            mSelectionModifierCursorController = new SelectionModifierCursorController();
+
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+        }
+
+        return mSelectionModifierCursorController;
+    }
+
+    @VisibleForTesting
+    @Nullable
+    public Drawable getCursorDrawable() {
+        return mCursorDrawable;
+    }
+
+    private void updateCursorPosition(int top, int bottom, float horizontal) {
+        if (mCursorDrawable == null) {
+            mCursorDrawable = mTextView.getContext().getDrawable(
+                    mTextView.mCursorDrawableRes);
+        }
+        final int left = clampHorizontalPosition(mCursorDrawable, horizontal);
+        final int width = mCursorDrawable.getIntrinsicWidth();
+        mCursorDrawable.setBounds(left, top - mTempRect.top, left + width,
+                bottom + mTempRect.bottom);
+    }
+
+    /**
+     * Return clamped position for the drawable. If the drawable is within the boundaries of the
+     * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
+     * the beginning or the end of the text then its drawable edge is aligned with left or right of
+     * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
+     * of the view.
+     *
+     * @param drawable Drawable. Can be null.
+     * @param horizontal Horizontal position for the drawable.
+     * @return The clamped horizontal position for the drawable.
+     */
+    private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
+        horizontal = Math.max(0.5f, horizontal - 0.5f);
+        if (mTempRect == null) mTempRect = new Rect();
+
+        int drawableWidth = 0;
+        if (drawable != null) {
+            drawable.getPadding(mTempRect);
+            drawableWidth = drawable.getIntrinsicWidth();
+        } else {
+            mTempRect.setEmpty();
+        }
+
+        int scrollX = mTextView.getScrollX();
+        float horizontalDiff = horizontal - scrollX;
+        int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
+                - mTextView.getCompoundPaddingRight();
+
+        final int left;
+        if (horizontalDiff >= (viewClippedWidth - 1f)) {
+            // at the rightmost position
+            left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
+        } else if (Math.abs(horizontalDiff) <= 1f
+                || (TextUtils.isEmpty(mTextView.getText())
+                        && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
+                        && horizontal <= 1f)) {
+            // at the leftmost position
+            left = scrollX - mTempRect.left;
+        } else {
+            left = (int) horizontal - mTempRect.left;
+        }
+        return left;
+    }
+
+    /**
+     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+     * a dictionary) from the current input method, provided by it calling
+     * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
+     * implementation flashes the background of the corrected word to provide feedback to the user.
+     *
+     * @param info The auto correct info about the text that was corrected.
+     */
+    public void onCommitCorrection(CorrectionInfo info) {
+        if (mCorrectionHighlighter == null) {
+            mCorrectionHighlighter = new CorrectionHighlighter();
+        } else {
+            mCorrectionHighlighter.invalidate(false);
+        }
+
+        mCorrectionHighlighter.highlight(info);
+        mUndoInputFilter.freezeLastEdit();
+    }
+
+    void onScrollChanged() {
+        if (mPositionListener != null) {
+            mPositionListener.onScrollChanged();
+        }
+        if (mTextActionMode != null) {
+            mTextActionMode.invalidateContentRect();
+        }
+    }
+
+    /**
+     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
+     */
+    private boolean shouldBlink() {
+        if (!isCursorVisible() || !mTextView.isFocused()) return false;
+
+        final int start = mTextView.getSelectionStart();
+        if (start < 0) return false;
+
+        final int end = mTextView.getSelectionEnd();
+        if (end < 0) return false;
+
+        return start == end;
+    }
+
+    void makeBlink() {
+        if (shouldBlink()) {
+            mShowCursor = SystemClock.uptimeMillis();
+            if (mBlink == null) mBlink = new Blink();
+            mTextView.removeCallbacks(mBlink);
+            mTextView.postDelayed(mBlink, BLINK);
+        } else {
+            if (mBlink != null) mTextView.removeCallbacks(mBlink);
+        }
+    }
+
+    private class Blink implements Runnable {
+        private boolean mCancelled;
+
+        public void run() {
+            if (mCancelled) {
+                return;
+            }
+
+            mTextView.removeCallbacks(this);
+
+            if (shouldBlink()) {
+                if (mTextView.getLayout() != null) {
+                    mTextView.invalidateCursorPath();
+                }
+
+                mTextView.postDelayed(this, BLINK);
+            }
+        }
+
+        void cancel() {
+            if (!mCancelled) {
+                mTextView.removeCallbacks(this);
+                mCancelled = true;
+            }
+        }
+
+        void uncancel() {
+            mCancelled = false;
+        }
+    }
+
+    private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
+        TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
+                com.android.internal.R.layout.text_drag_thumbnail, null);
+
+        if (shadowView == null) {
+            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
+        }
+
+        if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+            final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
+            end = TextUtils.unpackRangeEndFromLong(range);
+        }
+        final CharSequence text = mTextView.getTransformedText(start, end);
+        shadowView.setText(text);
+        shadowView.setTextColor(mTextView.getTextColors());
+
+        shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
+        shadowView.setGravity(Gravity.CENTER);
+
+        shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT));
+
+        final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
+        shadowView.measure(size, size);
+
+        shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
+        shadowView.invalidate();
+        return new DragShadowBuilder(shadowView);
+    }
+
+    private static class DragLocalState {
+        public TextView sourceTextView;
+        public int start, end;
+
+        public DragLocalState(TextView sourceTextView, int start, int end) {
+            this.sourceTextView = sourceTextView;
+            this.start = start;
+            this.end = end;
+        }
+    }
+
+    void onDrop(DragEvent event) {
+        SpannableStringBuilder content = new SpannableStringBuilder();
+
+        final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
+        if (permissions != null) {
+            permissions.takeTransient();
+        }
+
+        try {
+            ClipData clipData = event.getClipData();
+            final int itemCount = clipData.getItemCount();
+            for (int i = 0; i < itemCount; i++) {
+                Item item = clipData.getItemAt(i);
+                content.append(item.coerceToStyledText(mTextView.getContext()));
+            }
+        } finally {
+            if (permissions != null) {
+                permissions.release();
+            }
+        }
+
+        mTextView.beginBatchEdit();
+        mUndoInputFilter.freezeLastEdit();
+        try {
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+            Object localState = event.getLocalState();
+            DragLocalState dragLocalState = null;
+            if (localState instanceof DragLocalState) {
+                dragLocalState = (DragLocalState) localState;
+            }
+            boolean dragDropIntoItself = dragLocalState != null
+                    && dragLocalState.sourceTextView == mTextView;
+
+            if (dragDropIntoItself) {
+                if (offset >= dragLocalState.start && offset < dragLocalState.end) {
+                    // A drop inside the original selection discards the drop.
+                    return;
+                }
+            }
+
+            final int originalLength = mTextView.getText().length();
+            int min = offset;
+            int max = offset;
+
+            Selection.setSelection((Spannable) mTextView.getText(), max);
+            mTextView.replaceText_internal(min, max, content);
+
+            if (dragDropIntoItself) {
+                int dragSourceStart = dragLocalState.start;
+                int dragSourceEnd = dragLocalState.end;
+                if (max <= dragSourceStart) {
+                    // Inserting text before selection has shifted positions
+                    final int shift = mTextView.getText().length() - originalLength;
+                    dragSourceStart += shift;
+                    dragSourceEnd += shift;
+                }
+
+                // Delete original selection
+                mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
+
+                // Make sure we do not leave two adjacent spaces.
+                final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
+                final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
+                if (nextCharIdx > prevCharIdx + 1) {
+                    CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
+                    if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
+                        mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
+                    }
+                }
+            }
+        } finally {
+            mTextView.endBatchEdit();
+            mUndoInputFilter.freezeLastEdit();
+        }
+    }
+
+    public void addSpanWatchers(Spannable text) {
+        final int textLength = text.length();
+
+        if (mKeyListener != null) {
+            text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+        }
+
+        if (mSpanController == null) {
+            mSpanController = new SpanController();
+        }
+        text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+    }
+
+    void setContextMenuAnchor(float x, float y) {
+        mContextMenuAnchorX = x;
+        mContextMenuAnchorY = y;
+    }
+
+    void onCreateContextMenu(ContextMenu menu) {
+        if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
+                || Float.isNaN(mContextMenuAnchorY)) {
+            return;
+        }
+        final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
+        if (offset == -1) {
+            return;
+        }
+
+        stopTextActionModeWithPreservingSelection();
+        if (mTextView.canSelectText()) {
+            final boolean isOnSelection = mTextView.hasSelection()
+                    && offset >= mTextView.getSelectionStart()
+                    && offset <= mTextView.getSelectionEnd();
+            if (!isOnSelection) {
+                // Right clicked position is not on the selection. Remove the selection and move the
+                // cursor to the right clicked position.
+                Selection.setSelection((Spannable) mTextView.getText(), offset);
+                stopTextActionMode();
+            }
+        }
+
+        if (shouldOfferToShowSuggestions()) {
+            final SuggestionInfo[] suggestionInfoArray =
+                    new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
+            for (int i = 0; i < suggestionInfoArray.length; i++) {
+                suggestionInfoArray[i] = new SuggestionInfo();
+            }
+            final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
+                    com.android.internal.R.string.replace);
+            final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
+            for (int i = 0; i < numItems; i++) {
+                final SuggestionInfo info = suggestionInfoArray[i];
+                subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
+                        .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
+                            @Override
+                            public boolean onMenuItemClick(MenuItem item) {
+                                replaceWithSuggestion(info);
+                                return true;
+                            }
+                        });
+            }
+        }
+
+        menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
+                com.android.internal.R.string.undo)
+                .setAlphabeticShortcut('z')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canUndo());
+        menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
+                com.android.internal.R.string.redo)
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canRedo());
+
+        menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+                com.android.internal.R.string.cut)
+                .setAlphabeticShortcut('x')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canCut());
+        menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+                com.android.internal.R.string.copy)
+                .setAlphabeticShortcut('c')
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
+                .setEnabled(mTextView.canCopy());
+        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+                com.android.internal.R.string.paste)
+                .setAlphabeticShortcut('v')
+                .setEnabled(mTextView.canPaste())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+                com.android.internal.R.string.paste_as_plain_text)
+                .setEnabled(mTextView.canPasteAsPlainText())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+                com.android.internal.R.string.share)
+                .setEnabled(mTextView.canShare())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+                com.android.internal.R.string.selectAll)
+                .setAlphabeticShortcut('a')
+                .setEnabled(mTextView.canSelectAllText())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+        menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+                android.R.string.autofill)
+                .setEnabled(mTextView.canRequestAutofill())
+                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
+
+        mPreserveSelection = true;
+    }
+
+    @Nullable
+    private SuggestionSpan findEquivalentSuggestionSpan(
+            @NonNull SuggestionSpanInfo suggestionSpanInfo) {
+        final Editable editable = (Editable) mTextView.getText();
+        if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
+            // Exactly same span is found.
+            return suggestionSpanInfo.mSuggestionSpan;
+        }
+        // Suggestion span couldn't be found. Try to find a suggestion span that has the same
+        // contents.
+        final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
+                suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
+        for (final SuggestionSpan suggestionSpan : suggestionSpans) {
+            final int start = editable.getSpanStart(suggestionSpan);
+            if (start != suggestionSpanInfo.mSpanStart) {
+                continue;
+            }
+            final int end = editable.getSpanEnd(suggestionSpan);
+            if (end != suggestionSpanInfo.mSpanEnd) {
+                continue;
+            }
+            if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
+                return suggestionSpan;
+            }
+        }
+        return null;
+    }
+
+    private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
+        final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
+                suggestionInfo.mSuggestionSpanInfo);
+        if (targetSuggestionSpan == null) {
+            // Span has been removed
+            return;
+        }
+        final Editable editable = (Editable) mTextView.getText();
+        final int spanStart = editable.getSpanStart(targetSuggestionSpan);
+        final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
+        if (spanStart < 0 || spanEnd <= spanStart) {
+            // Span has been removed
+            return;
+        }
+
+        final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
+        // SuggestionSpans are removed by replace: save them before
+        SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
+                SuggestionSpan.class);
+        final int length = suggestionSpans.length;
+        int[] suggestionSpansStarts = new int[length];
+        int[] suggestionSpansEnds = new int[length];
+        int[] suggestionSpansFlags = new int[length];
+        for (int i = 0; i < length; i++) {
+            final SuggestionSpan suggestionSpan = suggestionSpans[i];
+            suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
+            suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
+            suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
+
+            // Remove potential misspelled flags
+            int suggestionSpanFlags = suggestionSpan.getFlags();
+            if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+                suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
+                suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
+                suggestionSpan.setFlags(suggestionSpanFlags);
+            }
+        }
+
+        // Notify source IME of the suggestion pick. Do this before swapping texts.
+        targetSuggestionSpan.notifySelection(
+                mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
+
+        // Swap text content between actual text and Suggestion span
+        final int suggestionStart = suggestionInfo.mSuggestionStart;
+        final int suggestionEnd = suggestionInfo.mSuggestionEnd;
+        final String suggestion = suggestionInfo.mText.subSequence(
+                suggestionStart, suggestionEnd).toString();
+        mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
+
+        String[] suggestions = targetSuggestionSpan.getSuggestions();
+        suggestions[suggestionInfo.mSuggestionIndex] = originalText;
+
+        // Restore previous SuggestionSpans
+        final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
+        for (int i = 0; i < length; i++) {
+            // Only spans that include the modified region make sense after replacement
+            // Spans partially included in the replaced region are removed, there is no
+            // way to assign them a valid range after replacement
+            if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
+                mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
+                        suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
+            }
+        }
+        // Move cursor at the end of the replaced word
+        final int newCursorPosition = spanEnd + lengthDelta;
+        mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
+    }
+
+    private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
+            new MenuItem.OnMenuItemClickListener() {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
+                return true;
+            }
+            return mTextView.onTextContextMenuItem(item.getItemId());
+        }
+    };
+
+    /**
+     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
+     * pop-up should be displayed.
+     * Also monitors {@link Selection} to call back to the attached input method.
+     */
+    private class SpanController implements SpanWatcher {
+
+        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
+
+        private EasyEditPopupWindow mPopupWindow;
+
+        private Runnable mHidePopup;
+
+        // This function is pure but inner classes can't have static functions
+        private boolean isNonIntermediateSelectionSpan(final Spannable text,
+                final Object span) {
+            return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
+                    && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
+        }
+
+        @Override
+        public void onSpanAdded(Spannable text, Object span, int start, int end) {
+            if (isNonIntermediateSelectionSpan(text, span)) {
+                sendUpdateSelection();
+            } else if (span instanceof EasyEditSpan) {
+                if (mPopupWindow == null) {
+                    mPopupWindow = new EasyEditPopupWindow();
+                    mHidePopup = new Runnable() {
+                        @Override
+                        public void run() {
+                            hide();
+                        }
+                    };
+                }
+
+                // Make sure there is only at most one EasyEditSpan in the text
+                if (mPopupWindow.mEasyEditSpan != null) {
+                    mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
+                }
+
+                mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
+                mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
+                    @Override
+                    public void onDeleteClick(EasyEditSpan span) {
+                        Editable editable = (Editable) mTextView.getText();
+                        int start = editable.getSpanStart(span);
+                        int end = editable.getSpanEnd(span);
+                        if (start >= 0 && end >= 0) {
+                            sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
+                            mTextView.deleteText_internal(start, end);
+                        }
+                        editable.removeSpan(span);
+                    }
+                });
+
+                if (mTextView.getWindowVisibility() != View.VISIBLE) {
+                    // The window is not visible yet, ignore the text change.
+                    return;
+                }
+
+                if (mTextView.getLayout() == null) {
+                    // The view has not been laid out yet, ignore the text change
+                    return;
+                }
+
+                if (extractedTextModeWillBeStarted()) {
+                    // The input is in extract mode. Do not handle the easy edit in
+                    // the original TextView, as the ExtractEditText will do
+                    return;
+                }
+
+                mPopupWindow.show();
+                mTextView.removeCallbacks(mHidePopup);
+                mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
+            }
+        }
+
+        @Override
+        public void onSpanRemoved(Spannable text, Object span, int start, int end) {
+            if (isNonIntermediateSelectionSpan(text, span)) {
+                sendUpdateSelection();
+            } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
+                hide();
+            }
+        }
+
+        @Override
+        public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
+                int newStart, int newEnd) {
+            if (isNonIntermediateSelectionSpan(text, span)) {
+                sendUpdateSelection();
+            } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
+                EasyEditSpan easyEditSpan = (EasyEditSpan) span;
+                sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
+                text.removeSpan(easyEditSpan);
+            }
+        }
+
+        public void hide() {
+            if (mPopupWindow != null) {
+                mPopupWindow.hide();
+                mTextView.removeCallbacks(mHidePopup);
+            }
+        }
+
+        private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
+            try {
+                PendingIntent pendingIntent = span.getPendingIntent();
+                if (pendingIntent != null) {
+                    Intent intent = new Intent();
+                    intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
+                    pendingIntent.send(mTextView.getContext(), 0, intent);
+                }
+            } catch (CanceledException e) {
+                // This should not happen, as we should try to send the intent only once.
+                Log.w(TAG, "PendingIntent for notification cannot be sent", e);
+            }
+        }
+    }
+
+    /**
+     * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
+     */
+    private interface EasyEditDeleteListener {
+
+        /**
+         * Clicks the delete pop-up.
+         */
+        void onDeleteClick(EasyEditSpan span);
+    }
+
+    /**
+     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
+     * by {@link SpanController}.
+     */
+    private class EasyEditPopupWindow extends PinnedPopupWindow
+            implements OnClickListener {
+        private static final int POPUP_TEXT_LAYOUT =
+                com.android.internal.R.layout.text_edit_action_popup_text;
+        private TextView mDeleteTextView;
+        private EasyEditSpan mEasyEditSpan;
+        private EasyEditDeleteListener mOnDeleteListener;
+
+        @Override
+        protected void createPopupWindow() {
+            mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+                    com.android.internal.R.attr.textSelectHandleWindowStyle);
+            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+            mPopupWindow.setClippingEnabled(true);
+        }
+
+        @Override
+        protected void initContentView() {
+            LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
+            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
+            mContentView = linearLayout;
+            mContentView.setBackgroundResource(
+                    com.android.internal.R.drawable.text_edit_side_paste_window);
+
+            LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
+                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+            LayoutParams wrapContent = new LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+            mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+            mDeleteTextView.setLayoutParams(wrapContent);
+            mDeleteTextView.setText(com.android.internal.R.string.delete);
+            mDeleteTextView.setOnClickListener(this);
+            mContentView.addView(mDeleteTextView);
+        }
+
+        public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
+            mEasyEditSpan = easyEditSpan;
+        }
+
+        private void setOnDeleteListener(EasyEditDeleteListener listener) {
+            mOnDeleteListener = listener;
+        }
+
+        @Override
+        public void onClick(View view) {
+            if (view == mDeleteTextView
+                    && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
+                    && mOnDeleteListener != null) {
+                mOnDeleteListener.onDeleteClick(mEasyEditSpan);
+            }
+        }
+
+        @Override
+        public void hide() {
+            if (mEasyEditSpan != null) {
+                mEasyEditSpan.setDeleteEnabled(false);
+            }
+            mOnDeleteListener = null;
+            super.hide();
+        }
+
+        @Override
+        protected int getTextOffset() {
+            // Place the pop-up at the end of the span
+            Editable editable = (Editable) mTextView.getText();
+            return editable.getSpanEnd(mEasyEditSpan);
+        }
+
+        @Override
+        protected int getVerticalLocalPosition(int line) {
+            final Layout layout = mTextView.getLayout();
+            return layout.getLineBottomWithoutSpacing(line);
+        }
+
+        @Override
+        protected int clipVertically(int positionY) {
+            // As we display the pop-up below the span, no vertical clipping is required.
+            return positionY;
+        }
+    }
+
+    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
+        // 3 handles
+        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
+        // 1 CursorAnchorInfoNotifier
+        private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
+        private TextViewPositionListener[] mPositionListeners =
+                new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
+        private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
+        private boolean mPositionHasChanged = true;
+        // Absolute position of the TextView with respect to its parent window
+        private int mPositionX, mPositionY;
+        private int mPositionXOnScreen, mPositionYOnScreen;
+        private int mNumberOfListeners;
+        private boolean mScrollHasChanged;
+        final int[] mTempCoords = new int[2];
+
+        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
+            if (mNumberOfListeners == 0) {
+                updatePosition();
+                ViewTreeObserver vto = mTextView.getViewTreeObserver();
+                vto.addOnPreDrawListener(this);
+            }
+
+            int emptySlotIndex = -1;
+            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+                TextViewPositionListener listener = mPositionListeners[i];
+                if (listener == positionListener) {
+                    return;
+                } else if (emptySlotIndex < 0 && listener == null) {
+                    emptySlotIndex = i;
+                }
+            }
+
+            mPositionListeners[emptySlotIndex] = positionListener;
+            mCanMove[emptySlotIndex] = canMove;
+            mNumberOfListeners++;
+        }
+
+        public void removeSubscriber(TextViewPositionListener positionListener) {
+            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+                if (mPositionListeners[i] == positionListener) {
+                    mPositionListeners[i] = null;
+                    mNumberOfListeners--;
+                    break;
+                }
+            }
+
+            if (mNumberOfListeners == 0) {
+                ViewTreeObserver vto = mTextView.getViewTreeObserver();
+                vto.removeOnPreDrawListener(this);
+            }
+        }
+
+        public int getPositionX() {
+            return mPositionX;
+        }
+
+        public int getPositionY() {
+            return mPositionY;
+        }
+
+        public int getPositionXOnScreen() {
+            return mPositionXOnScreen;
+        }
+
+        public int getPositionYOnScreen() {
+            return mPositionYOnScreen;
+        }
+
+        @Override
+        public boolean onPreDraw() {
+            updatePosition();
+
+            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
+                if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
+                    TextViewPositionListener positionListener = mPositionListeners[i];
+                    if (positionListener != null) {
+                        positionListener.updatePosition(mPositionX, mPositionY,
+                                mPositionHasChanged, mScrollHasChanged);
+                    }
+                }
+            }
+
+            mScrollHasChanged = false;
+            return true;
+        }
+
+        private void updatePosition() {
+            mTextView.getLocationInWindow(mTempCoords);
+
+            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
+
+            mPositionX = mTempCoords[0];
+            mPositionY = mTempCoords[1];
+
+            mTextView.getLocationOnScreen(mTempCoords);
+
+            mPositionXOnScreen = mTempCoords[0];
+            mPositionYOnScreen = mTempCoords[1];
+        }
+
+        public void onScrollChanged() {
+            mScrollHasChanged = true;
+        }
+    }
+
+    private abstract class PinnedPopupWindow implements TextViewPositionListener {
+        protected PopupWindow mPopupWindow;
+        protected ViewGroup mContentView;
+        int mPositionX, mPositionY;
+        int mClippingLimitLeft, mClippingLimitRight;
+
+        protected abstract void createPopupWindow();
+        protected abstract void initContentView();
+        protected abstract int getTextOffset();
+        protected abstract int getVerticalLocalPosition(int line);
+        protected abstract int clipVertically(int positionY);
+        protected void setUp() {
+        }
+
+        public PinnedPopupWindow() {
+            // Due to calling subclass methods in base constructor, subclass constructor is not
+            // called before subclass methods, e.g. createPopupWindow or initContentView. To give
+            // a chance to initialize subclasses, call setUp() method here.
+            // TODO: It is good to extract non trivial initialization code from constructor.
+            setUp();
+
+            createPopupWindow();
+
+            mPopupWindow.setWindowLayoutType(
+                    WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
+            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+
+            initContentView();
+
+            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+            mContentView.setLayoutParams(wrapContent);
+
+            mPopupWindow.setContentView(mContentView);
+        }
+
+        public void show() {
+            getPositionListener().addSubscriber(this, false /* offset is fixed */);
+
+            computeLocalPosition();
+
+            final PositionListener positionListener = getPositionListener();
+            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
+        }
+
+        protected void measureContent() {
+            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+            mContentView.measure(
+                    View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
+                            View.MeasureSpec.AT_MOST),
+                    View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
+                            View.MeasureSpec.AT_MOST));
+        }
+
+        /* The popup window will be horizontally centered on the getTextOffset() and vertically
+         * positioned according to viewportToContentHorizontalOffset.
+         *
+         * This method assumes that mContentView has properly been measured from its content. */
+        private void computeLocalPosition() {
+            measureContent();
+            final int width = mContentView.getMeasuredWidth();
+            final int offset = getTextOffset();
+            mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
+            mPositionX += mTextView.viewportToContentHorizontalOffset();
+
+            final int line = mTextView.getLayout().getLineForOffset(offset);
+            mPositionY = getVerticalLocalPosition(line);
+            mPositionY += mTextView.viewportToContentVerticalOffset();
+        }
+
+        private void updatePosition(int parentPositionX, int parentPositionY) {
+            int positionX = parentPositionX + mPositionX;
+            int positionY = parentPositionY + mPositionY;
+
+            positionY = clipVertically(positionY);
+
+            // Horizontal clipping
+            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+            final int width = mContentView.getMeasuredWidth();
+            positionX = Math.min(
+                    displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
+            positionX = Math.max(-mClippingLimitLeft, positionX);
+
+            if (isShowing()) {
+                mPopupWindow.update(positionX, positionY, -1, -1);
+            } else {
+                mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+                        positionX, positionY);
+            }
+        }
+
+        public void hide() {
+            if (!isShowing()) {
+                return;
+            }
+            mPopupWindow.dismiss();
+            getPositionListener().removeSubscriber(this);
+        }
+
+        @Override
+        public void updatePosition(int parentPositionX, int parentPositionY,
+                boolean parentPositionChanged, boolean parentScrolled) {
+            // Either parentPositionChanged or parentScrolled is true, check if still visible
+            if (isShowing() && isOffsetVisible(getTextOffset())) {
+                if (parentScrolled) computeLocalPosition();
+                updatePosition(parentPositionX, parentPositionY);
+            } else {
+                hide();
+            }
+        }
+
+        public boolean isShowing() {
+            return mPopupWindow.isShowing();
+        }
+    }
+
+    private static final class SuggestionInfo {
+        // Range of actual suggestion within mText
+        int mSuggestionStart, mSuggestionEnd;
+
+        // The SuggestionSpan that this TextView represents
+        final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
+
+        // The index of this suggestion inside suggestionSpan
+        int mSuggestionIndex;
+
+        final SpannableStringBuilder mText = new SpannableStringBuilder();
+
+        void clear() {
+            mSuggestionSpanInfo.clear();
+            mText.clear();
+        }
+
+        // Utility method to set attributes about a SuggestionSpan.
+        void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
+            mSuggestionSpanInfo.mSuggestionSpan = span;
+            mSuggestionSpanInfo.mSpanStart = spanStart;
+            mSuggestionSpanInfo.mSpanEnd = spanEnd;
+        }
+    }
+
+    private static final class SuggestionSpanInfo {
+        // The SuggestionSpan;
+        @Nullable
+        SuggestionSpan mSuggestionSpan;
+
+        // The SuggestionSpan start position
+        int mSpanStart;
+
+        // The SuggestionSpan end position
+        int mSpanEnd;
+
+        void clear() {
+            mSuggestionSpan = null;
+        }
+    }
+
+    private class SuggestionHelper {
+        private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
+                new SuggestionSpanComparator();
+        private final HashMap<SuggestionSpan, Integer> mSpansLengths =
+                new HashMap<SuggestionSpan, Integer>();
+
+        private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
+            public int compare(SuggestionSpan span1, SuggestionSpan span2) {
+                final int flag1 = span1.getFlags();
+                final int flag2 = span2.getFlags();
+                if (flag1 != flag2) {
+                    // The order here should match what is used in updateDrawState
+                    final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+                    final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
+                    final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+                    final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
+                    if (easy1 && !misspelled1) return -1;
+                    if (easy2 && !misspelled2) return 1;
+                    if (misspelled1) return -1;
+                    if (misspelled2) return 1;
+                }
+
+                return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
+            }
+        }
+
+        /**
+         * Returns the suggestion spans that cover the current cursor position. The suggestion
+         * spans are sorted according to the length of text that they are attached to.
+         */
+        private SuggestionSpan[] getSortedSuggestionSpans() {
+            int pos = mTextView.getSelectionStart();
+            Spannable spannable = (Spannable) mTextView.getText();
+            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
+
+            mSpansLengths.clear();
+            for (SuggestionSpan suggestionSpan : suggestionSpans) {
+                int start = spannable.getSpanStart(suggestionSpan);
+                int end = spannable.getSpanEnd(suggestionSpan);
+                mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
+            }
+
+            // The suggestions are sorted according to their types (easy correction first, then
+            // misspelled) and to the length of the text that they cover (shorter first).
+            Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
+            mSpansLengths.clear();
+
+            return suggestionSpans;
+        }
+
+        /**
+         * Gets the SuggestionInfo list that contains suggestion information at the current cursor
+         * position.
+         *
+         * @param suggestionInfos SuggestionInfo array the results will be set.
+         * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
+         * @return the number of suggestions actually fetched.
+         */
+        public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
+                @Nullable SuggestionSpanInfo misspelledSpanInfo) {
+            final Spannable spannable = (Spannable) mTextView.getText();
+            final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
+            final int nbSpans = suggestionSpans.length;
+            if (nbSpans == 0) return 0;
+
+            int numberOfSuggestions = 0;
+            for (final SuggestionSpan suggestionSpan : suggestionSpans) {
+                final int spanStart = spannable.getSpanStart(suggestionSpan);
+                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
+
+                if (misspelledSpanInfo != null
+                        && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+                    misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
+                    misspelledSpanInfo.mSpanStart = spanStart;
+                    misspelledSpanInfo.mSpanEnd = spanEnd;
+                }
+
+                final String[] suggestions = suggestionSpan.getSuggestions();
+                final int nbSuggestions = suggestions.length;
+                suggestionLoop:
+                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
+                    final String suggestion = suggestions[suggestionIndex];
+                    for (int i = 0; i < numberOfSuggestions; i++) {
+                        final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
+                        if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
+                            final int otherSpanStart =
+                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
+                            final int otherSpanEnd =
+                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
+                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
+                                continue suggestionLoop;
+                            }
+                        }
+                    }
+
+                    SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
+                    suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
+                    suggestionInfo.mSuggestionIndex = suggestionIndex;
+                    suggestionInfo.mSuggestionStart = 0;
+                    suggestionInfo.mSuggestionEnd = suggestion.length();
+                    suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
+                    numberOfSuggestions++;
+                    if (numberOfSuggestions >= suggestionInfos.length) {
+                        return numberOfSuggestions;
+                    }
+                }
+            }
+            return numberOfSuggestions;
+        }
+    }
+
+    @VisibleForTesting
+    public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
+        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
+
+        // Key of intent extras for inserting new word into user dictionary.
+        private static final String USER_DICTIONARY_EXTRA_WORD = "word";
+        private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
+
+        private SuggestionInfo[] mSuggestionInfos;
+        private int mNumberOfSuggestions;
+        private boolean mCursorWasVisibleBeforeSuggestions;
+        private boolean mIsShowingUp = false;
+        private SuggestionAdapter mSuggestionsAdapter;
+        private TextAppearanceSpan mHighlightSpan;  // TODO: Make mHighlightSpan final.
+        private TextView mAddToDictionaryButton;
+        private TextView mDeleteButton;
+        private ListView mSuggestionListView;
+        private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
+        private int mContainerMarginWidth;
+        private int mContainerMarginTop;
+        private LinearLayout mContainerView;
+        private Context mContext;  // TODO: Make mContext final.
+
+        private class CustomPopupWindow extends PopupWindow {
+
+            @Override
+            public void dismiss() {
+                if (!isShowing()) {
+                    return;
+                }
+                super.dismiss();
+                getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
+
+                // Safe cast since show() checks that mTextView.getText() is an Editable
+                ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
+
+                mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
+                if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
+                    getInsertionController().show();
+                }
+            }
+        }
+
+        public SuggestionsPopupWindow() {
+            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+        }
+
+        @Override
+        protected void setUp() {
+            mContext = applyDefaultTheme(mTextView.getContext());
+            mHighlightSpan = new TextAppearanceSpan(mContext,
+                    mTextView.mTextEditSuggestionHighlightStyle);
+        }
+
+        private Context applyDefaultTheme(Context originalContext) {
+            TypedArray a = originalContext.obtainStyledAttributes(
+                    new int[]{com.android.internal.R.attr.isLightTheme});
+            boolean isLightTheme = a.getBoolean(0, true);
+            int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
+                    : R.style.ThemeOverlay_Material_Dark;
+            a.recycle();
+            return new ContextThemeWrapper(originalContext, themeId);
+        }
+
+        @Override
+        protected void createPopupWindow() {
+            mPopupWindow = new CustomPopupWindow();
+            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+            mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
+            mPopupWindow.setFocusable(true);
+            mPopupWindow.setClippingEnabled(false);
+        }
+
+        @Override
+        protected void initContentView() {
+            final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                    Context.LAYOUT_INFLATER_SERVICE);
+            mContentView = (ViewGroup) inflater.inflate(
+                    mTextView.mTextEditSuggestionContainerLayout, null);
+
+            mContainerView = (LinearLayout) mContentView.findViewById(
+                    com.android.internal.R.id.suggestionWindowContainer);
+            ViewGroup.MarginLayoutParams lp =
+                    (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
+            mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
+            mContainerMarginTop = lp.topMargin;
+            mClippingLimitLeft = lp.leftMargin;
+            mClippingLimitRight = lp.rightMargin;
+
+            mSuggestionListView = (ListView) mContentView.findViewById(
+                    com.android.internal.R.id.suggestionContainer);
+
+            mSuggestionsAdapter = new SuggestionAdapter();
+            mSuggestionListView.setAdapter(mSuggestionsAdapter);
+            mSuggestionListView.setOnItemClickListener(this);
+
+            // Inflate the suggestion items once and for all.
+            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
+            for (int i = 0; i < mSuggestionInfos.length; i++) {
+                mSuggestionInfos[i] = new SuggestionInfo();
+            }
+
+            mAddToDictionaryButton = (TextView) mContentView.findViewById(
+                    com.android.internal.R.id.addToDictionaryButton);
+            mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
+                public void onClick(View v) {
+                    final SuggestionSpan misspelledSpan =
+                            findEquivalentSuggestionSpan(mMisspelledSpanInfo);
+                    if (misspelledSpan == null) {
+                        // Span has been removed.
+                        return;
+                    }
+                    final Editable editable = (Editable) mTextView.getText();
+                    final int spanStart = editable.getSpanStart(misspelledSpan);
+                    final int spanEnd = editable.getSpanEnd(misspelledSpan);
+                    if (spanStart < 0 || spanEnd <= spanStart) {
+                        return;
+                    }
+                    final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
+
+                    final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
+                    intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
+                    intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
+                            mTextView.getTextServicesLocale().toString());
+                    intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
+                    mTextView.getContext().startActivity(intent);
+                    // There is no way to know if the word was indeed added. Re-check.
+                    // TODO The ExtractEditText should remove the span in the original text instead
+                    editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
+                    Selection.setSelection(editable, spanEnd);
+                    updateSpellCheckSpans(spanStart, spanEnd, false);
+                    hideWithCleanUp();
+                }
+            });
+
+            mDeleteButton = (TextView) mContentView.findViewById(
+                    com.android.internal.R.id.deleteButton);
+            mDeleteButton.setOnClickListener(new View.OnClickListener() {
+                public void onClick(View v) {
+                    final Editable editable = (Editable) mTextView.getText();
+
+                    final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
+                    int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
+                    if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
+                        // Do not leave two adjacent spaces after deletion, or one at beginning of
+                        // text
+                        if (spanUnionEnd < editable.length()
+                                && Character.isSpaceChar(editable.charAt(spanUnionEnd))
+                                && (spanUnionStart == 0
+                                        || Character.isSpaceChar(
+                                                editable.charAt(spanUnionStart - 1)))) {
+                            spanUnionEnd = spanUnionEnd + 1;
+                        }
+                        mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
+                    }
+                    hideWithCleanUp();
+                }
+            });
+
+        }
+
+        public boolean isShowingUp() {
+            return mIsShowingUp;
+        }
+
+        public void onParentLostFocus() {
+            mIsShowingUp = false;
+        }
+
+        private class SuggestionAdapter extends BaseAdapter {
+            private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
+                    Context.LAYOUT_INFLATER_SERVICE);
+
+            @Override
+            public int getCount() {
+                return mNumberOfSuggestions;
+            }
+
+            @Override
+            public Object getItem(int position) {
+                return mSuggestionInfos[position];
+            }
+
+            @Override
+            public long getItemId(int position) {
+                return position;
+            }
+
+            @Override
+            public View getView(int position, View convertView, ViewGroup parent) {
+                TextView textView = (TextView) convertView;
+
+                if (textView == null) {
+                    textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
+                            parent, false);
+                }
+
+                final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+                textView.setText(suggestionInfo.mText);
+                return textView;
+            }
+        }
+
+        @VisibleForTesting
+        public ViewGroup getContentViewForTesting() {
+            return mContentView;
+        }
+
+        @Override
+        public void show() {
+            if (!(mTextView.getText() instanceof Editable)) return;
+            if (extractedTextModeWillBeStarted()) {
+                return;
+            }
+
+            if (updateSuggestions()) {
+                mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+                mTextView.setCursorVisible(false);
+                mIsShowingUp = true;
+                super.show();
+            }
+        }
+
+        @Override
+        protected void measureContent() {
+            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+            final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
+                    displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
+            final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
+                    displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
+
+            int width = 0;
+            View view = null;
+            for (int i = 0; i < mNumberOfSuggestions; i++) {
+                view = mSuggestionsAdapter.getView(i, view, mContentView);
+                view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
+                view.measure(horizontalMeasure, verticalMeasure);
+                width = Math.max(width, view.getMeasuredWidth());
+            }
+
+            if (mAddToDictionaryButton.getVisibility() != View.GONE) {
+                mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
+                width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
+            }
+
+            mDeleteButton.measure(horizontalMeasure, verticalMeasure);
+            width = Math.max(width, mDeleteButton.getMeasuredWidth());
+
+            width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
+                    + mContainerMarginWidth;
+
+            // Enforce the width based on actual text widths
+            mContentView.measure(
+                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
+                    verticalMeasure);
+
+            Drawable popupBackground = mPopupWindow.getBackground();
+            if (popupBackground != null) {
+                if (mTempRect == null) mTempRect = new Rect();
+                popupBackground.getPadding(mTempRect);
+                width += mTempRect.left + mTempRect.right;
+            }
+            mPopupWindow.setWidth(width);
+        }
+
+        @Override
+        protected int getTextOffset() {
+            return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
+        }
+
+        @Override
+        protected int getVerticalLocalPosition(int line) {
+            final Layout layout = mTextView.getLayout();
+            return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
+        }
+
+        @Override
+        protected int clipVertically(int positionY) {
+            final int height = mContentView.getMeasuredHeight();
+            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+            return Math.min(positionY, displayMetrics.heightPixels - height);
+        }
+
+        private void hideWithCleanUp() {
+            for (final SuggestionInfo info : mSuggestionInfos) {
+                info.clear();
+            }
+            mMisspelledSpanInfo.clear();
+            hide();
+        }
+
+        private boolean updateSuggestions() {
+            Spannable spannable = (Spannable) mTextView.getText();
+            mNumberOfSuggestions =
+                    mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
+            if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
+                return false;
+            }
+
+            int spanUnionStart = mTextView.getText().length();
+            int spanUnionEnd = 0;
+
+            for (int i = 0; i < mNumberOfSuggestions; i++) {
+                final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
+                spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
+                spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
+            }
+            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
+                spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
+                spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
+            }
+
+            for (int i = 0; i < mNumberOfSuggestions; i++) {
+                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
+            }
+
+            // Make "Add to dictionary" item visible if there is a span with the misspelled flag
+            int addToDictionaryButtonVisibility = View.GONE;
+            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
+                if (mMisspelledSpanInfo.mSpanStart >= 0
+                        && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
+                    addToDictionaryButtonVisibility = View.VISIBLE;
+                }
+            }
+            mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
+
+            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
+            final int underlineColor;
+            if (mNumberOfSuggestions != 0) {
+                underlineColor =
+                        mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
+            } else {
+                underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
+            }
+
+            if (underlineColor == 0) {
+                // Fallback on the default highlight color when the first span does not provide one
+                mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
+            } else {
+                final float BACKGROUND_TRANSPARENCY = 0.4f;
+                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
+                mSuggestionRangeSpan.setBackgroundColor(
+                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
+            }
+            spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+            mSuggestionsAdapter.notifyDataSetChanged();
+            return true;
+        }
+
+        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
+                int unionEnd) {
+            final Spannable text = (Spannable) mTextView.getText();
+            final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
+            final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
+
+            // Adjust the start/end of the suggestion span
+            suggestionInfo.mSuggestionStart = spanStart - unionStart;
+            suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
+                    + suggestionInfo.mText.length();
+
+            suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+            // Add the text before and after the span.
+            final String textAsString = text.toString();
+            suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
+            suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
+        }
+
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+            replaceWithSuggestion(suggestionInfo);
+            hideWithCleanUp();
+        }
+    }
+
+    /**
+     * An ActionMode Callback class that is used to provide actions while in text insertion or
+     * selection mode.
+     *
+     * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
+     * actions, depending on which of these this TextView supports and the current selection.
+     */
+    private class TextActionModeCallback extends ActionMode.Callback2 {
+        private final Path mSelectionPath = new Path();
+        private final RectF mSelectionBounds = new RectF();
+        private final boolean mHasSelection;
+        private final int mHandleHeight;
+
+        public TextActionModeCallback(boolean hasSelection) {
+            mHasSelection = hasSelection;
+            if (mHasSelection) {
+                SelectionModifierCursorController selectionController = getSelectionController();
+                if (selectionController.mStartHandle == null) {
+                    // As these are for initializing selectionController, hide() must be called.
+                    selectionController.initDrawables();
+                    selectionController.initHandles();
+                    selectionController.hide();
+                }
+                mHandleHeight = Math.max(
+                        mSelectHandleLeft.getMinimumHeight(),
+                        mSelectHandleRight.getMinimumHeight());
+            } else {
+                InsertionPointCursorController insertionController = getInsertionController();
+                if (insertionController != null) {
+                    insertionController.getHandle();
+                    mHandleHeight = mSelectHandleCenter.getMinimumHeight();
+                } else {
+                    mHandleHeight = 0;
+                }
+            }
+        }
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            mode.setTitle(null);
+            mode.setSubtitle(null);
+            mode.setTitleOptionalHint(true);
+            populateMenuWithItems(menu);
+
+            Callback customCallback = getCustomCallback();
+            if (customCallback != null) {
+                if (!customCallback.onCreateActionMode(mode, menu)) {
+                    // The custom mode can choose to cancel the action mode, dismiss selection.
+                    Selection.setSelection((Spannable) mTextView.getText(),
+                            mTextView.getSelectionEnd());
+                    return false;
+                }
+            }
+
+            if (mTextView.canProcessText()) {
+                mProcessTextIntentActionsHandler.onInitializeMenu(menu);
+            }
+
+            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
+                if (mHasSelection && !mTextView.hasTransientState()) {
+                    mTextView.setHasTransientState(true);
+                }
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        private Callback getCustomCallback() {
+            return mHasSelection
+                    ? mCustomSelectionActionModeCallback
+                    : mCustomInsertionActionModeCallback;
+        }
+
+        private void populateMenuWithItems(Menu menu) {
+            if (mTextView.canCut()) {
+                menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
+                        com.android.internal.R.string.cut)
+                                .setAlphabeticShortcut('x')
+                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+            }
+
+            if (mTextView.canCopy()) {
+                menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
+                        com.android.internal.R.string.copy)
+                                .setAlphabeticShortcut('c')
+                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+            }
+
+            if (mTextView.canPaste()) {
+                menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
+                        com.android.internal.R.string.paste)
+                                .setAlphabeticShortcut('v')
+                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+            }
+
+            if (mTextView.canShare()) {
+                menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
+                        com.android.internal.R.string.share)
+                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            }
+
+            if (mTextView.canRequestAutofill()) {
+                final String selected = mTextView.getSelectedText();
+                if (selected == null || selected.isEmpty()) {
+                    menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
+                            com.android.internal.R.string.autofill)
+                            .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
+                }
+            }
+
+            if (mTextView.canPasteAsPlainText()) {
+                menu.add(
+                        Menu.NONE,
+                        TextView.ID_PASTE_AS_PLAIN_TEXT,
+                        MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
+                        com.android.internal.R.string.paste_as_plain_text)
+                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            }
+
+            updateSelectAllItem(menu);
+            updateReplaceItem(menu);
+            updateAssistMenuItem(menu);
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            updateSelectAllItem(menu);
+            updateReplaceItem(menu);
+            updateAssistMenuItem(menu);
+
+            Callback customCallback = getCustomCallback();
+            if (customCallback != null) {
+                return customCallback.onPrepareActionMode(mode, menu);
+            }
+            return true;
+        }
+
+        private void updateSelectAllItem(Menu menu) {
+            boolean canSelectAll = mTextView.canSelectAllText();
+            boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
+            if (canSelectAll && !selectAllItemExists) {
+                menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
+                        com.android.internal.R.string.selectAll)
+                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            } else if (!canSelectAll && selectAllItemExists) {
+                menu.removeItem(TextView.ID_SELECT_ALL);
+            }
+        }
+
+        private void updateReplaceItem(Menu menu) {
+            boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
+            boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
+            if (canReplace && !replaceItemExists) {
+                menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
+                        com.android.internal.R.string.replace)
+                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            } else if (!canReplace && replaceItemExists) {
+                menu.removeItem(TextView.ID_REPLACE);
+            }
+        }
+
+        private void updateAssistMenuItem(Menu menu) {
+            menu.removeItem(TextView.ID_ASSIST);
+            final TextClassification textClassification =
+                    getSelectionActionModeHelper().getTextClassification();
+            if (canAssist()) {
+                menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST,
+                        textClassification.getLabel())
+                        .setIcon(textClassification.getIcon())
+                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
+            }
+        }
+
+        private boolean canAssist() {
+            final TextClassification textClassification =
+                    getSelectionActionModeHelper().getTextClassification();
+            return mTextView.isDeviceProvisioned()
+                    && textClassification != null
+                    && (textClassification.getIcon() != null
+                            || !TextUtils.isEmpty(textClassification.getLabel()))
+                    && (textClassification.getOnClickListener() != null
+                            || (textClassification.getIntent() != null
+                                    && mTextView.getContext().canStartActivityForResult()));
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            getSelectionActionModeHelper().onSelectionAction(item.getItemId());
+
+            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
+                return true;
+            }
+            Callback customCallback = getCustomCallback();
+            if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
+                return true;
+            }
+            final TextClassification textClassification =
+                    getSelectionActionModeHelper().getTextClassification();
+            if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
+                final OnClickListener onClickListener =
+                        textClassification.getOnClickListener();
+                if (onClickListener != null) {
+                    onClickListener.onClick(mTextView);
+                } else {
+                    final Intent intent = textClassification.getIntent();
+                    if (intent != null) {
+                        TextClassification.createStartActivityOnClickListener(
+                                mTextView.getContext(), intent)
+                                .onClick(mTextView);
+                    }
+                }
+                stopTextActionMode();
+                return true;
+            }
+            return mTextView.onTextContextMenuItem(item.getItemId());
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
+            getSelectionActionModeHelper().onDestroyActionMode();
+            mTextActionMode = null;
+            Callback customCallback = getCustomCallback();
+            if (customCallback != null) {
+                customCallback.onDestroyActionMode(mode);
+            }
+
+            if (!mPreserveSelection) {
+                /*
+                 * Leave current selection when we tentatively destroy action mode for the
+                 * selection. If we're detaching from a window, we'll bring back the selection
+                 * mode when (if) we get reattached.
+                 */
+                Selection.setSelection((Spannable) mTextView.getText(),
+                        mTextView.getSelectionEnd());
+            }
+
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.hide();
+            }
+        }
+
+        @Override
+        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
+            if (!view.equals(mTextView) || mTextView.getLayout() == null) {
+                super.onGetContentRect(mode, view, outRect);
+                return;
+            }
+            if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
+                // We have a selection.
+                mSelectionPath.reset();
+                mTextView.getLayout().getSelectionPath(
+                        mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
+                mSelectionPath.computeBounds(mSelectionBounds, true);
+                mSelectionBounds.bottom += mHandleHeight;
+            } else {
+                // We have a cursor.
+                Layout layout = mTextView.getLayout();
+                int line = layout.getLineForOffset(mTextView.getSelectionStart());
+                float primaryHorizontal = clampHorizontalPosition(null,
+                        layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
+                mSelectionBounds.set(
+                        primaryHorizontal,
+                        layout.getLineTop(line),
+                        primaryHorizontal,
+                        layout.getLineBottom(line) - layout.getLineBottom(line) + mHandleHeight);
+            }
+            // Take TextView's padding and scroll into account.
+            int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
+            int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
+            outRect.set(
+                    (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
+                    (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
+                    (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
+                    (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
+        }
+    }
+
+    /**
+     * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
+     * while the input method is requesting the cursor/anchor position. Does nothing as long as
+     * {@link InputMethodManager#isWatchingCursor(View)} returns false.
+     */
+    private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
+        final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
+        final int[] mTmpIntOffset = new int[2];
+        final Matrix mViewToScreenMatrix = new Matrix();
+
+        @Override
+        public void updatePosition(int parentPositionX, int parentPositionY,
+                boolean parentPositionChanged, boolean parentScrolled) {
+            final InputMethodState ims = mInputMethodState;
+            if (ims == null || ims.mBatchEditNesting > 0) {
+                return;
+            }
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            if (null == imm) {
+                return;
+            }
+            if (!imm.isActive(mTextView)) {
+                return;
+            }
+            // Skip if the IME has not requested the cursor/anchor position.
+            if (!imm.isCursorAnchorInfoEnabled()) {
+                return;
+            }
+            Layout layout = mTextView.getLayout();
+            if (layout == null) {
+                return;
+            }
+
+            final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
+            builder.reset();
+
+            final int selectionStart = mTextView.getSelectionStart();
+            builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
+
+            // Construct transformation matrix from view local coordinates to screen coordinates.
+            mViewToScreenMatrix.set(mTextView.getMatrix());
+            mTextView.getLocationOnScreen(mTmpIntOffset);
+            mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
+            builder.setMatrix(mViewToScreenMatrix);
+
+            final float viewportToContentHorizontalOffset =
+                    mTextView.viewportToContentHorizontalOffset();
+            final float viewportToContentVerticalOffset =
+                    mTextView.viewportToContentVerticalOffset();
+
+            final CharSequence text = mTextView.getText();
+            if (text instanceof Spannable) {
+                final Spannable sp = (Spannable) text;
+                int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
+                int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
+                if (composingTextEnd < composingTextStart) {
+                    final int temp = composingTextEnd;
+                    composingTextEnd = composingTextStart;
+                    composingTextStart = temp;
+                }
+                final boolean hasComposingText =
+                        (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
+                if (hasComposingText) {
+                    final CharSequence composingText = text.subSequence(composingTextStart,
+                            composingTextEnd);
+                    builder.setComposingText(composingTextStart, composingText);
+                    mTextView.populateCharacterBounds(builder, composingTextStart,
+                            composingTextEnd, viewportToContentHorizontalOffset,
+                            viewportToContentVerticalOffset);
+                }
+            }
+
+            // Treat selectionStart as the insertion point.
+            if (0 <= selectionStart) {
+                final int offset = selectionStart;
+                final int line = layout.getLineForOffset(offset);
+                final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
+                        + viewportToContentHorizontalOffset;
+                final float insertionMarkerTop = layout.getLineTop(line)
+                        + viewportToContentVerticalOffset;
+                final float insertionMarkerBaseline = layout.getLineBaseline(line)
+                        + viewportToContentVerticalOffset;
+                final float insertionMarkerBottom = layout.getLineBottomWithoutSpacing(line)
+                        + viewportToContentVerticalOffset;
+                final boolean isTopVisible = mTextView
+                        .isPositionVisible(insertionMarkerX, insertionMarkerTop);
+                final boolean isBottomVisible = mTextView
+                        .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
+                int insertionMarkerFlags = 0;
+                if (isTopVisible || isBottomVisible) {
+                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
+                }
+                if (!isTopVisible || !isBottomVisible) {
+                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
+                }
+                if (layout.isRtlCharAt(offset)) {
+                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
+                }
+                builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
+                        insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
+            }
+
+            imm.updateCursorAnchorInfo(mTextView, builder.build());
+        }
+    }
+
+    @VisibleForTesting
+    public abstract class HandleView extends View implements TextViewPositionListener {
+        protected Drawable mDrawable;
+        protected Drawable mDrawableLtr;
+        protected Drawable mDrawableRtl;
+        private final PopupWindow mContainer;
+        // Position with respect to the parent TextView
+        private int mPositionX, mPositionY;
+        private boolean mIsDragging;
+        // Offset from touch position to mPosition
+        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
+        protected int mHotspotX;
+        protected int mHorizontalGravity;
+        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
+        private float mTouchOffsetY;
+        // Where the touch position should be on the handle to ensure a maximum cursor visibility
+        private float mIdealVerticalOffset;
+        // Parent's (TextView) previous position in window
+        private int mLastParentX, mLastParentY;
+        // Parent's (TextView) previous position on screen
+        private int mLastParentXOnScreen, mLastParentYOnScreen;
+        // Previous text character offset
+        protected int mPreviousOffset = -1;
+        // Previous text character offset
+        private boolean mPositionHasChanged = true;
+        // Minimum touch target size for handles
+        private int mMinSize;
+        // Indicates the line of text that the handle is on.
+        protected int mPrevLine = UNSET_LINE;
+        // Indicates the line of text that the user was touching. This can differ from mPrevLine
+        // when selecting text when the handles jump to the end / start of words which may be on
+        // a different line.
+        protected int mPreviousLineTouched = UNSET_LINE;
+
+        private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
+            super(mTextView.getContext());
+            setId(id);
+            mContainer = new PopupWindow(mTextView.getContext(), null,
+                    com.android.internal.R.attr.textSelectHandleWindowStyle);
+            mContainer.setSplitTouchEnabled(true);
+            mContainer.setClippingEnabled(false);
+            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
+            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
+            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
+            mContainer.setContentView(this);
+
+            mDrawableLtr = drawableLtr;
+            mDrawableRtl = drawableRtl;
+            mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
+                    com.android.internal.R.dimen.text_handle_min_size);
+
+            updateDrawable();
+
+            final int handleHeight = getPreferredHeight();
+            mTouchOffsetY = -0.3f * handleHeight;
+            mIdealVerticalOffset = 0.7f * handleHeight;
+        }
+
+        public float getIdealVerticalOffset() {
+            return mIdealVerticalOffset;
+        }
+
+        protected void updateDrawable() {
+            if (mIsDragging) {
+                // Don't update drawable during dragging.
+                return;
+            }
+            final Layout layout = mTextView.getLayout();
+            if (layout == null) {
+                return;
+            }
+            final int offset = getCurrentCursorOffset();
+            final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
+            final Drawable oldDrawable = mDrawable;
+            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
+            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
+            mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
+            if (oldDrawable != mDrawable && isShowing()) {
+                // Update popup window position.
+                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
+                        - getHorizontalOffset() + getCursorOffset();
+                mPositionX += mTextView.viewportToContentHorizontalOffset();
+                mPositionHasChanged = true;
+                updatePosition(mLastParentX, mLastParentY, false, false);
+                postInvalidate();
+            }
+        }
+
+        protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
+        protected abstract int getHorizontalGravity(boolean isRtlRun);
+
+        // Touch-up filter: number of previous positions remembered
+        private static final int HISTORY_SIZE = 5;
+        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
+        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
+        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
+        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
+        private int mPreviousOffsetIndex = 0;
+        private int mNumberPreviousOffsets = 0;
+
+        private void startTouchUpFilter(int offset) {
+            mNumberPreviousOffsets = 0;
+            addPositionToTouchUpFilter(offset);
+        }
+
+        private void addPositionToTouchUpFilter(int offset) {
+            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
+            mPreviousOffsets[mPreviousOffsetIndex] = offset;
+            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
+            mNumberPreviousOffsets++;
+        }
+
+        private void filterOnTouchUp(boolean fromTouchScreen) {
+            final long now = SystemClock.uptimeMillis();
+            int i = 0;
+            int index = mPreviousOffsetIndex;
+            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
+            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
+                i++;
+                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
+            }
+
+            if (i > 0 && i < iMax
+                    && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
+                positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
+            }
+        }
+
+        public boolean offsetHasBeenChanged() {
+            return mNumberPreviousOffsets > 1;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
+        }
+
+        @Override
+        public void invalidate() {
+            super.invalidate();
+            if (isShowing()) {
+                positionAtCursorOffset(getCurrentCursorOffset(), true, false);
+            }
+        };
+
+        private int getPreferredWidth() {
+            return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
+        }
+
+        private int getPreferredHeight() {
+            return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
+        }
+
+        public void show() {
+            if (isShowing()) return;
+
+            getPositionListener().addSubscriber(this, true /* local position may change */);
+
+            // Make sure the offset is always considered new, even when focusing at same position
+            mPreviousOffset = -1;
+            positionAtCursorOffset(getCurrentCursorOffset(), false, false);
+        }
+
+        protected void dismiss() {
+            mIsDragging = false;
+            mContainer.dismiss();
+            onDetached();
+        }
+
+        public void hide() {
+            dismiss();
+
+            getPositionListener().removeSubscriber(this);
+        }
+
+        public boolean isShowing() {
+            return mContainer.isShowing();
+        }
+
+        private boolean isVisible() {
+            // Always show a dragging handle.
+            if (mIsDragging) {
+                return true;
+            }
+
+            if (mTextView.isInBatchEditMode()) {
+                return false;
+            }
+
+            return mTextView.isPositionVisible(
+                    mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
+        }
+
+        public abstract int getCurrentCursorOffset();
+
+        protected abstract void updateSelection(int offset);
+
+        protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
+
+        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
+            return layout.isRtlCharAt(offset);
+        }
+
+        @VisibleForTesting
+        public float getHorizontal(@NonNull Layout layout, int offset) {
+            return layout.getPrimaryHorizontal(offset);
+        }
+
+        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
+            return mTextView.getOffsetAtCoordinate(line, x);
+        }
+
+        /**
+         * @param offset Cursor offset. Must be in [-1, length].
+         * @param forceUpdatePosition whether to force update the position.  This should be true
+         * when If the parent has been scrolled, for example.
+         * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
+         * touch screen.
+         */
+        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+                boolean fromTouchScreen) {
+            // A HandleView relies on the layout, which may be nulled by external methods
+            Layout layout = mTextView.getLayout();
+            if (layout == null) {
+                // Will update controllers' state, hiding them and stopping selection mode if needed
+                prepareCursorControllers();
+                return;
+            }
+            layout = mTextView.getLayout();
+
+            boolean offsetChanged = offset != mPreviousOffset;
+            if (offsetChanged || forceUpdatePosition) {
+                if (offsetChanged) {
+                    updateSelection(offset);
+                    if (fromTouchScreen && mHapticTextHandleEnabled) {
+                        mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+                    }
+                    addPositionToTouchUpFilter(offset);
+                }
+                final int line = layout.getLineForOffset(offset);
+                mPrevLine = line;
+
+                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
+                        - getHorizontalOffset() + getCursorOffset();
+                mPositionY = layout.getLineBottomWithoutSpacing(line);
+
+                // Take TextView's padding and scroll into account.
+                mPositionX += mTextView.viewportToContentHorizontalOffset();
+                mPositionY += mTextView.viewportToContentVerticalOffset();
+
+                mPreviousOffset = offset;
+                mPositionHasChanged = true;
+            }
+        }
+
+        /**
+         * Return the clamped horizontal position for the cursor.
+         *
+         * @param layout Text layout.
+         * @param offset Character offset for the cursor.
+         * @return The clamped horizontal position for the cursor.
+         */
+        int getCursorHorizontalPosition(Layout layout, int offset) {
+            return (int) (getHorizontal(layout, offset) - 0.5f);
+        }
+
+        @Override
+        public void updatePosition(int parentPositionX, int parentPositionY,
+                boolean parentPositionChanged, boolean parentScrolled) {
+            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
+            if (parentPositionChanged || mPositionHasChanged) {
+                if (mIsDragging) {
+                    // Update touchToWindow offset in case of parent scrolling while dragging
+                    if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
+                        mTouchToWindowOffsetX += parentPositionX - mLastParentX;
+                        mTouchToWindowOffsetY += parentPositionY - mLastParentY;
+                        mLastParentX = parentPositionX;
+                        mLastParentY = parentPositionY;
+                    }
+
+                    onHandleMoved();
+                }
+
+                if (isVisible()) {
+                    // Transform to the window coordinates to follow the view tranformation.
+                    final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
+                    mTextView.transformFromViewToWindowSpace(pts);
+                    pts[0] -= mHotspotX + getHorizontalOffset();
+
+                    if (isShowing()) {
+                        mContainer.update(pts[0], pts[1], -1, -1);
+                    } else {
+                        mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
+                    }
+                } else {
+                    if (isShowing()) {
+                        dismiss();
+                    }
+                }
+
+                mPositionHasChanged = false;
+            }
+        }
+
+        @Override
+        protected void onDraw(Canvas c) {
+            final int drawWidth = mDrawable.getIntrinsicWidth();
+            final int left = getHorizontalOffset();
+
+            mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
+            mDrawable.draw(c);
+        }
+
+        private int getHorizontalOffset() {
+            final int width = getPreferredWidth();
+            final int drawWidth = mDrawable.getIntrinsicWidth();
+            final int left;
+            switch (mHorizontalGravity) {
+                case Gravity.LEFT:
+                    left = 0;
+                    break;
+                default:
+                case Gravity.CENTER:
+                    left = (width - drawWidth) / 2;
+                    break;
+                case Gravity.RIGHT:
+                    left = width - drawWidth;
+                    break;
+            }
+            return left;
+        }
+
+        protected int getCursorOffset() {
+            return 0;
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            updateFloatingToolbarVisibility(ev);
+
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN: {
+                    startTouchUpFilter(getCurrentCursorOffset());
+
+                    final PositionListener positionListener = getPositionListener();
+                    mLastParentX = positionListener.getPositionX();
+                    mLastParentY = positionListener.getPositionY();
+                    mLastParentXOnScreen = positionListener.getPositionXOnScreen();
+                    mLastParentYOnScreen = positionListener.getPositionYOnScreen();
+
+                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
+                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
+                    mTouchToWindowOffsetX = xInWindow - mPositionX;
+                    mTouchToWindowOffsetY = yInWindow - mPositionY;
+
+                    mIsDragging = true;
+                    mPreviousLineTouched = UNSET_LINE;
+                    break;
+                }
+
+                case MotionEvent.ACTION_MOVE: {
+                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
+                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
+
+                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
+                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
+                    final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
+                    float newVerticalOffset;
+                    if (previousVerticalOffset < mIdealVerticalOffset) {
+                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
+                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
+                    } else {
+                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
+                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
+                    }
+                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
+
+                    final float newPosX =
+                            xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
+                    final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
+
+                    updatePosition(newPosX, newPosY,
+                            ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+                    break;
+                }
+
+                case MotionEvent.ACTION_UP:
+                    filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+                    mIsDragging = false;
+                    updateDrawable();
+                    break;
+
+                case MotionEvent.ACTION_CANCEL:
+                    mIsDragging = false;
+                    updateDrawable();
+                    break;
+            }
+            return true;
+        }
+
+        public boolean isDragging() {
+            return mIsDragging;
+        }
+
+        void onHandleMoved() {}
+
+        public void onDetached() {}
+    }
+
+    private class InsertionHandleView extends HandleView {
+        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
+        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
+
+        // Used to detect taps on the insertion handle, which will affect the insertion action mode
+        private float mDownPositionX, mDownPositionY;
+        private Runnable mHider;
+
+        public InsertionHandleView(Drawable drawable) {
+            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
+        }
+
+        @Override
+        public void show() {
+            super.show();
+
+            final long durationSinceCutOrCopy =
+                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
+
+            // Cancel the single tap delayed runnable.
+            if (mInsertionActionModeRunnable != null
+                    && ((mTapState == TAP_STATE_DOUBLE_TAP)
+                            || (mTapState == TAP_STATE_TRIPLE_CLICK)
+                            || isCursorInsideEasyCorrectionSpan())) {
+                mTextView.removeCallbacks(mInsertionActionModeRunnable);
+            }
+
+            // Prepare and schedule the single tap runnable to run exactly after the double tap
+            // timeout has passed.
+            if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
+                    && !isCursorInsideEasyCorrectionSpan()
+                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
+                if (mTextActionMode == null) {
+                    if (mInsertionActionModeRunnable == null) {
+                        mInsertionActionModeRunnable = new Runnable() {
+                            @Override
+                            public void run() {
+                                startInsertionActionMode();
+                            }
+                        };
+                    }
+                    mTextView.postDelayed(
+                            mInsertionActionModeRunnable,
+                            ViewConfiguration.getDoubleTapTimeout() + 1);
+                }
+
+            }
+
+            hideAfterDelay();
+        }
+
+        private void hideAfterDelay() {
+            if (mHider == null) {
+                mHider = new Runnable() {
+                    public void run() {
+                        hide();
+                    }
+                };
+            } else {
+                removeHiderCallback();
+            }
+            mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
+        }
+
+        private void removeHiderCallback() {
+            if (mHider != null) {
+                mTextView.removeCallbacks(mHider);
+            }
+        }
+
+        @Override
+        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+            return drawable.getIntrinsicWidth() / 2;
+        }
+
+        @Override
+        protected int getHorizontalGravity(boolean isRtlRun) {
+            return Gravity.CENTER_HORIZONTAL;
+        }
+
+        @Override
+        protected int getCursorOffset() {
+            int offset = super.getCursorOffset();
+            if (mCursorDrawable != null) {
+                mCursorDrawable.getPadding(mTempRect);
+                offset += (mCursorDrawable.getIntrinsicWidth()
+                           - mTempRect.left - mTempRect.right) / 2;
+            }
+            return offset;
+        }
+
+        @Override
+        int getCursorHorizontalPosition(Layout layout, int offset) {
+            if (mCursorDrawable != null) {
+                final float horizontal = getHorizontal(layout, offset);
+                return clampHorizontalPosition(mCursorDrawable, horizontal) + mTempRect.left;
+            }
+            return super.getCursorHorizontalPosition(layout, offset);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            final boolean result = super.onTouchEvent(ev);
+
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    mDownPositionX = ev.getRawX();
+                    mDownPositionY = ev.getRawY();
+                    break;
+
+                case MotionEvent.ACTION_UP:
+                    if (!offsetHasBeenChanged()) {
+                        final float deltaX = mDownPositionX - ev.getRawX();
+                        final float deltaY = mDownPositionY - ev.getRawY();
+                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+                                mTextView.getContext());
+                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
+
+                        if (distanceSquared < touchSlop * touchSlop) {
+                            // Tapping on the handle toggles the insertion action mode.
+                            if (mTextActionMode != null) {
+                                stopTextActionMode();
+                            } else {
+                                startInsertionActionMode();
+                            }
+                        }
+                    } else {
+                        if (mTextActionMode != null) {
+                            mTextActionMode.invalidateContentRect();
+                        }
+                    }
+                    hideAfterDelay();
+                    break;
+
+                case MotionEvent.ACTION_CANCEL:
+                    hideAfterDelay();
+                    break;
+
+                default:
+                    break;
+            }
+
+            return result;
+        }
+
+        @Override
+        public int getCurrentCursorOffset() {
+            return mTextView.getSelectionStart();
+        }
+
+        @Override
+        public void updateSelection(int offset) {
+            Selection.setSelection((Spannable) mTextView.getText(), offset);
+        }
+
+        @Override
+        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
+            Layout layout = mTextView.getLayout();
+            int offset;
+            if (layout != null) {
+                if (mPreviousLineTouched == UNSET_LINE) {
+                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
+                }
+                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
+                offset = getOffsetAtCoordinate(layout, currLine, x);
+                mPreviousLineTouched = currLine;
+            } else {
+                offset = -1;
+            }
+            positionAtCursorOffset(offset, false, fromTouchScreen);
+            if (mTextActionMode != null) {
+                invalidateActionMode();
+            }
+        }
+
+        @Override
+        void onHandleMoved() {
+            super.onHandleMoved();
+            removeHiderCallback();
+        }
+
+        @Override
+        public void onDetached() {
+            super.onDetached();
+            removeHiderCallback();
+        }
+    }
+
+    @Retention(RetentionPolicy.SOURCE)
+    @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
+    public @interface HandleType {}
+    public static final int HANDLE_TYPE_SELECTION_START = 0;
+    public static final int HANDLE_TYPE_SELECTION_END = 1;
+
+    /** For selection handles */
+    @VisibleForTesting
+    public final class SelectionHandleView extends HandleView {
+        // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
+        // end (HANDLE_TYPE_SELECTION_END).
+        @HandleType
+        private final int mHandleType;
+        // Indicates whether the cursor is making adjustments within a word.
+        private boolean mInWord = false;
+        // Difference between touch position and word boundary position.
+        private float mTouchWordDelta;
+        // X value of the previous updatePosition call.
+        private float mPrevX;
+        // Indicates if the handle has moved a boundary between LTR and RTL text.
+        private boolean mLanguageDirectionChanged = false;
+        // Distance from edge of horizontally scrolling text view
+        // to use to switch to character mode.
+        private final float mTextViewEdgeSlop;
+        // Used to save text view location.
+        private final int[] mTextViewLocation = new int[2];
+
+        public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
+                @HandleType int handleType) {
+            super(drawableLtr, drawableRtl, id);
+            mHandleType = handleType;
+            ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
+            mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
+        }
+
+        private boolean isStartHandle() {
+            return mHandleType == HANDLE_TYPE_SELECTION_START;
+        }
+
+        @Override
+        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+            if (isRtlRun == isStartHandle()) {
+                return drawable.getIntrinsicWidth() / 4;
+            } else {
+                return (drawable.getIntrinsicWidth() * 3) / 4;
+            }
+        }
+
+        @Override
+        protected int getHorizontalGravity(boolean isRtlRun) {
+            return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
+        }
+
+        @Override
+        public int getCurrentCursorOffset() {
+            return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
+        }
+
+        @Override
+        protected void updateSelection(int offset) {
+            if (isStartHandle()) {
+                Selection.setSelection((Spannable) mTextView.getText(), offset,
+                        mTextView.getSelectionEnd());
+            } else {
+                Selection.setSelection((Spannable) mTextView.getText(),
+                        mTextView.getSelectionStart(), offset);
+            }
+            updateDrawable();
+            if (mTextActionMode != null) {
+                invalidateActionMode();
+            }
+        }
+
+        @Override
+        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
+            final Layout layout = mTextView.getLayout();
+            if (layout == null) {
+                // HandleView will deal appropriately in positionAtCursorOffset when
+                // layout is null.
+                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
+                        fromTouchScreen);
+                return;
+            }
+
+            if (mPreviousLineTouched == UNSET_LINE) {
+                mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
+            }
+
+            boolean positionCursor = false;
+            final int anotherHandleOffset =
+                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
+            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
+            int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
+
+            if (isStartHandle() && initialOffset >= anotherHandleOffset
+                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
+                // Handles have crossed, bound it to the first selected line and
+                // adjust by word / char as normal.
+                currLine = layout.getLineForOffset(anotherHandleOffset);
+                initialOffset = getOffsetAtCoordinate(layout, currLine, x);
+            }
+
+            int offset = initialOffset;
+            final int wordEnd = getWordEnd(offset);
+            final int wordStart = getWordStart(offset);
+
+            if (mPrevX == UNSET_X_VALUE) {
+                mPrevX = x;
+            }
+
+            final int currentOffset = getCurrentCursorOffset();
+            final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
+            final boolean atRtl = isAtRtlRun(layout, offset);
+            final boolean isLvlBoundary = layout.isLevelBoundary(offset);
+
+            // We can't determine if the user is expanding or shrinking the selection if they're
+            // on a bi-di boundary, so until they've moved past the boundary we'll just place
+            // the cursor at the current position.
+            if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
+                // We're on a boundary or this is the first direction change -- just update
+                // to the current position.
+                mLanguageDirectionChanged = true;
+                mTouchWordDelta = 0.0f;
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+                return;
+            } else if (mLanguageDirectionChanged && !isLvlBoundary) {
+                // We've just moved past the boundary so update the position. After this we can
+                // figure out if the user is expanding or shrinking to go by word or character.
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+                mTouchWordDelta = 0.0f;
+                mLanguageDirectionChanged = false;
+                return;
+            }
+
+            boolean isExpanding;
+            final float xDiff = x - mPrevX;
+            if (isStartHandle()) {
+                isExpanding = currLine < mPreviousLineTouched;
+            } else {
+                isExpanding = currLine > mPreviousLineTouched;
+            }
+            if (atRtl == isStartHandle()) {
+                isExpanding |= xDiff > 0;
+            } else {
+                isExpanding |= xDiff < 0;
+            }
+
+            if (mTextView.getHorizontallyScrolling()) {
+                if (positionNearEdgeOfScrollingView(x, atRtl)
+                        && ((isStartHandle() && mTextView.getScrollX() != 0)
+                                || (!isStartHandle()
+                                        && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
+                        && ((isExpanding && ((isStartHandle() && offset < currentOffset)
+                                || (!isStartHandle() && offset > currentOffset)))
+                                        || !isExpanding)) {
+                    // If we're expanding ensure that the offset is actually expanding compared to
+                    // the current offset, if the handle snapped to the word, the finger position
+                    // may be out of sync and we don't want the selection to jump back.
+                    mTouchWordDelta = 0.0f;
+                    final int nextOffset = (atRtl == isStartHandle())
+                            ? layout.getOffsetToRightOf(mPreviousOffset)
+                            : layout.getOffsetToLeftOf(mPreviousOffset);
+                    positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
+                    return;
+                }
+            }
+
+            if (isExpanding) {
+                // User is increasing the selection.
+                int wordBoundary = isStartHandle() ? wordStart : wordEnd;
+                final boolean snapToWord = (!mInWord
+                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
+                                && atRtl == isAtRtlRun(layout, wordBoundary);
+                if (snapToWord) {
+                    // Sometimes words can be broken across lines (Chinese, hyphenation).
+                    // We still snap to the word boundary but we only use the letters on the
+                    // current line to determine if the user is far enough into the word to snap.
+                    if (layout.getLineForOffset(wordBoundary) != currLine) {
+                        wordBoundary = isStartHandle()
+                                ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
+                    }
+                    final int offsetThresholdToSnap = isStartHandle()
+                            ? wordEnd - ((wordEnd - wordBoundary) / 2)
+                            : wordStart + ((wordBoundary - wordStart) / 2);
+                    if (isStartHandle()
+                            && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
+                        // User is far enough into the word or on a different line so we expand by
+                        // word.
+                        offset = wordStart;
+                    } else if (!isStartHandle()
+                            && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
+                        // User is far enough into the word or on a different line so we expand by
+                        // word.
+                        offset = wordEnd;
+                    } else {
+                        offset = mPreviousOffset;
+                    }
+                }
+                if ((isStartHandle() && offset < initialOffset)
+                        || (!isStartHandle() && offset > initialOffset)) {
+                    final float adjustedX = getHorizontal(layout, offset);
+                    mTouchWordDelta =
+                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
+                } else {
+                    mTouchWordDelta = 0.0f;
+                }
+                positionCursor = true;
+            } else {
+                final int adjustedOffset =
+                        getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
+                final boolean shrinking = isStartHandle()
+                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
+                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
+                if (shrinking) {
+                    // User is shrinking the selection.
+                    if (currLine != mPrevLine) {
+                        // We're on a different line, so we'll snap to word boundaries.
+                        offset = isStartHandle() ? wordStart : wordEnd;
+                        if ((isStartHandle() && offset < initialOffset)
+                                || (!isStartHandle() && offset > initialOffset)) {
+                            final float adjustedX = getHorizontal(layout, offset);
+                            mTouchWordDelta =
+                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
+                        } else {
+                            mTouchWordDelta = 0.0f;
+                        }
+                    } else {
+                        offset = adjustedOffset;
+                    }
+                    positionCursor = true;
+                } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
+                        || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
+                    // Handle has jumped to the word boundary, and the user is moving
+                    // their finger towards the handle, the delta should be updated.
+                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
+                            - getHorizontal(layout, mPreviousOffset);
+                }
+            }
+
+            if (positionCursor) {
+                mPreviousLineTouched = currLine;
+                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
+            }
+            mPrevX = x;
+        }
+
+        @Override
+        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
+                boolean fromTouchScreen) {
+            super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
+            mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            boolean superResult = super.onTouchEvent(event);
+            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
+                // Reset the touch word offset and x value when the user
+                // re-engages the handle.
+                mTouchWordDelta = 0.0f;
+                mPrevX = UNSET_X_VALUE;
+            }
+            return superResult;
+        }
+
+        private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
+            final int anotherHandleOffset =
+                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
+            if ((isStartHandle() && offset >= anotherHandleOffset)
+                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
+                mTouchWordDelta = 0.0f;
+                final Layout layout = mTextView.getLayout();
+                if (layout != null && offset != anotherHandleOffset) {
+                    final float horiz = getHorizontal(layout, offset);
+                    final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
+                            !isStartHandle());
+                    final float currentHoriz = getHorizontal(layout, mPreviousOffset);
+                    if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
+                            || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
+                        // This handle passes another one as it crossed a direction boundary.
+                        // Don't minimize the selection, but keep the handle at the run boundary.
+                        final int currentOffset = getCurrentCursorOffset();
+                        final int offsetToGetRunRange = isStartHandle()
+                                ? currentOffset : Math.max(currentOffset - 1, 0);
+                        final long range = layout.getRunRange(offsetToGetRunRange);
+                        if (isStartHandle()) {
+                            offset = TextUtils.unpackRangeStartFromLong(range);
+                        } else {
+                            offset = TextUtils.unpackRangeEndFromLong(range);
+                        }
+                        positionAtCursorOffset(offset, false, fromTouchScreen);
+                        return;
+                    }
+                }
+                // Handles can not cross and selection is at least one character.
+                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
+            }
+            positionAtCursorOffset(offset, false, fromTouchScreen);
+        }
+
+        private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
+            mTextView.getLocationOnScreen(mTextViewLocation);
+            boolean nearEdge;
+            if (atRtl == isStartHandle()) {
+                int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
+                        - mTextView.getPaddingRight();
+                nearEdge = x > rightEdge - mTextViewEdgeSlop;
+            } else {
+                int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
+                nearEdge = x < leftEdge + mTextViewEdgeSlop;
+            }
+            return nearEdge;
+        }
+
+        @Override
+        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
+            final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
+            return layout.isRtlCharAt(offsetToCheck);
+        }
+
+        @Override
+        public float getHorizontal(@NonNull Layout layout, int offset) {
+            return getHorizontal(layout, offset, isStartHandle());
+        }
+
+        private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
+            final int line = layout.getLineForOffset(offset);
+            final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
+            final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
+            final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
+            return (isRtlChar == isRtlParagraph)
+                    ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
+        }
+
+        @Override
+        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
+            final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
+            final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
+            if (!layout.isLevelBoundary(primaryOffset)) {
+                return primaryOffset;
+            }
+            final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
+            final int currentOffset = getCurrentCursorOffset();
+            final int primaryDiff = Math.abs(primaryOffset - currentOffset);
+            final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
+            if (primaryDiff < secondaryDiff) {
+                return primaryOffset;
+            } else if (primaryDiff > secondaryDiff) {
+                return secondaryOffset;
+            } else {
+                final int offsetToCheck = isStartHandle()
+                        ? currentOffset : Math.max(currentOffset - 1, 0);
+                final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
+                final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
+                return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
+            }
+        }
+    }
+
+    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
+        final int trueLine = mTextView.getLineAtCoordinate(y);
+        if (layout == null || prevLine > layout.getLineCount()
+                || layout.getLineCount() <= 0 || prevLine < 0) {
+            // Invalid parameters, just return whatever line is at y.
+            return trueLine;
+        }
+
+        if (Math.abs(trueLine - prevLine) >= 2) {
+            // Only stick to lines if we're within a line of the previous selection.
+            return trueLine;
+        }
+
+        final float verticalOffset = mTextView.viewportToContentVerticalOffset();
+        final int lineCount = layout.getLineCount();
+        final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
+
+        final float firstLineTop = layout.getLineTop(0) + verticalOffset;
+        final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
+        final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
+
+        final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
+        final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
+        final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
+
+        // Determine if we've moved lines based on y position and previous line.
+        int currLine;
+        if (y <= yTopBound) {
+            currLine = Math.max(prevLine - 1, 0);
+        } else if (y >= yBottomBound) {
+            currLine = Math.min(prevLine + 1, lineCount - 1);
+        } else {
+            currLine = prevLine;
+        }
+        return currLine;
+    }
+
+    /**
+     * A CursorController instance can be used to control a cursor in the text.
+     */
+    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
+        /**
+         * Makes the cursor controller visible on screen.
+         * See also {@link #hide()}.
+         */
+        public void show();
+
+        /**
+         * Hide the cursor controller from screen.
+         * See also {@link #show()}.
+         */
+        public void hide();
+
+        /**
+         * Called when the view is detached from window. Perform house keeping task, such as
+         * stopping Runnable thread that would otherwise keep a reference on the context, thus
+         * preventing the activity from being recycled.
+         */
+        public void onDetached();
+
+        public boolean isCursorBeingModified();
+
+        public boolean isActive();
+    }
+
+    private class InsertionPointCursorController implements CursorController {
+        private InsertionHandleView mHandle;
+
+        public void show() {
+            getHandle().show();
+
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.hide();
+            }
+        }
+
+        public void hide() {
+            if (mHandle != null) {
+                mHandle.hide();
+            }
+        }
+
+        public void onTouchModeChanged(boolean isInTouchMode) {
+            if (!isInTouchMode) {
+                hide();
+            }
+        }
+
+        private InsertionHandleView getHandle() {
+            if (mSelectHandleCenter == null) {
+                mSelectHandleCenter = mTextView.getContext().getDrawable(
+                        mTextView.mTextSelectHandleRes);
+            }
+            if (mHandle == null) {
+                mHandle = new InsertionHandleView(mSelectHandleCenter);
+            }
+            return mHandle;
+        }
+
+        @Override
+        public void onDetached() {
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.removeOnTouchModeChangeListener(this);
+
+            if (mHandle != null) mHandle.onDetached();
+        }
+
+        @Override
+        public boolean isCursorBeingModified() {
+            return mHandle != null && mHandle.isDragging();
+        }
+
+        @Override
+        public boolean isActive() {
+            return mHandle != null && mHandle.isShowing();
+        }
+
+        public void invalidateHandle() {
+            if (mHandle != null) {
+                mHandle.invalidate();
+            }
+        }
+    }
+
+    class SelectionModifierCursorController implements CursorController {
+        // The cursor controller handles, lazily created when shown.
+        private SelectionHandleView mStartHandle;
+        private SelectionHandleView mEndHandle;
+        // The offsets of that last touch down event. Remembered to start selection there.
+        private int mMinTouchOffset, mMaxTouchOffset;
+
+        private float mDownPositionX, mDownPositionY;
+        private boolean mGestureStayedInTapRegion;
+
+        // Where the user first starts the drag motion.
+        private int mStartOffset = -1;
+
+        private boolean mHaventMovedEnoughToStartDrag;
+        // The line that a selection happened most recently with the drag accelerator.
+        private int mLineSelectionIsOn = -1;
+        // Whether the drag accelerator has selected past the initial line.
+        private boolean mSwitchedLines = false;
+
+        // Indicates the drag accelerator mode that the user is currently using.
+        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
+        // Drag accelerator is inactive.
+        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
+        // Character based selection by dragging. Only for mouse.
+        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
+        // Word based selection by dragging. Enabled after long pressing or double tapping.
+        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
+        // Paragraph based selection by dragging. Enabled after mouse triple click.
+        private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
+
+        SelectionModifierCursorController() {
+            resetTouchOffsets();
+        }
+
+        public void show() {
+            if (mTextView.isInBatchEditMode()) {
+                return;
+            }
+            initDrawables();
+            initHandles();
+        }
+
+        private void initDrawables() {
+            if (mSelectHandleLeft == null) {
+                mSelectHandleLeft = mTextView.getContext().getDrawable(
+                        mTextView.mTextSelectHandleLeftRes);
+            }
+            if (mSelectHandleRight == null) {
+                mSelectHandleRight = mTextView.getContext().getDrawable(
+                        mTextView.mTextSelectHandleRightRes);
+            }
+        }
+
+        private void initHandles() {
+            // Lazy object creation has to be done before updatePosition() is called.
+            if (mStartHandle == null) {
+                mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
+                        com.android.internal.R.id.selection_start_handle,
+                        HANDLE_TYPE_SELECTION_START);
+            }
+            if (mEndHandle == null) {
+                mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
+                        com.android.internal.R.id.selection_end_handle,
+                        HANDLE_TYPE_SELECTION_END);
+            }
+
+            mStartHandle.show();
+            mEndHandle.show();
+
+            hideInsertionPointCursorController();
+        }
+
+        public void hide() {
+            if (mStartHandle != null) mStartHandle.hide();
+            if (mEndHandle != null) mEndHandle.hide();
+        }
+
+        public void enterDrag(int dragAcceleratorMode) {
+            // Just need to init the handles / hide insertion cursor.
+            show();
+            mDragAcceleratorMode = dragAcceleratorMode;
+            // Start location of selection.
+            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
+                    mLastDownPositionY);
+            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
+            // Don't show the handles until user has lifted finger.
+            hide();
+
+            // This stops scrolling parents from intercepting the touch event, allowing
+            // the user to continue dragging across the screen to select text; TextView will
+            // scroll as necessary.
+            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
+            mTextView.cancelLongPress();
+        }
+
+        public void onTouchEvent(MotionEvent event) {
+            // This is done even when the View does not have focus, so that long presses can start
+            // selection and tap can move cursor from this tap position.
+            final float eventX = event.getX();
+            final float eventY = event.getY();
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    if (extractedTextModeWillBeStarted()) {
+                        // Prevent duplicating the selection handles until the mode starts.
+                        hide();
+                    } else {
+                        // Remember finger down position, to be able to start selection from there.
+                        mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
+                                eventX, eventY);
+
+                        // Double tap detection
+                        if (mGestureStayedInTapRegion) {
+                            if (mTapState == TAP_STATE_DOUBLE_TAP
+                                    || mTapState == TAP_STATE_TRIPLE_CLICK) {
+                                final float deltaX = eventX - mDownPositionX;
+                                final float deltaY = eventY - mDownPositionY;
+                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
+                                        mTextView.getContext());
+                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
+                                boolean stayedInArea =
+                                        distanceSquared < doubleTapSlop * doubleTapSlop;
+
+                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
+                                    if (mTapState == TAP_STATE_DOUBLE_TAP) {
+                                        selectCurrentWordAndStartDrag();
+                                    } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
+                                        selectCurrentParagraphAndStartDrag();
+                                    }
+                                    mDiscardNextActionUp = true;
+                                }
+                            }
+                        }
+
+                        mDownPositionX = eventX;
+                        mDownPositionY = eventY;
+                        mGestureStayedInTapRegion = true;
+                        mHaventMovedEnoughToStartDrag = true;
+                    }
+                    break;
+
+                case MotionEvent.ACTION_POINTER_DOWN:
+                case MotionEvent.ACTION_POINTER_UP:
+                    // Handle multi-point gestures. Keep min and max offset positions.
+                    // Only activated for devices that correctly handle multi-touch.
+                    if (mTextView.getContext().getPackageManager().hasSystemFeature(
+                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
+                        updateMinAndMaxOffsets(event);
+                    }
+                    break;
+
+                case MotionEvent.ACTION_MOVE:
+                    final ViewConfiguration viewConfig = ViewConfiguration.get(
+                            mTextView.getContext());
+                    final int touchSlop = viewConfig.getScaledTouchSlop();
+
+                    if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
+                        final float deltaX = eventX - mDownPositionX;
+                        final float deltaY = eventY - mDownPositionY;
+                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+                        if (mGestureStayedInTapRegion) {
+                            int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
+                            mGestureStayedInTapRegion =
+                                    distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
+                        }
+                        if (mHaventMovedEnoughToStartDrag) {
+                            // We don't start dragging until the user has moved enough.
+                            mHaventMovedEnoughToStartDrag =
+                                    distanceSquared <= touchSlop * touchSlop;
+                        }
+                    }
+
+                    if (isMouse && !isDragAcceleratorActive()) {
+                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
+                        if (mTextView.hasSelection()
+                                && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
+                                && offset >= mTextView.getSelectionStart()
+                                && offset <= mTextView.getSelectionEnd()) {
+                            startDragAndDrop();
+                            break;
+                        }
+
+                        if (mStartOffset != offset) {
+                            // Start character based drag accelerator.
+                            stopTextActionMode();
+                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
+                            mDiscardNextActionUp = true;
+                            mHaventMovedEnoughToStartDrag = false;
+                        }
+                    }
+
+                    if (mStartHandle != null && mStartHandle.isShowing()) {
+                        // Don't do the drag if the handles are showing already.
+                        break;
+                    }
+
+                    updateSelection(event);
+                    break;
+
+                case MotionEvent.ACTION_UP:
+                    if (!isDragAcceleratorActive()) {
+                        break;
+                    }
+                    updateSelection(event);
+
+                    // No longer dragging to select text, let the parent intercept events.
+                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
+
+                    // No longer the first dragging motion, reset.
+                    resetDragAcceleratorState();
+
+                    if (mTextView.hasSelection()) {
+                        // Drag selection should not be adjusted by the text classifier.
+                        startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
+                    }
+                    break;
+            }
+        }
+
+        private void updateSelection(MotionEvent event) {
+            if (mTextView.getLayout() != null) {
+                switch (mDragAcceleratorMode) {
+                    case DRAG_ACCELERATOR_MODE_CHARACTER:
+                        updateCharacterBasedSelection(event);
+                        break;
+                    case DRAG_ACCELERATOR_MODE_WORD:
+                        updateWordBasedSelection(event);
+                        break;
+                    case DRAG_ACCELERATOR_MODE_PARAGRAPH:
+                        updateParagraphBasedSelection(event);
+                        break;
+                }
+            }
+        }
+
+        /**
+         * If the TextView allows text selection, selects the current paragraph and starts a drag.
+         *
+         * @return true if the drag was started.
+         */
+        private boolean selectCurrentParagraphAndStartDrag() {
+            if (mInsertionActionModeRunnable != null) {
+                mTextView.removeCallbacks(mInsertionActionModeRunnable);
+            }
+            stopTextActionMode();
+            if (!selectCurrentParagraph()) {
+                return false;
+            }
+            enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
+            return true;
+        }
+
+        private void updateCharacterBasedSelection(MotionEvent event) {
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+            updateSelectionInternal(mStartOffset, offset,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+        }
+
+        private void updateWordBasedSelection(MotionEvent event) {
+            if (mHaventMovedEnoughToStartDrag) {
+                return;
+            }
+            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
+            final ViewConfiguration viewConfig = ViewConfiguration.get(
+                    mTextView.getContext());
+            final float eventX = event.getX();
+            final float eventY = event.getY();
+            final int currLine;
+            if (isMouse) {
+                // No need to offset the y coordinate for mouse input.
+                currLine = mTextView.getLineAtCoordinate(eventY);
+            } else {
+                float y = eventY;
+                if (mSwitchedLines) {
+                    // Offset the finger by the same vertical offset as the handles.
+                    // This improves visibility of the content being selected by
+                    // shifting the finger below the content, this is applied once
+                    // the user has switched lines.
+                    final int touchSlop = viewConfig.getScaledTouchSlop();
+                    final float fingerOffset = (mStartHandle != null)
+                            ? mStartHandle.getIdealVerticalOffset()
+                            : touchSlop;
+                    y = eventY - fingerOffset;
+                }
+
+                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
+                        y);
+                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
+                    // Break early here, we want to offset the finger position from
+                    // the selection highlight, once the user moved their finger
+                    // to a different line we should apply the offset and *not* switch
+                    // lines until recomputing the position with the finger offset.
+                    mSwitchedLines = true;
+                    return;
+                }
+            }
+
+            int startOffset;
+            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
+            // Snap to word boundaries.
+            if (mStartOffset < offset) {
+                // Expanding with end handle.
+                offset = getWordEnd(offset);
+                startOffset = getWordStart(mStartOffset);
+            } else {
+                // Expanding with start handle.
+                offset = getWordStart(offset);
+                startOffset = getWordEnd(mStartOffset);
+                if (startOffset == offset) {
+                    offset = getNextCursorOffset(offset, false);
+                }
+            }
+            mLineSelectionIsOn = currLine;
+            updateSelectionInternal(startOffset, offset,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+        }
+
+        private void updateParagraphBasedSelection(MotionEvent event) {
+            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
+
+            final int start = Math.min(offset, mStartOffset);
+            final int end = Math.max(offset, mStartOffset);
+            final long paragraphsRange = getParagraphsRange(start, end);
+            final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
+            final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
+            updateSelectionInternal(selectionStart, selectionEnd,
+                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
+        }
+
+        private void updateSelectionInternal(int selectionStart, int selectionEnd,
+                boolean fromTouchScreen) {
+            final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
+                    && ((mTextView.getSelectionStart() != selectionStart)
+                            || (mTextView.getSelectionEnd() != selectionEnd));
+            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+            if (performHapticFeedback) {
+                mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
+            }
+        }
+
+        /**
+         * @param event
+         */
+        private void updateMinAndMaxOffsets(MotionEvent event) {
+            int pointerCount = event.getPointerCount();
+            for (int index = 0; index < pointerCount; index++) {
+                int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
+                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
+                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
+            }
+        }
+
+        public int getMinTouchOffset() {
+            return mMinTouchOffset;
+        }
+
+        public int getMaxTouchOffset() {
+            return mMaxTouchOffset;
+        }
+
+        public void resetTouchOffsets() {
+            mMinTouchOffset = mMaxTouchOffset = -1;
+            resetDragAcceleratorState();
+        }
+
+        private void resetDragAcceleratorState() {
+            mStartOffset = -1;
+            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
+            mSwitchedLines = false;
+            final int selectionStart = mTextView.getSelectionStart();
+            final int selectionEnd = mTextView.getSelectionEnd();
+            if (selectionStart > selectionEnd) {
+                Selection.setSelection((Spannable) mTextView.getText(),
+                        selectionEnd, selectionStart);
+            }
+        }
+
+        /**
+         * @return true iff this controller is currently used to move the selection start.
+         */
+        public boolean isSelectionStartDragged() {
+            return mStartHandle != null && mStartHandle.isDragging();
+        }
+
+        @Override
+        public boolean isCursorBeingModified() {
+            return isDragAcceleratorActive() || isSelectionStartDragged()
+                    || (mEndHandle != null && mEndHandle.isDragging());
+        }
+
+        /**
+         * @return true if the user is selecting text using the drag accelerator.
+         */
+        public boolean isDragAcceleratorActive() {
+            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
+        }
+
+        public void onTouchModeChanged(boolean isInTouchMode) {
+            if (!isInTouchMode) {
+                hide();
+            }
+        }
+
+        @Override
+        public void onDetached() {
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.removeOnTouchModeChangeListener(this);
+
+            if (mStartHandle != null) mStartHandle.onDetached();
+            if (mEndHandle != null) mEndHandle.onDetached();
+        }
+
+        @Override
+        public boolean isActive() {
+            return mStartHandle != null && mStartHandle.isShowing();
+        }
+
+        public void invalidateHandles() {
+            if (mStartHandle != null) {
+                mStartHandle.invalidate();
+            }
+            if (mEndHandle != null) {
+                mEndHandle.invalidate();
+            }
+        }
+    }
+
+    private class CorrectionHighlighter {
+        private final Path mPath = new Path();
+        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        private int mStart, mEnd;
+        private long mFadingStartTime;
+        private RectF mTempRectF;
+        private static final int FADE_OUT_DURATION = 400;
+
+        public CorrectionHighlighter() {
+            mPaint.setCompatibilityScaling(
+                    mTextView.getResources().getCompatibilityInfo().applicationScale);
+            mPaint.setStyle(Paint.Style.FILL);
+        }
+
+        public void highlight(CorrectionInfo info) {
+            mStart = info.getOffset();
+            mEnd = mStart + info.getNewText().length();
+            mFadingStartTime = SystemClock.uptimeMillis();
+
+            if (mStart < 0 || mEnd < 0) {
+                stopAnimation();
+            }
+        }
+
+        public void draw(Canvas canvas, int cursorOffsetVertical) {
+            if (updatePath() && updatePaint()) {
+                if (cursorOffsetVertical != 0) {
+                    canvas.translate(0, cursorOffsetVertical);
+                }
+
+                canvas.drawPath(mPath, mPaint);
+
+                if (cursorOffsetVertical != 0) {
+                    canvas.translate(0, -cursorOffsetVertical);
+                }
+                invalidate(true); // TODO invalidate cursor region only
+            } else {
+                stopAnimation();
+                invalidate(false); // TODO invalidate cursor region only
+            }
+        }
+
+        private boolean updatePaint() {
+            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
+            if (duration > FADE_OUT_DURATION) return false;
+
+            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
+            final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
+            final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
+                    + ((int) (highlightColorAlpha * coef) << 24);
+            mPaint.setColor(color);
+            return true;
+        }
+
+        private boolean updatePath() {
+            final Layout layout = mTextView.getLayout();
+            if (layout == null) return false;
+
+            // Update in case text is edited while the animation is run
+            final int length = mTextView.getText().length();
+            int start = Math.min(length, mStart);
+            int end = Math.min(length, mEnd);
+
+            mPath.reset();
+            layout.getSelectionPath(start, end, mPath);
+            return true;
+        }
+
+        private void invalidate(boolean delayed) {
+            if (mTextView.getLayout() == null) return;
+
+            if (mTempRectF == null) mTempRectF = new RectF();
+            mPath.computeBounds(mTempRectF, false);
+
+            int left = mTextView.getCompoundPaddingLeft();
+            int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
+
+            if (delayed) {
+                mTextView.postInvalidateOnAnimation(
+                        left + (int) mTempRectF.left, top + (int) mTempRectF.top,
+                        left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
+            } else {
+                mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
+                        (int) mTempRectF.right, (int) mTempRectF.bottom);
+            }
+        }
+
+        private void stopAnimation() {
+            Editor.this.mCorrectionHighlighter = null;
+        }
+    }
+
+    private static class ErrorPopup extends PopupWindow {
+        private boolean mAbove = false;
+        private final TextView mView;
+        private int mPopupInlineErrorBackgroundId = 0;
+        private int mPopupInlineErrorAboveBackgroundId = 0;
+
+        ErrorPopup(TextView v, int width, int height) {
+            super(v, width, height);
+            mView = v;
+            // Make sure the TextView has a background set as it will be used the first time it is
+            // shown and positioned. Initialized with below background, which should have
+            // dimensions identical to the above version for this to work (and is more likely).
+            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+                    com.android.internal.R.styleable.Theme_errorMessageBackground);
+            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
+        }
+
+        void fixDirection(boolean above) {
+            mAbove = above;
+
+            if (above) {
+                mPopupInlineErrorAboveBackgroundId =
+                    getResourceId(mPopupInlineErrorAboveBackgroundId,
+                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
+            } else {
+                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
+                        com.android.internal.R.styleable.Theme_errorMessageBackground);
+            }
+
+            mView.setBackgroundResource(
+                    above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
+        }
+
+        private int getResourceId(int currentId, int index) {
+            if (currentId == 0) {
+                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
+                        R.styleable.Theme);
+                currentId = styledAttributes.getResourceId(index, 0);
+                styledAttributes.recycle();
+            }
+            return currentId;
+        }
+
+        @Override
+        public void update(int x, int y, int w, int h, boolean force) {
+            super.update(x, y, w, h, force);
+
+            boolean above = isAboveAnchor();
+            if (above != mAbove) {
+                fixDirection(above);
+            }
+        }
+    }
+
+    static class InputContentType {
+        int imeOptions = EditorInfo.IME_NULL;
+        String privateImeOptions;
+        CharSequence imeActionLabel;
+        int imeActionId;
+        Bundle extras;
+        OnEditorActionListener onEditorActionListener;
+        boolean enterDown;
+        LocaleList imeHintLocales;
+    }
+
+    static class InputMethodState {
+        ExtractedTextRequest mExtractedTextRequest;
+        final ExtractedText mExtractedText = new ExtractedText();
+        int mBatchEditNesting;
+        boolean mCursorChanged;
+        boolean mSelectionModeChanged;
+        boolean mContentChanged;
+        int mChangedStart, mChangedEnd, mChangedDelta;
+    }
+
+    /**
+     * @return True iff (start, end) is a valid range within the text.
+     */
+    private static boolean isValidRange(CharSequence text, int start, int end) {
+        return 0 <= start && start <= end && end <= text.length();
+    }
+
+    @VisibleForTesting
+    public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
+        return mSuggestionsPopupWindow;
+    }
+
+    /**
+     * An InputFilter that monitors text input to maintain undo history. It does not modify the
+     * text being typed (and hence always returns null from the filter() method).
+     *
+     * TODO: Make this span aware.
+     */
+    public static class UndoInputFilter implements InputFilter {
+        private final Editor mEditor;
+
+        // Whether the current filter pass is directly caused by an end-user text edit.
+        private boolean mIsUserEdit;
+
+        // Whether the text field is handling an IME composition. Must be parceled in case the user
+        // rotates the screen during composition.
+        private boolean mHasComposition;
+
+        // Whether the user is expanding or shortening the text
+        private boolean mExpanding;
+
+        // Whether the previous edit operation was in the current batch edit.
+        private boolean mPreviousOperationWasInSameBatchEdit;
+
+        public UndoInputFilter(Editor editor) {
+            mEditor = editor;
+        }
+
+        public void saveInstanceState(Parcel parcel) {
+            parcel.writeInt(mIsUserEdit ? 1 : 0);
+            parcel.writeInt(mHasComposition ? 1 : 0);
+            parcel.writeInt(mExpanding ? 1 : 0);
+            parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
+        }
+
+        public void restoreInstanceState(Parcel parcel) {
+            mIsUserEdit = parcel.readInt() != 0;
+            mHasComposition = parcel.readInt() != 0;
+            mExpanding = parcel.readInt() != 0;
+            mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
+        }
+
+        /**
+         * Signals that a user-triggered edit is starting.
+         */
+        public void beginBatchEdit() {
+            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
+            mIsUserEdit = true;
+        }
+
+        public void endBatchEdit() {
+            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
+            mIsUserEdit = false;
+            mPreviousOperationWasInSameBatchEdit = false;
+        }
+
+        @Override
+        public CharSequence filter(CharSequence source, int start, int end,
+                Spanned dest, int dstart, int dend) {
+            if (DEBUG_UNDO) {
+                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
+                        + "dest=" + dest + " (" + dstart + "-" + dend + ")");
+            }
+
+            // Check to see if this edit should be tracked for undo.
+            if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
+                return null;
+            }
+
+            final boolean hadComposition = mHasComposition;
+            mHasComposition = isComposition(source);
+            final boolean wasExpanding = mExpanding;
+            boolean shouldCreateSeparateState = false;
+            if ((end - start) != (dend - dstart)) {
+                mExpanding = (end - start) > (dend - dstart);
+                if (hadComposition && mExpanding != wasExpanding) {
+                    shouldCreateSeparateState = true;
+                }
+            }
+
+            // Handle edit.
+            handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
+            return null;
+        }
+
+        void freezeLastEdit() {
+            mEditor.mUndoManager.beginUpdate("Edit text");
+            EditOperation lastEdit = getLastEdit();
+            if (lastEdit != null) {
+                lastEdit.mFrozen = true;
+            }
+            mEditor.mUndoManager.endUpdate();
+        }
+
+        @Retention(RetentionPolicy.SOURCE)
+        @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
+        private @interface MergeMode {}
+        private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
+        private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
+        /** Use {@link EditOperation#mergeWith} to merge */
+        private static final int MERGE_EDIT_MODE_NORMAL = 2;
+
+        private void handleEdit(CharSequence source, int start, int end,
+                Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
+            // An application may install a TextWatcher to provide additional modifications after
+            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
+            // string). This results in multiple filter() calls for what the user considers to be
+            // a single operation. Always undo the whole set of changes in one step.
+            @MergeMode
+            final int mergeMode;
+            if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
+                mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
+            } else if (shouldCreateSeparateState) {
+                mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
+            } else {
+                mergeMode = MERGE_EDIT_MODE_NORMAL;
+            }
+            // Build a new operation with all the information from this edit.
+            String newText = TextUtils.substring(source, start, end);
+            String oldText = TextUtils.substring(dest, dstart, dend);
+            EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
+                    mHasComposition);
+            if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
+                return;
+            }
+            recordEdit(edit, mergeMode);
+        }
+
+        private EditOperation getLastEdit() {
+            final UndoManager um = mEditor.mUndoManager;
+            return um.getLastOperation(
+                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
+        }
+        /**
+         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
+         * If forceMerge is true then the new edit is always merged.
+         */
+        private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
+            // Fetch the last edit operation and attempt to merge in the new edit.
+            final UndoManager um = mEditor.mUndoManager;
+            um.beginUpdate("Edit text");
+            EditOperation lastEdit = getLastEdit();
+            if (lastEdit == null) {
+                // Add this as the first edit.
+                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
+                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+            } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
+                // Forced merges take priority because they could be the result of a non-user-edit
+                // change and this case should not create a new undo operation.
+                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
+                lastEdit.forceMergeWith(edit);
+            } else if (!mIsUserEdit) {
+                // An application directly modified the Editable outside of a text edit. Treat this
+                // as a new change and don't attempt to merge.
+                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
+                um.commitState(mEditor.mUndoOwner);
+                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+            } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
+                // Merge succeeded, nothing else to do.
+                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
+            } else {
+                // Could not merge with the last edit, so commit the last edit and add this edit.
+                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
+                um.commitState(mEditor.mUndoOwner);
+                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
+            }
+            mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
+            um.endUpdate();
+        }
+
+        private boolean canUndoEdit(CharSequence source, int start, int end,
+                Spanned dest, int dstart, int dend) {
+            if (!mEditor.mAllowUndo) {
+                if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
+                return false;
+            }
+
+            if (mEditor.mUndoManager.isInUndo()) {
+                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
+                return false;
+            }
+
+            // Text filters run before input operations are applied. However, some input operations
+            // are invalid and will throw exceptions when applied. This is common in tests. Don't
+            // attempt to undo invalid operations.
+            if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
+                if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
+                return false;
+            }
+
+            // Earlier filters can rewrite input to be a no-op, for example due to a length limit
+            // on an input field. Skip no-op changes.
+            if (start == end && dstart == dend) {
+                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
+                return false;
+            }
+
+            return true;
+        }
+
+        private static boolean isComposition(CharSequence source) {
+            if (!(source instanceof Spannable)) {
+                return false;
+            }
+            // This is a composition edit if the source has a non-zero-length composing span.
+            Spannable text = (Spannable) source;
+            int composeBegin = EditableInputConnection.getComposingSpanStart(text);
+            int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
+            return composeBegin < composeEnd;
+        }
+
+        private boolean isInTextWatcher() {
+            CharSequence text = mEditor.mTextView.getText();
+            return (text instanceof SpannableStringBuilder)
+                    && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
+        }
+    }
+
+    /**
+     * An operation to undo a single "edit" to a text view.
+     */
+    public static class EditOperation extends UndoOperation<Editor> {
+        private static final int TYPE_INSERT = 0;
+        private static final int TYPE_DELETE = 1;
+        private static final int TYPE_REPLACE = 2;
+
+        private int mType;
+        private String mOldText;
+        private String mNewText;
+        private int mStart;
+
+        private int mOldCursorPos;
+        private int mNewCursorPos;
+        private boolean mFrozen;
+        private boolean mIsComposition;
+
+        /**
+         * Constructs an edit operation from a text input operation on editor that replaces the
+         * oldText starting at dstart with newText.
+         */
+        public EditOperation(Editor editor, String oldText, int dstart, String newText,
+                boolean isComposition) {
+            super(editor.mUndoOwner);
+            mOldText = oldText;
+            mNewText = newText;
+
+            // Determine the type of the edit.
+            if (mNewText.length() > 0 && mOldText.length() == 0) {
+                mType = TYPE_INSERT;
+            } else if (mNewText.length() == 0 && mOldText.length() > 0) {
+                mType = TYPE_DELETE;
+            } else {
+                mType = TYPE_REPLACE;
+            }
+
+            mStart = dstart;
+            // Store cursor data.
+            mOldCursorPos = editor.mTextView.getSelectionStart();
+            mNewCursorPos = dstart + mNewText.length();
+            mIsComposition = isComposition;
+        }
+
+        public EditOperation(Parcel src, ClassLoader loader) {
+            super(src, loader);
+            mType = src.readInt();
+            mOldText = src.readString();
+            mNewText = src.readString();
+            mStart = src.readInt();
+            mOldCursorPos = src.readInt();
+            mNewCursorPos = src.readInt();
+            mFrozen = src.readInt() == 1;
+            mIsComposition = src.readInt() == 1;
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(mType);
+            dest.writeString(mOldText);
+            dest.writeString(mNewText);
+            dest.writeInt(mStart);
+            dest.writeInt(mOldCursorPos);
+            dest.writeInt(mNewCursorPos);
+            dest.writeInt(mFrozen ? 1 : 0);
+            dest.writeInt(mIsComposition ? 1 : 0);
+        }
+
+        private int getNewTextEnd() {
+            return mStart + mNewText.length();
+        }
+
+        private int getOldTextEnd() {
+            return mStart + mOldText.length();
+        }
+
+        @Override
+        public void commit() {
+        }
+
+        @Override
+        public void undo() {
+            if (DEBUG_UNDO) Log.d(TAG, "undo");
+            // Remove the new text and insert the old.
+            Editor editor = getOwnerData();
+            Editable text = (Editable) editor.mTextView.getText();
+            modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
+        }
+
+        @Override
+        public void redo() {
+            if (DEBUG_UNDO) Log.d(TAG, "redo");
+            // Remove the old text and insert the new.
+            Editor editor = getOwnerData();
+            Editable text = (Editable) editor.mTextView.getText();
+            modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
+        }
+
+        /**
+         * Attempts to merge this existing operation with a new edit.
+         * @param edit The new edit operation.
+         * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
+         * object unchanged.
+         */
+        private boolean mergeWith(EditOperation edit) {
+            if (DEBUG_UNDO) {
+                Log.d(TAG, "mergeWith old " + this);
+                Log.d(TAG, "mergeWith new " + edit);
+            }
+
+            if (mFrozen) {
+                return false;
+            }
+
+            switch (mType) {
+                case TYPE_INSERT:
+                    return mergeInsertWith(edit);
+                case TYPE_DELETE:
+                    return mergeDeleteWith(edit);
+                case TYPE_REPLACE:
+                    return mergeReplaceWith(edit);
+                default:
+                    return false;
+            }
+        }
+
+        private boolean mergeInsertWith(EditOperation edit) {
+            if (edit.mType == TYPE_INSERT) {
+                // Merge insertions that are contiguous even when it's frozen.
+                if (getNewTextEnd() != edit.mStart) {
+                    return false;
+                }
+                mNewText += edit.mNewText;
+                mNewCursorPos = edit.mNewCursorPos;
+                mFrozen = edit.mFrozen;
+                mIsComposition = edit.mIsComposition;
+                return true;
+            }
+            if (mIsComposition && edit.mType == TYPE_REPLACE
+                    && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
+                // Merge insertion with replace as they can be single insertion.
+                mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
+                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
+                mNewCursorPos = edit.mNewCursorPos;
+                mIsComposition = edit.mIsComposition;
+                return true;
+            }
+            return false;
+        }
+
+        // TODO: Support forward delete.
+        private boolean mergeDeleteWith(EditOperation edit) {
+            // Only merge continuous deletes.
+            if (edit.mType != TYPE_DELETE) {
+                return false;
+            }
+            // Only merge deletions that are contiguous.
+            if (mStart != edit.getOldTextEnd()) {
+                return false;
+            }
+            mStart = edit.mStart;
+            mOldText = edit.mOldText + mOldText;
+            mNewCursorPos = edit.mNewCursorPos;
+            mIsComposition = edit.mIsComposition;
+            return true;
+        }
+
+        private boolean mergeReplaceWith(EditOperation edit) {
+            if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
+                // Merge with adjacent insert.
+                mNewText += edit.mNewText;
+                mNewCursorPos = edit.mNewCursorPos;
+                return true;
+            }
+            if (!mIsComposition) {
+                return false;
+            }
+            if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
+                    && getNewTextEnd() >= edit.getOldTextEnd()) {
+                // Merge with delete as they can be single operation.
+                mNewText = mNewText.substring(0, edit.mStart - mStart)
+                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
+                if (mNewText.isEmpty()) {
+                    mType = TYPE_DELETE;
+                }
+                mNewCursorPos = edit.mNewCursorPos;
+                mIsComposition = edit.mIsComposition;
+                return true;
+            }
+            if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
+                    && TextUtils.equals(mNewText, edit.mOldText)) {
+                // Merge with the replace that replaces the same region.
+                mNewText = edit.mNewText;
+                mNewCursorPos = edit.mNewCursorPos;
+                mIsComposition = edit.mIsComposition;
+                return true;
+            }
+            return false;
+        }
+
+        /**
+         * Forcibly creates a single merged edit operation by simulating the entire text
+         * contents being replaced.
+         */
+        public void forceMergeWith(EditOperation edit) {
+            if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
+            if (mergeWith(edit)) {
+                return;
+            }
+            Editor editor = getOwnerData();
+
+            // Copy the text of the current field.
+            // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
+            // but would require two parallel implementations of modifyText() because Editable and
+            // StringBuilder do not share an interface for replace/delete/insert.
+            Editable editable = (Editable) editor.mTextView.getText();
+            Editable originalText = new SpannableStringBuilder(editable.toString());
+
+            // Roll back the last operation.
+            modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
+
+            // Clone the text again and apply the new operation.
+            Editable finalText = new SpannableStringBuilder(editable.toString());
+            modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
+                    edit.mNewText, edit.mStart, edit.mNewCursorPos);
+
+            // Convert this operation into a replace operation.
+            mType = TYPE_REPLACE;
+            mNewText = finalText.toString();
+            mOldText = originalText.toString();
+            mStart = 0;
+            mNewCursorPos = edit.mNewCursorPos;
+            mIsComposition = edit.mIsComposition;
+            // mOldCursorPos is unchanged.
+        }
+
+        private static void modifyText(Editable text, int deleteFrom, int deleteTo,
+                CharSequence newText, int newTextInsertAt, int newCursorPos) {
+            // Apply the edit if it is still valid.
+            if (isValidRange(text, deleteFrom, deleteTo)
+                    && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
+                if (deleteFrom != deleteTo) {
+                    text.delete(deleteFrom, deleteTo);
+                }
+                if (newText.length() != 0) {
+                    text.insert(newTextInsertAt, newText);
+                }
+            }
+            // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
+            // don't explicitly set it and rely on SpannableStringBuilder to position it.
+            // TODO: Select all the text that was undone.
+            if (0 <= newCursorPos && newCursorPos <= text.length()) {
+                Selection.setSelection(text, newCursorPos);
+            }
+        }
+
+        private String getTypeString() {
+            switch (mType) {
+                case TYPE_INSERT:
+                    return "insert";
+                case TYPE_DELETE:
+                    return "delete";
+                case TYPE_REPLACE:
+                    return "replace";
+                default:
+                    return "";
+            }
+        }
+
+        @Override
+        public String toString() {
+            return "[mType=" + getTypeString() + ", "
+                    + "mOldText=" + mOldText + ", "
+                    + "mNewText=" + mNewText + ", "
+                    + "mStart=" + mStart + ", "
+                    + "mOldCursorPos=" + mOldCursorPos + ", "
+                    + "mNewCursorPos=" + mNewCursorPos + ", "
+                    + "mFrozen=" + mFrozen + ", "
+                    + "mIsComposition=" + mIsComposition + "]";
+        }
+
+        public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
+                new Parcelable.ClassLoaderCreator<EditOperation>() {
+            @Override
+            public EditOperation createFromParcel(Parcel in) {
+                return new EditOperation(in, null);
+            }
+
+            @Override
+            public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
+                return new EditOperation(in, loader);
+            }
+
+            @Override
+            public EditOperation[] newArray(int size) {
+                return new EditOperation[size];
+            }
+        };
+    }
+
+    /**
+     * A helper for enabling and handling "PROCESS_TEXT" menu actions.
+     * These allow external applications to plug into currently selected text.
+     */
+    static final class ProcessTextIntentActionsHandler {
+
+        private final Editor mEditor;
+        private final TextView mTextView;
+        private final Context mContext;
+        private final PackageManager mPackageManager;
+        private final String mPackageName;
+        private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
+        private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
+                new SparseArray<>();
+        private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
+
+        private ProcessTextIntentActionsHandler(Editor editor) {
+            mEditor = Preconditions.checkNotNull(editor);
+            mTextView = Preconditions.checkNotNull(mEditor.mTextView);
+            mContext = Preconditions.checkNotNull(mTextView.getContext());
+            mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
+            mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
+        }
+
+        /**
+         * Adds "PROCESS_TEXT" menu items to the specified menu.
+         */
+        public void onInitializeMenu(Menu menu) {
+            final int size = mSupportedActivities.size();
+            loadSupportedActivities();
+            for (int i = 0; i < size; i++) {
+                final ResolveInfo resolveInfo = mSupportedActivities.get(i);
+                menu.add(Menu.NONE, Menu.NONE,
+                        Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
+                        getLabel(resolveInfo))
+                        .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
+                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
+            }
+        }
+
+        /**
+         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
+         * menu item.
+         *
+         * @return True if the action was performed, false otherwise.
+         */
+        public boolean performMenuItemAction(MenuItem item) {
+            return fireIntent(item.getIntent());
+        }
+
+        /**
+         * Initializes and caches "PROCESS_TEXT" accessibility actions.
+         */
+        public void initializeAccessibilityActions() {
+            mAccessibilityIntents.clear();
+            mAccessibilityActions.clear();
+            int i = 0;
+            loadSupportedActivities();
+            for (ResolveInfo resolveInfo : mSupportedActivities) {
+                int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
+                mAccessibilityActions.put(
+                        actionId,
+                        new AccessibilityNodeInfo.AccessibilityAction(
+                                actionId, getLabel(resolveInfo)));
+                mAccessibilityIntents.put(
+                        actionId, createProcessTextIntentForResolveInfo(resolveInfo));
+            }
+        }
+
+        /**
+         * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
+         * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
+         * latest accessibility actions available for this call.
+         */
+        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
+            for (int i = 0; i < mAccessibilityActions.size(); i++) {
+                nodeInfo.addAction(mAccessibilityActions.valueAt(i));
+            }
+        }
+
+        /**
+         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
+         * accessibility action id.
+         *
+         * @return True if the action was performed, false otherwise.
+         */
+        public boolean performAccessibilityAction(int actionId) {
+            return fireIntent(mAccessibilityIntents.get(actionId));
+        }
+
+        private boolean fireIntent(Intent intent) {
+            if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
+                String selectedText = mTextView.getSelectedText();
+                selectedText = TextUtils.trimToParcelableSize(selectedText);
+                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
+                mEditor.mPreserveSelection = true;
+                mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
+                return true;
+            }
+            return false;
+        }
+
+        private void loadSupportedActivities() {
+            mSupportedActivities.clear();
+            PackageManager packageManager = mTextView.getContext().getPackageManager();
+            List<ResolveInfo> unfiltered =
+                    packageManager.queryIntentActivities(createProcessTextIntent(), 0);
+            for (ResolveInfo info : unfiltered) {
+                if (isSupportedActivity(info)) {
+                    mSupportedActivities.add(info);
+                }
+            }
+        }
+
+        private boolean isSupportedActivity(ResolveInfo info) {
+            return mPackageName.equals(info.activityInfo.packageName)
+                    || info.activityInfo.exported
+                            && (info.activityInfo.permission == null
+                                    || mContext.checkSelfPermission(info.activityInfo.permission)
+                                            == PackageManager.PERMISSION_GRANTED);
+        }
+
+        private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
+            return createProcessTextIntent()
+                    .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
+                    .setClassName(info.activityInfo.packageName, info.activityInfo.name);
+        }
+
+        private Intent createProcessTextIntent() {
+            return new Intent()
+                    .setAction(Intent.ACTION_PROCESS_TEXT)
+                    .setType("text/plain");
+        }
+
+        private CharSequence getLabel(ResolveInfo resolveInfo) {
+            return resolveInfo.loadLabel(mPackageManager);
+        }
+    }
+}
diff --git a/android/widget/ExpandableListAdapter.java b/android/widget/ExpandableListAdapter.java
new file mode 100644
index 0000000..7f6781b
--- /dev/null
+++ b/android/widget/ExpandableListAdapter.java
@@ -0,0 +1,209 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An adapter that links a {@link ExpandableListView} with the underlying
+ * data. The implementation of this interface will provide access
+ * to the data of the children (categorized by groups), and also instantiate
+ * {@link View}s for children and groups.
+ */
+public interface ExpandableListAdapter {
+    /**
+     * @see Adapter#registerDataSetObserver(DataSetObserver)
+     */
+    void registerDataSetObserver(DataSetObserver observer);
+
+    /**
+     * @see Adapter#unregisterDataSetObserver(DataSetObserver)
+     */
+    void unregisterDataSetObserver(DataSetObserver observer);
+
+    /**
+     * Gets the number of groups.
+     * 
+     * @return the number of groups
+     */
+    int getGroupCount();
+
+    /**
+     * Gets the number of children in a specified group.
+     * 
+     * @param groupPosition the position of the group for which the children
+     *            count should be returned
+     * @return the children count in the specified group
+     */
+    int getChildrenCount(int groupPosition);
+
+    /**
+     * Gets the data associated with the given group.
+     * 
+     * @param groupPosition the position of the group
+     * @return the data child for the specified group
+     */
+    Object getGroup(int groupPosition);
+    
+    /**
+     * Gets the data associated with the given child within the given group.
+     * 
+     * @param groupPosition the position of the group that the child resides in
+     * @param childPosition the position of the child with respect to other
+     *            children in the group
+     * @return the data of the child
+     */
+    Object getChild(int groupPosition, int childPosition);
+
+    /**
+     * Gets the ID for the group at the given position. This group ID must be
+     * unique across groups. The combined ID (see
+     * {@link #getCombinedGroupId(long)}) must be unique across ALL items
+     * (groups and all children).
+     * 
+     * @param groupPosition the position of the group for which the ID is wanted
+     * @return the ID associated with the group
+     */
+    long getGroupId(int groupPosition);
+
+    /**
+     * Gets the ID for the given child within the given group. This ID must be
+     * unique across all children within the group. The combined ID (see
+     * {@link #getCombinedChildId(long, long)}) must be unique across ALL items
+     * (groups and all children).
+     * 
+     * @param groupPosition the position of the group that contains the child
+     * @param childPosition the position of the child within the group for which
+     *            the ID is wanted
+     * @return the ID associated with the child
+     */
+    long getChildId(int groupPosition, int childPosition);
+
+    /**
+     * Indicates whether the child and group IDs are stable across changes to the
+     * underlying data.
+     * 
+     * @return whether or not the same ID always refers to the same object
+     * @see Adapter#hasStableIds()
+     */
+    boolean hasStableIds();
+
+    /**
+     * Gets a View that displays the given group. This View is only for the
+     * group--the Views for the group's children will be fetched using
+     * {@link #getChildView(int, int, boolean, View, ViewGroup)}.
+     * 
+     * @param groupPosition the position of the group for which the View is
+     *            returned
+     * @param isExpanded whether the group is expanded or collapsed
+     * @param convertView the old view to reuse, if possible. You should check
+     *            that this view is non-null and of an appropriate type before
+     *            using. If it is not possible to convert this view to display
+     *            the correct data, this method can create a new view. It is not
+     *            guaranteed that the convertView will have been previously
+     *            created by
+     *            {@link #getGroupView(int, boolean, View, ViewGroup)}.
+     * @param parent the parent that this view will eventually be attached to
+     * @return the View corresponding to the group at the specified position
+     */
+    View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent);
+
+    /**
+     * Gets a View that displays the data for the given child within the given
+     * group.
+     * 
+     * @param groupPosition the position of the group that contains the child
+     * @param childPosition the position of the child (for which the View is
+     *            returned) within the group
+     * @param isLastChild Whether the child is the last child within the group
+     * @param convertView the old view to reuse, if possible. You should check
+     *            that this view is non-null and of an appropriate type before
+     *            using. If it is not possible to convert this view to display
+     *            the correct data, this method can create a new view. It is not
+     *            guaranteed that the convertView will have been previously
+     *            created by
+     *            {@link #getChildView(int, int, boolean, View, ViewGroup)}.
+     * @param parent the parent that this view will eventually be attached to
+     * @return the View corresponding to the child at the specified position
+     */
+    View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+            View convertView, ViewGroup parent);
+
+    /**
+     * Whether the child at the specified position is selectable.
+     * 
+     * @param groupPosition the position of the group that contains the child
+     * @param childPosition the position of the child within the group
+     * @return whether the child is selectable.
+     */
+    boolean isChildSelectable(int groupPosition, int childPosition);
+
+    /**
+     * @see ListAdapter#areAllItemsEnabled()
+     */
+    boolean areAllItemsEnabled();
+    
+    /**
+     * @see ListAdapter#isEmpty()
+     */
+    boolean isEmpty();
+
+    /**
+     * Called when a group is expanded.
+     * 
+     * @param groupPosition The group being expanded.
+     */
+    void onGroupExpanded(int groupPosition);
+    
+    /**
+     * Called when a group is collapsed.
+     * 
+     * @param groupPosition The group being collapsed.
+     */
+    void onGroupCollapsed(int groupPosition);
+    
+    /**
+     * Gets an ID for a child that is unique across any item (either group or
+     * child) that is in this list. Expandable lists require each item (group or
+     * child) to have a unique ID among all children and groups in the list.
+     * This method is responsible for returning that unique ID given a child's
+     * ID and its group's ID. Furthermore, if {@link #hasStableIds()} is true, the
+     * returned ID must be stable as well.
+     * 
+     * @param groupId The ID of the group that contains this child.
+     * @param childId The ID of the child.
+     * @return The unique (and possibly stable) ID of the child across all
+     *         groups and children in this list.
+     */
+    long getCombinedChildId(long groupId, long childId);
+
+    /**
+     * Gets an ID for a group that is unique across any item (either group or
+     * child) that is in this list. Expandable lists require each item (group or
+     * child) to have a unique ID among all children and groups in the list.
+     * This method is responsible for returning that unique ID given a group's
+     * ID. Furthermore, if {@link #hasStableIds()} is true, the returned ID must be
+     * stable as well.
+     * 
+     * @param groupId The ID of the group
+     * @return The unique (and possibly stable) ID of the group across all
+     *         groups and children in this list.
+     */
+    long getCombinedGroupId(long groupId);
+}
diff --git a/android/widget/ExpandableListConnector.java b/android/widget/ExpandableListConnector.java
new file mode 100644
index 0000000..bda64ba
--- /dev/null
+++ b/android/widget/ExpandableListConnector.java
@@ -0,0 +1,1035 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.database.DataSetObserver;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.SystemClock;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+/*
+ * Implementation notes:
+ * 
+ * <p>
+ * Terminology:
+ * <li> flPos - Flat list position, the position used by ListView
+ * <li> gPos - Group position, the position of a group among all the groups
+ * <li> cPos - Child position, the position of a child among all the children
+ * in a group
+ */
+
+/**
+ * A {@link BaseAdapter} that provides data/Views in an expandable list (offers
+ * features such as collapsing/expanding groups containing children). By
+ * itself, this adapter has no data and is a connector to a
+ * {@link ExpandableListAdapter} which provides the data.
+ * <p>
+ * Internally, this connector translates the flat list position that the
+ * ListAdapter expects to/from group and child positions that the ExpandableListAdapter
+ * expects.
+ */
+class ExpandableListConnector extends BaseAdapter implements Filterable {
+    /**
+     * The ExpandableListAdapter to fetch the data/Views for this expandable list
+     */
+    private ExpandableListAdapter mExpandableListAdapter;
+
+    /**
+     * List of metadata for the currently expanded groups. The metadata consists
+     * of data essential for efficiently translating between flat list positions
+     * and group/child positions. See {@link GroupMetadata}.
+     */
+    private ArrayList<GroupMetadata> mExpGroupMetadataList;
+
+    /** The number of children from all currently expanded groups */
+    private int mTotalExpChildrenCount;
+    
+    /** The maximum number of allowable expanded groups. Defaults to 'no limit' */
+    private int mMaxExpGroupCount = Integer.MAX_VALUE;
+
+    /** Change observer used to have ExpandableListAdapter changes pushed to us */
+    private final DataSetObserver mDataSetObserver = new MyDataSetObserver();
+
+    /**
+     * Constructs the connector
+     */
+    public ExpandableListConnector(ExpandableListAdapter expandableListAdapter) {
+        mExpGroupMetadataList = new ArrayList<GroupMetadata>();
+
+        setExpandableListAdapter(expandableListAdapter);
+    }
+
+    /**
+     * Point to the {@link ExpandableListAdapter} that will give us data/Views
+     * 
+     * @param expandableListAdapter the adapter that supplies us with data/Views
+     */
+    public void setExpandableListAdapter(ExpandableListAdapter expandableListAdapter) {
+        if (mExpandableListAdapter != null) {
+            mExpandableListAdapter.unregisterDataSetObserver(mDataSetObserver);
+        }
+        
+        mExpandableListAdapter = expandableListAdapter;
+        expandableListAdapter.registerDataSetObserver(mDataSetObserver);
+    }
+
+    /**
+     * Translates a flat list position to either a) group pos if the specified
+     * flat list position corresponds to a group, or b) child pos if it
+     * corresponds to a child.  Performs a binary search on the expanded
+     * groups list to find the flat list pos if it is an exp group, otherwise
+     * finds where the flat list pos fits in between the exp groups.
+     * 
+     * @param flPos the flat list position to be translated
+     * @return the group position or child position of the specified flat list
+     *         position encompassed in a {@link PositionMetadata} object
+     *         that contains additional useful info for insertion, etc.
+     */
+    PositionMetadata getUnflattenedPos(final int flPos) {
+        /* Keep locally since frequent use */
+        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+        final int numExpGroups = egml.size();
+        
+        /* Binary search variables */
+        int leftExpGroupIndex = 0;
+        int rightExpGroupIndex = numExpGroups - 1;
+        int midExpGroupIndex = 0;
+        GroupMetadata midExpGm; 
+        
+        if (numExpGroups == 0) {
+            /*
+             * There aren't any expanded groups (hence no visible children
+             * either), so flPos must be a group and its group pos will be the
+             * same as its flPos
+             */
+            return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, flPos,
+                    -1, null, 0);
+        }
+
+        /*
+         * Binary search over the expanded groups to find either the exact
+         * expanded group (if we're looking for a group) or the group that
+         * contains the child we're looking for. If we are looking for a
+         * collapsed group, we will not have a direct match here, but we will
+         * find the expanded group just before the group we're searching for (so
+         * then we can calculate the group position of the group we're searching
+         * for). If there isn't an expanded group prior to the group being
+         * searched for, then the group being searched for's group position is
+         * the same as the flat list position (since there are no children before
+         * it, and all groups before it are collapsed).
+         */
+        while (leftExpGroupIndex <= rightExpGroupIndex) {
+            midExpGroupIndex =
+                    (rightExpGroupIndex - leftExpGroupIndex) / 2
+                            + leftExpGroupIndex;
+            midExpGm = egml.get(midExpGroupIndex);
+            
+            if (flPos > midExpGm.lastChildFlPos) {
+                /*
+                 * The flat list position is after the current middle group's
+                 * last child's flat list position, so search right
+                 */
+                leftExpGroupIndex = midExpGroupIndex + 1;
+            } else if (flPos < midExpGm.flPos) {
+                /*
+                 * The flat list position is before the current middle group's
+                 * flat list position, so search left
+                 */
+                rightExpGroupIndex = midExpGroupIndex - 1;
+            } else if (flPos == midExpGm.flPos) {
+                /*
+                 * The flat list position is this middle group's flat list
+                 * position, so we've found an exact hit
+                 */
+                return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP,
+                        midExpGm.gPos, -1, midExpGm, midExpGroupIndex);
+            } else if (flPos <= midExpGm.lastChildFlPos
+                    /* && flPos > midGm.flPos as deduced from previous
+                     * conditions */) {
+                /* The flat list position is a child of the middle group */
+                
+                /* 
+                 * Subtract the first child's flat list position from the
+                 * specified flat list pos to get the child's position within
+                 * the group
+                 */
+                final int childPos = flPos - (midExpGm.flPos + 1);
+                return PositionMetadata.obtain(flPos, ExpandableListPosition.CHILD,
+                        midExpGm.gPos, childPos, midExpGm, midExpGroupIndex);
+            } 
+        }
+
+        /* 
+         * If we've reached here, it means the flat list position must be a
+         * group that is not expanded, since otherwise we would have hit it
+         * in the above search.
+         */
+
+
+        /**
+         * If we are to expand this group later, where would it go in the
+         * mExpGroupMetadataList ?
+         */
+        int insertPosition = 0;
+        
+        /** What is its group position in the list of all groups? */
+        int groupPos = 0;
+        
+        /*
+         * To figure out exact insertion and prior group positions, we need to
+         * determine how we broke out of the binary search.  We backtrack
+         * to see this.
+         */ 
+        if (leftExpGroupIndex > midExpGroupIndex) {
+            
+            /*
+             * This would occur in the first conditional, so the flat list
+             * insertion position is after the left group. Also, the
+             * leftGroupPos is one more than it should be (since that broke out
+             * of our binary search), so we decrement it.
+             */  
+            final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);            
+
+            insertPosition = leftExpGroupIndex;
+
+            /*
+             * Sums the number of groups between the prior exp group and this
+             * one, and then adds it to the prior group's group pos
+             */
+            groupPos =
+                (flPos - leftExpGm.lastChildFlPos) + leftExpGm.gPos;            
+        } else if (rightExpGroupIndex < midExpGroupIndex) {
+
+            /*
+             * This would occur in the second conditional, so the flat list
+             * insertion position is before the right group. Also, the
+             * rightGroupPos is one less than it should be, so increment it.
+             */
+            final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);            
+
+            insertPosition = rightExpGroupIndex;
+            
+            /*
+             * Subtracts this group's flat list pos from the group after's flat
+             * list position to find out how many groups are in between the two
+             * groups. Then, subtracts that number from the group after's group
+             * pos to get this group's pos.
+             */
+            groupPos = rightExpGm.gPos - (rightExpGm.flPos - flPos);
+        } else {
+            // TODO: clean exit
+            throw new RuntimeException("Unknown state");
+        }
+        
+        return PositionMetadata.obtain(flPos, ExpandableListPosition.GROUP, groupPos, -1,
+                null, insertPosition);
+    }
+
+    /**
+     * Translates either a group pos or a child pos (+ group it belongs to) to a
+     * flat list position.  If searching for a child and its group is not expanded, this will
+     * return null since the child isn't being shown in the ListView, and hence it has no
+     * position.
+     * 
+     * @param pos a {@link ExpandableListPosition} representing either a group position
+     *        or child position
+     * @return the flat list position encompassed in a {@link PositionMetadata}
+     *         object that contains additional useful info for insertion, etc., or null.
+     */
+    PositionMetadata getFlattenedPos(final ExpandableListPosition pos) {
+        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+        final int numExpGroups = egml.size();
+
+        /* Binary search variables */
+        int leftExpGroupIndex = 0;
+        int rightExpGroupIndex = numExpGroups - 1;
+        int midExpGroupIndex = 0;
+        GroupMetadata midExpGm; 
+        
+        if (numExpGroups == 0) {
+            /*
+             * There aren't any expanded groups, so flPos must be a group and
+             * its flPos will be the same as its group pos.  The
+             * insert position is 0 (since the list is empty).
+             */
+            return PositionMetadata.obtain(pos.groupPos, pos.type,
+                    pos.groupPos, pos.childPos, null, 0);
+        }
+
+        /*
+         * Binary search over the expanded groups to find either the exact
+         * expanded group (if we're looking for a group) or the group that
+         * contains the child we're looking for.
+         */
+        while (leftExpGroupIndex <= rightExpGroupIndex) {
+            midExpGroupIndex = (rightExpGroupIndex - leftExpGroupIndex)/2 + leftExpGroupIndex;
+            midExpGm = egml.get(midExpGroupIndex);
+            
+            if (pos.groupPos > midExpGm.gPos) {
+                /*
+                 * It's after the current middle group, so search right
+                 */
+                leftExpGroupIndex = midExpGroupIndex + 1;
+            } else if (pos.groupPos < midExpGm.gPos) {
+                /*
+                 * It's before the current middle group, so search left
+                 */
+                rightExpGroupIndex = midExpGroupIndex - 1;
+            } else if (pos.groupPos == midExpGm.gPos) {
+                /*
+                 * It's this middle group, exact hit
+                 */
+                
+                if (pos.type == ExpandableListPosition.GROUP) {
+                    /* If it's a group, give them this matched group's flPos */
+                    return PositionMetadata.obtain(midExpGm.flPos, pos.type,
+                            pos.groupPos, pos.childPos, midExpGm, midExpGroupIndex);
+                } else if (pos.type == ExpandableListPosition.CHILD) {
+                    /* If it's a child, calculate the flat list pos */
+                    return PositionMetadata.obtain(midExpGm.flPos + pos.childPos
+                            + 1, pos.type, pos.groupPos, pos.childPos,
+                            midExpGm, midExpGroupIndex);
+                } else {
+                    return null;
+                }
+            } 
+        }
+
+        /* 
+         * If we've reached here, it means there was no match in the expanded
+         * groups, so it must be a collapsed group that they're search for
+         */
+        if (pos.type != ExpandableListPosition.GROUP) {
+            /* If it isn't a group, return null */
+            return null;
+        }
+        
+        /*
+         * To figure out exact insertion and prior group positions, we need to
+         * determine how we broke out of the binary search. We backtrack to see
+         * this.
+         */ 
+        if (leftExpGroupIndex > midExpGroupIndex) {
+            
+            /*
+             * This would occur in the first conditional, so the flat list
+             * insertion position is after the left group.
+             * 
+             * The leftGroupPos is one more than it should be (from the binary
+             * search loop) so we subtract 1 to get the actual left group.  Since
+             * the insertion point is AFTER the left group, we keep this +1
+             * value as the insertion point
+             */  
+            final GroupMetadata leftExpGm = egml.get(leftExpGroupIndex-1);            
+            final int flPos =
+                    leftExpGm.lastChildFlPos
+                            + (pos.groupPos - leftExpGm.gPos);
+
+            return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
+                    pos.childPos, null, leftExpGroupIndex);
+        } else if (rightExpGroupIndex < midExpGroupIndex) {
+
+            /*
+             * This would occur in the second conditional, so the flat list
+             * insertion position is before the right group. Also, the
+             * rightGroupPos is one less than it should be (from binary search
+             * loop), so we increment to it.
+             */
+            final GroupMetadata rightExpGm = egml.get(++rightExpGroupIndex);            
+            final int flPos =
+                    rightExpGm.flPos
+                            - (rightExpGm.gPos - pos.groupPos);
+            return PositionMetadata.obtain(flPos, pos.type, pos.groupPos,
+                    pos.childPos, null, rightExpGroupIndex);
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public boolean areAllItemsEnabled() {
+        return mExpandableListAdapter.areAllItemsEnabled();
+    }
+
+    @Override
+    public boolean isEnabled(int flatListPos) {
+        final PositionMetadata metadata = getUnflattenedPos(flatListPos);
+        final ExpandableListPosition pos = metadata.position;
+        
+        boolean retValue;
+        if (pos.type == ExpandableListPosition.CHILD) {
+            retValue = mExpandableListAdapter.isChildSelectable(pos.groupPos, pos.childPos);
+        } else {
+            // Groups are always selectable
+            retValue = true;
+        }
+        
+        metadata.recycle();
+        
+        return retValue;
+    }
+
+    public int getCount() {
+        /*
+         * Total count for the list view is the number groups plus the 
+         * number of children from currently expanded groups (a value we keep
+         * cached in this class)
+         */ 
+        return mExpandableListAdapter.getGroupCount() + mTotalExpChildrenCount;
+    }
+
+    public Object getItem(int flatListPos) {
+        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+
+        Object retValue;
+        if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+            retValue = mExpandableListAdapter
+                    .getGroup(posMetadata.position.groupPos);
+        } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+            retValue = mExpandableListAdapter.getChild(posMetadata.position.groupPos,
+                    posMetadata.position.childPos);
+        } else {
+            // TODO: clean exit
+            throw new RuntimeException("Flat list position is of unknown type");
+        }
+        
+        posMetadata.recycle();
+        
+        return retValue;
+    }
+
+    public long getItemId(int flatListPos) {
+        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+        final long groupId = mExpandableListAdapter.getGroupId(posMetadata.position.groupPos);
+        
+        long retValue;
+        if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+            retValue = mExpandableListAdapter.getCombinedGroupId(groupId);
+        } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+            final long childId = mExpandableListAdapter.getChildId(posMetadata.position.groupPos,
+                    posMetadata.position.childPos);
+            retValue = mExpandableListAdapter.getCombinedChildId(groupId, childId);
+        } else {
+            // TODO: clean exit
+            throw new RuntimeException("Flat list position is of unknown type");
+        }
+        
+        posMetadata.recycle();
+        
+        return retValue;
+    }
+
+    public View getView(int flatListPos, View convertView, ViewGroup parent) {
+        final PositionMetadata posMetadata = getUnflattenedPos(flatListPos);
+
+        View retValue;
+        if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+            retValue = mExpandableListAdapter.getGroupView(posMetadata.position.groupPos,
+                    posMetadata.isExpanded(), convertView, parent);
+        } else if (posMetadata.position.type == ExpandableListPosition.CHILD) {
+            final boolean isLastChild = posMetadata.groupMetadata.lastChildFlPos == flatListPos;
+            
+            retValue = mExpandableListAdapter.getChildView(posMetadata.position.groupPos,
+                    posMetadata.position.childPos, isLastChild, convertView, parent);
+        } else {
+            // TODO: clean exit
+            throw new RuntimeException("Flat list position is of unknown type");
+        }
+        
+        posMetadata.recycle();
+        
+        return retValue;
+    }
+
+    @Override
+    public int getItemViewType(int flatListPos) {
+        final PositionMetadata metadata = getUnflattenedPos(flatListPos);
+        final ExpandableListPosition pos = metadata.position;
+
+        int retValue;
+        if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
+            HeterogeneousExpandableList adapter =
+                    (HeterogeneousExpandableList) mExpandableListAdapter;
+            if (pos.type == ExpandableListPosition.GROUP) {
+                retValue = adapter.getGroupType(pos.groupPos);
+            } else {
+                final int childType = adapter.getChildType(pos.groupPos, pos.childPos);
+                retValue = adapter.getGroupTypeCount() + childType;
+            }
+        } else {
+            if (pos.type == ExpandableListPosition.GROUP) {
+                retValue = 0;
+            } else {
+                retValue = 1;
+            }
+        }
+        
+        metadata.recycle();
+        
+        return retValue;
+    }
+
+    @Override
+    public int getViewTypeCount() {
+        if (mExpandableListAdapter instanceof HeterogeneousExpandableList) {
+            HeterogeneousExpandableList adapter =
+                    (HeterogeneousExpandableList) mExpandableListAdapter;
+            return adapter.getGroupTypeCount() + adapter.getChildTypeCount();
+        } else {
+            return 2;
+        }
+    }
+    
+    @Override
+    public boolean hasStableIds() {
+        return mExpandableListAdapter.hasStableIds();
+    }
+
+    /**
+     * Traverses the expanded group metadata list and fills in the flat list
+     * positions.
+     * 
+     * @param forceChildrenCountRefresh Forces refreshing of the children count
+     *        for all expanded groups.
+     * @param syncGroupPositions Whether to search for the group positions
+     *         based on the group IDs. This should only be needed when calling
+     *         this from an onChanged callback.
+     */
+    @SuppressWarnings("unchecked")
+    private void refreshExpGroupMetadataList(boolean forceChildrenCountRefresh,
+            boolean syncGroupPositions) {
+        final ArrayList<GroupMetadata> egml = mExpGroupMetadataList;
+        int egmlSize = egml.size();
+        int curFlPos = 0;
+        
+        /* Update child count as we go through */
+        mTotalExpChildrenCount = 0;
+        
+        if (syncGroupPositions) {
+            // We need to check whether any groups have moved positions
+            boolean positionsChanged = false;
+            
+            for (int i = egmlSize - 1; i >= 0; i--) {
+                GroupMetadata curGm = egml.get(i);
+                int newGPos = findGroupPosition(curGm.gId, curGm.gPos);
+                if (newGPos != curGm.gPos) {
+                    if (newGPos == AdapterView.INVALID_POSITION) {
+                        // Doh, just remove it from the list of expanded groups
+                        egml.remove(i);
+                        egmlSize--;
+                    }
+                    
+                    curGm.gPos = newGPos;
+                    if (!positionsChanged) positionsChanged = true;
+                }
+            }
+            
+            if (positionsChanged) {
+                // At least one group changed positions, so re-sort
+                Collections.sort(egml);
+            }
+        }
+        
+        int gChildrenCount;
+        int lastGPos = 0;
+        for (int i = 0; i < egmlSize; i++) {
+            /* Store in local variable since we'll access freq */
+            GroupMetadata curGm = egml.get(i);
+            
+            /*
+             * Get the number of children, try to refrain from calling
+             * another class's method unless we have to (so do a subtraction)
+             */
+            if ((curGm.lastChildFlPos == GroupMetadata.REFRESH) || forceChildrenCountRefresh) {
+                gChildrenCount = mExpandableListAdapter.getChildrenCount(curGm.gPos);
+            } else {
+                /* Num children for this group is its last child's fl pos minus
+                 * the group's fl pos
+                 */
+                gChildrenCount = curGm.lastChildFlPos - curGm.flPos;
+            }
+            
+            /* Update */
+            mTotalExpChildrenCount += gChildrenCount;
+            
+            /*
+             * This skips the collapsed groups and increments the flat list
+             * position (for subsequent exp groups) by accounting for the collapsed
+             * groups
+             */
+            curFlPos += (curGm.gPos - lastGPos);
+            lastGPos = curGm.gPos;
+            
+            /* Update the flat list positions, and the current flat list pos */
+            curGm.flPos = curFlPos;
+            curFlPos += gChildrenCount; 
+            curGm.lastChildFlPos = curFlPos; 
+        }
+    }
+    
+    /**
+     * Collapse a group in the grouped list view
+     * 
+     * @param groupPos position of the group to collapse
+     */
+    boolean collapseGroup(int groupPos) {
+        ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+                ExpandableListPosition.GROUP, groupPos, -1, -1);
+        PositionMetadata pm = getFlattenedPos(elGroupPos);
+        elGroupPos.recycle();
+        if (pm == null) return false;
+        
+        boolean retValue = collapseGroup(pm);
+        pm.recycle();
+        return retValue;
+    }
+    
+    boolean collapseGroup(PositionMetadata posMetadata) {
+        /*
+         * Collapsing requires removal from mExpGroupMetadataList 
+         */
+        
+        /*
+         * If it is null, it must be already collapsed. This group metadata
+         * object should have been set from the search that returned the
+         * position metadata object.
+         */
+        if (posMetadata.groupMetadata == null) return false;
+        
+        // Remove the group from the list of expanded groups 
+        mExpGroupMetadataList.remove(posMetadata.groupMetadata);
+
+        // Refresh the metadata
+        refreshExpGroupMetadataList(false, false);
+        
+        // Notify of change
+        notifyDataSetChanged();
+        
+        // Give the callback
+        mExpandableListAdapter.onGroupCollapsed(posMetadata.groupMetadata.gPos);
+        
+        return true;
+    }
+
+    /**
+     * Expand a group in the grouped list view
+     * @param groupPos the group to be expanded
+     */
+    boolean expandGroup(int groupPos) {
+        ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+                ExpandableListPosition.GROUP, groupPos, -1, -1);
+        PositionMetadata pm = getFlattenedPos(elGroupPos);
+        elGroupPos.recycle();
+        boolean retValue = expandGroup(pm);
+        pm.recycle();
+        return retValue;
+    }
+
+    boolean expandGroup(PositionMetadata posMetadata) {
+        /*
+         * Expanding requires insertion into the mExpGroupMetadataList 
+         */
+
+        if (posMetadata.position.groupPos < 0) {
+            // TODO clean exit
+            throw new RuntimeException("Need group");
+        }
+
+        if (mMaxExpGroupCount == 0) return false;
+        
+        // Check to see if it's already expanded
+        if (posMetadata.groupMetadata != null) return false;
+        
+        /* Restrict number of expanded groups to mMaxExpGroupCount */
+        if (mExpGroupMetadataList.size() >= mMaxExpGroupCount) {
+            /* Collapse a group */
+            // TODO: Collapse something not on the screen instead of the first one?
+            // TODO: Could write overloaded function to take GroupMetadata to collapse
+            GroupMetadata collapsedGm = mExpGroupMetadataList.get(0);
+            
+            int collapsedIndex = mExpGroupMetadataList.indexOf(collapsedGm);
+            
+            collapseGroup(collapsedGm.gPos);
+
+            /* Decrement index if it is after the group we removed */
+            if (posMetadata.groupInsertIndex > collapsedIndex) {
+                posMetadata.groupInsertIndex--;
+            }
+        }
+        
+        GroupMetadata expandedGm = GroupMetadata.obtain(
+                GroupMetadata.REFRESH,
+                GroupMetadata.REFRESH,
+                posMetadata.position.groupPos,
+                mExpandableListAdapter.getGroupId(posMetadata.position.groupPos));
+        
+        mExpGroupMetadataList.add(posMetadata.groupInsertIndex, expandedGm);
+
+        // Refresh the metadata
+        refreshExpGroupMetadataList(false, false);
+        
+        // Notify of change
+        notifyDataSetChanged();
+        
+        // Give the callback
+        mExpandableListAdapter.onGroupExpanded(expandedGm.gPos);
+
+        return true;
+    }
+    
+    /**
+     * Whether the given group is currently expanded.
+     * @param groupPosition The group to check.
+     * @return Whether the group is currently expanded.
+     */
+    public boolean isGroupExpanded(int groupPosition) {
+        GroupMetadata groupMetadata;
+        for (int i = mExpGroupMetadataList.size() - 1; i >= 0; i--) {
+            groupMetadata = mExpGroupMetadataList.get(i);
+            
+            if (groupMetadata.gPos == groupPosition) {
+                return true;
+            }
+        }
+        
+        return false;
+    }
+    
+    /**
+     * Set the maximum number of groups that can be expanded at any given time
+     */
+    public void setMaxExpGroupCount(int maxExpGroupCount) {
+        mMaxExpGroupCount = maxExpGroupCount;
+    }    
+
+    ExpandableListAdapter getAdapter() {
+        return mExpandableListAdapter;
+    }
+    
+    public Filter getFilter() {
+        ExpandableListAdapter adapter = getAdapter();
+        if (adapter instanceof Filterable) {
+            return ((Filterable) adapter).getFilter();
+        } else {
+            return null;
+        }
+    }
+
+    ArrayList<GroupMetadata> getExpandedGroupMetadataList() {
+        return mExpGroupMetadataList;
+    }
+    
+    void setExpandedGroupMetadataList(ArrayList<GroupMetadata> expandedGroupMetadataList) {
+        
+        if ((expandedGroupMetadataList == null) || (mExpandableListAdapter == null)) {
+            return;
+        }
+        
+        // Make sure our current data set is big enough for the previously
+        // expanded groups, if not, ignore this request
+        int numGroups = mExpandableListAdapter.getGroupCount();
+        for (int i = expandedGroupMetadataList.size() - 1; i >= 0; i--) {
+            if (expandedGroupMetadataList.get(i).gPos >= numGroups) {
+                // Doh, for some reason the client doesn't have some of the groups
+                return;
+            }
+        }
+        
+        mExpGroupMetadataList = expandedGroupMetadataList;
+        refreshExpGroupMetadataList(true, false);
+    }
+    
+    @Override
+    public boolean isEmpty() {
+        ExpandableListAdapter adapter = getAdapter();
+        return adapter != null ? adapter.isEmpty() : true;
+    }
+
+    /**
+     * Searches the expandable list adapter for a group position matching the
+     * given group ID. The search starts at the given seed position and then
+     * alternates between moving up and moving down until 1) we find the right
+     * position, or 2) we run out of time, or 3) we have looked at every
+     * position
+     * 
+     * @return Position of the row that matches the given row ID, or
+     *         {@link AdapterView#INVALID_POSITION} if it can't be found
+     * @see AdapterView#findSyncPosition()
+     */
+    int findGroupPosition(long groupIdToMatch, int seedGroupPosition) {
+        int count = mExpandableListAdapter.getGroupCount();
+
+        if (count == 0) {
+            return AdapterView.INVALID_POSITION;
+        }
+
+        // If there isn't a selection don't hunt for it
+        if (groupIdToMatch == AdapterView.INVALID_ROW_ID) {
+            return AdapterView.INVALID_POSITION;
+        }
+
+        // Pin seed to reasonable values
+        seedGroupPosition = Math.max(0, seedGroupPosition);
+        seedGroupPosition = Math.min(count - 1, seedGroupPosition);
+
+        long endTime = SystemClock.uptimeMillis() + AdapterView.SYNC_MAX_DURATION_MILLIS;
+
+        long rowId;
+
+        // first position scanned so far
+        int first = seedGroupPosition;
+
+        // last position scanned so far
+        int last = seedGroupPosition;
+
+        // True if we should move down on the next iteration
+        boolean next = false;
+
+        // True when we have looked at the first item in the data
+        boolean hitFirst;
+
+        // True when we have looked at the last item in the data
+        boolean hitLast;
+
+        // Get the item ID locally (instead of getItemIdAtPosition), so
+        // we need the adapter
+        ExpandableListAdapter adapter = getAdapter();
+        if (adapter == null) {
+            return AdapterView.INVALID_POSITION;
+        }
+
+        while (SystemClock.uptimeMillis() <= endTime) {
+            rowId = adapter.getGroupId(seedGroupPosition);
+            if (rowId == groupIdToMatch) {
+                // Found it!
+                return seedGroupPosition;
+            }
+
+            hitLast = last == count - 1;
+            hitFirst = first == 0;
+
+            if (hitLast && hitFirst) {
+                // Looked at everything
+                break;
+            }
+
+            if (hitFirst || (next && !hitLast)) {
+                // Either we hit the top, or we are trying to move down
+                last++;
+                seedGroupPosition = last;
+                // Try going up next time
+                next = false;
+            } else if (hitLast || (!next && !hitFirst)) {
+                // Either we hit the bottom, or we are trying to move up
+                first--;
+                seedGroupPosition = first;
+                // Try going down next time
+                next = true;
+            }
+
+        }
+
+        return AdapterView.INVALID_POSITION;
+    }
+
+    protected class MyDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            refreshExpGroupMetadataList(true, true);
+            
+            notifyDataSetChanged();
+        }
+
+        @Override
+        public void onInvalidated() {
+            refreshExpGroupMetadataList(true, true);
+            
+            notifyDataSetInvalidated();
+        }
+    }
+    
+    /**
+     * Metadata about an expanded group to help convert from a flat list
+     * position to either a) group position for groups, or b) child position for
+     * children
+     */
+    static class GroupMetadata implements Parcelable, Comparable<GroupMetadata> {
+        final static int REFRESH = -1;
+        
+        /** This group's flat list position */
+        int flPos;
+        
+        /* firstChildFlPos isn't needed since it's (flPos + 1) */
+        
+        /**
+         * This group's last child's flat list position, so basically
+         * the range of this group in the flat list
+         */
+        int lastChildFlPos;
+        
+        /**
+         * This group's group position
+         */
+        int gPos;
+        
+        /**
+         * This group's id
+         */
+        long gId;
+        
+        private GroupMetadata() {
+        }
+
+        static GroupMetadata obtain(int flPos, int lastChildFlPos, int gPos, long gId) {
+            GroupMetadata gm = new GroupMetadata();
+            gm.flPos = flPos;
+            gm.lastChildFlPos = lastChildFlPos;
+            gm.gPos = gPos;
+            gm.gId = gId;
+            return gm;
+        }
+        
+        public int compareTo(GroupMetadata another) {
+            if (another == null) {
+                throw new IllegalArgumentException();
+            }
+            
+            return gPos - another.gPos;
+        }
+
+        public int describeContents() {
+            return 0;
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(flPos);
+            dest.writeInt(lastChildFlPos);
+            dest.writeInt(gPos);
+            dest.writeLong(gId);
+        }
+        
+        public static final Parcelable.Creator<GroupMetadata> CREATOR =
+                new Parcelable.Creator<GroupMetadata>() {
+            
+            public GroupMetadata createFromParcel(Parcel in) {
+                GroupMetadata gm = GroupMetadata.obtain(
+                        in.readInt(),
+                        in.readInt(),
+                        in.readInt(),
+                        in.readLong());
+                return gm;
+            }
+    
+            public GroupMetadata[] newArray(int size) {
+                return new GroupMetadata[size];
+            }
+        };
+        
+    }
+
+    /**
+     * Data type that contains an expandable list position (can refer to either a group
+     * or child) and some extra information regarding referred item (such as
+     * where to insert into the flat list, etc.)
+     */
+    static public class PositionMetadata {
+        
+        private static final int MAX_POOL_SIZE = 5;
+        private static ArrayList<PositionMetadata> sPool =
+                new ArrayList<PositionMetadata>(MAX_POOL_SIZE);
+        
+        /** Data type to hold the position and its type (child/group) */
+        public ExpandableListPosition position;
+        
+        /**
+         * Link back to the expanded GroupMetadata for this group. Useful for
+         * removing the group from the list of expanded groups inside the
+         * connector when we collapse the group, and also as a check to see if
+         * the group was expanded or collapsed (this will be null if the group
+         * is collapsed since we don't keep that group's metadata)
+         */
+        public GroupMetadata groupMetadata;
+
+        /**
+         * For groups that are collapsed, we use this as the index (in
+         * mExpGroupMetadataList) to insert this group when we are expanding
+         * this group.
+         */
+        public int groupInsertIndex;
+        
+        private void resetState() {
+            if (position != null) {
+                position.recycle();
+                position = null;
+            }
+            groupMetadata = null;
+            groupInsertIndex = 0;
+        }
+        
+        /**
+         * Use {@link #obtain(int, int, int, int, GroupMetadata, int)}
+         */
+        private PositionMetadata() {
+        }
+        
+        static PositionMetadata obtain(int flatListPos, int type, int groupPos,
+                int childPos, GroupMetadata groupMetadata, int groupInsertIndex) {
+            PositionMetadata pm = getRecycledOrCreate();
+            pm.position = ExpandableListPosition.obtain(type, groupPos, childPos, flatListPos);
+            pm.groupMetadata = groupMetadata;
+            pm.groupInsertIndex = groupInsertIndex;
+            return pm;
+        }
+        
+        private static PositionMetadata getRecycledOrCreate() {
+            PositionMetadata pm;
+            synchronized (sPool) {
+                if (sPool.size() > 0) {
+                    pm = sPool.remove(0);
+                } else {
+                    return new PositionMetadata();
+                }
+            }
+            pm.resetState();
+            return pm;
+        }
+        
+        public void recycle() {
+            resetState();
+            synchronized (sPool) {
+                if (sPool.size() < MAX_POOL_SIZE) {
+                    sPool.add(this);
+                }
+            }
+        }
+        
+        /**
+         * Checks whether the group referred to in this object is expanded,
+         * or not (at the time this object was created)
+         * 
+         * @return whether the group at groupPos is expanded or not
+         */
+        public boolean isExpanded() {
+            return groupMetadata != null;
+        }
+    }
+}
diff --git a/android/widget/ExpandableListPosition.java b/android/widget/ExpandableListPosition.java
new file mode 100644
index 0000000..bb68da6
--- /dev/null
+++ b/android/widget/ExpandableListPosition.java
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import java.util.ArrayList;
+
+/**
+ * ExpandableListPosition can refer to either a group's position or a child's
+ * position. Referring to a child's position requires both a group position (the
+ * group containing the child) and a child position (the child's position within
+ * that group). To create objects, use {@link #obtainChildPosition(int, int)} or
+ * {@link #obtainGroupPosition(int)}.
+ */
+class ExpandableListPosition {
+    
+    private static final int MAX_POOL_SIZE = 5;
+    private static ArrayList<ExpandableListPosition> sPool =
+        new ArrayList<ExpandableListPosition>(MAX_POOL_SIZE);
+    
+    /**
+     * This data type represents a child position
+     */
+    public final static int CHILD = 1;
+
+    /**
+     * This data type represents a group position
+     */
+    public final static int GROUP = 2;
+
+    /**
+     * The position of either the group being referred to, or the parent
+     * group of the child being referred to
+     */
+    public int groupPos;
+
+    /**
+     * The position of the child within its parent group 
+     */
+    public int childPos;
+
+    /**
+     * The position of the item in the flat list (optional, used internally when
+     * the corresponding flat list position for the group or child is known)
+     */
+    int flatListPos;
+    
+    /**
+     * What type of position this ExpandableListPosition represents
+     */
+    public int type;
+    
+    private void resetState() {
+        groupPos = 0;
+        childPos = 0;
+        flatListPos = 0;
+        type = 0;
+    }
+    
+    private ExpandableListPosition() {
+    }
+    
+    long getPackedPosition() {
+        if (type == CHILD) return ExpandableListView.getPackedPositionForChild(groupPos, childPos);
+        else return ExpandableListView.getPackedPositionForGroup(groupPos);
+    }
+
+    static ExpandableListPosition obtainGroupPosition(int groupPosition) {
+        return obtain(GROUP, groupPosition, 0, 0);
+    }
+    
+    static ExpandableListPosition obtainChildPosition(int groupPosition, int childPosition) {
+        return obtain(CHILD, groupPosition, childPosition, 0);
+    }
+
+    static ExpandableListPosition obtainPosition(long packedPosition) {
+        if (packedPosition == ExpandableListView.PACKED_POSITION_VALUE_NULL) {
+            return null;
+        }
+        
+        ExpandableListPosition elp = getRecycledOrCreate(); 
+        elp.groupPos = ExpandableListView.getPackedPositionGroup(packedPosition);
+        if (ExpandableListView.getPackedPositionType(packedPosition) ==
+                ExpandableListView.PACKED_POSITION_TYPE_CHILD) {
+            elp.type = CHILD;
+            elp.childPos = ExpandableListView.getPackedPositionChild(packedPosition);
+        } else {
+            elp.type = GROUP;
+        }
+        return elp;
+    }
+    
+    static ExpandableListPosition obtain(int type, int groupPos, int childPos, int flatListPos) {
+        ExpandableListPosition elp = getRecycledOrCreate(); 
+        elp.type = type;
+        elp.groupPos = groupPos;
+        elp.childPos = childPos;
+        elp.flatListPos = flatListPos;
+        return elp;
+    }
+    
+    private static ExpandableListPosition getRecycledOrCreate() {
+        ExpandableListPosition elp;
+        synchronized (sPool) {
+            if (sPool.size() > 0) {
+                elp = sPool.remove(0);
+            } else {
+                return new ExpandableListPosition();
+            }
+        }
+        elp.resetState();
+        return elp;
+    }
+    
+    /**
+     * Do not call this unless you obtained this via ExpandableListPosition.obtain().
+     * PositionMetadata will handle recycling its own children.
+     */
+    public void recycle() {
+        synchronized (sPool) {
+            if (sPool.size() < MAX_POOL_SIZE) {
+                sPool.add(this);
+            }
+        }
+    }
+}
diff --git a/android/widget/ExpandableListView.java b/android/widget/ExpandableListView.java
new file mode 100644
index 0000000..8d9848d
--- /dev/null
+++ b/android/widget/ExpandableListView.java
@@ -0,0 +1,1346 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.view.ContextMenu;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.widget.ExpandableListConnector.PositionMetadata;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * A view that shows items in a vertically scrolling two-level list. This
+ * differs from the {@link ListView} by allowing two levels: groups which can
+ * individually be expanded to show its children. The items come from the
+ * {@link ExpandableListAdapter} associated with this view.
+ * <p>
+ * Expandable lists are able to show an indicator beside each item to display
+ * the item's current state (the states are usually one of expanded group,
+ * collapsed group, child, or last child). Use
+ * {@link #setChildIndicator(Drawable)} or {@link #setGroupIndicator(Drawable)}
+ * (or the corresponding XML attributes) to set these indicators (see the docs
+ * for each method to see additional state that each Drawable can have). The
+ * default style for an {@link ExpandableListView} provides indicators which
+ * will be shown next to Views given to the {@link ExpandableListView}. The
+ * layouts android.R.layout.simple_expandable_list_item_1 and
+ * android.R.layout.simple_expandable_list_item_2 (which should be used with
+ * {@link SimpleCursorTreeAdapter}) contain the preferred position information
+ * for indicators.
+ * <p>
+ * The context menu information set by an {@link ExpandableListView} will be a
+ * {@link ExpandableListContextMenuInfo} object with
+ * {@link ExpandableListContextMenuInfo#packedPosition} being a packed position
+ * that can be used with {@link #getPackedPositionType(long)} and the other
+ * similar methods.
+ * <p>
+ * <em><b>Note:</b></em> You cannot use the value <code>wrap_content</code>
+ * for the <code>android:layout_height</code> attribute of a
+ * ExpandableListView in XML if the parent's size is also not strictly specified
+ * (for example, if the parent were ScrollView you could not specify
+ * wrap_content since it also can be any length. However, you can use
+ * wrap_content if the ExpandableListView parent has a specific size, such as
+ * 100 pixels.
+ *
+ * @attr ref android.R.styleable#ExpandableListView_groupIndicator
+ * @attr ref android.R.styleable#ExpandableListView_indicatorLeft
+ * @attr ref android.R.styleable#ExpandableListView_indicatorRight
+ * @attr ref android.R.styleable#ExpandableListView_childIndicator
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorLeft
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorRight
+ * @attr ref android.R.styleable#ExpandableListView_childDivider
+ * @attr ref android.R.styleable#ExpandableListView_indicatorStart
+ * @attr ref android.R.styleable#ExpandableListView_indicatorEnd
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorStart
+ * @attr ref android.R.styleable#ExpandableListView_childIndicatorEnd
+ */
+public class ExpandableListView extends ListView {
+
+    /**
+     * The packed position represents a group.
+     */
+    public static final int PACKED_POSITION_TYPE_GROUP = 0;
+
+    /**
+     * The packed position represents a child.
+     */
+    public static final int PACKED_POSITION_TYPE_CHILD = 1;
+
+    /**
+     * The packed position represents a neither/null/no preference.
+     */
+    public static final int PACKED_POSITION_TYPE_NULL = 2;
+
+    /**
+     * The value for a packed position that represents neither/null/no
+     * preference. This value is not otherwise possible since a group type
+     * (first bit 0) should not have a child position filled.
+     */
+    public static final long PACKED_POSITION_VALUE_NULL = 0x00000000FFFFFFFFL;
+
+    /** The mask (in packed position representation) for the child */
+    private static final long PACKED_POSITION_MASK_CHILD = 0x00000000FFFFFFFFL;
+
+    /** The mask (in packed position representation) for the group */
+    private static final long PACKED_POSITION_MASK_GROUP = 0x7FFFFFFF00000000L;
+
+    /** The mask (in packed position representation) for the type */
+    private static final long PACKED_POSITION_MASK_TYPE  = 0x8000000000000000L;
+
+    /** The shift amount (in packed position representation) for the group */
+    private static final long PACKED_POSITION_SHIFT_GROUP = 32;
+
+    /** The shift amount (in packed position representation) for the type */
+    private static final long PACKED_POSITION_SHIFT_TYPE  = 63;
+
+    /** The mask (in integer child position representation) for the child */
+    private static final long PACKED_POSITION_INT_MASK_CHILD = 0xFFFFFFFF;
+
+    /** The mask (in integer group position representation) for the group */
+    private static final long PACKED_POSITION_INT_MASK_GROUP = 0x7FFFFFFF;
+
+    /** Serves as the glue/translator between a ListView and an ExpandableListView */
+    private ExpandableListConnector mConnector;
+
+    /** Gives us Views through group+child positions */
+    private ExpandableListAdapter mAdapter;
+
+    /** Left bound for drawing the indicator. */
+    private int mIndicatorLeft;
+
+    /** Right bound for drawing the indicator. */
+    private int mIndicatorRight;
+
+    /** Start bound for drawing the indicator. */
+    private int mIndicatorStart;
+
+    /** End bound for drawing the indicator. */
+    private int mIndicatorEnd;
+
+    /**
+     * Left bound for drawing the indicator of a child. Value of
+     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorLeft.
+     */
+    private int mChildIndicatorLeft;
+
+    /**
+     * Right bound for drawing the indicator of a child. Value of
+     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorRight.
+     */
+    private int mChildIndicatorRight;
+
+    /**
+     * Start bound for drawing the indicator of a child. Value of
+     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorStart.
+     */
+    private int mChildIndicatorStart;
+
+    /**
+     * End bound for drawing the indicator of a child. Value of
+     * {@link #CHILD_INDICATOR_INHERIT} means use mIndicatorEnd.
+     */
+    private int mChildIndicatorEnd;
+
+    /**
+     * Denotes when a child indicator should inherit this bound from the generic
+     * indicator bounds
+     */
+    public static final int CHILD_INDICATOR_INHERIT = -1;
+
+    /**
+     * Denotes an undefined value for an indicator
+     */
+    private static final int INDICATOR_UNDEFINED = -2;
+
+    /** The indicator drawn next to a group. */
+    private Drawable mGroupIndicator;
+
+    /** The indicator drawn next to a child. */
+    private Drawable mChildIndicator;
+
+    private static final int[] EMPTY_STATE_SET = {};
+
+    /** State indicating the group is expanded. */
+    private static final int[] GROUP_EXPANDED_STATE_SET =
+            {R.attr.state_expanded};
+
+    /** State indicating the group is empty (has no children). */
+    private static final int[] GROUP_EMPTY_STATE_SET =
+            {R.attr.state_empty};
+
+    /** State indicating the group is expanded and empty (has no children). */
+    private static final int[] GROUP_EXPANDED_EMPTY_STATE_SET =
+            {R.attr.state_expanded, R.attr.state_empty};
+
+    /** States for the group where the 0th bit is expanded and 1st bit is empty. */
+    private static final int[][] GROUP_STATE_SETS = {
+         EMPTY_STATE_SET, // 00
+         GROUP_EXPANDED_STATE_SET, // 01
+         GROUP_EMPTY_STATE_SET, // 10
+         GROUP_EXPANDED_EMPTY_STATE_SET // 11
+    };
+
+    /** State indicating the child is the last within its group. */
+    private static final int[] CHILD_LAST_STATE_SET =
+            {R.attr.state_last};
+
+    /** Drawable to be used as a divider when it is adjacent to any children */
+    private Drawable mChildDivider;
+
+    // Bounds of the indicator to be drawn
+    private final Rect mIndicatorRect = new Rect();
+
+    public ExpandableListView(Context context) {
+        this(context, null);
+    }
+
+    public ExpandableListView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.expandableListViewStyle);
+    }
+
+    public ExpandableListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ExpandableListView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.ExpandableListView, defStyleAttr, defStyleRes);
+
+        mGroupIndicator = a.getDrawable(
+                com.android.internal.R.styleable.ExpandableListView_groupIndicator);
+        mChildIndicator = a.getDrawable(
+                com.android.internal.R.styleable.ExpandableListView_childIndicator);
+        mIndicatorLeft = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.ExpandableListView_indicatorLeft, 0);
+        mIndicatorRight = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.ExpandableListView_indicatorRight, 0);
+        if (mIndicatorRight == 0 && mGroupIndicator != null) {
+            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+        }
+        mChildIndicatorLeft = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.ExpandableListView_childIndicatorLeft,
+                CHILD_INDICATOR_INHERIT);
+        mChildIndicatorRight = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.ExpandableListView_childIndicatorRight,
+                CHILD_INDICATOR_INHERIT);
+        mChildDivider = a.getDrawable(
+                com.android.internal.R.styleable.ExpandableListView_childDivider);
+
+        if (!isRtlCompatibilityMode()) {
+            mIndicatorStart = a.getDimensionPixelSize(
+                    com.android.internal.R.styleable.ExpandableListView_indicatorStart,
+                    INDICATOR_UNDEFINED);
+            mIndicatorEnd = a.getDimensionPixelSize(
+                    com.android.internal.R.styleable.ExpandableListView_indicatorEnd,
+                    INDICATOR_UNDEFINED);
+
+            mChildIndicatorStart = a.getDimensionPixelSize(
+                    com.android.internal.R.styleable.ExpandableListView_childIndicatorStart,
+                    CHILD_INDICATOR_INHERIT);
+            mChildIndicatorEnd = a.getDimensionPixelSize(
+                    com.android.internal.R.styleable.ExpandableListView_childIndicatorEnd,
+                    CHILD_INDICATOR_INHERIT);
+        }
+
+        a.recycle();
+    }
+
+    /**
+     * Return true if we are in RTL compatibility mode (either before Jelly Bean MR1 or
+     * RTL not supported)
+     */
+    private boolean isRtlCompatibilityMode() {
+        final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+        return targetSdkVersion < JELLY_BEAN_MR1 || !hasRtlSupport();
+    }
+
+    /**
+     * Return true if the application tag in the AndroidManifest has set "supportRtl" to true
+     */
+    private boolean hasRtlSupport() {
+        return mContext.getApplicationInfo().hasRtlSupport();
+    }
+
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        resolveIndicator();
+        resolveChildIndicator();
+    }
+
+    /**
+     * Resolve start/end indicator. start/end indicator always takes precedence over left/right
+     * indicator when defined.
+     */
+    private void resolveIndicator() {
+        final boolean isLayoutRtl = isLayoutRtl();
+        if (isLayoutRtl) {
+            if (mIndicatorStart >= 0) {
+                mIndicatorRight = mIndicatorStart;
+            }
+            if (mIndicatorEnd >= 0) {
+                mIndicatorLeft = mIndicatorEnd;
+            }
+        } else {
+            if (mIndicatorStart >= 0) {
+                mIndicatorLeft = mIndicatorStart;
+            }
+            if (mIndicatorEnd >= 0) {
+                mIndicatorRight = mIndicatorEnd;
+            }
+        }
+        if (mIndicatorRight == 0 && mGroupIndicator != null) {
+            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+        }
+    }
+
+    /**
+     * Resolve start/end child indicator. start/end child indicator always takes precedence over
+     * left/right child indicator when defined.
+     */
+    private void resolveChildIndicator() {
+        final boolean isLayoutRtl = isLayoutRtl();
+        if (isLayoutRtl) {
+            if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
+                mChildIndicatorRight = mChildIndicatorStart;
+            }
+            if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
+                mChildIndicatorLeft = mChildIndicatorEnd;
+            }
+        } else {
+            if (mChildIndicatorStart >= CHILD_INDICATOR_INHERIT) {
+                mChildIndicatorLeft = mChildIndicatorStart;
+            }
+            if (mChildIndicatorEnd >= CHILD_INDICATOR_INHERIT) {
+                mChildIndicatorRight = mChildIndicatorEnd;
+            }
+        }
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        // Draw children, etc.
+        super.dispatchDraw(canvas);
+
+        // If we have any indicators to draw, we do it here
+        if ((mChildIndicator == null) && (mGroupIndicator == null)) {
+            return;
+        }
+
+        int saveCount = 0;
+        final boolean clipToPadding = (mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK;
+        if (clipToPadding) {
+            saveCount = canvas.save();
+            final int scrollX = mScrollX;
+            final int scrollY = mScrollY;
+            canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+                    scrollX + mRight - mLeft - mPaddingRight,
+                    scrollY + mBottom - mTop - mPaddingBottom);
+        }
+
+        final int headerViewsCount = getHeaderViewsCount();
+
+        final int lastChildFlPos = mItemCount - getFooterViewsCount() - headerViewsCount - 1;
+
+        final int myB = mBottom;
+
+        PositionMetadata pos;
+        View item;
+        Drawable indicator;
+        int t, b;
+
+        // Start at a value that is neither child nor group
+        int lastItemType = ~(ExpandableListPosition.CHILD | ExpandableListPosition.GROUP);
+
+        final Rect indicatorRect = mIndicatorRect;
+
+        // The "child" mentioned in the following two lines is this
+        // View's child, not referring to an expandable list's
+        // notion of a child (as opposed to a group)
+        final int childCount = getChildCount();
+        for (int i = 0, childFlPos = mFirstPosition - headerViewsCount; i < childCount;
+             i++, childFlPos++) {
+
+            if (childFlPos < 0) {
+                // This child is header
+                continue;
+            } else if (childFlPos > lastChildFlPos) {
+                // This child is footer, so are all subsequent children
+                break;
+            }
+
+            item = getChildAt(i);
+            t = item.getTop();
+            b = item.getBottom();
+
+            // This item isn't on the screen
+            if ((b < 0) || (t > myB)) continue;
+
+            // Get more expandable list-related info for this item
+            pos = mConnector.getUnflattenedPos(childFlPos);
+
+            final boolean isLayoutRtl = isLayoutRtl();
+            final int width = getWidth();
+
+            // If this item type and the previous item type are different, then we need to change
+            // the left & right bounds
+            if (pos.position.type != lastItemType) {
+                if (pos.position.type == ExpandableListPosition.CHILD) {
+                    indicatorRect.left = (mChildIndicatorLeft == CHILD_INDICATOR_INHERIT) ?
+                            mIndicatorLeft : mChildIndicatorLeft;
+                    indicatorRect.right = (mChildIndicatorRight == CHILD_INDICATOR_INHERIT) ?
+                            mIndicatorRight : mChildIndicatorRight;
+                } else {
+                    indicatorRect.left = mIndicatorLeft;
+                    indicatorRect.right = mIndicatorRight;
+                }
+
+                if (isLayoutRtl) {
+                    final int temp = indicatorRect.left;
+                    indicatorRect.left = width - indicatorRect.right;
+                    indicatorRect.right = width - temp;
+
+                    indicatorRect.left -= mPaddingRight;
+                    indicatorRect.right -= mPaddingRight;
+                } else {
+                    indicatorRect.left += mPaddingLeft;
+                    indicatorRect.right += mPaddingLeft;
+                }
+
+                lastItemType = pos.position.type;
+            }
+
+            if (indicatorRect.left != indicatorRect.right) {
+                // Use item's full height + the divider height
+                if (mStackFromBottom) {
+                    // See ListView#dispatchDraw
+                    indicatorRect.top = t;// - mDividerHeight;
+                    indicatorRect.bottom = b;
+                } else {
+                    indicatorRect.top = t;
+                    indicatorRect.bottom = b;// + mDividerHeight;
+                }
+
+                // Get the indicator (with its state set to the item's state)
+                indicator = getIndicator(pos);
+                if (indicator != null) {
+                    // Draw the indicator
+                    indicator.setBounds(indicatorRect);
+                    indicator.draw(canvas);
+                }
+            }
+            pos.recycle();
+        }
+
+        if (clipToPadding) {
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    /**
+     * Gets the indicator for the item at the given position. If the indicator
+     * is stateful, the state will be given to the indicator.
+     *
+     * @param pos The flat list position of the item whose indicator
+     *            should be returned.
+     * @return The indicator in the proper state.
+     */
+    private Drawable getIndicator(PositionMetadata pos) {
+        Drawable indicator;
+
+        if (pos.position.type == ExpandableListPosition.GROUP) {
+            indicator = mGroupIndicator;
+
+            if (indicator != null && indicator.isStateful()) {
+                // Empty check based on availability of data.  If the groupMetadata isn't null,
+                // we do a check on it. Otherwise, the group is collapsed so we consider it
+                // empty for performance reasons.
+                boolean isEmpty = (pos.groupMetadata == null) ||
+                        (pos.groupMetadata.lastChildFlPos == pos.groupMetadata.flPos);
+
+                final int stateSetIndex =
+                    (pos.isExpanded() ? 1 : 0) | // Expanded?
+                    (isEmpty ? 2 : 0); // Empty?
+                indicator.setState(GROUP_STATE_SETS[stateSetIndex]);
+            }
+        } else {
+            indicator = mChildIndicator;
+
+            if (indicator != null && indicator.isStateful()) {
+                // No need for a state sets array for the child since it only has two states
+                final int stateSet[] = pos.position.flatListPos == pos.groupMetadata.lastChildFlPos
+                        ? CHILD_LAST_STATE_SET
+                        : EMPTY_STATE_SET;
+                indicator.setState(stateSet);
+            }
+        }
+
+        return indicator;
+    }
+
+    /**
+     * Sets the drawable that will be drawn adjacent to every child in the list. This will
+     * be drawn using the same height as the normal divider ({@link #setDivider(Drawable)}) or
+     * if it does not have an intrinsic height, the height set by {@link #setDividerHeight(int)}.
+     *
+     * @param childDivider The drawable to use.
+     */
+    public void setChildDivider(Drawable childDivider) {
+        mChildDivider = childDivider;
+    }
+
+    @Override
+    void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
+        int flatListPosition = childIndex + mFirstPosition;
+
+        // Only proceed as possible child if the divider isn't above all items (if it is above
+        // all items, then the item below it has to be a group)
+        if (flatListPosition >= 0) {
+            final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+            PositionMetadata pos = mConnector.getUnflattenedPos(adjustedPosition);
+            // If this item is a child, or it is a non-empty group that is expanded
+            if ((pos.position.type == ExpandableListPosition.CHILD) || (pos.isExpanded() &&
+                    pos.groupMetadata.lastChildFlPos != pos.groupMetadata.flPos)) {
+                // These are the cases where we draw the child divider
+                final Drawable divider = mChildDivider;
+                divider.setBounds(bounds);
+                divider.draw(canvas);
+                pos.recycle();
+                return;
+            }
+            pos.recycle();
+        }
+
+        // Otherwise draw the default divider
+        super.drawDivider(canvas, bounds, flatListPosition);
+    }
+
+    /**
+     * This overloaded method should not be used, instead use
+     * {@link #setAdapter(ExpandableListAdapter)}.
+     * <p>
+     * {@inheritDoc}
+     */
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        throw new RuntimeException(
+                "For ExpandableListView, use setAdapter(ExpandableListAdapter) instead of " +
+                "setAdapter(ListAdapter)");
+    }
+
+    /**
+     * This method should not be used, use {@link #getExpandableListAdapter()}.
+     */
+    @Override
+    public ListAdapter getAdapter() {
+        /*
+         * The developer should never really call this method on an
+         * ExpandableListView, so it would be nice to throw a RuntimeException,
+         * but AdapterView calls this
+         */
+        return super.getAdapter();
+    }
+
+    /**
+     * Register a callback to be invoked when an item has been clicked and the
+     * caller prefers to receive a ListView-style position instead of a group
+     * and/or child position. In most cases, the caller should use
+     * {@link #setOnGroupClickListener} and/or {@link #setOnChildClickListener}.
+     * <p />
+     * {@inheritDoc}
+     */
+    @Override
+    public void setOnItemClickListener(OnItemClickListener l) {
+        super.setOnItemClickListener(l);
+    }
+
+    /**
+     * Sets the adapter that provides data to this view.
+     * @param adapter The adapter that provides data to this view.
+     */
+    public void setAdapter(ExpandableListAdapter adapter) {
+        // Set member variable
+        mAdapter = adapter;
+
+        if (adapter != null) {
+            // Create the connector
+            mConnector = new ExpandableListConnector(adapter);
+        } else {
+            mConnector = null;
+        }
+
+        // Link the ListView (superclass) to the expandable list data through the connector
+        super.setAdapter(mConnector);
+    }
+
+    /**
+     * Gets the adapter that provides data to this view.
+     * @return The adapter that provides data to this view.
+     */
+    public ExpandableListAdapter getExpandableListAdapter() {
+        return mAdapter;
+    }
+
+    /**
+     * @param position An absolute (including header and footer) flat list position.
+     * @return true if the position corresponds to a header or a footer item.
+     */
+    private boolean isHeaderOrFooterPosition(int position) {
+        final int footerViewsStart = mItemCount - getFooterViewsCount();
+        return (position < getHeaderViewsCount() || position >= footerViewsStart);
+    }
+
+    /**
+     * Converts an absolute item flat position into a group/child flat position, shifting according
+     * to the number of header items.
+     *
+     * @param flatListPosition The absolute flat position
+     * @return A group/child flat position as expected by the connector.
+     */
+    private int getFlatPositionForConnector(int flatListPosition) {
+        return flatListPosition - getHeaderViewsCount();
+    }
+
+    /**
+     * Converts a group/child flat position into an absolute flat position, that takes into account
+     * the possible headers.
+     *
+     * @param flatListPosition The child/group flat position
+     * @return An absolute flat position.
+     */
+    private int getAbsoluteFlatPosition(int flatListPosition) {
+        return flatListPosition + getHeaderViewsCount();
+    }
+
+    @Override
+    public boolean performItemClick(View v, int position, long id) {
+        // Ignore clicks in header/footers
+        if (isHeaderOrFooterPosition(position)) {
+            // Clicked on a header/footer, so ignore pass it on to super
+            return super.performItemClick(v, position, id);
+        }
+
+        // Internally handle the item click
+        final int adjustedPosition = getFlatPositionForConnector(position);
+        return handleItemClick(v, adjustedPosition, id);
+    }
+
+    /**
+     * This will either expand/collapse groups (if a group was clicked) or pass
+     * on the click to the proper child (if a child was clicked)
+     *
+     * @param position The flat list position. This has already been factored to
+     *            remove the header/footer.
+     * @param id The ListAdapter ID, not the group or child ID.
+     */
+    boolean handleItemClick(View v, int position, long id) {
+        final PositionMetadata posMetadata = mConnector.getUnflattenedPos(position);
+
+        id = getChildOrGroupId(posMetadata.position);
+
+        boolean returnValue;
+        if (posMetadata.position.type == ExpandableListPosition.GROUP) {
+            /* It's a group, so handle collapsing/expanding */
+
+            /* It's a group click, so pass on event */
+            if (mOnGroupClickListener != null) {
+                if (mOnGroupClickListener.onGroupClick(this, v,
+                        posMetadata.position.groupPos, id)) {
+                    posMetadata.recycle();
+                    return true;
+                }
+            }
+
+            if (posMetadata.isExpanded()) {
+                /* Collapse it */
+                mConnector.collapseGroup(posMetadata);
+
+                playSoundEffect(SoundEffectConstants.CLICK);
+
+                if (mOnGroupCollapseListener != null) {
+                    mOnGroupCollapseListener.onGroupCollapse(posMetadata.position.groupPos);
+                }
+            } else {
+                /* Expand it */
+                mConnector.expandGroup(posMetadata);
+
+                playSoundEffect(SoundEffectConstants.CLICK);
+
+                if (mOnGroupExpandListener != null) {
+                    mOnGroupExpandListener.onGroupExpand(posMetadata.position.groupPos);
+                }
+
+                final int groupPos = posMetadata.position.groupPos;
+                final int groupFlatPos = posMetadata.position.flatListPos;
+
+                final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
+                smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
+                        shiftedGroupPosition);
+            }
+
+            returnValue = true;
+        } else {
+            /* It's a child, so pass on event */
+            if (mOnChildClickListener != null) {
+                playSoundEffect(SoundEffectConstants.CLICK);
+                return mOnChildClickListener.onChildClick(this, v, posMetadata.position.groupPos,
+                        posMetadata.position.childPos, id);
+            }
+
+            returnValue = false;
+        }
+
+        posMetadata.recycle();
+
+        return returnValue;
+    }
+
+    /**
+     * Expand a group in the grouped list view
+     *
+     * @param groupPos the group to be expanded
+     * @return True if the group was expanded, false otherwise (if the group
+     *         was already expanded, this will return false)
+     */
+    public boolean expandGroup(int groupPos) {
+       return expandGroup(groupPos, false);
+    }
+
+    /**
+     * Expand a group in the grouped list view
+     *
+     * @param groupPos the group to be expanded
+     * @param animate true if the expanding group should be animated in
+     * @return True if the group was expanded, false otherwise (if the group
+     *         was already expanded, this will return false)
+     */
+    public boolean expandGroup(int groupPos, boolean animate) {
+        ExpandableListPosition elGroupPos = ExpandableListPosition.obtain(
+                ExpandableListPosition.GROUP, groupPos, -1, -1);
+        PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
+        elGroupPos.recycle();
+        boolean retValue = mConnector.expandGroup(pm);
+
+        if (mOnGroupExpandListener != null) {
+            mOnGroupExpandListener.onGroupExpand(groupPos);
+        }
+
+        if (animate) {
+            final int groupFlatPos = pm.position.flatListPos;
+
+            final int shiftedGroupPosition = groupFlatPos + getHeaderViewsCount();
+            smoothScrollToPosition(shiftedGroupPosition + mAdapter.getChildrenCount(groupPos),
+                    shiftedGroupPosition);
+        }
+        pm.recycle();
+
+        return retValue;
+    }
+
+    /**
+     * Collapse a group in the grouped list view
+     *
+     * @param groupPos position of the group to collapse
+     * @return True if the group was collapsed, false otherwise (if the group
+     *         was already collapsed, this will return false)
+     */
+    public boolean collapseGroup(int groupPos) {
+        boolean retValue = mConnector.collapseGroup(groupPos);
+
+        if (mOnGroupCollapseListener != null) {
+            mOnGroupCollapseListener.onGroupCollapse(groupPos);
+        }
+
+        return retValue;
+    }
+
+    /** Used for being notified when a group is collapsed */
+    public interface OnGroupCollapseListener {
+        /**
+         * Callback method to be invoked when a group in this expandable list has
+         * been collapsed.
+         *
+         * @param groupPosition The group position that was collapsed
+         */
+        void onGroupCollapse(int groupPosition);
+    }
+
+    private OnGroupCollapseListener mOnGroupCollapseListener;
+
+    public void setOnGroupCollapseListener(
+            OnGroupCollapseListener onGroupCollapseListener) {
+        mOnGroupCollapseListener = onGroupCollapseListener;
+    }
+
+    /** Used for being notified when a group is expanded */
+    public interface OnGroupExpandListener {
+        /**
+         * Callback method to be invoked when a group in this expandable list has
+         * been expanded.
+         *
+         * @param groupPosition The group position that was expanded
+         */
+        void onGroupExpand(int groupPosition);
+    }
+
+    private OnGroupExpandListener mOnGroupExpandListener;
+
+    public void setOnGroupExpandListener(
+            OnGroupExpandListener onGroupExpandListener) {
+        mOnGroupExpandListener = onGroupExpandListener;
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when a group in this
+     * expandable list has been clicked.
+     */
+    public interface OnGroupClickListener {
+        /**
+         * Callback method to be invoked when a group in this expandable list has
+         * been clicked.
+         *
+         * @param parent The ExpandableListConnector where the click happened
+         * @param v The view within the expandable list/ListView that was clicked
+         * @param groupPosition The group position that was clicked
+         * @param id The row id of the group that was clicked
+         * @return True if the click was handled
+         */
+        boolean onGroupClick(ExpandableListView parent, View v, int groupPosition,
+                long id);
+    }
+
+    private OnGroupClickListener mOnGroupClickListener;
+
+    public void setOnGroupClickListener(OnGroupClickListener onGroupClickListener) {
+        mOnGroupClickListener = onGroupClickListener;
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when a child in this
+     * expandable list has been clicked.
+     */
+    public interface OnChildClickListener {
+        /**
+         * Callback method to be invoked when a child in this expandable list has
+         * been clicked.
+         *
+         * @param parent The ExpandableListView where the click happened
+         * @param v The view within the expandable list/ListView that was clicked
+         * @param groupPosition The group position that contains the child that
+         *        was clicked
+         * @param childPosition The child position within the group
+         * @param id The row id of the child that was clicked
+         * @return True if the click was handled
+         */
+        boolean onChildClick(ExpandableListView parent, View v, int groupPosition,
+                int childPosition, long id);
+    }
+
+    private OnChildClickListener mOnChildClickListener;
+
+    public void setOnChildClickListener(OnChildClickListener onChildClickListener) {
+        mOnChildClickListener = onChildClickListener;
+    }
+
+    /**
+     * Converts a flat list position (the raw position of an item (child or group)
+     * in the list) to a group and/or child position (represented in a
+     * packed position). This is useful in situations where the caller needs to
+     * use the underlying {@link ListView}'s methods. Use
+     * {@link ExpandableListView#getPackedPositionType} ,
+     * {@link ExpandableListView#getPackedPositionChild},
+     * {@link ExpandableListView#getPackedPositionGroup} to unpack.
+     *
+     * @param flatListPosition The flat list position to be converted.
+     * @return The group and/or child position for the given flat list position
+     *         in packed position representation. #PACKED_POSITION_VALUE_NULL if
+     *         the position corresponds to a header or a footer item.
+     */
+    public long getExpandableListPosition(int flatListPosition) {
+        if (isHeaderOrFooterPosition(flatListPosition)) {
+            return PACKED_POSITION_VALUE_NULL;
+        }
+
+        final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+        PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
+        long packedPos = pm.position.getPackedPosition();
+        pm.recycle();
+        return packedPos;
+    }
+
+    /**
+     * Converts a group and/or child position to a flat list position. This is
+     * useful in situations where the caller needs to use the underlying
+     * {@link ListView}'s methods.
+     *
+     * @param packedPosition The group and/or child positions to be converted in
+     *            packed position representation. Use
+     *            {@link #getPackedPositionForChild(int, int)} or
+     *            {@link #getPackedPositionForGroup(int)}.
+     * @return The flat list position for the given child or group.
+     */
+    public int getFlatListPosition(long packedPosition) {
+        ExpandableListPosition elPackedPos = ExpandableListPosition
+                .obtainPosition(packedPosition);
+        PositionMetadata pm = mConnector.getFlattenedPos(elPackedPos);
+        elPackedPos.recycle();
+        final int flatListPosition = pm.position.flatListPos;
+        pm.recycle();
+        return getAbsoluteFlatPosition(flatListPosition);
+    }
+
+    /**
+     * Gets the position of the currently selected group or child (along with
+     * its type). Can return {@link #PACKED_POSITION_VALUE_NULL} if no selection.
+     *
+     * @return A packed position containing the currently selected group or
+     *         child's position and type. #PACKED_POSITION_VALUE_NULL if no selection
+     *         or if selection is on a header or a footer item.
+     */
+    public long getSelectedPosition() {
+        final int selectedPos = getSelectedItemPosition();
+
+        // The case where there is no selection (selectedPos == -1) is also handled here.
+        return getExpandableListPosition(selectedPos);
+    }
+
+    /**
+     * Gets the ID of the currently selected group or child. Can return -1 if no
+     * selection.
+     *
+     * @return The ID of the currently selected group or child. -1 if no
+     *         selection.
+     */
+    public long getSelectedId() {
+        long packedPos = getSelectedPosition();
+        if (packedPos == PACKED_POSITION_VALUE_NULL) return -1;
+
+        int groupPos = getPackedPositionGroup(packedPos);
+
+        if (getPackedPositionType(packedPos) == PACKED_POSITION_TYPE_GROUP) {
+            // It's a group
+            return mAdapter.getGroupId(groupPos);
+        } else {
+            // It's a child
+            return mAdapter.getChildId(groupPos, getPackedPositionChild(packedPos));
+        }
+    }
+
+    /**
+     * Sets the selection to the specified group.
+     * @param groupPosition The position of the group that should be selected.
+     */
+    public void setSelectedGroup(int groupPosition) {
+        ExpandableListPosition elGroupPos = ExpandableListPosition
+                .obtainGroupPosition(groupPosition);
+        PositionMetadata pm = mConnector.getFlattenedPos(elGroupPos);
+        elGroupPos.recycle();
+        final int absoluteFlatPosition = getAbsoluteFlatPosition(pm.position.flatListPos);
+        super.setSelection(absoluteFlatPosition);
+        pm.recycle();
+    }
+
+    /**
+     * Sets the selection to the specified child. If the child is in a collapsed
+     * group, the group will only be expanded and child subsequently selected if
+     * shouldExpandGroup is set to true, otherwise the method will return false.
+     *
+     * @param groupPosition The position of the group that contains the child.
+     * @param childPosition The position of the child within the group.
+     * @param shouldExpandGroup Whether the child's group should be expanded if
+     *            it is collapsed.
+     * @return Whether the selection was successfully set on the child.
+     */
+    public boolean setSelectedChild(int groupPosition, int childPosition, boolean shouldExpandGroup) {
+        ExpandableListPosition elChildPos = ExpandableListPosition.obtainChildPosition(
+                groupPosition, childPosition);
+        PositionMetadata flatChildPos = mConnector.getFlattenedPos(elChildPos);
+
+        if (flatChildPos == null) {
+            // The child's group isn't expanded
+
+            // Shouldn't expand the group, so return false for we didn't set the selection
+            if (!shouldExpandGroup) return false;
+
+            expandGroup(groupPosition);
+
+            flatChildPos = mConnector.getFlattenedPos(elChildPos);
+
+            // Sanity check
+            if (flatChildPos == null) {
+                throw new IllegalStateException("Could not find child");
+            }
+        }
+
+        int absoluteFlatPosition = getAbsoluteFlatPosition(flatChildPos.position.flatListPos);
+        super.setSelection(absoluteFlatPosition);
+
+        elChildPos.recycle();
+        flatChildPos.recycle();
+
+        return true;
+    }
+
+    /**
+     * Whether the given group is currently expanded.
+     *
+     * @param groupPosition The group to check.
+     * @return Whether the group is currently expanded.
+     */
+    public boolean isGroupExpanded(int groupPosition) {
+        return mConnector.isGroupExpanded(groupPosition);
+    }
+
+    /**
+     * Gets the type of a packed position. See
+     * {@link #getPackedPositionForChild(int, int)}.
+     *
+     * @param packedPosition The packed position for which to return the type.
+     * @return The type of the position contained within the packed position,
+     *         either {@link #PACKED_POSITION_TYPE_CHILD}, {@link #PACKED_POSITION_TYPE_GROUP}, or
+     *         {@link #PACKED_POSITION_TYPE_NULL}.
+     */
+    public static int getPackedPositionType(long packedPosition) {
+        if (packedPosition == PACKED_POSITION_VALUE_NULL) {
+            return PACKED_POSITION_TYPE_NULL;
+        }
+
+        return (packedPosition & PACKED_POSITION_MASK_TYPE) == PACKED_POSITION_MASK_TYPE
+                ? PACKED_POSITION_TYPE_CHILD
+                : PACKED_POSITION_TYPE_GROUP;
+    }
+
+    /**
+     * Gets the group position from a packed position. See
+     * {@link #getPackedPositionForChild(int, int)}.
+     *
+     * @param packedPosition The packed position from which the group position
+     *            will be returned.
+     * @return The group position portion of the packed position. If this does
+     *         not contain a group, returns -1.
+     */
+    public static int getPackedPositionGroup(long packedPosition) {
+        // Null
+        if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
+
+        return (int) ((packedPosition & PACKED_POSITION_MASK_GROUP) >> PACKED_POSITION_SHIFT_GROUP);
+    }
+
+    /**
+     * Gets the child position from a packed position that is of
+     * {@link #PACKED_POSITION_TYPE_CHILD} type (use {@link #getPackedPositionType(long)}).
+     * To get the group that this child belongs to, use
+     * {@link #getPackedPositionGroup(long)}. See
+     * {@link #getPackedPositionForChild(int, int)}.
+     *
+     * @param packedPosition The packed position from which the child position
+     *            will be returned.
+     * @return The child position portion of the packed position. If this does
+     *         not contain a child, returns -1.
+     */
+    public static int getPackedPositionChild(long packedPosition) {
+        // Null
+        if (packedPosition == PACKED_POSITION_VALUE_NULL) return -1;
+
+        // Group since a group type clears this bit
+        if ((packedPosition & PACKED_POSITION_MASK_TYPE) != PACKED_POSITION_MASK_TYPE) return -1;
+
+        return (int) (packedPosition & PACKED_POSITION_MASK_CHILD);
+    }
+
+    /**
+     * Returns the packed position representation of a child's position.
+     * <p>
+     * In general, a packed position should be used in
+     * situations where the position given to/returned from an
+     * {@link ExpandableListAdapter} or {@link ExpandableListView} method can
+     * either be a child or group. The two positions are packed into a single
+     * long which can be unpacked using
+     * {@link #getPackedPositionChild(long)},
+     * {@link #getPackedPositionGroup(long)}, and
+     * {@link #getPackedPositionType(long)}.
+     *
+     * @param groupPosition The child's parent group's position.
+     * @param childPosition The child position within the group.
+     * @return The packed position representation of the child (and parent group).
+     */
+    public static long getPackedPositionForChild(int groupPosition, int childPosition) {
+        return (((long)PACKED_POSITION_TYPE_CHILD) << PACKED_POSITION_SHIFT_TYPE)
+                | ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
+                        << PACKED_POSITION_SHIFT_GROUP)
+                | (childPosition & PACKED_POSITION_INT_MASK_CHILD);
+    }
+
+    /**
+     * Returns the packed position representation of a group's position. See
+     * {@link #getPackedPositionForChild(int, int)}.
+     *
+     * @param groupPosition The child's parent group's position.
+     * @return The packed position representation of the group.
+     */
+    public static long getPackedPositionForGroup(int groupPosition) {
+        // No need to OR a type in because PACKED_POSITION_GROUP == 0
+        return ((((long)groupPosition) & PACKED_POSITION_INT_MASK_GROUP)
+                        << PACKED_POSITION_SHIFT_GROUP);
+    }
+
+    @Override
+    ContextMenuInfo createContextMenuInfo(View view, int flatListPosition, long id) {
+        if (isHeaderOrFooterPosition(flatListPosition)) {
+            // Return normal info for header/footer view context menus
+            return new AdapterContextMenuInfo(view, flatListPosition, id);
+        }
+
+        final int adjustedPosition = getFlatPositionForConnector(flatListPosition);
+        PositionMetadata pm = mConnector.getUnflattenedPos(adjustedPosition);
+        ExpandableListPosition pos = pm.position;
+
+        id = getChildOrGroupId(pos);
+        long packedPosition = pos.getPackedPosition();
+
+        pm.recycle();
+
+        return new ExpandableListContextMenuInfo(view, packedPosition, id);
+    }
+
+    /**
+     * Gets the ID of the group or child at the given <code>position</code>.
+     * This is useful since there is no ListAdapter ID -> ExpandableListAdapter
+     * ID conversion mechanism (in some cases, it isn't possible).
+     *
+     * @param position The position of the child or group whose ID should be
+     *            returned.
+     */
+    private long getChildOrGroupId(ExpandableListPosition position) {
+        if (position.type == ExpandableListPosition.CHILD) {
+            return mAdapter.getChildId(position.groupPos, position.childPos);
+        } else {
+            return mAdapter.getGroupId(position.groupPos);
+        }
+    }
+
+    /**
+     * Sets the indicator to be drawn next to a child.
+     *
+     * @param childIndicator The drawable to be used as an indicator. If the
+     *            child is the last child for a group, the state
+     *            {@link android.R.attr#state_last} will be set.
+     */
+    public void setChildIndicator(Drawable childIndicator) {
+        mChildIndicator = childIndicator;
+    }
+
+    /**
+     * Sets the drawing bounds for the child indicator. For either, you can
+     * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
+     * indicator's bounds.
+     *
+     * @see #setIndicatorBounds(int, int)
+     * @param left The left position (relative to the left bounds of this View)
+     *            to start drawing the indicator.
+     * @param right The right position (relative to the left bounds of this
+     *            View) to end the drawing of the indicator.
+     */
+    public void setChildIndicatorBounds(int left, int right) {
+        mChildIndicatorLeft = left;
+        mChildIndicatorRight = right;
+        resolveChildIndicator();
+    }
+
+    /**
+     * Sets the relative drawing bounds for the child indicator. For either, you can
+     * specify {@link #CHILD_INDICATOR_INHERIT} to use inherit from the general
+     * indicator's bounds.
+     *
+     * @see #setIndicatorBounds(int, int)
+     * @param start The start position (relative to the start bounds of this View)
+     *            to start drawing the indicator.
+     * @param end The end position (relative to the end bounds of this
+     *            View) to end the drawing of the indicator.
+     */
+    public void setChildIndicatorBoundsRelative(int start, int end) {
+        mChildIndicatorStart = start;
+        mChildIndicatorEnd = end;
+        resolveChildIndicator();
+    }
+
+    /**
+     * Sets the indicator to be drawn next to a group.
+     *
+     * @param groupIndicator The drawable to be used as an indicator. If the
+     *            group is empty, the state {@link android.R.attr#state_empty} will be
+     *            set. If the group is expanded, the state
+     *            {@link android.R.attr#state_expanded} will be set.
+     */
+    public void setGroupIndicator(Drawable groupIndicator) {
+        mGroupIndicator = groupIndicator;
+        if (mIndicatorRight == 0 && mGroupIndicator != null) {
+            mIndicatorRight = mIndicatorLeft + mGroupIndicator.getIntrinsicWidth();
+        }
+    }
+
+    /**
+     * Sets the drawing bounds for the indicators (at minimum, the group indicator
+     * is affected by this; the child indicator is affected by this if the
+     * child indicator bounds are set to inherit).
+     *
+     * @see #setChildIndicatorBounds(int, int)
+     * @param left The left position (relative to the left bounds of this View)
+     *            to start drawing the indicator.
+     * @param right The right position (relative to the left bounds of this
+     *            View) to end the drawing of the indicator.
+     */
+    public void setIndicatorBounds(int left, int right) {
+        mIndicatorLeft = left;
+        mIndicatorRight = right;
+        resolveIndicator();
+    }
+
+    /**
+     * Sets the relative drawing bounds for the indicators (at minimum, the group indicator
+     * is affected by this; the child indicator is affected by this if the
+     * child indicator bounds are set to inherit).
+     *
+     * @see #setChildIndicatorBounds(int, int)
+     * @param start The start position (relative to the start bounds of this View)
+     *            to start drawing the indicator.
+     * @param end The end position (relative to the end bounds of this
+     *            View) to end the drawing of the indicator.
+     */
+    public void setIndicatorBoundsRelative(int start, int end) {
+        mIndicatorStart = start;
+        mIndicatorEnd = end;
+        resolveIndicator();
+    }
+
+    /**
+     * Extra menu information specific to an {@link ExpandableListView} provided
+     * to the
+     * {@link android.view.View.OnCreateContextMenuListener#onCreateContextMenu(ContextMenu, View, ContextMenuInfo) }
+     * callback when a context menu is brought up for this AdapterView.
+     */
+    public static class ExpandableListContextMenuInfo implements ContextMenu.ContextMenuInfo {
+
+        public ExpandableListContextMenuInfo(View targetView, long packedPosition, long id) {
+            this.targetView = targetView;
+            this.packedPosition = packedPosition;
+            this.id = id;
+        }
+
+        /**
+         * The view for which the context menu is being displayed. This
+         * will be one of the children Views of this {@link ExpandableListView}.
+         */
+        public View targetView;
+
+        /**
+         * The packed position in the list represented by the adapter for which
+         * the context menu is being displayed. Use the methods
+         * {@link ExpandableListView#getPackedPositionType},
+         * {@link ExpandableListView#getPackedPositionChild}, and
+         * {@link ExpandableListView#getPackedPositionGroup} to unpack this.
+         */
+        public long packedPosition;
+
+        /**
+         * The ID of the item (group or child) for which the context menu is
+         * being displayed.
+         */
+        public long id;
+    }
+
+    static class SavedState extends BaseSavedState {
+        ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList;
+
+        /**
+         * Constructor called from {@link ExpandableListView#onSaveInstanceState()}
+         */
+        SavedState(
+                Parcelable superState,
+                ArrayList<ExpandableListConnector.GroupMetadata> expandedGroupMetadataList) {
+            super(superState);
+            this.expandedGroupMetadataList = expandedGroupMetadataList;
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            expandedGroupMetadataList = new ArrayList<ExpandableListConnector.GroupMetadata>();
+            in.readList(expandedGroupMetadataList, ExpandableListConnector.class.getClassLoader());
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeList(expandedGroupMetadataList);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        return new SavedState(superState,
+                mConnector != null ? mConnector.getExpandedGroupMetadataList() : null);
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (mConnector != null && ss.expandedGroupMetadataList != null) {
+            mConnector.setExpandedGroupMetadataList(ss.expandedGroupMetadataList);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ExpandableListView.class.getName();
+    }
+}
diff --git a/android/widget/FastScroller.java b/android/widget/FastScroller.java
new file mode 100644
index 0000000..198bf27
--- /dev/null
+++ b/android/widget/FastScroller.java
@@ -0,0 +1,1681 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.animation.Animator;
+import android.animation.Animator.AnimatorListener;
+import android.animation.AnimatorListenerAdapter;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.util.IntProperty;
+import android.util.MathUtils;
+import android.util.Property;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewGroupOverlay;
+import android.widget.AbsListView.OnScrollListener;
+import android.widget.ImageView.ScaleType;
+
+import com.android.internal.R;
+
+/**
+ * Helper class for AbsListView to draw and control the Fast Scroll thumb
+ */
+class FastScroller {
+    /** Duration of fade-out animation. */
+    private static final int DURATION_FADE_OUT = 300;
+
+    /** Duration of fade-in animation. */
+    private static final int DURATION_FADE_IN = 150;
+
+    /** Duration of transition cross-fade animation. */
+    private static final int DURATION_CROSS_FADE = 50;
+
+    /** Duration of transition resize animation. */
+    private static final int DURATION_RESIZE = 100;
+
+    /** Inactivity timeout before fading controls. */
+    private static final long FADE_TIMEOUT = 1500;
+
+    /** Minimum number of pages to justify showing a fast scroll thumb. */
+    private static final int MIN_PAGES = 4;
+
+    /** Scroll thumb and preview not showing. */
+    private static final int STATE_NONE = 0;
+
+    /** Scroll thumb visible and moving along with the scrollbar. */
+    private static final int STATE_VISIBLE = 1;
+
+    /** Scroll thumb and preview being dragged by user. */
+    private static final int STATE_DRAGGING = 2;
+
+    // Positions for preview image and text.
+    private static final int OVERLAY_FLOATING = 0;
+    private static final int OVERLAY_AT_THUMB = 1;
+    private static final int OVERLAY_ABOVE_THUMB = 2;
+
+    // Positions for thumb in relation to track.
+    private static final int THUMB_POSITION_MIDPOINT = 0;
+    private static final int THUMB_POSITION_INSIDE = 1;
+
+    // Indices for mPreviewResId.
+    private static final int PREVIEW_LEFT = 0;
+    private static final int PREVIEW_RIGHT = 1;
+
+    /** Delay before considering a tap in the thumb area to be a drag. */
+    private static final long TAP_TIMEOUT = ViewConfiguration.getTapTimeout();
+
+    private final Rect mTempBounds = new Rect();
+    private final Rect mTempMargins = new Rect();
+    private final Rect mContainerRect = new Rect();
+
+    private final AbsListView mList;
+    private final ViewGroupOverlay mOverlay;
+    private final TextView mPrimaryText;
+    private final TextView mSecondaryText;
+    private final ImageView mThumbImage;
+    private final ImageView mTrackImage;
+    private final View mPreviewImage;
+    /**
+     * Preview image resource IDs for left- and right-aligned layouts. See
+     * {@link #PREVIEW_LEFT} and {@link #PREVIEW_RIGHT}.
+     */
+    private final int[] mPreviewResId = new int[2];
+
+    /** The minimum touch target size in pixels. */
+    private final int mMinimumTouchTarget;
+
+    /**
+     * Padding in pixels around the preview text. Applied as layout margins to
+     * the preview text and padding to the preview image.
+     */
+    private int mPreviewPadding;
+
+    private int mPreviewMinWidth;
+    private int mPreviewMinHeight;
+    private int mThumbMinWidth;
+    private int mThumbMinHeight;
+
+    /** Theme-specified text size. Used only if text appearance is not set. */
+    private float mTextSize;
+
+    /** Theme-specified text color. Used only if text appearance is not set. */
+    private ColorStateList mTextColor;
+
+    private Drawable mThumbDrawable;
+    private Drawable mTrackDrawable;
+    private int mTextAppearance;
+    private int mThumbPosition;
+
+    // Used to convert between y-coordinate and thumb position within track.
+    private float mThumbOffset;
+    private float mThumbRange;
+
+    /** Total width of decorations. */
+    private int mWidth;
+
+    /** Set containing decoration transition animations. */
+    private AnimatorSet mDecorAnimation;
+
+    /** Set containing preview text transition animations. */
+    private AnimatorSet mPreviewAnimation;
+
+    /** Whether the primary text is showing. */
+    private boolean mShowingPrimary;
+
+    /** Whether we're waiting for completion of scrollTo(). */
+    private boolean mScrollCompleted;
+
+    /** The position of the first visible item in the list. */
+    private int mFirstVisibleItem;
+
+    /** The number of headers at the top of the view. */
+    private int mHeaderCount;
+
+    /** The index of the current section. */
+    private int mCurrentSection = -1;
+
+    /** The current scrollbar position. */
+    private int mScrollbarPosition = -1;
+
+    /** Whether the list is long enough to need a fast scroller. */
+    private boolean mLongList;
+
+    private Object[] mSections;
+
+    /** Whether this view is currently performing layout. */
+    private boolean mUpdatingLayout;
+
+    /**
+     * Current decoration state, one of:
+     * <ul>
+     * <li>{@link #STATE_NONE}, nothing visible
+     * <li>{@link #STATE_VISIBLE}, showing track and thumb
+     * <li>{@link #STATE_DRAGGING}, visible and showing preview
+     * </ul>
+     */
+    private int mState;
+
+    /** Whether the preview image is visible. */
+    private boolean mShowingPreview;
+
+    private Adapter mListAdapter;
+    private SectionIndexer mSectionIndexer;
+
+    /** Whether decorations should be laid out from right to left. */
+    private boolean mLayoutFromRight;
+
+    /** Whether the fast scroller is enabled. */
+    private boolean mEnabled;
+
+    /** Whether the scrollbar and decorations should always be shown. */
+    private boolean mAlwaysShow;
+
+    /**
+     * Position for the preview image and text. One of:
+     * <ul>
+     * <li>{@link #OVERLAY_FLOATING}
+     * <li>{@link #OVERLAY_AT_THUMB}
+     * <li>{@link #OVERLAY_ABOVE_THUMB}
+     * </ul>
+     */
+    private int mOverlayPosition;
+
+    /** Current scrollbar style, including inset and overlay properties. */
+    private int mScrollBarStyle;
+
+    /** Whether to precisely match the thumb position to the list. */
+    private boolean mMatchDragPosition;
+
+    private float mInitialTouchY;
+    private long mPendingDrag = -1;
+    private int mScaledTouchSlop;
+
+    private int mOldItemCount;
+    private int mOldChildCount;
+
+    /**
+     * Used to delay hiding fast scroll decorations.
+     */
+    private final Runnable mDeferHide = new Runnable() {
+        @Override
+        public void run() {
+            setState(STATE_NONE);
+        }
+    };
+
+    /**
+     * Used to effect a transition from primary to secondary text.
+     */
+    private final AnimatorListener mSwitchPrimaryListener = new AnimatorListenerAdapter() {
+        @Override
+        public void onAnimationEnd(Animator animation) {
+            mShowingPrimary = !mShowingPrimary;
+        }
+    };
+
+    public FastScroller(AbsListView listView, int styleResId) {
+        mList = listView;
+        mOldItemCount = listView.getCount();
+        mOldChildCount = listView.getChildCount();
+
+        final Context context = listView.getContext();
+        mScaledTouchSlop = ViewConfiguration.get(context).getScaledTouchSlop();
+        mScrollBarStyle = listView.getScrollBarStyle();
+
+        mScrollCompleted = true;
+        mState = STATE_VISIBLE;
+        mMatchDragPosition =
+                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
+
+        mTrackImage = new ImageView(context);
+        mTrackImage.setScaleType(ScaleType.FIT_XY);
+        mThumbImage = new ImageView(context);
+        mThumbImage.setScaleType(ScaleType.FIT_XY);
+        mPreviewImage = new View(context);
+        mPreviewImage.setAlpha(0f);
+
+        mPrimaryText = createPreviewTextView(context);
+        mSecondaryText = createPreviewTextView(context);
+
+        mMinimumTouchTarget = listView.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.fast_scroller_minimum_touch_target);
+
+        setStyle(styleResId);
+
+        final ViewGroupOverlay overlay = listView.getOverlay();
+        mOverlay = overlay;
+        overlay.add(mTrackImage);
+        overlay.add(mThumbImage);
+        overlay.add(mPreviewImage);
+        overlay.add(mPrimaryText);
+        overlay.add(mSecondaryText);
+
+        getSectionsFromIndexer();
+        updateLongList(mOldChildCount, mOldItemCount);
+        setScrollbarPosition(listView.getVerticalScrollbarPosition());
+        postAutoHide();
+    }
+
+    private void updateAppearance() {
+        int width = 0;
+
+        // Add track to overlay if it has an image.
+        mTrackImage.setImageDrawable(mTrackDrawable);
+        if (mTrackDrawable != null) {
+            width = Math.max(width, mTrackDrawable.getIntrinsicWidth());
+        }
+
+        // Add thumb to overlay if it has an image.
+        mThumbImage.setImageDrawable(mThumbDrawable);
+        mThumbImage.setMinimumWidth(mThumbMinWidth);
+        mThumbImage.setMinimumHeight(mThumbMinHeight);
+        if (mThumbDrawable != null) {
+            width = Math.max(width, mThumbDrawable.getIntrinsicWidth());
+        }
+
+        // Account for minimum thumb width.
+        mWidth = Math.max(width, mThumbMinWidth);
+
+        if (mTextAppearance != 0) {
+            mPrimaryText.setTextAppearance(mTextAppearance);
+            mSecondaryText.setTextAppearance(mTextAppearance);
+        }
+
+        if (mTextColor != null) {
+            mPrimaryText.setTextColor(mTextColor);
+            mSecondaryText.setTextColor(mTextColor);
+        }
+
+        if (mTextSize > 0) {
+            mPrimaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+            mSecondaryText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mTextSize);
+        }
+
+        final int padding = mPreviewPadding;
+        mPrimaryText.setIncludeFontPadding(false);
+        mPrimaryText.setPadding(padding, padding, padding, padding);
+        mSecondaryText.setIncludeFontPadding(false);
+        mSecondaryText.setPadding(padding, padding, padding, padding);
+
+        refreshDrawablePressedState();
+    }
+
+    public void setStyle(@StyleRes int resId) {
+        final Context context = mList.getContext();
+        final TypedArray ta = context.obtainStyledAttributes(null,
+                R.styleable.FastScroll, R.attr.fastScrollStyle, resId);
+        final int N = ta.getIndexCount();
+        for (int i = 0; i < N; i++) {
+            final int index = ta.getIndex(i);
+            switch (index) {
+                case R.styleable.FastScroll_position:
+                    mOverlayPosition = ta.getInt(index, OVERLAY_FLOATING);
+                    break;
+                case R.styleable.FastScroll_backgroundLeft:
+                    mPreviewResId[PREVIEW_LEFT] = ta.getResourceId(index, 0);
+                    break;
+                case R.styleable.FastScroll_backgroundRight:
+                    mPreviewResId[PREVIEW_RIGHT] = ta.getResourceId(index, 0);
+                    break;
+                case R.styleable.FastScroll_thumbDrawable:
+                    mThumbDrawable = ta.getDrawable(index);
+                    break;
+                case R.styleable.FastScroll_trackDrawable:
+                    mTrackDrawable = ta.getDrawable(index);
+                    break;
+                case R.styleable.FastScroll_textAppearance:
+                    mTextAppearance = ta.getResourceId(index, 0);
+                    break;
+                case R.styleable.FastScroll_textColor:
+                    mTextColor = ta.getColorStateList(index);
+                    break;
+                case R.styleable.FastScroll_textSize:
+                    mTextSize = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_minWidth:
+                    mPreviewMinWidth = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_minHeight:
+                    mPreviewMinHeight = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_thumbMinWidth:
+                    mThumbMinWidth = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_thumbMinHeight:
+                    mThumbMinHeight = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_padding:
+                    mPreviewPadding = ta.getDimensionPixelSize(index, 0);
+                    break;
+                case R.styleable.FastScroll_thumbPosition:
+                    mThumbPosition = ta.getInt(index, THUMB_POSITION_MIDPOINT);
+                    break;
+            }
+        }
+        ta.recycle();
+
+        updateAppearance();
+    }
+
+    /**
+     * Removes this FastScroller overlay from the host view.
+     */
+    public void remove() {
+        mOverlay.remove(mTrackImage);
+        mOverlay.remove(mThumbImage);
+        mOverlay.remove(mPreviewImage);
+        mOverlay.remove(mPrimaryText);
+        mOverlay.remove(mSecondaryText);
+    }
+
+    /**
+     * @param enabled Whether the fast scroll thumb is enabled.
+     */
+    public void setEnabled(boolean enabled) {
+        if (mEnabled != enabled) {
+            mEnabled = enabled;
+
+            onStateDependencyChanged(true);
+        }
+    }
+
+    /**
+     * @return Whether the fast scroll thumb is enabled.
+     */
+    public boolean isEnabled() {
+        return mEnabled && (mLongList || mAlwaysShow);
+    }
+
+    /**
+     * @param alwaysShow Whether the fast scroll thumb should always be shown
+     */
+    public void setAlwaysShow(boolean alwaysShow) {
+        if (mAlwaysShow != alwaysShow) {
+            mAlwaysShow = alwaysShow;
+
+            onStateDependencyChanged(false);
+        }
+    }
+
+    /**
+     * @return Whether the fast scroll thumb will always be shown
+     * @see #setAlwaysShow(boolean)
+     */
+    public boolean isAlwaysShowEnabled() {
+        return mAlwaysShow;
+    }
+
+    /**
+     * Called when one of the variables affecting enabled state changes.
+     *
+     * @param peekIfEnabled whether the thumb should peek, if enabled
+     */
+    private void onStateDependencyChanged(boolean peekIfEnabled) {
+        if (isEnabled()) {
+            if (isAlwaysShowEnabled()) {
+                setState(STATE_VISIBLE);
+            } else if (mState == STATE_VISIBLE) {
+                postAutoHide();
+            } else if (peekIfEnabled) {
+                setState(STATE_VISIBLE);
+                postAutoHide();
+            }
+        } else {
+            stop();
+        }
+
+        mList.resolvePadding();
+    }
+
+    public void setScrollBarStyle(int style) {
+        if (mScrollBarStyle != style) {
+            mScrollBarStyle = style;
+
+            updateLayout();
+        }
+    }
+
+    /**
+     * Immediately transitions the fast scroller decorations to a hidden state.
+     */
+    public void stop() {
+        setState(STATE_NONE);
+    }
+
+    public void setScrollbarPosition(int position) {
+        if (position == View.SCROLLBAR_POSITION_DEFAULT) {
+            position = mList.isLayoutRtl() ?
+                    View.SCROLLBAR_POSITION_LEFT : View.SCROLLBAR_POSITION_RIGHT;
+        }
+
+        if (mScrollbarPosition != position) {
+            mScrollbarPosition = position;
+            mLayoutFromRight = position != View.SCROLLBAR_POSITION_LEFT;
+
+            final int previewResId = mPreviewResId[mLayoutFromRight ? PREVIEW_RIGHT : PREVIEW_LEFT];
+            mPreviewImage.setBackgroundResource(previewResId);
+
+            // Propagate padding to text min width/height.
+            final int textMinWidth = Math.max(0, mPreviewMinWidth - mPreviewImage.getPaddingLeft()
+                    - mPreviewImage.getPaddingRight());
+            mPrimaryText.setMinimumWidth(textMinWidth);
+            mSecondaryText.setMinimumWidth(textMinWidth);
+
+            final int textMinHeight = Math.max(0, mPreviewMinHeight - mPreviewImage.getPaddingTop()
+                    - mPreviewImage.getPaddingBottom());
+            mPrimaryText.setMinimumHeight(textMinHeight);
+            mSecondaryText.setMinimumHeight(textMinHeight);
+
+            // Requires re-layout.
+            updateLayout();
+        }
+    }
+
+    public int getWidth() {
+        return mWidth;
+    }
+
+    public void onSizeChanged(int w, int h, int oldw, int oldh) {
+        updateLayout();
+    }
+
+    public void onItemCountChanged(int childCount, int itemCount) {
+        if (mOldItemCount != itemCount || mOldChildCount != childCount) {
+            mOldItemCount = itemCount;
+            mOldChildCount = childCount;
+
+            final boolean hasMoreItems = itemCount - childCount > 0;
+            if (hasMoreItems && mState != STATE_DRAGGING) {
+                final int firstVisibleItem = mList.getFirstVisiblePosition();
+                setThumbPos(getPosFromItemCount(firstVisibleItem, childCount, itemCount));
+            }
+
+            updateLongList(childCount, itemCount);
+        }
+    }
+
+    private void updateLongList(int childCount, int itemCount) {
+        final boolean longList = childCount > 0 && itemCount / childCount >= MIN_PAGES;
+        if (mLongList != longList) {
+            mLongList = longList;
+
+            onStateDependencyChanged(false);
+        }
+    }
+
+    /**
+     * Creates a view into which preview text can be placed.
+     */
+    private TextView createPreviewTextView(Context context) {
+        final LayoutParams params = new LayoutParams(
+                LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+        final TextView textView = new TextView(context);
+        textView.setLayoutParams(params);
+        textView.setSingleLine(true);
+        textView.setEllipsize(TruncateAt.MIDDLE);
+        textView.setGravity(Gravity.CENTER);
+        textView.setAlpha(0f);
+
+        // Manually propagate inherited layout direction.
+        textView.setLayoutDirection(mList.getLayoutDirection());
+
+        return textView;
+    }
+
+    /**
+     * Measures and layouts the scrollbar and decorations.
+     */
+    public void updateLayout() {
+        // Prevent re-entry when RTL properties change as a side-effect of
+        // resolving padding.
+        if (mUpdatingLayout) {
+            return;
+        }
+
+        mUpdatingLayout = true;
+
+        updateContainerRect();
+
+        layoutThumb();
+        layoutTrack();
+
+        updateOffsetAndRange();
+
+        final Rect bounds = mTempBounds;
+        measurePreview(mPrimaryText, bounds);
+        applyLayout(mPrimaryText, bounds);
+        measurePreview(mSecondaryText, bounds);
+        applyLayout(mSecondaryText, bounds);
+
+        if (mPreviewImage != null) {
+            // Apply preview image padding.
+            bounds.left -= mPreviewImage.getPaddingLeft();
+            bounds.top -= mPreviewImage.getPaddingTop();
+            bounds.right += mPreviewImage.getPaddingRight();
+            bounds.bottom += mPreviewImage.getPaddingBottom();
+            applyLayout(mPreviewImage, bounds);
+        }
+
+        mUpdatingLayout = false;
+    }
+
+    /**
+     * Layouts a view within the specified bounds and pins the pivot point to
+     * the appropriate edge.
+     *
+     * @param view The view to layout.
+     * @param bounds Bounds at which to layout the view.
+     */
+    private void applyLayout(View view, Rect bounds) {
+        view.layout(bounds.left, bounds.top, bounds.right, bounds.bottom);
+        view.setPivotX(mLayoutFromRight ? bounds.right - bounds.left : 0);
+    }
+
+    /**
+     * Measures the preview text bounds, taking preview image padding into
+     * account. This method should only be called after {@link #layoutThumb()}
+     * and {@link #layoutTrack()} have both been called at least once.
+     *
+     * @param v The preview text view to measure.
+     * @param out Rectangle into which measured bounds are placed.
+     */
+    private void measurePreview(View v, Rect out) {
+        // Apply the preview image's padding as layout margins.
+        final Rect margins = mTempMargins;
+        margins.left = mPreviewImage.getPaddingLeft();
+        margins.top = mPreviewImage.getPaddingTop();
+        margins.right = mPreviewImage.getPaddingRight();
+        margins.bottom = mPreviewImage.getPaddingBottom();
+
+        if (mOverlayPosition == OVERLAY_FLOATING) {
+            measureFloating(v, margins, out);
+        } else {
+            measureViewToSide(v, mThumbImage, margins, out);
+        }
+    }
+
+    /**
+     * Measures the bounds for a view that should be laid out against the edge
+     * of an adjacent view. If no adjacent view is provided, lays out against
+     * the list edge.
+     *
+     * @param view The view to measure for layout.
+     * @param adjacent (Optional) The adjacent view, may be null to align to the
+     *            list edge.
+     * @param margins Layout margins to apply to the view.
+     * @param out Rectangle into which measured bounds are placed.
+     */
+    private void measureViewToSide(View view, View adjacent, Rect margins, Rect out) {
+        final int marginLeft;
+        final int marginTop;
+        final int marginRight;
+        if (margins == null) {
+            marginLeft = 0;
+            marginTop = 0;
+            marginRight = 0;
+        } else {
+            marginLeft = margins.left;
+            marginTop = margins.top;
+            marginRight = margins.right;
+        }
+
+        final Rect container = mContainerRect;
+        final int containerWidth = container.width();
+        final int maxWidth;
+        if (adjacent == null) {
+            maxWidth = containerWidth;
+        } else if (mLayoutFromRight) {
+            maxWidth = adjacent.getLeft();
+        } else {
+            maxWidth = containerWidth - adjacent.getRight();
+        }
+
+        final int adjMaxHeight = Math.max(0, container.height());
+        final int adjMaxWidth = Math.max(0, maxWidth - marginLeft - marginRight);
+        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
+        final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                adjMaxHeight, MeasureSpec.UNSPECIFIED);
+        view.measure(widthMeasureSpec, heightMeasureSpec);
+
+        // Align to the left or right.
+        final int width = Math.min(adjMaxWidth, view.getMeasuredWidth());
+        final int left;
+        final int right;
+        if (mLayoutFromRight) {
+            right = (adjacent == null ? container.right : adjacent.getLeft()) - marginRight;
+            left = right - width;
+        } else {
+            left = (adjacent == null ? container.left : adjacent.getRight()) + marginLeft;
+            right = left + width;
+        }
+
+        // Don't adjust the vertical position.
+        final int top = marginTop;
+        final int bottom = top + view.getMeasuredHeight();
+        out.set(left, top, right, bottom);
+    }
+
+    private void measureFloating(View preview, Rect margins, Rect out) {
+        final int marginLeft;
+        final int marginTop;
+        final int marginRight;
+        if (margins == null) {
+            marginLeft = 0;
+            marginTop = 0;
+            marginRight = 0;
+        } else {
+            marginLeft = margins.left;
+            marginTop = margins.top;
+            marginRight = margins.right;
+        }
+
+        final Rect container = mContainerRect;
+        final int containerWidth = container.width();
+        final int adjMaxHeight = Math.max(0, container.height());
+        final int adjMaxWidth = Math.max(0, containerWidth - marginLeft - marginRight);
+        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(adjMaxWidth, MeasureSpec.AT_MOST);
+        final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                adjMaxHeight, MeasureSpec.UNSPECIFIED);
+        preview.measure(widthMeasureSpec, heightMeasureSpec);
+
+        // Align at the vertical center, 10% from the top.
+        final int containerHeight = container.height();
+        final int width = preview.getMeasuredWidth();
+        final int top = containerHeight / 10 + marginTop + container.top;
+        final int bottom = top + preview.getMeasuredHeight();
+        final int left = (containerWidth - width) / 2 + container.left;
+        final int right = left + width;
+        out.set(left, top, right, bottom);
+    }
+
+    /**
+     * Updates the container rectangle used for layout.
+     */
+    private void updateContainerRect() {
+        final AbsListView list = mList;
+        list.resolvePadding();
+
+        final Rect container = mContainerRect;
+        container.left = 0;
+        container.top = 0;
+        container.right = list.getWidth();
+        container.bottom = list.getHeight();
+
+        final int scrollbarStyle = mScrollBarStyle;
+        if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET
+                || scrollbarStyle == View.SCROLLBARS_INSIDE_OVERLAY) {
+            container.left += list.getPaddingLeft();
+            container.top += list.getPaddingTop();
+            container.right -= list.getPaddingRight();
+            container.bottom -= list.getPaddingBottom();
+
+            // In inset mode, we need to adjust for padded scrollbar width.
+            if (scrollbarStyle == View.SCROLLBARS_INSIDE_INSET) {
+                final int width = getWidth();
+                if (mScrollbarPosition == View.SCROLLBAR_POSITION_RIGHT) {
+                    container.right += width;
+                } else {
+                    container.left -= width;
+                }
+            }
+        }
+    }
+
+    /**
+     * Lays out the thumb according to the current scrollbar position.
+     */
+    private void layoutThumb() {
+        final Rect bounds = mTempBounds;
+        measureViewToSide(mThumbImage, null, null, bounds);
+        applyLayout(mThumbImage, bounds);
+    }
+
+    /**
+     * Lays out the track centered on the thumb. Must be called after
+     * {@link #layoutThumb}.
+     */
+    private void layoutTrack() {
+        final View track = mTrackImage;
+        final View thumb = mThumbImage;
+        final Rect container = mContainerRect;
+        final int maxWidth = Math.max(0, container.width());
+        final int maxHeight = Math.max(0, container.height());
+        final int widthMeasureSpec = MeasureSpec.makeMeasureSpec(maxWidth, MeasureSpec.AT_MOST);
+        final int heightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                maxHeight, MeasureSpec.UNSPECIFIED);
+        track.measure(widthMeasureSpec, heightMeasureSpec);
+
+        final int top;
+        final int bottom;
+        if (mThumbPosition == THUMB_POSITION_INSIDE) {
+            top = container.top;
+            bottom = container.bottom;
+        } else {
+            final int thumbHalfHeight = thumb.getHeight() / 2;
+            top = container.top + thumbHalfHeight;
+            bottom = container.bottom - thumbHalfHeight;
+        }
+
+        final int trackWidth = track.getMeasuredWidth();
+        final int left = thumb.getLeft() + (thumb.getWidth() - trackWidth) / 2;
+        final int right = left + trackWidth;
+        track.layout(left, top, right, bottom);
+    }
+
+    /**
+     * Updates the offset and range used to convert from absolute y-position to
+     * thumb position within the track.
+     */
+    private void updateOffsetAndRange() {
+        final View trackImage = mTrackImage;
+        final View thumbImage = mThumbImage;
+        final float min;
+        final float max;
+        if (mThumbPosition == THUMB_POSITION_INSIDE) {
+            final float halfThumbHeight = thumbImage.getHeight() / 2f;
+            min = trackImage.getTop() + halfThumbHeight;
+            max = trackImage.getBottom() - halfThumbHeight;
+        } else{
+            min = trackImage.getTop();
+            max = trackImage.getBottom();
+        }
+
+        mThumbOffset = min;
+        mThumbRange = max - min;
+    }
+
+    private void setState(int state) {
+        mList.removeCallbacks(mDeferHide);
+
+        if (mAlwaysShow && state == STATE_NONE) {
+            state = STATE_VISIBLE;
+        }
+
+        if (state == mState) {
+            return;
+        }
+
+        switch (state) {
+            case STATE_NONE:
+                transitionToHidden();
+                break;
+            case STATE_VISIBLE:
+                transitionToVisible();
+                break;
+            case STATE_DRAGGING:
+                if (transitionPreviewLayout(mCurrentSection)) {
+                    transitionToDragging();
+                } else {
+                    transitionToVisible();
+                }
+                break;
+        }
+
+        mState = state;
+
+        refreshDrawablePressedState();
+    }
+
+    private void refreshDrawablePressedState() {
+        final boolean isPressed = mState == STATE_DRAGGING;
+        mThumbImage.setPressed(isPressed);
+        mTrackImage.setPressed(isPressed);
+    }
+
+    /**
+     * Shows nothing.
+     */
+    private void transitionToHidden() {
+        if (mDecorAnimation != null) {
+            mDecorAnimation.cancel();
+        }
+
+        final Animator fadeOut = groupAnimatorOfFloat(View.ALPHA, 0f, mThumbImage, mTrackImage,
+                mPreviewImage, mPrimaryText, mSecondaryText).setDuration(DURATION_FADE_OUT);
+
+        // Push the thumb and track outside the list bounds.
+        final float offset = mLayoutFromRight ? mThumbImage.getWidth() : -mThumbImage.getWidth();
+        final Animator slideOut = groupAnimatorOfFloat(
+                View.TRANSLATION_X, offset, mThumbImage, mTrackImage)
+                .setDuration(DURATION_FADE_OUT);
+
+        mDecorAnimation = new AnimatorSet();
+        mDecorAnimation.playTogether(fadeOut, slideOut);
+        mDecorAnimation.start();
+
+        mShowingPreview = false;
+    }
+
+    /**
+     * Shows the thumb and track.
+     */
+    private void transitionToVisible() {
+        if (mDecorAnimation != null) {
+            mDecorAnimation.cancel();
+        }
+
+        final Animator fadeIn = groupAnimatorOfFloat(View.ALPHA, 1f, mThumbImage, mTrackImage)
+                .setDuration(DURATION_FADE_IN);
+        final Animator fadeOut = groupAnimatorOfFloat(
+                View.ALPHA, 0f, mPreviewImage, mPrimaryText, mSecondaryText)
+                .setDuration(DURATION_FADE_OUT);
+        final Animator slideIn = groupAnimatorOfFloat(
+                View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
+
+        mDecorAnimation = new AnimatorSet();
+        mDecorAnimation.playTogether(fadeIn, fadeOut, slideIn);
+        mDecorAnimation.start();
+
+        mShowingPreview = false;
+    }
+
+    /**
+     * Shows the thumb, preview, and track.
+     */
+    private void transitionToDragging() {
+        if (mDecorAnimation != null) {
+            mDecorAnimation.cancel();
+        }
+
+        final Animator fadeIn = groupAnimatorOfFloat(
+                View.ALPHA, 1f, mThumbImage, mTrackImage, mPreviewImage)
+                .setDuration(DURATION_FADE_IN);
+        final Animator slideIn = groupAnimatorOfFloat(
+                View.TRANSLATION_X, 0f, mThumbImage, mTrackImage).setDuration(DURATION_FADE_IN);
+
+        mDecorAnimation = new AnimatorSet();
+        mDecorAnimation.playTogether(fadeIn, slideIn);
+        mDecorAnimation.start();
+
+        mShowingPreview = true;
+    }
+
+    private void postAutoHide() {
+        mList.removeCallbacks(mDeferHide);
+        mList.postDelayed(mDeferHide, FADE_TIMEOUT);
+    }
+
+    public void onScroll(int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+        if (!isEnabled()) {
+            setState(STATE_NONE);
+            return;
+        }
+
+        final boolean hasMoreItems = totalItemCount - visibleItemCount > 0;
+        if (hasMoreItems && mState != STATE_DRAGGING) {
+            setThumbPos(getPosFromItemCount(firstVisibleItem, visibleItemCount, totalItemCount));
+        }
+
+        mScrollCompleted = true;
+
+        if (mFirstVisibleItem != firstVisibleItem) {
+            mFirstVisibleItem = firstVisibleItem;
+
+            // Show the thumb, if necessary, and set up auto-fade.
+            if (mState != STATE_DRAGGING) {
+                setState(STATE_VISIBLE);
+                postAutoHide();
+            }
+        }
+    }
+
+    private void getSectionsFromIndexer() {
+        mSectionIndexer = null;
+
+        Adapter adapter = mList.getAdapter();
+        if (adapter instanceof HeaderViewListAdapter) {
+            mHeaderCount = ((HeaderViewListAdapter) adapter).getHeadersCount();
+            adapter = ((HeaderViewListAdapter) adapter).getWrappedAdapter();
+        }
+
+        if (adapter instanceof ExpandableListConnector) {
+            final ExpandableListAdapter expAdapter = ((ExpandableListConnector) adapter)
+                    .getAdapter();
+            if (expAdapter instanceof SectionIndexer) {
+                mSectionIndexer = (SectionIndexer) expAdapter;
+                mListAdapter = adapter;
+                mSections = mSectionIndexer.getSections();
+            }
+        } else if (adapter instanceof SectionIndexer) {
+            mListAdapter = adapter;
+            mSectionIndexer = (SectionIndexer) adapter;
+            mSections = mSectionIndexer.getSections();
+        } else {
+            mListAdapter = adapter;
+            mSections = null;
+        }
+    }
+
+    public void onSectionsChanged() {
+        mListAdapter = null;
+    }
+
+    /**
+     * Scrolls to a specific position within the section
+     * @param position
+     */
+    private void scrollTo(float position) {
+        mScrollCompleted = false;
+
+        final int count = mList.getCount();
+        final Object[] sections = mSections;
+        final int sectionCount = sections == null ? 0 : sections.length;
+        int sectionIndex;
+        if (sections != null && sectionCount > 1) {
+            final int exactSection = MathUtils.constrain(
+                    (int) (position * sectionCount), 0, sectionCount - 1);
+            int targetSection = exactSection;
+            int targetIndex = mSectionIndexer.getPositionForSection(targetSection);
+            sectionIndex = targetSection;
+
+            // Given the expected section and index, the following code will
+            // try to account for missing sections (no names starting with..)
+            // It will compute the scroll space of surrounding empty sections
+            // and interpolate the currently visible letter's range across the
+            // available space, so that there is always some list movement while
+            // the user moves the thumb.
+            int nextIndex = count;
+            int prevIndex = targetIndex;
+            int prevSection = targetSection;
+            int nextSection = targetSection + 1;
+
+            // Assume the next section is unique
+            if (targetSection < sectionCount - 1) {
+                nextIndex = mSectionIndexer.getPositionForSection(targetSection + 1);
+            }
+
+            // Find the previous index if we're slicing the previous section
+            if (nextIndex == targetIndex) {
+                // Non-existent letter
+                while (targetSection > 0) {
+                    targetSection--;
+                    prevIndex = mSectionIndexer.getPositionForSection(targetSection);
+                    if (prevIndex != targetIndex) {
+                        prevSection = targetSection;
+                        sectionIndex = targetSection;
+                        break;
+                    } else if (targetSection == 0) {
+                        // When section reaches 0 here, sectionIndex must follow it.
+                        // Assuming mSectionIndexer.getPositionForSection(0) == 0.
+                        sectionIndex = 0;
+                        break;
+                    }
+                }
+            }
+
+            // Find the next index, in case the assumed next index is not
+            // unique. For instance, if there is no P, then request for P's
+            // position actually returns Q's. So we need to look ahead to make
+            // sure that there is really a Q at Q's position. If not, move
+            // further down...
+            int nextNextSection = nextSection + 1;
+            while (nextNextSection < sectionCount &&
+                    mSectionIndexer.getPositionForSection(nextNextSection) == nextIndex) {
+                nextNextSection++;
+                nextSection++;
+            }
+
+            // Compute the beginning and ending scroll range percentage of the
+            // currently visible section. This could be equal to or greater than
+            // (1 / nSections). If the target position is near the previous
+            // position, snap to the previous position.
+            final float prevPosition = (float) prevSection / sectionCount;
+            final float nextPosition = (float) nextSection / sectionCount;
+            final float snapThreshold = (count == 0) ? Float.MAX_VALUE : .125f / count;
+            if (prevSection == exactSection && position - prevPosition < snapThreshold) {
+                targetIndex = prevIndex;
+            } else {
+                targetIndex = prevIndex + (int) ((nextIndex - prevIndex) * (position - prevPosition)
+                    / (nextPosition - prevPosition));
+            }
+
+            // Clamp to valid positions.
+            targetIndex = MathUtils.constrain(targetIndex, 0, count - 1);
+
+            if (mList instanceof ExpandableListView) {
+                final ExpandableListView expList = (ExpandableListView) mList;
+                expList.setSelectionFromTop(expList.getFlatListPosition(
+                        ExpandableListView.getPackedPositionForGroup(targetIndex + mHeaderCount)),
+                        0);
+            } else if (mList instanceof ListView) {
+                ((ListView) mList).setSelectionFromTop(targetIndex + mHeaderCount, 0);
+            } else {
+                mList.setSelection(targetIndex + mHeaderCount);
+            }
+        } else {
+            final int index = MathUtils.constrain((int) (position * count), 0, count - 1);
+
+            if (mList instanceof ExpandableListView) {
+                ExpandableListView expList = (ExpandableListView) mList;
+                expList.setSelectionFromTop(expList.getFlatListPosition(
+                        ExpandableListView.getPackedPositionForGroup(index + mHeaderCount)), 0);
+            } else if (mList instanceof ListView) {
+                ((ListView)mList).setSelectionFromTop(index + mHeaderCount, 0);
+            } else {
+                mList.setSelection(index + mHeaderCount);
+            }
+
+            sectionIndex = -1;
+        }
+
+        if (mCurrentSection != sectionIndex) {
+            mCurrentSection = sectionIndex;
+
+            final boolean hasPreview = transitionPreviewLayout(sectionIndex);
+            if (!mShowingPreview && hasPreview) {
+                transitionToDragging();
+            } else if (mShowingPreview && !hasPreview) {
+                transitionToVisible();
+            }
+        }
+    }
+
+    /**
+     * Transitions the preview text to a new section. Handles animation,
+     * measurement, and layout. If the new preview text is empty, returns false.
+     *
+     * @param sectionIndex The section index to which the preview should
+     *            transition.
+     * @return False if the new preview text is empty.
+     */
+    private boolean transitionPreviewLayout(int sectionIndex) {
+        final Object[] sections = mSections;
+        String text = null;
+        if (sections != null && sectionIndex >= 0 && sectionIndex < sections.length) {
+            final Object section = sections[sectionIndex];
+            if (section != null) {
+                text = section.toString();
+            }
+        }
+
+        final Rect bounds = mTempBounds;
+        final View preview = mPreviewImage;
+        final TextView showing;
+        final TextView target;
+        if (mShowingPrimary) {
+            showing = mPrimaryText;
+            target = mSecondaryText;
+        } else {
+            showing = mSecondaryText;
+            target = mPrimaryText;
+        }
+
+        // Set and layout target immediately.
+        target.setText(text);
+        measurePreview(target, bounds);
+        applyLayout(target, bounds);
+
+        if (mPreviewAnimation != null) {
+            mPreviewAnimation.cancel();
+        }
+
+        // Cross-fade preview text.
+        final Animator showTarget = animateAlpha(target, 1f).setDuration(DURATION_CROSS_FADE);
+        final Animator hideShowing = animateAlpha(showing, 0f).setDuration(DURATION_CROSS_FADE);
+        hideShowing.addListener(mSwitchPrimaryListener);
+
+        // Apply preview image padding and animate bounds, if necessary.
+        bounds.left -= preview.getPaddingLeft();
+        bounds.top -= preview.getPaddingTop();
+        bounds.right += preview.getPaddingRight();
+        bounds.bottom += preview.getPaddingBottom();
+        final Animator resizePreview = animateBounds(preview, bounds);
+        resizePreview.setDuration(DURATION_RESIZE);
+
+        mPreviewAnimation = new AnimatorSet();
+        final AnimatorSet.Builder builder = mPreviewAnimation.play(hideShowing).with(showTarget);
+        builder.with(resizePreview);
+
+        // The current preview size is unaffected by hidden or showing. It's
+        // used to set starting scales for things that need to be scaled down.
+        final int previewWidth = preview.getWidth() - preview.getPaddingLeft()
+                - preview.getPaddingRight();
+
+        // If target is too large, shrink it immediately to fit and expand to
+        // target size. Otherwise, start at target size.
+        final int targetWidth = target.getWidth();
+        if (targetWidth > previewWidth) {
+            target.setScaleX((float) previewWidth / targetWidth);
+            final Animator scaleAnim = animateScaleX(target, 1f).setDuration(DURATION_RESIZE);
+            builder.with(scaleAnim);
+        } else {
+            target.setScaleX(1f);
+        }
+
+        // If showing is larger than target, shrink to target size.
+        final int showingWidth = showing.getWidth();
+        if (showingWidth > targetWidth) {
+            final float scale = (float) targetWidth / showingWidth;
+            final Animator scaleAnim = animateScaleX(showing, scale).setDuration(DURATION_RESIZE);
+            builder.with(scaleAnim);
+        }
+
+        mPreviewAnimation.start();
+
+        return !TextUtils.isEmpty(text);
+    }
+
+    /**
+     * Positions the thumb and preview widgets.
+     *
+     * @param position The position, between 0 and 1, along the track at which
+     *            to place the thumb.
+     */
+    private void setThumbPos(float position) {
+        final float thumbMiddle = position * mThumbRange + mThumbOffset;
+        mThumbImage.setTranslationY(thumbMiddle - mThumbImage.getHeight() / 2f);
+
+        final View previewImage = mPreviewImage;
+        final float previewHalfHeight = previewImage.getHeight() / 2f;
+        final float previewPos;
+        switch (mOverlayPosition) {
+            case OVERLAY_AT_THUMB:
+                previewPos = thumbMiddle;
+                break;
+            case OVERLAY_ABOVE_THUMB:
+                previewPos = thumbMiddle - previewHalfHeight;
+                break;
+            case OVERLAY_FLOATING:
+            default:
+                previewPos = 0;
+                break;
+        }
+
+        // Center the preview on the thumb, constrained to the list bounds.
+        final Rect container = mContainerRect;
+        final int top = container.top;
+        final int bottom = container.bottom;
+        final float minP = top + previewHalfHeight;
+        final float maxP = bottom - previewHalfHeight;
+        final float previewMiddle = MathUtils.constrain(previewPos, minP, maxP);
+        final float previewTop = previewMiddle - previewHalfHeight;
+        previewImage.setTranslationY(previewTop);
+
+        mPrimaryText.setTranslationY(previewTop);
+        mSecondaryText.setTranslationY(previewTop);
+    }
+
+    private float getPosFromMotionEvent(float y) {
+        // If the list is the same height as the thumbnail or shorter,
+        // effectively disable scrolling.
+        if (mThumbRange <= 0) {
+            return 0f;
+        }
+
+        return MathUtils.constrain((y - mThumbOffset) / mThumbRange, 0f, 1f);
+    }
+
+    /**
+     * Calculates the thumb position based on the visible items.
+     *
+     * @param firstVisibleItem First visible item, >= 0.
+     * @param visibleItemCount Number of visible items, >= 0.
+     * @param totalItemCount Total number of items, >= 0.
+     * @return
+     */
+    private float getPosFromItemCount(
+            int firstVisibleItem, int visibleItemCount, int totalItemCount) {
+        final SectionIndexer sectionIndexer = mSectionIndexer;
+        if (sectionIndexer == null || mListAdapter == null) {
+            getSectionsFromIndexer();
+        }
+
+        if (visibleItemCount == 0 || totalItemCount == 0) {
+            // No items are visible.
+            return 0;
+        }
+
+        final boolean hasSections = sectionIndexer != null && mSections != null
+                && mSections.length > 0;
+        if (!hasSections || !mMatchDragPosition) {
+            if (visibleItemCount == totalItemCount) {
+                // All items are visible.
+                return 0;
+            } else {
+                return (float) firstVisibleItem / (totalItemCount - visibleItemCount);
+            }
+        }
+
+        // Ignore headers.
+        firstVisibleItem -= mHeaderCount;
+        if (firstVisibleItem < 0) {
+            return 0;
+        }
+        totalItemCount -= mHeaderCount;
+
+        // Hidden portion of the first visible row.
+        final View child = mList.getChildAt(0);
+        final float incrementalPos;
+        if (child == null || child.getHeight() == 0) {
+            incrementalPos = 0;
+        } else {
+            incrementalPos = (float) (mList.getPaddingTop() - child.getTop()) / child.getHeight();
+        }
+
+        // Number of rows in this section.
+        final int section = sectionIndexer.getSectionForPosition(firstVisibleItem);
+        final int sectionPos = sectionIndexer.getPositionForSection(section);
+        final int sectionCount = mSections.length;
+        final int positionsInSection;
+        if (section < sectionCount - 1) {
+            final int nextSectionPos;
+            if (section + 1 < sectionCount) {
+                nextSectionPos = sectionIndexer.getPositionForSection(section + 1);
+            } else {
+                nextSectionPos = totalItemCount - 1;
+            }
+            positionsInSection = nextSectionPos - sectionPos;
+        } else {
+            positionsInSection = totalItemCount - sectionPos;
+        }
+
+        // Position within this section.
+        final float posWithinSection;
+        if (positionsInSection == 0) {
+            posWithinSection = 0;
+        } else {
+            posWithinSection = (firstVisibleItem + incrementalPos - sectionPos)
+                    / positionsInSection;
+        }
+
+        float result = (section + posWithinSection) / sectionCount;
+
+        // Fake out the scroll bar for the last item. Since the section indexer
+        // won't ever actually move the list in this end space, make scrolling
+        // across the last item account for whatever space is remaining.
+        if (firstVisibleItem > 0 && firstVisibleItem + visibleItemCount == totalItemCount) {
+            final View lastChild = mList.getChildAt(visibleItemCount - 1);
+            final int bottomPadding = mList.getPaddingBottom();
+            final int maxSize;
+            final int currentVisibleSize;
+            if (mList.getClipToPadding()) {
+                maxSize = lastChild.getHeight();
+                currentVisibleSize = mList.getHeight() - bottomPadding - lastChild.getTop();
+            } else {
+                maxSize = lastChild.getHeight() + bottomPadding;
+                currentVisibleSize = mList.getHeight() - lastChild.getTop();
+            }
+            if (currentVisibleSize > 0 && maxSize > 0) {
+                result += (1 - result) * ((float) currentVisibleSize / maxSize );
+            }
+        }
+
+        return result;
+    }
+
+    /**
+     * Cancels an ongoing fling event by injecting a
+     * {@link MotionEvent#ACTION_CANCEL} into the host view.
+     */
+    private void cancelFling() {
+        final MotionEvent cancelFling = MotionEvent.obtain(
+                0, 0, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        mList.onTouchEvent(cancelFling);
+        cancelFling.recycle();
+    }
+
+    /**
+     * Cancels a pending drag.
+     *
+     * @see #startPendingDrag()
+     */
+    private void cancelPendingDrag() {
+        mPendingDrag = -1;
+    }
+
+    /**
+     * Delays dragging until after the framework has determined that the user is
+     * scrolling, rather than tapping.
+     */
+    private void startPendingDrag() {
+        mPendingDrag = SystemClock.uptimeMillis() + TAP_TIMEOUT;
+    }
+
+    private void beginDrag() {
+        mPendingDrag = -1;
+
+        setState(STATE_DRAGGING);
+
+        if (mListAdapter == null && mList != null) {
+            getSectionsFromIndexer();
+        }
+
+        if (mList != null) {
+            mList.requestDisallowInterceptTouchEvent(true);
+            mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+        }
+
+        cancelFling();
+    }
+
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        if (!isEnabled()) {
+            return false;
+        }
+
+        switch (ev.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN:
+                if (isPointInside(ev.getX(), ev.getY())) {
+                    // If the parent has requested that its children delay
+                    // pressed state (e.g. is a scrolling container) then we
+                    // need to allow the parent time to decide whether it wants
+                    // to intercept events. If it does, we will receive a CANCEL
+                    // event.
+                    if (!mList.isInScrollingContainer()) {
+                        // This will get dispatched to onTouchEvent(). Start
+                        // dragging there.
+                        return true;
+                    }
+
+                    mInitialTouchY = ev.getY();
+                    startPendingDrag();
+                }
+                break;
+            case MotionEvent.ACTION_MOVE:
+                if (!isPointInside(ev.getX(), ev.getY())) {
+                    cancelPendingDrag();
+                } else if (mPendingDrag >= 0 && mPendingDrag <= SystemClock.uptimeMillis()) {
+                    beginDrag();
+
+                    final float pos = getPosFromMotionEvent(mInitialTouchY);
+                    scrollTo(pos);
+
+                    // This may get dispatched to onTouchEvent(), but it
+                    // doesn't really matter since we'll already be in a drag.
+                    return onTouchEvent(ev);
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                cancelPendingDrag();
+                break;
+        }
+
+        return false;
+    }
+
+    public boolean onInterceptHoverEvent(MotionEvent ev) {
+        if (!isEnabled()) {
+            return false;
+        }
+
+        final int actionMasked = ev.getActionMasked();
+        if ((actionMasked == MotionEvent.ACTION_HOVER_ENTER
+                || actionMasked == MotionEvent.ACTION_HOVER_MOVE) && mState == STATE_NONE
+                && isPointInside(ev.getX(), ev.getY())) {
+            setState(STATE_VISIBLE);
+            postAutoHide();
+        }
+
+        return false;
+    }
+
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (mState == STATE_DRAGGING || isPointInside(event.getX(), event.getY())) {
+            return PointerIcon.getSystemIcon(mList.getContext(), PointerIcon.TYPE_ARROW);
+        }
+        return null;
+    }
+
+    public boolean onTouchEvent(MotionEvent me) {
+        if (!isEnabled()) {
+            return false;
+        }
+
+        switch (me.getActionMasked()) {
+            case MotionEvent.ACTION_DOWN: {
+                if (isPointInside(me.getX(), me.getY())) {
+                    if (!mList.isInScrollingContainer()) {
+                        beginDrag();
+                        return true;
+                    }
+                }
+            } break;
+
+            case MotionEvent.ACTION_UP: {
+                if (mPendingDrag >= 0) {
+                    // Allow a tap to scroll.
+                    beginDrag();
+
+                    final float pos = getPosFromMotionEvent(me.getY());
+                    setThumbPos(pos);
+                    scrollTo(pos);
+
+                    // Will hit the STATE_DRAGGING check below
+                }
+
+                if (mState == STATE_DRAGGING) {
+                    if (mList != null) {
+                        // ViewGroup does the right thing already, but there might
+                        // be other classes that don't properly reset on touch-up,
+                        // so do this explicitly just in case.
+                        mList.requestDisallowInterceptTouchEvent(false);
+                        mList.reportScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                    }
+
+                    setState(STATE_VISIBLE);
+                    postAutoHide();
+
+                    return true;
+                }
+            } break;
+
+            case MotionEvent.ACTION_MOVE: {
+                if (mPendingDrag >= 0 && Math.abs(me.getY() - mInitialTouchY) > mScaledTouchSlop) {
+                    beginDrag();
+
+                    // Will hit the STATE_DRAGGING check below
+                }
+
+                if (mState == STATE_DRAGGING) {
+                    // TODO: Ignore jitter.
+                    final float pos = getPosFromMotionEvent(me.getY());
+                    setThumbPos(pos);
+
+                    // If the previous scrollTo is still pending
+                    if (mScrollCompleted) {
+                        scrollTo(pos);
+                    }
+
+                    return true;
+                }
+            } break;
+
+            case MotionEvent.ACTION_CANCEL: {
+                cancelPendingDrag();
+            } break;
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns whether a coordinate is inside the scroller's activation area. If
+     * there is a track image, touching anywhere within the thumb-width of the
+     * track activates scrolling. Otherwise, the user has to touch inside thumb
+     * itself.
+     *
+     * @param x The x-coordinate.
+     * @param y The y-coordinate.
+     * @return Whether the coordinate is inside the scroller's activation area.
+     */
+    private boolean isPointInside(float x, float y) {
+        return isPointInsideX(x) && (mTrackDrawable != null || isPointInsideY(y));
+    }
+
+    private boolean isPointInsideX(float x) {
+        final float offset = mThumbImage.getTranslationX();
+        final float left = mThumbImage.getLeft() + offset;
+        final float right = mThumbImage.getRight() + offset;
+
+        // Apply the minimum touch target size.
+        final float targetSizeDiff = mMinimumTouchTarget - (right - left);
+        final float adjust = targetSizeDiff > 0 ? targetSizeDiff : 0;
+
+        if (mLayoutFromRight) {
+            return x >= mThumbImage.getLeft() - adjust;
+        } else {
+            return x <= mThumbImage.getRight() + adjust;
+        }
+    }
+
+    private boolean isPointInsideY(float y) {
+        final float offset = mThumbImage.getTranslationY();
+        final float top = mThumbImage.getTop() + offset;
+        final float bottom = mThumbImage.getBottom() + offset;
+
+        // Apply the minimum touch target size.
+        final float targetSizeDiff = mMinimumTouchTarget - (bottom - top);
+        final float adjust = targetSizeDiff > 0 ? targetSizeDiff / 2 : 0;
+
+        return y >= (top - adjust) && y <= (bottom + adjust);
+    }
+
+    /**
+     * Constructs an animator for the specified property on a group of views.
+     * See {@link ObjectAnimator#ofFloat(Object, String, float...)} for
+     * implementation details.
+     *
+     * @param property The property being animated.
+     * @param value The value to which that property should animate.
+     * @param views The target views to animate.
+     * @return An animator for all the specified views.
+     */
+    private static Animator groupAnimatorOfFloat(
+            Property<View, Float> property, float value, View... views) {
+        AnimatorSet animSet = new AnimatorSet();
+        AnimatorSet.Builder builder = null;
+
+        for (int i = views.length - 1; i >= 0; i--) {
+            final Animator anim = ObjectAnimator.ofFloat(views[i], property, value);
+            if (builder == null) {
+                builder = animSet.play(anim);
+            } else {
+                builder.with(anim);
+            }
+        }
+
+        return animSet;
+    }
+
+    /**
+     * Returns an animator for the view's scaleX value.
+     */
+    private static Animator animateScaleX(View v, float target) {
+        return ObjectAnimator.ofFloat(v, View.SCALE_X, target);
+    }
+
+    /**
+     * Returns an animator for the view's alpha value.
+     */
+    private static Animator animateAlpha(View v, float alpha) {
+        return ObjectAnimator.ofFloat(v, View.ALPHA, alpha);
+    }
+
+    /**
+     * A Property wrapper around the <code>left</code> functionality handled by the
+     * {@link View#setLeft(int)} and {@link View#getLeft()} methods.
+     */
+    private static Property<View, Integer> LEFT = new IntProperty<View>("left") {
+        @Override
+        public void setValue(View object, int value) {
+            object.setLeft(value);
+        }
+
+        @Override
+        public Integer get(View object) {
+            return object.getLeft();
+        }
+    };
+
+    /**
+     * A Property wrapper around the <code>top</code> functionality handled by the
+     * {@link View#setTop(int)} and {@link View#getTop()} methods.
+     */
+    private static Property<View, Integer> TOP = new IntProperty<View>("top") {
+        @Override
+        public void setValue(View object, int value) {
+            object.setTop(value);
+        }
+
+        @Override
+        public Integer get(View object) {
+            return object.getTop();
+        }
+    };
+
+    /**
+     * A Property wrapper around the <code>right</code> functionality handled by the
+     * {@link View#setRight(int)} and {@link View#getRight()} methods.
+     */
+    private static Property<View, Integer> RIGHT = new IntProperty<View>("right") {
+        @Override
+        public void setValue(View object, int value) {
+            object.setRight(value);
+        }
+
+        @Override
+        public Integer get(View object) {
+            return object.getRight();
+        }
+    };
+
+    /**
+     * A Property wrapper around the <code>bottom</code> functionality handled by the
+     * {@link View#setBottom(int)} and {@link View#getBottom()} methods.
+     */
+    private static Property<View, Integer> BOTTOM = new IntProperty<View>("bottom") {
+        @Override
+        public void setValue(View object, int value) {
+            object.setBottom(value);
+        }
+
+        @Override
+        public Integer get(View object) {
+            return object.getBottom();
+        }
+    };
+
+    /**
+     * Returns an animator for the view's bounds.
+     */
+    private static Animator animateBounds(View v, Rect bounds) {
+        final PropertyValuesHolder left = PropertyValuesHolder.ofInt(LEFT, bounds.left);
+        final PropertyValuesHolder top = PropertyValuesHolder.ofInt(TOP, bounds.top);
+        final PropertyValuesHolder right = PropertyValuesHolder.ofInt(RIGHT, bounds.right);
+        final PropertyValuesHolder bottom = PropertyValuesHolder.ofInt(BOTTOM, bounds.bottom);
+        return ObjectAnimator.ofPropertyValuesHolder(v, left, top, right, bottom);
+    }
+}
diff --git a/android/widget/Filter.java b/android/widget/Filter.java
new file mode 100644
index 0000000..d901540
--- /dev/null
+++ b/android/widget/Filter.java
@@ -0,0 +1,323 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Looper;
+import android.os.Message;
+import android.util.Log;
+
+/**
+ * <p>A filter constrains data with a filtering pattern.</p>
+ *
+ * <p>Filters are usually created by {@link android.widget.Filterable}
+ * classes.</p>
+ *
+ * <p>Filtering operations performed by calling {@link #filter(CharSequence)} or
+ * {@link #filter(CharSequence, android.widget.Filter.FilterListener)} are
+ * performed asynchronously. When these methods are called, a filtering request
+ * is posted in a request queue and processed later. Any call to one of these
+ * methods will cancel any previous non-executed filtering request.</p>
+ *
+ * @see android.widget.Filterable
+ */
+public abstract class Filter {
+    private static final String LOG_TAG = "Filter";
+    
+    private static final String THREAD_NAME = "Filter";
+    private static final int FILTER_TOKEN = 0xD0D0F00D;
+    private static final int FINISH_TOKEN = 0xDEADBEEF;
+
+    private Handler mThreadHandler;
+    private Handler mResultHandler;
+
+    private Delayer mDelayer;
+
+    private final Object mLock = new Object();
+
+    /**
+     * <p>Creates a new asynchronous filter.</p>
+     */
+    public Filter() {
+        mResultHandler = new ResultsHandler();
+    }
+
+    /**
+     * Provide an interface that decides how long to delay the message for a given query.  Useful
+     * for heuristics such as posting a delay for the delete key to avoid doing any work while the
+     * user holds down the delete key.
+     *
+     * @param delayer The delayer.
+     * @hide
+     */
+    public void setDelayer(Delayer delayer) {
+        synchronized (mLock) {
+            mDelayer = delayer;
+        }
+    }
+
+    /**
+     * <p>Starts an asynchronous filtering operation. Calling this method
+     * cancels all previous non-executed filtering requests and posts a new
+     * filtering request that will be executed later.</p>
+     *
+     * @param constraint the constraint used to filter the data
+     *
+     * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+     */
+    public final void filter(CharSequence constraint) {
+        filter(constraint, null);
+    }
+
+    /**
+     * <p>Starts an asynchronous filtering operation. Calling this method
+     * cancels all previous non-executed filtering requests and posts a new
+     * filtering request that will be executed later.</p>
+     *
+     * <p>Upon completion, the listener is notified.</p>
+     *
+     * @param constraint the constraint used to filter the data
+     * @param listener a listener notified upon completion of the operation
+     *
+     * @see #filter(CharSequence)
+     * @see #performFiltering(CharSequence)
+     * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
+     */
+    public final void filter(CharSequence constraint, FilterListener listener) {
+        synchronized (mLock) {
+            if (mThreadHandler == null) {
+                HandlerThread thread = new HandlerThread(
+                        THREAD_NAME, android.os.Process.THREAD_PRIORITY_BACKGROUND);
+                thread.start();
+                mThreadHandler = new RequestHandler(thread.getLooper());
+            }
+
+            final long delay = (mDelayer == null) ? 0 : mDelayer.getPostingDelay(constraint);
+            
+            Message message = mThreadHandler.obtainMessage(FILTER_TOKEN);
+    
+            RequestArguments args = new RequestArguments();
+            // make sure we use an immutable copy of the constraint, so that
+            // it doesn't change while the filter operation is in progress
+            args.constraint = constraint != null ? constraint.toString() : null;
+            args.listener = listener;
+            message.obj = args;
+    
+            mThreadHandler.removeMessages(FILTER_TOKEN);
+            mThreadHandler.removeMessages(FINISH_TOKEN);
+            mThreadHandler.sendMessageDelayed(message, delay);
+        }
+    }
+
+    /**
+     * <p>Invoked in a worker thread to filter the data according to the
+     * constraint. Subclasses must implement this method to perform the
+     * filtering operation. Results computed by the filtering operation
+     * must be returned as a {@link android.widget.Filter.FilterResults} that
+     * will then be published in the UI thread through
+     * {@link #publishResults(CharSequence,
+     * android.widget.Filter.FilterResults)}.</p>
+     *
+     * <p><strong>Contract:</strong> When the constraint is null, the original
+     * data must be restored.</p>
+     *
+     * @param constraint the constraint used to filter the data
+     * @return the results of the filtering operation
+     *
+     * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+     * @see #publishResults(CharSequence, android.widget.Filter.FilterResults)
+     * @see android.widget.Filter.FilterResults
+     */
+    protected abstract FilterResults performFiltering(CharSequence constraint);
+
+    /**
+     * <p>Invoked in the UI thread to publish the filtering results in the
+     * user interface. Subclasses must implement this method to display the
+     * results computed in {@link #performFiltering}.</p>
+     *
+     * @param constraint the constraint used to filter the data
+     * @param results the results of the filtering operation
+     *
+     * @see #filter(CharSequence, android.widget.Filter.FilterListener)
+     * @see #performFiltering(CharSequence)
+     * @see android.widget.Filter.FilterResults
+     */
+    protected abstract void publishResults(CharSequence constraint,
+            FilterResults results);
+
+    /**
+     * <p>Converts a value from the filtered set into a CharSequence. Subclasses
+     * should override this method to convert their results. The default
+     * implementation returns an empty String for null values or the default
+     * String representation of the value.</p>
+     *
+     * @param resultValue the value to convert to a CharSequence
+     * @return a CharSequence representing the value
+     */
+    public CharSequence convertResultToString(Object resultValue) {
+        return resultValue == null ? "" : resultValue.toString();
+    }
+
+    /**
+     * <p>Holds the results of a filtering operation. The results are the values
+     * computed by the filtering operation and the number of these values.</p>
+     */
+    protected static class FilterResults {
+        public FilterResults() {
+            // nothing to see here
+        }
+
+        /**
+         * <p>Contains all the values computed by the filtering operation.</p>
+         */
+        public Object values;
+
+        /**
+         * <p>Contains the number of values computed by the filtering
+         * operation.</p>
+         */
+        public int count;
+    }
+
+    /**
+     * <p>Listener used to receive a notification upon completion of a filtering
+     * operation.</p>
+     */
+    public static interface FilterListener {
+        /**
+         * <p>Notifies the end of a filtering operation.</p>
+         *
+         * @param count the number of values computed by the filter
+         */
+        public void onFilterComplete(int count);
+    }
+
+    /**
+     * <p>Worker thread handler. When a new filtering request is posted from
+     * {@link android.widget.Filter#filter(CharSequence, android.widget.Filter.FilterListener)},
+     * it is sent to this handler.</p>
+     */
+    private class RequestHandler extends Handler {
+        public RequestHandler(Looper looper) {
+            super(looper);
+        }
+        
+        /**
+         * <p>Handles filtering requests by calling
+         * {@link Filter#performFiltering} and then sending a message
+         * with the results to the results handler.</p>
+         *
+         * @param msg the filtering request
+         */
+        public void handleMessage(Message msg) {
+            int what = msg.what;
+            Message message;
+            switch (what) {
+                case FILTER_TOKEN:
+                    RequestArguments args = (RequestArguments) msg.obj;
+                    try {
+                        args.results = performFiltering(args.constraint);
+                    } catch (Exception e) {
+                        args.results = new FilterResults();
+                        Log.w(LOG_TAG, "An exception occured during performFiltering()!", e);
+                    } finally {
+                        message = mResultHandler.obtainMessage(what);
+                        message.obj = args;
+                        message.sendToTarget();
+                    }
+
+                    synchronized (mLock) {
+                        if (mThreadHandler != null) {
+                            Message finishMessage = mThreadHandler.obtainMessage(FINISH_TOKEN);
+                            mThreadHandler.sendMessageDelayed(finishMessage, 3000);
+                        }
+                    }
+                    break;
+                case FINISH_TOKEN:
+                    synchronized (mLock) {
+                        if (mThreadHandler != null) {
+                            mThreadHandler.getLooper().quit();
+                            mThreadHandler = null;
+                        }
+                    }
+                    break;
+            }
+        }
+    }
+
+    /**
+     * <p>Handles the results of a filtering operation. The results are
+     * handled in the UI thread.</p>
+     */
+    private class ResultsHandler extends Handler {
+        /**
+         * <p>Messages received from the request handler are processed in the
+         * UI thread. The processing involves calling
+         * {@link Filter#publishResults(CharSequence,
+         * android.widget.Filter.FilterResults)}
+         * to post the results back in the UI and then notifying the listener,
+         * if any.</p> 
+         *
+         * @param msg the filtering results
+         */
+        @Override
+        public void handleMessage(Message msg) {
+            RequestArguments args = (RequestArguments) msg.obj;
+
+            publishResults(args.constraint, args.results);
+            if (args.listener != null) {
+                int count = args.results != null ? args.results.count : -1;
+                args.listener.onFilterComplete(count);
+            }
+        }
+    }
+
+    /**
+     * <p>Holds the arguments of a filtering request as well as the results
+     * of the request.</p>
+     */
+    private static class RequestArguments {
+        /**
+         * <p>The constraint used to filter the data.</p>
+         */
+        CharSequence constraint;
+
+        /**
+         * <p>The listener to notify upon completion. Can be null.</p>
+         */
+        FilterListener listener;
+
+        /**
+         * <p>The results of the filtering operation.</p>
+         */
+        FilterResults results;
+    }
+
+    /**
+     * @hide
+     */
+    public interface Delayer {
+
+        /**
+         * @param constraint The constraint passed to {@link Filter#filter(CharSequence)}
+         * @return The delay that should be used for
+         *         {@link Handler#sendMessageDelayed(android.os.Message, long)}
+         */
+        long getPostingDelay(CharSequence constraint);
+    }
+}
diff --git a/android/widget/FilterQueryProvider.java b/android/widget/FilterQueryProvider.java
new file mode 100644
index 0000000..740d2f0
--- /dev/null
+++ b/android/widget/FilterQueryProvider.java
@@ -0,0 +1,42 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.database.Cursor;
+
+/**
+ * This class can be used by external clients of CursorAdapter and
+ * CursorTreeAdapter to define how the content of the adapter should be
+ * filtered.
+ * 
+ * @see #runQuery(CharSequence)
+ */
+public interface FilterQueryProvider {
+    /**
+     * Runs a query with the specified constraint. This query is requested
+     * by the filter attached to this adapter.
+     *
+     * Contract: when constraint is null or empty, the original results,
+     * prior to any filtering, must be returned.
+     *
+     * @param constraint the constraint with which the query must
+     *        be filtered
+     *
+     * @return a Cursor representing the results of the new query
+     */
+    Cursor runQuery(CharSequence constraint);
+}
diff --git a/android/widget/Filterable.java b/android/widget/Filterable.java
new file mode 100644
index 0000000..f7c8d59
--- /dev/null
+++ b/android/widget/Filterable.java
@@ -0,0 +1,37 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+/**
+ * <p>Defines a filterable behavior. A filterable class can have its data
+ * constrained by a filter. Filterable classes are usually
+ * {@link android.widget.Adapter} implementations.</p>
+ *
+ * @see android.widget.Filter
+ */
+public interface Filterable {
+    /**
+     * <p>Returns a filter that can be used to constrain data with a filtering
+     * pattern.</p>
+     *
+     * <p>This method is usually implemented by {@link android.widget.Adapter}
+     * classes.</p>
+     *
+     * @return a filter used to constrain data
+     */
+    Filter getFilter();
+}
diff --git a/android/widget/ForwardingListener.java b/android/widget/ForwardingListener.java
new file mode 100644
index 0000000..8b82c06
--- /dev/null
+++ b/android/widget/ForwardingListener.java
@@ -0,0 +1,294 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.os.SystemClock;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewParent;
+
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * Abstract class that forwards touch events to a {@link ShowableListMenu}.
+ *
+ * @hide
+ */
+public abstract class ForwardingListener
+        implements View.OnTouchListener, View.OnAttachStateChangeListener {
+
+    /** Scaled touch slop, used for detecting movement outside bounds. */
+    private final float mScaledTouchSlop;
+
+    /** Timeout before disallowing intercept on the source's parent. */
+    private final int mTapTimeout;
+
+    /** Timeout before accepting a long-press to start forwarding. */
+    private final int mLongPressTimeout;
+
+    /** Source view from which events are forwarded. */
+    private final View mSrc;
+
+    /** Runnable used to prevent conflicts with scrolling parents. */
+    private Runnable mDisallowIntercept;
+
+    /** Runnable used to trigger forwarding on long-press. */
+    private Runnable mTriggerLongPress;
+
+    /** Whether this listener is currently forwarding touch events. */
+    private boolean mForwarding;
+
+    /** The id of the first pointer down in the current event stream. */
+    private int mActivePointerId;
+
+    public ForwardingListener(View src) {
+        mSrc = src;
+        src.setLongClickable(true);
+        src.addOnAttachStateChangeListener(this);
+
+        mScaledTouchSlop = ViewConfiguration.get(src.getContext()).getScaledTouchSlop();
+        mTapTimeout = ViewConfiguration.getTapTimeout();
+
+        // Use a medium-press timeout. Halfway between tap and long-press.
+        mLongPressTimeout = (mTapTimeout + ViewConfiguration.getLongPressTimeout()) / 2;
+    }
+
+    /**
+     * Returns the popup to which this listener is forwarding events.
+     * <p>
+     * Override this to return the correct popup. If the popup is displayed
+     * asynchronously, you may also need to override
+     * {@link #onForwardingStopped} to prevent premature cancellation of
+     * forwarding.
+     *
+     * @return the popup to which this listener is forwarding events
+     */
+    public abstract ShowableListMenu getPopup();
+
+    @Override
+    public boolean onTouch(View v, MotionEvent event) {
+        final boolean wasForwarding = mForwarding;
+        final boolean forwarding;
+        if (wasForwarding) {
+            forwarding = onTouchForwarded(event) || !onForwardingStopped();
+        } else {
+            forwarding = onTouchObserved(event) && onForwardingStarted();
+
+            if (forwarding) {
+                // Make sure we cancel any ongoing source event stream.
+                final long now = SystemClock.uptimeMillis();
+                final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL,
+                        0.0f, 0.0f, 0);
+                mSrc.onTouchEvent(e);
+                e.recycle();
+            }
+        }
+
+        mForwarding = forwarding;
+        return forwarding || wasForwarding;
+    }
+
+    @Override
+    public void onViewAttachedToWindow(View v) {
+    }
+
+    @Override
+    public void onViewDetachedFromWindow(View v) {
+        mForwarding = false;
+        mActivePointerId = MotionEvent.INVALID_POINTER_ID;
+
+        if (mDisallowIntercept != null) {
+            mSrc.removeCallbacks(mDisallowIntercept);
+        }
+    }
+
+    /**
+     * Called when forwarding would like to start.
+     * <p>
+     * By default, this will show the popup returned by {@link #getPopup()}.
+     * It may be overridden to perform another action, like clicking the
+     * source view or preparing the popup before showing it.
+     *
+     * @return true to start forwarding, false otherwise
+     */
+    protected boolean onForwardingStarted() {
+        final ShowableListMenu popup = getPopup();
+        if (popup != null && !popup.isShowing()) {
+            popup.show();
+        }
+        return true;
+    }
+
+    /**
+     * Called when forwarding would like to stop.
+     * <p>
+     * By default, this will dismiss the popup returned by
+     * {@link #getPopup()}. It may be overridden to perform some other
+     * action.
+     *
+     * @return true to stop forwarding, false otherwise
+     */
+    protected boolean onForwardingStopped() {
+        final ShowableListMenu popup = getPopup();
+        if (popup != null && popup.isShowing()) {
+            popup.dismiss();
+        }
+        return true;
+    }
+
+    /**
+     * Observes motion events and determines when to start forwarding.
+     *
+     * @param srcEvent motion event in source view coordinates
+     * @return true to start forwarding motion events, false otherwise
+     */
+    private boolean onTouchObserved(MotionEvent srcEvent) {
+        final View src = mSrc;
+        if (!src.isEnabled()) {
+            return false;
+        }
+
+        final int actionMasked = srcEvent.getActionMasked();
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN:
+                mActivePointerId = srcEvent.getPointerId(0);
+
+                if (mDisallowIntercept == null) {
+                    mDisallowIntercept = new DisallowIntercept();
+                }
+                src.postDelayed(mDisallowIntercept, mTapTimeout);
+
+                if (mTriggerLongPress == null) {
+                    mTriggerLongPress = new TriggerLongPress();
+                }
+                src.postDelayed(mTriggerLongPress, mLongPressTimeout);
+                break;
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = srcEvent.findPointerIndex(mActivePointerId);
+                if (activePointerIndex >= 0) {
+                    final float x = srcEvent.getX(activePointerIndex);
+                    final float y = srcEvent.getY(activePointerIndex);
+
+                    // Has the pointer moved outside of the view?
+                    if (!src.pointInView(x, y, mScaledTouchSlop)) {
+                        clearCallbacks();
+
+                        // Don't let the parent intercept our events.
+                        src.getParent().requestDisallowInterceptTouchEvent(true);
+                        return true;
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                clearCallbacks();
+                break;
+        }
+
+        return false;
+    }
+
+    private void clearCallbacks() {
+        if (mTriggerLongPress != null) {
+            mSrc.removeCallbacks(mTriggerLongPress);
+        }
+
+        if (mDisallowIntercept != null) {
+            mSrc.removeCallbacks(mDisallowIntercept);
+        }
+    }
+
+    private void onLongPress() {
+        clearCallbacks();
+
+        final View src = mSrc;
+        if (!src.isEnabled() || src.isLongClickable()) {
+            // Ignore long-press if the view is disabled or has its own
+            // handler.
+            return;
+        }
+
+        if (!onForwardingStarted()) {
+            return;
+        }
+
+        // Don't let the parent intercept our events.
+        src.getParent().requestDisallowInterceptTouchEvent(true);
+
+        // Make sure we cancel any ongoing source event stream.
+        final long now = SystemClock.uptimeMillis();
+        final MotionEvent e = MotionEvent.obtain(now, now, MotionEvent.ACTION_CANCEL, 0, 0, 0);
+        src.onTouchEvent(e);
+        e.recycle();
+
+        mForwarding = true;
+    }
+
+    /**
+     * Handles forwarded motion events and determines when to stop
+     * forwarding.
+     *
+     * @param srcEvent motion event in source view coordinates
+     * @return true to continue forwarding motion events, false to cancel
+     */
+    private boolean onTouchForwarded(MotionEvent srcEvent) {
+        final View src = mSrc;
+        final ShowableListMenu popup = getPopup();
+        if (popup == null || !popup.isShowing()) {
+            return false;
+        }
+
+        final DropDownListView dst = (DropDownListView) popup.getListView();
+        if (dst == null || !dst.isShown()) {
+            return false;
+        }
+
+        // Convert event to destination-local coordinates.
+        final MotionEvent dstEvent = MotionEvent.obtainNoHistory(srcEvent);
+        src.toGlobalMotionEvent(dstEvent);
+        dst.toLocalMotionEvent(dstEvent);
+
+        // Forward converted event to destination view, then recycle it.
+        final boolean handled = dst.onForwardedEvent(dstEvent, mActivePointerId);
+        dstEvent.recycle();
+
+        // Always cancel forwarding when the touch stream ends.
+        final int action = srcEvent.getActionMasked();
+        final boolean keepForwarding = action != MotionEvent.ACTION_UP
+                && action != MotionEvent.ACTION_CANCEL;
+
+        return handled && keepForwarding;
+    }
+
+    private class DisallowIntercept implements Runnable {
+        @Override
+        public void run() {
+            final ViewParent parent = mSrc.getParent();
+            if (parent != null) {
+                parent.requestDisallowInterceptTouchEvent(true);
+            }
+        }
+    }
+
+    private class TriggerLongPress implements Runnable {
+        @Override
+        public void run() {
+            onLongPress();
+        }
+    }
+}
diff --git a/android/widget/FrameLayout.java b/android/widget/FrameLayout.java
new file mode 100644
index 0000000..dc8ee01
--- /dev/null
+++ b/android/widget/FrameLayout.java
@@ -0,0 +1,487 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * FrameLayout is designed to block out an area on the screen to display
+ * a single item. Generally, FrameLayout should be used to hold a single child view, because it can
+ * be difficult to organize child views in a way that's scalable to different screen sizes without
+ * the children overlapping each other. You can, however, add multiple children to a FrameLayout
+ * and control their position within the FrameLayout by assigning gravity to each child, using the
+ * <a href="FrameLayout.LayoutParams.html#attr_android:layout_gravity">{@code
+ * android:layout_gravity}</a> attribute.
+ * <p>Child views are drawn in a stack, with the most recently added child on top.
+ * The size of the FrameLayout is the size of its largest child (plus padding), visible
+ * or not (if the FrameLayout's parent permits). Views that are {@link android.view.View#GONE} are
+ * used for sizing
+ * only if {@link #setMeasureAllChildren(boolean) setConsiderGoneChildrenWhenMeasuring()}
+ * is set to true.
+ *
+ * @attr ref android.R.styleable#FrameLayout_measureAllChildren
+ */
+@RemoteView
+public class FrameLayout extends ViewGroup {
+    private static final int DEFAULT_CHILD_GRAVITY = Gravity.TOP | Gravity.START;
+
+    @ViewDebug.ExportedProperty(category = "measurement")
+    boolean mMeasureAllChildren = false;
+
+    @ViewDebug.ExportedProperty(category = "padding")
+    private int mForegroundPaddingLeft = 0;
+
+    @ViewDebug.ExportedProperty(category = "padding")
+    private int mForegroundPaddingTop = 0;
+
+    @ViewDebug.ExportedProperty(category = "padding")
+    private int mForegroundPaddingRight = 0;
+
+    @ViewDebug.ExportedProperty(category = "padding")
+    private int mForegroundPaddingBottom = 0;
+
+    private final ArrayList<View> mMatchParentChildren = new ArrayList<>(1);
+
+    public FrameLayout(@NonNull Context context) {
+        super(context);
+    }
+
+    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public FrameLayout(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.FrameLayout, defStyleAttr, defStyleRes);
+
+        if (a.getBoolean(R.styleable.FrameLayout_measureAllChildren, false)) {
+            setMeasureAllChildren(true);
+        }
+
+        a.recycle();
+    }
+
+    /**
+     * Describes how the foreground is positioned. Defaults to START and TOP.
+     *
+     * @param foregroundGravity See {@link android.view.Gravity}
+     *
+     * @see #getForegroundGravity()
+     *
+     * @attr ref android.R.styleable#View_foregroundGravity
+     */
+    @android.view.RemotableViewMethod
+    public void setForegroundGravity(int foregroundGravity) {
+        if (getForegroundGravity() != foregroundGravity) {
+            super.setForegroundGravity(foregroundGravity);
+
+            // calling get* again here because the set above may apply default constraints
+            final Drawable foreground = getForeground();
+            if (getForegroundGravity() == Gravity.FILL && foreground != null) {
+                Rect padding = new Rect();
+                if (foreground.getPadding(padding)) {
+                    mForegroundPaddingLeft = padding.left;
+                    mForegroundPaddingTop = padding.top;
+                    mForegroundPaddingRight = padding.right;
+                    mForegroundPaddingBottom = padding.bottom;
+                }
+            } else {
+                mForegroundPaddingLeft = 0;
+                mForegroundPaddingTop = 0;
+                mForegroundPaddingRight = 0;
+                mForegroundPaddingBottom = 0;
+            }
+
+            requestLayout();
+        }
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
+     * and a height of {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}.
+     */
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT);
+    }
+
+    int getPaddingLeftWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingLeft, mForegroundPaddingLeft) :
+            mPaddingLeft + mForegroundPaddingLeft;
+    }
+
+    int getPaddingRightWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingRight, mForegroundPaddingRight) :
+            mPaddingRight + mForegroundPaddingRight;
+    }
+
+    private int getPaddingTopWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingTop, mForegroundPaddingTop) :
+            mPaddingTop + mForegroundPaddingTop;
+    }
+
+    private int getPaddingBottomWithForeground() {
+        return isForegroundInsidePadding() ? Math.max(mPaddingBottom, mForegroundPaddingBottom) :
+            mPaddingBottom + mForegroundPaddingBottom;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int count = getChildCount();
+
+        final boolean measureMatchParentChildren =
+                MeasureSpec.getMode(widthMeasureSpec) != MeasureSpec.EXACTLY ||
+                MeasureSpec.getMode(heightMeasureSpec) != MeasureSpec.EXACTLY;
+        mMatchParentChildren.clear();
+
+        int maxHeight = 0;
+        int maxWidth = 0;
+        int childState = 0;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (mMeasureAllChildren || child.getVisibility() != GONE) {
+                measureChildWithMargins(child, widthMeasureSpec, 0, heightMeasureSpec, 0);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                maxWidth = Math.max(maxWidth,
+                        child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin);
+                maxHeight = Math.max(maxHeight,
+                        child.getMeasuredHeight() + lp.topMargin + lp.bottomMargin);
+                childState = combineMeasuredStates(childState, child.getMeasuredState());
+                if (measureMatchParentChildren) {
+                    if (lp.width == LayoutParams.MATCH_PARENT ||
+                            lp.height == LayoutParams.MATCH_PARENT) {
+                        mMatchParentChildren.add(child);
+                    }
+                }
+            }
+        }
+
+        // Account for padding too
+        maxWidth += getPaddingLeftWithForeground() + getPaddingRightWithForeground();
+        maxHeight += getPaddingTopWithForeground() + getPaddingBottomWithForeground();
+
+        // Check against our minimum height and width
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        // Check against our foreground's minimum height and width
+        final Drawable drawable = getForeground();
+        if (drawable != null) {
+            maxHeight = Math.max(maxHeight, drawable.getMinimumHeight());
+            maxWidth = Math.max(maxWidth, drawable.getMinimumWidth());
+        }
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        childState << MEASURED_HEIGHT_STATE_SHIFT));
+
+        count = mMatchParentChildren.size();
+        if (count > 1) {
+            for (int i = 0; i < count; i++) {
+                final View child = mMatchParentChildren.get(i);
+                final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+                final int childWidthMeasureSpec;
+                if (lp.width == LayoutParams.MATCH_PARENT) {
+                    final int width = Math.max(0, getMeasuredWidth()
+                            - getPaddingLeftWithForeground() - getPaddingRightWithForeground()
+                            - lp.leftMargin - lp.rightMargin);
+                    childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            width, MeasureSpec.EXACTLY);
+                } else {
+                    childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                            getPaddingLeftWithForeground() + getPaddingRightWithForeground() +
+                            lp.leftMargin + lp.rightMargin,
+                            lp.width);
+                }
+
+                final int childHeightMeasureSpec;
+                if (lp.height == LayoutParams.MATCH_PARENT) {
+                    final int height = Math.max(0, getMeasuredHeight()
+                            - getPaddingTopWithForeground() - getPaddingBottomWithForeground()
+                            - lp.topMargin - lp.bottomMargin);
+                    childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            height, MeasureSpec.EXACTLY);
+                } else {
+                    childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                            getPaddingTopWithForeground() + getPaddingBottomWithForeground() +
+                            lp.topMargin + lp.bottomMargin,
+                            lp.height);
+                }
+
+                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            }
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        layoutChildren(left, top, right, bottom, false /* no force left gravity */);
+    }
+
+    void layoutChildren(int left, int top, int right, int bottom, boolean forceLeftGravity) {
+        final int count = getChildCount();
+
+        final int parentLeft = getPaddingLeftWithForeground();
+        final int parentRight = right - left - getPaddingRightWithForeground();
+
+        final int parentTop = getPaddingTopWithForeground();
+        final int parentBottom = bottom - top - getPaddingBottomWithForeground();
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+                final int width = child.getMeasuredWidth();
+                final int height = child.getMeasuredHeight();
+
+                int childLeft;
+                int childTop;
+
+                int gravity = lp.gravity;
+                if (gravity == -1) {
+                    gravity = DEFAULT_CHILD_GRAVITY;
+                }
+
+                final int layoutDirection = getLayoutDirection();
+                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+                final int verticalGravity = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                    case Gravity.CENTER_HORIZONTAL:
+                        childLeft = parentLeft + (parentRight - parentLeft - width) / 2 +
+                        lp.leftMargin - lp.rightMargin;
+                        break;
+                    case Gravity.RIGHT:
+                        if (!forceLeftGravity) {
+                            childLeft = parentRight - width - lp.rightMargin;
+                            break;
+                        }
+                    case Gravity.LEFT:
+                    default:
+                        childLeft = parentLeft + lp.leftMargin;
+                }
+
+                switch (verticalGravity) {
+                    case Gravity.TOP:
+                        childTop = parentTop + lp.topMargin;
+                        break;
+                    case Gravity.CENTER_VERTICAL:
+                        childTop = parentTop + (parentBottom - parentTop - height) / 2 +
+                        lp.topMargin - lp.bottomMargin;
+                        break;
+                    case Gravity.BOTTOM:
+                        childTop = parentBottom - height - lp.bottomMargin;
+                        break;
+                    default:
+                        childTop = parentTop + lp.topMargin;
+                }
+
+                child.layout(childLeft, childTop, childLeft + width, childTop + height);
+            }
+        }
+    }
+
+    /**
+     * Sets whether to consider all children, or just those in
+     * the VISIBLE or INVISIBLE state, when measuring. Defaults to false.
+     *
+     * @param measureAll true to consider children marked GONE, false otherwise.
+     * Default value is false.
+     *
+     * @attr ref android.R.styleable#FrameLayout_measureAllChildren
+     */
+    @android.view.RemotableViewMethod
+    public void setMeasureAllChildren(boolean measureAll) {
+        mMeasureAllChildren = measureAll;
+    }
+
+    /**
+     * Determines whether all children, or just those in the VISIBLE or
+     * INVISIBLE state, are considered when measuring.
+     *
+     * @return Whether all children are considered when measuring.
+     *
+     * @deprecated This method is deprecated in favor of
+     * {@link #getMeasureAllChildren() getMeasureAllChildren()}, which was
+     * renamed for consistency with
+     * {@link #setMeasureAllChildren(boolean) setMeasureAllChildren()}.
+     */
+    @Deprecated
+    public boolean getConsiderGoneChildrenWhenMeasuring() {
+        return getMeasureAllChildren();
+    }
+
+    /**
+     * Determines whether all children, or just those in the VISIBLE or
+     * INVISIBLE state, are considered when measuring.
+     *
+     * @return Whether all children are considered when measuring.
+     */
+    public boolean getMeasureAllChildren() {
+        return mMeasureAllChildren;
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new FrameLayout.LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (sPreserveMarginParamsInLayoutParamConversion) {
+            if (lp instanceof LayoutParams) {
+                return new LayoutParams((LayoutParams) lp);
+            } else if (lp instanceof MarginLayoutParams) {
+                return new LayoutParams((MarginLayoutParams) lp);
+            }
+        }
+        return new LayoutParams(lp);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return FrameLayout.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+
+        encoder.addProperty("measurement:measureAllChildren", mMeasureAllChildren);
+        encoder.addProperty("padding:foregroundPaddingLeft", mForegroundPaddingLeft);
+        encoder.addProperty("padding:foregroundPaddingTop", mForegroundPaddingTop);
+        encoder.addProperty("padding:foregroundPaddingRight", mForegroundPaddingRight);
+        encoder.addProperty("padding:foregroundPaddingBottom", mForegroundPaddingBottom);
+    }
+
+    /**
+     * Per-child layout information for layouts that support margins.
+     * See {@link android.R.styleable#FrameLayout_Layout FrameLayout Layout Attributes}
+     * for a list of all child view attributes that this class supports.
+     *
+     * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
+     */
+    public static class LayoutParams extends MarginLayoutParams {
+        /**
+         * Value for {@link #gravity} indicating that a gravity has not been
+         * explicitly specified.
+         */
+        public static final int UNSPECIFIED_GRAVITY = -1;
+
+        /**
+         * The gravity to apply with the View to which these layout parameters
+         * are associated.
+         * <p>
+         * The default value is {@link #UNSPECIFIED_GRAVITY}, which is treated
+         * by FrameLayout as {@code Gravity.TOP | Gravity.START}.
+         *
+         * @see android.view.Gravity
+         * @attr ref android.R.styleable#FrameLayout_Layout_layout_gravity
+         */
+        public int gravity = UNSPECIFIED_GRAVITY;
+
+        public LayoutParams(@NonNull Context c, @Nullable AttributeSet attrs) {
+            super(c, attrs);
+
+            final TypedArray a = c.obtainStyledAttributes(attrs, R.styleable.FrameLayout_Layout);
+            gravity = a.getInt(R.styleable.FrameLayout_Layout_layout_gravity, UNSPECIFIED_GRAVITY);
+            a.recycle();
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+        }
+
+        /**
+         * Creates a new set of layout parameters with the specified width, height
+         * and weight.
+         *
+         * @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 gravity the gravity
+         *
+         * @see android.view.Gravity
+         */
+        public LayoutParams(int width, int height, int gravity) {
+            super(width, height);
+            this.gravity = gravity;
+        }
+
+        public LayoutParams(@NonNull ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(@NonNull ViewGroup.MarginLayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * Copy constructor. Clones the width, height, margin values, and
+         * gravity of the source.
+         *
+         * @param source The layout params to copy from.
+         */
+        public LayoutParams(@NonNull LayoutParams source) {
+            super(source);
+
+            this.gravity = source.gravity;
+        }
+    }
+}
diff --git a/android/widget/Gallery.java b/android/widget/Gallery.java
new file mode 100644
index 0000000..7655f3d
--- /dev/null
+++ b/android/widget/Gallery.java
@@ -0,0 +1,1574 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextMenu.ContextMenuInfo;
+import android.view.GestureDetector;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.Transformation;
+
+import com.android.internal.R;
+
+/**
+ * A view that shows items in a center-locked, horizontally scrolling list.
+ * <p>
+ * The default values for the Gallery assume you will be using
+ * {@link android.R.styleable#Theme_galleryItemBackground} as the background for
+ * each View given to the Gallery from the Adapter. If you are not doing this,
+ * you may need to adjust some Gallery properties, such as the spacing.
+ * <p>
+ * Views given to the Gallery should use {@link Gallery.LayoutParams} as their
+ * layout parameters type.
+ * 
+ * @attr ref android.R.styleable#Gallery_animationDuration
+ * @attr ref android.R.styleable#Gallery_spacing
+ * @attr ref android.R.styleable#Gallery_gravity
+ * 
+ * @deprecated This widget is no longer supported. Other horizontally scrolling
+ * widgets include {@link HorizontalScrollView} and {@link android.support.v4.view.ViewPager}
+ * from the support library.
+ */
+@Deprecated
+@Widget
+public class Gallery extends AbsSpinner implements GestureDetector.OnGestureListener {
+
+    private static final String TAG = "Gallery";
+
+    private static final boolean localLOGV = false;
+
+    /**
+     * Duration in milliseconds from the start of a scroll during which we're
+     * unsure whether the user is scrolling or flinging.
+     */
+    private static final int SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT = 250;
+
+    /**
+     * Horizontal spacing between items.
+     */
+    private int mSpacing = 0;
+
+    /**
+     * How long the transition animation should run when a child view changes
+     * position, measured in milliseconds.
+     */
+    private int mAnimationDuration = 400;
+
+    /**
+     * The alpha of items that are not selected.
+     */
+    private float mUnselectedAlpha;
+    
+    /**
+     * Left most edge of a child seen so far during layout.
+     */
+    private int mLeftMost;
+
+    /**
+     * Right most edge of a child seen so far during layout.
+     */
+    private int mRightMost;
+
+    private int mGravity;
+
+    /**
+     * Helper for detecting touch gestures.
+     */
+    private GestureDetector mGestureDetector;
+
+    /**
+     * The position of the item that received the user's down touch.
+     */
+    private int mDownTouchPosition;
+
+    /**
+     * The view of the item that received the user's down touch.
+     */
+    private View mDownTouchView;
+    
+    /**
+     * Executes the delta scrolls from a fling or scroll movement. 
+     */
+    private FlingRunnable mFlingRunnable = new FlingRunnable();
+
+    /**
+     * Sets mSuppressSelectionChanged = false. This is used to set it to false
+     * in the future. It will also trigger a selection changed.
+     */
+    private Runnable mDisableSuppressSelectionChangedRunnable = new Runnable() {
+        @Override
+        public void run() {
+            mSuppressSelectionChanged = false;
+            selectionChanged();
+        }
+    };
+    
+    /**
+     * When fling runnable runs, it resets this to false. Any method along the
+     * path until the end of its run() can set this to true to abort any
+     * remaining fling. For example, if we've reached either the leftmost or
+     * rightmost item, we will set this to true.
+     */
+    private boolean mShouldStopFling;
+    
+    /**
+     * The currently selected item's child.
+     */
+    private View mSelectedChild;
+    
+    /**
+     * Whether to continuously callback on the item selected listener during a
+     * fling.
+     */
+    private boolean mShouldCallbackDuringFling = true;
+
+    /**
+     * Whether to callback when an item that is not selected is clicked.
+     */
+    private boolean mShouldCallbackOnUnselectedItemClick = true;
+
+    /**
+     * If true, do not callback to item selected listener. 
+     */
+    private boolean mSuppressSelectionChanged;
+
+    /**
+     * If true, we have received the "invoke" (center or enter buttons) key
+     * down. This is checked before we action on the "invoke" key up, and is
+     * subsequently cleared.
+     */
+    private boolean mReceivedInvokeKeyDown;
+    
+    private AdapterContextMenuInfo mContextMenuInfo;
+
+    /**
+     * If true, this onScroll is the first for this user's drag (remember, a
+     * drag sends many onScrolls).
+     */
+    private boolean mIsFirstScroll;
+
+    /**
+     * If true, mFirstPosition is the position of the rightmost child, and
+     * the children are ordered right to left.
+     */
+    private boolean mIsRtl = true;
+    
+    /**
+     * Offset between the center of the selected child view and the center of the Gallery.
+     * Used to reset position correctly during layout.
+     */
+    private int mSelectedCenterOffset;
+
+    public Gallery(Context context) {
+        this(context, null);
+    }
+
+    public Gallery(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.galleryStyle);
+    }
+
+    public Gallery(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public Gallery(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.Gallery, defStyleAttr, defStyleRes);
+
+        int index = a.getInt(com.android.internal.R.styleable.Gallery_gravity, -1);
+        if (index >= 0) {
+            setGravity(index);
+        }
+
+        int animationDuration =
+                a.getInt(com.android.internal.R.styleable.Gallery_animationDuration, -1);
+        if (animationDuration > 0) {
+            setAnimationDuration(animationDuration);
+        }
+
+        int spacing =
+                a.getDimensionPixelOffset(com.android.internal.R.styleable.Gallery_spacing, 0);
+        setSpacing(spacing);
+
+        float unselectedAlpha = a.getFloat(
+                com.android.internal.R.styleable.Gallery_unselectedAlpha, 0.5f);
+        setUnselectedAlpha(unselectedAlpha);
+        
+        a.recycle();
+
+        // We draw the selected item last (because otherwise the item to the
+        // right overlaps it)
+        mGroupFlags |= FLAG_USE_CHILD_DRAWING_ORDER;
+        
+        mGroupFlags |= FLAG_SUPPORT_STATIC_TRANSFORMATIONS;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (mGestureDetector == null) {
+            mGestureDetector = new GestureDetector(getContext(), this);
+            mGestureDetector.setIsLongpressEnabled(true);
+        }
+    }
+
+    /**
+     * Whether or not to callback on any {@link #getOnItemSelectedListener()}
+     * while the items are being flinged. If false, only the final selected item
+     * will cause the callback. If true, all items between the first and the
+     * final will cause callbacks.
+     * 
+     * @param shouldCallback Whether or not to callback on the listener while
+     *            the items are being flinged.
+     */
+    public void setCallbackDuringFling(boolean shouldCallback) {
+        mShouldCallbackDuringFling = shouldCallback;
+    }
+
+    /**
+     * Whether or not to callback when an item that is not selected is clicked.
+     * If false, the item will become selected (and re-centered). If true, the
+     * {@link #getOnItemClickListener()} will get the callback.
+     * 
+     * @param shouldCallback Whether or not to callback on the listener when a
+     *            item that is not selected is clicked.
+     * @hide
+     */
+    public void setCallbackOnUnselectedItemClick(boolean shouldCallback) {
+        mShouldCallbackOnUnselectedItemClick = shouldCallback;
+    }
+    
+    /**
+     * Sets how long the transition animation should run when a child view
+     * changes position. Only relevant if animation is turned on.
+     * 
+     * @param animationDurationMillis The duration of the transition, in
+     *        milliseconds.
+     * 
+     * @attr ref android.R.styleable#Gallery_animationDuration
+     */
+    public void setAnimationDuration(int animationDurationMillis) {
+        mAnimationDuration = animationDurationMillis;
+    }
+
+    /**
+     * Sets the spacing between items in a Gallery
+     * 
+     * @param spacing The spacing in pixels between items in the Gallery
+     * 
+     * @attr ref android.R.styleable#Gallery_spacing
+     */
+    public void setSpacing(int spacing) {
+        mSpacing = spacing;
+    }
+
+    /**
+     * Sets the alpha of items that are not selected in the Gallery.
+     * 
+     * @param unselectedAlpha the alpha for the items that are not selected.
+     * 
+     * @attr ref android.R.styleable#Gallery_unselectedAlpha
+     */
+    public void setUnselectedAlpha(float unselectedAlpha) {
+        mUnselectedAlpha = unselectedAlpha;
+    }
+
+    @Override
+    protected boolean getChildStaticTransformation(View child, Transformation t) {
+        
+        t.clear();
+        t.setAlpha(child == mSelectedChild ? 1.0f : mUnselectedAlpha);
+        
+        return true;
+    }
+
+    @Override
+    protected int computeHorizontalScrollExtent() {
+        // Only 1 item is considered to be selected
+        return 1;
+    }
+
+    @Override
+    protected int computeHorizontalScrollOffset() {
+        // Current scroll position is the same as the selected position
+        return mSelectedPosition;
+    }
+
+    @Override
+    protected int computeHorizontalScrollRange() {
+        // Scroll range is the same as the item count
+        return mItemCount;
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    public ViewGroup.LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        /*
+         * Gallery expects Gallery.LayoutParams.
+         */
+        return new Gallery.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
+                ViewGroup.LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        
+        /*
+         * Remember that we are in layout to prevent more layout request from
+         * being generated.
+         */
+        mInLayout = true;
+        layout(0, false);
+        mInLayout = false;
+    }
+
+    @Override
+    int getChildHeight(View child) {
+        return child.getMeasuredHeight();
+    }
+
+    /**
+     * Tracks a motion scroll. In reality, this is used to do just about any
+     * movement to items (touch scroll, arrow-key scroll, set an item as selected).
+     * 
+     * @param deltaX Change in X from the previous event.
+     */
+    void trackMotionScroll(int deltaX) {
+
+        if (getChildCount() == 0) {
+            return;
+        }
+        
+        boolean toLeft = deltaX < 0; 
+        
+        int limitedDeltaX = getLimitedMotionScrollAmount(toLeft, deltaX);
+        if (limitedDeltaX != deltaX) {
+            // The above call returned a limited amount, so stop any scrolls/flings
+            mFlingRunnable.endFling(false);
+            onFinishedMovement();
+        }
+        
+        offsetChildrenLeftAndRight(limitedDeltaX);
+        
+        detachOffScreenChildren(toLeft);
+        
+        if (toLeft) {
+            // If moved left, there will be empty space on the right
+            fillToGalleryRight();
+        } else {
+            // Similarly, empty space on the left
+            fillToGalleryLeft();
+        }
+        
+        // Clear unused views
+        mRecycler.clear();
+        
+        setSelectionToCenterChild();
+
+        final View selChild = mSelectedChild;
+        if (selChild != null) {
+            final int childLeft = selChild.getLeft();
+            final int childCenter = selChild.getWidth() / 2;
+            final int galleryCenter = getWidth() / 2;
+            mSelectedCenterOffset = childLeft + childCenter - galleryCenter;
+        }
+
+        onScrollChanged(0, 0, 0, 0); // dummy values, View's implementation does not use these.
+
+        invalidate();
+    }
+
+    int getLimitedMotionScrollAmount(boolean motionToLeft, int deltaX) {
+        int extremeItemPosition = motionToLeft != mIsRtl ? mItemCount - 1 : 0;
+        View extremeChild = getChildAt(extremeItemPosition - mFirstPosition);
+        
+        if (extremeChild == null) {
+            return deltaX;
+        }
+        
+        int extremeChildCenter = getCenterOfView(extremeChild);
+        int galleryCenter = getCenterOfGallery();
+        
+        if (motionToLeft) {
+            if (extremeChildCenter <= galleryCenter) {
+                
+                // The extreme child is past his boundary point!
+                return 0;
+            }
+        } else {
+            if (extremeChildCenter >= galleryCenter) {
+
+                // The extreme child is past his boundary point!
+                return 0;
+            }
+        }
+        
+        int centerDifference = galleryCenter - extremeChildCenter;
+
+        return motionToLeft
+                ? Math.max(centerDifference, deltaX)
+                : Math.min(centerDifference, deltaX); 
+    }
+
+    /**
+     * Offset the horizontal location of all children of this view by the
+     * specified number of pixels.
+     * 
+     * @param offset the number of pixels to offset
+     */
+    private void offsetChildrenLeftAndRight(int offset) {
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            getChildAt(i).offsetLeftAndRight(offset);
+        }
+    }
+    
+    /**
+     * @return The center of this Gallery.
+     */
+    private int getCenterOfGallery() {
+        return (getWidth() - mPaddingLeft - mPaddingRight) / 2 + mPaddingLeft;
+    }
+    
+    /**
+     * @return The center of the given view.
+     */
+    private static int getCenterOfView(View view) {
+        return view.getLeft() + view.getWidth() / 2;
+    }
+    
+    /**
+     * Detaches children that are off the screen (i.e.: Gallery bounds).
+     * 
+     * @param toLeft Whether to detach children to the left of the Gallery, or
+     *            to the right.
+     */
+    private void detachOffScreenChildren(boolean toLeft) {
+        int numChildren = getChildCount();
+        int firstPosition = mFirstPosition;
+        int start = 0;
+        int count = 0;
+
+        if (toLeft) {
+            final int galleryLeft = mPaddingLeft;
+            for (int i = 0; i < numChildren; i++) {
+                int n = mIsRtl ? (numChildren - 1 - i) : i;
+                final View child = getChildAt(n);
+                if (child.getRight() >= galleryLeft) {
+                    break;
+                } else {
+                    start = n;
+                    count++;
+                    mRecycler.put(firstPosition + n, child);
+                }
+            }
+            if (!mIsRtl) {
+                start = 0;
+            }
+        } else {
+            final int galleryRight = getWidth() - mPaddingRight;
+            for (int i = numChildren - 1; i >= 0; i--) {
+                int n = mIsRtl ? numChildren - 1 - i : i;
+                final View child = getChildAt(n);
+                if (child.getLeft() <= galleryRight) {
+                    break;
+                } else {
+                    start = n;
+                    count++;
+                    mRecycler.put(firstPosition + n, child);
+                }
+            }
+            if (mIsRtl) {
+                start = 0;
+            }
+        }
+
+        detachViewsFromParent(start, count);
+        
+        if (toLeft != mIsRtl) {
+            mFirstPosition += count;
+        }
+    }
+    
+    /**
+     * Scrolls the items so that the selected item is in its 'slot' (its center
+     * is the gallery's center).
+     */
+    private void scrollIntoSlots() {
+        
+        if (getChildCount() == 0 || mSelectedChild == null) return;
+        
+        int selectedCenter = getCenterOfView(mSelectedChild);
+        int targetCenter = getCenterOfGallery();
+        
+        int scrollAmount = targetCenter - selectedCenter;
+        if (scrollAmount != 0) {
+            mFlingRunnable.startUsingDistance(scrollAmount);
+        } else {
+            onFinishedMovement();
+        }
+    }
+
+    private void onFinishedMovement() {
+        if (mSuppressSelectionChanged) {
+            mSuppressSelectionChanged = false;
+            
+            // We haven't been callbacking during the fling, so do it now
+            super.selectionChanged();
+        }
+        mSelectedCenterOffset = 0;
+        invalidate();
+    }
+    
+    @Override
+    void selectionChanged() {
+        if (!mSuppressSelectionChanged) {
+            super.selectionChanged();
+        }
+    }
+
+    /**
+     * Looks for the child that is closest to the center and sets it as the
+     * selected child.
+     */
+    private void setSelectionToCenterChild() {
+        
+        View selView = mSelectedChild;
+        if (mSelectedChild == null) return;
+        
+        int galleryCenter = getCenterOfGallery();
+        
+        // Common case where the current selected position is correct
+        if (selView.getLeft() <= galleryCenter && selView.getRight() >= galleryCenter) {
+            return;
+        }
+        
+        // TODO better search
+        int closestEdgeDistance = Integer.MAX_VALUE;
+        int newSelectedChildIndex = 0;
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            
+            View child = getChildAt(i);
+            
+            if (child.getLeft() <= galleryCenter && child.getRight() >=  galleryCenter) {
+                // This child is in the center
+                newSelectedChildIndex = i;
+                break;
+            }
+            
+            int childClosestEdgeDistance = Math.min(Math.abs(child.getLeft() - galleryCenter),
+                    Math.abs(child.getRight() - galleryCenter));
+            if (childClosestEdgeDistance < closestEdgeDistance) {
+                closestEdgeDistance = childClosestEdgeDistance;
+                newSelectedChildIndex = i;
+            }
+        }
+        
+        int newPos = mFirstPosition + newSelectedChildIndex;
+        
+        if (newPos != mSelectedPosition) {
+            setSelectedPositionInt(newPos);
+            setNextSelectedPositionInt(newPos);
+            checkSelectionChanged();
+        }
+    }
+
+    /**
+     * Creates and positions all views for this Gallery.
+     * <p>
+     * We layout rarely, most of the time {@link #trackMotionScroll(int)} takes
+     * care of repositioning, adding, and removing children.
+     * 
+     * @param delta Change in the selected position. +1 means the selection is
+     *            moving to the right, so views are scrolling to the left. -1
+     *            means the selection is moving to the left.
+     */
+    @Override
+    void layout(int delta, boolean animate) {
+
+        mIsRtl = isLayoutRtl();
+
+        int childrenLeft = mSpinnerPadding.left;
+        int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
+
+        if (mDataChanged) {
+            handleDataChanged();
+        }
+
+        // Handle an empty gallery by removing all views.
+        if (mItemCount == 0) {
+            resetList();
+            return;
+        }
+
+        // Update to the new selected position.
+        if (mNextSelectedPosition >= 0) {
+            setSelectedPositionInt(mNextSelectedPosition);
+        }
+
+        // All views go in recycler while we are in layout
+        recycleAllViews();
+
+        // Clear out old views
+        //removeAllViewsInLayout();
+        detachAllViewsFromParent();
+
+        /*
+         * These will be used to give initial positions to views entering the
+         * gallery as we scroll
+         */
+        mRightMost = 0;
+        mLeftMost = 0;
+
+        // Make selected view and center it
+        
+        /*
+         * mFirstPosition will be decreased as we add views to the left later
+         * on. The 0 for x will be offset in a couple lines down.
+         */  
+        mFirstPosition = mSelectedPosition;
+        View sel = makeAndAddView(mSelectedPosition, 0, 0, true);
+        
+        // Put the selected child in the center
+        int selectedOffset = childrenLeft + (childrenWidth / 2) - (sel.getWidth() / 2) +
+                mSelectedCenterOffset;
+        sel.offsetLeftAndRight(selectedOffset);
+
+        fillToGalleryRight();
+        fillToGalleryLeft();
+        
+        // Flush any cached views that did not get reused above
+        mRecycler.clear();
+
+        invalidate();
+        checkSelectionChanged();
+
+        mDataChanged = false;
+        mNeedSync = false;
+        setNextSelectedPositionInt(mSelectedPosition);
+        
+        updateSelectedItemMetadata();
+    }
+
+    private void fillToGalleryLeft() {
+        if (mIsRtl) {
+            fillToGalleryLeftRtl();
+        } else {
+            fillToGalleryLeftLtr();
+        }
+    }
+
+    private void fillToGalleryLeftRtl() {
+        int itemSpacing = mSpacing;
+        int galleryLeft = mPaddingLeft;
+        int numChildren = getChildCount();
+        int numItems = mItemCount;
+
+        // Set state for initial iteration
+        View prevIterationView = getChildAt(numChildren - 1);
+        int curPosition;
+        int curRightEdge;
+
+        if (prevIterationView != null) {
+            curPosition = mFirstPosition + numChildren;
+            curRightEdge = prevIterationView.getLeft() - itemSpacing;
+        } else {
+            // No children available!
+            mFirstPosition = curPosition = mItemCount - 1;
+            curRightEdge = mRight - mLeft - mPaddingRight;
+            mShouldStopFling = true;
+        }
+
+        while (curRightEdge > galleryLeft && curPosition < mItemCount) {
+            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+                    curRightEdge, false);
+
+            // Set state for next iteration
+            curRightEdge = prevIterationView.getLeft() - itemSpacing;
+            curPosition++;
+        }
+    }
+
+    private void fillToGalleryLeftLtr() {
+        int itemSpacing = mSpacing;
+        int galleryLeft = mPaddingLeft;
+        
+        // Set state for initial iteration
+        View prevIterationView = getChildAt(0);
+        int curPosition;
+        int curRightEdge;
+        
+        if (prevIterationView != null) {
+            curPosition = mFirstPosition - 1;
+            curRightEdge = prevIterationView.getLeft() - itemSpacing;
+        } else {
+            // No children available!
+            curPosition = 0; 
+            curRightEdge = mRight - mLeft - mPaddingRight;
+            mShouldStopFling = true;
+        }
+                
+        while (curRightEdge > galleryLeft && curPosition >= 0) {
+            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+                    curRightEdge, false);
+
+            // Remember some state
+            mFirstPosition = curPosition;
+            
+            // Set state for next iteration
+            curRightEdge = prevIterationView.getLeft() - itemSpacing;
+            curPosition--;
+        }
+    }
+    
+    private void fillToGalleryRight() {
+        if (mIsRtl) {
+            fillToGalleryRightRtl();
+        } else {
+            fillToGalleryRightLtr();
+        }
+    }
+
+    private void fillToGalleryRightRtl() {
+        int itemSpacing = mSpacing;
+        int galleryRight = mRight - mLeft - mPaddingRight;
+
+        // Set state for initial iteration
+        View prevIterationView = getChildAt(0);
+        int curPosition;
+        int curLeftEdge;
+
+        if (prevIterationView != null) {
+            curPosition = mFirstPosition -1;
+            curLeftEdge = prevIterationView.getRight() + itemSpacing;
+        } else {
+            curPosition = 0;
+            curLeftEdge = mPaddingLeft;
+            mShouldStopFling = true;
+        }
+
+        while (curLeftEdge < galleryRight && curPosition >= 0) {
+            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+                    curLeftEdge, true);
+
+            // Remember some state
+            mFirstPosition = curPosition;
+
+            // Set state for next iteration
+            curLeftEdge = prevIterationView.getRight() + itemSpacing;
+            curPosition--;
+        }
+    }
+
+    private void fillToGalleryRightLtr() {
+        int itemSpacing = mSpacing;
+        int galleryRight = mRight - mLeft - mPaddingRight;
+        int numChildren = getChildCount();
+        int numItems = mItemCount;
+        
+        // Set state for initial iteration
+        View prevIterationView = getChildAt(numChildren - 1);
+        int curPosition;
+        int curLeftEdge;
+        
+        if (prevIterationView != null) {
+            curPosition = mFirstPosition + numChildren;
+            curLeftEdge = prevIterationView.getRight() + itemSpacing;
+        } else {
+            mFirstPosition = curPosition = mItemCount - 1;
+            curLeftEdge = mPaddingLeft;
+            mShouldStopFling = true;
+        }
+                
+        while (curLeftEdge < galleryRight && curPosition < numItems) {
+            prevIterationView = makeAndAddView(curPosition, curPosition - mSelectedPosition,
+                    curLeftEdge, true);
+
+            // Set state for next iteration
+            curLeftEdge = prevIterationView.getRight() + itemSpacing;
+            curPosition++;
+        }
+    }
+
+    /**
+     * Obtain a view, either by pulling an existing view from the recycler or by
+     * getting a new one from the adapter. If we are animating, make sure there
+     * is enough information in the view's layout parameters to animate from the
+     * old to new positions.
+     * 
+     * @param position Position in the gallery for the view to obtain
+     * @param offset Offset from the selected position
+     * @param x X-coordinate indicating where this view should be placed. This
+     *        will either be the left or right edge of the view, depending on
+     *        the fromLeft parameter
+     * @param fromLeft Are we positioning views based on the left edge? (i.e.,
+     *        building from left to right)?
+     * @return A view that has been added to the gallery
+     */
+    private View makeAndAddView(int position, int offset, int x, boolean fromLeft) {
+
+        View child;
+        if (!mDataChanged) {
+            child = mRecycler.get(position);
+            if (child != null) {
+                // Can reuse an existing view
+                int childLeft = child.getLeft();
+                
+                // Remember left and right edges of where views have been placed
+                mRightMost = Math.max(mRightMost, childLeft 
+                        + child.getMeasuredWidth());
+                mLeftMost = Math.min(mLeftMost, childLeft);
+
+                // Position the view
+                setUpChild(child, offset, x, fromLeft);
+
+                return child;
+            }
+        }
+
+        // Nothing found in the recycler -- ask the adapter for a view
+        child = mAdapter.getView(position, null, this);
+
+        // Position the view
+        setUpChild(child, offset, x, fromLeft);
+
+        return child;
+    }
+
+    /**
+     * Helper for makeAndAddView to set the position of a view and fill out its
+     * layout parameters.
+     * 
+     * @param child The view to position
+     * @param offset Offset from the selected position
+     * @param x X-coordinate indicating where this view should be placed. This
+     *        will either be the left or right edge of the view, depending on
+     *        the fromLeft parameter
+     * @param fromLeft Are we positioning views based on the left edge? (i.e.,
+     *        building from left to right)?
+     */
+    private void setUpChild(View child, int offset, int x, boolean fromLeft) {
+
+        // Respect layout params that are already in the view. Otherwise
+        // make some up...
+        Gallery.LayoutParams lp = (Gallery.LayoutParams) child.getLayoutParams();
+        if (lp == null) {
+            lp = (Gallery.LayoutParams) generateDefaultLayoutParams();
+        }
+
+        addViewInLayout(child, fromLeft != mIsRtl ? -1 : 0, lp, true);
+
+        child.setSelected(offset == 0);
+
+        // Get measure specs
+        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
+                mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
+        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+                mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
+
+        // Measure child
+        child.measure(childWidthSpec, childHeightSpec);
+
+        int childLeft;
+        int childRight;
+
+        // Position vertically based on gravity setting
+        int childTop = calculateTop(child, true);
+        int childBottom = childTop + child.getMeasuredHeight();
+
+        int width = child.getMeasuredWidth();
+        if (fromLeft) {
+            childLeft = x;
+            childRight = childLeft + width;
+        } else {
+            childLeft = x - width;
+            childRight = x;
+        }
+
+        child.layout(childLeft, childTop, childRight, childBottom);
+    }
+
+    /**
+     * Figure out vertical placement based on mGravity
+     * 
+     * @param child Child to place
+     * @return Where the top of the child should be
+     */
+    private int calculateTop(View child, boolean duringLayout) {
+        int myHeight = duringLayout ? getMeasuredHeight() : getHeight();
+        int childHeight = duringLayout ? child.getMeasuredHeight() : child.getHeight(); 
+        
+        int childTop = 0;
+
+        switch (mGravity) {
+        case Gravity.TOP:
+            childTop = mSpinnerPadding.top;
+            break;
+        case Gravity.CENTER_VERTICAL:
+            int availableSpace = myHeight - mSpinnerPadding.bottom
+                    - mSpinnerPadding.top - childHeight;
+            childTop = mSpinnerPadding.top + (availableSpace / 2);
+            break;
+        case Gravity.BOTTOM:
+            childTop = myHeight - mSpinnerPadding.bottom - childHeight;
+            break;
+        }
+        return childTop;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+
+        // Give everything to the gesture detector
+        boolean retValue = mGestureDetector.onTouchEvent(event);
+
+        int action = event.getAction();
+        if (action == MotionEvent.ACTION_UP) {
+            // Helper method for lifted finger
+            onUp();
+        } else if (action == MotionEvent.ACTION_CANCEL) {
+            onCancel();
+        }
+        
+        return retValue;
+    }
+    
+    @Override
+    public boolean onSingleTapUp(MotionEvent e) {
+
+        if (mDownTouchPosition >= 0) {
+            
+            // An item tap should make it selected, so scroll to this child.
+            scrollToChild(mDownTouchPosition - mFirstPosition);
+
+            // Also pass the click so the client knows, if it wants to.
+            if (mShouldCallbackOnUnselectedItemClick || mDownTouchPosition == mSelectedPosition) {
+                performItemClick(mDownTouchView, mDownTouchPosition, mAdapter
+                        .getItemId(mDownTouchPosition));
+            }
+            
+            return true;
+        }
+        
+        return false;
+    }
+
+    @Override
+    public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
+        
+        if (!mShouldCallbackDuringFling) {
+            // We want to suppress selection changes
+            
+            // Remove any future code to set mSuppressSelectionChanged = false
+            removeCallbacks(mDisableSuppressSelectionChangedRunnable);
+
+            // This will get reset once we scroll into slots
+            if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
+        }
+        
+        // Fling the gallery!
+        mFlingRunnable.startUsingVelocity((int) -velocityX);
+        
+        return true;
+    }
+
+    @Override
+    public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
+
+        if (localLOGV) Log.v(TAG, String.valueOf(e2.getX() - e1.getX()));
+        
+        /*
+         * Now's a good time to tell our parent to stop intercepting our events!
+         * The user has moved more than the slop amount, since GestureDetector
+         * ensures this before calling this method. Also, if a parent is more
+         * interested in this touch's events than we are, it would have
+         * intercepted them by now (for example, we can assume when a Gallery is
+         * in the ListView, a vertical scroll would not end up in this method
+         * since a ListView would have intercepted it by now).
+         */
+        mParent.requestDisallowInterceptTouchEvent(true);
+        
+        // As the user scrolls, we want to callback selection changes so related-
+        // info on the screen is up-to-date with the gallery's selection
+        if (!mShouldCallbackDuringFling) {
+            if (mIsFirstScroll) {
+                /*
+                 * We're not notifying the client of selection changes during
+                 * the fling, and this scroll could possibly be a fling. Don't
+                 * do selection changes until we're sure it is not a fling.
+                 */
+                if (!mSuppressSelectionChanged) mSuppressSelectionChanged = true;
+                postDelayed(mDisableSuppressSelectionChangedRunnable, SCROLL_TO_FLING_UNCERTAINTY_TIMEOUT);
+            }
+        } else {
+            if (mSuppressSelectionChanged) mSuppressSelectionChanged = false;
+        }
+        
+        // Track the motion
+        trackMotionScroll(-1 * (int) distanceX);
+       
+        mIsFirstScroll = false;
+        return true;
+    }
+    
+    @Override
+    public boolean onDown(MotionEvent e) {
+
+        // Kill any existing fling/scroll
+        mFlingRunnable.stop(false);
+
+        // Get the item's view that was touched
+        mDownTouchPosition = pointToPosition((int) e.getX(), (int) e.getY());
+        
+        if (mDownTouchPosition >= 0) {
+            mDownTouchView = getChildAt(mDownTouchPosition - mFirstPosition);
+            mDownTouchView.setPressed(true);
+        }
+        
+        // Reset the multiple-scroll tracking state
+        mIsFirstScroll = true;
+        
+        // Must return true to get matching events for this down event.
+        return true;
+    }
+
+    /**
+     * Called when a touch event's action is MotionEvent.ACTION_UP.
+     */
+    void onUp() {
+        
+        if (mFlingRunnable.mScroller.isFinished()) {
+            scrollIntoSlots();
+        }
+        
+        dispatchUnpress();
+    }
+    
+    /**
+     * Called when a touch event's action is MotionEvent.ACTION_CANCEL.
+     */
+    void onCancel() {
+        onUp();
+    }
+    
+    @Override
+    public void onLongPress(@NonNull MotionEvent e) {
+        if (mDownTouchPosition < 0) {
+            return;
+        }
+        
+        performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+
+        final long id = getItemIdAtPosition(mDownTouchPosition);
+        dispatchLongPress(mDownTouchView, mDownTouchPosition, id, e.getX(), e.getY(), true);
+    }
+
+    // Unused methods from GestureDetector.OnGestureListener below
+    
+    @Override
+    public void onShowPress(MotionEvent e) {
+    }
+
+    // Unused methods from GestureDetector.OnGestureListener above
+    
+    private void dispatchPress(View child) {
+        
+        if (child != null) {
+            child.setPressed(true);
+        }
+        
+        setPressed(true);
+    }
+    
+    private void dispatchUnpress() {
+        
+        for (int i = getChildCount() - 1; i >= 0; i--) {
+            getChildAt(i).setPressed(false);
+        }
+        
+        setPressed(false);
+    }
+    
+    @Override
+    public void dispatchSetSelected(boolean selected) {
+        /*
+         * We don't want to pass the selected state given from its parent to its
+         * children since this widget itself has a selected state to give to its
+         * children.
+         */
+    }
+
+    @Override
+    protected void dispatchSetPressed(boolean pressed) {
+        
+        // Show the pressed state on the selected child
+        if (mSelectedChild != null) {
+            mSelectedChild.setPressed(pressed);
+        }
+    }
+
+    @Override
+    protected ContextMenuInfo getContextMenuInfo() {
+        return mContextMenuInfo;
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView) {
+        if (isShowingContextMenuWithCoords()) {
+            return false;
+        }
+        return showContextMenuForChildInternal(originalView, 0, 0, false);
+    }
+
+    @Override
+    public boolean showContextMenuForChild(View originalView, float x, float y) {
+        return showContextMenuForChildInternal(originalView, x, y, true);
+    }
+
+    private boolean showContextMenuForChildInternal(View originalView, float x, float y,
+            boolean useOffsets) {
+        final int longPressPosition = getPositionForView(originalView);
+        if (longPressPosition < 0) {
+            return false;
+        }
+        
+        final long longPressId = mAdapter.getItemId(longPressPosition);
+        return dispatchLongPress(originalView, longPressPosition, longPressId, x, y, useOffsets);
+    }
+
+    @Override
+    public boolean showContextMenu() {
+        return showContextMenuInternal(0, 0, false);
+    }
+
+    @Override
+    public boolean showContextMenu(float x, float y) {
+        return showContextMenuInternal(x, y, true);
+    }
+
+    private boolean showContextMenuInternal(float x, float y, boolean useOffsets) {
+        if (isPressed() && mSelectedPosition >= 0) {
+            final int index = mSelectedPosition - mFirstPosition;
+            final View v = getChildAt(index);
+            return dispatchLongPress(v, mSelectedPosition, mSelectedRowId, x, y, useOffsets);
+        }        
+        
+        return false;
+    }
+
+    private boolean dispatchLongPress(View view, int position, long id, float x, float y,
+            boolean useOffsets) {
+        boolean handled = false;
+        
+        if (mOnItemLongClickListener != null) {
+            handled = mOnItemLongClickListener.onItemLongClick(this, mDownTouchView,
+                    mDownTouchPosition, id);
+        }
+
+        if (!handled) {
+            mContextMenuInfo = new AdapterContextMenuInfo(view, position, id);
+
+            if (useOffsets) {
+                handled = super.showContextMenuForChild(view, x, y);
+            } else {
+                handled = super.showContextMenuForChild(this);
+            }
+        }
+
+        if (handled) {
+            performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+        }
+        
+        return handled;
+    }
+    
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Gallery steals all key events
+        return event.dispatch(this, null, null);
+    }
+
+    /**
+     * Handles left, right, and clicking
+     * @see android.view.View#onKeyDown
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        switch (keyCode) {
+            
+        case KeyEvent.KEYCODE_DPAD_LEFT:
+            if (moveDirection(-1)) {
+                playSoundEffect(SoundEffectConstants.NAVIGATION_LEFT);
+                return true;
+            }
+            break;
+        case KeyEvent.KEYCODE_DPAD_RIGHT:
+            if (moveDirection(1)) {
+                playSoundEffect(SoundEffectConstants.NAVIGATION_RIGHT);
+                return true;
+            }
+            break;
+        case KeyEvent.KEYCODE_DPAD_CENTER:
+        case KeyEvent.KEYCODE_ENTER:
+            mReceivedInvokeKeyDown = true;
+            // fallthrough to default handling
+        }
+        
+        return super.onKeyDown(keyCode, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (KeyEvent.isConfirmKey(keyCode)) {
+            if (mReceivedInvokeKeyDown) {
+                if (mItemCount > 0) {
+                    dispatchPress(mSelectedChild);
+                    postDelayed(new Runnable() {
+                        @Override
+                        public void run() {
+                            dispatchUnpress();
+                        }
+                    }, ViewConfiguration.getPressedStateDuration());
+
+                    int selectedIndex = mSelectedPosition - mFirstPosition;
+                    performItemClick(getChildAt(selectedIndex), mSelectedPosition, mAdapter
+                            .getItemId(mSelectedPosition));
+                }
+            }
+
+            // Clear the flag
+            mReceivedInvokeKeyDown = false;
+            return true;
+        }
+        return super.onKeyUp(keyCode, event);
+    }
+    
+    boolean moveDirection(int direction) {
+        direction = isLayoutRtl() ? -direction : direction;
+        int targetPosition = mSelectedPosition + direction;
+
+        if (mItemCount > 0 && targetPosition >= 0 && targetPosition < mItemCount) {
+            scrollToChild(targetPosition - mFirstPosition);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private boolean scrollToChild(int childPosition) {
+        View child = getChildAt(childPosition);
+        
+        if (child != null) {
+            int distance = getCenterOfGallery() - getCenterOfView(child);
+            mFlingRunnable.startUsingDistance(distance);
+            return true;
+        }
+        
+        return false;
+    }
+    
+    @Override
+    void setSelectedPositionInt(int position) {
+        super.setSelectedPositionInt(position);
+
+        // Updates any metadata we keep about the selected item.
+        updateSelectedItemMetadata();
+    }
+
+    private void updateSelectedItemMetadata() {
+        
+        View oldSelectedChild = mSelectedChild;
+
+        View child = mSelectedChild = getChildAt(mSelectedPosition - mFirstPosition);
+        if (child == null) {
+            return;
+        }
+
+        child.setSelected(true);
+        child.setFocusable(true);
+
+        if (hasFocus()) {
+            child.requestFocus();
+        }
+
+        // We unfocus the old child down here so the above hasFocus check
+        // returns true
+        if (oldSelectedChild != null && oldSelectedChild != child) {
+
+            // Make sure its drawable state doesn't contain 'selected'
+            oldSelectedChild.setSelected(false);
+            
+            // Make sure it is not focusable anymore, since otherwise arrow keys
+            // can make this one be focused
+            oldSelectedChild.setFocusable(false);
+        }
+        
+    }
+    
+    /**
+     * Describes how the child views are aligned.
+     * @param gravity
+     * 
+     * @attr ref android.R.styleable#Gallery_gravity
+     */
+    public void setGravity(int gravity)
+    {
+        if (mGravity != gravity) {
+            mGravity = gravity;
+            requestLayout();
+        }
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        int selectedIndex = mSelectedPosition - mFirstPosition;
+        
+        // Just to be safe
+        if (selectedIndex < 0) return i;
+        
+        if (i == childCount - 1) {
+            // Draw the selected child last
+            return selectedIndex;
+        } else if (i >= selectedIndex) {
+            // Move the children after the selected child earlier one
+            return i + 1;
+        } else {
+            // Keep the children before the selected child the same
+            return i;
+        }
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+        
+        /*
+         * The gallery shows focus by focusing the selected item. So, give
+         * focus to our selected item instead. We steal keys from our
+         * selected item elsewhere.
+         */
+        if (gainFocus && mSelectedChild != null) {
+            mSelectedChild.requestFocus(direction);
+            mSelectedChild.setSelected(true);
+        }
+
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Gallery.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setScrollable(mItemCount > 1);
+        if (isEnabled()) {
+            if (mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+            }
+            if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+                if (isEnabled() && mItemCount > 0 && mSelectedPosition < mItemCount - 1) {
+                    final int currentChildIndex = mSelectedPosition - mFirstPosition;
+                    return scrollToChild(currentChildIndex + 1);
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+                if (isEnabled() && mItemCount > 0 && mSelectedPosition > 0) {
+                    final int currentChildIndex = mSelectedPosition - mFirstPosition;
+                    return scrollToChild(currentChildIndex - 1);
+                }
+            } return false;
+        }
+        return false;
+    }
+
+    /**
+     * Responsible for fling behavior. Use {@link #startUsingVelocity(int)} to
+     * initiate a fling. Each frame of the fling is handled in {@link #run()}.
+     * A FlingRunnable will keep re-posting itself until the fling is done.
+     */
+    private class FlingRunnable implements Runnable {
+        /**
+         * Tracks the decay of a fling scroll
+         */
+        private Scroller mScroller;
+
+        /**
+         * X value reported by mScroller on the previous fling
+         */
+        private int mLastFlingX;
+
+        public FlingRunnable() {
+            mScroller = new Scroller(getContext());
+        }
+
+        private void startCommon() {
+            // Remove any pending flings
+            removeCallbacks(this);
+        }
+        
+        public void startUsingVelocity(int initialVelocity) {
+            if (initialVelocity == 0) return;
+            
+            startCommon();
+            
+            int initialX = initialVelocity < 0 ? Integer.MAX_VALUE : 0;
+            mLastFlingX = initialX;
+            mScroller.fling(initialX, 0, initialVelocity, 0,
+                    0, Integer.MAX_VALUE, 0, Integer.MAX_VALUE);
+            post(this);
+        }
+
+        public void startUsingDistance(int distance) {
+            if (distance == 0) return;
+            
+            startCommon();
+            
+            mLastFlingX = 0;
+            mScroller.startScroll(0, 0, -distance, 0, mAnimationDuration);
+            post(this);
+        }
+        
+        public void stop(boolean scrollIntoSlots) {
+            removeCallbacks(this);
+            endFling(scrollIntoSlots);
+        }
+        
+        private void endFling(boolean scrollIntoSlots) {
+            /*
+             * Force the scroller's status to finished (without setting its
+             * position to the end)
+             */
+            mScroller.forceFinished(true);
+            
+            if (scrollIntoSlots) scrollIntoSlots();
+        }
+
+        @Override
+        public void run() {
+
+            if (mItemCount == 0) {
+                endFling(true);
+                return;
+            }
+
+            mShouldStopFling = false;
+            
+            final Scroller scroller = mScroller;
+            boolean more = scroller.computeScrollOffset();
+            final int x = scroller.getCurrX();
+
+            // Flip sign to convert finger direction to list items direction
+            // (e.g. finger moving down means list is moving towards the top)
+            int delta = mLastFlingX - x;
+
+            // Pretend that each frame of a fling scroll is a touch scroll
+            if (delta > 0) {
+                // Moving towards the left. Use leftmost view as mDownTouchPosition
+                mDownTouchPosition = mIsRtl ? (mFirstPosition + getChildCount() - 1) :
+                    mFirstPosition;
+
+                // Don't fling more than 1 screen
+                delta = Math.min(getWidth() - mPaddingLeft - mPaddingRight - 1, delta);
+            } else {
+                // Moving towards the right. Use rightmost view as mDownTouchPosition
+                int offsetToLast = getChildCount() - 1;
+                mDownTouchPosition = mIsRtl ? mFirstPosition :
+                    (mFirstPosition + getChildCount() - 1);
+
+                // Don't fling more than 1 screen
+                delta = Math.max(-(getWidth() - mPaddingRight - mPaddingLeft - 1), delta);
+            }
+
+            trackMotionScroll(delta);
+
+            if (more && !mShouldStopFling) {
+                mLastFlingX = x;
+                post(this);
+            } else {
+               endFling(true);
+            }
+        }
+        
+    }
+    
+    /**
+     * Gallery extends LayoutParams to provide a place to hold current
+     * Transformation information along with previous position/transformation
+     * info.
+     */
+    public static class LayoutParams extends ViewGroup.LayoutParams {
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int w, int h) {
+            super(w, h);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+    }
+}
diff --git a/android/widget/GridLayout.java b/android/widget/GridLayout.java
new file mode 100644
index 0000000..cbd1e0a
--- /dev/null
+++ b/android/widget/GridLayout.java
@@ -0,0 +1,2985 @@
+/*
+ * 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 android.widget;
+
+import static android.view.Gravity.AXIS_PULL_AFTER;
+import static android.view.Gravity.AXIS_PULL_BEFORE;
+import static android.view.Gravity.AXIS_SPECIFIED;
+import static android.view.Gravity.AXIS_X_SHIFT;
+import static android.view.Gravity.AXIS_Y_SHIFT;
+import static android.view.Gravity.HORIZONTAL_GRAVITY_MASK;
+import static android.view.Gravity.RELATIVE_LAYOUT_DIRECTION;
+import static android.view.Gravity.VERTICAL_GRAVITY_MASK;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.makeMeasureSpec;
+
+import static java.lang.Math.max;
+import static java.lang.Math.min;
+
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.LogPrinter;
+import android.util.Pair;
+import android.util.Printer;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * A layout that places its children in a rectangular <em>grid</em>.
+ * <p>
+ * The grid is composed of a set of infinitely thin lines that separate the
+ * viewing area into <em>cells</em>. Throughout the API, grid lines are referenced
+ * by grid <em>indices</em>. A grid with {@code N} columns
+ * has {@code N + 1} grid indices that run from {@code 0}
+ * through {@code N} inclusive. Regardless of how GridLayout is
+ * configured, grid index {@code 0} is fixed to the leading edge of the
+ * container and grid index {@code N} is fixed to its trailing edge
+ * (after padding is taken into account).
+ *
+ * <h4>Row and Column Specs</h4>
+ *
+ * Children occupy one or more contiguous cells, as defined
+ * by their {@link GridLayout.LayoutParams#rowSpec rowSpec} and
+ * {@link GridLayout.LayoutParams#columnSpec columnSpec} layout parameters.
+ * Each spec defines the set of rows or columns that are to be
+ * occupied; and how children should be aligned within the resulting group of cells.
+ * Although cells do not normally overlap in a GridLayout, GridLayout does
+ * not prevent children being defined to occupy the same cell or group of cells.
+ * In this case however, there is no guarantee that children will not themselves
+ * overlap after the layout operation completes.
+ *
+ * <h4>Default Cell Assignment</h4>
+ *
+ * If a child does not specify the row and column indices of the cell it
+ * wishes to occupy, GridLayout assigns cell locations automatically using its:
+ * {@link GridLayout#setOrientation(int) orientation},
+ * {@link GridLayout#setRowCount(int) rowCount} and
+ * {@link GridLayout#setColumnCount(int) columnCount} properties.
+ *
+ * <h4>Space</h4>
+ *
+ * Space between children may be specified either by using instances of the
+ * dedicated {@link Space} view or by setting the
+ *
+ * {@link ViewGroup.MarginLayoutParams#leftMargin leftMargin},
+ * {@link ViewGroup.MarginLayoutParams#topMargin topMargin},
+ * {@link ViewGroup.MarginLayoutParams#rightMargin rightMargin} and
+ * {@link ViewGroup.MarginLayoutParams#bottomMargin bottomMargin}
+ *
+ * layout parameters. When the
+ * {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins}
+ * property is set, default margins around children are automatically
+ * allocated based on the prevailing UI style guide for the platform.
+ * Each of the margins so defined may be independently overridden by an assignment
+ * to the appropriate layout parameter.
+ * Default values will generally produce a reasonable spacing between components
+ * but values may change between different releases of the platform.
+ *
+ * <h4>Excess Space Distribution</h4>
+ *
+ * As of API 21, GridLayout's distribution of excess space accomodates the principle of weight.
+ * In the event that no weights are specified, the previous conventions are respected and
+ * columns and rows are taken as flexible if their views specify some form of alignment
+ * within their groups.
+ * <p>
+ * The flexibility of a view is therefore influenced by its alignment which is,
+ * in turn, typically defined by setting the
+ * {@link LayoutParams#setGravity(int) gravity} property of the child's layout parameters.
+ * If either a weight or alignment were defined along a given axis then the component
+ * is taken as <em>flexible</em> in that direction. If no weight or alignment was set,
+ * the component is instead assumed to be <em>inflexible</em>.
+ * <p>
+ * Multiple components in the same row or column group are
+ * considered to act in <em>parallel</em>. Such a
+ * group is flexible only if <em>all</em> of the components
+ * within it are flexible. Row and column groups that sit either side of a common boundary
+ * are instead considered to act in <em>series</em>. The composite group made of these two
+ * elements is flexible if <em>one</em> of its elements is flexible.
+ * <p>
+ * To make a column stretch, make sure all of the components inside it define a
+ * weight or a gravity. To prevent a column from stretching, ensure that one of the components
+ * in the column does not define a weight or a gravity.
+ * <p>
+ * When the principle of flexibility does not provide complete disambiguation,
+ * GridLayout's algorithms favour rows and columns that are closer to its <em>right</em>
+ * and <em>bottom</em> edges. To be more precise, GridLayout treats each of its layout
+ * parameters as a constraint in the a set of variables that define the grid-lines along a
+ * given axis. During layout, GridLayout solves the constraints so as to return the unique
+ * solution to those constraints for which all variables are less-than-or-equal-to
+ * the corresponding value in any other valid solution.
+ *
+ * <h4>Interpretation of GONE</h4>
+ *
+ * For layout purposes, GridLayout treats views whose visibility status is
+ * {@link View#GONE GONE}, as having zero width and height. This is subtly different from
+ * the policy of ignoring views that are marked as GONE outright. If, for example, a gone-marked
+ * view was alone in a column, that column would itself collapse to zero width if and only if
+ * no gravity was defined on the view. If gravity was defined, then the gone-marked
+ * view has no effect on the layout and the container should be laid out as if the view
+ * had never been added to it. GONE views are taken to have zero weight during excess space
+ * distribution.
+ * <p>
+ * These statements apply equally to rows as well as columns, and to groups of rows or columns.
+ *
+ * <p>
+ * See {@link GridLayout.LayoutParams} for a full description of the
+ * layout parameters used by GridLayout.
+ *
+ * @attr ref android.R.styleable#GridLayout_orientation
+ * @attr ref android.R.styleable#GridLayout_rowCount
+ * @attr ref android.R.styleable#GridLayout_columnCount
+ * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+ * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+ * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+ */
+@RemoteView
+public class GridLayout extends ViewGroup {
+
+    // Public constants
+
+    /** @hide */
+    @IntDef({HORIZONTAL, VERTICAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Orientation {}
+
+    /**
+     * The horizontal orientation.
+     */
+    public static final int HORIZONTAL = LinearLayout.HORIZONTAL;
+
+    /**
+     * The vertical orientation.
+     */
+    public static final int VERTICAL = LinearLayout.VERTICAL;
+
+    /**
+     * The constant used to indicate that a value is undefined.
+     * Fields can use this value to indicate that their values
+     * have not yet been set. Similarly, methods can return this value
+     * to indicate that there is no suitable value that the implementation
+     * can return.
+     * The value used for the constant (currently {@link Integer#MIN_VALUE}) is
+     * intended to avoid confusion between valid values whose sign may not be known.
+     */
+    public static final int UNDEFINED = Integer.MIN_VALUE;
+
+    /** @hide */
+    @IntDef({ALIGN_BOUNDS, ALIGN_MARGINS})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AlignmentMode {}
+
+    /**
+     * This constant is an {@link #setAlignmentMode(int) alignmentMode}.
+     * When the {@code alignmentMode} is set to {@link #ALIGN_BOUNDS}, alignment
+     * is made between the edges of each component's raw
+     * view boundary: i.e. the area delimited by the component's:
+     * {@link android.view.View#getTop() top},
+     * {@link android.view.View#getLeft() left},
+     * {@link android.view.View#getBottom() bottom} and
+     * {@link android.view.View#getRight() right} properties.
+     * <p>
+     * For example, when {@code GridLayout} is in {@link #ALIGN_BOUNDS} mode,
+     * children that belong to a row group that uses {@link #TOP} alignment will
+     * all return the same value when their {@link android.view.View#getTop()}
+     * method is called.
+     *
+     * @see #setAlignmentMode(int)
+     */
+    public static final int ALIGN_BOUNDS = 0;
+
+    /**
+     * This constant is an {@link #setAlignmentMode(int) alignmentMode}.
+     * When the {@code alignmentMode} is set to {@link #ALIGN_MARGINS},
+     * the bounds of each view are extended outwards, according
+     * to their margins, before the edges of the resulting rectangle are aligned.
+     * <p>
+     * For example, when {@code GridLayout} is in {@link #ALIGN_MARGINS} mode,
+     * the quantity {@code top - layoutParams.topMargin} is the same for all children that
+     * belong to a row group that uses {@link #TOP} alignment.
+     *
+     * @see #setAlignmentMode(int)
+     */
+    public static final int ALIGN_MARGINS = 1;
+
+    // Misc constants
+
+    static final int MAX_SIZE = 100000;
+    static final int DEFAULT_CONTAINER_MARGIN = 0;
+    static final int UNINITIALIZED_HASH = 0;
+    static final Printer LOG_PRINTER = new LogPrinter(Log.DEBUG, GridLayout.class.getName());
+    static final Printer NO_PRINTER = new Printer() {
+        @Override
+        public void println(String x) {
+        }
+    };
+
+    // Defaults
+
+    private static final int DEFAULT_ORIENTATION = HORIZONTAL;
+    private static final int DEFAULT_COUNT = UNDEFINED;
+    private static final boolean DEFAULT_USE_DEFAULT_MARGINS = false;
+    private static final boolean DEFAULT_ORDER_PRESERVED = true;
+    private static final int DEFAULT_ALIGNMENT_MODE = ALIGN_MARGINS;
+
+    // TypedArray indices
+
+    private static final int ORIENTATION = R.styleable.GridLayout_orientation;
+    private static final int ROW_COUNT = R.styleable.GridLayout_rowCount;
+    private static final int COLUMN_COUNT = R.styleable.GridLayout_columnCount;
+    private static final int USE_DEFAULT_MARGINS = R.styleable.GridLayout_useDefaultMargins;
+    private static final int ALIGNMENT_MODE = R.styleable.GridLayout_alignmentMode;
+    private static final int ROW_ORDER_PRESERVED = R.styleable.GridLayout_rowOrderPreserved;
+    private static final int COLUMN_ORDER_PRESERVED = R.styleable.GridLayout_columnOrderPreserved;
+
+    // Instance variables
+
+    final Axis mHorizontalAxis = new Axis(true);
+    final Axis mVerticalAxis = new Axis(false);
+    int mOrientation = DEFAULT_ORIENTATION;
+    boolean mUseDefaultMargins = DEFAULT_USE_DEFAULT_MARGINS;
+    int mAlignmentMode = DEFAULT_ALIGNMENT_MODE;
+    int mDefaultGap;
+    int mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
+    Printer mPrinter = LOG_PRINTER;
+
+    // Constructors
+
+    public GridLayout(Context context) {
+        this(context, null);
+    }
+
+    public GridLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public GridLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public GridLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        mDefaultGap = context.getResources().getDimensionPixelOffset(R.dimen.default_gap);
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.GridLayout, defStyleAttr, defStyleRes);
+        try {
+            setRowCount(a.getInt(ROW_COUNT, DEFAULT_COUNT));
+            setColumnCount(a.getInt(COLUMN_COUNT, DEFAULT_COUNT));
+            setOrientation(a.getInt(ORIENTATION, DEFAULT_ORIENTATION));
+            setUseDefaultMargins(a.getBoolean(USE_DEFAULT_MARGINS, DEFAULT_USE_DEFAULT_MARGINS));
+            setAlignmentMode(a.getInt(ALIGNMENT_MODE, DEFAULT_ALIGNMENT_MODE));
+            setRowOrderPreserved(a.getBoolean(ROW_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED));
+            setColumnOrderPreserved(a.getBoolean(COLUMN_ORDER_PRESERVED, DEFAULT_ORDER_PRESERVED));
+        } finally {
+            a.recycle();
+        }
+    }
+
+    // Implementation
+
+    /**
+     * Returns the current orientation.
+     *
+     * @return either {@link #HORIZONTAL} or {@link #VERTICAL}
+     *
+     * @see #setOrientation(int)
+     *
+     * @attr ref android.R.styleable#GridLayout_orientation
+     */
+    @Orientation
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    /**
+     *
+     * GridLayout uses the orientation property for two purposes:
+     * <ul>
+     *  <li>
+     *      To control the 'direction' in which default row/column indices are generated
+     *      when they are not specified in a component's layout parameters.
+     *  </li>
+     *  <li>
+     *      To control which axis should be processed first during the layout operation:
+     *      when orientation is {@link #HORIZONTAL} the horizontal axis is laid out first.
+     *  </li>
+     * </ul>
+     *
+     * The order in which axes are laid out is important if, for example, the height of
+     * one of GridLayout's children is dependent on its width - and its width is, in turn,
+     * dependent on the widths of other components.
+     * <p>
+     * If your layout contains a {@link TextView} (or derivative:
+     * {@code Button}, {@code EditText}, {@code CheckBox}, etc.) which is
+     * in multi-line mode (the default) it is normally best to leave GridLayout's
+     * orientation as {@code HORIZONTAL} - because {@code TextView} is capable of
+     * deriving its height for a given width, but not the other way around.
+     * <p>
+     * Other than the effects above, orientation does not affect the actual layout operation of
+     * GridLayout, so it's fine to leave GridLayout in {@code HORIZONTAL} mode even if
+     * the height of the intended layout greatly exceeds its width.
+     * <p>
+     * The default value of this property is {@link #HORIZONTAL}.
+     *
+     * @param orientation either {@link #HORIZONTAL} or {@link #VERTICAL}
+     *
+     * @see #getOrientation()
+     *
+     * @attr ref android.R.styleable#GridLayout_orientation
+     */
+    public void setOrientation(@Orientation int orientation) {
+        if (this.mOrientation != orientation) {
+            this.mOrientation = orientation;
+            invalidateStructure();
+            requestLayout();
+        }
+    }
+
+    /**
+     * Returns the current number of rows. This is either the last value that was set
+     * with {@link #setRowCount(int)} or, if no such value was set, the maximum
+     * value of each the upper bounds defined in {@link LayoutParams#rowSpec}.
+     *
+     * @return the current number of rows
+     *
+     * @see #setRowCount(int)
+     * @see LayoutParams#rowSpec
+     *
+     * @attr ref android.R.styleable#GridLayout_rowCount
+     */
+    public int getRowCount() {
+        return mVerticalAxis.getCount();
+    }
+
+    /**
+     * RowCount is used only to generate default row/column indices when
+     * they are not specified by a component's layout parameters.
+     *
+     * @param rowCount the number of rows
+     *
+     * @see #getRowCount()
+     * @see LayoutParams#rowSpec
+     *
+     * @attr ref android.R.styleable#GridLayout_rowCount
+     */
+    public void setRowCount(int rowCount) {
+        mVerticalAxis.setCount(rowCount);
+        invalidateStructure();
+        requestLayout();
+    }
+
+    /**
+     * Returns the current number of columns. This is either the last value that was set
+     * with {@link #setColumnCount(int)} or, if no such value was set, the maximum
+     * value of each the upper bounds defined in {@link LayoutParams#columnSpec}.
+     *
+     * @return the current number of columns
+     *
+     * @see #setColumnCount(int)
+     * @see LayoutParams#columnSpec
+     *
+     * @attr ref android.R.styleable#GridLayout_columnCount
+     */
+    public int getColumnCount() {
+        return mHorizontalAxis.getCount();
+    }
+
+    /**
+     * ColumnCount is used only to generate default column/column indices when
+     * they are not specified by a component's layout parameters.
+     *
+     * @param columnCount the number of columns.
+     *
+     * @see #getColumnCount()
+     * @see LayoutParams#columnSpec
+     *
+     * @attr ref android.R.styleable#GridLayout_columnCount
+     */
+    public void setColumnCount(int columnCount) {
+        mHorizontalAxis.setCount(columnCount);
+        invalidateStructure();
+        requestLayout();
+    }
+
+    /**
+     * Returns whether or not this GridLayout will allocate default margins when no
+     * corresponding layout parameters are defined.
+     *
+     * @return {@code true} if default margins should be allocated
+     *
+     * @see #setUseDefaultMargins(boolean)
+     *
+     * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+     */
+    public boolean getUseDefaultMargins() {
+        return mUseDefaultMargins;
+    }
+
+    /**
+     * When {@code true}, GridLayout allocates default margins around children
+     * based on the child's visual characteristics. Each of the
+     * margins so defined may be independently overridden by an assignment
+     * to the appropriate layout parameter.
+     * <p>
+     * When {@code false}, the default value of all margins is zero.
+     * <p>
+     * When setting to {@code true}, consider setting the value of the
+     * {@link #setAlignmentMode(int) alignmentMode}
+     * property to {@link #ALIGN_BOUNDS}.
+     * <p>
+     * The default value of this property is {@code false}.
+     *
+     * @param useDefaultMargins use {@code true} to make GridLayout allocate default margins
+     *
+     * @see #getUseDefaultMargins()
+     * @see #setAlignmentMode(int)
+     *
+     * @see MarginLayoutParams#leftMargin
+     * @see MarginLayoutParams#topMargin
+     * @see MarginLayoutParams#rightMargin
+     * @see MarginLayoutParams#bottomMargin
+     *
+     * @attr ref android.R.styleable#GridLayout_useDefaultMargins
+     */
+    public void setUseDefaultMargins(boolean useDefaultMargins) {
+        this.mUseDefaultMargins = useDefaultMargins;
+        requestLayout();
+    }
+
+    /**
+     * Returns the alignment mode.
+     *
+     * @return the alignment mode; either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
+     *
+     * @see #ALIGN_BOUNDS
+     * @see #ALIGN_MARGINS
+     *
+     * @see #setAlignmentMode(int)
+     *
+     * @attr ref android.R.styleable#GridLayout_alignmentMode
+     */
+    @AlignmentMode
+    public int getAlignmentMode() {
+        return mAlignmentMode;
+    }
+
+    /**
+     * Sets the alignment mode to be used for all of the alignments between the
+     * children of this container.
+     * <p>
+     * The default value of this property is {@link #ALIGN_MARGINS}.
+     *
+     * @param alignmentMode either {@link #ALIGN_BOUNDS} or {@link #ALIGN_MARGINS}
+     *
+     * @see #ALIGN_BOUNDS
+     * @see #ALIGN_MARGINS
+     *
+     * @see #getAlignmentMode()
+     *
+     * @attr ref android.R.styleable#GridLayout_alignmentMode
+     */
+    public void setAlignmentMode(@AlignmentMode int alignmentMode) {
+        this.mAlignmentMode = alignmentMode;
+        requestLayout();
+    }
+
+    /**
+     * Returns whether or not row boundaries are ordered by their grid indices.
+     *
+     * @return {@code true} if row boundaries must appear in the order of their indices,
+     *         {@code false} otherwise
+     *
+     * @see #setRowOrderPreserved(boolean)
+     *
+     * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+     */
+    public boolean isRowOrderPreserved() {
+        return mVerticalAxis.isOrderPreserved();
+    }
+
+    /**
+     * When this property is {@code true}, GridLayout is forced to place the row boundaries
+     * so that their associated grid indices are in ascending order in the view.
+     * <p>
+     * When this property is {@code false} GridLayout is at liberty to place the vertical row
+     * boundaries in whatever order best fits the given constraints.
+     * <p>
+     * The default value of this property is {@code true}.
+
+     * @param rowOrderPreserved {@code true} to force GridLayout to respect the order
+     *        of row boundaries
+     *
+     * @see #isRowOrderPreserved()
+     *
+     * @attr ref android.R.styleable#GridLayout_rowOrderPreserved
+     */
+    public void setRowOrderPreserved(boolean rowOrderPreserved) {
+        mVerticalAxis.setOrderPreserved(rowOrderPreserved);
+        invalidateStructure();
+        requestLayout();
+    }
+
+    /**
+     * Returns whether or not column boundaries are ordered by their grid indices.
+     *
+     * @return {@code true} if column boundaries must appear in the order of their indices,
+     *         {@code false} otherwise
+     *
+     * @see #setColumnOrderPreserved(boolean)
+     *
+     * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+     */
+    public boolean isColumnOrderPreserved() {
+        return mHorizontalAxis.isOrderPreserved();
+    }
+
+    /**
+     * When this property is {@code true}, GridLayout is forced to place the column boundaries
+     * so that their associated grid indices are in ascending order in the view.
+     * <p>
+     * When this property is {@code false} GridLayout is at liberty to place the horizontal column
+     * boundaries in whatever order best fits the given constraints.
+     * <p>
+     * The default value of this property is {@code true}.
+     *
+     * @param columnOrderPreserved use {@code true} to force GridLayout to respect the order
+     *        of column boundaries.
+     *
+     * @see #isColumnOrderPreserved()
+     *
+     * @attr ref android.R.styleable#GridLayout_columnOrderPreserved
+     */
+    public void setColumnOrderPreserved(boolean columnOrderPreserved) {
+        mHorizontalAxis.setOrderPreserved(columnOrderPreserved);
+        invalidateStructure();
+        requestLayout();
+    }
+
+    /**
+     * Return the printer that will log diagnostics from this layout.
+     *
+     * @see #setPrinter(android.util.Printer)
+     *
+     * @return the printer associated with this view
+     *
+     * @hide
+     */
+    public Printer getPrinter() {
+        return mPrinter;
+    }
+
+    /**
+     * Set the printer that will log diagnostics from this layout.
+     * The default value is created by {@link android.util.LogPrinter}.
+     *
+     * @param printer the printer associated with this layout
+     *
+     * @see #getPrinter()
+     *
+     * @hide
+     */
+    public void setPrinter(Printer printer) {
+        this.mPrinter = (printer == null) ? NO_PRINTER : printer;
+    }
+
+    // Static utility methods
+
+    static int max2(int[] a, int valueIfEmpty) {
+        int result = valueIfEmpty;
+        for (int i = 0, N = a.length; i < N; i++) {
+            result = Math.max(result, a[i]);
+        }
+        return result;
+    }
+
+    @SuppressWarnings("unchecked")
+    static <T> T[] append(T[] a, T[] b) {
+        T[] result = (T[]) Array.newInstance(a.getClass().getComponentType(), a.length + b.length);
+        System.arraycopy(a, 0, result, 0, a.length);
+        System.arraycopy(b, 0, result, a.length, b.length);
+        return result;
+    }
+
+    static Alignment getAlignment(int gravity, boolean horizontal) {
+        int mask = horizontal ? HORIZONTAL_GRAVITY_MASK : VERTICAL_GRAVITY_MASK;
+        int shift = horizontal ? AXIS_X_SHIFT : AXIS_Y_SHIFT;
+        int flags = (gravity & mask) >> shift;
+        switch (flags) {
+            case (AXIS_SPECIFIED | AXIS_PULL_BEFORE):
+                return horizontal ? LEFT : TOP;
+            case (AXIS_SPECIFIED | AXIS_PULL_AFTER):
+                return horizontal ? RIGHT : BOTTOM;
+            case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | AXIS_PULL_AFTER):
+                return FILL;
+            case AXIS_SPECIFIED:
+                return CENTER;
+            case (AXIS_SPECIFIED | AXIS_PULL_BEFORE | RELATIVE_LAYOUT_DIRECTION):
+                return START;
+            case (AXIS_SPECIFIED | AXIS_PULL_AFTER | RELATIVE_LAYOUT_DIRECTION):
+                return END;
+            default:
+                return UNDEFINED_ALIGNMENT;
+        }
+    }
+
+    /** @noinspection UnusedParameters*/
+    private int getDefaultMargin(View c, boolean horizontal, boolean leading) {
+        if (c.getClass() == Space.class) {
+            return 0;
+        }
+        return mDefaultGap / 2;
+    }
+
+    private int getDefaultMargin(View c, boolean isAtEdge, boolean horizontal, boolean leading) {
+        return /*isAtEdge ? DEFAULT_CONTAINER_MARGIN :*/ getDefaultMargin(c, horizontal, leading);
+    }
+
+    private int getDefaultMargin(View c, LayoutParams p, boolean horizontal, boolean leading) {
+        if (!mUseDefaultMargins) {
+            return 0;
+        }
+        Spec spec = horizontal ? p.columnSpec : p.rowSpec;
+        Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+        Interval span = spec.span;
+        boolean leading1 = (horizontal && isLayoutRtl()) ? !leading : leading;
+        boolean isAtEdge = leading1 ? (span.min == 0) : (span.max == axis.getCount());
+
+        return getDefaultMargin(c, isAtEdge, horizontal, leading);
+    }
+
+    int getMargin1(View view, boolean horizontal, boolean leading) {
+        LayoutParams lp = getLayoutParams(view);
+        int margin = horizontal ?
+                (leading ? lp.leftMargin : lp.rightMargin) :
+                (leading ? lp.topMargin : lp.bottomMargin);
+        return margin == UNDEFINED ? getDefaultMargin(view, lp, horizontal, leading) : margin;
+    }
+
+    private int getMargin(View view, boolean horizontal, boolean leading) {
+        if (mAlignmentMode == ALIGN_MARGINS) {
+            return getMargin1(view, horizontal, leading);
+        } else {
+            Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+            int[] margins = leading ? axis.getLeadingMargins() : axis.getTrailingMargins();
+            LayoutParams lp = getLayoutParams(view);
+            Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+            int index = leading ? spec.span.min : spec.span.max;
+            return margins[index];
+        }
+    }
+
+    private int getTotalMargin(View child, boolean horizontal) {
+        return getMargin(child, horizontal, true) + getMargin(child, horizontal, false);
+    }
+
+    private static boolean fits(int[] a, int value, int start, int end) {
+        if (end > a.length) {
+            return false;
+        }
+        for (int i = start; i < end; i++) {
+            if (a[i] > value) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private static void procrusteanFill(int[] a, int start, int end, int value) {
+        int length = a.length;
+        Arrays.fill(a, Math.min(start, length), Math.min(end, length), value);
+    }
+
+    private static void setCellGroup(LayoutParams lp, int row, int rowSpan, int col, int colSpan) {
+        lp.setRowSpecSpan(new Interval(row, row + rowSpan));
+        lp.setColumnSpecSpan(new Interval(col, col + colSpan));
+    }
+
+    // Logic to avert infinite loops by ensuring that the cells can be placed somewhere.
+    private static int clip(Interval minorRange, boolean minorWasDefined, int count) {
+        int size = minorRange.size();
+        if (count == 0) {
+            return size;
+        }
+        int min = minorWasDefined ? min(minorRange.min, count) : 0;
+        return min(size, count - min);
+    }
+
+    // install default indices for cells that don't define them
+    private void validateLayoutParams() {
+        final boolean horizontal = (mOrientation == HORIZONTAL);
+        final Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+        final int count = (axis.definedCount != UNDEFINED) ? axis.definedCount : 0;
+
+        int major = 0;
+        int minor = 0;
+        int[] maxSizes = new int[count];
+
+        for (int i = 0, N = getChildCount(); i < N; i++) {
+            LayoutParams lp = (LayoutParams) getChildAt(i).getLayoutParams();
+
+            final Spec majorSpec = horizontal ? lp.rowSpec : lp.columnSpec;
+            final Interval majorRange = majorSpec.span;
+            final boolean majorWasDefined = majorSpec.startDefined;
+            final int majorSpan = majorRange.size();
+            if (majorWasDefined) {
+                major = majorRange.min;
+            }
+
+            final Spec minorSpec = horizontal ? lp.columnSpec : lp.rowSpec;
+            final Interval minorRange = minorSpec.span;
+            final boolean minorWasDefined = minorSpec.startDefined;
+            final int minorSpan = clip(minorRange, minorWasDefined, count);
+            if (minorWasDefined) {
+                minor = minorRange.min;
+            }
+
+            if (count != 0) {
+                // Find suitable row/col values when at least one is undefined.
+                if (!majorWasDefined || !minorWasDefined) {
+                    while (!fits(maxSizes, major, minor, minor + minorSpan)) {
+                        if (minorWasDefined) {
+                            major++;
+                        } else {
+                            if (minor + minorSpan <= count) {
+                                minor++;
+                            } else {
+                                minor = 0;
+                                major++;
+                            }
+                        }
+                    }
+                }
+                procrusteanFill(maxSizes, minor, minor + minorSpan, major + majorSpan);
+            }
+
+            if (horizontal) {
+                setCellGroup(lp, major, majorSpan, minor, minorSpan);
+            } else {
+                setCellGroup(lp, minor, minorSpan, major, majorSpan);
+            }
+
+            minor = minor + minorSpan;
+        }
+    }
+
+    private void invalidateStructure() {
+        mLastLayoutParamsHashCode = UNINITIALIZED_HASH;
+        mHorizontalAxis.invalidateStructure();
+        mVerticalAxis.invalidateStructure();
+        // This can end up being done twice. Better twice than not at all.
+        invalidateValues();
+    }
+
+    private void invalidateValues() {
+        // Need null check because requestLayout() is called in View's initializer,
+        // before we are set up.
+        if (mHorizontalAxis != null && mVerticalAxis != null) {
+            mHorizontalAxis.invalidateValues();
+            mVerticalAxis.invalidateValues();
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void onSetLayoutParams(View child, ViewGroup.LayoutParams layoutParams) {
+        super.onSetLayoutParams(child, layoutParams);
+
+        if (!checkLayoutParams(layoutParams)) {
+            handleInvalidParams("supplied LayoutParams are of the wrong type");
+        }
+
+        invalidateStructure();
+    }
+
+    final LayoutParams getLayoutParams(View c) {
+        return (LayoutParams) c.getLayoutParams();
+    }
+
+    private static void handleInvalidParams(String msg) {
+        throw new IllegalArgumentException(msg + ". ");
+    }
+
+    private void checkLayoutParams(LayoutParams lp, boolean horizontal) {
+        String groupName = horizontal ? "column" : "row";
+        Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+        Interval span = spec.span;
+        if (span.min != UNDEFINED && span.min < 0) {
+            handleInvalidParams(groupName + " indices must be positive");
+        }
+        Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+        int count = axis.definedCount;
+        if (count != UNDEFINED) {
+            if (span.max > count) {
+                handleInvalidParams(groupName +
+                        " indices (start + span) mustn't exceed the " + groupName + " count");
+            }
+            if (span.size() > count) {
+                handleInvalidParams(groupName + " span mustn't exceed the " + groupName + " count");
+            }
+        }
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        if (!(p instanceof LayoutParams)) {
+            return false;
+        }
+        LayoutParams lp = (LayoutParams) p;
+
+        checkLayoutParams(lp, true);
+        checkLayoutParams(lp, false);
+
+        return true;
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams();
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (sPreserveMarginParamsInLayoutParamConversion) {
+            if (lp instanceof LayoutParams) {
+                return new LayoutParams((LayoutParams) lp);
+            } else if (lp instanceof MarginLayoutParams) {
+                return new LayoutParams((MarginLayoutParams) lp);
+            }
+        }
+        return new LayoutParams(lp);
+    }
+
+    // Draw grid
+
+    private void drawLine(Canvas graphics, int x1, int y1, int x2, int y2, Paint paint) {
+        if (isLayoutRtl()) {
+            int width = getWidth();
+            graphics.drawLine(width - x1, y1, width - x2, y2, paint);
+        } else {
+            graphics.drawLine(x1, y1, x2, y2, paint);
+        }
+    }
+
+    @Override
+    protected void onDebugDrawMargins(Canvas canvas, Paint paint) {
+        // Apply defaults, so as to remove UNDEFINED values
+        LayoutParams lp = new LayoutParams();
+        for (int i = 0; i < getChildCount(); i++) {
+            View c = getChildAt(i);
+            lp.setMargins(
+                    getMargin1(c, true, true),
+                    getMargin1(c, false, true),
+                    getMargin1(c, true, false),
+                    getMargin1(c, false, false));
+            lp.onDebugDraw(c, canvas, paint);
+        }
+    }
+
+    @Override
+    protected void onDebugDraw(Canvas canvas) {
+        Paint paint = new Paint();
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setColor(Color.argb(50, 255, 255, 255));
+
+        Insets insets = getOpticalInsets();
+
+        int top    =               getPaddingTop()    + insets.top;
+        int left   =               getPaddingLeft()   + insets.left;
+        int right  = getWidth()  - getPaddingRight()  - insets.right;
+        int bottom = getHeight() - getPaddingBottom() - insets.bottom;
+
+        int[] xs = mHorizontalAxis.locations;
+        if (xs != null) {
+            for (int i = 0, length = xs.length; i < length; i++) {
+                int x = left + xs[i];
+                drawLine(canvas, x, top, x, bottom, paint);
+            }
+        }
+
+        int[] ys = mVerticalAxis.locations;
+        if (ys != null) {
+            for (int i = 0, length = ys.length; i < length; i++) {
+                int y = top + ys[i];
+                drawLine(canvas, left, y, right, y, paint);
+            }
+        }
+
+        super.onDebugDraw(canvas);
+    }
+
+    @Override
+    public void onViewAdded(View child) {
+        super.onViewAdded(child);
+        invalidateStructure();
+    }
+
+    @Override
+    public void onViewRemoved(View child) {
+        super.onViewRemoved(child);
+        invalidateStructure();
+    }
+
+    /**
+     * We need to call invalidateStructure() when a child's GONE flag changes state.
+     * This implementation is a catch-all, invalidating on any change in the visibility flags.
+     *
+     * @hide
+     */
+    @Override
+    protected void onChildVisibilityChanged(View child, int oldVisibility, int newVisibility) {
+        super.onChildVisibilityChanged(child, oldVisibility, newVisibility);
+        if (oldVisibility == GONE || newVisibility == GONE) {
+        invalidateStructure();
+        }
+    }
+
+    private int computeLayoutParamsHashCode() {
+        int result = 1;
+        for (int i = 0, N = getChildCount(); i < N; i++) {
+            View c = getChildAt(i);
+            if (c.getVisibility() == View.GONE) continue;
+            LayoutParams lp = (LayoutParams) c.getLayoutParams();
+            result = 31 * result + lp.hashCode();
+        }
+        return result;
+    }
+
+    private void consistencyCheck() {
+        if (mLastLayoutParamsHashCode == UNINITIALIZED_HASH) {
+            validateLayoutParams();
+            mLastLayoutParamsHashCode = computeLayoutParamsHashCode();
+        } else if (mLastLayoutParamsHashCode != computeLayoutParamsHashCode()) {
+            mPrinter.println("The fields of some layout parameters were modified in between "
+                    + "layout operations. Check the javadoc for GridLayout.LayoutParams#rowSpec.");
+            invalidateStructure();
+            consistencyCheck();
+        }
+    }
+
+    // Measurement
+
+    // Note: padding has already been removed from the supplied specs
+    private void measureChildWithMargins2(View child, int parentWidthSpec, int parentHeightSpec,
+            int childWidth, int childHeight) {
+        int childWidthSpec = getChildMeasureSpec(parentWidthSpec,
+                getTotalMargin(child, true), childWidth);
+        int childHeightSpec = getChildMeasureSpec(parentHeightSpec,
+                getTotalMargin(child, false), childHeight);
+        child.measure(childWidthSpec, childHeightSpec);
+    }
+
+    // Note: padding has already been removed from the supplied specs
+    private void measureChildrenWithMargins(int widthSpec, int heightSpec, boolean firstPass) {
+        for (int i = 0, N = getChildCount(); i < N; i++) {
+            View c = getChildAt(i);
+            if (c.getVisibility() == View.GONE) continue;
+            LayoutParams lp = getLayoutParams(c);
+            if (firstPass) {
+                measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, lp.height);
+            } else {
+                boolean horizontal = (mOrientation == HORIZONTAL);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                if (spec.getAbsoluteAlignment(horizontal) == FILL) {
+                    Interval span = spec.span;
+                    Axis axis = horizontal ? mHorizontalAxis : mVerticalAxis;
+                    int[] locations = axis.getLocations();
+                    int cellSize = locations[span.max] - locations[span.min];
+                    int viewSize = cellSize - getTotalMargin(c, horizontal);
+                    if (horizontal) {
+                        measureChildWithMargins2(c, widthSpec, heightSpec, viewSize, lp.height);
+                    } else {
+                        measureChildWithMargins2(c, widthSpec, heightSpec, lp.width, viewSize);
+                    }
+                }
+            }
+        }
+    }
+
+    static int adjust(int measureSpec, int delta) {
+        return makeMeasureSpec(
+                MeasureSpec.getSize(measureSpec + delta),  MeasureSpec.getMode(measureSpec));
+    }
+
+    @Override
+    protected void onMeasure(int widthSpec, int heightSpec) {
+        consistencyCheck();
+
+        /** If we have been called by {@link View#measure(int, int)}, one of width or height
+         *  is  likely to have changed. We must invalidate if so. */
+        invalidateValues();
+
+        int hPadding = getPaddingLeft() + getPaddingRight();
+        int vPadding = getPaddingTop()  + getPaddingBottom();
+
+        int widthSpecSansPadding =  adjust( widthSpec, -hPadding);
+        int heightSpecSansPadding = adjust(heightSpec, -vPadding);
+
+        measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, true);
+
+        int widthSansPadding;
+        int heightSansPadding;
+
+        // Use the orientation property to decide which axis should be laid out first.
+        if (mOrientation == HORIZONTAL) {
+            widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
+            measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
+            heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
+        } else {
+            heightSansPadding = mVerticalAxis.getMeasure(heightSpecSansPadding);
+            measureChildrenWithMargins(widthSpecSansPadding, heightSpecSansPadding, false);
+            widthSansPadding = mHorizontalAxis.getMeasure(widthSpecSansPadding);
+        }
+
+        int measuredWidth  = Math.max(widthSansPadding  + hPadding, getSuggestedMinimumWidth());
+        int measuredHeight = Math.max(heightSansPadding + vPadding, getSuggestedMinimumHeight());
+
+        setMeasuredDimension(
+                resolveSizeAndState(measuredWidth,   widthSpec, 0),
+                resolveSizeAndState(measuredHeight, heightSpec, 0));
+    }
+
+    private int getMeasurement(View c, boolean horizontal) {
+        return horizontal ? c.getMeasuredWidth() : c.getMeasuredHeight();
+    }
+
+    final int getMeasurementIncludingMargin(View c, boolean horizontal) {
+        if (c.getVisibility() == View.GONE) {
+            return 0;
+        }
+        return getMeasurement(c, horizontal) + getTotalMargin(c, horizontal);
+    }
+
+    @Override
+    public void requestLayout() {
+        super.requestLayout();
+        invalidateValues();
+    }
+
+    // Layout container
+
+    /**
+     * {@inheritDoc}
+     */
+    /*
+     The layout operation is implemented by delegating the heavy lifting to the
+     to the mHorizontalAxis and mVerticalAxis instances of the internal Axis class.
+     Together they compute the locations of the vertical and horizontal lines of
+     the grid (respectively!).
+
+     This method is then left with the simpler task of applying margins, gravity
+     and sizing to each child view and then placing it in its cell.
+     */
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        consistencyCheck();
+
+        int targetWidth = right - left;
+        int targetHeight = bottom - top;
+
+        int paddingLeft = getPaddingLeft();
+        int paddingTop = getPaddingTop();
+        int paddingRight = getPaddingRight();
+        int paddingBottom = getPaddingBottom();
+
+        mHorizontalAxis.layout(targetWidth - paddingLeft - paddingRight);
+        mVerticalAxis.layout(targetHeight - paddingTop - paddingBottom);
+
+        int[] hLocations = mHorizontalAxis.getLocations();
+        int[] vLocations = mVerticalAxis.getLocations();
+
+        for (int i = 0, N = getChildCount(); i < N; i++) {
+            View c = getChildAt(i);
+            if (c.getVisibility() == View.GONE) continue;
+            LayoutParams lp = getLayoutParams(c);
+            Spec columnSpec = lp.columnSpec;
+            Spec rowSpec = lp.rowSpec;
+
+            Interval colSpan = columnSpec.span;
+            Interval rowSpan = rowSpec.span;
+
+            int x1 = hLocations[colSpan.min];
+            int y1 = vLocations[rowSpan.min];
+
+            int x2 = hLocations[colSpan.max];
+            int y2 = vLocations[rowSpan.max];
+
+            int cellWidth = x2 - x1;
+            int cellHeight = y2 - y1;
+
+            int pWidth = getMeasurement(c, true);
+            int pHeight = getMeasurement(c, false);
+
+            Alignment hAlign = columnSpec.getAbsoluteAlignment(true);
+            Alignment vAlign = rowSpec.getAbsoluteAlignment(false);
+
+            Bounds boundsX = mHorizontalAxis.getGroupBounds().getValue(i);
+            Bounds boundsY = mVerticalAxis.getGroupBounds().getValue(i);
+
+            // Gravity offsets: the location of the alignment group relative to its cell group.
+            int gravityOffsetX = hAlign.getGravityOffset(c, cellWidth - boundsX.size(true));
+            int gravityOffsetY = vAlign.getGravityOffset(c, cellHeight - boundsY.size(true));
+
+            int leftMargin = getMargin(c, true, true);
+            int topMargin = getMargin(c, false, true);
+            int rightMargin = getMargin(c, true, false);
+            int bottomMargin = getMargin(c, false, false);
+
+            int sumMarginsX = leftMargin + rightMargin;
+            int sumMarginsY = topMargin + bottomMargin;
+
+            // Alignment offsets: the location of the view relative to its alignment group.
+            int alignmentOffsetX = boundsX.getOffset(this, c, hAlign, pWidth + sumMarginsX, true);
+            int alignmentOffsetY = boundsY.getOffset(this, c, vAlign, pHeight + sumMarginsY, false);
+
+            int width = hAlign.getSizeInCell(c, pWidth, cellWidth - sumMarginsX);
+            int height = vAlign.getSizeInCell(c, pHeight, cellHeight - sumMarginsY);
+
+            int dx = x1 + gravityOffsetX + alignmentOffsetX;
+
+            int cx = !isLayoutRtl() ? paddingLeft + leftMargin + dx :
+                    targetWidth - width - paddingRight - rightMargin - dx;
+            int cy = paddingTop + y1 + gravityOffsetY + alignmentOffsetY + topMargin;
+
+            if (width != c.getMeasuredWidth() || height != c.getMeasuredHeight()) {
+                c.measure(makeMeasureSpec(width, EXACTLY), makeMeasureSpec(height, EXACTLY));
+            }
+            c.layout(cx, cy, cx + width, cy + height);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return GridLayout.class.getName();
+    }
+
+    // Inner classes
+
+    /*
+     This internal class houses the algorithm for computing the locations of grid lines;
+     along either the horizontal or vertical axis. A GridLayout uses two instances of this class -
+     distinguished by the "horizontal" flag which is true for the horizontal axis and false
+     for the vertical one.
+     */
+    final class Axis {
+        private static final int NEW = 0;
+        private static final int PENDING = 1;
+        private static final int COMPLETE = 2;
+
+        public final boolean horizontal;
+
+        public int definedCount = UNDEFINED;
+        private int maxIndex = UNDEFINED;
+
+        PackedMap<Spec, Bounds> groupBounds;
+        public boolean groupBoundsValid = false;
+
+        PackedMap<Interval, MutableInt> forwardLinks;
+        public boolean forwardLinksValid = false;
+
+        PackedMap<Interval, MutableInt> backwardLinks;
+        public boolean backwardLinksValid = false;
+
+        public int[] leadingMargins;
+        public boolean leadingMarginsValid = false;
+
+        public int[] trailingMargins;
+        public boolean trailingMarginsValid = false;
+
+        public Arc[] arcs;
+        public boolean arcsValid = false;
+
+        public int[] locations;
+        public boolean locationsValid = false;
+
+        public boolean hasWeights;
+        public boolean hasWeightsValid = false;
+        public int[] deltas;
+
+        boolean orderPreserved = DEFAULT_ORDER_PRESERVED;
+
+        private MutableInt parentMin = new MutableInt(0);
+        private MutableInt parentMax = new MutableInt(-MAX_SIZE);
+
+        private Axis(boolean horizontal) {
+            this.horizontal = horizontal;
+        }
+
+        private int calculateMaxIndex() {
+            // the number Integer.MIN_VALUE + 1 comes up in undefined cells
+            int result = -1;
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                View c = getChildAt(i);
+                LayoutParams params = getLayoutParams(c);
+                Spec spec = horizontal ? params.columnSpec : params.rowSpec;
+                Interval span = spec.span;
+                result = max(result, span.min);
+                result = max(result, span.max);
+                result = max(result, span.size());
+            }
+            return result == -1 ? UNDEFINED : result;
+        }
+
+        private int getMaxIndex() {
+            if (maxIndex == UNDEFINED) {
+                maxIndex = max(0, calculateMaxIndex()); // use zero when there are no children
+            }
+            return maxIndex;
+        }
+
+        public int getCount() {
+            return max(definedCount, getMaxIndex());
+        }
+
+        public void setCount(int count) {
+            if (count != UNDEFINED && count < getMaxIndex()) {
+                handleInvalidParams((horizontal ? "column" : "row") +
+                        "Count must be greater than or equal to the maximum of all grid indices " +
+                        "(and spans) defined in the LayoutParams of each child");
+            }
+            this.definedCount = count;
+        }
+
+        public boolean isOrderPreserved() {
+            return orderPreserved;
+        }
+
+        public void setOrderPreserved(boolean orderPreserved) {
+            this.orderPreserved = orderPreserved;
+            invalidateStructure();
+        }
+
+        private PackedMap<Spec, Bounds> createGroupBounds() {
+            Assoc<Spec, Bounds> assoc = Assoc.of(Spec.class, Bounds.class);
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                View c = getChildAt(i);
+                // we must include views that are GONE here, see introductory javadoc
+                LayoutParams lp = getLayoutParams(c);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                Bounds bounds = spec.getAbsoluteAlignment(horizontal).getBounds();
+                assoc.put(spec, bounds);
+            }
+            return assoc.pack();
+        }
+
+        private void computeGroupBounds() {
+            Bounds[] values = groupBounds.values;
+            for (int i = 0; i < values.length; i++) {
+                values[i].reset();
+            }
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                View c = getChildAt(i);
+                // we must include views that are GONE here, see introductory javadoc
+                LayoutParams lp = getLayoutParams(c);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                int size = getMeasurementIncludingMargin(c, horizontal) +
+                        ((spec.weight == 0) ? 0 : getDeltas()[i]);
+                groupBounds.getValue(i).include(GridLayout.this, c, spec, this, size);
+            }
+        }
+
+        public PackedMap<Spec, Bounds> getGroupBounds() {
+            if (groupBounds == null) {
+                groupBounds = createGroupBounds();
+            }
+            if (!groupBoundsValid) {
+                computeGroupBounds();
+                groupBoundsValid = true;
+            }
+            return groupBounds;
+        }
+
+        // Add values computed by alignment - taking the max of all alignments in each span
+        private PackedMap<Interval, MutableInt> createLinks(boolean min) {
+            Assoc<Interval, MutableInt> result = Assoc.of(Interval.class, MutableInt.class);
+            Spec[] keys = getGroupBounds().keys;
+            for (int i = 0, N = keys.length; i < N; i++) {
+                Interval span = min ? keys[i].span : keys[i].span.inverse();
+                result.put(span, new MutableInt());
+            }
+            return result.pack();
+        }
+
+        private void computeLinks(PackedMap<Interval, MutableInt> links, boolean min) {
+            MutableInt[] spans = links.values;
+            for (int i = 0; i < spans.length; i++) {
+                spans[i].reset();
+            }
+
+            // Use getter to trigger a re-evaluation
+            Bounds[] bounds = getGroupBounds().values;
+            for (int i = 0; i < bounds.length; i++) {
+                int size = bounds[i].size(min);
+                MutableInt valueHolder = links.getValue(i);
+                // this effectively takes the max() of the minima and the min() of the maxima
+                valueHolder.value = max(valueHolder.value, min ? size : -size);
+            }
+        }
+
+        private PackedMap<Interval, MutableInt> getForwardLinks() {
+            if (forwardLinks == null) {
+                forwardLinks = createLinks(true);
+            }
+            if (!forwardLinksValid) {
+                computeLinks(forwardLinks, true);
+                forwardLinksValid = true;
+            }
+            return forwardLinks;
+        }
+
+        private PackedMap<Interval, MutableInt> getBackwardLinks() {
+            if (backwardLinks == null) {
+                backwardLinks = createLinks(false);
+            }
+            if (!backwardLinksValid) {
+                computeLinks(backwardLinks, false);
+                backwardLinksValid = true;
+            }
+            return backwardLinks;
+        }
+
+        private void include(List<Arc> arcs, Interval key, MutableInt size,
+                boolean ignoreIfAlreadyPresent) {
+            /*
+            Remove self referential links.
+            These appear:
+                . as parental constraints when GridLayout has no children
+                . when components have been marked as GONE
+            */
+            if (key.size() == 0) {
+                return;
+            }
+            // this bit below should really be computed outside here -
+            // its just to stop default (row/col > 0) constraints obliterating valid entries
+            if (ignoreIfAlreadyPresent) {
+                for (Arc arc : arcs) {
+                    Interval span = arc.span;
+                    if (span.equals(key)) {
+                        return;
+                    }
+                }
+            }
+            arcs.add(new Arc(key, size));
+        }
+
+        private void include(List<Arc> arcs, Interval key, MutableInt size) {
+            include(arcs, key, size, true);
+        }
+
+        // Group arcs by their first vertex, returning an array of arrays.
+        // This is linear in the number of arcs.
+        Arc[][] groupArcsByFirstVertex(Arc[] arcs) {
+            int N = getCount() + 1; // the number of vertices
+            Arc[][] result = new Arc[N][];
+            int[] sizes = new int[N];
+            for (Arc arc : arcs) {
+                sizes[arc.span.min]++;
+            }
+            for (int i = 0; i < sizes.length; i++) {
+                result[i] = new Arc[sizes[i]];
+            }
+            // reuse the sizes array to hold the current last elements as we insert each arc
+            Arrays.fill(sizes, 0);
+            for (Arc arc : arcs) {
+                int i = arc.span.min;
+                result[i][sizes[i]++] = arc;
+            }
+
+            return result;
+        }
+
+        private Arc[] topologicalSort(final Arc[] arcs) {
+            return new Object() {
+                Arc[] result = new Arc[arcs.length];
+                int cursor = result.length - 1;
+                Arc[][] arcsByVertex = groupArcsByFirstVertex(arcs);
+                int[] visited = new int[getCount() + 1];
+
+                void walk(int loc) {
+                    switch (visited[loc]) {
+                        case NEW: {
+                            visited[loc] = PENDING;
+                            for (Arc arc : arcsByVertex[loc]) {
+                                walk(arc.span.max);
+                                result[cursor--] = arc;
+                            }
+                            visited[loc] = COMPLETE;
+                            break;
+                        }
+                        case PENDING: {
+                            // le singe est dans l'arbre
+                            assert false;
+                            break;
+                        }
+                        case COMPLETE: {
+                            break;
+                        }
+                    }
+                }
+
+                Arc[] sort() {
+                    for (int loc = 0, N = arcsByVertex.length; loc < N; loc++) {
+                        walk(loc);
+                    }
+                    assert cursor == -1;
+                    return result;
+                }
+            }.sort();
+        }
+
+        private Arc[] topologicalSort(List<Arc> arcs) {
+            return topologicalSort(arcs.toArray(new Arc[arcs.size()]));
+        }
+
+        private void addComponentSizes(List<Arc> result, PackedMap<Interval, MutableInt> links) {
+            for (int i = 0; i < links.keys.length; i++) {
+                Interval key = links.keys[i];
+                include(result, key, links.values[i], false);
+            }
+        }
+
+        private Arc[] createArcs() {
+            List<Arc> mins = new ArrayList<Arc>();
+            List<Arc> maxs = new ArrayList<Arc>();
+
+            // Add the minimum values from the components.
+            addComponentSizes(mins, getForwardLinks());
+            // Add the maximum values from the components.
+            addComponentSizes(maxs, getBackwardLinks());
+
+            // Add ordering constraints to prevent row/col sizes from going negative
+            if (orderPreserved) {
+                // Add a constraint for every row/col
+                for (int i = 0; i < getCount(); i++) {
+                    include(mins, new Interval(i, i + 1), new MutableInt(0));
+                }
+            }
+
+            // Add the container constraints. Use the version of include that allows
+            // duplicate entries in case a child spans the entire grid.
+            int N = getCount();
+            include(mins, new Interval(0, N), parentMin, false);
+            include(maxs, new Interval(N, 0), parentMax, false);
+
+            // Sort
+            Arc[] sMins = topologicalSort(mins);
+            Arc[] sMaxs = topologicalSort(maxs);
+
+            return append(sMins, sMaxs);
+        }
+
+        private void computeArcs() {
+            // getting the links validates the values that are shared by the arc list
+            getForwardLinks();
+            getBackwardLinks();
+        }
+
+        public Arc[] getArcs() {
+            if (arcs == null) {
+                arcs = createArcs();
+            }
+            if (!arcsValid) {
+                computeArcs();
+                arcsValid = true;
+            }
+            return arcs;
+        }
+
+        private boolean relax(int[] locations, Arc entry) {
+            if (!entry.valid) {
+                return false;
+            }
+            Interval span = entry.span;
+            int u = span.min;
+            int v = span.max;
+            int value = entry.value.value;
+            int candidate = locations[u] + value;
+            if (candidate > locations[v]) {
+                locations[v] = candidate;
+                return true;
+            }
+            return false;
+        }
+
+        private void init(int[] locations) {
+            Arrays.fill(locations, 0);
+        }
+
+        private String arcsToString(List<Arc> arcs) {
+            String var = horizontal ? "x" : "y";
+            StringBuilder result = new StringBuilder();
+            boolean first = true;
+            for (Arc arc : arcs) {
+                if (first) {
+                    first = false;
+                } else {
+                    result = result.append(", ");
+                }
+                int src = arc.span.min;
+                int dst = arc.span.max;
+                int value = arc.value.value;
+                result.append((src < dst) ?
+                        var + dst + "-" + var + src + ">=" + value :
+                        var + src + "-" + var + dst + "<=" + -value);
+
+            }
+            return result.toString();
+        }
+
+        private void logError(String axisName, Arc[] arcs, boolean[] culprits0) {
+            List<Arc> culprits = new ArrayList<Arc>();
+            List<Arc> removed = new ArrayList<Arc>();
+            for (int c = 0; c < arcs.length; c++) {
+                Arc arc = arcs[c];
+                if (culprits0[c]) {
+                    culprits.add(arc);
+                }
+                if (!arc.valid) {
+                    removed.add(arc);
+                }
+            }
+            mPrinter.println(axisName + " constraints: " + arcsToString(culprits) +
+                    " are inconsistent; permanently removing: " + arcsToString(removed) + ". ");
+        }
+
+        /*
+        Bellman-Ford variant - modified to reduce typical running time from O(N^2) to O(N)
+
+        GridLayout converts its requirements into a system of linear constraints of the
+        form:
+
+        x[i] - x[j] < a[k]
+
+        Where the x[i] are variables and the a[k] are constants.
+
+        For example, if the variables were instead labeled x, y, z we might have:
+
+            x - y < 17
+            y - z < 23
+            z - x < 42
+
+        This is a special case of the Linear Programming problem that is, in turn,
+        equivalent to the single-source shortest paths problem on a digraph, for
+        which the O(n^2) Bellman-Ford algorithm the most commonly used general solution.
+        */
+        private boolean solve(Arc[] arcs, int[] locations) {
+            return solve(arcs, locations, true);
+        }
+
+        private boolean solve(Arc[] arcs, int[] locations, boolean modifyOnError) {
+            String axisName = horizontal ? "horizontal" : "vertical";
+            int N = getCount() + 1; // The number of vertices is the number of columns/rows + 1.
+            boolean[] originalCulprits = null;
+
+            for (int p = 0; p < arcs.length; p++) {
+                init(locations);
+
+                // We take one extra pass over traditional Bellman-Ford (and omit their final step)
+                for (int i = 0; i < N; i++) {
+                    boolean changed = false;
+                    for (int j = 0, length = arcs.length; j < length; j++) {
+                        changed |= relax(locations, arcs[j]);
+                    }
+                    if (!changed) {
+                        if (originalCulprits != null) {
+                            logError(axisName, arcs, originalCulprits);
+                        }
+                        return true;
+                    }
+                }
+
+                if (!modifyOnError) {
+                    return false; // cannot solve with these constraints
+                }
+
+                boolean[] culprits = new boolean[arcs.length];
+                for (int i = 0; i < N; i++) {
+                    for (int j = 0, length = arcs.length; j < length; j++) {
+                        culprits[j] |= relax(locations, arcs[j]);
+                    }
+                }
+
+                if (p == 0) {
+                    originalCulprits = culprits;
+                }
+
+                for (int i = 0; i < arcs.length; i++) {
+                    if (culprits[i]) {
+                        Arc arc = arcs[i];
+                        // Only remove max values, min values alone cannot be inconsistent
+                        if (arc.span.min < arc.span.max) {
+                            continue;
+                        }
+                        arc.valid = false;
+                        break;
+                    }
+                }
+            }
+            return true;
+        }
+
+        private void computeMargins(boolean leading) {
+            int[] margins = leading ? leadingMargins : trailingMargins;
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                View c = getChildAt(i);
+                if (c.getVisibility() == View.GONE) continue;
+                LayoutParams lp = getLayoutParams(c);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                Interval span = spec.span;
+                int index = leading ? span.min : span.max;
+                margins[index] = max(margins[index], getMargin1(c, horizontal, leading));
+            }
+        }
+
+        // External entry points
+
+        public int[] getLeadingMargins() {
+            if (leadingMargins == null) {
+                leadingMargins = new int[getCount() + 1];
+            }
+            if (!leadingMarginsValid) {
+                computeMargins(true);
+                leadingMarginsValid = true;
+            }
+            return leadingMargins;
+        }
+
+        public int[] getTrailingMargins() {
+            if (trailingMargins == null) {
+                trailingMargins = new int[getCount() + 1];
+            }
+            if (!trailingMarginsValid) {
+                computeMargins(false);
+                trailingMarginsValid = true;
+            }
+            return trailingMargins;
+        }
+
+        private boolean solve(int[] a) {
+            return solve(getArcs(), a);
+        }
+
+        private boolean computeHasWeights() {
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() == View.GONE) {
+                    continue;
+                }
+                LayoutParams lp = getLayoutParams(child);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                if (spec.weight != 0) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private boolean hasWeights() {
+            if (!hasWeightsValid) {
+                hasWeights = computeHasWeights();
+                hasWeightsValid = true;
+            }
+            return hasWeights;
+        }
+
+        public int[] getDeltas() {
+            if (deltas == null) {
+                deltas = new int[getChildCount()];
+            }
+            return deltas;
+        }
+
+        private void shareOutDelta(int totalDelta, float totalWeight) {
+            Arrays.fill(deltas, 0);
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                final View c = getChildAt(i);
+                if (c.getVisibility() == View.GONE) {
+                    continue;
+                }
+                LayoutParams lp = getLayoutParams(c);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                float weight = spec.weight;
+                if (weight != 0) {
+                    int delta = Math.round((weight * totalDelta / totalWeight));
+                    deltas[i] = delta;
+                    // the two adjustments below are to counter the above rounding and avoid
+                    // off-by-ones at the end
+                    totalDelta -= delta;
+                    totalWeight -= weight;
+                }
+            }
+        }
+
+        private void solveAndDistributeSpace(int[] a) {
+            Arrays.fill(getDeltas(), 0);
+            solve(a);
+            int deltaMax = parentMin.value * getChildCount() + 1; //exclusive
+            if (deltaMax < 2) {
+                return; //don't have any delta to distribute
+            }
+            int deltaMin = 0; //inclusive
+
+            float totalWeight = calculateTotalWeight();
+
+            int validDelta = -1; //delta for which a solution exists
+            boolean validSolution = true;
+            // do a binary search to find the max delta that won't conflict with constraints
+            while(deltaMin < deltaMax) {
+                // cast to long to prevent overflow.
+                final int delta = (int) (((long) deltaMin + deltaMax) / 2);
+                invalidateValues();
+                shareOutDelta(delta, totalWeight);
+                validSolution = solve(getArcs(), a, false);
+                if (validSolution) {
+                    validDelta = delta;
+                    deltaMin = delta + 1;
+                } else {
+                    deltaMax = delta;
+                }
+            }
+            if (validDelta > 0 && !validSolution) {
+                // last solution was not successful but we have a successful one. Use it.
+                invalidateValues();
+                shareOutDelta(validDelta, totalWeight);
+                solve(a);
+            }
+        }
+
+        private float calculateTotalWeight() {
+            float totalWeight = 0f;
+            for (int i = 0, N = getChildCount(); i < N; i++) {
+                View c = getChildAt(i);
+                if (c.getVisibility() == View.GONE) {
+                    continue;
+                }
+                LayoutParams lp = getLayoutParams(c);
+                Spec spec = horizontal ? lp.columnSpec : lp.rowSpec;
+                totalWeight += spec.weight;
+            }
+            return totalWeight;
+        }
+
+        private void computeLocations(int[] a) {
+            if (!hasWeights()) {
+                solve(a);
+            } else {
+                solveAndDistributeSpace(a);
+            }
+            if (!orderPreserved) {
+                // Solve returns the smallest solution to the constraint system for which all
+                // values are positive. One value is therefore zero - though if the row/col
+                // order is not preserved this may not be the first vertex. For consistency,
+                // translate all the values so that they measure the distance from a[0]; the
+                // leading edge of the parent. After this transformation some values may be
+                // negative.
+                int a0 = a[0];
+                for (int i = 0, N = a.length; i < N; i++) {
+                    a[i] = a[i] - a0;
+                }
+            }
+        }
+
+        public int[] getLocations() {
+            if (locations == null) {
+                int N = getCount() + 1;
+                locations = new int[N];
+            }
+            if (!locationsValid) {
+                computeLocations(locations);
+                locationsValid = true;
+            }
+            return locations;
+        }
+
+        private int size(int[] locations) {
+            // The parental edges are attached to vertices 0 and N - even when order is not
+            // being preserved and other vertices fall outside this range. Measure the distance
+            // between vertices 0 and N, assuming that locations[0] = 0.
+            return locations[getCount()];
+        }
+
+        private void setParentConstraints(int min, int max) {
+            parentMin.value = min;
+            parentMax.value = -max;
+            locationsValid = false;
+        }
+
+        private int getMeasure(int min, int max) {
+            setParentConstraints(min, max);
+            return size(getLocations());
+        }
+
+        public int getMeasure(int measureSpec) {
+            int mode = MeasureSpec.getMode(measureSpec);
+            int size = MeasureSpec.getSize(measureSpec);
+            switch (mode) {
+                case MeasureSpec.UNSPECIFIED: {
+                    return getMeasure(0, MAX_SIZE);
+                }
+                case MeasureSpec.EXACTLY: {
+                    return getMeasure(size, size);
+                }
+                case MeasureSpec.AT_MOST: {
+                    return getMeasure(0, size);
+                }
+                default: {
+                    assert false;
+                    return 0;
+                }
+            }
+        }
+
+        public void layout(int size) {
+            setParentConstraints(size, size);
+            getLocations();
+        }
+
+        public void invalidateStructure() {
+            maxIndex = UNDEFINED;
+
+            groupBounds = null;
+            forwardLinks = null;
+            backwardLinks = null;
+
+            leadingMargins = null;
+            trailingMargins = null;
+            arcs = null;
+
+            locations = null;
+
+            deltas = null;
+            hasWeightsValid = false;
+
+            invalidateValues();
+        }
+
+        public void invalidateValues() {
+            groupBoundsValid = false;
+            forwardLinksValid = false;
+            backwardLinksValid = false;
+
+            leadingMarginsValid = false;
+            trailingMarginsValid = false;
+            arcsValid = false;
+
+            locationsValid = false;
+        }
+    }
+
+    /**
+     * Layout information associated with each of the children of a GridLayout.
+     * <p>
+     * GridLayout supports both row and column spanning and arbitrary forms of alignment within
+     * each cell group. The fundamental parameters associated with each cell group are
+     * gathered into their vertical and horizontal components and stored
+     * in the {@link #rowSpec} and {@link #columnSpec} layout parameters.
+     * {@link GridLayout.Spec Specs} are immutable structures
+     * and may be shared between the layout parameters of different children.
+     * <p>
+     * The row and column specs contain the leading and trailing indices along each axis
+     * and together specify the four grid indices that delimit the cells of this cell group.
+     * <p>
+     * The  alignment properties of the row and column specs together specify
+     * both aspects of alignment within the cell group. It is also possible to specify a child's
+     * alignment within its cell group by using the {@link GridLayout.LayoutParams#setGravity(int)}
+     * method.
+     * <p>
+     * The weight property is also included in Spec and specifies the proportion of any
+     * excess space that is due to the associated view.
+     *
+     * <h4>WRAP_CONTENT and MATCH_PARENT</h4>
+     *
+     * Because the default values of the {@link #width} and {@link #height}
+     * properties are both {@link #WRAP_CONTENT}, this value never needs to be explicitly
+     * declared in the layout parameters of GridLayout's children. In addition,
+     * GridLayout does not distinguish the special size value {@link #MATCH_PARENT} from
+     * {@link #WRAP_CONTENT}. A component's ability to expand to the size of the parent is
+     * instead controlled by the principle of <em>flexibility</em>,
+     * as discussed in {@link GridLayout}.
+     *
+     * <h4>Summary</h4>
+     *
+     * You should not need to use either of the special size values:
+     * {@code WRAP_CONTENT} or {@code MATCH_PARENT} when configuring the children of
+     * a GridLayout.
+     *
+     * <h4>Default values</h4>
+     *
+     * <ul>
+     *     <li>{@link #width} = {@link #WRAP_CONTENT}</li>
+     *     <li>{@link #height} = {@link #WRAP_CONTENT}</li>
+     *     <li>{@link #topMargin} = 0 when
+     *          {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+     *          {@code false}; otherwise {@link #UNDEFINED}, to
+     *          indicate that a default value should be computed on demand. </li>
+     *     <li>{@link #leftMargin} = 0 when
+     *          {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+     *          {@code false}; otherwise {@link #UNDEFINED}, to
+     *          indicate that a default value should be computed on demand. </li>
+     *     <li>{@link #bottomMargin} = 0 when
+     *          {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+     *          {@code false}; otherwise {@link #UNDEFINED}, to
+     *          indicate that a default value should be computed on demand. </li>
+     *     <li>{@link #rightMargin} = 0 when
+     *          {@link GridLayout#setUseDefaultMargins(boolean) useDefaultMargins} is
+     *          {@code false}; otherwise {@link #UNDEFINED}, to
+     *          indicate that a default value should be computed on demand. </li>
+     *     <li>{@link #rowSpec}<code>.row</code> = {@link #UNDEFINED} </li>
+     *     <li>{@link #rowSpec}<code>.rowSpan</code> = 1 </li>
+     *     <li>{@link #rowSpec}<code>.alignment</code> = {@link #BASELINE} </li>
+     *     <li>{@link #rowSpec}<code>.weight</code> = 0 </li>
+     *     <li>{@link #columnSpec}<code>.column</code> = {@link #UNDEFINED} </li>
+     *     <li>{@link #columnSpec}<code>.columnSpan</code> = 1 </li>
+     *     <li>{@link #columnSpec}<code>.alignment</code> = {@link #START} </li>
+     *     <li>{@link #columnSpec}<code>.weight</code> = 0 </li>
+     * </ul>
+     *
+     * See {@link GridLayout} for a more complete description of the conventions
+     * used by GridLayout in the interpretation of the properties of this class.
+     *
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_row
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_rowSpan
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_rowWeight
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_column
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_columnSpan
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_columnWeight
+     * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity
+     */
+    public static class LayoutParams extends MarginLayoutParams {
+
+        // Default values
+
+        private static final int DEFAULT_WIDTH = WRAP_CONTENT;
+        private static final int DEFAULT_HEIGHT = WRAP_CONTENT;
+        private static final int DEFAULT_MARGIN = UNDEFINED;
+        private static final int DEFAULT_ROW = UNDEFINED;
+        private static final int DEFAULT_COLUMN = UNDEFINED;
+        private static final Interval DEFAULT_SPAN = new Interval(UNDEFINED, UNDEFINED + 1);
+        private static final int DEFAULT_SPAN_SIZE = DEFAULT_SPAN.size();
+
+        // TypedArray indices
+
+        private static final int MARGIN = R.styleable.ViewGroup_MarginLayout_layout_margin;
+        private static final int LEFT_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginLeft;
+        private static final int TOP_MARGIN = R.styleable.ViewGroup_MarginLayout_layout_marginTop;
+        private static final int RIGHT_MARGIN =
+                R.styleable.ViewGroup_MarginLayout_layout_marginRight;
+        private static final int BOTTOM_MARGIN =
+                R.styleable.ViewGroup_MarginLayout_layout_marginBottom;
+        private static final int COLUMN = R.styleable.GridLayout_Layout_layout_column;
+        private static final int COLUMN_SPAN = R.styleable.GridLayout_Layout_layout_columnSpan;
+        private static final int COLUMN_WEIGHT = R.styleable.GridLayout_Layout_layout_columnWeight;
+
+        private static final int ROW = R.styleable.GridLayout_Layout_layout_row;
+        private static final int ROW_SPAN = R.styleable.GridLayout_Layout_layout_rowSpan;
+        private static final int ROW_WEIGHT = R.styleable.GridLayout_Layout_layout_rowWeight;
+
+        private static final int GRAVITY = R.styleable.GridLayout_Layout_layout_gravity;
+
+        // Instance variables
+
+        /**
+         * The spec that defines the vertical characteristics of the cell group
+         * described by these layout parameters.
+         * If an assignment is made to this field after a measurement or layout operation
+         * has already taken place, a call to
+         * {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
+         * must be made to notify GridLayout of the change. GridLayout is normally able
+         * to detect when code fails to observe this rule, issue a warning and take steps to
+         * compensate for the omission. This facility is implemented on a best effort basis
+         * and should not be relied upon in production code - so it is best to include the above
+         * calls to remove the warnings as soon as it is practical.
+         */
+        public Spec rowSpec = Spec.UNDEFINED;
+
+        /**
+         * The spec that defines the horizontal characteristics of the cell group
+         * described by these layout parameters.
+         * If an assignment is made to this field after a measurement or layout operation
+         * has already taken place, a call to
+         * {@link ViewGroup#setLayoutParams(ViewGroup.LayoutParams)}
+         * must be made to notify GridLayout of the change. GridLayout is normally able
+         * to detect when code fails to observe this rule, issue a warning and take steps to
+         * compensate for the omission. This facility is implemented on a best effort basis
+         * and should not be relied upon in production code - so it is best to include the above
+         * calls to remove the warnings as soon as it is practical.
+         */
+        public Spec columnSpec = Spec.UNDEFINED;
+
+        // Constructors
+
+        private LayoutParams(
+                int width, int height,
+                int left, int top, int right, int bottom,
+                Spec rowSpec, Spec columnSpec) {
+            super(width, height);
+            setMargins(left, top, right, bottom);
+            this.rowSpec = rowSpec;
+            this.columnSpec = columnSpec;
+        }
+
+        /**
+         * Constructs a new LayoutParams instance for this <code>rowSpec</code>
+         * and <code>columnSpec</code>. All other fields are initialized with
+         * default values as defined in {@link LayoutParams}.
+         *
+         * @param rowSpec    the rowSpec
+         * @param columnSpec the columnSpec
+         */
+        public LayoutParams(Spec rowSpec, Spec columnSpec) {
+            this(DEFAULT_WIDTH, DEFAULT_HEIGHT,
+                    DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN, DEFAULT_MARGIN,
+                    rowSpec, columnSpec);
+        }
+
+        /**
+         * Constructs a new LayoutParams with default values as defined in {@link LayoutParams}.
+         */
+        public LayoutParams() {
+            this(Spec.UNDEFINED, Spec.UNDEFINED);
+        }
+
+        // Copying constructors
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams params) {
+            super(params);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(MarginLayoutParams params) {
+            super(params);
+        }
+
+        /**
+         * Copy constructor. Clones the width, height, margin values, row spec,
+         * and column spec of the source.
+         *
+         * @param source The layout params to copy from.
+         */
+        public LayoutParams(LayoutParams source) {
+            super(source);
+
+            this.rowSpec = source.rowSpec;
+            this.columnSpec = source.columnSpec;
+        }
+
+        // AttributeSet constructors
+
+        /**
+         * {@inheritDoc}
+         *
+         * Values not defined in the attribute set take the default values
+         * defined in {@link LayoutParams}.
+         */
+        public LayoutParams(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            reInitSuper(context, attrs);
+            init(context, attrs);
+        }
+
+        // Implementation
+
+        // Reinitialise the margins using a different default policy than MarginLayoutParams.
+        // Here we use the value UNDEFINED (as distinct from zero) to represent the undefined state
+        // so that a layout manager default can be accessed post set up. We need this as, at the
+        // point of installation, we do not know how many rows/cols there are and therefore
+        // which elements are positioned next to the container's trailing edges. We need to
+        // know this as margins around the container's boundary should have different
+        // defaults to those between peers.
+
+        // This method could be parametrized and moved into MarginLayout.
+        private void reInitSuper(Context context, AttributeSet attrs) {
+            TypedArray a =
+                    context.obtainStyledAttributes(attrs, R.styleable.ViewGroup_MarginLayout);
+            try {
+                int margin = a.getDimensionPixelSize(MARGIN, DEFAULT_MARGIN);
+
+                this.leftMargin = a.getDimensionPixelSize(LEFT_MARGIN, margin);
+                this.topMargin = a.getDimensionPixelSize(TOP_MARGIN, margin);
+                this.rightMargin = a.getDimensionPixelSize(RIGHT_MARGIN, margin);
+                this.bottomMargin = a.getDimensionPixelSize(BOTTOM_MARGIN, margin);
+            } finally {
+                a.recycle();
+            }
+        }
+
+        private void init(Context context, AttributeSet attrs) {
+            TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.GridLayout_Layout);
+            try {
+                int gravity = a.getInt(GRAVITY, Gravity.NO_GRAVITY);
+
+                int column = a.getInt(COLUMN, DEFAULT_COLUMN);
+                int colSpan = a.getInt(COLUMN_SPAN, DEFAULT_SPAN_SIZE);
+                float colWeight = a.getFloat(COLUMN_WEIGHT, Spec.DEFAULT_WEIGHT);
+                this.columnSpec = spec(column, colSpan, getAlignment(gravity, true), colWeight);
+
+                int row = a.getInt(ROW, DEFAULT_ROW);
+                int rowSpan = a.getInt(ROW_SPAN, DEFAULT_SPAN_SIZE);
+                float rowWeight = a.getFloat(ROW_WEIGHT, Spec.DEFAULT_WEIGHT);
+                this.rowSpec = spec(row, rowSpan, getAlignment(gravity, false), rowWeight);
+            } finally {
+                a.recycle();
+            }
+        }
+
+        /**
+         * Describes how the child views are positioned. Default is {@code LEFT | BASELINE}.
+         * See {@link Gravity}.
+         *
+         * @param gravity the new gravity value
+         *
+         * @attr ref android.R.styleable#GridLayout_Layout_layout_gravity
+         */
+        public void setGravity(int gravity) {
+            rowSpec = rowSpec.copyWriteAlignment(getAlignment(gravity, false));
+            columnSpec = columnSpec.copyWriteAlignment(getAlignment(gravity, true));
+        }
+
+        @Override
+        protected void setBaseAttributes(TypedArray attributes, int widthAttr, int heightAttr) {
+            this.width = attributes.getLayoutDimension(widthAttr, DEFAULT_WIDTH);
+            this.height = attributes.getLayoutDimension(heightAttr, DEFAULT_HEIGHT);
+        }
+
+        final void setRowSpecSpan(Interval span) {
+            rowSpec = rowSpec.copyWriteSpan(span);
+        }
+
+        final void setColumnSpecSpan(Interval span) {
+            columnSpec = columnSpec.copyWriteSpan(span);
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (this == o) return true;
+            if (o == null || getClass() != o.getClass()) return false;
+
+            LayoutParams that = (LayoutParams) o;
+
+            if (!columnSpec.equals(that.columnSpec)) return false;
+            if (!rowSpec.equals(that.rowSpec)) return false;
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = rowSpec.hashCode();
+            result = 31 * result + columnSpec.hashCode();
+            return result;
+        }
+    }
+
+    /*
+    In place of a HashMap from span to Int, use an array of key/value pairs - stored in Arcs.
+    Add the mutables completesCycle flag to avoid creating another hash table for detecting cycles.
+     */
+    final static class Arc {
+        public final Interval span;
+        public final MutableInt value;
+        public boolean valid = true;
+
+        public Arc(Interval span, MutableInt value) {
+            this.span = span;
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            return span + " " + (!valid ? "+>" : "->") + " " + value;
+        }
+    }
+
+    // A mutable Integer - used to avoid heap allocation during the layout operation
+
+    final static class MutableInt {
+        public int value;
+
+        public MutableInt() {
+            reset();
+        }
+
+        public MutableInt(int value) {
+            this.value = value;
+        }
+
+        public void reset() {
+            value = Integer.MIN_VALUE;
+        }
+
+        @Override
+        public String toString() {
+            return Integer.toString(value);
+        }
+    }
+
+    final static class Assoc<K, V> extends ArrayList<Pair<K, V>> {
+        private final Class<K> keyType;
+        private final Class<V> valueType;
+
+        private Assoc(Class<K> keyType, Class<V> valueType) {
+            this.keyType = keyType;
+            this.valueType = valueType;
+        }
+
+        public static <K, V> Assoc<K, V> of(Class<K> keyType, Class<V> valueType) {
+            return new Assoc<K, V>(keyType, valueType);
+        }
+
+        public void put(K key, V value) {
+            add(Pair.create(key, value));
+        }
+
+        @SuppressWarnings(value = "unchecked")
+        public PackedMap<K, V> pack() {
+            int N = size();
+            K[] keys = (K[]) Array.newInstance(keyType, N);
+            V[] values = (V[]) Array.newInstance(valueType, N);
+            for (int i = 0; i < N; i++) {
+                keys[i] = get(i).first;
+                values[i] = get(i).second;
+            }
+            return new PackedMap<K, V>(keys, values);
+        }
+    }
+
+    /*
+    This data structure is used in place of a Map where we have an index that refers to the order
+    in which each key/value pairs were added to the map. In this case we store keys and values
+    in arrays of a length that is equal to the number of unique keys. We also maintain an
+    array of indexes from insertion order to the compacted arrays of keys and values.
+
+    Note that behavior differs from that of a LinkedHashMap in that repeated entries
+    *do* get added multiples times. So the length of index is equals to the number of
+    items added.
+
+    This is useful in the GridLayout class where we can rely on the order of children not
+    changing during layout - to use integer-based lookup for our internal structures
+    rather than using (and storing) an implementation of Map<Key, ?>.
+     */
+    @SuppressWarnings(value = "unchecked")
+    final static class PackedMap<K, V> {
+        public final int[] index;
+        public final K[] keys;
+        public final V[] values;
+
+        private PackedMap(K[] keys, V[] values) {
+            this.index = createIndex(keys);
+
+            this.keys = compact(keys, index);
+            this.values = compact(values, index);
+        }
+
+        public V getValue(int i) {
+            return values[index[i]];
+        }
+
+        private static <K> int[] createIndex(K[] keys) {
+            int size = keys.length;
+            int[] result = new int[size];
+
+            Map<K, Integer> keyToIndex = new HashMap<K, Integer>();
+            for (int i = 0; i < size; i++) {
+                K key = keys[i];
+                Integer index = keyToIndex.get(key);
+                if (index == null) {
+                    index = keyToIndex.size();
+                    keyToIndex.put(key, index);
+                }
+                result[i] = index;
+            }
+            return result;
+        }
+
+        /*
+        Create a compact array of keys or values using the supplied index.
+         */
+        private static <K> K[] compact(K[] a, int[] index) {
+            int size = a.length;
+            Class<?> componentType = a.getClass().getComponentType();
+            K[] result = (K[]) Array.newInstance(componentType, max2(index, -1) + 1);
+
+            // this overwrite duplicates, retaining the last equivalent entry
+            for (int i = 0; i < size; i++) {
+                result[index[i]] = a[i];
+            }
+            return result;
+        }
+    }
+
+    /*
+    For each group (with a given alignment) we need to store the amount of space required
+    before the alignment point and the amount of space required after it. One side of this
+    calculation is always 0 for START and END alignments but we don't make use of this.
+    For CENTER and BASELINE alignments both sides are needed and in the BASELINE case no
+    simple optimisations are possible.
+
+    The general algorithm therefore is to create a Map (actually a PackedMap) from
+    group to Bounds and to loop through all Views in the group taking the maximum
+    of the values for each View.
+    */
+    static class Bounds {
+        public int before;
+        public int after;
+        public int flexibility; // we're flexible iff all included specs are flexible
+
+        private Bounds() {
+            reset();
+        }
+
+        protected void reset() {
+            before = Integer.MIN_VALUE;
+            after = Integer.MIN_VALUE;
+            flexibility = CAN_STRETCH; // from the above, we're flexible when empty
+        }
+
+        protected void include(int before, int after) {
+            this.before = max(this.before, before);
+            this.after = max(this.after, after);
+        }
+
+        protected int size(boolean min) {
+            if (!min) {
+                if (canStretch(flexibility)) {
+                    return MAX_SIZE;
+                }
+            }
+            return before + after;
+        }
+
+        protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean horizontal) {
+            return before - a.getAlignmentValue(c, size, gl.getLayoutMode());
+        }
+
+        protected final void include(GridLayout gl, View c, Spec spec, Axis axis, int size) {
+            this.flexibility &= spec.getFlexibility();
+            boolean horizontal = axis.horizontal;
+            Alignment alignment = spec.getAbsoluteAlignment(axis.horizontal);
+            // todo test this works correctly when the returned value is UNDEFINED
+            int before = alignment.getAlignmentValue(c, size, gl.getLayoutMode());
+            include(before, size - before);
+        }
+
+        @Override
+        public String toString() {
+            return "Bounds{" +
+                    "before=" + before +
+                    ", after=" + after +
+                    '}';
+        }
+    }
+
+    /**
+     * An Interval represents a contiguous range of values that lie between
+     * the interval's {@link #min} and {@link #max} values.
+     * <p>
+     * Intervals are immutable so may be passed as values and used as keys in hash tables.
+     * It is not necessary to have multiple instances of Intervals which have the same
+     * {@link #min} and {@link #max} values.
+     * <p>
+     * Intervals are often written as {@code [min, max]} and represent the set of values
+     * {@code x} such that {@code min <= x < max}.
+     */
+    final static class Interval {
+        /**
+         * The minimum value.
+         */
+        public final int min;
+
+        /**
+         * The maximum value.
+         */
+        public final int max;
+
+        /**
+         * Construct a new Interval, {@code interval}, where:
+         * <ul>
+         *     <li> {@code interval.min = min} </li>
+         *     <li> {@code interval.max = max} </li>
+         * </ul>
+         *
+         * @param min the minimum value.
+         * @param max the maximum value.
+         */
+        public Interval(int min, int max) {
+            this.min = min;
+            this.max = max;
+        }
+
+        int size() {
+            return max - min;
+        }
+
+        Interval inverse() {
+            return new Interval(max, min);
+        }
+
+        /**
+         * Returns {@code true} if the {@link #getClass class},
+         * {@link #min} and {@link #max} properties of this Interval and the
+         * supplied parameter are pairwise equal; {@code false} otherwise.
+         *
+         * @param that the object to compare this interval with
+         *
+         * @return {@code true} if the specified object is equal to this
+         *         {@code Interval}, {@code false} otherwise.
+         */
+        @Override
+        public boolean equals(Object that) {
+            if (this == that) {
+                return true;
+            }
+            if (that == null || getClass() != that.getClass()) {
+                return false;
+            }
+
+            Interval interval = (Interval) that;
+
+            if (max != interval.max) {
+                return false;
+            }
+            //noinspection RedundantIfStatement
+            if (min != interval.min) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = min;
+            result = 31 * result + max;
+            return result;
+        }
+
+        @Override
+        public String toString() {
+            return "[" + min + ", " + max + "]";
+        }
+    }
+
+    /**
+     * A Spec defines the horizontal or vertical characteristics of a group of
+     * cells. Each spec. defines the <em>grid indices</em> and <em>alignment</em>
+     * along the appropriate axis.
+     * <p>
+     * The <em>grid indices</em> are the leading and trailing edges of this cell group.
+     * See {@link GridLayout} for a description of the conventions used by GridLayout
+     * for grid indices.
+     * <p>
+     * The <em>alignment</em> property specifies how cells should be aligned in this group.
+     * For row groups, this specifies the vertical alignment.
+     * For column groups, this specifies the horizontal alignment.
+     * <p>
+     * Use the following static methods to create specs:
+     * <ul>
+     *   <li>{@link #spec(int)}</li>
+     *   <li>{@link #spec(int, int)}</li>
+     *   <li>{@link #spec(int, Alignment)}</li>
+     *   <li>{@link #spec(int, int, Alignment)}</li>
+     *   <li>{@link #spec(int, float)}</li>
+     *   <li>{@link #spec(int, int, float)}</li>
+     *   <li>{@link #spec(int, Alignment, float)}</li>
+     *   <li>{@link #spec(int, int, Alignment, float)}</li>
+     * </ul>
+     *
+     */
+    public static class Spec {
+        static final Spec UNDEFINED = spec(GridLayout.UNDEFINED);
+        static final float DEFAULT_WEIGHT = 0;
+
+        final boolean startDefined;
+        final Interval span;
+        final Alignment alignment;
+        final float weight;
+
+        private Spec(boolean startDefined, Interval span, Alignment alignment, float weight) {
+            this.startDefined = startDefined;
+            this.span = span;
+            this.alignment = alignment;
+            this.weight = weight;
+        }
+
+        private Spec(boolean startDefined, int start, int size, Alignment alignment, float weight) {
+            this(startDefined, new Interval(start, start + size), alignment, weight);
+        }
+
+        private Alignment getAbsoluteAlignment(boolean horizontal) {
+            if (alignment != UNDEFINED_ALIGNMENT) {
+                return alignment;
+            }
+            if (weight == 0f) {
+                return horizontal ? START : BASELINE;
+            }
+            return FILL;
+        }
+
+        final Spec copyWriteSpan(Interval span) {
+            return new Spec(startDefined, span, alignment, weight);
+        }
+
+        final Spec copyWriteAlignment(Alignment alignment) {
+            return new Spec(startDefined, span, alignment, weight);
+        }
+
+        final int getFlexibility() {
+            return (alignment == UNDEFINED_ALIGNMENT && weight == 0) ? INFLEXIBLE : CAN_STRETCH;
+        }
+
+        /**
+         * Returns {@code true} if the {@code class}, {@code alignment} and {@code span}
+         * properties of this Spec and the supplied parameter are pairwise equal,
+         * {@code false} otherwise.
+         *
+         * @param that the object to compare this spec with
+         *
+         * @return {@code true} if the specified object is equal to this
+         *         {@code Spec}; {@code false} otherwise
+         */
+        @Override
+        public boolean equals(Object that) {
+            if (this == that) {
+                return true;
+            }
+            if (that == null || getClass() != that.getClass()) {
+                return false;
+            }
+
+            Spec spec = (Spec) that;
+
+            if (!alignment.equals(spec.alignment)) {
+                return false;
+            }
+            //noinspection RedundantIfStatement
+            if (!span.equals(spec.span)) {
+                return false;
+            }
+
+            return true;
+        }
+
+        @Override
+        public int hashCode() {
+            int result = span.hashCode();
+            result = 31 * result + alignment.hashCode();
+            return result;
+        }
+    }
+
+    /**
+     * Return a Spec, {@code spec}, where:
+     * <ul>
+     *     <li> {@code spec.span = [start, start + size]} </li>
+     *     <li> {@code spec.alignment = alignment} </li>
+     *     <li> {@code spec.weight = weight} </li>
+     * </ul>
+     * <p>
+     * To leave the start index undefined, use the value {@link #UNDEFINED}.
+     *
+     * @param start     the start
+     * @param size      the size
+     * @param alignment the alignment
+     * @param weight    the weight
+     */
+    public static Spec spec(int start, int size, Alignment alignment, float weight) {
+        return new Spec(start != UNDEFINED, start, size, alignment, weight);
+    }
+
+    /**
+     * Equivalent to: {@code spec(start, 1, alignment, weight)}.
+     *
+     * @param start     the start
+     * @param alignment the alignment
+     * @param weight    the weight
+     */
+    public static Spec spec(int start, Alignment alignment, float weight) {
+        return spec(start, 1, alignment, weight);
+    }
+
+    /**
+     * Equivalent to: {@code spec(start, 1, default_alignment, weight)} -
+     * where {@code default_alignment} is specified in
+     * {@link android.widget.GridLayout.LayoutParams}.
+     *
+     * @param start  the start
+     * @param size   the size
+     * @param weight the weight
+     */
+    public static Spec spec(int start, int size, float weight) {
+        return spec(start, size, UNDEFINED_ALIGNMENT, weight);
+    }
+
+    /**
+     * Equivalent to: {@code spec(start, 1, weight)}.
+     *
+     * @param start  the start
+     * @param weight the weight
+     */
+    public static Spec spec(int start, float weight) {
+        return spec(start, 1, weight);
+    }
+
+    /**
+     * Equivalent to: {@code spec(start, size, alignment, 0f)}.
+     *
+     * @param start     the start
+     * @param size      the size
+     * @param alignment the alignment
+     */
+    public static Spec spec(int start, int size, Alignment alignment) {
+        return spec(start, size, alignment, Spec.DEFAULT_WEIGHT);
+    }
+
+    /**
+     * Return a Spec, {@code spec}, where:
+     * <ul>
+     *     <li> {@code spec.span = [start, start + 1]} </li>
+     *     <li> {@code spec.alignment = alignment} </li>
+     * </ul>
+     * <p>
+     * To leave the start index undefined, use the value {@link #UNDEFINED}.
+     *
+     * @param start     the start index
+     * @param alignment the alignment
+     *
+     * @see #spec(int, int, Alignment)
+     */
+    public static Spec spec(int start, Alignment alignment) {
+        return spec(start, 1, alignment);
+    }
+
+    /**
+     * Return a Spec, {@code spec}, where:
+     * <ul>
+     *     <li> {@code spec.span = [start, start + size]} </li>
+     * </ul>
+     * <p>
+     * To leave the start index undefined, use the value {@link #UNDEFINED}.
+     *
+     * @param start     the start
+     * @param size      the size
+     *
+     * @see #spec(int, Alignment)
+     */
+    public static Spec spec(int start, int size) {
+        return spec(start, size, UNDEFINED_ALIGNMENT);
+    }
+
+    /**
+     * Return a Spec, {@code spec}, where:
+     * <ul>
+     *     <li> {@code spec.span = [start, start + 1]} </li>
+     * </ul>
+     * <p>
+     * To leave the start index undefined, use the value {@link #UNDEFINED}.
+     *
+     * @param start     the start index
+     *
+     * @see #spec(int, int)
+     */
+    public static Spec spec(int start) {
+        return spec(start, 1);
+    }
+
+    /**
+     * Alignments specify where a view should be placed within a cell group and
+     * what size it should be.
+     * <p>
+     * The {@link LayoutParams} class contains a {@link LayoutParams#rowSpec rowSpec}
+     * and a {@link LayoutParams#columnSpec columnSpec} each of which contains an
+     * {@code alignment}. Overall placement of the view in the cell
+     * group is specified by the two alignments which act along each axis independently.
+     * <p>
+     *  The GridLayout class defines the most common alignments used in general layout:
+     * {@link #TOP}, {@link #LEFT}, {@link #BOTTOM}, {@link #RIGHT}, {@link #START},
+     * {@link #END}, {@link #CENTER}, {@link #BASELINE} and {@link #FILL}.
+     */
+    /*
+     * An Alignment implementation must define {@link #getAlignmentValue(View, int, int)},
+     * to return the appropriate value for the type of alignment being defined.
+     * The enclosing algorithms position the children
+     * so that the locations defined by the alignment values
+     * are the same for all of the views in a group.
+     * <p>
+     */
+    public static abstract class Alignment {
+        Alignment() {
+        }
+
+        abstract int getGravityOffset(View view, int cellDelta);
+
+        /**
+         * Returns an alignment value. In the case of vertical alignments the value
+         * returned should indicate the distance from the top of the view to the
+         * alignment location.
+         * For horizontal alignments measurement is made from the left edge of the component.
+         *
+         * @param view              the view to which this alignment should be applied
+         * @param viewSize          the measured size of the view
+         * @param mode              the basis of alignment: CLIP or OPTICAL
+         * @return the alignment value
+         */
+        abstract int getAlignmentValue(View view, int viewSize, int mode);
+
+        /**
+         * Returns the size of the view specified by this alignment.
+         * In the case of vertical alignments this method should return a height; for
+         * horizontal alignments this method should return the width.
+         * <p>
+         * The default implementation returns {@code viewSize}.
+         *
+         * @param view              the view to which this alignment should be applied
+         * @param viewSize          the measured size of the view
+         * @param cellSize          the size of the cell into which this view will be placed
+         * @return the aligned size
+         */
+        int getSizeInCell(View view, int viewSize, int cellSize) {
+            return viewSize;
+        }
+
+        Bounds getBounds() {
+            return new Bounds();
+        }
+    }
+
+    static final Alignment UNDEFINED_ALIGNMENT = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return UNDEFINED;
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            return UNDEFINED;
+        }
+    };
+
+    /**
+     * Indicates that a view should be aligned with the <em>start</em>
+     * edges of the other views in its cell group.
+     */
+    private static final Alignment LEADING = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return 0;
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            return 0;
+        }
+    };
+
+    /**
+     * Indicates that a view should be aligned with the <em>end</em>
+     * edges of the other views in its cell group.
+     */
+    private static final Alignment TRAILING = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return cellDelta;
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            return viewSize;
+        }
+    };
+
+    /**
+     * Indicates that a view should be aligned with the <em>top</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment TOP = LEADING;
+
+    /**
+     * Indicates that a view should be aligned with the <em>bottom</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment BOTTOM = TRAILING;
+
+    /**
+     * Indicates that a view should be aligned with the <em>start</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment START = LEADING;
+
+    /**
+     * Indicates that a view should be aligned with the <em>end</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment END = TRAILING;
+
+    private static Alignment createSwitchingAlignment(final Alignment ltr, final Alignment rtl) {
+        return new Alignment() {
+            @Override
+            int getGravityOffset(View view, int cellDelta) {
+                return (!view.isLayoutRtl() ? ltr : rtl).getGravityOffset(view, cellDelta);
+            }
+
+            @Override
+            public int getAlignmentValue(View view, int viewSize, int mode) {
+                return (!view.isLayoutRtl() ? ltr : rtl).getAlignmentValue(view, viewSize, mode);
+            }
+        };
+    }
+
+    /**
+     * Indicates that a view should be aligned with the <em>left</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment LEFT = createSwitchingAlignment(START, END);
+
+    /**
+     * Indicates that a view should be aligned with the <em>right</em>
+     * edges of the other views in its cell group.
+     */
+    public static final Alignment RIGHT = createSwitchingAlignment(END, START);
+
+    /**
+     * Indicates that a view should be <em>centered</em> with the other views in its cell group.
+     * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and {@link
+     * LayoutParams#columnSpec columnSpecs}.
+     */
+    public static final Alignment CENTER = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return cellDelta >> 1;
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            return viewSize >> 1;
+        }
+    };
+
+    /**
+     * Indicates that a view should be aligned with the <em>baselines</em>
+     * of the other views in its cell group.
+     * This constant may only be used as an alignment in {@link LayoutParams#rowSpec rowSpecs}.
+     *
+     * @see View#getBaseline()
+     */
+    public static final Alignment BASELINE = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return 0; // baseline gravity is top
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            if (view.getVisibility() == GONE) {
+                return 0;
+            }
+            int baseline = view.getBaseline();
+            return baseline == -1 ? UNDEFINED : baseline;
+        }
+
+        @Override
+        public Bounds getBounds() {
+            return new Bounds() {
+                /*
+                In a baseline aligned row in which some components define a baseline
+                and some don't, we need a third variable to properly account for all
+                the sizes. This tracks the maximum size of all the components -
+                including those that don't define a baseline.
+                */
+                private int size;
+
+                @Override
+                protected void reset() {
+                    super.reset();
+                    size = Integer.MIN_VALUE;
+                }
+
+                @Override
+                protected void include(int before, int after) {
+                    super.include(before, after);
+                    size = max(size, before + after);
+                }
+
+                @Override
+                protected int size(boolean min) {
+                    return max(super.size(min), size);
+                }
+
+                @Override
+                protected int getOffset(GridLayout gl, View c, Alignment a, int size, boolean hrz) {
+                    return max(0, super.getOffset(gl, c, a, size, hrz));
+                }
+            };
+        }
+    };
+
+    /**
+     * Indicates that a view should expanded to fit the boundaries of its cell group.
+     * This constant may be used in both {@link LayoutParams#rowSpec rowSpecs} and
+     * {@link LayoutParams#columnSpec columnSpecs}.
+     */
+    public static final Alignment FILL = new Alignment() {
+        @Override
+        int getGravityOffset(View view, int cellDelta) {
+            return 0;
+        }
+
+        @Override
+        public int getAlignmentValue(View view, int viewSize, int mode) {
+            return UNDEFINED;
+        }
+
+        @Override
+        public int getSizeInCell(View view, int viewSize, int cellSize) {
+            return cellSize;
+        }
+    };
+
+    static boolean canStretch(int flexibility) {
+        return (flexibility & CAN_STRETCH) != 0;
+    }
+
+    private static final int INFLEXIBLE = 0;
+    private static final int CAN_STRETCH = 2;
+}
diff --git a/android/widget/GridView.java b/android/widget/GridView.java
new file mode 100644
index 0000000..fcb44af
--- /dev/null
+++ b/android/widget/GridView.java
@@ -0,0 +1,2424 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Bundle;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewRootImpl;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.animation.GridLayoutAnimationController;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * A view that shows items in two-dimensional scrolling grid. The items in the
+ * grid come from the {@link ListAdapter} associated with this view.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/layout/gridview.html">Grid
+ * View</a> guide.</p>
+ *
+ * @attr ref android.R.styleable#GridView_horizontalSpacing
+ * @attr ref android.R.styleable#GridView_verticalSpacing
+ * @attr ref android.R.styleable#GridView_stretchMode
+ * @attr ref android.R.styleable#GridView_columnWidth
+ * @attr ref android.R.styleable#GridView_numColumns
+ * @attr ref android.R.styleable#GridView_gravity
+ */
+@RemoteView
+public class GridView extends AbsListView {
+    /** @hide */
+    @IntDef({NO_STRETCH, STRETCH_SPACING, STRETCH_COLUMN_WIDTH, STRETCH_SPACING_UNIFORM})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface StretchMode {}
+
+    /**
+     * Disables stretching.
+     *
+     * @see #setStretchMode(int)
+     */
+    public static final int NO_STRETCH = 0;
+    /**
+     * Stretches the spacing between columns.
+     *
+     * @see #setStretchMode(int)
+     */
+    public static final int STRETCH_SPACING = 1;
+    /**
+     * Stretches columns.
+     *
+     * @see #setStretchMode(int)
+     */
+    public static final int STRETCH_COLUMN_WIDTH = 2;
+    /**
+     * Stretches the spacing between columns. The spacing is uniform.
+     *
+     * @see #setStretchMode(int)
+     */
+    public static final int STRETCH_SPACING_UNIFORM = 3;
+
+    /**
+     * Creates as many columns as can fit on screen.
+     *
+     * @see #setNumColumns(int)
+     */
+    public static final int AUTO_FIT = -1;
+
+    private int mNumColumns = AUTO_FIT;
+
+    private int mHorizontalSpacing = 0;
+    private int mRequestedHorizontalSpacing;
+    private int mVerticalSpacing = 0;
+    private int mStretchMode = STRETCH_COLUMN_WIDTH;
+    private int mColumnWidth;
+    private int mRequestedColumnWidth;
+    private int mRequestedNumColumns;
+
+    private View mReferenceView = null;
+    private View mReferenceViewInSelectedRow = null;
+
+    private int mGravity = Gravity.START;
+
+    private final Rect mTempRect = new Rect();
+
+    public GridView(Context context) {
+        this(context, null);
+    }
+
+    public GridView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.gridViewStyle);
+    }
+
+    public GridView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public GridView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.GridView, defStyleAttr, defStyleRes);
+
+        int hSpacing = a.getDimensionPixelOffset(
+                R.styleable.GridView_horizontalSpacing, 0);
+        setHorizontalSpacing(hSpacing);
+
+        int vSpacing = a.getDimensionPixelOffset(
+                R.styleable.GridView_verticalSpacing, 0);
+        setVerticalSpacing(vSpacing);
+
+        int index = a.getInt(R.styleable.GridView_stretchMode, STRETCH_COLUMN_WIDTH);
+        if (index >= 0) {
+            setStretchMode(index);
+        }
+
+        int columnWidth = a.getDimensionPixelOffset(R.styleable.GridView_columnWidth, -1);
+        if (columnWidth > 0) {
+            setColumnWidth(columnWidth);
+        }
+
+        int numColumns = a.getInt(R.styleable.GridView_numColumns, 1);
+        setNumColumns(numColumns);
+
+        index = a.getInt(R.styleable.GridView_gravity, -1);
+        if (index >= 0) {
+            setGravity(index);
+        }
+
+        a.recycle();
+    }
+
+    @Override
+    public ListAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    /**
+     * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+     * through the specified intent.
+     * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+    public void setRemoteViewsAdapter(Intent intent) {
+        super.setRemoteViewsAdapter(intent);
+    }
+
+    /**
+     * Sets the data behind this GridView.
+     *
+     * @param adapter the adapter providing the grid's data
+     */
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        if (mAdapter != null && mDataSetObserver != null) {
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
+        }
+
+        resetList();
+        mRecycler.clear();
+        mAdapter = adapter;
+
+        mOldSelectedPosition = INVALID_POSITION;
+        mOldSelectedRowId = INVALID_ROW_ID;
+
+        // AbsListView#setAdapter will update choice mode states.
+        super.setAdapter(adapter);
+
+        if (mAdapter != null) {
+            mOldItemCount = mItemCount;
+            mItemCount = mAdapter.getCount();
+            mDataChanged = true;
+            checkFocus();
+
+            mDataSetObserver = new AdapterDataSetObserver();
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+
+            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
+
+            int position;
+            if (mStackFromBottom) {
+                position = lookForSelectablePosition(mItemCount - 1, false);
+            } else {
+                position = lookForSelectablePosition(0, true);
+            }
+            setSelectedPositionInt(position);
+            setNextSelectedPositionInt(position);
+            checkSelectionChanged();
+        } else {
+            checkFocus();
+            // Nothing selected
+            checkSelectionChanged();
+        }
+
+        requestLayout();
+    }
+
+    @Override
+    int lookForSelectablePosition(int position, boolean lookDown) {
+        final ListAdapter adapter = mAdapter;
+        if (adapter == null || isInTouchMode()) {
+            return INVALID_POSITION;
+        }
+
+        if (position < 0 || position >= mItemCount) {
+            return INVALID_POSITION;
+        }
+        return position;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void fillGap(boolean down) {
+        final int numColumns = mNumColumns;
+        final int verticalSpacing = mVerticalSpacing;
+
+        final int count = getChildCount();
+
+        if (down) {
+            int paddingTop = 0;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                paddingTop = getListPaddingTop();
+            }
+            final int startOffset = count > 0 ?
+                    getChildAt(count - 1).getBottom() + verticalSpacing : paddingTop;
+            int position = mFirstPosition + count;
+            if (mStackFromBottom) {
+                position += numColumns - 1;
+            }
+            fillDown(position, startOffset);
+            correctTooHigh(numColumns, verticalSpacing, getChildCount());
+        } else {
+            int paddingBottom = 0;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                paddingBottom = getListPaddingBottom();
+            }
+            final int startOffset = count > 0 ?
+                    getChildAt(0).getTop() - verticalSpacing : getHeight() - paddingBottom;
+            int position = mFirstPosition;
+            if (!mStackFromBottom) {
+                position -= numColumns;
+            } else {
+                position--;
+            }
+            fillUp(position, startOffset);
+            correctTooLow(numColumns, verticalSpacing, getChildCount());
+        }
+    }
+
+    /**
+     * Fills the list from pos down to the end of the list view.
+     *
+     * @param pos The first position to put in the list
+     *
+     * @param nextTop The location where the top of the item associated with pos
+     *        should be drawn
+     *
+     * @return The view that is currently selected, if it happens to be in the
+     *         range that we draw.
+     */
+    private View fillDown(int pos, int nextTop) {
+        View selectedView = null;
+
+        int end = (mBottom - mTop);
+        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+            end -= mListPadding.bottom;
+        }
+
+        while (nextTop < end && pos < mItemCount) {
+            View temp = makeRow(pos, nextTop, true);
+            if (temp != null) {
+                selectedView = temp;
+            }
+
+            // mReferenceView will change with each call to makeRow()
+            // do not cache in a local variable outside of this loop
+            nextTop = mReferenceView.getBottom() + mVerticalSpacing;
+
+            pos += mNumColumns;
+        }
+
+        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+        return selectedView;
+    }
+
+    private View makeRow(int startPos, int y, boolean flow) {
+        final int columnWidth = mColumnWidth;
+        final int horizontalSpacing = mHorizontalSpacing;
+
+        final boolean isLayoutRtl = isLayoutRtl();
+
+        int last;
+        int nextLeft;
+
+        if (isLayoutRtl) {
+            nextLeft = getWidth() - mListPadding.right - columnWidth -
+                    ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
+        } else {
+            nextLeft = mListPadding.left +
+                    ((mStretchMode == STRETCH_SPACING_UNIFORM) ? horizontalSpacing : 0);
+        }
+
+        if (!mStackFromBottom) {
+            last = Math.min(startPos + mNumColumns, mItemCount);
+        } else {
+            last = startPos + 1;
+            startPos = Math.max(0, startPos - mNumColumns + 1);
+
+            if (last - startPos < mNumColumns) {
+                final int deltaLeft = (mNumColumns - (last - startPos)) * (columnWidth + horizontalSpacing);
+                nextLeft += (isLayoutRtl ? -1 : +1) * deltaLeft;
+            }
+        }
+
+        View selectedView = null;
+
+        final boolean hasFocus = shouldShowSelector();
+        final boolean inClick = touchModeDrawsInPressedState();
+        final int selectedPosition = mSelectedPosition;
+
+        View child = null;
+        final int nextChildDir = isLayoutRtl ? -1 : +1;
+        for (int pos = startPos; pos < last; pos++) {
+            // is this the selected item?
+            boolean selected = pos == selectedPosition;
+            // does the list view have focus or contain focus
+
+            final int where = flow ? -1 : pos - startPos;
+            child = makeAndAddView(pos, y, flow, nextLeft, selected, where);
+
+            nextLeft += nextChildDir * columnWidth;
+            if (pos < last - 1) {
+                nextLeft += nextChildDir * horizontalSpacing;
+            }
+
+            if (selected && (hasFocus || inClick)) {
+                selectedView = child;
+            }
+        }
+
+        mReferenceView = child;
+
+        if (selectedView != null) {
+            mReferenceViewInSelectedRow = mReferenceView;
+        }
+
+        return selectedView;
+    }
+
+    /**
+     * Fills the list from pos up to the top of the list view.
+     *
+     * @param pos The first position to put in the list
+     *
+     * @param nextBottom The location where the bottom of the item associated
+     *        with pos should be drawn
+     *
+     * @return The view that is currently selected
+     */
+    private View fillUp(int pos, int nextBottom) {
+        View selectedView = null;
+
+        int end = 0;
+        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+            end = mListPadding.top;
+        }
+
+        while (nextBottom > end && pos >= 0) {
+
+            View temp = makeRow(pos, nextBottom, false);
+            if (temp != null) {
+                selectedView = temp;
+            }
+
+            nextBottom = mReferenceView.getTop() - mVerticalSpacing;
+
+            mFirstPosition = pos;
+
+            pos -= mNumColumns;
+        }
+
+        if (mStackFromBottom) {
+            mFirstPosition = Math.max(0, pos + 1);
+        }
+
+        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+        return selectedView;
+    }
+
+    /**
+     * Fills the list from top to bottom, starting with mFirstPosition
+     *
+     * @param nextTop The location where the top of the first item should be
+     *        drawn
+     *
+     * @return The view that is currently selected
+     */
+    private View fillFromTop(int nextTop) {
+        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
+        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
+        if (mFirstPosition < 0) {
+            mFirstPosition = 0;
+        }
+        mFirstPosition -= mFirstPosition % mNumColumns;
+        return fillDown(mFirstPosition, nextTop);
+    }
+
+    private View fillFromBottom(int lastPosition, int nextBottom) {
+        lastPosition = Math.max(lastPosition, mSelectedPosition);
+        lastPosition = Math.min(lastPosition, mItemCount - 1);
+
+        final int invertedPosition = mItemCount - 1 - lastPosition;
+        lastPosition = mItemCount - 1 - (invertedPosition - (invertedPosition % mNumColumns));
+
+        return fillUp(lastPosition, nextBottom);
+    }
+
+    private View fillSelection(int childrenTop, int childrenBottom) {
+        final int selectedPosition = reconcileSelectedPosition();
+        final int numColumns = mNumColumns;
+        final int verticalSpacing = mVerticalSpacing;
+
+        int rowStart;
+        int rowEnd = -1;
+
+        if (!mStackFromBottom) {
+            rowStart = selectedPosition - (selectedPosition % numColumns);
+        } else {
+            final int invertedSelection = mItemCount - 1 - selectedPosition;
+
+            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+            rowStart = Math.max(0, rowEnd - numColumns + 1);
+        }
+
+        final int fadingEdgeLength = getVerticalFadingEdgeLength();
+        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+
+        final View sel = makeRow(mStackFromBottom ? rowEnd : rowStart, topSelectionPixel, true);
+        mFirstPosition = rowStart;
+
+        final View referenceView = mReferenceView;
+
+        if (!mStackFromBottom) {
+            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+            pinToBottom(childrenBottom);
+            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+            adjustViewsUpOrDown();
+        } else {
+            final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom,
+                    fadingEdgeLength, numColumns, rowStart);
+            final int offset = bottomSelectionPixel - referenceView.getBottom();
+            offsetChildrenTopAndBottom(offset);
+            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+            pinToTop(childrenTop);
+            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+            adjustViewsUpOrDown();
+        }
+
+        return sel;
+    }
+
+    private void pinToTop(int childrenTop) {
+        if (mFirstPosition == 0) {
+            final int top = getChildAt(0).getTop();
+            final int offset = childrenTop - top;
+            if (offset < 0) {
+                offsetChildrenTopAndBottom(offset);
+            }
+        }
+    }
+
+    private void pinToBottom(int childrenBottom) {
+        final int count = getChildCount();
+        if (mFirstPosition + count == mItemCount) {
+            final int bottom = getChildAt(count - 1).getBottom();
+            final int offset = childrenBottom - bottom;
+            if (offset > 0) {
+                offsetChildrenTopAndBottom(offset);
+            }
+        }
+    }
+
+    @Override
+    int findMotionRow(int y) {
+        final int childCount = getChildCount();
+        if (childCount > 0) {
+
+            final int numColumns = mNumColumns;
+            if (!mStackFromBottom) {
+                for (int i = 0; i < childCount; i += numColumns) {
+                    if (y <= getChildAt(i).getBottom()) {
+                        return mFirstPosition + i;
+                    }
+                }
+            } else {
+                for (int i = childCount - 1; i >= 0; i -= numColumns) {
+                    if (y >= getChildAt(i).getTop()) {
+                        return mFirstPosition + i;
+                    }
+                }
+            }
+        }
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Layout during a scroll that results from tracking motion events. Places
+     * the mMotionPosition view at the offset specified by mMotionViewTop, and
+     * then build surrounding views from there.
+     *
+     * @param position the position at which to start filling
+     * @param top the top of the view at that position
+     * @return The selected view, or null if the selected view is outside the
+     *         visible area.
+     */
+    private View fillSpecific(int position, int top) {
+        final int numColumns = mNumColumns;
+
+        int motionRowStart;
+        int motionRowEnd = -1;
+
+        if (!mStackFromBottom) {
+            motionRowStart = position - (position % numColumns);
+        } else {
+            final int invertedSelection = mItemCount - 1 - position;
+
+            motionRowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+            motionRowStart = Math.max(0, motionRowEnd - numColumns + 1);
+        }
+
+        final View temp = makeRow(mStackFromBottom ? motionRowEnd : motionRowStart, top, true);
+
+        // Possibly changed again in fillUp if we add rows above this one.
+        mFirstPosition = motionRowStart;
+
+        final View referenceView = mReferenceView;
+        // We didn't have anything to layout, bail out
+        if (referenceView == null) {
+            return null;
+        }
+
+        final int verticalSpacing = mVerticalSpacing;
+
+        View above;
+        View below;
+
+        if (!mStackFromBottom) {
+            above = fillUp(motionRowStart - numColumns, referenceView.getTop() - verticalSpacing);
+            adjustViewsUpOrDown();
+            below = fillDown(motionRowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+            // Check if we have dragged the bottom of the grid too high
+            final int childCount = getChildCount();
+            if (childCount > 0) {
+                correctTooHigh(numColumns, verticalSpacing, childCount);
+            }
+        } else {
+            below = fillDown(motionRowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+            adjustViewsUpOrDown();
+            above = fillUp(motionRowStart - 1, referenceView.getTop() - verticalSpacing);
+            // Check if we have dragged the bottom of the grid too high
+            final int childCount = getChildCount();
+            if (childCount > 0) {
+                correctTooLow(numColumns, verticalSpacing, childCount);
+            }
+        }
+
+        if (temp != null) {
+            return temp;
+        } else if (above != null) {
+            return above;
+        } else {
+            return below;
+        }
+    }
+
+    private void correctTooHigh(int numColumns, int verticalSpacing, int childCount) {
+        // First see if the last item is visible
+        final int lastPosition = mFirstPosition + childCount - 1;
+        if (lastPosition == mItemCount - 1 && childCount > 0) {
+            // Get the last child ...
+            final View lastChild = getChildAt(childCount - 1);
+
+            // ... and its bottom edge
+            final int lastBottom = lastChild.getBottom();
+            // This is bottom of our drawable area
+            final int end = (mBottom - mTop) - mListPadding.bottom;
+
+            // This is how far the bottom edge of the last view is from the bottom of the
+            // drawable area
+            int bottomOffset = end - lastBottom;
+
+            final View firstChild = getChildAt(0);
+            final int firstTop = firstChild.getTop();
+
+            // Make sure we are 1) Too high, and 2) Either there are more rows above the
+            // first row or the first row is scrolled off the top of the drawable area
+            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
+                if (mFirstPosition == 0) {
+                    // Don't pull the top too far down
+                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
+                }
+
+                // Move everything down
+                offsetChildrenTopAndBottom(bottomOffset);
+                if (mFirstPosition > 0) {
+                    // Fill the gap that was opened above mFirstPosition with more rows, if
+                    // possible
+                    fillUp(mFirstPosition - (mStackFromBottom ? 1 : numColumns),
+                            firstChild.getTop() - verticalSpacing);
+                    // Close up the remaining gap
+                    adjustViewsUpOrDown();
+                }
+            }
+        }
+    }
+
+    private void correctTooLow(int numColumns, int verticalSpacing, int childCount) {
+        if (mFirstPosition == 0 && childCount > 0) {
+            // Get the first child ...
+            final View firstChild = getChildAt(0);
+
+            // ... and its top edge
+            final int firstTop = firstChild.getTop();
+
+            // This is top of our drawable area
+            final int start = mListPadding.top;
+
+            // This is bottom of our drawable area
+            final int end = (mBottom - mTop) - mListPadding.bottom;
+
+            // This is how far the top edge of the first view is from the top of the
+            // drawable area
+            int topOffset = firstTop - start;
+            final View lastChild = getChildAt(childCount - 1);
+            final int lastBottom = lastChild.getBottom();
+            final int lastPosition = mFirstPosition + childCount - 1;
+
+            // Make sure we are 1) Too low, and 2) Either there are more rows below the
+            // last row or the last row is scrolled off the bottom of the drawable area
+            if (topOffset > 0 && (lastPosition < mItemCount - 1 || lastBottom > end))  {
+                if (lastPosition == mItemCount - 1 ) {
+                    // Don't pull the bottom too far up
+                    topOffset = Math.min(topOffset, lastBottom - end);
+                }
+
+                // Move everything up
+                offsetChildrenTopAndBottom(-topOffset);
+                if (lastPosition < mItemCount - 1) {
+                    // Fill the gap that was opened below the last position with more rows, if
+                    // possible
+                    fillDown(lastPosition + (!mStackFromBottom ? 1 : numColumns),
+                            lastChild.getBottom() + verticalSpacing);
+                    // Close up the remaining gap
+                    adjustViewsUpOrDown();
+                }
+            }
+        }
+    }
+
+    /**
+     * Fills the grid based on positioning the new selection at a specific
+     * location. The selection may be moved so that it does not intersect the
+     * faded edges. The grid is then filled upwards and downwards from there.
+     *
+     * @param selectedTop Where the selected item should be
+     * @param childrenTop Where to start drawing children
+     * @param childrenBottom Last pixel where children can be drawn
+     * @return The view that currently has selection
+     */
+    private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
+        final int fadingEdgeLength = getVerticalFadingEdgeLength();
+        final int selectedPosition = mSelectedPosition;
+        final int numColumns = mNumColumns;
+        final int verticalSpacing = mVerticalSpacing;
+
+        int rowStart;
+        int rowEnd = -1;
+
+        if (!mStackFromBottom) {
+            rowStart = selectedPosition - (selectedPosition % numColumns);
+        } else {
+            int invertedSelection = mItemCount - 1 - selectedPosition;
+
+            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+            rowStart = Math.max(0, rowEnd - numColumns + 1);
+        }
+
+        View sel;
+        View referenceView;
+
+        int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+        int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+                numColumns, rowStart);
+
+        sel = makeRow(mStackFromBottom ? rowEnd : rowStart, selectedTop, true);
+        // Possibly changed again in fillUp if we add rows above this one.
+        mFirstPosition = rowStart;
+
+        referenceView = mReferenceView;
+        adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+        adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+
+        if (!mStackFromBottom) {
+            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+            adjustViewsUpOrDown();
+            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+        } else {
+            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+            adjustViewsUpOrDown();
+            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+        }
+
+
+        return sel;
+    }
+
+    /**
+     * Calculate the bottom-most pixel we can draw the selection into
+     *
+     * @param childrenBottom Bottom pixel were children can be drawn
+     * @param fadingEdgeLength Length of the fading edge in pixels, if present
+     * @param numColumns Number of columns in the grid
+     * @param rowStart The start of the row that will contain the selection
+     * @return The bottom-most pixel we can draw the selection into
+     */
+    private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
+            int numColumns, int rowStart) {
+        // Last pixel we can draw the selection into
+        int bottomSelectionPixel = childrenBottom;
+        if (rowStart + numColumns - 1 < mItemCount - 1) {
+            bottomSelectionPixel -= fadingEdgeLength;
+        }
+        return bottomSelectionPixel;
+    }
+
+    /**
+     * Calculate the top-most pixel we can draw the selection into
+     *
+     * @param childrenTop Top pixel were children can be drawn
+     * @param fadingEdgeLength Length of the fading edge in pixels, if present
+     * @param rowStart The start of the row that will contain the selection
+     * @return The top-most pixel we can draw the selection into
+     */
+    private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int rowStart) {
+        // first pixel we can draw the selection into
+        int topSelectionPixel = childrenTop;
+        if (rowStart > 0) {
+            topSelectionPixel += fadingEdgeLength;
+        }
+        return topSelectionPixel;
+    }
+
+    /**
+     * Move all views upwards so the selected row does not interesect the bottom
+     * fading edge (if necessary).
+     *
+     * @param childInSelectedRow A child in the row that contains the selection
+     * @param topSelectionPixel The topmost pixel we can draw the selection into
+     * @param bottomSelectionPixel The bottommost pixel we can draw the
+     *        selection into
+     */
+    private void adjustForBottomFadingEdge(View childInSelectedRow,
+            int topSelectionPixel, int bottomSelectionPixel) {
+        // Some of the newly selected item extends below the bottom of the
+        // list
+        if (childInSelectedRow.getBottom() > bottomSelectionPixel) {
+
+            // Find space available above the selection into which we can
+            // scroll upwards
+            int spaceAbove = childInSelectedRow.getTop() - topSelectionPixel;
+
+            // Find space required to bring the bottom of the selected item
+            // fully into view
+            int spaceBelow = childInSelectedRow.getBottom() - bottomSelectionPixel;
+            int offset = Math.min(spaceAbove, spaceBelow);
+
+            // Now offset the selected item to get it into view
+            offsetChildrenTopAndBottom(-offset);
+        }
+    }
+
+    /**
+     * Move all views upwards so the selected row does not interesect the top
+     * fading edge (if necessary).
+     *
+     * @param childInSelectedRow A child in the row that contains the selection
+     * @param topSelectionPixel The topmost pixel we can draw the selection into
+     * @param bottomSelectionPixel The bottommost pixel we can draw the
+     *        selection into
+     */
+    private void adjustForTopFadingEdge(View childInSelectedRow,
+            int topSelectionPixel, int bottomSelectionPixel) {
+        // Some of the newly selected item extends above the top of the list
+        if (childInSelectedRow.getTop() < topSelectionPixel) {
+            // Find space required to bring the top of the selected item
+            // fully into view
+            int spaceAbove = topSelectionPixel - childInSelectedRow.getTop();
+
+            // Find space available below the selection into which we can
+            // scroll downwards
+            int spaceBelow = bottomSelectionPixel - childInSelectedRow.getBottom();
+            int offset = Math.min(spaceAbove, spaceBelow);
+
+            // Now offset the selected item to get it into view
+            offsetChildrenTopAndBottom(offset);
+        }
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will
+     * scroll such that the indicated position is displayed.
+     * @param position Scroll to this adapter position.
+     */
+    @android.view.RemotableViewMethod
+    public void smoothScrollToPosition(int position) {
+        super.smoothScrollToPosition(position);
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position offset. The view will
+     * scroll such that the indicated position is displayed.
+     * @param offset The amount to offset from the adapter position to scroll to.
+     */
+    @android.view.RemotableViewMethod
+    public void smoothScrollByOffset(int offset) {
+        super.smoothScrollByOffset(offset);
+    }
+
+    /**
+     * Fills the grid based on positioning the new selection relative to the old
+     * selection. The new selection will be placed at, above, or below the
+     * location of the new selection depending on how the selection is moving.
+     * The selection will then be pinned to the visible part of the screen,
+     * excluding the edges that are faded. The grid is then filled upwards and
+     * downwards from there.
+     *
+     * @param delta Which way we are moving
+     * @param childrenTop Where to start drawing children
+     * @param childrenBottom Last pixel where children can be drawn
+     * @return The view that currently has selection
+     */
+    private View moveSelection(int delta, int childrenTop, int childrenBottom) {
+        final int fadingEdgeLength = getVerticalFadingEdgeLength();
+        final int selectedPosition = mSelectedPosition;
+        final int numColumns = mNumColumns;
+        final int verticalSpacing = mVerticalSpacing;
+
+        int oldRowStart;
+        int rowStart;
+        int rowEnd = -1;
+
+        if (!mStackFromBottom) {
+            oldRowStart = (selectedPosition - delta) - ((selectedPosition - delta) % numColumns);
+
+            rowStart = selectedPosition - (selectedPosition % numColumns);
+        } else {
+            int invertedSelection = mItemCount - 1 - selectedPosition;
+
+            rowEnd = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+            rowStart = Math.max(0, rowEnd - numColumns + 1);
+
+            invertedSelection = mItemCount - 1 - (selectedPosition - delta);
+            oldRowStart = mItemCount - 1 - (invertedSelection - (invertedSelection % numColumns));
+            oldRowStart = Math.max(0, oldRowStart - numColumns + 1);
+        }
+
+        final int rowDelta = rowStart - oldRowStart;
+
+        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength, rowStart);
+        final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+                numColumns, rowStart);
+
+        // Possibly changed again in fillUp if we add rows above this one.
+        mFirstPosition = rowStart;
+
+        View sel;
+        View referenceView;
+
+        if (rowDelta > 0) {
+            /*
+             * Case 1: Scrolling down.
+             */
+
+            final int oldBottom = mReferenceViewInSelectedRow == null ? 0 :
+                    mReferenceViewInSelectedRow.getBottom();
+
+            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldBottom + verticalSpacing, true);
+            referenceView = mReferenceView;
+
+            adjustForBottomFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+        } else if (rowDelta < 0) {
+            /*
+             * Case 2: Scrolling up.
+             */
+            final int oldTop = mReferenceViewInSelectedRow == null ?
+                    0 : mReferenceViewInSelectedRow .getTop();
+
+            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop - verticalSpacing, false);
+            referenceView = mReferenceView;
+
+            adjustForTopFadingEdge(referenceView, topSelectionPixel, bottomSelectionPixel);
+        } else {
+            /*
+             * Keep selection where it was
+             */
+            final int oldTop = mReferenceViewInSelectedRow == null ?
+                    0 : mReferenceViewInSelectedRow .getTop();
+
+            sel = makeRow(mStackFromBottom ? rowEnd : rowStart, oldTop, true);
+            referenceView = mReferenceView;
+        }
+
+        if (!mStackFromBottom) {
+            fillUp(rowStart - numColumns, referenceView.getTop() - verticalSpacing);
+            adjustViewsUpOrDown();
+            fillDown(rowStart + numColumns, referenceView.getBottom() + verticalSpacing);
+        } else {
+            fillDown(rowEnd + numColumns, referenceView.getBottom() + verticalSpacing);
+            adjustViewsUpOrDown();
+            fillUp(rowStart - 1, referenceView.getTop() - verticalSpacing);
+        }
+
+        return sel;
+    }
+
+    private boolean determineColumns(int availableSpace) {
+        final int requestedHorizontalSpacing = mRequestedHorizontalSpacing;
+        final int stretchMode = mStretchMode;
+        final int requestedColumnWidth = mRequestedColumnWidth;
+        boolean didNotInitiallyFit = false;
+
+        if (mRequestedNumColumns == AUTO_FIT) {
+            if (requestedColumnWidth > 0) {
+                // Client told us to pick the number of columns
+                mNumColumns = (availableSpace + requestedHorizontalSpacing) /
+                        (requestedColumnWidth + requestedHorizontalSpacing);
+            } else {
+                // Just make up a number if we don't have enough info
+                mNumColumns = 2;
+            }
+        } else {
+            // We picked the columns
+            mNumColumns = mRequestedNumColumns;
+        }
+
+        if (mNumColumns <= 0) {
+            mNumColumns = 1;
+        }
+
+        switch (stretchMode) {
+            case NO_STRETCH:
+                // Nobody stretches
+                mColumnWidth = requestedColumnWidth;
+                mHorizontalSpacing = requestedHorizontalSpacing;
+                break;
+
+            default:
+                int spaceLeftOver = availableSpace - (mNumColumns * requestedColumnWidth)
+                        - ((mNumColumns - 1) * requestedHorizontalSpacing);
+
+                if (spaceLeftOver < 0) {
+                    didNotInitiallyFit = true;
+                }
+
+                switch (stretchMode) {
+                    case STRETCH_COLUMN_WIDTH:
+                        // Stretch the columns
+                        mColumnWidth = requestedColumnWidth + spaceLeftOver / mNumColumns;
+                        mHorizontalSpacing = requestedHorizontalSpacing;
+                        break;
+
+                    case STRETCH_SPACING:
+                        // Stretch the spacing between columns
+                        mColumnWidth = requestedColumnWidth;
+                        if (mNumColumns > 1) {
+                            mHorizontalSpacing = requestedHorizontalSpacing
+                                    + spaceLeftOver / (mNumColumns - 1);
+                        } else {
+                            mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
+                        }
+                        break;
+
+                    case STRETCH_SPACING_UNIFORM:
+                        // Stretch the spacing between columns
+                        mColumnWidth = requestedColumnWidth;
+                        if (mNumColumns > 1) {
+                            mHorizontalSpacing = requestedHorizontalSpacing
+                                    + spaceLeftOver / (mNumColumns + 1);
+                        } else {
+                            mHorizontalSpacing = requestedHorizontalSpacing + spaceLeftOver;
+                        }
+                        break;
+                }
+
+                break;
+        }
+        return didNotInitiallyFit;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Sets up mListPadding
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        if (widthMode == MeasureSpec.UNSPECIFIED) {
+            if (mColumnWidth > 0) {
+                widthSize = mColumnWidth + mListPadding.left + mListPadding.right;
+            } else {
+                widthSize = mListPadding.left + mListPadding.right;
+            }
+            widthSize += getVerticalScrollbarWidth();
+        }
+
+        int childWidth = widthSize - mListPadding.left - mListPadding.right;
+        boolean didNotInitiallyFit = determineColumns(childWidth);
+
+        int childHeight = 0;
+        int childState = 0;
+
+        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+        final int count = mItemCount;
+        if (count > 0) {
+            final View child = obtainView(0, mIsScrap);
+
+            AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
+            if (p == null) {
+                p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+                child.setLayoutParams(p);
+            }
+            p.viewType = mAdapter.getItemViewType(0);
+            p.isEnabled = mAdapter.isEnabled(0);
+            p.forceAdd = true;
+
+            int childHeightSpec = getChildMeasureSpec(
+                    MeasureSpec.makeSafeMeasureSpec(MeasureSpec.getSize(heightMeasureSpec),
+                            MeasureSpec.UNSPECIFIED), 0, p.height);
+            int childWidthSpec = getChildMeasureSpec(
+                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
+            child.measure(childWidthSpec, childHeightSpec);
+
+            childHeight = child.getMeasuredHeight();
+            childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+            if (mRecycler.shouldRecycleViewType(p.viewType)) {
+                mRecycler.addScrapView(child, -1);
+            }
+        }
+
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
+                    getVerticalFadingEdgeLength() * 2;
+        }
+
+        if (heightMode == MeasureSpec.AT_MOST) {
+            int ourSize =  mListPadding.top + mListPadding.bottom;
+
+            final int numColumns = mNumColumns;
+            for (int i = 0; i < count; i += numColumns) {
+                ourSize += childHeight;
+                if (i + numColumns < count) {
+                    ourSize += mVerticalSpacing;
+                }
+                if (ourSize >= heightSize) {
+                    ourSize = heightSize;
+                    break;
+                }
+            }
+            heightSize = ourSize;
+        }
+
+        if (widthMode == MeasureSpec.AT_MOST && mRequestedNumColumns != AUTO_FIT) {
+            int ourSize = (mRequestedNumColumns*mColumnWidth)
+                    + ((mRequestedNumColumns-1)*mHorizontalSpacing)
+                    + mListPadding.left + mListPadding.right;
+            if (ourSize > widthSize || didNotInitiallyFit) {
+                widthSize |= MEASURED_STATE_TOO_SMALL;
+            }
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+        mWidthMeasureSpec = widthMeasureSpec;
+    }
+
+    @Override
+    protected void attachLayoutAnimationParameters(View child,
+            ViewGroup.LayoutParams params, int index, int count) {
+
+        GridLayoutAnimationController.AnimationParameters animationParams =
+                (GridLayoutAnimationController.AnimationParameters) params.layoutAnimationParameters;
+
+        if (animationParams == null) {
+            animationParams = new GridLayoutAnimationController.AnimationParameters();
+            params.layoutAnimationParameters = animationParams;
+        }
+
+        animationParams.count = count;
+        animationParams.index = index;
+        animationParams.columnsCount = mNumColumns;
+        animationParams.rowsCount = count / mNumColumns;
+
+        if (!mStackFromBottom) {
+            animationParams.column = index % mNumColumns;
+            animationParams.row = index / mNumColumns;
+        } else {
+            final int invertedIndex = count - 1 - index;
+
+            animationParams.column = mNumColumns - 1 - (invertedIndex % mNumColumns);
+            animationParams.row = animationParams.rowsCount - 1 - invertedIndex / mNumColumns;
+        }
+    }
+
+    @Override
+    protected void layoutChildren() {
+        final boolean blockLayoutRequests = mBlockLayoutRequests;
+        if (!blockLayoutRequests) {
+            mBlockLayoutRequests = true;
+        }
+
+        try {
+            super.layoutChildren();
+
+            invalidate();
+
+            if (mAdapter == null) {
+                resetList();
+                invokeOnItemScrollListener();
+                return;
+            }
+
+            final int childrenTop = mListPadding.top;
+            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
+
+            int childCount = getChildCount();
+            int index;
+            int delta = 0;
+
+            View sel;
+            View oldSel = null;
+            View oldFirst = null;
+            View newSel = null;
+
+            // Remember stuff we will need down below
+            switch (mLayoutMode) {
+            case LAYOUT_SET_SELECTION:
+                index = mNextSelectedPosition - mFirstPosition;
+                if (index >= 0 && index < childCount) {
+                    newSel = getChildAt(index);
+                }
+                break;
+            case LAYOUT_FORCE_TOP:
+            case LAYOUT_FORCE_BOTTOM:
+            case LAYOUT_SPECIFIC:
+            case LAYOUT_SYNC:
+                break;
+            case LAYOUT_MOVE_SELECTION:
+                if (mNextSelectedPosition >= 0) {
+                    delta = mNextSelectedPosition - mSelectedPosition;
+                }
+                break;
+            default:
+                // Remember the previously selected view
+                index = mSelectedPosition - mFirstPosition;
+                if (index >= 0 && index < childCount) {
+                    oldSel = getChildAt(index);
+                }
+
+                // Remember the previous first child
+                oldFirst = getChildAt(0);
+            }
+
+            boolean dataChanged = mDataChanged;
+            if (dataChanged) {
+                handleDataChanged();
+            }
+
+            // Handle the empty set by removing all views that are visible
+            // and calling it a day
+            if (mItemCount == 0) {
+                resetList();
+                invokeOnItemScrollListener();
+                return;
+            }
+
+            setSelectedPositionInt(mNextSelectedPosition);
+
+            AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
+            View accessibilityFocusLayoutRestoreView = null;
+            int accessibilityFocusPosition = INVALID_POSITION;
+
+            // Remember which child, if any, had accessibility focus. This must
+            // occur before recycling any views, since that will clear
+            // accessibility focus.
+            final ViewRootImpl viewRootImpl = getViewRootImpl();
+            if (viewRootImpl != null) {
+                final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
+                if (focusHost != null) {
+                    final View focusChild = getAccessibilityFocusedChild(focusHost);
+                    if (focusChild != null) {
+                        if (!dataChanged || focusChild.hasTransientState()
+                                || mAdapterHasStableIds) {
+                            // The views won't be changing, so try to maintain
+                            // focus on the current host and virtual view.
+                            accessibilityFocusLayoutRestoreView = focusHost;
+                            accessibilityFocusLayoutRestoreNode = viewRootImpl
+                                    .getAccessibilityFocusedVirtualView();
+                        }
+
+                        // Try to maintain focus at the same position.
+                        accessibilityFocusPosition = getPositionForView(focusChild);
+                    }
+                }
+            }
+
+            // Pull all children into the RecycleBin.
+            // These views will be reused if possible
+            final int firstPosition = mFirstPosition;
+            final RecycleBin recycleBin = mRecycler;
+
+            if (dataChanged) {
+                for (int i = 0; i < childCount; i++) {
+                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
+                }
+            } else {
+                recycleBin.fillActiveViews(childCount, firstPosition);
+            }
+
+            // Clear out old views
+            detachAllViewsFromParent();
+            recycleBin.removeSkippedScrap();
+
+            switch (mLayoutMode) {
+            case LAYOUT_SET_SELECTION:
+                if (newSel != null) {
+                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
+                } else {
+                    sel = fillSelection(childrenTop, childrenBottom);
+                }
+                break;
+            case LAYOUT_FORCE_TOP:
+                mFirstPosition = 0;
+                sel = fillFromTop(childrenTop);
+                adjustViewsUpOrDown();
+                break;
+            case LAYOUT_FORCE_BOTTOM:
+                sel = fillUp(mItemCount - 1, childrenBottom);
+                adjustViewsUpOrDown();
+                break;
+            case LAYOUT_SPECIFIC:
+                sel = fillSpecific(mSelectedPosition, mSpecificTop);
+                break;
+            case LAYOUT_SYNC:
+                sel = fillSpecific(mSyncPosition, mSpecificTop);
+                break;
+            case LAYOUT_MOVE_SELECTION:
+                // Move the selection relative to its old position
+                sel = moveSelection(delta, childrenTop, childrenBottom);
+                break;
+            default:
+                if (childCount == 0) {
+                    if (!mStackFromBottom) {
+                        setSelectedPositionInt(mAdapter == null || isInTouchMode() ?
+                                INVALID_POSITION : 0);
+                        sel = fillFromTop(childrenTop);
+                    } else {
+                        final int last = mItemCount - 1;
+                        setSelectedPositionInt(mAdapter == null || isInTouchMode() ?
+                                INVALID_POSITION : last);
+                        sel = fillFromBottom(last, childrenBottom);
+                    }
+                } else {
+                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
+                        sel = fillSpecific(mSelectedPosition, oldSel == null ?
+                                childrenTop : oldSel.getTop());
+                    } else if (mFirstPosition < mItemCount)  {
+                        sel = fillSpecific(mFirstPosition, oldFirst == null ?
+                                childrenTop : oldFirst.getTop());
+                    } else {
+                        sel = fillSpecific(0, childrenTop);
+                    }
+                }
+                break;
+            }
+
+            // Flush any cached views that did not get reused above
+            recycleBin.scrapActiveViews();
+
+            if (sel != null) {
+               positionSelector(INVALID_POSITION, sel);
+               mSelectedTop = sel.getTop();
+            } else {
+                final boolean inTouchMode = mTouchMode > TOUCH_MODE_DOWN
+                        && mTouchMode < TOUCH_MODE_SCROLL;
+                if (inTouchMode) {
+                    // If the user's finger is down, select the motion position.
+                    final View child = getChildAt(mMotionPosition - mFirstPosition);
+                    if (child != null) {
+                        positionSelector(mMotionPosition, child);
+                    }
+                } else if (mSelectedPosition != INVALID_POSITION) {
+                    // If we had previously positioned the selector somewhere,
+                    // put it back there. It might not match up with the data,
+                    // but it's transitioning out so it's not a big deal.
+                    final View child = getChildAt(mSelectorPosition - mFirstPosition);
+                    if (child != null) {
+                        positionSelector(mSelectorPosition, child);
+                    }
+                } else {
+                    // Otherwise, clear selection.
+                    mSelectedTop = 0;
+                    mSelectorRect.setEmpty();
+                }
+            }
+
+            // Attempt to restore accessibility focus, if necessary.
+            if (viewRootImpl != null) {
+                final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
+                if (newAccessibilityFocusedView == null) {
+                    if (accessibilityFocusLayoutRestoreView != null
+                            && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
+                        final AccessibilityNodeProvider provider =
+                                accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
+                        if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
+                            final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
+                                    accessibilityFocusLayoutRestoreNode.getSourceNodeId());
+                            provider.performAction(virtualViewId,
+                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+                        } else {
+                            accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
+                        }
+                    } else if (accessibilityFocusPosition != INVALID_POSITION) {
+                        // Bound the position within the visible children.
+                        final int position = MathUtils.constrain(
+                                accessibilityFocusPosition - mFirstPosition, 0,
+                                getChildCount() - 1);
+                        final View restoreView = getChildAt(position);
+                        if (restoreView != null) {
+                            restoreView.requestAccessibilityFocus();
+                        }
+                    }
+                }
+            }
+
+            mLayoutMode = LAYOUT_NORMAL;
+            mDataChanged = false;
+            if (mPositionScrollAfterLayout != null) {
+                post(mPositionScrollAfterLayout);
+                mPositionScrollAfterLayout = null;
+            }
+            mNeedSync = false;
+            setNextSelectedPositionInt(mSelectedPosition);
+
+            updateScrollIndicators();
+
+            if (mItemCount > 0) {
+                checkSelectionChanged();
+            }
+
+            invokeOnItemScrollListener();
+        } finally {
+            if (!blockLayoutRequests) {
+                mBlockLayoutRequests = false;
+            }
+        }
+    }
+
+
+    /**
+     * Obtains the view and adds it to our list of children. The view can be
+     * made fresh, converted from an unused view, or used as is if it was in
+     * the recycle bin.
+     *
+     * @param position logical position in the list
+     * @param y top or bottom edge of the view to add
+     * @param flow {@code true} to align top edge to y, {@code false} to align
+     *             bottom edge to y
+     * @param childrenLeft left edge where children should be positioned
+     * @param selected {@code true} if the position is selected, {@code false}
+     *                 otherwise
+     * @param where position at which to add new item in the list
+     * @return View that was added
+     */
+    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
+            boolean selected, int where) {
+        if (!mDataChanged) {
+            // Try to use an existing view for this position
+            final View activeView = mRecycler.getActiveView(position);
+            if (activeView != null) {
+                // Found it -- we're using an existing child
+                // This just needs to be positioned
+                setupChild(activeView, position, y, flow, childrenLeft, selected, true, where);
+                return activeView;
+            }
+        }
+
+        // Make a new view for this position, or convert an unused view if
+        // possible.
+        final View child = obtainView(position, mIsScrap);
+
+        // This needs to be positioned and measured.
+        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0], where);
+
+        return child;
+    }
+
+    /**
+     * Adds a view as a child and make sure it is measured (if necessary) and
+     * positioned properly.
+     *
+     * @param child the view to add
+     * @param position the position of this child
+     * @param y the y position relative to which this view will be positioned
+     * @param flowDown {@code true} to align top edge to y, {@code false} to
+     *                 align bottom edge to y
+     * @param childrenLeft left edge where children should be positioned
+     * @param selected {@code true} if the position is selected, {@code false}
+     *                 otherwise
+     * @param isAttachedToWindow {@code true} if the view is already attached
+     *                           to the window, e.g. whether it was reused, or
+     *                           {@code false} otherwise
+     * @param where position at which to add new item in the list
+     *
+     */
+    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
+            boolean selected, boolean isAttachedToWindow, int where) {
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupGridItem");
+
+        boolean isSelected = selected && shouldShowSelector();
+        final boolean updateChildSelected = isSelected != child.isSelected();
+        final int mode = mTouchMode;
+        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
+                && mMotionPosition == position;
+        final boolean updateChildPressed = isPressed != child.isPressed();
+        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
+                || child.isLayoutRequested();
+
+        // Respect layout params that are already in the view. Otherwise make
+        // some up...
+        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
+        if (p == null) {
+            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+        }
+        p.viewType = mAdapter.getItemViewType(position);
+        p.isEnabled = mAdapter.isEnabled(position);
+
+        // Set up view state before attaching the view, since we may need to
+        // rely on the jumpDrawablesToCurrentState() call that occurs as part
+        // of view attachment.
+        if (updateChildSelected) {
+            child.setSelected(isSelected);
+            if (isSelected) {
+                requestFocus();
+            }
+        }
+
+        if (updateChildPressed) {
+            child.setPressed(isPressed);
+        }
+
+        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+            if (child instanceof Checkable) {
+                ((Checkable) child).setChecked(mCheckStates.get(position));
+            } else if (getContext().getApplicationInfo().targetSdkVersion
+                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+                child.setActivated(mCheckStates.get(position));
+            }
+        }
+
+        if (isAttachedToWindow && !p.forceAdd) {
+            attachViewToParent(child, where, p);
+
+            // If the view isn't attached, or if it's attached but for a different
+            // position, then jump the drawables.
+            if (!isAttachedToWindow
+                    || (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
+                            != position) {
+                child.jumpDrawablesToCurrentState();
+            }
+        } else {
+            p.forceAdd = false;
+            addViewInLayout(child, where, p, true);
+        }
+
+        if (needToMeasure) {
+            int childHeightSpec = ViewGroup.getChildMeasureSpec(
+                    MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), 0, p.height);
+
+            int childWidthSpec = ViewGroup.getChildMeasureSpec(
+                    MeasureSpec.makeMeasureSpec(mColumnWidth, MeasureSpec.EXACTLY), 0, p.width);
+            child.measure(childWidthSpec, childHeightSpec);
+        } else {
+            cleanupLayoutState(child);
+        }
+
+        final int w = child.getMeasuredWidth();
+        final int h = child.getMeasuredHeight();
+
+        int childLeft;
+        final int childTop = flowDown ? y : y - h;
+
+        final int layoutDirection = getLayoutDirection();
+        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+        switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+            case Gravity.LEFT:
+                childLeft = childrenLeft;
+                break;
+            case Gravity.CENTER_HORIZONTAL:
+                childLeft = childrenLeft + ((mColumnWidth - w) / 2);
+                break;
+            case Gravity.RIGHT:
+                childLeft = childrenLeft + mColumnWidth - w;
+                break;
+            default:
+                childLeft = childrenLeft;
+                break;
+        }
+
+        if (needToMeasure) {
+            final int childRight = childLeft + w;
+            final int childBottom = childTop + h;
+            child.layout(childLeft, childTop, childRight, childBottom);
+        } else {
+            child.offsetLeftAndRight(childLeft - child.getLeft());
+            child.offsetTopAndBottom(childTop - child.getTop());
+        }
+
+        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
+            child.setDrawingCacheEnabled(true);
+        }
+
+        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+    }
+
+    /**
+     * Sets the currently selected item
+     *
+     * @param position Index (starting at 0) of the data item to be selected.
+     *
+     * If in touch mode, the item will not be selected but it will still be positioned
+     * appropriately.
+     */
+    @Override
+    public void setSelection(int position) {
+        if (!isInTouchMode()) {
+            setNextSelectedPositionInt(position);
+        } else {
+            mResurrectToPosition = position;
+        }
+        mLayoutMode = LAYOUT_SET_SELECTION;
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+        requestLayout();
+    }
+
+    /**
+     * Makes the item at the supplied position selected.
+     *
+     * @param position the position of the new selection
+     */
+    @Override
+    void setSelectionInt(int position) {
+        int previousSelectedPosition = mNextSelectedPosition;
+
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+
+        setNextSelectedPositionInt(position);
+        layoutChildren();
+
+        final int next = mStackFromBottom ? mItemCount - 1  - mNextSelectedPosition :
+            mNextSelectedPosition;
+        final int previous = mStackFromBottom ? mItemCount - 1
+                - previousSelectedPosition : previousSelectedPosition;
+
+        final int nextRow = next / mNumColumns;
+        final int previousRow = previous / mNumColumns;
+
+        if (nextRow != previousRow) {
+            awakenScrollBars();
+        }
+
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return commonKey(keyCode, 1, event);
+    }
+
+    @Override
+    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+        return commonKey(keyCode, repeatCount, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return commonKey(keyCode, 1, event);
+    }
+
+    private boolean commonKey(int keyCode, int count, KeyEvent event) {
+        if (mAdapter == null) {
+            return false;
+        }
+
+        if (mDataChanged) {
+            layoutChildren();
+        }
+
+        boolean handled = false;
+        int action = event.getAction();
+        if (KeyEvent.isConfirmKey(keyCode)
+                && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
+            handled = resurrectSelectionIfNeeded();
+            if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
+                keyPressed();
+                handled = true;
+            }
+        }
+
+        if (!handled && action != KeyEvent.ACTION_UP) {
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_LEFT);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_RIGHT);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_PAGE_UP:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_PAGE_DOWN:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
+                    } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_MOVE_HOME:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_MOVE_END:
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                    }
+                    break;
+
+                case KeyEvent.KEYCODE_TAB:
+                    // TODO: Sometimes it is useful to be able to TAB through the items in
+                    //     a GridView sequentially.  Unfortunately this can create an
+                    //     asymmetry in TAB navigation order unless the list selection
+                    //     always reverts to the top or bottom when receiving TAB focus from
+                    //     another widget.
+                    if (event.hasNoModifiers()) {
+                        handled = resurrectSelectionIfNeeded()
+                                || sequenceScroll(FOCUS_FORWARD);
+                    } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                        handled = resurrectSelectionIfNeeded()
+                                || sequenceScroll(FOCUS_BACKWARD);
+                    }
+                    break;
+            }
+        }
+
+        if (handled) {
+            return true;
+        }
+
+        if (sendToTextFilter(keyCode, count, event)) {
+            return true;
+        }
+
+        switch (action) {
+            case KeyEvent.ACTION_DOWN:
+                return super.onKeyDown(keyCode, event);
+            case KeyEvent.ACTION_UP:
+                return super.onKeyUp(keyCode, event);
+            case KeyEvent.ACTION_MULTIPLE:
+                return super.onKeyMultiple(keyCode, count, event);
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * Scrolls up or down by the number of items currently present on screen.
+     *
+     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+     * @return whether selection was moved
+     */
+    boolean pageScroll(int direction) {
+        int nextPage = -1;
+
+        if (direction == FOCUS_UP) {
+            nextPage = Math.max(0, mSelectedPosition - getChildCount());
+        } else if (direction == FOCUS_DOWN) {
+            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount());
+        }
+
+        if (nextPage >= 0) {
+            setSelectionInt(nextPage);
+            invokeOnItemScrollListener();
+            awakenScrollBars();
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Go to the last or first item if possible.
+     *
+     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}.
+     *
+     * @return Whether selection was moved.
+     */
+    boolean fullScroll(int direction) {
+        boolean moved = false;
+        if (direction == FOCUS_UP) {
+            mLayoutMode = LAYOUT_SET_SELECTION;
+            setSelectionInt(0);
+            invokeOnItemScrollListener();
+            moved = true;
+        } else if (direction == FOCUS_DOWN) {
+            mLayoutMode = LAYOUT_SET_SELECTION;
+            setSelectionInt(mItemCount - 1);
+            invokeOnItemScrollListener();
+            moved = true;
+        }
+
+        if (moved) {
+            awakenScrollBars();
+        }
+
+        return moved;
+    }
+
+    /**
+     * Scrolls to the next or previous item, horizontally or vertically.
+     *
+     * @param direction either {@link View#FOCUS_LEFT}, {@link View#FOCUS_RIGHT},
+     *        {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+     *
+     * @return whether selection was moved
+     */
+    boolean arrowScroll(int direction) {
+        final int selectedPosition = mSelectedPosition;
+        final int numColumns = mNumColumns;
+
+        int startOfRowPos;
+        int endOfRowPos;
+
+        boolean moved = false;
+
+        if (!mStackFromBottom) {
+            startOfRowPos = (selectedPosition / numColumns) * numColumns;
+            endOfRowPos = Math.min(startOfRowPos + numColumns - 1, mItemCount - 1);
+        } else {
+            final int invertedSelection = mItemCount - 1 - selectedPosition;
+            endOfRowPos = mItemCount - 1 - (invertedSelection / numColumns) * numColumns;
+            startOfRowPos = Math.max(0, endOfRowPos - numColumns + 1);
+        }
+
+        switch (direction) {
+            case FOCUS_UP:
+                if (startOfRowPos > 0) {
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(Math.max(0, selectedPosition - numColumns));
+                    moved = true;
+                }
+                break;
+            case FOCUS_DOWN:
+                if (endOfRowPos < mItemCount - 1) {
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(Math.min(selectedPosition + numColumns, mItemCount - 1));
+                    moved = true;
+                }
+                break;
+        }
+
+        final boolean isLayoutRtl = isLayoutRtl();
+        if (selectedPosition > startOfRowPos && ((direction == FOCUS_LEFT && !isLayoutRtl) ||
+                (direction == FOCUS_RIGHT && isLayoutRtl))) {
+            mLayoutMode = LAYOUT_MOVE_SELECTION;
+            setSelectionInt(Math.max(0, selectedPosition - 1));
+            moved = true;
+        } else if (selectedPosition < endOfRowPos && ((direction == FOCUS_LEFT && isLayoutRtl) ||
+                (direction == FOCUS_RIGHT && !isLayoutRtl))) {
+            mLayoutMode = LAYOUT_MOVE_SELECTION;
+            setSelectionInt(Math.min(selectedPosition + 1, mItemCount - 1));
+            moved = true;
+        }
+
+        if (moved) {
+            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+            invokeOnItemScrollListener();
+        }
+
+        if (moved) {
+            awakenScrollBars();
+        }
+
+        return moved;
+    }
+
+    /**
+     * Goes to the next or previous item according to the order set by the
+     * adapter.
+     */
+    boolean sequenceScroll(int direction) {
+        int selectedPosition = mSelectedPosition;
+        int numColumns = mNumColumns;
+        int count = mItemCount;
+
+        int startOfRow;
+        int endOfRow;
+        if (!mStackFromBottom) {
+            startOfRow = (selectedPosition / numColumns) * numColumns;
+            endOfRow = Math.min(startOfRow + numColumns - 1, count - 1);
+        } else {
+            int invertedSelection = count - 1 - selectedPosition;
+            endOfRow = count - 1 - (invertedSelection / numColumns) * numColumns;
+            startOfRow = Math.max(0, endOfRow - numColumns + 1);
+        }
+
+        boolean moved = false;
+        boolean showScroll = false;
+        switch (direction) {
+            case FOCUS_FORWARD:
+                if (selectedPosition < count - 1) {
+                    // Move to the next item.
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(selectedPosition + 1);
+                    moved = true;
+                    // Show the scrollbar only if changing rows.
+                    showScroll = selectedPosition == endOfRow;
+                }
+                break;
+
+            case FOCUS_BACKWARD:
+                if (selectedPosition > 0) {
+                    // Move to the previous item.
+                    mLayoutMode = LAYOUT_MOVE_SELECTION;
+                    setSelectionInt(selectedPosition - 1);
+                    moved = true;
+                    // Show the scrollbar only if changing rows.
+                    showScroll = selectedPosition == startOfRow;
+                }
+                break;
+        }
+
+        if (moved) {
+            playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+            invokeOnItemScrollListener();
+        }
+
+        if (showScroll) {
+            awakenScrollBars();
+        }
+
+        return moved;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+        int closestChildIndex = -1;
+        if (gainFocus && previouslyFocusedRect != null) {
+            previouslyFocusedRect.offset(mScrollX, mScrollY);
+
+            // figure out which item should be selected based on previously
+            // focused rect
+            Rect otherRect = mTempRect;
+            int minDistance = Integer.MAX_VALUE;
+            final int childCount = getChildCount();
+            for (int i = 0; i < childCount; i++) {
+                // only consider view's on appropriate edge of grid
+                if (!isCandidateSelection(i, direction)) {
+                    continue;
+                }
+
+                final View other = getChildAt(i);
+                other.getDrawingRect(otherRect);
+                offsetDescendantRectToMyCoords(other, otherRect);
+                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
+
+                if (distance < minDistance) {
+                    minDistance = distance;
+                    closestChildIndex = i;
+                }
+            }
+        }
+
+        if (closestChildIndex >= 0) {
+            setSelection(closestChildIndex + mFirstPosition);
+        } else {
+            requestLayout();
+        }
+    }
+
+    /**
+     * Is childIndex a candidate for next focus given the direction the focus
+     * change is coming from?
+     * @param childIndex The index to check.
+     * @param direction The direction, one of
+     *        {FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, FOCUS_FORWARD, FOCUS_BACKWARD}
+     * @return Whether childIndex is a candidate.
+     */
+    private boolean isCandidateSelection(int childIndex, int direction) {
+        final int count = getChildCount();
+        final int invertedIndex = count - 1 - childIndex;
+
+        int rowStart;
+        int rowEnd;
+
+        if (!mStackFromBottom) {
+            rowStart = childIndex - (childIndex % mNumColumns);
+            rowEnd = Math.min(rowStart + mNumColumns - 1, count);
+        } else {
+            rowEnd = count - 1 - (invertedIndex - (invertedIndex % mNumColumns));
+            rowStart = Math.max(0, rowEnd - mNumColumns + 1);
+        }
+
+        switch (direction) {
+            case View.FOCUS_RIGHT:
+                // coming from left, selection is only valid if it is on left
+                // edge
+                return childIndex == rowStart;
+            case View.FOCUS_DOWN:
+                // coming from top; only valid if in top row
+                return rowStart == 0;
+            case View.FOCUS_LEFT:
+                // coming from right, must be on right edge
+                return childIndex == rowEnd;
+            case View.FOCUS_UP:
+                // coming from bottom, need to be in last row
+                return rowEnd == count - 1;
+            case View.FOCUS_FORWARD:
+                // coming from top-left, need to be first in top row
+                return childIndex == rowStart && rowStart == 0;
+            case View.FOCUS_BACKWARD:
+                // coming from bottom-right, need to be last in bottom row
+                return childIndex == rowEnd && rowEnd == count - 1;
+            default:
+                throw new IllegalArgumentException("direction must be one of "
+                        + "{FOCUS_UP, FOCUS_DOWN, FOCUS_LEFT, FOCUS_RIGHT, "
+                        + "FOCUS_FORWARD, FOCUS_BACKWARD}.");
+        }
+    }
+
+    /**
+     * Set the gravity for this grid. Gravity describes how the child views
+     * are horizontally aligned. Defaults to Gravity.LEFT
+     *
+     * @param gravity the gravity to apply to this grid's children
+     *
+     * @attr ref android.R.styleable#GridView_gravity
+     */
+    public void setGravity(int gravity) {
+        if (mGravity != gravity) {
+            mGravity = gravity;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    /**
+     * Describes how the child views are horizontally aligned. Defaults to Gravity.LEFT
+     *
+     * @return the gravity that will be applied to this grid's children
+     *
+     * @attr ref android.R.styleable#GridView_gravity
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    /**
+     * Set the amount of horizontal (x) spacing to place between each item
+     * in the grid.
+     *
+     * @param horizontalSpacing The amount of horizontal space between items,
+     * in pixels.
+     *
+     * @attr ref android.R.styleable#GridView_horizontalSpacing
+     */
+    public void setHorizontalSpacing(int horizontalSpacing) {
+        if (horizontalSpacing != mRequestedHorizontalSpacing) {
+            mRequestedHorizontalSpacing = horizontalSpacing;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    /**
+     * Returns the amount of horizontal spacing currently used between each item in the grid.
+     *
+     * <p>This is only accurate for the current layout. If {@link #setHorizontalSpacing(int)}
+     * has been called but layout is not yet complete, this method may return a stale value.
+     * To get the horizontal spacing that was explicitly requested use
+     * {@link #getRequestedHorizontalSpacing()}.</p>
+     *
+     * @return Current horizontal spacing between each item in pixels
+     *
+     * @see #setHorizontalSpacing(int)
+     * @see #getRequestedHorizontalSpacing()
+     *
+     * @attr ref android.R.styleable#GridView_horizontalSpacing
+     */
+    public int getHorizontalSpacing() {
+        return mHorizontalSpacing;
+    }
+
+    /**
+     * Returns the requested amount of horizontal spacing between each item in the grid.
+     *
+     * <p>The value returned may have been supplied during inflation as part of a style,
+     * the default GridView style, or by a call to {@link #setHorizontalSpacing(int)}.
+     * If layout is not yet complete or if GridView calculated a different horizontal spacing
+     * from what was requested, this may return a different value from
+     * {@link #getHorizontalSpacing()}.</p>
+     *
+     * @return The currently requested horizontal spacing between items, in pixels
+     *
+     * @see #setHorizontalSpacing(int)
+     * @see #getHorizontalSpacing()
+     *
+     * @attr ref android.R.styleable#GridView_horizontalSpacing
+     */
+    public int getRequestedHorizontalSpacing() {
+        return mRequestedHorizontalSpacing;
+    }
+
+    /**
+     * Set the amount of vertical (y) spacing to place between each item
+     * in the grid.
+     *
+     * @param verticalSpacing The amount of vertical space between items,
+     * in pixels.
+     *
+     * @see #getVerticalSpacing()
+     *
+     * @attr ref android.R.styleable#GridView_verticalSpacing
+     */
+    public void setVerticalSpacing(int verticalSpacing) {
+        if (verticalSpacing != mVerticalSpacing) {
+            mVerticalSpacing = verticalSpacing;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    /**
+     * Returns the amount of vertical spacing between each item in the grid.
+     *
+     * @return The vertical spacing between items in pixels
+     *
+     * @see #setVerticalSpacing(int)
+     *
+     * @attr ref android.R.styleable#GridView_verticalSpacing
+     */
+    public int getVerticalSpacing() {
+        return mVerticalSpacing;
+    }
+
+    /**
+     * Control how items are stretched to fill their space.
+     *
+     * @param stretchMode Either {@link #NO_STRETCH},
+     * {@link #STRETCH_SPACING}, {@link #STRETCH_SPACING_UNIFORM}, or {@link #STRETCH_COLUMN_WIDTH}.
+     *
+     * @attr ref android.R.styleable#GridView_stretchMode
+     */
+    public void setStretchMode(@StretchMode int stretchMode) {
+        if (stretchMode != mStretchMode) {
+            mStretchMode = stretchMode;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    @StretchMode
+    public int getStretchMode() {
+        return mStretchMode;
+    }
+
+    /**
+     * Set the width of columns in the grid.
+     *
+     * @param columnWidth The column width, in pixels.
+     *
+     * @attr ref android.R.styleable#GridView_columnWidth
+     */
+    public void setColumnWidth(int columnWidth) {
+        if (columnWidth != mRequestedColumnWidth) {
+            mRequestedColumnWidth = columnWidth;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    /**
+     * Return the width of a column in the grid.
+     *
+     * <p>This may not be valid yet if a layout is pending.</p>
+     *
+     * @return The column width in pixels
+     *
+     * @see #setColumnWidth(int)
+     * @see #getRequestedColumnWidth()
+     *
+     * @attr ref android.R.styleable#GridView_columnWidth
+     */
+    public int getColumnWidth() {
+        return mColumnWidth;
+    }
+
+    /**
+     * Return the requested width of a column in the grid.
+     *
+     * <p>This may not be the actual column width used. Use {@link #getColumnWidth()}
+     * to retrieve the current real width of a column.</p>
+     *
+     * @return The requested column width in pixels
+     *
+     * @see #setColumnWidth(int)
+     * @see #getColumnWidth()
+     *
+     * @attr ref android.R.styleable#GridView_columnWidth
+     */
+    public int getRequestedColumnWidth() {
+        return mRequestedColumnWidth;
+    }
+
+    /**
+     * Set the number of columns in the grid
+     *
+     * @param numColumns The desired number of columns.
+     *
+     * @attr ref android.R.styleable#GridView_numColumns
+     */
+    public void setNumColumns(int numColumns) {
+        if (numColumns != mRequestedNumColumns) {
+            mRequestedNumColumns = numColumns;
+            requestLayoutIfNecessary();
+        }
+    }
+
+    /**
+     * Get the number of columns in the grid.
+     * Returns {@link #AUTO_FIT} if the Grid has never been laid out.
+     *
+     * @attr ref android.R.styleable#GridView_numColumns
+     *
+     * @see #setNumColumns(int)
+     */
+    @ViewDebug.ExportedProperty
+    public int getNumColumns() {
+        return mNumColumns;
+    }
+
+    /**
+     * Make sure views are touching the top or bottom edge, as appropriate for
+     * our gravity
+     */
+    private void adjustViewsUpOrDown() {
+        final int childCount = getChildCount();
+
+        if (childCount > 0) {
+            int delta;
+            View child;
+
+            if (!mStackFromBottom) {
+                // Uh-oh -- we came up short. Slide all views up to make them
+                // align with the top
+                child = getChildAt(0);
+                delta = child.getTop() - mListPadding.top;
+                if (mFirstPosition != 0) {
+                    // It's OK to have some space above the first item if it is
+                    // part of the vertical spacing
+                    delta -= mVerticalSpacing;
+                }
+                if (delta < 0) {
+                    // We only are looking to see if we are too low, not too high
+                    delta = 0;
+                }
+            } else {
+                // we are too high, slide all views down to align with bottom
+                child = getChildAt(childCount - 1);
+                delta = child.getBottom() - (getHeight() - mListPadding.bottom);
+
+                if (mFirstPosition + childCount < mItemCount) {
+                    // It's OK to have some space below the last item if it is
+                    // part of the vertical spacing
+                    delta += mVerticalSpacing;
+                }
+
+                if (delta > 0) {
+                    // We only are looking to see if we are too high, not too low
+                    delta = 0;
+                }
+            }
+
+            if (delta != 0) {
+                offsetChildrenTopAndBottom(-delta);
+            }
+        }
+    }
+
+    @Override
+    protected int computeVerticalScrollExtent() {
+        final int count = getChildCount();
+        if (count > 0) {
+            final int numColumns = mNumColumns;
+            final int rowCount = (count + numColumns - 1) / numColumns;
+
+            int extent = rowCount * 100;
+
+            View view = getChildAt(0);
+            final int top = view.getTop();
+            int height = view.getHeight();
+            if (height > 0) {
+                extent += (top * 100) / height;
+            }
+
+            view = getChildAt(count - 1);
+            final int bottom = view.getBottom();
+            height = view.getHeight();
+            if (height > 0) {
+                extent -= ((bottom - getHeight()) * 100) / height;
+            }
+
+            return extent;
+        }
+        return 0;
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        if (mFirstPosition >= 0 && getChildCount() > 0) {
+            final View view = getChildAt(0);
+            final int top = view.getTop();
+            int height = view.getHeight();
+            if (height > 0) {
+                final int numColumns = mNumColumns;
+                final int rowCount = (mItemCount + numColumns - 1) / numColumns;
+                // In case of stackFromBottom the calculation of whichRow needs
+                // to take into account that counting from the top the first row
+                // might not be entirely filled.
+                final int oddItemsOnFirstRow = isStackFromBottom() ? ((rowCount * numColumns) -
+                        mItemCount) : 0;
+                final int whichRow = (mFirstPosition + oddItemsOnFirstRow) / numColumns;
+                return Math.max(whichRow * 100 - (top * 100) / height +
+                        (int) ((float) mScrollY / getHeight() * rowCount * 100), 0);
+            }
+        }
+        return 0;
+    }
+
+    @Override
+    protected int computeVerticalScrollRange() {
+        // TODO: Account for vertical spacing too
+        final int numColumns = mNumColumns;
+        final int rowCount = (mItemCount + numColumns - 1) / numColumns;
+        int result = Math.max(rowCount * 100, 0);
+        if (mScrollY != 0) {
+            // Compensate for overscroll
+            result += Math.abs((int) ((float) mScrollY / getHeight() * rowCount * 100));
+        }
+        return result;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return GridView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        final int columnsCount = getNumColumns();
+        final int rowsCount = getCount() / columnsCount;
+        final int selectionMode = getSelectionModeForAccessibility();
+        final CollectionInfo collectionInfo = CollectionInfo.obtain(
+                rowsCount, columnsCount, false, selectionMode);
+        info.setCollectionInfo(collectionInfo);
+
+        if (columnsCount > 0 || rowsCount > 0) {
+            info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+
+        switch (action) {
+            case R.id.accessibilityActionScrollToPosition: {
+                // GridView only supports scrolling in one direction, so we can
+                // ignore the column argument.
+                final int numColumns = getNumColumns();
+                final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+                final int position = Math.min(row * numColumns, getCount() - 1);
+                if (row >= 0) {
+                    // The accessibility service gets data asynchronously, so
+                    // we'll be a little lenient by clamping the last position.
+                    smoothScrollToPosition(position);
+                    return true;
+                }
+            } break;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoForItem(
+            View view, int position, AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoForItem(view, position, info);
+
+        final int count = getCount();
+        final int columnsCount = getNumColumns();
+        final int rowsCount = count / columnsCount;
+
+        final int row;
+        final int column;
+        if (!mStackFromBottom) {
+            column = position % columnsCount;
+            row = position / columnsCount;
+        } else {
+            final int invertedIndex = count - 1 - position;
+
+            column = columnsCount - 1 - (invertedIndex % columnsCount);
+            row = rowsCount - 1 - invertedIndex / columnsCount;
+        }
+
+        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        final boolean isSelected = isItemChecked(position);
+        final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
+                row, 1, column, 1, isHeading, isSelected);
+        info.setCollectionItemInfo(itemInfo);
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+        encoder.addProperty("numColumns", getNumColumns());
+    }
+}
diff --git a/android/widget/HeaderViewListAdapter.java b/android/widget/HeaderViewListAdapter.java
new file mode 100644
index 0000000..f9d8f92
--- /dev/null
+++ b/android/widget/HeaderViewListAdapter.java
@@ -0,0 +1,270 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.database.DataSetObserver;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * ListAdapter used when a ListView has header views. This ListAdapter
+ * wraps another one and also keeps track of the header views and their
+ * associated data objects.
+ *<p>This is intended as a base class; you will probably not need to
+ * use this class directly in your own code.
+ */
+public class HeaderViewListAdapter implements WrapperListAdapter, Filterable {
+
+    private final ListAdapter mAdapter;
+
+    // These two ArrayList are assumed to NOT be null.
+    // They are indeed created when declared in ListView and then shared.
+    ArrayList<ListView.FixedViewInfo> mHeaderViewInfos;
+    ArrayList<ListView.FixedViewInfo> mFooterViewInfos;
+
+    // Used as a placeholder in case the provided info views are indeed null.
+    // Currently only used by some CTS tests, which may be removed.
+    static final ArrayList<ListView.FixedViewInfo> EMPTY_INFO_LIST =
+        new ArrayList<ListView.FixedViewInfo>();
+
+    boolean mAreAllFixedViewsSelectable;
+
+    private final boolean mIsFilterable;
+
+    public HeaderViewListAdapter(ArrayList<ListView.FixedViewInfo> headerViewInfos,
+                                 ArrayList<ListView.FixedViewInfo> footerViewInfos,
+                                 ListAdapter adapter) {
+        mAdapter = adapter;
+        mIsFilterable = adapter instanceof Filterable;
+
+        if (headerViewInfos == null) {
+            mHeaderViewInfos = EMPTY_INFO_LIST;
+        } else {
+            mHeaderViewInfos = headerViewInfos;
+        }
+
+        if (footerViewInfos == null) {
+            mFooterViewInfos = EMPTY_INFO_LIST;
+        } else {
+            mFooterViewInfos = footerViewInfos;
+        }
+
+        mAreAllFixedViewsSelectable =
+                areAllListInfosSelectable(mHeaderViewInfos)
+                && areAllListInfosSelectable(mFooterViewInfos);
+    }
+
+    public int getHeadersCount() {
+        return mHeaderViewInfos.size();
+    }
+
+    public int getFootersCount() {
+        return mFooterViewInfos.size();
+    }
+
+    public boolean isEmpty() {
+        return mAdapter == null || mAdapter.isEmpty();
+    }
+
+    private boolean areAllListInfosSelectable(ArrayList<ListView.FixedViewInfo> infos) {
+        if (infos != null) {
+            for (ListView.FixedViewInfo info : infos) {
+                if (!info.isSelectable) {
+                    return false;
+                }
+            }
+        }
+        return true;
+    }
+
+    public boolean removeHeader(View v) {
+        for (int i = 0; i < mHeaderViewInfos.size(); i++) {
+            ListView.FixedViewInfo info = mHeaderViewInfos.get(i);
+            if (info.view == v) {
+                mHeaderViewInfos.remove(i);
+
+                mAreAllFixedViewsSelectable =
+                        areAllListInfosSelectable(mHeaderViewInfos)
+                        && areAllListInfosSelectable(mFooterViewInfos);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public boolean removeFooter(View v) {
+        for (int i = 0; i < mFooterViewInfos.size(); i++) {
+            ListView.FixedViewInfo info = mFooterViewInfos.get(i);
+            if (info.view == v) {
+                mFooterViewInfos.remove(i);
+
+                mAreAllFixedViewsSelectable =
+                        areAllListInfosSelectable(mHeaderViewInfos)
+                        && areAllListInfosSelectable(mFooterViewInfos);
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    public int getCount() {
+        if (mAdapter != null) {
+            return getFootersCount() + getHeadersCount() + mAdapter.getCount();
+        } else {
+            return getFootersCount() + getHeadersCount();
+        }
+    }
+
+    public boolean areAllItemsEnabled() {
+        if (mAdapter != null) {
+            return mAreAllFixedViewsSelectable && mAdapter.areAllItemsEnabled();
+        } else {
+            return true;
+        }
+    }
+
+    public boolean isEnabled(int position) {
+        // Header (negative positions will throw an IndexOutOfBoundsException)
+        int numHeaders = getHeadersCount();
+        if (position < numHeaders) {
+            return mHeaderViewInfos.get(position).isSelectable;
+        }
+
+        // Adapter
+        final int adjPosition = position - numHeaders;
+        int adapterCount = 0;
+        if (mAdapter != null) {
+            adapterCount = mAdapter.getCount();
+            if (adjPosition < adapterCount) {
+                return mAdapter.isEnabled(adjPosition);
+            }
+        }
+
+        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+        return mFooterViewInfos.get(adjPosition - adapterCount).isSelectable;
+    }
+
+    public Object getItem(int position) {
+        // Header (negative positions will throw an IndexOutOfBoundsException)
+        int numHeaders = getHeadersCount();
+        if (position < numHeaders) {
+            return mHeaderViewInfos.get(position).data;
+        }
+
+        // Adapter
+        final int adjPosition = position - numHeaders;
+        int adapterCount = 0;
+        if (mAdapter != null) {
+            adapterCount = mAdapter.getCount();
+            if (adjPosition < adapterCount) {
+                return mAdapter.getItem(adjPosition);
+            }
+        }
+
+        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+        return mFooterViewInfos.get(adjPosition - adapterCount).data;
+    }
+
+    public long getItemId(int position) {
+        int numHeaders = getHeadersCount();
+        if (mAdapter != null && position >= numHeaders) {
+            int adjPosition = position - numHeaders;
+            int adapterCount = mAdapter.getCount();
+            if (adjPosition < adapterCount) {
+                return mAdapter.getItemId(adjPosition);
+            }
+        }
+        return -1;
+    }
+
+    public boolean hasStableIds() {
+        if (mAdapter != null) {
+            return mAdapter.hasStableIds();
+        }
+        return false;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        // Header (negative positions will throw an IndexOutOfBoundsException)
+        int numHeaders = getHeadersCount();
+        if (position < numHeaders) {
+            return mHeaderViewInfos.get(position).view;
+        }
+
+        // Adapter
+        final int adjPosition = position - numHeaders;
+        int adapterCount = 0;
+        if (mAdapter != null) {
+            adapterCount = mAdapter.getCount();
+            if (adjPosition < adapterCount) {
+                return mAdapter.getView(adjPosition, convertView, parent);
+            }
+        }
+
+        // Footer (off-limits positions will throw an IndexOutOfBoundsException)
+        return mFooterViewInfos.get(adjPosition - adapterCount).view;
+    }
+
+    public int getItemViewType(int position) {
+        int numHeaders = getHeadersCount();
+        if (mAdapter != null && position >= numHeaders) {
+            int adjPosition = position - numHeaders;
+            int adapterCount = mAdapter.getCount();
+            if (adjPosition < adapterCount) {
+                return mAdapter.getItemViewType(adjPosition);
+            }
+        }
+
+        return AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+    }
+
+    public int getViewTypeCount() {
+        if (mAdapter != null) {
+            return mAdapter.getViewTypeCount();
+        }
+        return 1;
+    }
+
+    public void registerDataSetObserver(DataSetObserver observer) {
+        if (mAdapter != null) {
+            mAdapter.registerDataSetObserver(observer);
+        }
+    }
+
+    public void unregisterDataSetObserver(DataSetObserver observer) {
+        if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(observer);
+        }
+    }
+
+    public Filter getFilter() {
+        if (mIsFilterable) {
+            return ((Filterable) mAdapter).getFilter();
+        }
+        return null;
+    }
+    
+    public ListAdapter getWrappedAdapter() {
+        return mAdapter;
+    }
+}
diff --git a/android/widget/HeterogeneousExpandableList.java b/android/widget/HeterogeneousExpandableList.java
new file mode 100644
index 0000000..e7e0933
--- /dev/null
+++ b/android/widget/HeterogeneousExpandableList.java
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Additional methods that when implemented make an
+ * {@link ExpandableListAdapter} take advantage of the {@link Adapter} view type
+ * mechanism.
+ * <p>
+ * An {@link ExpandableListAdapter} declares it has one view type for its group items
+ * and one view type for its child items. Although adapted for most {@link ExpandableListView}s,
+ * these values should be tuned for heterogeneous {@link ExpandableListView}s.
+ * </p>
+ * Lists that contain different types of group and/or child item views, should use an adapter that
+ * implements this interface. This way, the recycled views that will be provided to
+ * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+ * and
+ * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+ * will be of the appropriate group or child type, resulting in a more efficient reuse of the
+ * previously created views.
+ */
+public interface HeterogeneousExpandableList {
+    /**
+     * Get the type of group View that will be created by
+     * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+     * . for the specified group item.
+     * 
+     * @param groupPosition the position of the group for which the type should be returned.
+     * @return An integer representing the type of group View. Two group views should share the same
+     *         type if one can be converted to the other in
+     *         {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+     *         . Note: Integers must be in the range 0 to {@link #getGroupTypeCount} - 1.
+     *         {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned.
+     * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE
+     * @see #getGroupTypeCount()
+     */
+    int getGroupType(int groupPosition);
+
+    /**
+     * Get the type of child View that will be created by
+     * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+     * for the specified child item.
+     * 
+     * @param groupPosition the position of the group that the child resides in
+     * @param childPosition the position of the child with respect to other children in the group
+     * @return An integer representing the type of child View. Two child views should share the same
+     *         type if one can be converted to the other in
+     *         {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+     *         Note: Integers must be in the range 0 to {@link #getChildTypeCount} - 1.
+     *         {@link android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE} can also be returned.
+     * @see android.widget.Adapter#IGNORE_ITEM_VIEW_TYPE
+     * @see #getChildTypeCount()
+     */
+    int getChildType(int groupPosition, int childPosition);
+
+    /**
+     * <p>
+     * Returns the number of types of group Views that will be created by
+     * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+     * . Each type represents a set of views that can be converted in
+     * {@link android.widget.ExpandableListAdapter#getGroupView(int, boolean, View, ViewGroup)}
+     * . If the adapter always returns the same type of View for all group items, this method should
+     * return 1.
+     * </p>
+     * This method will only be called when the adapter is set on the {@link AdapterView}.
+     * 
+     * @return The number of types of group Views that will be created by this adapter.
+     * @see #getChildTypeCount()
+     * @see #getGroupType(int)
+     */
+    int getGroupTypeCount();
+
+    /**
+     * <p>
+     * Returns the number of types of child Views that will be created by
+     * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+     * . Each type represents a set of views that can be converted in
+     * {@link android.widget.ExpandableListAdapter#getChildView(int, int, boolean, View, ViewGroup)}
+     * , for any group. If the adapter always returns the same type of View for
+     * all child items, this method should return 1.
+     * </p>
+     * This method will only be called when the adapter is set on the {@link AdapterView}.
+     * 
+     * @return The total number of types of child Views that will be created by this adapter.
+     * @see #getGroupTypeCount()
+     * @see #getChildType(int, int)
+     */
+    int getChildTypeCount();
+}
diff --git a/android/widget/HorizontalScrollView.java b/android/widget/HorizontalScrollView.java
new file mode 100644
index 0000000..0bf2460
--- /dev/null
+++ b/android/widget/HorizontalScrollView.java
@@ -0,0 +1,1771 @@
+/*
+ * Copyright (C) 2009 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 android.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * Layout container for a view hierarchy that can be scrolled by the user,
+ * allowing it to be larger than the physical display.  A HorizontalScrollView
+ * is a {@link FrameLayout}, meaning you should place one child in it
+ * containing the entire contents to scroll; this child may itself be a layout
+ * manager with a complex hierarchy of objects.  A child that is often used
+ * is a {@link LinearLayout} in a horizontal orientation, presenting a horizontal
+ * array of top-level items that the user can scroll through.
+ *
+ * <p>The {@link TextView} class also
+ * takes care of its own scrolling, so does not require a HorizontalScrollView, but
+ * using the two together is possible to achieve the effect of a text view
+ * within a larger container.
+ *
+ * <p>HorizontalScrollView only supports horizontal scrolling. For vertical scrolling,
+ * use either {@link ScrollView} or {@link ListView}.
+ *
+ * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+ */
+public class HorizontalScrollView extends FrameLayout {
+    private static final int ANIMATED_SCROLL_GAP = ScrollView.ANIMATED_SCROLL_GAP;
+
+    private static final float MAX_SCROLL_FACTOR = ScrollView.MAX_SCROLL_FACTOR;
+
+    private static final String TAG = "HorizontalScrollView";
+
+    private long mLastScroll;
+
+    private final Rect mTempRect = new Rect();
+    private OverScroller mScroller;
+    private EdgeEffect mEdgeGlowLeft;
+    private EdgeEffect mEdgeGlowRight;
+
+    /**
+     * Position of the last motion event.
+     */
+    private int mLastMotionX;
+
+    /**
+     * True when the layout has changed but the traversal has not come through yet.
+     * Ideally the view hierarchy would keep track of this for us.
+     */
+    private boolean mIsLayoutDirty = true;
+
+    /**
+     * The child to give focus to in the event that a child has requested focus while the
+     * layout is dirty. This prevents the scroll from being wrong if the child has not been
+     * laid out before requesting focus.
+     */
+    private View mChildToScrollTo = null;
+
+    /**
+     * True if the user is currently dragging this ScrollView around. This is
+     * not the same as 'is being flinged', which can be checked by
+     * mScroller.isFinished() (flinging begins when the user lifts his finger).
+     */
+    private boolean mIsBeingDragged = false;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * When set to true, the scroll view measure its child to make it fill the currently
+     * visible area.
+     */
+    @ViewDebug.ExportedProperty(category = "layout")
+    private boolean mFillViewport;
+
+    /**
+     * Whether arrow scrolling is animated.
+     */
+    private boolean mSmoothScrollingEnabled = true;
+
+    private int mTouchSlop;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+
+    private int mOverscrollDistance;
+    private int mOverflingDistance;
+
+    private float mHorizontalScrollFactor;
+
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    private SavedState mSavedState;
+
+    public HorizontalScrollView(Context context) {
+        this(context, null);
+    }
+
+    public HorizontalScrollView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.horizontalScrollViewStyle);
+    }
+
+    public HorizontalScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public HorizontalScrollView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initScrollView();
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, android.R.styleable.HorizontalScrollView, defStyleAttr, defStyleRes);
+
+        setFillViewport(a.getBoolean(android.R.styleable.HorizontalScrollView_fillViewport, false));
+
+        a.recycle();
+
+        if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+            setRevealOnFocusHint(false);
+        }
+    }
+
+    @Override
+    protected float getLeftFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+
+        final int length = getHorizontalFadingEdgeLength();
+        if (mScrollX < length) {
+            return mScrollX / (float) length;
+        }
+
+        return 1.0f;
+    }
+
+    @Override
+    protected float getRightFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+
+        final int length = getHorizontalFadingEdgeLength();
+        final int rightEdge = getWidth() - mPaddingRight;
+        final int span = getChildAt(0).getRight() - mScrollX - rightEdge;
+        if (span < length) {
+            return span / (float) length;
+        }
+
+        return 1.0f;
+    }
+
+    /**
+     * @return The maximum amount this scroll view will scroll in response to
+     *   an arrow event.
+     */
+    public int getMaxScrollAmount() {
+        return (int) (MAX_SCROLL_FACTOR * (mRight - mLeft));
+    }
+
+
+    private void initScrollView() {
+        mScroller = new OverScroller(getContext());
+        setFocusable(true);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setWillNotDraw(false);
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverscrollDistance = configuration.getScaledOverscrollDistance();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+        mHorizontalScrollFactor = configuration.getScaledHorizontalScrollFactor();
+    }
+
+    @Override
+    public void addView(View child) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("HorizontalScrollView can host only one direct child");
+        }
+
+        super.addView(child);
+    }
+
+    @Override
+    public void addView(View child, int index) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("HorizontalScrollView can host only one direct child");
+        }
+
+        super.addView(child, index);
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("HorizontalScrollView can host only one direct child");
+        }
+
+        super.addView(child, params);
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("HorizontalScrollView can host only one direct child");
+        }
+
+        super.addView(child, index, params);
+    }
+
+    /**
+     * @return Returns true this HorizontalScrollView can be scrolled
+     */
+    private boolean canScroll() {
+        View child = getChildAt(0);
+        if (child != null) {
+            int childWidth = child.getWidth();
+            return getWidth() < childWidth + mPaddingLeft + mPaddingRight ;
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this HorizontalScrollView's content is stretched to
+     * fill the viewport.
+     *
+     * @return True if the content fills the viewport, false otherwise.
+     *
+     * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+     */
+    public boolean isFillViewport() {
+        return mFillViewport;
+    }
+
+    /**
+     * Indicates this HorizontalScrollView whether it should stretch its content width
+     * to fill the viewport or not.
+     *
+     * @param fillViewport True to stretch the content's width to the viewport's
+     *        boundaries, false otherwise.
+     *
+     * @attr ref android.R.styleable#HorizontalScrollView_fillViewport
+     */
+    public void setFillViewport(boolean fillViewport) {
+        if (fillViewport != mFillViewport) {
+            mFillViewport = fillViewport;
+            requestLayout();
+        }
+    }
+
+    /**
+     * @return Whether arrow scrolling will animate its transition.
+     */
+    public boolean isSmoothScrollingEnabled() {
+        return mSmoothScrollingEnabled;
+    }
+
+    /**
+     * Set whether arrow scrolling will animate its transition.
+     * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+     */
+    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+        mSmoothScrollingEnabled = smoothScrollingEnabled;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (!mFillViewport) {
+            return;
+        }
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        if (widthMode == MeasureSpec.UNSPECIFIED) {
+            return;
+        }
+
+        if (getChildCount() > 0) {
+            final View child = getChildAt(0);
+            final int widthPadding;
+            final int heightPadding;
+            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+            if (targetSdkVersion >= Build.VERSION_CODES.M) {
+                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
+                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
+            } else {
+                widthPadding = mPaddingLeft + mPaddingRight;
+                heightPadding = mPaddingTop + mPaddingBottom;
+            }
+
+            int desiredWidth = getMeasuredWidth() - widthPadding;
+            if (child.getMeasuredWidth() < desiredWidth) {
+                final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        desiredWidth, MeasureSpec.EXACTLY);
+                final int childHeightMeasureSpec = getChildMeasureSpec(
+                        heightMeasureSpec, heightPadding, lp.height);
+                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            }
+        }
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Let the focused view and/or our descendants get the key first
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+    }
+
+    /**
+     * You can call this function yourself to have the scroll view perform
+     * scrolling from a key event, just as if the event had been dispatched to
+     * it by the view hierarchy.
+     *
+     * @param event The key event to execute.
+     * @return Return true if the event was handled, else false.
+     */
+    public boolean executeKeyEvent(KeyEvent event) {
+        mTempRect.setEmpty();
+
+        if (!canScroll()) {
+            if (isFocused()) {
+                View currentFocused = findFocus();
+                if (currentFocused == this) currentFocused = null;
+                View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+                        currentFocused, View.FOCUS_RIGHT);
+                return nextFocused != null && nextFocused != this &&
+                        nextFocused.requestFocus(View.FOCUS_RIGHT);
+            }
+            return false;
+        }
+
+        boolean handled = false;
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_LEFT:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_LEFT);
+                    } else {
+                        handled = fullScroll(View.FOCUS_LEFT);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_RIGHT);
+                    } else {
+                        handled = fullScroll(View.FOCUS_RIGHT);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_SPACE:
+                    pageScroll(event.isShiftPressed() ? View.FOCUS_LEFT : View.FOCUS_RIGHT);
+                    break;
+            }
+        }
+
+        return handled;
+    }
+
+    private boolean inChild(int x, int y) {
+        if (getChildCount() > 0) {
+            final int scrollX = mScrollX;
+            final View child = getChildAt(0);
+            return !(y < child.getTop()
+                    || y >= child.getBottom()
+                    || x < child.getLeft() - scrollX
+                    || x >= child.getRight() - scrollX);
+        }
+        return false;
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (disallowIntercept) {
+            recycleVelocityTracker();
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        /*
+        * Shortcut the most recurring case: the user is in the dragging
+        * state and he is moving his finger.  We want to intercept this
+        * motion.
+        */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+            return true;
+        }
+
+        if (super.onInterceptTouchEvent(ev)) {
+            return true;
+        }
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionX is set to the x value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + activePointerId
+                            + " in onInterceptTouchEvent");
+                    break;
+                }
+
+                final int x = (int) ev.getX(pointerIndex);
+                final int xDiff = (int) Math.abs(x - mLastMotionX);
+                if (xDiff > mTouchSlop) {
+                    mIsBeingDragged = true;
+                    mLastMotionX = x;
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                    if (mParent != null) mParent.requestDisallowInterceptTouchEvent(true);
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                final int x = (int) ev.getX();
+                if (!inChild((int) x, (int) ev.getY())) {
+                    mIsBeingDragged = false;
+                    recycleVelocityTracker();
+                    break;
+                }
+
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionX = x;
+                mActivePointerId = ev.getPointerId(0);
+
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+
+                /*
+                * If being flinged and user touches the screen, initiate drag;
+                * otherwise don't.  mScroller.isFinished should be false when
+                * being flinged.
+                */
+                mIsBeingDragged = !mScroller.isFinished();
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                /* Release the drag */
+                mIsBeingDragged = false;
+                mActivePointerId = INVALID_POINTER;
+                if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
+                    postInvalidateOnAnimation();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                mLastMotionX = (int) ev.getX(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionX = (int) ev.getX(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+
+        /*
+        * The only time we want to intercept motion events is if we are in the
+        * drag mode.
+        */
+        return mIsBeingDragged;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        initVelocityTrackerIfNotExists();
+        mVelocityTracker.addMovement(ev);
+
+        final int action = ev.getAction();
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                if (getChildCount() == 0) {
+                    return false;
+                }
+                if ((mIsBeingDragged = !mScroller.isFinished())) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.abortAnimation();
+                }
+
+                // Remember where the motion event started
+                mLastMotionX = (int) ev.getX();
+                mActivePointerId = ev.getPointerId(0);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (activePointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+                    break;
+                }
+
+                final int x = (int) ev.getX(activePointerIndex);
+                int deltaX = mLastMotionX - x;
+                if (!mIsBeingDragged && Math.abs(deltaX) > mTouchSlop) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                    mIsBeingDragged = true;
+                    if (deltaX > 0) {
+                        deltaX -= mTouchSlop;
+                    } else {
+                        deltaX += mTouchSlop;
+                    }
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    mLastMotionX = x;
+
+                    final int oldX = mScrollX;
+                    final int oldY = mScrollY;
+                    final int range = getScrollRange();
+                    final int overscrollMode = getOverScrollMode();
+                    final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                    // Calling overScrollBy will call onOverScrolled, which
+                    // calls onScrollChanged if applicable.
+                    if (overScrollBy(deltaX, 0, mScrollX, 0, range, 0,
+                            mOverscrollDistance, 0, true)) {
+                        // Break our velocity if we hit a scroll barrier.
+                        mVelocityTracker.clear();
+                    }
+
+                    if (canOverscroll) {
+                        final int pulledToX = oldX + deltaX;
+                        if (pulledToX < 0) {
+                            mEdgeGlowLeft.onPull((float) deltaX / getWidth(),
+                                    1.f - ev.getY(activePointerIndex) / getHeight());
+                            if (!mEdgeGlowRight.isFinished()) {
+                                mEdgeGlowRight.onRelease();
+                            }
+                        } else if (pulledToX > range) {
+                            mEdgeGlowRight.onPull((float) deltaX / getWidth(),
+                                    ev.getY(activePointerIndex) / getHeight());
+                            if (!mEdgeGlowLeft.isFinished()) {
+                                mEdgeGlowLeft.onRelease();
+                            }
+                        }
+                        if (mEdgeGlowLeft != null
+                                && (!mEdgeGlowLeft.isFinished() || !mEdgeGlowRight.isFinished())) {
+                            postInvalidateOnAnimation();
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) velocityTracker.getXVelocity(mActivePointerId);
+
+                    if (getChildCount() > 0) {
+                        if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                            fling(-initialVelocity);
+                        } else {
+                            if (mScroller.springBack(mScrollX, mScrollY, 0,
+                                    getScrollRange(), 0, 0)) {
+                                postInvalidateOnAnimation();
+                            }
+                        }
+                    }
+
+                    mActivePointerId = INVALID_POINTER;
+                    mIsBeingDragged = false;
+                    recycleVelocityTracker();
+
+                    if (mEdgeGlowLeft != null) {
+                        mEdgeGlowLeft.onRelease();
+                        mEdgeGlowRight.onRelease();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0)) {
+                        postInvalidateOnAnimation();
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    mIsBeingDragged = false;
+                    recycleVelocityTracker();
+
+                    if (mEdgeGlowLeft != null) {
+                        mEdgeGlowLeft.onRelease();
+                        mEdgeGlowRight.onRelease();
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+        return true;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionX = (int) ev.getX(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_SCROLL: {
+                if (!mIsBeingDragged) {
+                    final float axisValue;
+                    if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+                        if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) {
+                            axisValue = -event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                        } else {
+                            axisValue = event.getAxisValue(MotionEvent.AXIS_HSCROLL);
+                        }
+                    } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+                        axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+                    } else {
+                        axisValue = 0;
+                    }
+
+                    final int delta = Math.round(axisValue * mHorizontalScrollFactor);
+                    if (delta != 0) {
+                        final int range = getScrollRange();
+                        int oldScrollX = mScrollX;
+                        int newScrollX = oldScrollX + delta;
+                        if (newScrollX < 0) {
+                            newScrollX = 0;
+                        } else if (newScrollX > range) {
+                            newScrollX = range;
+                        }
+                        if (newScrollX != oldScrollX) {
+                            super.scrollTo(newScrollX, mScrollY);
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY,
+            boolean clampedX, boolean clampedY) {
+        // Treat animating scrolls differently; see #computeScroll() for why.
+        if (!mScroller.isFinished()) {
+            final int oldX = mScrollX;
+            final int oldY = mScrollY;
+            mScrollX = scrollX;
+            mScrollY = scrollY;
+            invalidateParentIfNeeded();
+            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+            if (clampedX) {
+                mScroller.springBack(mScrollX, mScrollY, 0, getScrollRange(), 0, 0);
+            }
+        } else {
+            super.scrollTo(scrollX, scrollY);
+        }
+
+        awakenScrollBars();
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case R.id.accessibilityActionScrollRight: {
+                if (!isEnabled()) {
+                    return false;
+                }
+                final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+                final int targetScrollX = Math.min(mScrollX + viewportWidth, getScrollRange());
+                if (targetScrollX != mScrollX) {
+                    smoothScrollTo(targetScrollX, 0);
+                    return true;
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+            case R.id.accessibilityActionScrollLeft: {
+                if (!isEnabled()) {
+                    return false;
+                }
+                final int viewportWidth = getWidth() - mPaddingLeft - mPaddingRight;
+                final int targetScrollX = Math.max(0, mScrollX - viewportWidth);
+                if (targetScrollX != mScrollX) {
+                    smoothScrollTo(targetScrollX, 0);
+                    return true;
+                }
+            } return false;
+        }
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return HorizontalScrollView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        final int scrollRange = getScrollRange();
+        if (scrollRange > 0) {
+            info.setScrollable(true);
+            if (isEnabled() && mScrollX > 0) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_LEFT);
+            }
+            if (isEnabled() && mScrollX < scrollRange) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_RIGHT);
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setScrollable(getScrollRange() > 0);
+        event.setScrollX(mScrollX);
+        event.setScrollY(mScrollY);
+        event.setMaxScrollX(getScrollRange());
+        event.setMaxScrollY(mScrollY);
+    }
+
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            scrollRange = Math.max(0,
+                    child.getWidth() - (getWidth() - mPaddingLeft - mPaddingRight));
+        }
+        return scrollRange;
+    }
+
+    /**
+     * <p>
+     * Finds the next focusable component that fits in this View's bounds
+     * (excluding fading edges) pretending that this View's left is located at
+     * the parameter left.
+     * </p>
+     *
+     * @param leftFocus          look for a candidate is the one at the left of the bounds
+     *                           if leftFocus is true, or at the right of the bounds if leftFocus
+     *                           is false
+     * @param left               the left offset of the bounds in which a focusable must be
+     *                           found (the fading edge is assumed to start at this position)
+     * @param preferredFocusable the View that has highest priority and will be
+     *                           returned if it is within my bounds (null is valid)
+     * @return the next focusable component in the bounds or null if none can be found
+     */
+    private View findFocusableViewInMyBounds(final boolean leftFocus,
+            final int left, View preferredFocusable) {
+        /*
+         * The fading edge's transparent side should be considered for focus
+         * since it's mostly visible, so we divide the actual fading edge length
+         * by 2.
+         */
+        final int fadingEdgeLength = getHorizontalFadingEdgeLength() / 2;
+        final int leftWithoutFadingEdge = left + fadingEdgeLength;
+        final int rightWithoutFadingEdge = left + getWidth() - fadingEdgeLength;
+
+        if ((preferredFocusable != null)
+                && (preferredFocusable.getLeft() < rightWithoutFadingEdge)
+                && (preferredFocusable.getRight() > leftWithoutFadingEdge)) {
+            return preferredFocusable;
+        }
+
+        return findFocusableViewInBounds(leftFocus, leftWithoutFadingEdge,
+                rightWithoutFadingEdge);
+    }
+
+    /**
+     * <p>
+     * Finds the next focusable component that fits in the specified bounds.
+     * </p>
+     *
+     * @param leftFocus look for a candidate is the one at the left of the bounds
+     *                  if leftFocus is true, or at the right of the bounds if
+     *                  leftFocus is false
+     * @param left      the left offset of the bounds in which a focusable must be
+     *                  found
+     * @param right     the right offset of the bounds in which a focusable must
+     *                  be found
+     * @return the next focusable component in the bounds or null if none can
+     *         be found
+     */
+    private View findFocusableViewInBounds(boolean leftFocus, int left, int right) {
+
+        List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+        View focusCandidate = null;
+
+        /*
+         * A fully contained focusable is one where its left is below the bound's
+         * left, and its right is above the bound's right. A partially
+         * contained focusable is one where some part of it is within the
+         * bounds, but it also has some part that is not within bounds.  A fully contained
+         * focusable is preferred to a partially contained focusable.
+         */
+        boolean foundFullyContainedFocusable = false;
+
+        int count = focusables.size();
+        for (int i = 0; i < count; i++) {
+            View view = focusables.get(i);
+            int viewLeft = view.getLeft();
+            int viewRight = view.getRight();
+
+            if (left < viewRight && viewLeft < right) {
+                /*
+                 * the focusable is in the target area, it is a candidate for
+                 * focusing
+                 */
+
+                final boolean viewIsFullyContained = (left < viewLeft) &&
+                        (viewRight < right);
+
+                if (focusCandidate == null) {
+                    /* No candidate, take this one */
+                    focusCandidate = view;
+                    foundFullyContainedFocusable = viewIsFullyContained;
+                } else {
+                    final boolean viewIsCloserToBoundary =
+                            (leftFocus && viewLeft < focusCandidate.getLeft()) ||
+                                    (!leftFocus && viewRight > focusCandidate.getRight());
+
+                    if (foundFullyContainedFocusable) {
+                        if (viewIsFullyContained && viewIsCloserToBoundary) {
+                            /*
+                             * We're dealing with only fully contained views, so
+                             * it has to be closer to the boundary to beat our
+                             * candidate
+                             */
+                            focusCandidate = view;
+                        }
+                    } else {
+                        if (viewIsFullyContained) {
+                            /* Any fully contained view beats a partially contained view */
+                            focusCandidate = view;
+                            foundFullyContainedFocusable = true;
+                        } else if (viewIsCloserToBoundary) {
+                            /*
+                             * Partially contained view beats another partially
+                             * contained view if it's closer
+                             */
+                            focusCandidate = view;
+                        }
+                    }
+                }
+            }
+        }
+
+        return focusCandidate;
+    }
+
+    /**
+     * <p>Handles scrolling in response to a "page up/down" shortcut press. This
+     * method will scroll the view by one page left or right and give the focus
+     * to the leftmost/rightmost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
+     *                  to go one page left or {@link android.view.View#FOCUS_RIGHT}
+     *                  to go one page right
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean pageScroll(int direction) {
+        boolean right = direction == View.FOCUS_RIGHT;
+        int width = getWidth();
+
+        if (right) {
+            mTempRect.left = getScrollX() + width;
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(0);
+                if (mTempRect.left + width > view.getRight()) {
+                    mTempRect.left = view.getRight() - width;
+                }
+            }
+        } else {
+            mTempRect.left = getScrollX() - width;
+            if (mTempRect.left < 0) {
+                mTempRect.left = 0;
+            }
+        }
+        mTempRect.right = mTempRect.left + width;
+
+        return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
+    }
+
+    /**
+     * <p>Handles scrolling in response to a "home/end" shortcut press. This
+     * method will scroll the view to the left or right and give the focus
+     * to the leftmost/rightmost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
+     *                  to go the left of the view or {@link android.view.View#FOCUS_RIGHT}
+     *                  to go the right
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean fullScroll(int direction) {
+        boolean right = direction == View.FOCUS_RIGHT;
+        int width = getWidth();
+
+        mTempRect.left = 0;
+        mTempRect.right = width;
+
+        if (right) {
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(0);
+                mTempRect.right = view.getRight();
+                mTempRect.left = mTempRect.right - width;
+            }
+        }
+
+        return scrollAndFocus(direction, mTempRect.left, mTempRect.right);
+    }
+
+    /**
+     * <p>Scrolls the view to make the area defined by <code>left</code> and
+     * <code>right</code> visible. This method attempts to give the focus
+     * to a component visible in this area. If no component can be focused in
+     * the new visible area, the focus is reclaimed by this scrollview.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_LEFT}
+     *                  to go left {@link android.view.View#FOCUS_RIGHT} to right
+     * @param left     the left offset of the new area to be made visible
+     * @param right    the right offset of the new area to be made visible
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    private boolean scrollAndFocus(int direction, int left, int right) {
+        boolean handled = true;
+
+        int width = getWidth();
+        int containerLeft = getScrollX();
+        int containerRight = containerLeft + width;
+        boolean goLeft = direction == View.FOCUS_LEFT;
+
+        View newFocused = findFocusableViewInBounds(goLeft, left, right);
+        if (newFocused == null) {
+            newFocused = this;
+        }
+
+        if (left >= containerLeft && right <= containerRight) {
+            handled = false;
+        } else {
+            int delta = goLeft ? (left - containerLeft) : (right - containerRight);
+            doScrollX(delta);
+        }
+
+        if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+        return handled;
+    }
+
+    /**
+     * Handle scrolling in response to a left or right arrow click.
+     *
+     * @param direction The direction corresponding to the arrow key that was
+     *                  pressed
+     * @return True if we consumed the event, false otherwise
+     */
+    public boolean arrowScroll(int direction) {
+
+        View currentFocused = findFocus();
+        if (currentFocused == this) currentFocused = null;
+
+        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+        final int maxJump = getMaxScrollAmount();
+
+        if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump)) {
+            nextFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollX(scrollDelta);
+            nextFocused.requestFocus(direction);
+        } else {
+            // no new focus
+            int scrollDelta = maxJump;
+
+            if (direction == View.FOCUS_LEFT && getScrollX() < scrollDelta) {
+                scrollDelta = getScrollX();
+            } else if (direction == View.FOCUS_RIGHT && getChildCount() > 0) {
+
+                int daRight = getChildAt(0).getRight();
+
+                int screenRight = getScrollX() + getWidth();
+
+                if (daRight - screenRight < maxJump) {
+                    scrollDelta = daRight - screenRight;
+                }
+            }
+            if (scrollDelta == 0) {
+                return false;
+            }
+            doScrollX(direction == View.FOCUS_RIGHT ? scrollDelta : -scrollDelta);
+        }
+
+        if (currentFocused != null && currentFocused.isFocused()
+                && isOffScreen(currentFocused)) {
+            // previously focused item still has focus and is off screen, give
+            // it up (take it back to ourselves)
+            // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+            // sure to
+            // get it)
+            final int descendantFocusability = getDescendantFocusability();  // save
+            setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+            requestFocus();
+            setDescendantFocusability(descendantFocusability);  // restore
+        }
+        return true;
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is scrolled off
+     *  screen.
+     */
+    private boolean isOffScreen(View descendant) {
+        return !isWithinDeltaOfScreen(descendant, 0);
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is within delta
+     *  pixels of being on the screen.
+     */
+    private boolean isWithinDeltaOfScreen(View descendant, int delta) {
+        descendant.getDrawingRect(mTempRect);
+        offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+        return (mTempRect.right + delta) >= getScrollX()
+                && (mTempRect.left - delta) <= (getScrollX() + getWidth());
+    }
+
+    /**
+     * Smooth scroll by a X delta
+     *
+     * @param delta the number of pixels to scroll by on the X axis
+     */
+    private void doScrollX(int delta) {
+        if (delta != 0) {
+            if (mSmoothScrollingEnabled) {
+                smoothScrollBy(delta, 0);
+            } else {
+                scrollBy(delta, 0);
+            }
+        }
+    }
+
+    /**
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param dx the number of pixels to scroll by on the X axis
+     * @param dy the number of pixels to scroll by on the Y axis
+     */
+    public final void smoothScrollBy(int dx, int dy) {
+        if (getChildCount() == 0) {
+            // Nothing to do.
+            return;
+        }
+        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+        if (duration > ANIMATED_SCROLL_GAP) {
+            final int width = getWidth() - mPaddingRight - mPaddingLeft;
+            final int right = getChildAt(0).getWidth();
+            final int maxX = Math.max(0, right - width);
+            final int scrollX = mScrollX;
+            dx = Math.max(0, Math.min(scrollX + dx, maxX)) - scrollX;
+
+            mScroller.startScroll(scrollX, mScrollY, dx, 0);
+            postInvalidateOnAnimation();
+        } else {
+            if (!mScroller.isFinished()) {
+                mScroller.abortAnimation();
+            }
+            scrollBy(dx, dy);
+        }
+        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+    }
+
+    /**
+     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+     *
+     * @param x the position where to scroll on the X axis
+     * @param y the position where to scroll on the Y axis
+     */
+    public final void smoothScrollTo(int x, int y) {
+        smoothScrollBy(x - mScrollX, y - mScrollY);
+    }
+
+    /**
+     * <p>The scroll range of a scroll view is the overall width of all of its
+     * children.</p>
+     */
+    @Override
+    protected int computeHorizontalScrollRange() {
+        final int count = getChildCount();
+        final int contentWidth = getWidth() - mPaddingLeft - mPaddingRight;
+        if (count == 0) {
+            return contentWidth;
+        }
+
+        int scrollRange = getChildAt(0).getRight();
+        final int scrollX = mScrollX;
+        final int overscrollRight = Math.max(0, scrollRange - contentWidth);
+        if (scrollX < 0) {
+            scrollRange -= scrollX;
+        } else if (scrollX > overscrollRight) {
+            scrollRange += scrollX - overscrollRight;
+        }
+
+        return scrollRange;
+    }
+
+    @Override
+    protected int computeHorizontalScrollOffset() {
+        return Math.max(0, super.computeHorizontalScrollOffset());
+    }
+
+    @Override
+    protected void measureChild(View child, int parentWidthMeasureSpec,
+            int parentHeightMeasureSpec) {
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+        final int horizontalPadding = mPaddingLeft + mPaddingRight;
+        final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - horizontalPadding),
+                MeasureSpec.UNSPECIFIED);
+
+        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                mPaddingTop + mPaddingBottom, lp.height);
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+                        + heightUsed, lp.height);
+        final int usedTotal = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin +
+                widthUsed;
+        final int childWidthMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                Math.max(0, MeasureSpec.getSize(parentWidthMeasureSpec) - usedTotal),
+                MeasureSpec.UNSPECIFIED);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            // This is called at drawing time by ViewGroup.  We don't want to
+            // re-show the scrollbars at this point, which scrollTo will do,
+            // so we replicate most of scrollTo here.
+            //
+            //         It's a little odd to call onScrollChanged from inside the drawing.
+            //
+            //         It is, except when you remember that computeScroll() is used to
+            //         animate scrolling. So unless we want to defer the onScrollChanged()
+            //         until the end of the animated scrolling, we don't really have a
+            //         choice here.
+            //
+            //         I agree.  The alternative, which I think would be worse, is to post
+            //         something and tell the subclasses later.  This is bad because there
+            //         will be a window where mScrollX/Y is different from what the app
+            //         thinks it is.
+            //
+            int oldX = mScrollX;
+            int oldY = mScrollY;
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                final int range = getScrollRange();
+                final int overscrollMode = getOverScrollMode();
+                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                overScrollBy(x - oldX, y - oldY, oldX, oldY, range, 0,
+                        mOverflingDistance, 0, false);
+                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+
+                if (canOverscroll) {
+                    if (x < 0 && oldX >= 0) {
+                        mEdgeGlowLeft.onAbsorb((int) mScroller.getCurrVelocity());
+                    } else if (x > range && oldX <= range) {
+                        mEdgeGlowRight.onAbsorb((int) mScroller.getCurrVelocity());
+                    }
+                }
+            }
+
+            if (!awakenScrollBars()) {
+                postInvalidateOnAnimation();
+            }
+        }
+    }
+
+    /**
+     * Scrolls the view to the given child.
+     *
+     * @param child the View to scroll to
+     */
+    private void scrollToChild(View child) {
+        child.getDrawingRect(mTempRect);
+
+        /* Offset from child's local coordinates to ScrollView coordinates */
+        offsetDescendantRectToMyCoords(child, mTempRect);
+
+        int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+
+        if (scrollDelta != 0) {
+            scrollBy(scrollDelta, 0);
+        }
+    }
+
+    /**
+     * If rect is off screen, scroll just enough to get it (or at least the
+     * first screen size chunk of it) on screen.
+     *
+     * @param rect      The rectangle.
+     * @param immediate True to scroll immediately without animation
+     * @return true if scrolling was performed
+     */
+    private boolean scrollToChildRect(Rect rect, boolean immediate) {
+        final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
+        final boolean scroll = delta != 0;
+        if (scroll) {
+            if (immediate) {
+                scrollBy(delta, 0);
+            } else {
+                smoothScrollBy(delta, 0);
+            }
+        }
+        return scroll;
+    }
+
+    /**
+     * Compute the amount to scroll in the X direction in order to get
+     * a rectangle completely on the screen (or, if taller than the screen,
+     * at least the first screen size chunk of it).
+     *
+     * @param rect The rect.
+     * @return The scroll delta.
+     */
+    protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+        if (getChildCount() == 0) return 0;
+
+        int width = getWidth();
+        int screenLeft = getScrollX();
+        int screenRight = screenLeft + width;
+
+        int fadingEdge = getHorizontalFadingEdgeLength();
+
+        // leave room for left fading edge as long as rect isn't at very left
+        if (rect.left > 0) {
+            screenLeft += fadingEdge;
+        }
+
+        // leave room for right fading edge as long as rect isn't at very right
+        if (rect.right < getChildAt(0).getWidth()) {
+            screenRight -= fadingEdge;
+        }
+
+        int scrollXDelta = 0;
+
+        if (rect.right > screenRight && rect.left > screenLeft) {
+            // need to move right to get it in view: move right just enough so
+            // that the entire rectangle is in view (or at least the first
+            // screen size chunk).
+
+            if (rect.width() > width) {
+                // just enough to get screen size chunk on
+                scrollXDelta += (rect.left - screenLeft);
+            } else {
+                // get entire rect at right of screen
+                scrollXDelta += (rect.right - screenRight);
+            }
+
+            // make sure we aren't scrolling beyond the end of our content
+            int right = getChildAt(0).getRight();
+            int distanceToRight = right - screenRight;
+            scrollXDelta = Math.min(scrollXDelta, distanceToRight);
+
+        } else if (rect.left < screenLeft && rect.right < screenRight) {
+            // need to move right to get it in view: move right just enough so that
+            // entire rectangle is in view (or at least the first screen
+            // size chunk of it).
+
+            if (rect.width() > width) {
+                // screen size chunk
+                scrollXDelta -= (screenRight - rect.right);
+            } else {
+                // entire rect at left
+                scrollXDelta -= (screenLeft - rect.left);
+            }
+
+            // make sure we aren't scrolling any further than the left our content
+            scrollXDelta = Math.max(scrollXDelta, -getScrollX());
+        }
+        return scrollXDelta;
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (focused != null && focused.getRevealOnFocusHint()) {
+            if (!mIsLayoutDirty) {
+                scrollToChild(focused);
+            } else {
+                // The child may not be laid out yet, we can't compute the scroll yet
+                mChildToScrollTo = focused;
+            }
+        }
+        super.requestChildFocus(child, focused);
+    }
+
+
+    /**
+     * When looking for focus in children of a scroll view, need to be a little
+     * more careful not to give focus to something that is scrolled off screen.
+     *
+     * This is more expensive than the default {@link android.view.ViewGroup}
+     * implementation, otherwise this behavior might have been made the default.
+     */
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction,
+            Rect previouslyFocusedRect) {
+
+        // convert from forward / backward notation to up / down / left / right
+        // (ugh).
+        if (direction == View.FOCUS_FORWARD) {
+            direction = View.FOCUS_RIGHT;
+        } else if (direction == View.FOCUS_BACKWARD) {
+            direction = View.FOCUS_LEFT;
+        }
+
+        final View nextFocus = previouslyFocusedRect == null ?
+                FocusFinder.getInstance().findNextFocus(this, null, direction) :
+                FocusFinder.getInstance().findNextFocusFromRect(this,
+                        previouslyFocusedRect, direction);
+
+        if (nextFocus == null) {
+            return false;
+        }
+
+        if (isOffScreen(nextFocus)) {
+            return false;
+        }
+
+        return nextFocus.requestFocus(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+            boolean immediate) {
+        // offset into coordinate space of this scroll view
+        rectangle.offset(child.getLeft() - child.getScrollX(),
+                child.getTop() - child.getScrollY());
+
+        return scrollToChildRect(rectangle, immediate);
+    }
+
+    @Override
+    public void requestLayout() {
+        mIsLayoutDirty = true;
+        super.requestLayout();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        int childWidth = 0;
+        int childMargins = 0;
+
+        if (getChildCount() > 0) {
+            childWidth = getChildAt(0).getMeasuredWidth();
+            LayoutParams childParams = (LayoutParams) getChildAt(0).getLayoutParams();
+            childMargins = childParams.leftMargin + childParams.rightMargin;
+        }
+
+        final int available = r - l - getPaddingLeftWithForeground() -
+                getPaddingRightWithForeground() - childMargins;
+
+        final boolean forceLeftGravity = (childWidth > available);
+
+        layoutChildren(l, t, r, b, forceLeftGravity);
+
+        mIsLayoutDirty = false;
+        // Give a child focus if it needs it
+        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+            scrollToChild(mChildToScrollTo);
+        }
+        mChildToScrollTo = null;
+
+        if (!isLaidOut()) {
+            final int scrollRange = Math.max(0,
+                    childWidth - (r - l - mPaddingLeft - mPaddingRight));
+            if (mSavedState != null) {
+                mScrollX = isLayoutRtl()
+                        ? scrollRange - mSavedState.scrollOffsetFromStart
+                        : mSavedState.scrollOffsetFromStart;
+                mSavedState = null;
+            } else {
+                if (isLayoutRtl()) {
+                    mScrollX = scrollRange - mScrollX;
+                } // mScrollX default value is "0" for LTR
+            }
+            // Don't forget to clamp
+            if (mScrollX > scrollRange) {
+                mScrollX = scrollRange;
+            } else if (mScrollX < 0) {
+                mScrollX = 0;
+            }
+        }
+
+        // Calling this with the present values causes it to re-claim them
+        scrollTo(mScrollX, mScrollY);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        View currentFocused = findFocus();
+        if (null == currentFocused || this == currentFocused)
+            return;
+
+        final int maxJump = mRight - mLeft;
+
+        if (isWithinDeltaOfScreen(currentFocused, maxJump)) {
+            currentFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollX(scrollDelta);
+        }
+    }
+
+    /**
+     * Return true if child is a descendant of parent, (or equal to the parent).
+     */
+    private static boolean isViewDescendantOf(View child, View parent) {
+        if (child == parent) {
+            return true;
+        }
+
+        final ViewParent theParent = child.getParent();
+        return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+    }
+
+    /**
+     * Fling the scroll view
+     *
+     * @param velocityX The initial velocity in the X direction. Positive
+     *                  numbers mean that the finger/cursor is moving down the screen,
+     *                  which means we want to scroll towards the left.
+     */
+    public void fling(int velocityX) {
+        if (getChildCount() > 0) {
+            int width = getWidth() - mPaddingRight - mPaddingLeft;
+            int right = getChildAt(0).getWidth();
+
+            mScroller.fling(mScrollX, mScrollY, velocityX, 0, 0,
+                    Math.max(0, right - width), 0, 0, width/2, 0);
+
+            final boolean movingRight = velocityX > 0;
+
+            View currentFocused = findFocus();
+            View newFocused = findFocusableViewInMyBounds(movingRight,
+                    mScroller.getFinalX(), currentFocused);
+
+            if (newFocused == null) {
+                newFocused = this;
+            }
+
+            if (newFocused != currentFocused) {
+                newFocused.requestFocus(movingRight ? View.FOCUS_RIGHT : View.FOCUS_LEFT);
+            }
+
+            postInvalidateOnAnimation();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This version also clamps the scrolling to the bounds of our child.
+     */
+    @Override
+    public void scrollTo(int x, int y) {
+        // we rely on the fact the View.scrollBy calls scrollTo.
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
+            y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
+            if (x != mScrollX || y != mScrollY) {
+                super.scrollTo(x, y);
+            }
+        }
+    }
+
+    @Override
+    public void setOverScrollMode(int mode) {
+        if (mode != OVER_SCROLL_NEVER) {
+            if (mEdgeGlowLeft == null) {
+                Context context = getContext();
+                mEdgeGlowLeft = new EdgeEffect(context);
+                mEdgeGlowRight = new EdgeEffect(context);
+            }
+        } else {
+            mEdgeGlowLeft = null;
+            mEdgeGlowRight = null;
+        }
+        super.setOverScrollMode(mode);
+    }
+
+    @SuppressWarnings({"SuspiciousNameCombination"})
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        if (mEdgeGlowLeft != null) {
+            final int scrollX = mScrollX;
+            if (!mEdgeGlowLeft.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int height = getHeight() - mPaddingTop - mPaddingBottom;
+
+                canvas.rotate(270);
+                canvas.translate(-height + mPaddingTop, Math.min(0, scrollX));
+                mEdgeGlowLeft.setSize(height, getWidth());
+                if (mEdgeGlowLeft.draw(canvas)) {
+                    postInvalidateOnAnimation();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mEdgeGlowRight.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int width = getWidth();
+                final int height = getHeight() - mPaddingTop - mPaddingBottom;
+
+                canvas.rotate(90);
+                canvas.translate(-mPaddingTop,
+                        -(Math.max(getScrollRange(), scrollX) + width));
+                mEdgeGlowRight.setSize(height, width);
+                if (mEdgeGlowRight.draw(canvas)) {
+                    postInvalidateOnAnimation();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+        }
+    }
+
+    private static int clamp(int n, int my, int child) {
+        if (my >= child || n < 0) {
+            return 0;
+        }
+        if ((my + n) > child) {
+            return child - my;
+        }
+        return n;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            // Some old apps reused IDs in ways they shouldn't have.
+            // Don't break them, but they don't get scroll state restoration.
+            super.onRestoreInstanceState(state);
+            return;
+        }
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mSavedState = ss;
+        requestLayout();
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            // Some old apps reused IDs in ways they shouldn't have.
+            // Don't break them, but they don't get scroll state restoration.
+            return super.onSaveInstanceState();
+        }
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.scrollOffsetFromStart = isLayoutRtl() ? -mScrollX : mScrollX;
+        return ss;
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+        encoder.addProperty("layout:fillViewPort", mFillViewport);
+    }
+
+    static class SavedState extends BaseSavedState {
+        public int scrollOffsetFromStart;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public SavedState(Parcel source) {
+            super(source);
+            scrollOffsetFromStart = source.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(scrollOffsetFromStart);
+        }
+
+        @Override
+        public String toString() {
+            return "HorizontalScrollView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " scrollPosition=" + scrollOffsetFromStart
+                    + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+}
diff --git a/android/widget/ImageButton.java b/android/widget/ImageButton.java
new file mode 100644
index 0000000..e1b0c91
--- /dev/null
+++ b/android/widget/ImageButton.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * <p>
+ * Displays a button with an image (instead of text) that can be pressed 
+ * or clicked by the user. By default, an ImageButton looks like a regular 
+ * {@link android.widget.Button}, with the standard button background
+ * that changes color during different button states. The image on the surface
+ * of the button is defined either by the {@code android:src} attribute in the
+ * {@code <ImageButton>} XML element or by the
+ * {@link #setImageResource(int)} method.</p>
+ * 
+ * <p>To remove the standard button background image, define your own 
+ * background image or set the background color to be transparent.</p>
+ * <p>To indicate the different button states (focused, selected, etc.), you can
+ * define a different image for each state. E.g., a blue image by default, an
+ * orange one for when focused, and a yellow one for when pressed. An easy way to
+ * do this is with an XML drawable "selector." For example:</p>
+ * <pre>
+ * &lt;?xml version="1.0" encoding="utf-8"?&gt;
+ * &lt;selector xmlns:android="http://schemas.android.com/apk/res/android"&gt;
+ *     &lt;item android:state_pressed="true"
+ *           android:drawable="@drawable/button_pressed" /&gt; &lt;!-- pressed --&gt;
+ *     &lt;item android:state_focused="true"
+ *           android:drawable="@drawable/button_focused" /&gt; &lt;!-- focused --&gt;
+ *     &lt;item android:drawable="@drawable/button_normal" /&gt; &lt;!-- default --&gt;
+ * &lt;/selector&gt;</pre>
+ *
+ * <p>Save the XML file in your project {@code res/drawable/} folder and then 
+ * reference it as a drawable for the source of your ImageButton (in the 
+ * {@code android:src} attribute). Android will automatically change the image 
+ * based on the state of the button and the corresponding images
+ * defined in the XML.</p>
+ *
+ * <p>The order of the {@code <item>} elements is important because they are
+ * evaluated in order. This is why the "normal" button image comes last, because
+ * it will only be applied after {@code android:state_pressed} and {@code
+ * android:state_focused} have both evaluated false.</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/button.html">Buttons</a>
+ * guide.</p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p>
+ * See {@link android.R.styleable#ImageView Button Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+@RemoteView
+public class ImageButton extends ImageView {
+    public ImageButton(Context context) {
+        this(context, null);
+    }
+
+    public ImageButton(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.imageButtonStyle);
+    }
+
+    public ImageButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ImageButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        setFocusable(true);
+    }
+
+    @Override
+    protected boolean onSetAlpha(int alpha) {
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ImageButton.class.getName();
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+}
diff --git a/android/widget/ImageSwitcher.java b/android/widget/ImageSwitcher.java
new file mode 100644
index 0000000..112fcc3
--- /dev/null
+++ b/android/widget/ImageSwitcher.java
@@ -0,0 +1,105 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.content.Context;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.util.AttributeSet;
+
+/**
+ * {@link ViewSwitcher} that switches between two ImageViews when a new
+ * image is set on it. The views added to an ImageSwitcher must all be
+ * {@link ImageView ImageViews}.
+ */
+public class ImageSwitcher extends ViewSwitcher
+{
+    /**
+     * Creates a new empty ImageSwitcher.
+     *
+     * @param context the application's environment
+     */
+    public ImageSwitcher(Context context)
+    {
+        super(context);
+    }
+
+    /**
+     * Creates a new empty ImageSwitcher for the given context and with the
+     * specified set attributes.
+     *
+     * @param context the application environment
+     * @param attrs a collection of attributes
+     */
+    public ImageSwitcher(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /**
+     * Sets a new image on the ImageSwitcher with the given resource id.
+     * This will set that image resource on the next ImageView in the switcher and will
+     * then switch to that view.
+     *
+     * @param resid a Drawable resource id
+     *
+     * @see ImageView#setImageResource(int)
+     */
+    public void setImageResource(@DrawableRes int resid)
+    {
+        ImageView image = (ImageView)this.getNextView();
+        image.setImageResource(resid);
+        showNext();
+    }
+
+    /**
+     * Sets a new image on the ImageSwitcher with the given Uri.
+     * This will set that image on the next ImageView in the switcher and will
+     * then switch to that view.
+     *
+     * @param uri the Uri of an image
+     *
+     * @see ImageView#setImageURI(Uri)
+     */
+    public void setImageURI(Uri uri)
+    {
+        ImageView image = (ImageView)this.getNextView();
+        image.setImageURI(uri);
+        showNext();
+    }
+
+    /**
+     * Sets a new drawable on the ImageSwitcher.
+     * This will set that drawable on the next ImageView in the switcher and will
+     * then switch to that view.
+     *
+     * @param drawable the drawable to be set or {@code null} to clear the content
+     *
+     * @see ImageView#setImageDrawable(Drawable)
+     */
+    public void setImageDrawable(Drawable drawable)
+    {
+        ImageView image = (ImageView)this.getNextView();
+        image.setImageDrawable(drawable);
+        showNext();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ImageSwitcher.class.getName();
+    }
+}
diff --git a/android/widget/ImageView.java b/android/widget/ImageView.java
new file mode 100644
index 0000000..1dc5b44
--- /dev/null
+++ b/android/widget/ImageView.java
@@ -0,0 +1,1653 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.Matrix;
+import android.graphics.PixelFormat;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffColorFilter;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Xfermode;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.Build;
+import android.os.Handler;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+/**
+ * Displays image resources, for example {@link android.graphics.Bitmap}
+ * or {@link android.graphics.drawable.Drawable} resources.
+ * ImageView is also commonly used to {@link #setImageTintMode(PorterDuff.Mode)
+ * apply tints to an image} and handle {@link #setScaleType(ScaleType) image scaling}.
+ *
+ * <p>
+ * The following XML snippet is a common example of using an ImageView to display an image resource:
+ * </p>
+ * <pre>
+ * &lt;LinearLayout
+ *     xmlns:android="http://schemas.android.com/apk/res/android"
+ *     android:layout_width="match_parent"
+ *     android:layout_height="match_parent"&gt;
+ *     &lt;ImageView
+ *         android:layout_width="wrap_content"
+ *         android:layout_height="wrap_content"
+ *         android:src="@mipmap/ic_launcher"
+ *         /&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ *
+ * <p>
+ * To learn more about Drawables, see: <a href="{@docRoot}guide/topics/resources/drawable-resource.html">Drawable Resources</a>.
+ * To learn more about working with Bitmaps, see: <a href="{@docRoot}topic/performance/graphics/index.htm">Handling Bitmaps</a>.
+ * </p>
+ *
+ * @attr ref android.R.styleable#ImageView_adjustViewBounds
+ * @attr ref android.R.styleable#ImageView_src
+ * @attr ref android.R.styleable#ImageView_maxWidth
+ * @attr ref android.R.styleable#ImageView_maxHeight
+ * @attr ref android.R.styleable#ImageView_tint
+ * @attr ref android.R.styleable#ImageView_scaleType
+ * @attr ref android.R.styleable#ImageView_cropToPadding
+ */
+@RemoteView
+public class ImageView extends View {
+    private static final String LOG_TAG = "ImageView";
+
+    // settable by the client
+    private Uri mUri;
+    private int mResource = 0;
+    private Matrix mMatrix;
+    private ScaleType mScaleType;
+    private boolean mHaveFrame = false;
+    private boolean mAdjustViewBounds = false;
+    private int mMaxWidth = Integer.MAX_VALUE;
+    private int mMaxHeight = Integer.MAX_VALUE;
+
+    // these are applied to the drawable
+    private ColorFilter mColorFilter = null;
+    private boolean mHasColorFilter = false;
+    private Xfermode mXfermode;
+    private int mAlpha = 255;
+    private final int mViewAlphaScale = 256;
+    private boolean mColorMod = false;
+
+    private Drawable mDrawable = null;
+    private BitmapDrawable mRecycleableBitmapDrawable = null;
+    private ColorStateList mDrawableTintList = null;
+    private PorterDuff.Mode mDrawableTintMode = null;
+    private boolean mHasDrawableTint = false;
+    private boolean mHasDrawableTintMode = false;
+
+    private int[] mState = null;
+    private boolean mMergeState = false;
+    private int mLevel = 0;
+    private int mDrawableWidth;
+    private int mDrawableHeight;
+    private Matrix mDrawMatrix = null;
+
+    // Avoid allocations...
+    private final RectF mTempSrc = new RectF();
+    private final RectF mTempDst = new RectF();
+
+    private boolean mCropToPadding;
+
+    private int mBaseline = -1;
+    private boolean mBaselineAlignBottom = false;
+
+    /** Compatibility modes dependent on targetSdkVersion of the app. */
+    private static boolean sCompatDone;
+
+    /** AdjustViewBounds behavior will be in compatibility mode for older apps. */
+    private static boolean sCompatAdjustViewBounds;
+
+    /** Whether to pass Resources when creating the source from a stream. */
+    private static boolean sCompatUseCorrectStreamDensity;
+
+    /** Whether to use pre-Nougat drawable visibility dispatching conditions. */
+    private static boolean sCompatDrawableVisibilityDispatch;
+
+    private static final ScaleType[] sScaleTypeArray = {
+        ScaleType.MATRIX,
+        ScaleType.FIT_XY,
+        ScaleType.FIT_START,
+        ScaleType.FIT_CENTER,
+        ScaleType.FIT_END,
+        ScaleType.CENTER,
+        ScaleType.CENTER_CROP,
+        ScaleType.CENTER_INSIDE
+    };
+
+    public ImageView(Context context) {
+        super(context);
+        initImageView();
+    }
+
+    public ImageView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ImageView(Context context, @Nullable AttributeSet attrs, int defStyleAttr,
+            int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        initImageView();
+
+        // ImageView is not important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_NO);
+        }
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.ImageView, defStyleAttr, defStyleRes);
+
+        final Drawable d = a.getDrawable(R.styleable.ImageView_src);
+        if (d != null) {
+            setImageDrawable(d);
+        }
+
+        mBaselineAlignBottom = a.getBoolean(R.styleable.ImageView_baselineAlignBottom, false);
+        mBaseline = a.getDimensionPixelSize(R.styleable.ImageView_baseline, -1);
+
+        setAdjustViewBounds(a.getBoolean(R.styleable.ImageView_adjustViewBounds, false));
+        setMaxWidth(a.getDimensionPixelSize(R.styleable.ImageView_maxWidth, Integer.MAX_VALUE));
+        setMaxHeight(a.getDimensionPixelSize(R.styleable.ImageView_maxHeight, Integer.MAX_VALUE));
+
+        final int index = a.getInt(R.styleable.ImageView_scaleType, -1);
+        if (index >= 0) {
+            setScaleType(sScaleTypeArray[index]);
+        }
+
+        if (a.hasValue(R.styleable.ImageView_tint)) {
+            mDrawableTintList = a.getColorStateList(R.styleable.ImageView_tint);
+            mHasDrawableTint = true;
+
+            // Prior to L, this attribute would always set a color filter with
+            // blending mode SRC_ATOP. Preserve that default behavior.
+            mDrawableTintMode = PorterDuff.Mode.SRC_ATOP;
+            mHasDrawableTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.ImageView_tintMode)) {
+            mDrawableTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.ImageView_tintMode, -1), mDrawableTintMode);
+            mHasDrawableTintMode = true;
+        }
+
+        applyImageTint();
+
+        final int alpha = a.getInt(R.styleable.ImageView_drawableAlpha, 255);
+        if (alpha != 255) {
+            setImageAlpha(alpha);
+        }
+
+        mCropToPadding = a.getBoolean(
+                R.styleable.ImageView_cropToPadding, false);
+
+        a.recycle();
+
+        //need inflate syntax/reader for matrix
+    }
+
+    private void initImageView() {
+        mMatrix = new Matrix();
+        mScaleType = ScaleType.FIT_CENTER;
+
+        if (!sCompatDone) {
+            final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+            sCompatAdjustViewBounds = targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR1;
+            sCompatUseCorrectStreamDensity = targetSdkVersion > Build.VERSION_CODES.M;
+            sCompatDrawableVisibilityDispatch = targetSdkVersion < Build.VERSION_CODES.N;
+            sCompatDone = true;
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable dr) {
+        return mDrawable == dr || super.verifyDrawable(dr);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mDrawable != null) mDrawable.jumpToCurrentState();
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable dr) {
+        if (dr == mDrawable) {
+            if (dr != null) {
+                // update cached drawable dimensions if they've changed
+                final int w = dr.getIntrinsicWidth();
+                final int h = dr.getIntrinsicHeight();
+                if (w != mDrawableWidth || h != mDrawableHeight) {
+                    mDrawableWidth = w;
+                    mDrawableHeight = h;
+                    // updates the matrix, which is dependent on the bounds
+                    configureBounds();
+                }
+            }
+            /* we invalidate the whole view in this case because it's very
+             * hard to know where the drawable actually is. This is made
+             * complicated because of the offsets and transformations that
+             * can be applied. In theory we could get the drawable's bounds
+             * and run them through the transformation and offsets, but this
+             * is probably not worth the effort.
+             */
+            invalidate();
+        } else {
+            super.invalidateDrawable(dr);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        return (getBackground() != null && getBackground().getCurrent() != null);
+    }
+
+    /** @hide */
+    @Override
+    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEventInternal(event);
+
+        final CharSequence contentDescription = getContentDescription();
+        if (!TextUtils.isEmpty(contentDescription)) {
+            event.getText().add(contentDescription);
+        }
+    }
+
+    /**
+     * True when ImageView is adjusting its bounds
+     * to preserve the aspect ratio of its drawable
+     *
+     * @return whether to adjust the bounds of this view
+     * to preserve the original aspect ratio of the drawable
+     *
+     * @see #setAdjustViewBounds(boolean)
+     *
+     * @attr ref android.R.styleable#ImageView_adjustViewBounds
+     */
+    public boolean getAdjustViewBounds() {
+        return mAdjustViewBounds;
+    }
+
+    /**
+     * Set this to true if you want the ImageView to adjust its bounds
+     * to preserve the aspect ratio of its drawable.
+     *
+     * <p><strong>Note:</strong> If the application targets API level 17 or lower,
+     * adjustViewBounds will allow the drawable to shrink the view bounds, but not grow
+     * to fill available measured space in all cases. This is for compatibility with
+     * legacy {@link android.view.View.MeasureSpec MeasureSpec} and
+     * {@link android.widget.RelativeLayout RelativeLayout} behavior.</p>
+     *
+     * @param adjustViewBounds Whether to adjust the bounds of this view
+     * to preserve the original aspect ratio of the drawable.
+     *
+     * @see #getAdjustViewBounds()
+     *
+     * @attr ref android.R.styleable#ImageView_adjustViewBounds
+     */
+    @android.view.RemotableViewMethod
+    public void setAdjustViewBounds(boolean adjustViewBounds) {
+        mAdjustViewBounds = adjustViewBounds;
+        if (adjustViewBounds) {
+            setScaleType(ScaleType.FIT_CENTER);
+        }
+    }
+
+    /**
+     * The maximum width of this view.
+     *
+     * @return The maximum width of this view
+     *
+     * @see #setMaxWidth(int)
+     *
+     * @attr ref android.R.styleable#ImageView_maxWidth
+     */
+    public int getMaxWidth() {
+        return mMaxWidth;
+    }
+
+    /**
+     * An optional argument to supply a maximum width for this view. Only valid if
+     * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a maximum
+     * of 100 x 100 while preserving the original aspect ratio, do the following: 1) set
+     * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width
+     * layout params to WRAP_CONTENT.
+     *
+     * <p>
+     * Note that this view could be still smaller than 100 x 100 using this approach if the original
+     * image is small. To set an image to a fixed size, specify that size in the layout params and
+     * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit
+     * the image within the bounds.
+     * </p>
+     *
+     * @param maxWidth maximum width for this view
+     *
+     * @see #getMaxWidth()
+     *
+     * @attr ref android.R.styleable#ImageView_maxWidth
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxWidth(int maxWidth) {
+        mMaxWidth = maxWidth;
+    }
+
+    /**
+     * The maximum height of this view.
+     *
+     * @return The maximum height of this view
+     *
+     * @see #setMaxHeight(int)
+     *
+     * @attr ref android.R.styleable#ImageView_maxHeight
+     */
+    public int getMaxHeight() {
+        return mMaxHeight;
+    }
+
+    /**
+     * An optional argument to supply a maximum height for this view. Only valid if
+     * {@link #setAdjustViewBounds(boolean)} has been set to true. To set an image to be a
+     * maximum of 100 x 100 while preserving the original aspect ratio, do the following: 1) set
+     * adjustViewBounds to true 2) set maxWidth and maxHeight to 100 3) set the height and width
+     * layout params to WRAP_CONTENT.
+     *
+     * <p>
+     * Note that this view could be still smaller than 100 x 100 using this approach if the original
+     * image is small. To set an image to a fixed size, specify that size in the layout params and
+     * then use {@link #setScaleType(android.widget.ImageView.ScaleType)} to determine how to fit
+     * the image within the bounds.
+     * </p>
+     *
+     * @param maxHeight maximum height for this view
+     *
+     * @see #getMaxHeight()
+     *
+     * @attr ref android.R.styleable#ImageView_maxHeight
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxHeight(int maxHeight) {
+        mMaxHeight = maxHeight;
+    }
+
+    /**
+     * Gets the current Drawable, or null if no Drawable has been
+     * assigned.
+     *
+     * @return the view's drawable, or null if no drawable has been
+     * assigned.
+     */
+    public Drawable getDrawable() {
+        if (mDrawable == mRecycleableBitmapDrawable) {
+            // Consider our cached version dirty since app code now has a reference to it
+            mRecycleableBitmapDrawable = null;
+        }
+        return mDrawable;
+    }
+
+    private class ImageDrawableCallback implements Runnable {
+
+        private final Drawable drawable;
+        private final Uri uri;
+        private final int resource;
+
+        ImageDrawableCallback(Drawable drawable, Uri uri, int resource) {
+            this.drawable = drawable;
+            this.uri = uri;
+            this.resource = resource;
+        }
+
+        @Override
+        public void run() {
+            setImageDrawable(drawable);
+            mUri = uri;
+            mResource = resource;
+        }
+    }
+
+    /**
+     * Sets a drawable as the content of this ImageView.
+     * <p class="note">This does Bitmap reading and decoding on the UI
+     * thread, which can cause a latency hiccup.  If that's a concern,
+     * consider using {@link #setImageDrawable(android.graphics.drawable.Drawable)} or
+     * {@link #setImageBitmap(android.graphics.Bitmap)} and
+     * {@link android.graphics.BitmapFactory} instead.</p>
+     *
+     * @param resId the resource identifier of the drawable
+     *
+     * @attr ref android.R.styleable#ImageView_src
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setImageResourceAsync")
+    public void setImageResource(@DrawableRes int resId) {
+        // The resource configuration may have changed, so we should always
+        // try to load the resource even if the resId hasn't changed.
+        final int oldWidth = mDrawableWidth;
+        final int oldHeight = mDrawableHeight;
+
+        updateDrawable(null);
+        mResource = resId;
+        mUri = null;
+
+        resolveUri();
+
+        if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+            requestLayout();
+        }
+        invalidate();
+    }
+
+    /** @hide **/
+    public Runnable setImageResourceAsync(@DrawableRes int resId) {
+        Drawable d = null;
+        if (resId != 0) {
+            try {
+                d = getContext().getDrawable(resId);
+            } catch (Exception e) {
+                Log.w(LOG_TAG, "Unable to find resource: " + resId, e);
+                resId = 0;
+            }
+        }
+        return new ImageDrawableCallback(d, null, resId);
+    }
+
+    /**
+     * Sets the content of this ImageView to the specified Uri.
+     * Note that you use this method to load images from a local Uri only.
+     * <p/>
+     * To learn how to display images from a remote Uri see: <a href="https://developer.android.com/topic/performance/graphics/index.html">Handling Bitmaps</a>
+     * <p/>
+     * <p class="note">This does Bitmap reading and decoding on the UI
+     * thread, which can cause a latency hiccup.  If that's a concern,
+     * consider using {@link #setImageDrawable(Drawable)} or
+     * {@link #setImageBitmap(android.graphics.Bitmap)} and
+     * {@link android.graphics.BitmapFactory} instead.</p>
+     *
+     * <p class="note">On devices running SDK < 24, this method will fail to
+     * apply correct density scaling to images loaded from
+     * {@link ContentResolver#SCHEME_CONTENT content} and
+     * {@link ContentResolver#SCHEME_FILE file} schemes. Applications running
+     * on devices with SDK >= 24 <strong>MUST</strong> specify the
+     * {@code targetSdkVersion} in their manifest as 24 or above for density
+     * scaling to be applied to images loaded from these schemes.</p>
+     *
+     * @param uri the Uri of an image, or {@code null} to clear the content
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setImageURIAsync")
+    public void setImageURI(@Nullable Uri uri) {
+        if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) {
+            updateDrawable(null);
+            mResource = 0;
+            mUri = uri;
+
+            final int oldWidth = mDrawableWidth;
+            final int oldHeight = mDrawableHeight;
+
+            resolveUri();
+
+            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+                requestLayout();
+            }
+            invalidate();
+        }
+    }
+
+    /** @hide **/
+    public Runnable setImageURIAsync(@Nullable Uri uri) {
+        if (mResource != 0 || (mUri != uri && (uri == null || mUri == null || !uri.equals(mUri)))) {
+            Drawable d = uri == null ? null : getDrawableFromUri(uri);
+            if (d == null) {
+                // Do not set the URI if the drawable couldn't be loaded.
+                uri = null;
+            }
+            return new ImageDrawableCallback(d, uri, 0);
+        }
+        return null;
+    }
+
+    /**
+     * Sets a drawable as the content of this ImageView.
+     *
+     * @param drawable the Drawable to set, or {@code null} to clear the
+     *                 content
+     */
+    public void setImageDrawable(@Nullable Drawable drawable) {
+        if (mDrawable != drawable) {
+            mResource = 0;
+            mUri = null;
+
+            final int oldWidth = mDrawableWidth;
+            final int oldHeight = mDrawableHeight;
+
+            updateDrawable(drawable);
+
+            if (oldWidth != mDrawableWidth || oldHeight != mDrawableHeight) {
+                requestLayout();
+            }
+            invalidate();
+        }
+    }
+
+    /**
+     * Sets the content of this ImageView to the specified Icon.
+     *
+     * <p class="note">Depending on the Icon type, this may do Bitmap reading
+     * and decoding on the UI thread, which can cause UI jank.  If that's a
+     * concern, consider using
+     * {@link Icon#loadDrawableAsync(Context, Icon.OnDrawableLoadedListener, Handler)}
+     * and then {@link #setImageDrawable(android.graphics.drawable.Drawable)}
+     * instead.</p>
+     *
+     * @param icon an Icon holding the desired image, or {@code null} to clear
+     *             the content
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setImageIconAsync")
+    public void setImageIcon(@Nullable Icon icon) {
+        setImageDrawable(icon == null ? null : icon.loadDrawable(mContext));
+    }
+
+    /** @hide **/
+    public Runnable setImageIconAsync(@Nullable Icon icon) {
+        return new ImageDrawableCallback(icon == null ? null : icon.loadDrawable(mContext), null, 0);
+    }
+
+    /**
+     * Applies a tint to the image drawable. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setImageDrawable(Drawable)} will automatically
+     * mutate the drawable and apply the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     * <p>
+     * <em>Note:</em> The default tint mode used by this setter is NOT
+     * consistent with the default tint mode used by the
+     * {@link android.R.styleable#ImageView_tint android:tint}
+     * attribute. If the {@code android:tint} attribute is specified, the
+     * default tint mode will be set to {@link PorterDuff.Mode#SRC_ATOP} to
+     * ensure consistency with earlier versions of the platform.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#ImageView_tint
+     * @see #getImageTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setImageTintList(@Nullable ColorStateList tint) {
+        mDrawableTintList = tint;
+        mHasDrawableTint = true;
+
+        applyImageTint();
+    }
+
+    /**
+     * Get the current {@link android.content.res.ColorStateList} used to tint the image Drawable,
+     * or null if no tint is applied.
+     *
+     * @return the tint applied to the image drawable
+     * @attr ref android.R.styleable#ImageView_tint
+     * @see #setImageTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getImageTintList() {
+        return mDrawableTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setImageTintList(ColorStateList)}} to the image drawable. The default
+     * mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#ImageView_tintMode
+     * @see #getImageTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setImageTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mDrawableTintMode = tintMode;
+        mHasDrawableTintMode = true;
+
+        applyImageTint();
+    }
+
+    /**
+     * Gets the blending mode used to apply the tint to the image Drawable
+     * @return the blending mode used to apply the tint to the image Drawable
+     * @attr ref android.R.styleable#ImageView_tintMode
+     * @see #setImageTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getImageTintMode() {
+        return mDrawableTintMode;
+    }
+
+    private void applyImageTint() {
+        if (mDrawable != null && (mHasDrawableTint || mHasDrawableTintMode)) {
+            mDrawable = mDrawable.mutate();
+
+            if (mHasDrawableTint) {
+                mDrawable.setTintList(mDrawableTintList);
+            }
+
+            if (mHasDrawableTintMode) {
+                mDrawable.setTintMode(mDrawableTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mDrawable.isStateful()) {
+                mDrawable.setState(getDrawableState());
+            }
+        }
+    }
+
+    /**
+     * Sets a Bitmap as the content of this ImageView.
+     *
+     * @param bm The bitmap to set
+     */
+    @android.view.RemotableViewMethod
+    public void setImageBitmap(Bitmap bm) {
+        // Hacky fix to force setImageDrawable to do a full setImageDrawable
+        // instead of doing an object reference comparison
+        mDrawable = null;
+        if (mRecycleableBitmapDrawable == null) {
+            mRecycleableBitmapDrawable = new BitmapDrawable(mContext.getResources(), bm);
+        } else {
+            mRecycleableBitmapDrawable.setBitmap(bm);
+        }
+        setImageDrawable(mRecycleableBitmapDrawable);
+    }
+
+    /**
+     * Set the state of the current {@link android.graphics.drawable.StateListDrawable}.
+     * For more information about State List Drawables, see: <a href="https://developer.android.com/guide/topics/resources/drawable-resource.html#StateList">the Drawable Resource Guide</a>.
+     *
+     * @param state the state to set for the StateListDrawable
+     * @param merge if true, merges the state values for the state you specify into the current state
+     */
+    public void setImageState(int[] state, boolean merge) {
+        mState = state;
+        mMergeState = merge;
+        if (mDrawable != null) {
+            refreshDrawableState();
+            resizeFromDrawable();
+        }
+    }
+
+    @Override
+    public void setSelected(boolean selected) {
+        super.setSelected(selected);
+        resizeFromDrawable();
+    }
+
+    /**
+     * Sets the image level, when it is constructed from a
+     * {@link android.graphics.drawable.LevelListDrawable}.
+     *
+     * @param level The new level for the image.
+     */
+    @android.view.RemotableViewMethod
+    public void setImageLevel(int level) {
+        mLevel = level;
+        if (mDrawable != null) {
+            mDrawable.setLevel(level);
+            resizeFromDrawable();
+        }
+    }
+
+    /**
+     * Options for scaling the bounds of an image to the bounds of this view.
+     */
+    public enum ScaleType {
+        /**
+         * Scale using the image matrix when drawing. The image matrix can be set using
+         * {@link ImageView#setImageMatrix(Matrix)}. From XML, use this syntax:
+         * <code>android:scaleType="matrix"</code>.
+         */
+        MATRIX      (0),
+        /**
+         * Scale the image using {@link Matrix.ScaleToFit#FILL}.
+         * From XML, use this syntax: <code>android:scaleType="fitXY"</code>.
+         */
+        FIT_XY      (1),
+        /**
+         * Scale the image using {@link Matrix.ScaleToFit#START}.
+         * From XML, use this syntax: <code>android:scaleType="fitStart"</code>.
+         */
+        FIT_START   (2),
+        /**
+         * Scale the image using {@link Matrix.ScaleToFit#CENTER}.
+         * From XML, use this syntax:
+         * <code>android:scaleType="fitCenter"</code>.
+         */
+        FIT_CENTER  (3),
+        /**
+         * Scale the image using {@link Matrix.ScaleToFit#END}.
+         * From XML, use this syntax: <code>android:scaleType="fitEnd"</code>.
+         */
+        FIT_END     (4),
+        /**
+         * Center the image in the view, but perform no scaling.
+         * From XML, use this syntax: <code>android:scaleType="center"</code>.
+         */
+        CENTER      (5),
+        /**
+         * Scale the image uniformly (maintain the image's aspect ratio) so
+         * that both dimensions (width and height) of the image will be equal
+         * to or larger than the corresponding dimension of the view
+         * (minus padding). The image is then centered in the view.
+         * From XML, use this syntax: <code>android:scaleType="centerCrop"</code>.
+         */
+        CENTER_CROP (6),
+        /**
+         * Scale the image uniformly (maintain the image's aspect ratio) so
+         * that both dimensions (width and height) of the image will be equal
+         * to or less than the corresponding dimension of the view
+         * (minus padding). The image is then centered in the view.
+         * From XML, use this syntax: <code>android:scaleType="centerInside"</code>.
+         */
+        CENTER_INSIDE (7);
+
+        ScaleType(int ni) {
+            nativeInt = ni;
+        }
+        final int nativeInt;
+    }
+
+    /**
+     * Controls how the image should be resized or moved to match the size
+     * of this ImageView.
+     *
+     * @param scaleType The desired scaling mode.
+     *
+     * @attr ref android.R.styleable#ImageView_scaleType
+     */
+    public void setScaleType(ScaleType scaleType) {
+        if (scaleType == null) {
+            throw new NullPointerException();
+        }
+
+        if (mScaleType != scaleType) {
+            mScaleType = scaleType;
+
+            setWillNotCacheDrawing(mScaleType == ScaleType.CENTER);
+
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Returns the current ScaleType that is used to scale the bounds of an image to the bounds of the ImageView.
+     * @return The ScaleType used to scale the image.
+     * @see ImageView.ScaleType
+     * @attr ref android.R.styleable#ImageView_scaleType
+     */
+    public ScaleType getScaleType() {
+        return mScaleType;
+    }
+
+    /** Returns the view's optional matrix. This is applied to the
+        view's drawable when it is drawn. If there is no matrix,
+        this method will return an identity matrix.
+        Do not change this matrix in place but make a copy.
+        If you want a different matrix applied to the drawable,
+        be sure to call setImageMatrix().
+    */
+    public Matrix getImageMatrix() {
+        if (mDrawMatrix == null) {
+            return new Matrix(Matrix.IDENTITY_MATRIX);
+        }
+        return mDrawMatrix;
+    }
+
+    /**
+     * Adds a transformation {@link Matrix} that is applied
+     * to the view's drawable when it is drawn.  Allows custom scaling,
+     * translation, and perspective distortion.
+     *
+     * @param matrix The transformation parameters in matrix form.
+     */
+    public void setImageMatrix(Matrix matrix) {
+        // collapse null and identity to just null
+        if (matrix != null && matrix.isIdentity()) {
+            matrix = null;
+        }
+
+        // don't invalidate unless we're actually changing our matrix
+        if (matrix == null && !mMatrix.isIdentity() ||
+                matrix != null && !mMatrix.equals(matrix)) {
+            mMatrix.set(matrix);
+            configureBounds();
+            invalidate();
+        }
+    }
+
+    /**
+     * Return whether this ImageView crops to padding.
+     *
+     * @return whether this ImageView crops to padding
+     *
+     * @see #setCropToPadding(boolean)
+     *
+     * @attr ref android.R.styleable#ImageView_cropToPadding
+     */
+    public boolean getCropToPadding() {
+        return mCropToPadding;
+    }
+
+    /**
+     * Sets whether this ImageView will crop to padding.
+     *
+     * @param cropToPadding whether this ImageView will crop to padding
+     *
+     * @see #getCropToPadding()
+     *
+     * @attr ref android.R.styleable#ImageView_cropToPadding
+     */
+    public void setCropToPadding(boolean cropToPadding) {
+        if (mCropToPadding != cropToPadding) {
+            mCropToPadding = cropToPadding;
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    private void resolveUri() {
+        if (mDrawable != null) {
+            return;
+        }
+
+        if (getResources() == null) {
+            return;
+        }
+
+        Drawable d = null;
+
+        if (mResource != 0) {
+            try {
+                d = mContext.getDrawable(mResource);
+            } catch (Exception e) {
+                Log.w(LOG_TAG, "Unable to find resource: " + mResource, e);
+                // Don't try again.
+                mResource = 0;
+            }
+        } else if (mUri != null) {
+            d = getDrawableFromUri(mUri);
+
+            if (d == null) {
+                Log.w(LOG_TAG, "resolveUri failed on bad bitmap uri: " + mUri);
+                // Don't try again.
+                mUri = null;
+            }
+        } else {
+            return;
+        }
+
+        updateDrawable(d);
+    }
+
+    private Drawable getDrawableFromUri(Uri uri) {
+        final String scheme = uri.getScheme();
+        if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+            try {
+                // Load drawable through Resources, to get the source density information
+                ContentResolver.OpenResourceIdResult r =
+                        mContext.getContentResolver().getResourceId(uri);
+                return r.r.getDrawable(r.id, mContext.getTheme());
+            } catch (Exception e) {
+                Log.w(LOG_TAG, "Unable to open content: " + uri, e);
+            }
+        } else if (ContentResolver.SCHEME_CONTENT.equals(scheme)
+                || ContentResolver.SCHEME_FILE.equals(scheme)) {
+            InputStream stream = null;
+            try {
+                stream = mContext.getContentResolver().openInputStream(uri);
+                return Drawable.createFromResourceStream(sCompatUseCorrectStreamDensity
+                        ? getResources() : null, null, stream, null);
+            } catch (Exception e) {
+                Log.w(LOG_TAG, "Unable to open content: " + uri, e);
+            } finally {
+                if (stream != null) {
+                    try {
+                        stream.close();
+                    } catch (IOException e) {
+                        Log.w(LOG_TAG, "Unable to close content: " + uri, e);
+                    }
+                }
+            }
+        } else {
+            return Drawable.createFromPath(uri.toString());
+        }
+        return null;
+    }
+
+    @Override
+    public int[] onCreateDrawableState(int extraSpace) {
+        if (mState == null) {
+            return super.onCreateDrawableState(extraSpace);
+        } else if (!mMergeState) {
+            return mState;
+        } else {
+            return mergeDrawableStates(
+                    super.onCreateDrawableState(extraSpace + mState.length), mState);
+        }
+    }
+
+    private void updateDrawable(Drawable d) {
+        if (d != mRecycleableBitmapDrawable && mRecycleableBitmapDrawable != null) {
+            mRecycleableBitmapDrawable.setBitmap(null);
+        }
+
+        boolean sameDrawable = false;
+
+        if (mDrawable != null) {
+            sameDrawable = mDrawable == d;
+            mDrawable.setCallback(null);
+            unscheduleDrawable(mDrawable);
+            if (!sCompatDrawableVisibilityDispatch && !sameDrawable && isAttachedToWindow()) {
+                mDrawable.setVisible(false, false);
+            }
+        }
+
+        mDrawable = d;
+
+        if (d != null) {
+            d.setCallback(this);
+            d.setLayoutDirection(getLayoutDirection());
+            if (d.isStateful()) {
+                d.setState(getDrawableState());
+            }
+            if (!sameDrawable || sCompatDrawableVisibilityDispatch) {
+                final boolean visible = sCompatDrawableVisibilityDispatch
+                        ? getVisibility() == VISIBLE
+                        : isAttachedToWindow() && getWindowVisibility() == VISIBLE && isShown();
+                d.setVisible(visible, true);
+            }
+            d.setLevel(mLevel);
+            mDrawableWidth = d.getIntrinsicWidth();
+            mDrawableHeight = d.getIntrinsicHeight();
+            applyImageTint();
+            applyColorMod();
+
+            configureBounds();
+        } else {
+            mDrawableWidth = mDrawableHeight = -1;
+        }
+    }
+
+    private void resizeFromDrawable() {
+        final Drawable d = mDrawable;
+        if (d != null) {
+            int w = d.getIntrinsicWidth();
+            if (w < 0) w = mDrawableWidth;
+            int h = d.getIntrinsicHeight();
+            if (h < 0) h = mDrawableHeight;
+            if (w != mDrawableWidth || h != mDrawableHeight) {
+                mDrawableWidth = w;
+                mDrawableHeight = h;
+                requestLayout();
+            }
+        }
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        if (mDrawable != null) {
+            mDrawable.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    private static final Matrix.ScaleToFit[] sS2FArray = {
+        Matrix.ScaleToFit.FILL,
+        Matrix.ScaleToFit.START,
+        Matrix.ScaleToFit.CENTER,
+        Matrix.ScaleToFit.END
+    };
+
+    private static Matrix.ScaleToFit scaleTypeToScaleToFit(ScaleType st)  {
+        // ScaleToFit enum to their corresponding Matrix.ScaleToFit values
+        return sS2FArray[st.nativeInt - 1];
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        resolveUri();
+        int w;
+        int h;
+
+        // Desired aspect ratio of the view's contents (not including padding)
+        float desiredAspect = 0.0f;
+
+        // We are allowed to change the view's width
+        boolean resizeWidth = false;
+
+        // We are allowed to change the view's height
+        boolean resizeHeight = false;
+
+        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        if (mDrawable == null) {
+            // If no drawable, its intrinsic size is 0.
+            mDrawableWidth = -1;
+            mDrawableHeight = -1;
+            w = h = 0;
+        } else {
+            w = mDrawableWidth;
+            h = mDrawableHeight;
+            if (w <= 0) w = 1;
+            if (h <= 0) h = 1;
+
+            // We are supposed to adjust view bounds to match the aspect
+            // ratio of our drawable. See if that is possible.
+            if (mAdjustViewBounds) {
+                resizeWidth = widthSpecMode != MeasureSpec.EXACTLY;
+                resizeHeight = heightSpecMode != MeasureSpec.EXACTLY;
+
+                desiredAspect = (float) w / (float) h;
+            }
+        }
+
+        final int pleft = mPaddingLeft;
+        final int pright = mPaddingRight;
+        final int ptop = mPaddingTop;
+        final int pbottom = mPaddingBottom;
+
+        int widthSize;
+        int heightSize;
+
+        if (resizeWidth || resizeHeight) {
+            /* If we get here, it means we want to resize to match the
+                drawables aspect ratio, and we have the freedom to change at
+                least one dimension.
+            */
+
+            // Get the max possible width given our constraints
+            widthSize = resolveAdjustedSize(w + pleft + pright, mMaxWidth, widthMeasureSpec);
+
+            // Get the max possible height given our constraints
+            heightSize = resolveAdjustedSize(h + ptop + pbottom, mMaxHeight, heightMeasureSpec);
+
+            if (desiredAspect != 0.0f) {
+                // See what our actual aspect ratio is
+                final float actualAspect = (float)(widthSize - pleft - pright) /
+                                        (heightSize - ptop - pbottom);
+
+                if (Math.abs(actualAspect - desiredAspect) > 0.0000001) {
+
+                    boolean done = false;
+
+                    // Try adjusting width to be proportional to height
+                    if (resizeWidth) {
+                        int newWidth = (int)(desiredAspect * (heightSize - ptop - pbottom)) +
+                                pleft + pright;
+
+                        // Allow the width to outgrow its original estimate if height is fixed.
+                        if (!resizeHeight && !sCompatAdjustViewBounds) {
+                            widthSize = resolveAdjustedSize(newWidth, mMaxWidth, widthMeasureSpec);
+                        }
+
+                        if (newWidth <= widthSize) {
+                            widthSize = newWidth;
+                            done = true;
+                        }
+                    }
+
+                    // Try adjusting height to be proportional to width
+                    if (!done && resizeHeight) {
+                        int newHeight = (int)((widthSize - pleft - pright) / desiredAspect) +
+                                ptop + pbottom;
+
+                        // Allow the height to outgrow its original estimate if width is fixed.
+                        if (!resizeWidth && !sCompatAdjustViewBounds) {
+                            heightSize = resolveAdjustedSize(newHeight, mMaxHeight,
+                                    heightMeasureSpec);
+                        }
+
+                        if (newHeight <= heightSize) {
+                            heightSize = newHeight;
+                        }
+                    }
+                }
+            }
+        } else {
+            /* We are either don't want to preserve the drawables aspect ratio,
+               or we are not allowed to change view dimensions. Just measure in
+               the normal way.
+            */
+            w += pleft + pright;
+            h += ptop + pbottom;
+
+            w = Math.max(w, getSuggestedMinimumWidth());
+            h = Math.max(h, getSuggestedMinimumHeight());
+
+            widthSize = resolveSizeAndState(w, widthMeasureSpec, 0);
+            heightSize = resolveSizeAndState(h, heightMeasureSpec, 0);
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+    }
+
+    private int resolveAdjustedSize(int desiredSize, int maxSize,
+                                   int measureSpec) {
+        int result = desiredSize;
+        final int specMode = MeasureSpec.getMode(measureSpec);
+        final int specSize =  MeasureSpec.getSize(measureSpec);
+        switch (specMode) {
+            case MeasureSpec.UNSPECIFIED:
+                /* Parent says we can be as big as we want. Just don't be larger
+                   than max size imposed on ourselves.
+                */
+                result = Math.min(desiredSize, maxSize);
+                break;
+            case MeasureSpec.AT_MOST:
+                // Parent says we can be as big as we want, up to specSize.
+                // Don't be larger than specSize, and don't be larger than
+                // the max size imposed on ourselves.
+                result = Math.min(Math.min(desiredSize, specSize), maxSize);
+                break;
+            case MeasureSpec.EXACTLY:
+                // No choice. Do what we are told.
+                result = specSize;
+                break;
+        }
+        return result;
+    }
+
+    @Override
+    protected boolean setFrame(int l, int t, int r, int b) {
+        final boolean changed = super.setFrame(l, t, r, b);
+        mHaveFrame = true;
+        configureBounds();
+        return changed;
+    }
+
+    private void configureBounds() {
+        if (mDrawable == null || !mHaveFrame) {
+            return;
+        }
+
+        final int dwidth = mDrawableWidth;
+        final int dheight = mDrawableHeight;
+
+        final int vwidth = getWidth() - mPaddingLeft - mPaddingRight;
+        final int vheight = getHeight() - mPaddingTop - mPaddingBottom;
+
+        final boolean fits = (dwidth < 0 || vwidth == dwidth)
+                && (dheight < 0 || vheight == dheight);
+
+        if (dwidth <= 0 || dheight <= 0 || ScaleType.FIT_XY == mScaleType) {
+            /* If the drawable has no intrinsic size, or we're told to
+                scaletofit, then we just fill our entire view.
+            */
+            mDrawable.setBounds(0, 0, vwidth, vheight);
+            mDrawMatrix = null;
+        } else {
+            // We need to do the scaling ourself, so have the drawable
+            // use its native size.
+            mDrawable.setBounds(0, 0, dwidth, dheight);
+
+            if (ScaleType.MATRIX == mScaleType) {
+                // Use the specified matrix as-is.
+                if (mMatrix.isIdentity()) {
+                    mDrawMatrix = null;
+                } else {
+                    mDrawMatrix = mMatrix;
+                }
+            } else if (fits) {
+                // The bitmap fits exactly, no transform needed.
+                mDrawMatrix = null;
+            } else if (ScaleType.CENTER == mScaleType) {
+                // Center bitmap in view, no scaling.
+                mDrawMatrix = mMatrix;
+                mDrawMatrix.setTranslate(Math.round((vwidth - dwidth) * 0.5f),
+                                         Math.round((vheight - dheight) * 0.5f));
+            } else if (ScaleType.CENTER_CROP == mScaleType) {
+                mDrawMatrix = mMatrix;
+
+                float scale;
+                float dx = 0, dy = 0;
+
+                if (dwidth * vheight > vwidth * dheight) {
+                    scale = (float) vheight / (float) dheight;
+                    dx = (vwidth - dwidth * scale) * 0.5f;
+                } else {
+                    scale = (float) vwidth / (float) dwidth;
+                    dy = (vheight - dheight * scale) * 0.5f;
+                }
+
+                mDrawMatrix.setScale(scale, scale);
+                mDrawMatrix.postTranslate(Math.round(dx), Math.round(dy));
+            } else if (ScaleType.CENTER_INSIDE == mScaleType) {
+                mDrawMatrix = mMatrix;
+                float scale;
+                float dx;
+                float dy;
+
+                if (dwidth <= vwidth && dheight <= vheight) {
+                    scale = 1.0f;
+                } else {
+                    scale = Math.min((float) vwidth / (float) dwidth,
+                            (float) vheight / (float) dheight);
+                }
+
+                dx = Math.round((vwidth - dwidth * scale) * 0.5f);
+                dy = Math.round((vheight - dheight * scale) * 0.5f);
+
+                mDrawMatrix.setScale(scale, scale);
+                mDrawMatrix.postTranslate(dx, dy);
+            } else {
+                // Generate the required transform.
+                mTempSrc.set(0, 0, dwidth, dheight);
+                mTempDst.set(0, 0, vwidth, vheight);
+
+                mDrawMatrix = mMatrix;
+                mDrawMatrix.setRectToRect(mTempSrc, mTempDst, scaleTypeToScaleToFit(mScaleType));
+            }
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable drawable = mDrawable;
+        if (drawable != null && drawable.isStateful()
+                && drawable.setState(getDrawableState())) {
+            invalidateDrawable(drawable);
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mDrawable != null) {
+            mDrawable.setHotspot(x, y);
+        }
+    }
+
+    /** @hide */
+    public void animateTransform(Matrix matrix) {
+        if (mDrawable == null) {
+            return;
+        }
+        if (matrix == null) {
+            mDrawable.setBounds(0, 0, getWidth(), getHeight());
+        } else {
+            mDrawable.setBounds(0, 0, mDrawableWidth, mDrawableHeight);
+            if (mDrawMatrix == null) {
+                mDrawMatrix = new Matrix();
+            }
+            mDrawMatrix.set(matrix);
+        }
+        invalidate();
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (mDrawable == null) {
+            return; // couldn't resolve the URI
+        }
+
+        if (mDrawableWidth == 0 || mDrawableHeight == 0) {
+            return;     // nothing to draw (empty bounds)
+        }
+
+        if (mDrawMatrix == null && mPaddingTop == 0 && mPaddingLeft == 0) {
+            mDrawable.draw(canvas);
+        } else {
+            final int saveCount = canvas.getSaveCount();
+            canvas.save();
+
+            if (mCropToPadding) {
+                final int scrollX = mScrollX;
+                final int scrollY = mScrollY;
+                canvas.clipRect(scrollX + mPaddingLeft, scrollY + mPaddingTop,
+                        scrollX + mRight - mLeft - mPaddingRight,
+                        scrollY + mBottom - mTop - mPaddingBottom);
+            }
+
+            canvas.translate(mPaddingLeft, mPaddingTop);
+
+            if (mDrawMatrix != null) {
+                canvas.concat(mDrawMatrix);
+            }
+            mDrawable.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    /**
+     * <p>Return the offset of the widget's text baseline from the widget's top
+     * boundary. </p>
+     *
+     * @return the offset of the baseline within the widget's bounds or -1
+     *         if baseline alignment is not supported.
+     */
+    @Override
+    @ViewDebug.ExportedProperty(category = "layout")
+    public int getBaseline() {
+        if (mBaselineAlignBottom) {
+            return getMeasuredHeight();
+        } else {
+            return mBaseline;
+        }
+    }
+
+    /**
+     * <p>Set the offset of the widget's text baseline from the widget's top
+     * boundary.  This value is overridden by the {@link #setBaselineAlignBottom(boolean)}
+     * property.</p>
+     *
+     * @param baseline The baseline to use, or -1 if none is to be provided.
+     *
+     * @see #setBaseline(int)
+     * @attr ref android.R.styleable#ImageView_baseline
+     */
+    public void setBaseline(int baseline) {
+        if (mBaseline != baseline) {
+            mBaseline = baseline;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Sets whether the baseline of this view to the bottom of the view.
+     * Setting this value overrides any calls to setBaseline.
+     *
+     * @param aligned If true, the image view will be baseline aligned by its bottom edge.
+     *
+     * @attr ref android.R.styleable#ImageView_baselineAlignBottom
+     */
+    public void setBaselineAlignBottom(boolean aligned) {
+        if (mBaselineAlignBottom != aligned) {
+            mBaselineAlignBottom = aligned;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Checks whether this view's baseline is considered the bottom of the view.
+     *
+     * @return True if the ImageView's baseline is considered the bottom of the view, false if otherwise.
+     * @see #setBaselineAlignBottom(boolean)
+     */
+    public boolean getBaselineAlignBottom() {
+        return mBaselineAlignBottom;
+    }
+
+    /**
+     * Sets a tinting option for the image.
+     *
+     * @param color Color tint to apply.
+     * @param mode How to apply the color.  The standard mode is
+     * {@link PorterDuff.Mode#SRC_ATOP}
+     *
+     * @attr ref android.R.styleable#ImageView_tint
+     */
+    public final void setColorFilter(int color, PorterDuff.Mode mode) {
+        setColorFilter(new PorterDuffColorFilter(color, mode));
+    }
+
+    /**
+     * Set a tinting option for the image. Assumes
+     * {@link PorterDuff.Mode#SRC_ATOP} blending mode.
+     *
+     * @param color Color tint to apply.
+     * @attr ref android.R.styleable#ImageView_tint
+     */
+    @RemotableViewMethod
+    public final void setColorFilter(int color) {
+        setColorFilter(color, PorterDuff.Mode.SRC_ATOP);
+    }
+
+    /**
+     * Removes the image's {@link android.graphics.ColorFilter}.
+     *
+     * @see #setColorFilter(int)
+     * @see #getColorFilter()
+     */
+    public final void clearColorFilter() {
+        setColorFilter(null);
+    }
+
+    /**
+     * @hide Candidate for future API inclusion
+     */
+    public final void setXfermode(Xfermode mode) {
+        if (mXfermode != mode) {
+            mXfermode = mode;
+            mColorMod = true;
+            applyColorMod();
+            invalidate();
+        }
+    }
+
+    /**
+     * Returns the active color filter for this ImageView.
+     *
+     * @return the active color filter for this ImageView
+     *
+     * @see #setColorFilter(android.graphics.ColorFilter)
+     */
+    public ColorFilter getColorFilter() {
+        return mColorFilter;
+    }
+
+    /**
+     * Apply an arbitrary colorfilter to the image.
+     *
+     * @param cf the colorfilter to apply (may be null)
+     *
+     * @see #getColorFilter()
+     */
+    public void setColorFilter(ColorFilter cf) {
+        if (mColorFilter != cf) {
+            mColorFilter = cf;
+            mHasColorFilter = true;
+            mColorMod = true;
+            applyColorMod();
+            invalidate();
+        }
+    }
+
+    /**
+     * Returns the alpha that will be applied to the drawable of this ImageView.
+     *
+     * @return the alpha value that will be applied to the drawable of this
+     * ImageView (between 0 and 255 inclusive, with 0 being transparent and
+     * 255 being opaque)
+     *
+     * @see #setImageAlpha(int)
+     */
+    public int getImageAlpha() {
+        return mAlpha;
+    }
+
+    /**
+     * Sets the alpha value that should be applied to the image.
+     *
+     * @param alpha the alpha value that should be applied to the image (between
+     * 0 and 255 inclusive, with 0 being transparent and 255 being opaque)
+     *
+     * @see #getImageAlpha()
+     */
+    @RemotableViewMethod
+    public void setImageAlpha(int alpha) {
+        setAlpha(alpha);
+    }
+
+    /**
+     * Sets the alpha value that should be applied to the image.
+     *
+     * @param alpha the alpha value that should be applied to the image
+     *
+     * @deprecated use #setImageAlpha(int) instead
+     */
+    @Deprecated
+    @RemotableViewMethod
+    public void setAlpha(int alpha) {
+        alpha &= 0xFF;          // keep it legal
+        if (mAlpha != alpha) {
+            mAlpha = alpha;
+            mColorMod = true;
+            applyColorMod();
+            invalidate();
+        }
+    }
+
+    private void applyColorMod() {
+        // Only mutate and apply when modifications have occurred. This should
+        // not reset the mColorMod flag, since these filters need to be
+        // re-applied if the Drawable is changed.
+        if (mDrawable != null && mColorMod) {
+            mDrawable = mDrawable.mutate();
+            if (mHasColorFilter) {
+                mDrawable.setColorFilter(mColorFilter);
+            }
+            mDrawable.setXfermode(mXfermode);
+            mDrawable.setAlpha(mAlpha * mViewAlphaScale >> 8);
+        }
+    }
+
+    @Override
+    public boolean isOpaque() {
+        return super.isOpaque() || mDrawable != null && mXfermode == null
+                && mDrawable.getOpacity() == PixelFormat.OPAQUE
+                && mAlpha * mViewAlphaScale >> 8 == 255
+                && isFilledByImage();
+    }
+
+    private boolean isFilledByImage() {
+        if (mDrawable == null) {
+            return false;
+        }
+
+        final Rect bounds = mDrawable.getBounds();
+        final Matrix matrix = mDrawMatrix;
+        if (matrix == null) {
+            return bounds.left <= 0 && bounds.top <= 0 && bounds.right >= getWidth()
+                    && bounds.bottom >= getHeight();
+        } else if (matrix.rectStaysRect()) {
+            final RectF boundsSrc = mTempSrc;
+            final RectF boundsDst = mTempDst;
+            boundsSrc.set(bounds);
+            matrix.mapRect(boundsDst, boundsSrc);
+            return boundsDst.left <= 0 && boundsDst.top <= 0 && boundsDst.right >= getWidth()
+                    && boundsDst.bottom >= getHeight();
+        } else {
+            // If the matrix doesn't map to a rectangle, assume the worst.
+            return false;
+        }
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+        // Only do this for new apps post-Nougat
+        if (mDrawable != null && !sCompatDrawableVisibilityDispatch) {
+            mDrawable.setVisible(isVisible, false);
+        }
+    }
+
+    @RemotableViewMethod
+    @Override
+    public void setVisibility(int visibility) {
+        super.setVisibility(visibility);
+        // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+        if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+            mDrawable.setVisible(visibility == VISIBLE, false);
+        }
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+        if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+            mDrawable.setVisible(getVisibility() == VISIBLE, false);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        // Only do this for old apps pre-Nougat; new apps use onVisibilityAggregated
+        if (mDrawable != null && sCompatDrawableVisibilityDispatch) {
+            mDrawable.setVisible(false, false);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ImageView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+        stream.addProperty("layout:baseline", getBaseline());
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public boolean isDefaultFocusHighlightNeeded(Drawable background, Drawable foreground) {
+        final boolean lackFocusState = mDrawable == null || !mDrawable.isStateful()
+                || !mDrawable.hasFocusStateSpecified();
+        return super.isDefaultFocusHighlightNeeded(background, foreground) && lackFocusState;
+    }
+}
diff --git a/android/widget/LayoutPerfTest.java b/android/widget/LayoutPerfTest.java
new file mode 100644
index 0000000..d570ef3
--- /dev/null
+++ b/android/widget/LayoutPerfTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.perftests.utils.StubActivity;
+import android.support.test.annotation.UiThreadTest;
+import android.support.test.filters.LargeTest;
+import android.support.test.rule.ActivityTestRule;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.perftests.core.R;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.List;
+
+import static android.perftests.utils.LayoutUtils.gatherViewTree;
+import static android.perftests.utils.LayoutUtils.requestLayoutForAllNodes;
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class LayoutPerfTest {
+    @Parameterized.Parameters(name = "{0}")
+    public static Collection measureSpecs() {
+        return Arrays.asList(new Object[][] {
+                { "relative", R.layout.test_relative_layout, R.id.relative_layout_root },
+                { "linear", R.layout.test_linear_layout, R.id.linear_layout_root },
+                { "linear_weighted", R.layout.test_linear_layout_weighted,
+                        R.id.linear_layout_weighted_root },
+        });
+    }
+
+    private int[] mMeasureSpecs = {EXACTLY, AT_MOST, UNSPECIFIED};
+
+    private int mLayoutId;
+    private int mViewId;
+
+    public LayoutPerfTest(String key, int layoutId, int viewId) {
+        // key is used in the final report automatically.
+        mLayoutId = layoutId;
+        mViewId = viewId;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule =
+            new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    @UiThreadTest
+    public void testLayoutPerf() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue("We should be running on the main thread",
+                    Looper.getMainLooper().getThread() == Thread.currentThread());
+            assertTrue("We should be running on the main thread",
+                    Looper.myLooper() == Looper.getMainLooper());
+
+            Activity activity = mActivityRule.getActivity();
+            activity.setContentView(mLayoutId);
+
+            ViewGroup viewGroup = (ViewGroup) activity.findViewById(mViewId);
+
+            List<View> allNodes = gatherViewTree(viewGroup);
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+            int length = mMeasureSpecs.length;
+            while (state.keepRunning()) {
+                for (int i = 0; i < length; i++) {
+                    // The overhead of this call is ignorable, like within 1% difference.
+                    requestLayoutForAllNodes(allNodes);
+
+                    viewGroup.measure(mMeasureSpecs[i % length], mMeasureSpecs[i % length]);
+                    viewGroup.layout(0, 0, viewGroup.getMeasuredWidth(), viewGroup.getMeasuredHeight());
+                }
+            }
+        });
+    }
+}
diff --git a/android/widget/LinearLayout.java b/android/widget/LinearLayout.java
new file mode 100644
index 0000000..380bf7a
--- /dev/null
+++ b/android/widget/LinearLayout.java
@@ -0,0 +1,2051 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+
+/**
+ * A layout that arranges other views either horizontally in a single column
+ * or vertically in a single row.
+ *
+ * <p>The following snippet shows how to include a linear layout in your layout XML file:</p>
+ *
+ * <pre>&lt;LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ *   android:layout_width="match_parent"
+ *   android:layout_height="match_parent"
+ *   android:paddingLeft="16dp"
+ *   android:paddingRight="16dp"
+ *   android:orientation="horizontal"
+ *   android:gravity="center"&gt;
+ *
+ *   &lt;!-- Include other widget or layout tags here. These are considered
+ *           "child views" or "children" of the linear layout --&gt;
+ *
+ * &lt;/LinearLayout&gt;</pre>
+ *
+ * <p>Set {@link android.R.styleable#LinearLayout_orientation android:orientation} to specify
+ * whether child views are displayed in a row or column.</p>
+ *
+ * <p>To control how linear layout aligns all the views it contains, set a value for
+ * {@link android.R.styleable#LinearLayout_gravity android:gravity}.  For example, the
+ * snippet above sets android:gravity to "center".  The value you set affects
+ * both horizontal and vertical alignment of all child views within the single row or column.</p>
+ *
+ * <p>You can set
+ * {@link android.R.styleable#LinearLayout_Layout_layout_weight android:layout_weight}
+ * on individual child views to specify how linear layout divides remaining space amongst
+ * the views it contains. See the
+ * <a href="https://developer.android.com/guide/topics/ui/layout/linear.html">Linear Layout</a>
+ * guide for an example.</p>
+ *
+ * <p>See
+ * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
+ * to learn about other attributes you can set on a child view to affect its
+ * position and size in the containing linear layout.</p>
+ *
+ * @attr ref android.R.styleable#LinearLayout_baselineAligned
+ * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex
+ * @attr ref android.R.styleable#LinearLayout_gravity
+ * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+ * @attr ref android.R.styleable#LinearLayout_orientation
+ * @attr ref android.R.styleable#LinearLayout_weightSum
+ */
+@RemoteView
+public class LinearLayout extends ViewGroup {
+    /** @hide */
+    @IntDef({HORIZONTAL, VERTICAL})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface OrientationMode {}
+
+    public static final int HORIZONTAL = 0;
+    public static final int VERTICAL = 1;
+
+    /** @hide */
+    @IntDef(flag = true,
+            value = {
+                SHOW_DIVIDER_NONE,
+                SHOW_DIVIDER_BEGINNING,
+                SHOW_DIVIDER_MIDDLE,
+                SHOW_DIVIDER_END
+            })
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface DividerMode {}
+
+    /**
+     * Don't show any dividers.
+     */
+    public static final int SHOW_DIVIDER_NONE = 0;
+    /**
+     * Show a divider at the beginning of the group.
+     */
+    public static final int SHOW_DIVIDER_BEGINNING = 1;
+    /**
+     * Show dividers between each item in the group.
+     */
+    public static final int SHOW_DIVIDER_MIDDLE = 2;
+    /**
+     * Show a divider at the end of the group.
+     */
+    public static final int SHOW_DIVIDER_END = 4;
+
+    /**
+     * Compatibility check. Old versions of the platform would give different
+     * results from measurement passes using EXACTLY and non-EXACTLY modes,
+     * even when the resulting size was the same.
+     */
+    private final boolean mAllowInconsistentMeasurement;
+
+    /**
+     * Whether the children of this layout are baseline aligned.  Only applicable
+     * if {@link #mOrientation} is horizontal.
+     */
+    @ViewDebug.ExportedProperty(category = "layout")
+    private boolean mBaselineAligned = true;
+
+    /**
+     * If this layout is part of another layout that is baseline aligned,
+     * use the child at this index as the baseline.
+     *
+     * Note: this is orthogonal to {@link #mBaselineAligned}, which is concerned
+     * with whether the children of this layout are baseline aligned.
+     */
+    @ViewDebug.ExportedProperty(category = "layout")
+    private int mBaselineAlignedChildIndex = -1;
+
+    /**
+     * The additional offset to the child's baseline.
+     * We'll calculate the baseline of this layout as we measure vertically; for
+     * horizontal linear layouts, the offset of 0 is appropriate.
+     */
+    @ViewDebug.ExportedProperty(category = "measurement")
+    private int mBaselineChildTop = 0;
+
+    @ViewDebug.ExportedProperty(category = "measurement")
+    private int mOrientation;
+
+    @ViewDebug.ExportedProperty(category = "measurement", flagMapping = {
+            @ViewDebug.FlagToString(mask = -1,
+                equals = -1, name = "NONE"),
+            @ViewDebug.FlagToString(mask = Gravity.NO_GRAVITY,
+                equals = Gravity.NO_GRAVITY,name = "NONE"),
+            @ViewDebug.FlagToString(mask = Gravity.TOP,
+                equals = Gravity.TOP, name = "TOP"),
+            @ViewDebug.FlagToString(mask = Gravity.BOTTOM,
+                equals = Gravity.BOTTOM, name = "BOTTOM"),
+            @ViewDebug.FlagToString(mask = Gravity.LEFT,
+                equals = Gravity.LEFT, name = "LEFT"),
+            @ViewDebug.FlagToString(mask = Gravity.RIGHT,
+                equals = Gravity.RIGHT, name = "RIGHT"),
+            @ViewDebug.FlagToString(mask = Gravity.START,
+                equals = Gravity.START, name = "START"),
+            @ViewDebug.FlagToString(mask = Gravity.END,
+                equals = Gravity.END, name = "END"),
+            @ViewDebug.FlagToString(mask = Gravity.CENTER_VERTICAL,
+                equals = Gravity.CENTER_VERTICAL, name = "CENTER_VERTICAL"),
+            @ViewDebug.FlagToString(mask = Gravity.FILL_VERTICAL,
+                equals = Gravity.FILL_VERTICAL, name = "FILL_VERTICAL"),
+            @ViewDebug.FlagToString(mask = Gravity.CENTER_HORIZONTAL,
+                equals = Gravity.CENTER_HORIZONTAL, name = "CENTER_HORIZONTAL"),
+            @ViewDebug.FlagToString(mask = Gravity.FILL_HORIZONTAL,
+                equals = Gravity.FILL_HORIZONTAL, name = "FILL_HORIZONTAL"),
+            @ViewDebug.FlagToString(mask = Gravity.CENTER,
+                equals = Gravity.CENTER, name = "CENTER"),
+            @ViewDebug.FlagToString(mask = Gravity.FILL,
+                equals = Gravity.FILL, name = "FILL"),
+            @ViewDebug.FlagToString(mask = Gravity.RELATIVE_LAYOUT_DIRECTION,
+                equals = Gravity.RELATIVE_LAYOUT_DIRECTION, name = "RELATIVE")
+        }, formatToHexString = true)
+    private int mGravity = Gravity.START | Gravity.TOP;
+
+    @ViewDebug.ExportedProperty(category = "measurement")
+    private int mTotalLength;
+
+    @ViewDebug.ExportedProperty(category = "layout")
+    private float mWeightSum;
+
+    @ViewDebug.ExportedProperty(category = "layout")
+    private boolean mUseLargestChild;
+
+    private int[] mMaxAscent;
+    private int[] mMaxDescent;
+
+    private static final int VERTICAL_GRAVITY_COUNT = 4;
+
+    private static final int INDEX_CENTER_VERTICAL = 0;
+    private static final int INDEX_TOP = 1;
+    private static final int INDEX_BOTTOM = 2;
+    private static final int INDEX_FILL = 3;
+
+    private Drawable mDivider;
+    private int mDividerWidth;
+    private int mDividerHeight;
+    private int mShowDividers;
+    private int mDividerPadding;
+
+    private int mLayoutDirection = View.LAYOUT_DIRECTION_UNDEFINED;
+
+    public LinearLayout(Context context) {
+        this(context, null);
+    }
+
+    public LinearLayout(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public LinearLayout(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public LinearLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.LinearLayout, defStyleAttr, defStyleRes);
+
+        int index = a.getInt(com.android.internal.R.styleable.LinearLayout_orientation, -1);
+        if (index >= 0) {
+            setOrientation(index);
+        }
+
+        index = a.getInt(com.android.internal.R.styleable.LinearLayout_gravity, -1);
+        if (index >= 0) {
+            setGravity(index);
+        }
+
+        boolean baselineAligned = a.getBoolean(R.styleable.LinearLayout_baselineAligned, true);
+        if (!baselineAligned) {
+            setBaselineAligned(baselineAligned);
+        }
+
+        mWeightSum = a.getFloat(R.styleable.LinearLayout_weightSum, -1.0f);
+
+        mBaselineAlignedChildIndex =
+                a.getInt(com.android.internal.R.styleable.LinearLayout_baselineAlignedChildIndex, -1);
+
+        mUseLargestChild = a.getBoolean(R.styleable.LinearLayout_measureWithLargestChild, false);
+
+        mShowDividers = a.getInt(R.styleable.LinearLayout_showDividers, SHOW_DIVIDER_NONE);
+        mDividerPadding = a.getDimensionPixelSize(R.styleable.LinearLayout_dividerPadding, 0);
+        setDividerDrawable(a.getDrawable(R.styleable.LinearLayout_divider));
+
+        final int version = context.getApplicationInfo().targetSdkVersion;
+        mAllowInconsistentMeasurement = version <= Build.VERSION_CODES.M;
+
+        a.recycle();
+    }
+
+    /**
+     * Returns <code>true</code> if this layout is currently configured to show at least one
+     * divider.
+     */
+    private boolean isShowingDividers() {
+        return (mShowDividers != SHOW_DIVIDER_NONE) && (mDivider != null);
+    }
+
+    /**
+     * Set how dividers should be shown between items in this layout
+     *
+     * @param showDividers One or more of {@link #SHOW_DIVIDER_BEGINNING},
+     *                     {@link #SHOW_DIVIDER_MIDDLE}, or {@link #SHOW_DIVIDER_END}
+     *                     to show dividers, or {@link #SHOW_DIVIDER_NONE} to show no dividers.
+     */
+    public void setShowDividers(@DividerMode int showDividers) {
+        if (showDividers == mShowDividers) {
+            return;
+        }
+        mShowDividers = showDividers;
+
+        setWillNotDraw(!isShowingDividers());
+        requestLayout();
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    /**
+     * @return A flag set indicating how dividers should be shown around items.
+     * @see #setShowDividers(int)
+     */
+    @DividerMode
+    public int getShowDividers() {
+        return mShowDividers;
+    }
+
+    /**
+     * @return the divider Drawable that will divide each item.
+     *
+     * @see #setDividerDrawable(Drawable)
+     *
+     * @attr ref android.R.styleable#LinearLayout_divider
+     */
+    public Drawable getDividerDrawable() {
+        return mDivider;
+    }
+
+    /**
+     * Set a drawable to be used as a divider between items.
+     *
+     * @param divider Drawable that will divide each item.
+     *
+     * @see #setShowDividers(int)
+     *
+     * @attr ref android.R.styleable#LinearLayout_divider
+     */
+    public void setDividerDrawable(Drawable divider) {
+        if (divider == mDivider) {
+            return;
+        }
+        mDivider = divider;
+        if (divider != null) {
+            mDividerWidth = divider.getIntrinsicWidth();
+            mDividerHeight = divider.getIntrinsicHeight();
+        } else {
+            mDividerWidth = 0;
+            mDividerHeight = 0;
+        }
+
+        setWillNotDraw(!isShowingDividers());
+        requestLayout();
+    }
+
+    /**
+     * Set padding displayed on both ends of dividers. For a vertical layout, the padding is applied
+     * to left and right end of dividers. For a horizontal layout, the padding is applied to top and
+     * bottom end of dividers.
+     *
+     * @param padding Padding value in pixels that will be applied to each end
+     *
+     * @see #setShowDividers(int)
+     * @see #setDividerDrawable(Drawable)
+     * @see #getDividerPadding()
+     */
+    public void setDividerPadding(int padding) {
+        if (padding == mDividerPadding) {
+            return;
+        }
+        mDividerPadding = padding;
+
+        if (isShowingDividers()) {
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Get the padding size used to inset dividers in pixels
+     *
+     * @see #setShowDividers(int)
+     * @see #setDividerDrawable(Drawable)
+     * @see #setDividerPadding(int)
+     */
+    public int getDividerPadding() {
+        return mDividerPadding;
+    }
+
+    /**
+     * Get the width of the current divider drawable.
+     *
+     * @hide Used internally by framework.
+     */
+    public int getDividerWidth() {
+        return mDividerWidth;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (mDivider == null) {
+            return;
+        }
+
+        if (mOrientation == VERTICAL) {
+            drawDividersVertical(canvas);
+        } else {
+            drawDividersHorizontal(canvas);
+        }
+    }
+
+    void drawDividersVertical(Canvas canvas) {
+        final int count = getVirtualChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getVirtualChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                if (hasDividerBeforeChildAt(i)) {
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                    final int top = child.getTop() - lp.topMargin - mDividerHeight;
+                    drawHorizontalDivider(canvas, top);
+                }
+            }
+        }
+
+        if (hasDividerBeforeChildAt(count)) {
+            final View child = getLastNonGoneChild();
+            int bottom = 0;
+            if (child == null) {
+                bottom = getHeight() - getPaddingBottom() - mDividerHeight;
+            } else {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                bottom = child.getBottom() + lp.bottomMargin;
+            }
+            drawHorizontalDivider(canvas, bottom);
+        }
+    }
+
+    /**
+     * Finds the last child that is not gone. The last child will be used as the reference for
+     * where the end divider should be drawn.
+     */
+    private View getLastNonGoneChild() {
+        for (int i = getVirtualChildCount() - 1; i >= 0; i--) {
+            final View child = getVirtualChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                return child;
+            }
+        }
+        return null;
+    }
+
+    void drawDividersHorizontal(Canvas canvas) {
+        final int count = getVirtualChildCount();
+        final boolean isLayoutRtl = isLayoutRtl();
+        for (int i = 0; i < count; i++) {
+            final View child = getVirtualChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                if (hasDividerBeforeChildAt(i)) {
+                    final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                    final int position;
+                    if (isLayoutRtl) {
+                        position = child.getRight() + lp.rightMargin;
+                    } else {
+                        position = child.getLeft() - lp.leftMargin - mDividerWidth;
+                    }
+                    drawVerticalDivider(canvas, position);
+                }
+            }
+        }
+
+        if (hasDividerBeforeChildAt(count)) {
+            final View child = getLastNonGoneChild();
+            int position;
+            if (child == null) {
+                if (isLayoutRtl) {
+                    position = getPaddingLeft();
+                } else {
+                    position = getWidth() - getPaddingRight() - mDividerWidth;
+                }
+            } else {
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (isLayoutRtl) {
+                    position = child.getLeft() - lp.leftMargin - mDividerWidth;
+                } else {
+                    position = child.getRight() + lp.rightMargin;
+                }
+            }
+            drawVerticalDivider(canvas, position);
+        }
+    }
+
+    void drawHorizontalDivider(Canvas canvas, int top) {
+        mDivider.setBounds(getPaddingLeft() + mDividerPadding, top,
+                getWidth() - getPaddingRight() - mDividerPadding, top + mDividerHeight);
+        mDivider.draw(canvas);
+    }
+
+    void drawVerticalDivider(Canvas canvas, int left) {
+        mDivider.setBounds(left, getPaddingTop() + mDividerPadding,
+                left + mDividerWidth, getHeight() - getPaddingBottom() - mDividerPadding);
+        mDivider.draw(canvas);
+    }
+
+    /**
+     * <p>Indicates whether widgets contained within this layout are aligned
+     * on their baseline or not.</p>
+     *
+     * @return true when widgets are baseline-aligned, false otherwise
+     */
+    public boolean isBaselineAligned() {
+        return mBaselineAligned;
+    }
+
+    /**
+     * <p>Defines whether widgets contained in this layout are
+     * baseline-aligned or not.</p>
+     *
+     * @param baselineAligned true to align widgets on their baseline,
+     *         false otherwise
+     *
+     * @attr ref android.R.styleable#LinearLayout_baselineAligned
+     */
+    @android.view.RemotableViewMethod
+    public void setBaselineAligned(boolean baselineAligned) {
+        mBaselineAligned = baselineAligned;
+    }
+
+    /**
+     * When true, all children with a weight will be considered having
+     * the minimum size of the largest child. If false, all children are
+     * measured normally.
+     *
+     * @return True to measure children with a weight using the minimum
+     *         size of the largest child, false otherwise.
+     *
+     * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+     */
+    public boolean isMeasureWithLargestChildEnabled() {
+        return mUseLargestChild;
+    }
+
+    /**
+     * When set to true, all children with a weight will be considered having
+     * the minimum size of the largest child. If false, all children are
+     * measured normally.
+     *
+     * Disabled by default.
+     *
+     * @param enabled True to measure children with a weight using the
+     *        minimum size of the largest child, false otherwise.
+     *
+     * @attr ref android.R.styleable#LinearLayout_measureWithLargestChild
+     */
+    @android.view.RemotableViewMethod
+    public void setMeasureWithLargestChildEnabled(boolean enabled) {
+        mUseLargestChild = enabled;
+    }
+
+    @Override
+    public int getBaseline() {
+        if (mBaselineAlignedChildIndex < 0) {
+            return super.getBaseline();
+        }
+
+        if (getChildCount() <= mBaselineAlignedChildIndex) {
+            throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout "
+                    + "set to an index that is out of bounds.");
+        }
+
+        final View child = getChildAt(mBaselineAlignedChildIndex);
+        final int childBaseline = child.getBaseline();
+
+        if (childBaseline == -1) {
+            if (mBaselineAlignedChildIndex == 0) {
+                // this is just the default case, safe to return -1
+                return -1;
+            }
+            // the user picked an index that points to something that doesn't
+            // know how to calculate its baseline.
+            throw new RuntimeException("mBaselineAlignedChildIndex of LinearLayout "
+                    + "points to a View that doesn't know how to get its baseline.");
+        }
+
+        // TODO: This should try to take into account the virtual offsets
+        // (See getNextLocationOffset and getLocationOffset)
+        // We should add to childTop:
+        // sum([getNextLocationOffset(getChildAt(i)) / i < mBaselineAlignedChildIndex])
+        // and also add:
+        // getLocationOffset(child)
+        int childTop = mBaselineChildTop;
+
+        if (mOrientation == VERTICAL) {
+            final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+            if (majorGravity != Gravity.TOP) {
+               switch (majorGravity) {
+                   case Gravity.BOTTOM:
+                       childTop = mBottom - mTop - mPaddingBottom - mTotalLength;
+                       break;
+
+                   case Gravity.CENTER_VERTICAL:
+                       childTop += ((mBottom - mTop - mPaddingTop - mPaddingBottom) -
+                               mTotalLength) / 2;
+                       break;
+               }
+            }
+        }
+
+        LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+        return childTop + lp.topMargin + childBaseline;
+    }
+
+    /**
+     * @return The index of the child that will be used if this layout is
+     *   part of a larger layout that is baseline aligned, or -1 if none has
+     *   been set.
+     */
+    public int getBaselineAlignedChildIndex() {
+        return mBaselineAlignedChildIndex;
+    }
+
+    /**
+     * @param i The index of the child that will be used if this layout is
+     *          part of a larger layout that is baseline aligned.
+     *
+     * @attr ref android.R.styleable#LinearLayout_baselineAlignedChildIndex
+     */
+    @android.view.RemotableViewMethod
+    public void setBaselineAlignedChildIndex(int i) {
+        if ((i < 0) || (i >= getChildCount())) {
+            throw new IllegalArgumentException("base aligned child index out "
+                    + "of range (0, " + getChildCount() + ")");
+        }
+        mBaselineAlignedChildIndex = i;
+    }
+
+    /**
+     * <p>Returns the view at the specified index. This method can be overriden
+     * to take into account virtual children. Refer to
+     * {@link android.widget.TableLayout} and {@link android.widget.TableRow}
+     * for an example.</p>
+     *
+     * @param index the child's index
+     * @return the child at the specified index, may be {@code null}
+     */
+    @Nullable
+    View getVirtualChildAt(int index) {
+        return getChildAt(index);
+    }
+
+    /**
+     * <p>Returns the virtual number of children. This number might be different
+     * than the actual number of children if the layout can hold virtual
+     * children. Refer to
+     * {@link android.widget.TableLayout} and {@link android.widget.TableRow}
+     * for an example.</p>
+     *
+     * @return the virtual number of children
+     */
+    int getVirtualChildCount() {
+        return getChildCount();
+    }
+
+    /**
+     * Returns the desired weights sum.
+     *
+     * @return A number greater than 0.0f if the weight sum is defined, or
+     *         a number lower than or equals to 0.0f if not weight sum is
+     *         to be used.
+     */
+    public float getWeightSum() {
+        return mWeightSum;
+    }
+
+    /**
+     * Defines the desired weights sum. If unspecified the weights sum is computed
+     * at layout time by adding the layout_weight of each child.
+     *
+     * This can be used for instance to give a single child 50% of the total
+     * available space by giving it a layout_weight of 0.5 and setting the
+     * weightSum to 1.0.
+     *
+     * @param weightSum a number greater than 0.0f, or a number lower than or equals
+     *        to 0.0f if the weight sum should be computed from the children's
+     *        layout_weight
+     */
+    @android.view.RemotableViewMethod
+    public void setWeightSum(float weightSum) {
+        mWeightSum = Math.max(0.0f, weightSum);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mOrientation == VERTICAL) {
+            measureVertical(widthMeasureSpec, heightMeasureSpec);
+        } else {
+            measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+        }
+    }
+
+    /**
+     * Determines where to position dividers between children.
+     *
+     * @param childIndex Index of child to check for preceding divider
+     * @return true if there should be a divider before the child at childIndex
+     * @hide Pending API consideration. Currently only used internally by the system.
+     */
+    protected boolean hasDividerBeforeChildAt(int childIndex) {
+        if (childIndex == getVirtualChildCount()) {
+            // Check whether the end divider should draw.
+            return (mShowDividers & SHOW_DIVIDER_END) != 0;
+        }
+        boolean allViewsAreGoneBefore = allViewsAreGoneBefore(childIndex);
+        if (allViewsAreGoneBefore) {
+            // This is the first view that's not gone, check if beginning divider is enabled.
+            return (mShowDividers & SHOW_DIVIDER_BEGINNING) != 0;
+        } else {
+            return (mShowDividers & SHOW_DIVIDER_MIDDLE) != 0;
+        }
+    }
+
+    /**
+     * Checks whether all (virtual) child views before the given index are gone.
+     */
+    private boolean allViewsAreGoneBefore(int childIndex) {
+        for (int i = childIndex - 1; i >= 0; i--) {
+            final View child = getVirtualChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Measures the children when the orientation of this LinearLayout is set
+     * to {@link #VERTICAL}.
+     *
+     * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
+     * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
+     *
+     * @see #getOrientation()
+     * @see #setOrientation(int)
+     * @see #onMeasure(int, int)
+     */
+    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
+        mTotalLength = 0;
+        int maxWidth = 0;
+        int childState = 0;
+        int alternativeMaxWidth = 0;
+        int weightedMaxWidth = 0;
+        boolean allFillParent = true;
+        float totalWeight = 0;
+
+        final int count = getVirtualChildCount();
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        boolean matchWidth = false;
+        boolean skippedMeasure = false;
+
+        final int baselineChildIndex = mBaselineAlignedChildIndex;
+        final boolean useLargestChild = mUseLargestChild;
+
+        int largestChildHeight = Integer.MIN_VALUE;
+        int consumedExcessSpace = 0;
+
+        int nonSkippedChildCount = 0;
+
+        // See how tall everyone is. Also remember max width.
+        for (int i = 0; i < count; ++i) {
+            final View child = getVirtualChildAt(i);
+            if (child == null) {
+                mTotalLength += measureNullChild(i);
+                continue;
+            }
+
+            if (child.getVisibility() == View.GONE) {
+               i += getChildrenSkipCount(child, i);
+               continue;
+            }
+
+            nonSkippedChildCount++;
+            if (hasDividerBeforeChildAt(i)) {
+                mTotalLength += mDividerHeight;
+            }
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            totalWeight += lp.weight;
+
+            final boolean useExcessSpace = lp.height == 0 && lp.weight > 0;
+            if (heightMode == MeasureSpec.EXACTLY && useExcessSpace) {
+                // Optimization: don't bother measuring children who are only
+                // laid out using excess space. These views will get measured
+                // later if we have space to distribute.
+                final int totalLength = mTotalLength;
+                mTotalLength = Math.max(totalLength, totalLength + lp.topMargin + lp.bottomMargin);
+                skippedMeasure = true;
+            } else {
+                if (useExcessSpace) {
+                    // The heightMode is either UNSPECIFIED or AT_MOST, and
+                    // this child is only laid out using excess space. Measure
+                    // using WRAP_CONTENT so that we can find out the view's
+                    // optimal height. We'll restore the original height of 0
+                    // after measurement.
+                    lp.height = LayoutParams.WRAP_CONTENT;
+                }
+
+                // Determine how big this child would like to be. If this or
+                // previous children have given a weight, then we allow it to
+                // use all available space (and we will shrink things later
+                // if needed).
+                final int usedHeight = totalWeight == 0 ? mTotalLength : 0;
+                measureChildBeforeLayout(child, i, widthMeasureSpec, 0,
+                        heightMeasureSpec, usedHeight);
+
+                final int childHeight = child.getMeasuredHeight();
+                if (useExcessSpace) {
+                    // Restore the original height and record how much space
+                    // we've allocated to excess-only children so that we can
+                    // match the behavior of EXACTLY measurement.
+                    lp.height = 0;
+                    consumedExcessSpace += childHeight;
+                }
+
+                final int totalLength = mTotalLength;
+                mTotalLength = Math.max(totalLength, totalLength + childHeight + lp.topMargin +
+                       lp.bottomMargin + getNextLocationOffset(child));
+
+                if (useLargestChild) {
+                    largestChildHeight = Math.max(childHeight, largestChildHeight);
+                }
+            }
+
+            /**
+             * If applicable, compute the additional offset to the child's baseline
+             * we'll need later when asked {@link #getBaseline}.
+             */
+            if ((baselineChildIndex >= 0) && (baselineChildIndex == i + 1)) {
+               mBaselineChildTop = mTotalLength;
+            }
+
+            // if we are trying to use a child index for our baseline, the above
+            // book keeping only works if there are no children above it with
+            // weight.  fail fast to aid the developer.
+            if (i < baselineChildIndex && lp.weight > 0) {
+                throw new RuntimeException("A child of LinearLayout with index "
+                        + "less than mBaselineAlignedChildIndex has weight > 0, which "
+                        + "won't work.  Either remove the weight, or don't set "
+                        + "mBaselineAlignedChildIndex.");
+            }
+
+            boolean matchWidthLocally = false;
+            if (widthMode != MeasureSpec.EXACTLY && lp.width == LayoutParams.MATCH_PARENT) {
+                // The width of the linear layout will scale, and at least one
+                // child said it wanted to match our width. Set a flag
+                // indicating that we need to remeasure at least that view when
+                // we know our width.
+                matchWidth = true;
+                matchWidthLocally = true;
+            }
+
+            final int margin = lp.leftMargin + lp.rightMargin;
+            final int measuredWidth = child.getMeasuredWidth() + margin;
+            maxWidth = Math.max(maxWidth, measuredWidth);
+            childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+            allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
+            if (lp.weight > 0) {
+                /*
+                 * Widths of weighted Views are bogus if we end up
+                 * remeasuring, so keep them separate.
+                 */
+                weightedMaxWidth = Math.max(weightedMaxWidth,
+                        matchWidthLocally ? margin : measuredWidth);
+            } else {
+                alternativeMaxWidth = Math.max(alternativeMaxWidth,
+                        matchWidthLocally ? margin : measuredWidth);
+            }
+
+            i += getChildrenSkipCount(child, i);
+        }
+
+        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
+            mTotalLength += mDividerHeight;
+        }
+
+        if (useLargestChild &&
+                (heightMode == MeasureSpec.AT_MOST || heightMode == MeasureSpec.UNSPECIFIED)) {
+            mTotalLength = 0;
+
+            for (int i = 0; i < count; ++i) {
+                final View child = getVirtualChildAt(i);
+                if (child == null) {
+                    mTotalLength += measureNullChild(i);
+                    continue;
+                }
+
+                if (child.getVisibility() == GONE) {
+                    i += getChildrenSkipCount(child, i);
+                    continue;
+                }
+
+                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+                        child.getLayoutParams();
+                // Account for negative margins
+                final int totalLength = mTotalLength;
+                mTotalLength = Math.max(totalLength, totalLength + largestChildHeight +
+                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
+            }
+        }
+
+        // Add in our padding
+        mTotalLength += mPaddingTop + mPaddingBottom;
+
+        int heightSize = mTotalLength;
+
+        // Check against our minimum height
+        heightSize = Math.max(heightSize, getSuggestedMinimumHeight());
+
+        // Reconcile our calculated size with the heightMeasureSpec
+        int heightSizeAndState = resolveSizeAndState(heightSize, heightMeasureSpec, 0);
+        heightSize = heightSizeAndState & MEASURED_SIZE_MASK;
+        // Either expand children with weight to take up available space or
+        // shrink them if they extend beyond our current bounds. If we skipped
+        // measurement on any children, we need to measure them now.
+        int remainingExcess = heightSize - mTotalLength
+                + (mAllowInconsistentMeasurement ? 0 : consumedExcessSpace);
+        if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
+            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
+
+            mTotalLength = 0;
+
+            for (int i = 0; i < count; ++i) {
+                final View child = getVirtualChildAt(i);
+                if (child == null || child.getVisibility() == View.GONE) {
+                    continue;
+                }
+
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                final float childWeight = lp.weight;
+                if (childWeight > 0) {
+                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
+                    remainingExcess -= share;
+                    remainingWeightSum -= childWeight;
+
+                    final int childHeight;
+                    if (mUseLargestChild && heightMode != MeasureSpec.EXACTLY) {
+                        childHeight = largestChildHeight;
+                    } else if (lp.height == 0 && (!mAllowInconsistentMeasurement
+                            || heightMode == MeasureSpec.EXACTLY)) {
+                        // This child needs to be laid out from scratch using
+                        // only its share of excess space.
+                        childHeight = share;
+                    } else {
+                        // This child had some intrinsic height to which we
+                        // need to add its share of excess space.
+                        childHeight = child.getMeasuredHeight() + share;
+                    }
+
+                    final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            Math.max(0, childHeight), MeasureSpec.EXACTLY);
+                    final int childWidthMeasureSpec = getChildMeasureSpec(widthMeasureSpec,
+                            mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin,
+                            lp.width);
+                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+                    // Child may now not fit in vertical dimension.
+                    childState = combineMeasuredStates(childState, child.getMeasuredState()
+                            & (MEASURED_STATE_MASK>>MEASURED_HEIGHT_STATE_SHIFT));
+                }
+
+                final int margin =  lp.leftMargin + lp.rightMargin;
+                final int measuredWidth = child.getMeasuredWidth() + margin;
+                maxWidth = Math.max(maxWidth, measuredWidth);
+
+                boolean matchWidthLocally = widthMode != MeasureSpec.EXACTLY &&
+                        lp.width == LayoutParams.MATCH_PARENT;
+
+                alternativeMaxWidth = Math.max(alternativeMaxWidth,
+                        matchWidthLocally ? margin : measuredWidth);
+
+                allFillParent = allFillParent && lp.width == LayoutParams.MATCH_PARENT;
+
+                final int totalLength = mTotalLength;
+                mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredHeight() +
+                        lp.topMargin + lp.bottomMargin + getNextLocationOffset(child));
+            }
+
+            // Add in our padding
+            mTotalLength += mPaddingTop + mPaddingBottom;
+            // TODO: Should we recompute the heightSpec based on the new total length?
+        } else {
+            alternativeMaxWidth = Math.max(alternativeMaxWidth,
+                                           weightedMaxWidth);
+
+
+            // We have no limit, so make all weighted views as tall as the largest child.
+            // Children will have already been measured once.
+            if (useLargestChild && heightMode != MeasureSpec.EXACTLY) {
+                for (int i = 0; i < count; i++) {
+                    final View child = getVirtualChildAt(i);
+                    if (child == null || child.getVisibility() == View.GONE) {
+                        continue;
+                    }
+
+                    final LinearLayout.LayoutParams lp =
+                            (LinearLayout.LayoutParams) child.getLayoutParams();
+
+                    float childExtra = lp.weight;
+                    if (childExtra > 0) {
+                        child.measure(
+                                MeasureSpec.makeMeasureSpec(child.getMeasuredWidth(),
+                                        MeasureSpec.EXACTLY),
+                                MeasureSpec.makeMeasureSpec(largestChildHeight,
+                                        MeasureSpec.EXACTLY));
+                    }
+                }
+            }
+        }
+
+        if (!allFillParent && widthMode != MeasureSpec.EXACTLY) {
+            maxWidth = alternativeMaxWidth;
+        }
+
+        maxWidth += mPaddingLeft + mPaddingRight;
+
+        // Check against our minimum width
+        maxWidth = Math.max(maxWidth, getSuggestedMinimumWidth());
+
+        setMeasuredDimension(resolveSizeAndState(maxWidth, widthMeasureSpec, childState),
+                heightSizeAndState);
+
+        if (matchWidth) {
+            forceUniformWidth(count, heightMeasureSpec);
+        }
+    }
+
+    private void forceUniformWidth(int count, int heightMeasureSpec) {
+        // Pretend that the linear layout has an exact size.
+        int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredWidth(),
+                MeasureSpec.EXACTLY);
+        for (int i = 0; i< count; ++i) {
+           final View child = getVirtualChildAt(i);
+           if (child != null && child.getVisibility() != GONE) {
+               LinearLayout.LayoutParams lp = ((LinearLayout.LayoutParams)child.getLayoutParams());
+
+               if (lp.width == LayoutParams.MATCH_PARENT) {
+                   // Temporarily force children to reuse their old measured height
+                   // FIXME: this may not be right for something like wrapping text?
+                   int oldHeight = lp.height;
+                   lp.height = child.getMeasuredHeight();
+
+                   // Remeasue with new dimensions
+                   measureChildWithMargins(child, uniformMeasureSpec, 0, heightMeasureSpec, 0);
+                   lp.height = oldHeight;
+               }
+           }
+        }
+    }
+
+    /**
+     * Measures the children when the orientation of this LinearLayout is set
+     * to {@link #HORIZONTAL}.
+     *
+     * @param widthMeasureSpec Horizontal space requirements as imposed by the parent.
+     * @param heightMeasureSpec Vertical space requirements as imposed by the parent.
+     *
+     * @see #getOrientation()
+     * @see #setOrientation(int)
+     * @see #onMeasure(int, int)
+     */
+    void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
+        mTotalLength = 0;
+        int maxHeight = 0;
+        int childState = 0;
+        int alternativeMaxHeight = 0;
+        int weightedMaxHeight = 0;
+        boolean allFillParent = true;
+        float totalWeight = 0;
+
+        final int count = getVirtualChildCount();
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        boolean matchHeight = false;
+        boolean skippedMeasure = false;
+
+        if (mMaxAscent == null || mMaxDescent == null) {
+            mMaxAscent = new int[VERTICAL_GRAVITY_COUNT];
+            mMaxDescent = new int[VERTICAL_GRAVITY_COUNT];
+        }
+
+        final int[] maxAscent = mMaxAscent;
+        final int[] maxDescent = mMaxDescent;
+
+        maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
+        maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
+
+        final boolean baselineAligned = mBaselineAligned;
+        final boolean useLargestChild = mUseLargestChild;
+
+        final boolean isExactly = widthMode == MeasureSpec.EXACTLY;
+
+        int largestChildWidth = Integer.MIN_VALUE;
+        int usedExcessSpace = 0;
+
+        int nonSkippedChildCount = 0;
+
+        // See how wide everyone is. Also remember max height.
+        for (int i = 0; i < count; ++i) {
+            final View child = getVirtualChildAt(i);
+            if (child == null) {
+                mTotalLength += measureNullChild(i);
+                continue;
+            }
+
+            if (child.getVisibility() == GONE) {
+                i += getChildrenSkipCount(child, i);
+                continue;
+            }
+
+            nonSkippedChildCount++;
+            if (hasDividerBeforeChildAt(i)) {
+                mTotalLength += mDividerWidth;
+            }
+
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            totalWeight += lp.weight;
+
+            final boolean useExcessSpace = lp.width == 0 && lp.weight > 0;
+            if (widthMode == MeasureSpec.EXACTLY && useExcessSpace) {
+                // Optimization: don't bother measuring children who are only
+                // laid out using excess space. These views will get measured
+                // later if we have space to distribute.
+                if (isExactly) {
+                    mTotalLength += lp.leftMargin + lp.rightMargin;
+                } else {
+                    final int totalLength = mTotalLength;
+                    mTotalLength = Math.max(totalLength, totalLength +
+                            lp.leftMargin + lp.rightMargin);
+                }
+
+                // Baseline alignment requires to measure widgets to obtain the
+                // baseline offset (in particular for TextViews). The following
+                // defeats the optimization mentioned above. Allow the child to
+                // use as much space as it wants because we can shrink things
+                // later (and re-measure).
+                if (baselineAligned) {
+                    final int freeWidthSpec = MeasureSpec.makeSafeMeasureSpec(
+                            MeasureSpec.getSize(widthMeasureSpec), MeasureSpec.UNSPECIFIED);
+                    final int freeHeightSpec = MeasureSpec.makeSafeMeasureSpec(
+                            MeasureSpec.getSize(heightMeasureSpec), MeasureSpec.UNSPECIFIED);
+                    child.measure(freeWidthSpec, freeHeightSpec);
+                } else {
+                    skippedMeasure = true;
+                }
+            } else {
+                if (useExcessSpace) {
+                    // The widthMode is either UNSPECIFIED or AT_MOST, and
+                    // this child is only laid out using excess space. Measure
+                    // using WRAP_CONTENT so that we can find out the view's
+                    // optimal width. We'll restore the original width of 0
+                    // after measurement.
+                    lp.width = LayoutParams.WRAP_CONTENT;
+                }
+
+                // Determine how big this child would like to be. If this or
+                // previous children have given a weight, then we allow it to
+                // use all available space (and we will shrink things later
+                // if needed).
+                final int usedWidth = totalWeight == 0 ? mTotalLength : 0;
+                measureChildBeforeLayout(child, i, widthMeasureSpec, usedWidth,
+                        heightMeasureSpec, 0);
+
+                final int childWidth = child.getMeasuredWidth();
+                if (useExcessSpace) {
+                    // Restore the original width and record how much space
+                    // we've allocated to excess-only children so that we can
+                    // match the behavior of EXACTLY measurement.
+                    lp.width = 0;
+                    usedExcessSpace += childWidth;
+                }
+
+                if (isExactly) {
+                    mTotalLength += childWidth + lp.leftMargin + lp.rightMargin
+                            + getNextLocationOffset(child);
+                } else {
+                    final int totalLength = mTotalLength;
+                    mTotalLength = Math.max(totalLength, totalLength + childWidth + lp.leftMargin
+                            + lp.rightMargin + getNextLocationOffset(child));
+                }
+
+                if (useLargestChild) {
+                    largestChildWidth = Math.max(childWidth, largestChildWidth);
+                }
+            }
+
+            boolean matchHeightLocally = false;
+            if (heightMode != MeasureSpec.EXACTLY && lp.height == LayoutParams.MATCH_PARENT) {
+                // The height of the linear layout will scale, and at least one
+                // child said it wanted to match our height. Set a flag indicating that
+                // we need to remeasure at least that view when we know our height.
+                matchHeight = true;
+                matchHeightLocally = true;
+            }
+
+            final int margin = lp.topMargin + lp.bottomMargin;
+            final int childHeight = child.getMeasuredHeight() + margin;
+            childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+            if (baselineAligned) {
+                final int childBaseline = child.getBaseline();
+                if (childBaseline != -1) {
+                    // Translates the child's vertical gravity into an index
+                    // in the range 0..VERTICAL_GRAVITY_COUNT
+                    final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
+                            & Gravity.VERTICAL_GRAVITY_MASK;
+                    final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
+                            & ~Gravity.AXIS_SPECIFIED) >> 1;
+
+                    maxAscent[index] = Math.max(maxAscent[index], childBaseline);
+                    maxDescent[index] = Math.max(maxDescent[index], childHeight - childBaseline);
+                }
+            }
+
+            maxHeight = Math.max(maxHeight, childHeight);
+
+            allFillParent = allFillParent && lp.height == LayoutParams.MATCH_PARENT;
+            if (lp.weight > 0) {
+                /*
+                 * Heights of weighted Views are bogus if we end up
+                 * remeasuring, so keep them separate.
+                 */
+                weightedMaxHeight = Math.max(weightedMaxHeight,
+                        matchHeightLocally ? margin : childHeight);
+            } else {
+                alternativeMaxHeight = Math.max(alternativeMaxHeight,
+                        matchHeightLocally ? margin : childHeight);
+            }
+
+            i += getChildrenSkipCount(child, i);
+        }
+
+        if (nonSkippedChildCount > 0 && hasDividerBeforeChildAt(count)) {
+            mTotalLength += mDividerWidth;
+        }
+
+        // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP,
+        // the most common case
+        if (maxAscent[INDEX_TOP] != -1 ||
+                maxAscent[INDEX_CENTER_VERTICAL] != -1 ||
+                maxAscent[INDEX_BOTTOM] != -1 ||
+                maxAscent[INDEX_FILL] != -1) {
+            final int ascent = Math.max(maxAscent[INDEX_FILL],
+                    Math.max(maxAscent[INDEX_CENTER_VERTICAL],
+                    Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM])));
+            final int descent = Math.max(maxDescent[INDEX_FILL],
+                    Math.max(maxDescent[INDEX_CENTER_VERTICAL],
+                    Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM])));
+            maxHeight = Math.max(maxHeight, ascent + descent);
+        }
+
+        if (useLargestChild &&
+                (widthMode == MeasureSpec.AT_MOST || widthMode == MeasureSpec.UNSPECIFIED)) {
+            mTotalLength = 0;
+
+            for (int i = 0; i < count; ++i) {
+                final View child = getVirtualChildAt(i);
+                if (child == null) {
+                    mTotalLength += measureNullChild(i);
+                    continue;
+                }
+
+                if (child.getVisibility() == GONE) {
+                    i += getChildrenSkipCount(child, i);
+                    continue;
+                }
+
+                final LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams)
+                        child.getLayoutParams();
+                if (isExactly) {
+                    mTotalLength += largestChildWidth + lp.leftMargin + lp.rightMargin +
+                            getNextLocationOffset(child);
+                } else {
+                    final int totalLength = mTotalLength;
+                    mTotalLength = Math.max(totalLength, totalLength + largestChildWidth +
+                            lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
+                }
+            }
+        }
+
+        // Add in our padding
+        mTotalLength += mPaddingLeft + mPaddingRight;
+
+        int widthSize = mTotalLength;
+
+        // Check against our minimum width
+        widthSize = Math.max(widthSize, getSuggestedMinimumWidth());
+
+        // Reconcile our calculated size with the widthMeasureSpec
+        int widthSizeAndState = resolveSizeAndState(widthSize, widthMeasureSpec, 0);
+        widthSize = widthSizeAndState & MEASURED_SIZE_MASK;
+
+        // Either expand children with weight to take up available space or
+        // shrink them if they extend beyond our current bounds. If we skipped
+        // measurement on any children, we need to measure them now.
+        int remainingExcess = widthSize - mTotalLength
+                + (mAllowInconsistentMeasurement ? 0 : usedExcessSpace);
+        if (skippedMeasure || remainingExcess != 0 && totalWeight > 0.0f) {
+            float remainingWeightSum = mWeightSum > 0.0f ? mWeightSum : totalWeight;
+
+            maxAscent[0] = maxAscent[1] = maxAscent[2] = maxAscent[3] = -1;
+            maxDescent[0] = maxDescent[1] = maxDescent[2] = maxDescent[3] = -1;
+            maxHeight = -1;
+
+            mTotalLength = 0;
+
+            for (int i = 0; i < count; ++i) {
+                final View child = getVirtualChildAt(i);
+                if (child == null || child.getVisibility() == View.GONE) {
+                    continue;
+                }
+
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                final float childWeight = lp.weight;
+                if (childWeight > 0) {
+                    final int share = (int) (childWeight * remainingExcess / remainingWeightSum);
+                    remainingExcess -= share;
+                    remainingWeightSum -= childWeight;
+
+                    final int childWidth;
+                    if (mUseLargestChild && widthMode != MeasureSpec.EXACTLY) {
+                        childWidth = largestChildWidth;
+                    } else if (lp.width == 0 && (!mAllowInconsistentMeasurement
+                            || widthMode == MeasureSpec.EXACTLY)) {
+                        // This child needs to be laid out from scratch using
+                        // only its share of excess space.
+                        childWidth = share;
+                    } else {
+                        // This child had some intrinsic width to which we
+                        // need to add its share of excess space.
+                        childWidth = child.getMeasuredWidth() + share;
+                    }
+
+                    final int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                            Math.max(0, childWidth), MeasureSpec.EXACTLY);
+                    final int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                            mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin,
+                            lp.height);
+                    child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+                    // Child may now not fit in horizontal dimension.
+                    childState = combineMeasuredStates(childState,
+                            child.getMeasuredState() & MEASURED_STATE_MASK);
+                }
+
+                if (isExactly) {
+                    mTotalLength += child.getMeasuredWidth() + lp.leftMargin + lp.rightMargin +
+                            getNextLocationOffset(child);
+                } else {
+                    final int totalLength = mTotalLength;
+                    mTotalLength = Math.max(totalLength, totalLength + child.getMeasuredWidth() +
+                            lp.leftMargin + lp.rightMargin + getNextLocationOffset(child));
+                }
+
+                boolean matchHeightLocally = heightMode != MeasureSpec.EXACTLY &&
+                        lp.height == LayoutParams.MATCH_PARENT;
+
+                final int margin = lp.topMargin + lp .bottomMargin;
+                int childHeight = child.getMeasuredHeight() + margin;
+                maxHeight = Math.max(maxHeight, childHeight);
+                alternativeMaxHeight = Math.max(alternativeMaxHeight,
+                        matchHeightLocally ? margin : childHeight);
+
+                allFillParent = allFillParent && lp.height == LayoutParams.MATCH_PARENT;
+
+                if (baselineAligned) {
+                    final int childBaseline = child.getBaseline();
+                    if (childBaseline != -1) {
+                        // Translates the child's vertical gravity into an index in the range 0..2
+                        final int gravity = (lp.gravity < 0 ? mGravity : lp.gravity)
+                                & Gravity.VERTICAL_GRAVITY_MASK;
+                        final int index = ((gravity >> Gravity.AXIS_Y_SHIFT)
+                                & ~Gravity.AXIS_SPECIFIED) >> 1;
+
+                        maxAscent[index] = Math.max(maxAscent[index], childBaseline);
+                        maxDescent[index] = Math.max(maxDescent[index],
+                                childHeight - childBaseline);
+                    }
+                }
+            }
+
+            // Add in our padding
+            mTotalLength += mPaddingLeft + mPaddingRight;
+            // TODO: Should we update widthSize with the new total length?
+
+            // Check mMaxAscent[INDEX_TOP] first because it maps to Gravity.TOP,
+            // the most common case
+            if (maxAscent[INDEX_TOP] != -1 ||
+                    maxAscent[INDEX_CENTER_VERTICAL] != -1 ||
+                    maxAscent[INDEX_BOTTOM] != -1 ||
+                    maxAscent[INDEX_FILL] != -1) {
+                final int ascent = Math.max(maxAscent[INDEX_FILL],
+                        Math.max(maxAscent[INDEX_CENTER_VERTICAL],
+                        Math.max(maxAscent[INDEX_TOP], maxAscent[INDEX_BOTTOM])));
+                final int descent = Math.max(maxDescent[INDEX_FILL],
+                        Math.max(maxDescent[INDEX_CENTER_VERTICAL],
+                        Math.max(maxDescent[INDEX_TOP], maxDescent[INDEX_BOTTOM])));
+                maxHeight = Math.max(maxHeight, ascent + descent);
+            }
+        } else {
+            alternativeMaxHeight = Math.max(alternativeMaxHeight, weightedMaxHeight);
+
+            // We have no limit, so make all weighted views as wide as the largest child.
+            // Children will have already been measured once.
+            if (useLargestChild && widthMode != MeasureSpec.EXACTLY) {
+                for (int i = 0; i < count; i++) {
+                    final View child = getVirtualChildAt(i);
+                    if (child == null || child.getVisibility() == View.GONE) {
+                        continue;
+                    }
+
+                    final LinearLayout.LayoutParams lp =
+                            (LinearLayout.LayoutParams) child.getLayoutParams();
+
+                    float childExtra = lp.weight;
+                    if (childExtra > 0) {
+                        child.measure(
+                                MeasureSpec.makeMeasureSpec(largestChildWidth, MeasureSpec.EXACTLY),
+                                MeasureSpec.makeMeasureSpec(child.getMeasuredHeight(),
+                                        MeasureSpec.EXACTLY));
+                    }
+                }
+            }
+        }
+
+        if (!allFillParent && heightMode != MeasureSpec.EXACTLY) {
+            maxHeight = alternativeMaxHeight;
+        }
+
+        maxHeight += mPaddingTop + mPaddingBottom;
+
+        // Check against our minimum height
+        maxHeight = Math.max(maxHeight, getSuggestedMinimumHeight());
+
+        setMeasuredDimension(widthSizeAndState | (childState&MEASURED_STATE_MASK),
+                resolveSizeAndState(maxHeight, heightMeasureSpec,
+                        (childState<<MEASURED_HEIGHT_STATE_SHIFT)));
+
+        if (matchHeight) {
+            forceUniformHeight(count, widthMeasureSpec);
+        }
+    }
+
+    private void forceUniformHeight(int count, int widthMeasureSpec) {
+        // Pretend that the linear layout has an exact size. This is the measured height of
+        // ourselves. The measured height should be the max height of the children, changed
+        // to accommodate the heightMeasureSpec from the parent
+        int uniformMeasureSpec = MeasureSpec.makeMeasureSpec(getMeasuredHeight(),
+                MeasureSpec.EXACTLY);
+        for (int i = 0; i < count; ++i) {
+           final View child = getVirtualChildAt(i);
+           if (child != null && child.getVisibility() != GONE) {
+               LinearLayout.LayoutParams lp = (LinearLayout.LayoutParams) child.getLayoutParams();
+
+               if (lp.height == LayoutParams.MATCH_PARENT) {
+                   // Temporarily force children to reuse their old measured width
+                   // FIXME: this may not be right for something like wrapping text?
+                   int oldWidth = lp.width;
+                   lp.width = child.getMeasuredWidth();
+
+                   // Remeasure with new dimensions
+                   measureChildWithMargins(child, widthMeasureSpec, 0, uniformMeasureSpec, 0);
+                   lp.width = oldWidth;
+               }
+           }
+        }
+    }
+
+    /**
+     * <p>Returns the number of children to skip after measuring/laying out
+     * the specified child.</p>
+     *
+     * @param child the child after which we want to skip children
+     * @param index the index of the child after which we want to skip children
+     * @return the number of children to skip, 0 by default
+     */
+    int getChildrenSkipCount(View child, int index) {
+        return 0;
+    }
+
+    /**
+     * <p>Returns the size (width or height) that should be occupied by a null
+     * child.</p>
+     *
+     * @param childIndex the index of the null child
+     * @return the width or height of the child depending on the orientation
+     */
+    int measureNullChild(int childIndex) {
+        return 0;
+    }
+
+    /**
+     * <p>Measure the child according to the parent's measure specs. This
+     * method should be overriden by subclasses to force the sizing of
+     * children. This method is called by {@link #measureVertical(int, int)} and
+     * {@link #measureHorizontal(int, int)}.</p>
+     *
+     * @param child the child to measure
+     * @param childIndex the index of the child in this view
+     * @param widthMeasureSpec horizontal space requirements as imposed by the parent
+     * @param totalWidth extra space that has been used up by the parent horizontally
+     * @param heightMeasureSpec vertical space requirements as imposed by the parent
+     * @param totalHeight extra space that has been used up by the parent vertically
+     */
+    void measureChildBeforeLayout(View child, int childIndex,
+            int widthMeasureSpec, int totalWidth, int heightMeasureSpec,
+            int totalHeight) {
+        measureChildWithMargins(child, widthMeasureSpec, totalWidth,
+                heightMeasureSpec, totalHeight);
+    }
+
+    /**
+     * <p>Return the location offset of the specified child. This can be used
+     * by subclasses to change the location of a given widget.</p>
+     *
+     * @param child the child for which to obtain the location offset
+     * @return the location offset in pixels
+     */
+    int getLocationOffset(View child) {
+        return 0;
+    }
+
+    /**
+     * <p>Return the size offset of the next sibling of the specified child.
+     * This can be used by subclasses to change the location of the widget
+     * following <code>child</code>.</p>
+     *
+     * @param child the child whose next sibling will be moved
+     * @return the location offset of the next child in pixels
+     */
+    int getNextLocationOffset(View child) {
+        return 0;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (mOrientation == VERTICAL) {
+            layoutVertical(l, t, r, b);
+        } else {
+            layoutHorizontal(l, t, r, b);
+        }
+    }
+
+    /**
+     * Position the children during a layout pass if the orientation of this
+     * LinearLayout is set to {@link #VERTICAL}.
+     *
+     * @see #getOrientation()
+     * @see #setOrientation(int)
+     * @see #onLayout(boolean, int, int, int, int)
+     * @param left
+     * @param top
+     * @param right
+     * @param bottom
+     */
+    void layoutVertical(int left, int top, int right, int bottom) {
+        final int paddingLeft = mPaddingLeft;
+
+        int childTop;
+        int childLeft;
+
+        // Where right end of child should go
+        final int width = right - left;
+        int childRight = width - mPaddingRight;
+
+        // Space available for child
+        int childSpace = width - paddingLeft - mPaddingRight;
+
+        final int count = getVirtualChildCount();
+
+        final int majorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        final int minorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+
+        switch (majorGravity) {
+           case Gravity.BOTTOM:
+               // mTotalLength contains the padding already
+               childTop = mPaddingTop + bottom - top - mTotalLength;
+               break;
+
+               // mTotalLength contains the padding already
+           case Gravity.CENTER_VERTICAL:
+               childTop = mPaddingTop + (bottom - top - mTotalLength) / 2;
+               break;
+
+           case Gravity.TOP:
+           default:
+               childTop = mPaddingTop;
+               break;
+        }
+
+        for (int i = 0; i < count; i++) {
+            final View child = getVirtualChildAt(i);
+            if (child == null) {
+                childTop += measureNullChild(i);
+            } else if (child.getVisibility() != GONE) {
+                final int childWidth = child.getMeasuredWidth();
+                final int childHeight = child.getMeasuredHeight();
+
+                final LinearLayout.LayoutParams lp =
+                        (LinearLayout.LayoutParams) child.getLayoutParams();
+
+                int gravity = lp.gravity;
+                if (gravity < 0) {
+                    gravity = minorGravity;
+                }
+                final int layoutDirection = getLayoutDirection();
+                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                    case Gravity.CENTER_HORIZONTAL:
+                        childLeft = paddingLeft + ((childSpace - childWidth) / 2)
+                                + lp.leftMargin - lp.rightMargin;
+                        break;
+
+                    case Gravity.RIGHT:
+                        childLeft = childRight - childWidth - lp.rightMargin;
+                        break;
+
+                    case Gravity.LEFT:
+                    default:
+                        childLeft = paddingLeft + lp.leftMargin;
+                        break;
+                }
+
+                if (hasDividerBeforeChildAt(i)) {
+                    childTop += mDividerHeight;
+                }
+
+                childTop += lp.topMargin;
+                setChildFrame(child, childLeft, childTop + getLocationOffset(child),
+                        childWidth, childHeight);
+                childTop += childHeight + lp.bottomMargin + getNextLocationOffset(child);
+
+                i += getChildrenSkipCount(child, i);
+            }
+        }
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        if (layoutDirection != mLayoutDirection) {
+            mLayoutDirection = layoutDirection;
+            if (mOrientation == HORIZONTAL) {
+                requestLayout();
+            }
+        }
+    }
+
+    /**
+     * Position the children during a layout pass if the orientation of this
+     * LinearLayout is set to {@link #HORIZONTAL}.
+     *
+     * @see #getOrientation()
+     * @see #setOrientation(int)
+     * @see #onLayout(boolean, int, int, int, int)
+     * @param left
+     * @param top
+     * @param right
+     * @param bottom
+     */
+    void layoutHorizontal(int left, int top, int right, int bottom) {
+        final boolean isLayoutRtl = isLayoutRtl();
+        final int paddingTop = mPaddingTop;
+
+        int childTop;
+        int childLeft;
+
+        // Where bottom of child should go
+        final int height = bottom - top;
+        int childBottom = height - mPaddingBottom;
+
+        // Space available for child
+        int childSpace = height - paddingTop - mPaddingBottom;
+
+        final int count = getVirtualChildCount();
+
+        final int majorGravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+        final int minorGravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+        final boolean baselineAligned = mBaselineAligned;
+
+        final int[] maxAscent = mMaxAscent;
+        final int[] maxDescent = mMaxDescent;
+
+        final int layoutDirection = getLayoutDirection();
+        switch (Gravity.getAbsoluteGravity(majorGravity, layoutDirection)) {
+            case Gravity.RIGHT:
+                // mTotalLength contains the padding already
+                childLeft = mPaddingLeft + right - left - mTotalLength;
+                break;
+
+            case Gravity.CENTER_HORIZONTAL:
+                // mTotalLength contains the padding already
+                childLeft = mPaddingLeft + (right - left - mTotalLength) / 2;
+                break;
+
+            case Gravity.LEFT:
+            default:
+                childLeft = mPaddingLeft;
+                break;
+        }
+
+        int start = 0;
+        int dir = 1;
+        //In case of RTL, start drawing from the last child.
+        if (isLayoutRtl) {
+            start = count - 1;
+            dir = -1;
+        }
+
+        for (int i = 0; i < count; i++) {
+            final int childIndex = start + dir * i;
+            final View child = getVirtualChildAt(childIndex);
+            if (child == null) {
+                childLeft += measureNullChild(childIndex);
+            } else if (child.getVisibility() != GONE) {
+                final int childWidth = child.getMeasuredWidth();
+                final int childHeight = child.getMeasuredHeight();
+                int childBaseline = -1;
+
+                final LinearLayout.LayoutParams lp =
+                        (LinearLayout.LayoutParams) child.getLayoutParams();
+
+                if (baselineAligned && lp.height != LayoutParams.MATCH_PARENT) {
+                    childBaseline = child.getBaseline();
+                }
+
+                int gravity = lp.gravity;
+                if (gravity < 0) {
+                    gravity = minorGravity;
+                }
+
+                switch (gravity & Gravity.VERTICAL_GRAVITY_MASK) {
+                    case Gravity.TOP:
+                        childTop = paddingTop + lp.topMargin;
+                        if (childBaseline != -1) {
+                            childTop += maxAscent[INDEX_TOP] - childBaseline;
+                        }
+                        break;
+
+                    case Gravity.CENTER_VERTICAL:
+                        // Removed support for baseline alignment when layout_gravity or
+                        // gravity == center_vertical. See bug #1038483.
+                        // Keep the code around if we need to re-enable this feature
+                        // if (childBaseline != -1) {
+                        //     // Align baselines vertically only if the child is smaller than us
+                        //     if (childSpace - childHeight > 0) {
+                        //         childTop = paddingTop + (childSpace / 2) - childBaseline;
+                        //     } else {
+                        //         childTop = paddingTop + (childSpace - childHeight) / 2;
+                        //     }
+                        // } else {
+                        childTop = paddingTop + ((childSpace - childHeight) / 2)
+                                + lp.topMargin - lp.bottomMargin;
+                        break;
+
+                    case Gravity.BOTTOM:
+                        childTop = childBottom - childHeight - lp.bottomMargin;
+                        if (childBaseline != -1) {
+                            int descent = child.getMeasuredHeight() - childBaseline;
+                            childTop -= (maxDescent[INDEX_BOTTOM] - descent);
+                        }
+                        break;
+                    default:
+                        childTop = paddingTop;
+                        break;
+                }
+
+                if (hasDividerBeforeChildAt(childIndex)) {
+                    childLeft += mDividerWidth;
+                }
+
+                childLeft += lp.leftMargin;
+                setChildFrame(child, childLeft + getLocationOffset(child), childTop,
+                        childWidth, childHeight);
+                childLeft += childWidth + lp.rightMargin +
+                        getNextLocationOffset(child);
+
+                i += getChildrenSkipCount(child, childIndex);
+            }
+        }
+    }
+
+    private void setChildFrame(View child, int left, int top, int width, int height) {
+        child.layout(left, top, left + width, top + height);
+    }
+
+    /**
+     * Should the layout be a column or a row.
+     * @param orientation Pass {@link #HORIZONTAL} or {@link #VERTICAL}. Default
+     * value is {@link #HORIZONTAL}.
+     *
+     * @attr ref android.R.styleable#LinearLayout_orientation
+     */
+    public void setOrientation(@OrientationMode int orientation) {
+        if (mOrientation != orientation) {
+            mOrientation = orientation;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Returns the current orientation.
+     *
+     * @return either {@link #HORIZONTAL} or {@link #VERTICAL}
+     */
+    @OrientationMode
+    public int getOrientation() {
+        return mOrientation;
+    }
+
+    /**
+     * Describes how the child views are positioned. Defaults to GRAVITY_TOP. If
+     * this layout has a VERTICAL orientation, this controls where all the child
+     * views are placed if there is extra vertical space. If this layout has a
+     * HORIZONTAL orientation, this controls the alignment of the children.
+     *
+     * @param gravity See {@link android.view.Gravity}
+     *
+     * @attr ref android.R.styleable#LinearLayout_gravity
+     */
+    @android.view.RemotableViewMethod
+    public void setGravity(int gravity) {
+        if (mGravity != gravity) {
+            if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+                gravity |= Gravity.START;
+            }
+
+            if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+                gravity |= Gravity.TOP;
+            }
+
+            mGravity = gravity;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Returns the current gravity. See {@link android.view.Gravity}
+     *
+     * @return the current gravity.
+     * @see #setGravity
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    @android.view.RemotableViewMethod
+    public void setHorizontalGravity(int horizontalGravity) {
+        final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+        if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) {
+            mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity;
+            requestLayout();
+        }
+    }
+
+    @android.view.RemotableViewMethod
+    public void setVerticalGravity(int verticalGravity) {
+        final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) {
+            mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity;
+            requestLayout();
+        }
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LinearLayout.LayoutParams(getContext(), attrs);
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+     * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+     * when the layout's orientation is {@link #VERTICAL}. When the orientation is
+     * {@link #HORIZONTAL}, the width is set to {@link LayoutParams#WRAP_CONTENT}
+     * and the height to {@link LayoutParams#WRAP_CONTENT}.
+     */
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        if (mOrientation == HORIZONTAL) {
+            return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+        } else if (mOrientation == VERTICAL) {
+            return new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        }
+        return null;
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (sPreserveMarginParamsInLayoutParamConversion) {
+            if (lp instanceof LayoutParams) {
+                return new LayoutParams((LayoutParams) lp);
+            } else if (lp instanceof MarginLayoutParams) {
+                return new LayoutParams((MarginLayoutParams) lp);
+            }
+        }
+        return new LayoutParams(lp);
+    }
+
+
+    // Override to allow type-checking of LayoutParams.
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof LinearLayout.LayoutParams;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return LinearLayout.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+        encoder.addProperty("layout:baselineAligned", mBaselineAligned);
+        encoder.addProperty("layout:baselineAlignedChildIndex", mBaselineAlignedChildIndex);
+        encoder.addProperty("measurement:baselineChildTop", mBaselineChildTop);
+        encoder.addProperty("measurement:orientation", mOrientation);
+        encoder.addProperty("measurement:gravity", mGravity);
+        encoder.addProperty("measurement:totalLength", mTotalLength);
+        encoder.addProperty("layout:totalLength", mTotalLength);
+        encoder.addProperty("layout:useLargestChild", mUseLargestChild);
+    }
+
+    /**
+     * Per-child layout information associated with ViewLinearLayout.
+     *
+     * @attr ref android.R.styleable#LinearLayout_Layout_layout_weight
+     * @attr ref android.R.styleable#LinearLayout_Layout_layout_gravity
+     */
+    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+        /**
+         * Indicates how much of the extra space in the LinearLayout will be
+         * allocated to the view associated with these LayoutParams. Specify
+         * 0 if the view should not be stretched. Otherwise the extra pixels
+         * will be pro-rated among all views whose weight is greater than 0.
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public float weight;
+
+        /**
+         * Gravity for the view associated with these LayoutParams.
+         *
+         * @see android.view.Gravity
+         */
+        @ViewDebug.ExportedProperty(category = "layout", mapping = {
+            @ViewDebug.IntToString(from =  -1,                       to = "NONE"),
+            @ViewDebug.IntToString(from = Gravity.NO_GRAVITY,        to = "NONE"),
+            @ViewDebug.IntToString(from = Gravity.TOP,               to = "TOP"),
+            @ViewDebug.IntToString(from = Gravity.BOTTOM,            to = "BOTTOM"),
+            @ViewDebug.IntToString(from = Gravity.LEFT,              to = "LEFT"),
+            @ViewDebug.IntToString(from = Gravity.RIGHT,             to = "RIGHT"),
+            @ViewDebug.IntToString(from = Gravity.START,             to = "START"),
+            @ViewDebug.IntToString(from = Gravity.END,               to = "END"),
+            @ViewDebug.IntToString(from = Gravity.CENTER_VERTICAL,   to = "CENTER_VERTICAL"),
+            @ViewDebug.IntToString(from = Gravity.FILL_VERTICAL,     to = "FILL_VERTICAL"),
+            @ViewDebug.IntToString(from = Gravity.CENTER_HORIZONTAL, to = "CENTER_HORIZONTAL"),
+            @ViewDebug.IntToString(from = Gravity.FILL_HORIZONTAL,   to = "FILL_HORIZONTAL"),
+            @ViewDebug.IntToString(from = Gravity.CENTER,            to = "CENTER"),
+            @ViewDebug.IntToString(from = Gravity.FILL,              to = "FILL")
+        })
+        public int gravity = -1;
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            TypedArray a =
+                    c.obtainStyledAttributes(attrs, com.android.internal.R.styleable.LinearLayout_Layout);
+
+            weight = a.getFloat(com.android.internal.R.styleable.LinearLayout_Layout_layout_weight, 0);
+            gravity = a.getInt(com.android.internal.R.styleable.LinearLayout_Layout_layout_gravity, -1);
+
+            a.recycle();
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int width, int height) {
+            super(width, height);
+            weight = 0;
+        }
+
+        /**
+         * Creates a new set of layout parameters with the specified width, height
+         * and weight.
+         *
+         * @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 weight the weight
+         */
+        public LayoutParams(int width, int height, float weight) {
+            super(width, height);
+            this.weight = weight;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams p) {
+            super(p);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.MarginLayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * Copy constructor. Clones the width, height, margin values, weight,
+         * and gravity of the source.
+         *
+         * @param source The layout params to copy from.
+         */
+        public LayoutParams(LayoutParams source) {
+            super(source);
+
+            this.weight = source.weight;
+            this.gravity = source.gravity;
+        }
+
+        @Override
+        public String debug(String output) {
+            return output + "LinearLayout.LayoutParams={width=" + sizeToString(width) +
+                    ", height=" + sizeToString(height) + " weight=" + weight +  "}";
+        }
+
+        /** @hide */
+        @Override
+        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+            super.encodeProperties(encoder);
+
+            encoder.addProperty("layout:weight", weight);
+            encoder.addProperty("layout:gravity", gravity);
+        }
+    }
+}
diff --git a/android/widget/ListAdapter.java b/android/widget/ListAdapter.java
new file mode 100644
index 0000000..d8fd1c9
--- /dev/null
+++ b/android/widget/ListAdapter.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+/**
+ * Extended {@link Adapter} that is the bridge between a {@link ListView}
+ * and the data that backs the list. Frequently that data comes from a Cursor,
+ * but that is not
+ * required. The ListView can display any data provided that it is wrapped in a
+ * ListAdapter.
+ */
+public interface ListAdapter extends Adapter {
+
+    /**
+     * Indicates whether all the items in this adapter are enabled. If the
+     * value returned by this method changes over time, there is no guarantee
+     * it will take effect.  If true, it means all items are selectable and
+     * clickable (there is no separator.)
+     * 
+     * @return True if all items are enabled, false otherwise.
+     * 
+     * @see #isEnabled(int) 
+     */
+    public boolean areAllItemsEnabled();
+
+    /**
+     * Returns true if the item at the specified position is not a separator.
+     * (A separator is a non-selectable, non-clickable item).
+     * 
+     * The result is unspecified if position is invalid. An {@link ArrayIndexOutOfBoundsException}
+     * should be thrown in that case for fast failure.
+     *
+     * @param position Index of the item
+     * 
+     * @return True if the item is not a separator
+     * 
+     * @see #areAllItemsEnabled() 
+     */
+    boolean isEnabled(int position);
+}
diff --git a/android/widget/ListPopupWindow.java b/android/widget/ListPopupWindow.java
new file mode 100644
index 0000000..0d67615
--- /dev/null
+++ b/android/widget/ListPopupWindow.java
@@ -0,0 +1,1339 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.AttrRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Handler;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.WindowManager;
+import android.widget.AdapterView.OnItemSelectedListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * A ListPopupWindow anchors itself to a host view and displays a
+ * list of choices.
+ *
+ * <p>ListPopupWindow contains a number of tricky behaviors surrounding
+ * positioning, scrolling parents to fit the dropdown, interacting
+ * sanely with the IME if present, and others.
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class ListPopupWindow implements ShowableListMenu {
+    private static final String TAG = "ListPopupWindow";
+    private static final boolean DEBUG = false;
+
+    /**
+     * This value controls the length of time that the user
+     * must leave a pointer down without scrolling to expand
+     * the autocomplete dropdown list to cover the IME.
+     */
+    private static final int EXPAND_LIST_TIMEOUT = 250;
+
+    private Context mContext;
+    private ListAdapter mAdapter;
+    private DropDownListView mDropDownList;
+
+    private int mDropDownHeight = ViewGroup.LayoutParams.WRAP_CONTENT;
+    private int mDropDownWidth = ViewGroup.LayoutParams.WRAP_CONTENT;
+    private int mDropDownHorizontalOffset;
+    private int mDropDownVerticalOffset;
+    private int mDropDownWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL;
+    private boolean mDropDownVerticalOffsetSet;
+    private boolean mIsAnimatedFromAnchor = true;
+    private boolean mOverlapAnchor;
+    private boolean mOverlapAnchorSet;
+
+    private int mDropDownGravity = Gravity.NO_GRAVITY;
+
+    private boolean mDropDownAlwaysVisible = false;
+    private boolean mForceIgnoreOutsideTouch = false;
+    int mListItemExpandMaximum = Integer.MAX_VALUE;
+
+    private View mPromptView;
+    private int mPromptPosition = POSITION_PROMPT_ABOVE;
+
+    private DataSetObserver mObserver;
+
+    private View mDropDownAnchorView;
+
+    private Drawable mDropDownListHighlight;
+
+    private AdapterView.OnItemClickListener mItemClickListener;
+    private AdapterView.OnItemSelectedListener mItemSelectedListener;
+
+    private final ResizePopupRunnable mResizePopupRunnable = new ResizePopupRunnable();
+    private final PopupTouchInterceptor mTouchInterceptor = new PopupTouchInterceptor();
+    private final PopupScrollListener mScrollListener = new PopupScrollListener();
+    private final ListSelectorHider mHideSelector = new ListSelectorHider();
+    private Runnable mShowDropDownRunnable;
+
+    private final Handler mHandler;
+
+    private final Rect mTempRect = new Rect();
+
+    /**
+     * Optional anchor-relative bounds to be used as the transition epicenter.
+     * When {@code null}, the anchor bounds are used as the epicenter.
+     */
+    private Rect mEpicenterBounds;
+
+    private boolean mModal;
+
+    PopupWindow mPopup;
+
+    /**
+     * The provided prompt view should appear above list content.
+     *
+     * @see #setPromptPosition(int)
+     * @see #getPromptPosition()
+     * @see #setPromptView(View)
+     */
+    public static final int POSITION_PROMPT_ABOVE = 0;
+
+    /**
+     * The provided prompt view should appear below list content.
+     *
+     * @see #setPromptPosition(int)
+     * @see #getPromptPosition()
+     * @see #setPromptView(View)
+     */
+    public static final int POSITION_PROMPT_BELOW = 1;
+
+    /**
+     * Alias for {@link ViewGroup.LayoutParams#MATCH_PARENT}.
+     * If used to specify a popup width, the popup will match the width of the anchor view.
+     * If used to specify a popup height, the popup will fill available space.
+     */
+    public static final int MATCH_PARENT = ViewGroup.LayoutParams.MATCH_PARENT;
+
+    /**
+     * Alias for {@link ViewGroup.LayoutParams#WRAP_CONTENT}.
+     * If used to specify a popup width, the popup will use the width of its content.
+     */
+    public static final int WRAP_CONTENT = ViewGroup.LayoutParams.WRAP_CONTENT;
+
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: the requirements for the
+     * input method should be based on the focusability of the popup.  That is
+     * if it is focusable than it needs to work with the input method, else
+     * it doesn't.
+     */
+    public static final int INPUT_METHOD_FROM_FOCUSABLE = PopupWindow.INPUT_METHOD_FROM_FOCUSABLE;
+
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
+     * work with an input method, regardless of whether it is focusable.  This
+     * means that it will always be displayed so that the user can also operate
+     * the input method while it is shown.
+     */
+    public static final int INPUT_METHOD_NEEDED = PopupWindow.INPUT_METHOD_NEEDED;
+
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
+     * work with an input method, regardless of whether it is focusable.  This
+     * means that it will always be displayed to use as much space on the
+     * screen as needed, regardless of whether this covers the input method.
+     */
+    public static final int INPUT_METHOD_NOT_NEEDED = PopupWindow.INPUT_METHOD_NOT_NEEDED;
+
+    /**
+     * Create a new, empty popup window capable of displaying items from a ListAdapter.
+     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+     *
+     * @param context Context used for contained views.
+     */
+    public ListPopupWindow(@NonNull Context context) {
+        this(context, null, com.android.internal.R.attr.listPopupWindowStyle, 0);
+    }
+
+    /**
+     * Create a new, empty popup window capable of displaying items from a ListAdapter.
+     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+     *
+     * @param context Context used for contained views.
+     * @param attrs Attributes from inflating parent views used to style the popup.
+     */
+    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.listPopupWindowStyle, 0);
+    }
+
+    /**
+     * Create a new, empty popup window capable of displaying items from a ListAdapter.
+     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+     *
+     * @param context Context used for contained views.
+     * @param attrs Attributes from inflating parent views used to style the popup.
+     * @param defStyleAttr Default style attribute to use for popup content.
+     */
+    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * Create a new, empty popup window capable of displaying items from a ListAdapter.
+     * Backgrounds should be set using {@link #setBackgroundDrawable(Drawable)}.
+     *
+     * @param context Context used for contained views.
+     * @param attrs Attributes from inflating parent views used to style the popup.
+     * @param defStyleAttr Style attribute to read for default styling of popup content.
+     * @param defStyleRes Style resource ID to use for default styling of popup content.
+     */
+    public ListPopupWindow(@NonNull Context context, @Nullable AttributeSet attrs,
+            @AttrRes int defStyleAttr, @StyleRes int defStyleRes) {
+        mContext = context;
+        mHandler = new Handler(context.getMainLooper());
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.ListPopupWindow,
+                defStyleAttr, defStyleRes);
+        mDropDownHorizontalOffset = a.getDimensionPixelOffset(
+                R.styleable.ListPopupWindow_dropDownHorizontalOffset, 0);
+        mDropDownVerticalOffset = a.getDimensionPixelOffset(
+                R.styleable.ListPopupWindow_dropDownVerticalOffset, 0);
+        if (mDropDownVerticalOffset != 0) {
+            mDropDownVerticalOffsetSet = true;
+        }
+        a.recycle();
+
+        mPopup = new PopupWindow(context, attrs, defStyleAttr, defStyleRes);
+        mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+    }
+
+    /**
+     * Sets the adapter that provides the data and the views to represent the data
+     * in this popup window.
+     *
+     * @param adapter The adapter to use to create this window's content.
+     */
+    public void setAdapter(@Nullable ListAdapter adapter) {
+        if (mObserver == null) {
+            mObserver = new PopupDataSetObserver();
+        } else if (mAdapter != null) {
+            mAdapter.unregisterDataSetObserver(mObserver);
+        }
+        mAdapter = adapter;
+        if (mAdapter != null) {
+            adapter.registerDataSetObserver(mObserver);
+        }
+
+        if (mDropDownList != null) {
+            mDropDownList.setAdapter(mAdapter);
+        }
+    }
+
+    /**
+     * Set where the optional prompt view should appear. The default is
+     * {@link #POSITION_PROMPT_ABOVE}.
+     *
+     * @param position A position constant declaring where the prompt should be displayed.
+     *
+     * @see #POSITION_PROMPT_ABOVE
+     * @see #POSITION_PROMPT_BELOW
+     */
+    public void setPromptPosition(int position) {
+        mPromptPosition = position;
+    }
+
+    /**
+     * @return Where the optional prompt view should appear.
+     *
+     * @see #POSITION_PROMPT_ABOVE
+     * @see #POSITION_PROMPT_BELOW
+     */
+    public int getPromptPosition() {
+        return mPromptPosition;
+    }
+
+    /**
+     * Set whether this window should be modal when shown.
+     *
+     * <p>If a popup window is modal, it will receive all touch and key input.
+     * If the user touches outside the popup window's content area the popup window
+     * will be dismissed.
+     *
+     * @param modal {@code true} if the popup window should be modal, {@code false} otherwise.
+     */
+    public void setModal(boolean modal) {
+        mModal = modal;
+        mPopup.setFocusable(modal);
+    }
+
+    /**
+     * Returns whether the popup window will be modal when shown.
+     *
+     * @return {@code true} if the popup window will be modal, {@code false} otherwise.
+     */
+    public boolean isModal() {
+        return mModal;
+    }
+
+    /**
+     * Forces outside touches to be ignored. Normally if {@link #isDropDownAlwaysVisible()} is
+     * false, we allow outside touch to dismiss the dropdown. If this is set to true, then we
+     * ignore outside touch even when the drop down is not set to always visible.
+     *
+     * @hide Used only by AutoCompleteTextView to handle some internal special cases.
+     */
+    public void setForceIgnoreOutsideTouch(boolean forceIgnoreOutsideTouch) {
+        mForceIgnoreOutsideTouch = forceIgnoreOutsideTouch;
+    }
+
+    /**
+     * Sets whether the drop-down should remain visible under certain conditions.
+     *
+     * The drop-down will occupy the entire screen below {@link #getAnchorView} regardless
+     * of the size or content of the list.  {@link #getBackground()} will fill any space
+     * that is not used by the list.
+     *
+     * @param dropDownAlwaysVisible Whether to keep the drop-down visible.
+     *
+     * @hide Only used by AutoCompleteTextView under special conditions.
+     */
+    public void setDropDownAlwaysVisible(boolean dropDownAlwaysVisible) {
+        mDropDownAlwaysVisible = dropDownAlwaysVisible;
+    }
+
+    /**
+     * @return Whether the drop-down is visible under special conditions.
+     *
+     * @hide Only used by AutoCompleteTextView under special conditions.
+     */
+    public boolean isDropDownAlwaysVisible() {
+        return mDropDownAlwaysVisible;
+    }
+
+    /**
+     * Sets the operating mode for the soft input area.
+     *
+     * @param mode The desired mode, see
+     *        {@link android.view.WindowManager.LayoutParams#softInputMode}
+     *        for the full list
+     *
+     * @see android.view.WindowManager.LayoutParams#softInputMode
+     * @see #getSoftInputMode()
+     */
+    public void setSoftInputMode(int mode) {
+        mPopup.setSoftInputMode(mode);
+    }
+
+    /**
+     * Returns the current value in {@link #setSoftInputMode(int)}.
+     *
+     * @see #setSoftInputMode(int)
+     * @see android.view.WindowManager.LayoutParams#softInputMode
+     */
+    public int getSoftInputMode() {
+        return mPopup.getSoftInputMode();
+    }
+
+    /**
+     * Sets a drawable to use as the list item selector.
+     *
+     * @param selector List selector drawable to use in the popup.
+     */
+    public void setListSelector(Drawable selector) {
+        mDropDownListHighlight = selector;
+    }
+
+    /**
+     * @return The background drawable for the popup window.
+     */
+    public @Nullable Drawable getBackground() {
+        return mPopup.getBackground();
+    }
+
+    /**
+     * Sets a drawable to be the background for the popup window.
+     *
+     * @param d A drawable to set as the background.
+     */
+    public void setBackgroundDrawable(@Nullable Drawable d) {
+        mPopup.setBackgroundDrawable(d);
+    }
+
+    /**
+     * Set an animation style to use when the popup window is shown or dismissed.
+     *
+     * @param animationStyle Animation style to use.
+     */
+    public void setAnimationStyle(@StyleRes int animationStyle) {
+        mPopup.setAnimationStyle(animationStyle);
+    }
+
+    /**
+     * Returns the animation style that will be used when the popup window is
+     * shown or dismissed.
+     *
+     * @return Animation style that will be used.
+     */
+    public @StyleRes int getAnimationStyle() {
+        return mPopup.getAnimationStyle();
+    }
+
+    /**
+     * Returns the view that will be used to anchor this popup.
+     *
+     * @return The popup's anchor view
+     */
+    public @Nullable View getAnchorView() {
+        return mDropDownAnchorView;
+    }
+
+    /**
+     * Sets the popup's anchor view. This popup will always be positioned relative to
+     * the anchor view when shown.
+     *
+     * @param anchor The view to use as an anchor.
+     */
+    public void setAnchorView(@Nullable View anchor) {
+        mDropDownAnchorView = anchor;
+    }
+
+    /**
+     * @return The horizontal offset of the popup from its anchor in pixels.
+     */
+    public int getHorizontalOffset() {
+        return mDropDownHorizontalOffset;
+    }
+
+    /**
+     * Set the horizontal offset of this popup from its anchor view in pixels.
+     *
+     * @param offset The horizontal offset of the popup from its anchor.
+     */
+    public void setHorizontalOffset(int offset) {
+        mDropDownHorizontalOffset = offset;
+    }
+
+    /**
+     * @return The vertical offset of the popup from its anchor in pixels.
+     */
+    public int getVerticalOffset() {
+        if (!mDropDownVerticalOffsetSet) {
+            return 0;
+        }
+        return mDropDownVerticalOffset;
+    }
+
+    /**
+     * Set the vertical offset of this popup from its anchor view in pixels.
+     *
+     * @param offset The vertical offset of the popup from its anchor.
+     */
+    public void setVerticalOffset(int offset) {
+        mDropDownVerticalOffset = offset;
+        mDropDownVerticalOffsetSet = true;
+    }
+
+    /**
+     * Specifies the anchor-relative bounds of the popup's transition
+     * epicenter.
+     *
+     * @param bounds anchor-relative bounds
+     * @hide
+     */
+    public void setEpicenterBounds(Rect bounds) {
+        mEpicenterBounds = bounds;
+    }
+
+    /**
+     * Set the gravity of the dropdown list. This is commonly used to
+     * set gravity to START or END for alignment with the anchor.
+     *
+     * @param gravity Gravity value to use
+     */
+    public void setDropDownGravity(int gravity) {
+        mDropDownGravity = gravity;
+    }
+
+    /**
+     * @return The width of the popup window in pixels.
+     */
+    public int getWidth() {
+        return mDropDownWidth;
+    }
+
+    /**
+     * Sets the width of the popup window in pixels. Can also be {@link #MATCH_PARENT}
+     * or {@link #WRAP_CONTENT}.
+     *
+     * @param width Width of the popup window.
+     */
+    public void setWidth(int width) {
+        mDropDownWidth = width;
+    }
+
+    /**
+     * Sets the width of the popup window by the size of its content. The final width may be
+     * larger to accommodate styled window dressing.
+     *
+     * @param width Desired width of content in pixels.
+     */
+    public void setContentWidth(int width) {
+        Drawable popupBackground = mPopup.getBackground();
+        if (popupBackground != null) {
+            popupBackground.getPadding(mTempRect);
+            mDropDownWidth = mTempRect.left + mTempRect.right + width;
+        } else {
+            setWidth(width);
+        }
+    }
+
+    /**
+     * @return The height of the popup window in pixels.
+     */
+    public int getHeight() {
+        return mDropDownHeight;
+    }
+
+    /**
+     * Sets the height of the popup window in pixels. Can also be {@link #MATCH_PARENT}.
+     *
+     * @param height Height of the popup window must be a positive value,
+     *               {@link #MATCH_PARENT}, or {@link #WRAP_CONTENT}.
+     *
+     * @throws IllegalArgumentException if height is set to negative value
+     */
+    public void setHeight(int height) {
+        if (height < 0 && ViewGroup.LayoutParams.WRAP_CONTENT != height
+                && ViewGroup.LayoutParams.MATCH_PARENT != height) {
+            if (mContext.getApplicationInfo().targetSdkVersion
+                    < Build.VERSION_CODES.O) {
+                Log.e(TAG, "Negative value " + height + " passed to ListPopupWindow#setHeight"
+                        + " produces undefined results");
+            } else {
+                throw new IllegalArgumentException(
+                        "Invalid height. Must be a positive value, MATCH_PARENT, or WRAP_CONTENT.");
+            }
+        }
+        mDropDownHeight = height;
+    }
+
+    /**
+     * Set the layout type for this popup window.
+     * <p>
+     * See {@link WindowManager.LayoutParams#type} for possible values.
+     *
+     * @param layoutType Layout type for this window.
+     *
+     * @see WindowManager.LayoutParams#type
+     */
+    public void setWindowLayoutType(int layoutType) {
+        mDropDownWindowLayoutType = layoutType;
+    }
+
+    /**
+     * Sets a listener to receive events when a list item is clicked.
+     *
+     * @param clickListener Listener to register
+     *
+     * @see ListView#setOnItemClickListener(android.widget.AdapterView.OnItemClickListener)
+     */
+    public void setOnItemClickListener(@Nullable AdapterView.OnItemClickListener clickListener) {
+        mItemClickListener = clickListener;
+    }
+
+    /**
+     * Sets a listener to receive events when a list item is selected.
+     *
+     * @param selectedListener Listener to register.
+     *
+     * @see ListView#setOnItemSelectedListener(OnItemSelectedListener)
+     */
+    public void setOnItemSelectedListener(@Nullable OnItemSelectedListener selectedListener) {
+        mItemSelectedListener = selectedListener;
+    }
+
+    /**
+     * Set a view to act as a user prompt for this popup window. Where the prompt view will appear
+     * is controlled by {@link #setPromptPosition(int)}.
+     *
+     * @param prompt View to use as an informational prompt.
+     */
+    public void setPromptView(@Nullable View prompt) {
+        boolean showing = isShowing();
+        if (showing) {
+            removePromptView();
+        }
+        mPromptView = prompt;
+        if (showing) {
+            show();
+        }
+    }
+
+    /**
+     * Post a {@link #show()} call to the UI thread.
+     */
+    public void postShow() {
+        mHandler.post(mShowDropDownRunnable);
+    }
+
+    /**
+     * Show the popup list. If the list is already showing, this method
+     * will recalculate the popup's size and position.
+     */
+    @Override
+    public void show() {
+        int height = buildDropDown();
+
+        final boolean noInputMethod = isInputMethodNotNeeded();
+        mPopup.setAllowScrollingAnchorParent(!noInputMethod);
+        mPopup.setWindowLayoutType(mDropDownWindowLayoutType);
+
+        if (mPopup.isShowing()) {
+            if (!getAnchorView().isAttachedToWindow()) {
+                //Don't update position if the anchor view is detached from window.
+                return;
+            }
+            final int widthSpec;
+            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+                // The call to PopupWindow's update method below can accept -1 for any
+                // value you do not want to update.
+                widthSpec = -1;
+            } else if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+                widthSpec = getAnchorView().getWidth();
+            } else {
+                widthSpec = mDropDownWidth;
+            }
+
+            final int heightSpec;
+            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+                // The call to PopupWindow's update method below can accept -1 for any
+                // value you do not want to update.
+                heightSpec = noInputMethod ? height : ViewGroup.LayoutParams.MATCH_PARENT;
+                if (noInputMethod) {
+                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+                            ViewGroup.LayoutParams.MATCH_PARENT : 0);
+                    mPopup.setHeight(0);
+                } else {
+                    mPopup.setWidth(mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT ?
+                                    ViewGroup.LayoutParams.MATCH_PARENT : 0);
+                    mPopup.setHeight(ViewGroup.LayoutParams.MATCH_PARENT);
+                }
+            } else if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+                heightSpec = height;
+            } else {
+                heightSpec = mDropDownHeight;
+            }
+
+            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+
+            mPopup.update(getAnchorView(), mDropDownHorizontalOffset,
+                            mDropDownVerticalOffset, (widthSpec < 0)? -1 : widthSpec,
+                            (heightSpec < 0)? -1 : heightSpec);
+        } else {
+            final int widthSpec;
+            if (mDropDownWidth == ViewGroup.LayoutParams.MATCH_PARENT) {
+                widthSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+            } else {
+                if (mDropDownWidth == ViewGroup.LayoutParams.WRAP_CONTENT) {
+                    widthSpec = getAnchorView().getWidth();
+                } else {
+                    widthSpec = mDropDownWidth;
+                }
+            }
+
+            final int heightSpec;
+            if (mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+                heightSpec = ViewGroup.LayoutParams.MATCH_PARENT;
+            } else {
+                if (mDropDownHeight == ViewGroup.LayoutParams.WRAP_CONTENT) {
+                    heightSpec = height;
+                } else {
+                    heightSpec = mDropDownHeight;
+                }
+            }
+
+            mPopup.setWidth(widthSpec);
+            mPopup.setHeight(heightSpec);
+            mPopup.setClipToScreenEnabled(true);
+
+            // use outside touchable to dismiss drop down when touching outside of it, so
+            // only set this if the dropdown is not always visible
+            mPopup.setOutsideTouchable(!mForceIgnoreOutsideTouch && !mDropDownAlwaysVisible);
+            mPopup.setTouchInterceptor(mTouchInterceptor);
+            mPopup.setEpicenterBounds(mEpicenterBounds);
+            if (mOverlapAnchorSet) {
+                mPopup.setOverlapAnchor(mOverlapAnchor);
+            }
+            mPopup.showAsDropDown(getAnchorView(), mDropDownHorizontalOffset,
+                    mDropDownVerticalOffset, mDropDownGravity);
+            mDropDownList.setSelection(ListView.INVALID_POSITION);
+
+            if (!mModal || mDropDownList.isInTouchMode()) {
+                clearListSelection();
+            }
+            if (!mModal) {
+                mHandler.post(mHideSelector);
+            }
+        }
+    }
+
+    /**
+     * Dismiss the popup window.
+     */
+    @Override
+    public void dismiss() {
+        mPopup.dismiss();
+        removePromptView();
+        mPopup.setContentView(null);
+        mDropDownList = null;
+        mHandler.removeCallbacks(mResizePopupRunnable);
+    }
+
+    /**
+     * Set a listener to receive a callback when the popup is dismissed.
+     *
+     * @param listener Listener that will be notified when the popup is dismissed.
+     */
+    public void setOnDismissListener(@Nullable PopupWindow.OnDismissListener listener) {
+        mPopup.setOnDismissListener(listener);
+    }
+
+    private void removePromptView() {
+        if (mPromptView != null) {
+            final ViewParent parent = mPromptView.getParent();
+            if (parent instanceof ViewGroup) {
+                final ViewGroup group = (ViewGroup) parent;
+                group.removeView(mPromptView);
+            }
+        }
+    }
+
+    /**
+     * Control how the popup operates with an input method: one of
+     * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+     * or {@link #INPUT_METHOD_NOT_NEEDED}.
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to the {@link #show()}
+     * method.</p>
+     *
+     * @see #getInputMethodMode()
+     * @see #show()
+     */
+    public void setInputMethodMode(int mode) {
+        mPopup.setInputMethodMode(mode);
+    }
+
+    /**
+     * Return the current value in {@link #setInputMethodMode(int)}.
+     *
+     * @see #setInputMethodMode(int)
+     */
+    public int getInputMethodMode() {
+        return mPopup.getInputMethodMode();
+    }
+
+    /**
+     * Set the selected position of the list.
+     * Only valid when {@link #isShowing()} == {@code true}.
+     *
+     * @param position List position to set as selected.
+     */
+    public void setSelection(int position) {
+        DropDownListView list = mDropDownList;
+        if (isShowing() && list != null) {
+            list.setListSelectionHidden(false);
+            list.setSelection(position);
+            if (list.getChoiceMode() != ListView.CHOICE_MODE_NONE) {
+                list.setItemChecked(position, true);
+            }
+        }
+    }
+
+    /**
+     * Clear any current list selection.
+     * Only valid when {@link #isShowing()} == {@code true}.
+     */
+    public void clearListSelection() {
+        final DropDownListView list = mDropDownList;
+        if (list != null) {
+            // WARNING: Please read the comment where mListSelectionHidden is declared
+            list.setListSelectionHidden(true);
+            list.hideSelector();
+            list.requestLayout();
+        }
+    }
+
+    /**
+     * @return {@code true} if the popup is currently showing, {@code false} otherwise.
+     */
+    @Override
+    public boolean isShowing() {
+        return mPopup.isShowing();
+    }
+
+    /**
+     * @return {@code true} if this popup is configured to assume the user does not need
+     * to interact with the IME while it is showing, {@code false} otherwise.
+     */
+    public boolean isInputMethodNotNeeded() {
+        return mPopup.getInputMethodMode() == INPUT_METHOD_NOT_NEEDED;
+    }
+
+    /**
+     * Perform an item click operation on the specified list adapter position.
+     *
+     * @param position Adapter position for performing the click
+     * @return true if the click action could be performed, false if not.
+     *         (e.g. if the popup was not showing, this method would return false.)
+     */
+    public boolean performItemClick(int position) {
+        if (isShowing()) {
+            if (mItemClickListener != null) {
+                final DropDownListView list = mDropDownList;
+                final View child = list.getChildAt(position - list.getFirstVisiblePosition());
+                final ListAdapter adapter = list.getAdapter();
+                mItemClickListener.onItemClick(list, child, position, adapter.getItemId(position));
+            }
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * @return The currently selected item or null if the popup is not showing.
+     */
+    public @Nullable Object getSelectedItem() {
+        if (!isShowing()) {
+            return null;
+        }
+        return mDropDownList.getSelectedItem();
+    }
+
+    /**
+     * @return The position of the currently selected item or {@link ListView#INVALID_POSITION}
+     * if {@link #isShowing()} == {@code false}.
+     *
+     * @see ListView#getSelectedItemPosition()
+     */
+    public int getSelectedItemPosition() {
+        if (!isShowing()) {
+            return ListView.INVALID_POSITION;
+        }
+        return mDropDownList.getSelectedItemPosition();
+    }
+
+    /**
+     * @return The ID of the currently selected item or {@link ListView#INVALID_ROW_ID}
+     * if {@link #isShowing()} == {@code false}.
+     *
+     * @see ListView#getSelectedItemId()
+     */
+    public long getSelectedItemId() {
+        if (!isShowing()) {
+            return ListView.INVALID_ROW_ID;
+        }
+        return mDropDownList.getSelectedItemId();
+    }
+
+    /**
+     * @return The View for the currently selected item or null if
+     * {@link #isShowing()} == {@code false}.
+     *
+     * @see ListView#getSelectedView()
+     */
+    public @Nullable View getSelectedView() {
+        if (!isShowing()) {
+            return null;
+        }
+        return mDropDownList.getSelectedView();
+    }
+
+    /**
+     * @return The {@link ListView} displayed within the popup window.
+     * Only valid when {@link #isShowing()} == {@code true}.
+     */
+    @Override
+    public @Nullable ListView getListView() {
+        return mDropDownList;
+    }
+
+    @NonNull DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
+        return new DropDownListView(context, hijackFocus);
+    }
+
+    /**
+     * The maximum number of list items that can be visible and still have
+     * the list expand when touched.
+     *
+     * @param max Max number of items that can be visible and still allow the list to expand.
+     */
+    void setListItemExpandMax(int max) {
+        mListItemExpandMaximum = max;
+    }
+
+    /**
+     * Filter key down events. By forwarding key down events to this function,
+     * views using non-modal ListPopupWindow can have it handle key selection of items.
+     *
+     * @param keyCode keyCode param passed to the host view's onKeyDown
+     * @param event event param passed to the host view's onKeyDown
+     * @return true if the event was handled, false if it was ignored.
+     *
+     * @see #setModal(boolean)
+     * @see #onKeyUp(int, KeyEvent)
+     */
+    public boolean onKeyDown(int keyCode, @NonNull KeyEvent event) {
+        // when the drop down is shown, we drive it directly
+        if (isShowing()) {
+            // the key events are forwarded to the list in the drop down view
+            // note that ListView handles space but we don't want that to happen
+            // also if selection is not currently in the drop down, then don't
+            // let center or enter presses go there since that would cause it
+            // to select one of its items
+            if (keyCode != KeyEvent.KEYCODE_SPACE
+                    && (mDropDownList.getSelectedItemPosition() >= 0
+                            || !KeyEvent.isConfirmKey(keyCode))) {
+                int curIndex = mDropDownList.getSelectedItemPosition();
+                boolean consumed;
+
+                final boolean below = !mPopup.isAboveAnchor();
+
+                final ListAdapter adapter = mAdapter;
+
+                boolean allEnabled;
+                int firstItem = Integer.MAX_VALUE;
+                int lastItem = Integer.MIN_VALUE;
+
+                if (adapter != null) {
+                    allEnabled = adapter.areAllItemsEnabled();
+                    firstItem = allEnabled ? 0 :
+                            mDropDownList.lookForSelectablePosition(0, true);
+                    lastItem = allEnabled ? adapter.getCount() - 1 :
+                            mDropDownList.lookForSelectablePosition(adapter.getCount() - 1, false);
+                }
+
+                if ((below && keyCode == KeyEvent.KEYCODE_DPAD_UP && curIndex <= firstItem) ||
+                        (!below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN && curIndex >= lastItem)) {
+                    // When the selection is at the top, we block the key
+                    // event to prevent focus from moving.
+                    clearListSelection();
+                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
+                    show();
+                    return true;
+                } else {
+                    // WARNING: Please read the comment where mListSelectionHidden
+                    //          is declared
+                    mDropDownList.setListSelectionHidden(false);
+                }
+
+                consumed = mDropDownList.onKeyDown(keyCode, event);
+                if (DEBUG) Log.v(TAG, "Key down: code=" + keyCode + " list consumed=" + consumed);
+
+                if (consumed) {
+                    // If it handled the key event, then the user is
+                    // navigating in the list, so we should put it in front.
+                    mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+                    // Here's a little trick we need to do to make sure that
+                    // the list view is actually showing its focus indicator,
+                    // by ensuring it has focus and getting its window out
+                    // of touch mode.
+                    mDropDownList.requestFocusFromTouch();
+                    show();
+
+                    switch (keyCode) {
+                        // avoid passing the focus from the text view to the
+                        // next component
+                        case KeyEvent.KEYCODE_ENTER:
+                        case KeyEvent.KEYCODE_DPAD_CENTER:
+                        case KeyEvent.KEYCODE_DPAD_DOWN:
+                        case KeyEvent.KEYCODE_DPAD_UP:
+                            return true;
+                    }
+                } else {
+                    if (below && keyCode == KeyEvent.KEYCODE_DPAD_DOWN) {
+                        // when the selection is at the bottom, we block the
+                        // event to avoid going to the next focusable widget
+                        if (curIndex == lastItem) {
+                            return true;
+                        }
+                    } else if (!below && keyCode == KeyEvent.KEYCODE_DPAD_UP &&
+                            curIndex == firstItem) {
+                        return true;
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Filter key up events. By forwarding key up events to this function,
+     * views using non-modal ListPopupWindow can have it handle key selection of items.
+     *
+     * @param keyCode keyCode param passed to the host view's onKeyUp
+     * @param event event param passed to the host view's onKeyUp
+     * @return true if the event was handled, false if it was ignored.
+     *
+     * @see #setModal(boolean)
+     * @see #onKeyDown(int, KeyEvent)
+     */
+    public boolean onKeyUp(int keyCode, @NonNull KeyEvent event) {
+        if (isShowing() && mDropDownList.getSelectedItemPosition() >= 0) {
+            boolean consumed = mDropDownList.onKeyUp(keyCode, event);
+            if (consumed && KeyEvent.isConfirmKey(keyCode)) {
+                // if the list accepts the key events and the key event was a click, the text view
+                // gets the selected item from the drop down as its content
+                dismiss();
+            }
+            return consumed;
+        }
+        return false;
+    }
+
+    /**
+     * Filter pre-IME key events. By forwarding {@link View#onKeyPreIme(int, KeyEvent)}
+     * events to this function, views using ListPopupWindow can have it dismiss the popup
+     * when the back key is pressed.
+     *
+     * @param keyCode keyCode param passed to the host view's onKeyPreIme
+     * @param event event param passed to the host view's onKeyPreIme
+     * @return true if the event was handled, false if it was ignored.
+     *
+     * @see #setModal(boolean)
+     */
+    public boolean onKeyPreIme(int keyCode, @NonNull KeyEvent event) {
+        if (keyCode == KeyEvent.KEYCODE_BACK && isShowing()) {
+            // special case for the back key, we do not even try to send it
+            // to the drop down list but instead, consume it immediately
+            final View anchorView = mDropDownAnchorView;
+            if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+                if (state != null) {
+                    state.startTracking(event, this);
+                }
+                return true;
+            } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                KeyEvent.DispatcherState state = anchorView.getKeyDispatcherState();
+                if (state != null) {
+                    state.handleUpEvent(event);
+                }
+                if (event.isTracking() && !event.isCanceled()) {
+                    dismiss();
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Returns an {@link OnTouchListener} that can be added to the source view
+     * to implement drag-to-open behavior. Generally, the source view should be
+     * the same view that was passed to {@link #setAnchorView}.
+     * <p>
+     * When the listener is set on a view, touching that view and dragging
+     * outside of its bounds will open the popup window. Lifting will select the
+     * currently touched list item.
+     * <p>
+     * Example usage:
+     * <pre>
+     * ListPopupWindow myPopup = new ListPopupWindow(context);
+     * myPopup.setAnchor(myAnchor);
+     * OnTouchListener dragListener = myPopup.createDragToOpenListener(myAnchor);
+     * myAnchor.setOnTouchListener(dragListener);
+     * </pre>
+     *
+     * @param src the view on which the resulting listener will be set
+     * @return a touch listener that controls drag-to-open behavior
+     */
+    public OnTouchListener createDragToOpenListener(View src) {
+        return new ForwardingListener(src) {
+            @Override
+            public ShowableListMenu getPopup() {
+                return ListPopupWindow.this;
+            }
+        };
+    }
+
+    /**
+     * <p>Builds the popup window's content and returns the height the popup
+     * should have. Returns -1 when the content already exists.</p>
+     *
+     * @return the content's height or -1 if content already exists
+     */
+    private int buildDropDown() {
+        ViewGroup dropDownView;
+        int otherHeights = 0;
+
+        if (mDropDownList == null) {
+            Context context = mContext;
+
+            /**
+             * This Runnable exists for the sole purpose of checking if the view layout has got
+             * completed and if so call showDropDown to display the drop down. This is used to show
+             * the drop down as soon as possible after user opens up the search dialog, without
+             * waiting for the normal UI pipeline to do it's job which is slower than this method.
+             */
+            mShowDropDownRunnable = new Runnable() {
+                public void run() {
+                    // View layout should be all done before displaying the drop down.
+                    View view = getAnchorView();
+                    if (view != null && view.getWindowToken() != null) {
+                        show();
+                    }
+                }
+            };
+
+            mDropDownList = createDropDownListView(context, !mModal);
+            if (mDropDownListHighlight != null) {
+                mDropDownList.setSelector(mDropDownListHighlight);
+            }
+            mDropDownList.setAdapter(mAdapter);
+            mDropDownList.setOnItemClickListener(mItemClickListener);
+            mDropDownList.setFocusable(true);
+            mDropDownList.setFocusableInTouchMode(true);
+            mDropDownList.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+                public void onItemSelected(AdapterView<?> parent, View view,
+                        int position, long id) {
+
+                    if (position != -1) {
+                        DropDownListView dropDownList = mDropDownList;
+
+                        if (dropDownList != null) {
+                            dropDownList.setListSelectionHidden(false);
+                        }
+                    }
+                }
+
+                public void onNothingSelected(AdapterView<?> parent) {
+                }
+            });
+            mDropDownList.setOnScrollListener(mScrollListener);
+
+            if (mItemSelectedListener != null) {
+                mDropDownList.setOnItemSelectedListener(mItemSelectedListener);
+            }
+
+            dropDownView = mDropDownList;
+
+            View hintView = mPromptView;
+            if (hintView != null) {
+                // if a hint has been specified, we accomodate more space for it and
+                // add a text view in the drop down menu, at the bottom of the list
+                LinearLayout hintContainer = new LinearLayout(context);
+                hintContainer.setOrientation(LinearLayout.VERTICAL);
+
+                LinearLayout.LayoutParams hintParams = new LinearLayout.LayoutParams(
+                        ViewGroup.LayoutParams.MATCH_PARENT, 0, 1.0f
+                );
+
+                switch (mPromptPosition) {
+                case POSITION_PROMPT_BELOW:
+                    hintContainer.addView(dropDownView, hintParams);
+                    hintContainer.addView(hintView);
+                    break;
+
+                case POSITION_PROMPT_ABOVE:
+                    hintContainer.addView(hintView);
+                    hintContainer.addView(dropDownView, hintParams);
+                    break;
+
+                default:
+                    Log.e(TAG, "Invalid hint position " + mPromptPosition);
+                    break;
+                }
+
+                // Measure the hint's height to find how much more vertical
+                // space we need to add to the drop down's height.
+                final int widthSize;
+                final int widthMode;
+                if (mDropDownWidth >= 0) {
+                    widthMode = MeasureSpec.AT_MOST;
+                    widthSize = mDropDownWidth;
+                } else {
+                    widthMode = MeasureSpec.UNSPECIFIED;
+                    widthSize = 0;
+                }
+                final int widthSpec = MeasureSpec.makeMeasureSpec(widthSize, widthMode);
+                final int heightSpec = MeasureSpec.UNSPECIFIED;
+                hintView.measure(widthSpec, heightSpec);
+
+                hintParams = (LinearLayout.LayoutParams) hintView.getLayoutParams();
+                otherHeights = hintView.getMeasuredHeight() + hintParams.topMargin
+                        + hintParams.bottomMargin;
+
+                dropDownView = hintContainer;
+            }
+
+            mPopup.setContentView(dropDownView);
+        } else {
+            final View view = mPromptView;
+            if (view != null) {
+                LinearLayout.LayoutParams hintParams =
+                        (LinearLayout.LayoutParams) view.getLayoutParams();
+                otherHeights = view.getMeasuredHeight() + hintParams.topMargin
+                        + hintParams.bottomMargin;
+            }
+        }
+
+        // getMaxAvailableHeight() subtracts the padding, so we put it back
+        // to get the available height for the whole window.
+        final int padding;
+        final Drawable background = mPopup.getBackground();
+        if (background != null) {
+            background.getPadding(mTempRect);
+            padding = mTempRect.top + mTempRect.bottom;
+
+            // If we don't have an explicit vertical offset, determine one from
+            // the window background so that content will line up.
+            if (!mDropDownVerticalOffsetSet) {
+                mDropDownVerticalOffset = -mTempRect.top;
+            }
+        } else {
+            mTempRect.setEmpty();
+            padding = 0;
+        }
+
+        // Max height available on the screen for a popup.
+        final boolean ignoreBottomDecorations =
+                mPopup.getInputMethodMode() == PopupWindow.INPUT_METHOD_NOT_NEEDED;
+        final int maxHeight = mPopup.getMaxAvailableHeight(
+                getAnchorView(), mDropDownVerticalOffset, ignoreBottomDecorations);
+        if (mDropDownAlwaysVisible || mDropDownHeight == ViewGroup.LayoutParams.MATCH_PARENT) {
+            return maxHeight + padding;
+        }
+
+        final int childWidthSpec;
+        switch (mDropDownWidth) {
+            case ViewGroup.LayoutParams.WRAP_CONTENT:
+                childWidthSpec = MeasureSpec.makeMeasureSpec(
+                        mContext.getResources().getDisplayMetrics().widthPixels
+                                - (mTempRect.left + mTempRect.right),
+                        MeasureSpec.AT_MOST);
+                break;
+            case ViewGroup.LayoutParams.MATCH_PARENT:
+                childWidthSpec = MeasureSpec.makeMeasureSpec(
+                        mContext.getResources().getDisplayMetrics().widthPixels
+                                - (mTempRect.left + mTempRect.right),
+                        MeasureSpec.EXACTLY);
+                break;
+            default:
+                childWidthSpec = MeasureSpec.makeMeasureSpec(mDropDownWidth, MeasureSpec.EXACTLY);
+                break;
+        }
+
+        // Add padding only if the list has items in it, that way we don't show
+        // the popup if it is not needed.
+        final int listContent = mDropDownList.measureHeightOfChildren(childWidthSpec,
+                0, DropDownListView.NO_POSITION, maxHeight - otherHeights, -1);
+        if (listContent > 0) {
+            final int listPadding = mDropDownList.getPaddingTop()
+                    + mDropDownList.getPaddingBottom();
+            otherHeights += padding + listPadding;
+        }
+
+        return listContent + otherHeights;
+    }
+
+    /**
+     * @hide
+     */
+    public void setOverlapAnchor(boolean overlap) {
+        mOverlapAnchorSet = true;
+        mOverlapAnchor = overlap;
+    }
+
+    private class PopupDataSetObserver extends DataSetObserver {
+        @Override
+        public void onChanged() {
+            if (isShowing()) {
+                // Resize the popup to fit new content
+                show();
+            }
+        }
+
+        @Override
+        public void onInvalidated() {
+            dismiss();
+        }
+    }
+
+    private class ListSelectorHider implements Runnable {
+        public void run() {
+            clearListSelection();
+        }
+    }
+
+    private class ResizePopupRunnable implements Runnable {
+        public void run() {
+            if (mDropDownList != null && mDropDownList.isAttachedToWindow()
+                    && mDropDownList.getCount() > mDropDownList.getChildCount()
+                    && mDropDownList.getChildCount() <= mListItemExpandMaximum) {
+                mPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+                show();
+            }
+        }
+    }
+
+    private class PopupTouchInterceptor implements OnTouchListener {
+        public boolean onTouch(View v, MotionEvent event) {
+            final int action = event.getAction();
+            final int x = (int) event.getX();
+            final int y = (int) event.getY();
+
+            if (action == MotionEvent.ACTION_DOWN &&
+                    mPopup != null && mPopup.isShowing() &&
+                    (x >= 0 && x < mPopup.getWidth() && y >= 0 && y < mPopup.getHeight())) {
+                mHandler.postDelayed(mResizePopupRunnable, EXPAND_LIST_TIMEOUT);
+            } else if (action == MotionEvent.ACTION_UP) {
+                mHandler.removeCallbacks(mResizePopupRunnable);
+            }
+            return false;
+        }
+    }
+
+    private class PopupScrollListener implements ListView.OnScrollListener {
+        public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount,
+                int totalItemCount) {
+
+        }
+
+        public void onScrollStateChanged(AbsListView view, int scrollState) {
+            if (scrollState == SCROLL_STATE_TOUCH_SCROLL &&
+                    !isInputMethodNotNeeded() && mPopup.getContentView() != null) {
+                mHandler.removeCallbacks(mResizePopupRunnable);
+                mResizePopupRunnable.run();
+            }
+        }
+    }
+}
diff --git a/android/widget/ListView.java b/android/widget/ListView.java
new file mode 100644
index 0000000..fc9e8e7
--- /dev/null
+++ b/android/widget/ListView.java
@@ -0,0 +1,4155 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.IdRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.os.Trace;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.SparseBooleanArray;
+import android.view.FocusFinder;
+import android.view.KeyEvent;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionInfo;
+import android.view.accessibility.AccessibilityNodeInfo.CollectionItemInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import com.google.android.collect.Lists;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.function.Predicate;
+
+/*
+ * Implementation Notes:
+ *
+ * Some terminology:
+ *
+ *     index    - index of the items that are currently visible
+ *     position - index of the items in the cursor
+ */
+
+
+/**
+ * <p>Displays a vertically-scrollable collection of views, where each view is positioned
+ * immediatelybelow the previous view in the list.  For a more modern, flexible, and performant
+ * approach to displaying lists, use {@link android.support.v7.widget.RecyclerView}.</p>
+ *
+ * <p>To display a list, you can include a list view in your layout XML file:</p>
+ *
+ * <pre>&lt;ListView
+ *      android:id="@+id/list_view"
+ *      android:layout_width="match_parent"
+ *      android:layout_height="match_parent" /&gt;</pre>
+ *
+ * <p>A list view is an <a href="{@docRoot}guide/topics/ui/declaring-layout.html#AdapterViews">
+ * adapter view</a> that does not know the details, such as type and contents, of the views it
+ * contains. Instead list view requests views on demand from a {@link ListAdapter} as needed,
+ * such as to display new views as the user scrolls up or down.</p>
+ *
+ * <p>In order to display items in the list, call {@link #setAdapter(ListAdapter adapter)}
+ * to associate an adapter with the list.  For a simple example, see the discussion of filling an
+ * adapter view with text in the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#FillingTheLayout">
+ * Layouts</a> guide.</p>
+ *
+ * <p>To display a more custom view for each item in your dataset, implement a ListAdapter.
+ * For example, extend {@link BaseAdapter} and create and configure the view for each data item in
+ * {@code getView(...)}:</p>
+ *
+ *  <pre>private class MyAdapter extends BaseAdapter {
+ *
+ *      // override other abstract methods here
+ *
+ *      &#64;Override
+ *      public View getView(int position, View convertView, ViewGroup container) {
+ *          if (convertView == null) {
+ *              convertView = getLayoutInflater().inflate(R.layout.list_item, container, false);
+ *          }
+ *
+ *          ((TextView) convertView.findViewById(android.R.id.text1))
+ *                  .setText(getItem(position));
+ *          return convertView;
+ *      }
+ *  }</pre>
+ *
+ * <p class="note">ListView attempts to reuse view objects in order to improve performance and
+ * avoid a lag in response to user scrolls.  To take advantage of this feature, check if the
+ * {@code convertView} provided to {@code getView(...)} is null before creating or inflating a new
+ * view object.  See
+ * <a href="{@docRoot}training/improving-layouts/smooth-scrolling.html">
+ * Making ListView Scrolling Smooth</a> for more ways to ensure a smooth user experience.</p>
+ *
+ * <p>For a more complete example of creating a custom adapter, see the
+ * <a href="{@docRoot}samples/CustomChoiceList/index.html">
+ *     Custom Choice List</a> sample app.</p>
+ *
+ * <p>To specify an action when a user clicks or taps on a single list item, see
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#HandlingUserSelections">
+ *     Handling click events</a>.</p>
+ *
+ * <p>To learn how to populate a list view with a CursorAdapter, see the discussion of filling an
+ * adapter view with text in the
+ * <a href="{@docRoot}guide/topics/ui/declaring-layout.html#FillingTheLayout">
+ * Layouts</a> guide.
+ * See <a href="{@docRoot}guide/topics/ui/layout/listview.html">
+ *     Using a Loader</a>
+ * to learn how to avoid blocking the main thread when using a cursor.</p>
+ *
+ * <p class="note">Note, many examples use {@link android.app.ListActivity ListActivity}
+ * or {@link android.app.ListFragment ListFragment}
+ * to display a list view. Instead, favor the more flexible approach when writing your own app:
+ * use a more generic Activity subclass or Fragment subclass and add a list view to the layout
+ * or view hierarchy directly.  This approach gives you more direct control of the
+ * list view and adapter.</p>
+ *
+ * @attr ref android.R.styleable#ListView_entries
+ * @attr ref android.R.styleable#ListView_divider
+ * @attr ref android.R.styleable#ListView_dividerHeight
+ * @attr ref android.R.styleable#ListView_headerDividersEnabled
+ * @attr ref android.R.styleable#ListView_footerDividersEnabled
+ */
+@RemoteView
+public class ListView extends AbsListView {
+    static final String TAG = "ListView";
+
+    /**
+     * Used to indicate a no preference for a position type.
+     */
+    static final int NO_POSITION = -1;
+
+    /**
+     * When arrow scrolling, ListView will never scroll more than this factor
+     * times the height of the list.
+     */
+    private static final float MAX_SCROLL_FACTOR = 0.33f;
+
+    /**
+     * When arrow scrolling, need a certain amount of pixels to preview next
+     * items.  This is usually the fading edge, but if that is small enough,
+     * we want to make sure we preview at least this many pixels.
+     */
+    private static final int MIN_SCROLL_PREVIEW_PIXELS = 2;
+
+    /**
+     * A class that represents a fixed view in a list, for example a header at the top
+     * or a footer at the bottom.
+     */
+    public class FixedViewInfo {
+        /** The view to add to the list */
+        public View view;
+        /** The data backing the view. This is returned from {@link ListAdapter#getItem(int)}. */
+        public Object data;
+        /** <code>true</code> if the fixed view should be selectable in the list */
+        public boolean isSelectable;
+    }
+
+    ArrayList<FixedViewInfo> mHeaderViewInfos = Lists.newArrayList();
+    ArrayList<FixedViewInfo> mFooterViewInfos = Lists.newArrayList();
+
+    Drawable mDivider;
+    int mDividerHeight;
+
+    Drawable mOverScrollHeader;
+    Drawable mOverScrollFooter;
+
+    private boolean mIsCacheColorOpaque;
+    private boolean mDividerIsOpaque;
+
+    private boolean mHeaderDividersEnabled;
+    private boolean mFooterDividersEnabled;
+
+    private boolean mAreAllItemsSelectable = true;
+
+    private boolean mItemsCanFocus = false;
+
+    // used for temporary calculations.
+    private final Rect mTempRect = new Rect();
+    private Paint mDividerPaint;
+
+    // the single allocated result per list view; kinda cheesey but avoids
+    // allocating these thingies too often.
+    private final ArrowScrollFocusResult mArrowScrollFocusResult = new ArrowScrollFocusResult();
+
+    // Keeps focused children visible through resizes
+    private FocusSelector mFocusSelector;
+
+    public ListView(Context context) {
+        this(context, null);
+    }
+
+    public ListView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.listViewStyle);
+    }
+
+    public ListView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ListView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.ListView, defStyleAttr, defStyleRes);
+
+        final CharSequence[] entries = a.getTextArray(R.styleable.ListView_entries);
+        if (entries != null) {
+            setAdapter(new ArrayAdapter<>(context, R.layout.simple_list_item_1, entries));
+        }
+
+        final Drawable d = a.getDrawable(R.styleable.ListView_divider);
+        if (d != null) {
+            // Use an implicit divider height which may be explicitly
+            // overridden by android:dividerHeight further down.
+            setDivider(d);
+        }
+
+        final Drawable osHeader = a.getDrawable(R.styleable.ListView_overScrollHeader);
+        if (osHeader != null) {
+            setOverscrollHeader(osHeader);
+        }
+
+        final Drawable osFooter = a.getDrawable(R.styleable.ListView_overScrollFooter);
+        if (osFooter != null) {
+            setOverscrollFooter(osFooter);
+        }
+
+        // Use an explicit divider height, if specified.
+        if (a.hasValueOrEmpty(R.styleable.ListView_dividerHeight)) {
+            final int dividerHeight = a.getDimensionPixelSize(
+                    R.styleable.ListView_dividerHeight, 0);
+            if (dividerHeight != 0) {
+                setDividerHeight(dividerHeight);
+            }
+        }
+
+        mHeaderDividersEnabled = a.getBoolean(R.styleable.ListView_headerDividersEnabled, true);
+        mFooterDividersEnabled = a.getBoolean(R.styleable.ListView_footerDividersEnabled, true);
+
+        a.recycle();
+    }
+
+    /**
+     * @return The maximum amount a list view will scroll in response to
+     *   an arrow event.
+     */
+    public int getMaxScrollAmount() {
+        return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
+    }
+
+    /**
+     * Make sure views are touching the top or bottom edge, as appropriate for
+     * our gravity
+     */
+    private void adjustViewsUpOrDown() {
+        final int childCount = getChildCount();
+        int delta;
+
+        if (childCount > 0) {
+            View child;
+
+            if (!mStackFromBottom) {
+                // Uh-oh -- we came up short. Slide all views up to make them
+                // align with the top
+                child = getChildAt(0);
+                delta = child.getTop() - mListPadding.top;
+                if (mFirstPosition != 0) {
+                    // It's OK to have some space above the first item if it is
+                    // part of the vertical spacing
+                    delta -= mDividerHeight;
+                }
+                if (delta < 0) {
+                    // We only are looking to see if we are too low, not too high
+                    delta = 0;
+                }
+            } else {
+                // we are too high, slide all views down to align with bottom
+                child = getChildAt(childCount - 1);
+                delta = child.getBottom() - (getHeight() - mListPadding.bottom);
+
+                if (mFirstPosition + childCount < mItemCount) {
+                    // It's OK to have some space below the last item if it is
+                    // part of the vertical spacing
+                    delta += mDividerHeight;
+                }
+
+                if (delta > 0) {
+                    delta = 0;
+                }
+            }
+
+            if (delta != 0) {
+                offsetChildrenTopAndBottom(-delta);
+            }
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the list. If this method is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * Note: When first introduced, this method could only be called before
+     * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+     * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+     * called at any time. If the ListView's adapter does not extend
+     * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+     * instance of {@link WrapperListAdapter}.
+     *
+     * @param v The view to add.
+     * @param data Data to associate with this view
+     * @param isSelectable whether the item is selectable
+     */
+    public void addHeaderView(View v, Object data, boolean isSelectable) {
+        if (v.getParent() != null && v.getParent() != this) {
+            if (Log.isLoggable(TAG, Log.WARN)) {
+                Log.w(TAG, "The specified child already has a parent. "
+                           + "You must call removeView() on the child's parent first.");
+            }
+        }
+        final FixedViewInfo info = new FixedViewInfo();
+        info.view = v;
+        info.data = data;
+        info.isSelectable = isSelectable;
+        mHeaderViewInfos.add(info);
+        mAreAllItemsSelectable &= isSelectable;
+
+        // Wrap the adapter if it wasn't already wrapped.
+        if (mAdapter != null) {
+            if (!(mAdapter instanceof HeaderViewListAdapter)) {
+                wrapHeaderListAdapterInternal();
+            }
+
+            // In the case of re-adding a header view, or adding one later on,
+            // we need to notify the observer.
+            if (mDataSetObserver != null) {
+                mDataSetObserver.onChanged();
+            }
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the top of the list. If addHeaderView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * Note: When first introduced, this method could only be called before
+     * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+     * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+     * called at any time. If the ListView's adapter does not extend
+     * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+     * instance of {@link WrapperListAdapter}.
+     *
+     * @param v The view to add.
+     */
+    public void addHeaderView(View v) {
+        addHeaderView(v, null, true);
+    }
+
+    @Override
+    public int getHeaderViewsCount() {
+        return mHeaderViewInfos.size();
+    }
+
+    /**
+     * Removes a previously-added header view.
+     *
+     * @param v The view to remove
+     * @return true if the view was removed, false if the view was not a header
+     *         view
+     */
+    public boolean removeHeaderView(View v) {
+        if (mHeaderViewInfos.size() > 0) {
+            boolean result = false;
+            if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeHeader(v)) {
+                if (mDataSetObserver != null) {
+                    mDataSetObserver.onChanged();
+                }
+                result = true;
+            }
+            removeFixedViewInfo(v, mHeaderViewInfos);
+            return result;
+        }
+        return false;
+    }
+
+    private void removeFixedViewInfo(View v, ArrayList<FixedViewInfo> where) {
+        int len = where.size();
+        for (int i = 0; i < len; ++i) {
+            FixedViewInfo info = where.get(i);
+            if (info.view == v) {
+                where.remove(i);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the bottom of the list. If addFooterView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * Note: When first introduced, this method could only be called before
+     * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+     * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+     * called at any time. If the ListView's adapter does not extend
+     * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+     * instance of {@link WrapperListAdapter}.
+     *
+     * @param v The view to add.
+     * @param data Data to associate with this view
+     * @param isSelectable true if the footer view can be selected
+     */
+    public void addFooterView(View v, Object data, boolean isSelectable) {
+        if (v.getParent() != null && v.getParent() != this) {
+            if (Log.isLoggable(TAG, Log.WARN)) {
+                Log.w(TAG, "The specified child already has a parent. "
+                           + "You must call removeView() on the child's parent first.");
+            }
+        }
+
+        final FixedViewInfo info = new FixedViewInfo();
+        info.view = v;
+        info.data = data;
+        info.isSelectable = isSelectable;
+        mFooterViewInfos.add(info);
+        mAreAllItemsSelectable &= isSelectable;
+
+        // Wrap the adapter if it wasn't already wrapped.
+        if (mAdapter != null) {
+            if (!(mAdapter instanceof HeaderViewListAdapter)) {
+                wrapHeaderListAdapterInternal();
+            }
+
+            // In the case of re-adding a footer view, or adding one later on,
+            // we need to notify the observer.
+            if (mDataSetObserver != null) {
+                mDataSetObserver.onChanged();
+            }
+        }
+    }
+
+    /**
+     * Add a fixed view to appear at the bottom of the list. If addFooterView is
+     * called more than once, the views will appear in the order they were
+     * added. Views added using this call can take focus if they want.
+     * <p>
+     * Note: When first introduced, this method could only be called before
+     * setting the adapter with {@link #setAdapter(ListAdapter)}. Starting with
+     * {@link android.os.Build.VERSION_CODES#KITKAT}, this method may be
+     * called at any time. If the ListView's adapter does not extend
+     * {@link HeaderViewListAdapter}, it will be wrapped with a supporting
+     * instance of {@link WrapperListAdapter}.
+     *
+     * @param v The view to add.
+     */
+    public void addFooterView(View v) {
+        addFooterView(v, null, true);
+    }
+
+    @Override
+    public int getFooterViewsCount() {
+        return mFooterViewInfos.size();
+    }
+
+    /**
+     * Removes a previously-added footer view.
+     *
+     * @param v The view to remove
+     * @return
+     * true if the view was removed, false if the view was not a footer view
+     */
+    public boolean removeFooterView(View v) {
+        if (mFooterViewInfos.size() > 0) {
+            boolean result = false;
+            if (mAdapter != null && ((HeaderViewListAdapter) mAdapter).removeFooter(v)) {
+                if (mDataSetObserver != null) {
+                    mDataSetObserver.onChanged();
+                }
+                result = true;
+            }
+            removeFixedViewInfo(v, mFooterViewInfos);
+            return result;
+        }
+        return false;
+    }
+
+    /**
+     * Returns the adapter currently in use in this ListView. The returned adapter
+     * might not be the same adapter passed to {@link #setAdapter(ListAdapter)} but
+     * might be a {@link WrapperListAdapter}.
+     *
+     * @return The adapter currently used to display data in this ListView.
+     *
+     * @see #setAdapter(ListAdapter)
+     */
+    @Override
+    public ListAdapter getAdapter() {
+        return mAdapter;
+    }
+
+    /**
+     * Sets up this AbsListView to use a remote views adapter which connects to a RemoteViewsService
+     * through the specified intent.
+     * @param intent the intent used to identify the RemoteViewsService for the adapter to connect to.
+     */
+    @android.view.RemotableViewMethod(asyncImpl="setRemoteViewsAdapterAsync")
+    public void setRemoteViewsAdapter(Intent intent) {
+        super.setRemoteViewsAdapter(intent);
+    }
+
+    /**
+     * Sets the data behind this ListView.
+     *
+     * The adapter passed to this method may be wrapped by a {@link WrapperListAdapter},
+     * depending on the ListView features currently in use. For instance, adding
+     * headers and/or footers will cause the adapter to be wrapped.
+     *
+     * @param adapter The ListAdapter which is responsible for maintaining the
+     *        data backing this list and for producing a view to represent an
+     *        item in that data set.
+     *
+     * @see #getAdapter()
+     */
+    @Override
+    public void setAdapter(ListAdapter adapter) {
+        if (mAdapter != null && mDataSetObserver != null) {
+            mAdapter.unregisterDataSetObserver(mDataSetObserver);
+        }
+
+        resetList();
+        mRecycler.clear();
+
+        if (mHeaderViewInfos.size() > 0|| mFooterViewInfos.size() > 0) {
+            mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, adapter);
+        } else {
+            mAdapter = adapter;
+        }
+
+        mOldSelectedPosition = INVALID_POSITION;
+        mOldSelectedRowId = INVALID_ROW_ID;
+
+        // AbsListView#setAdapter will update choice mode states.
+        super.setAdapter(adapter);
+
+        if (mAdapter != null) {
+            mAreAllItemsSelectable = mAdapter.areAllItemsEnabled();
+            mOldItemCount = mItemCount;
+            mItemCount = mAdapter.getCount();
+            checkFocus();
+
+            mDataSetObserver = new AdapterDataSetObserver();
+            mAdapter.registerDataSetObserver(mDataSetObserver);
+
+            mRecycler.setViewTypeCount(mAdapter.getViewTypeCount());
+
+            int position;
+            if (mStackFromBottom) {
+                position = lookForSelectablePosition(mItemCount - 1, false);
+            } else {
+                position = lookForSelectablePosition(0, true);
+            }
+            setSelectedPositionInt(position);
+            setNextSelectedPositionInt(position);
+
+            if (mItemCount == 0) {
+                // Nothing selected
+                checkSelectionChanged();
+            }
+        } else {
+            mAreAllItemsSelectable = true;
+            checkFocus();
+            // Nothing selected
+            checkSelectionChanged();
+        }
+
+        requestLayout();
+    }
+
+    /**
+     * The list is empty. Clear everything out.
+     */
+    @Override
+    void resetList() {
+        // The parent's resetList() will remove all views from the layout so we need to
+        // cleanup the state of our footers and headers
+        clearRecycledState(mHeaderViewInfos);
+        clearRecycledState(mFooterViewInfos);
+
+        super.resetList();
+
+        mLayoutMode = LAYOUT_NORMAL;
+    }
+
+    private void clearRecycledState(ArrayList<FixedViewInfo> infos) {
+        if (infos != null) {
+            final int count = infos.size();
+
+            for (int i = 0; i < count; i++) {
+                final View child = infos.get(i).view;
+                final ViewGroup.LayoutParams params = child.getLayoutParams();
+                if (checkLayoutParams(params)) {
+                    ((LayoutParams) params).recycledHeaderFooter = false;
+                }
+            }
+        }
+    }
+
+    /**
+     * @return Whether the list needs to show the top fading edge
+     */
+    private boolean showingTopFadingEdge() {
+        final int listTop = mScrollY + mListPadding.top;
+        return (mFirstPosition > 0) || (getChildAt(0).getTop() > listTop);
+    }
+
+    /**
+     * @return Whether the list needs to show the bottom fading edge
+     */
+    private boolean showingBottomFadingEdge() {
+        final int childCount = getChildCount();
+        final int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
+        final int lastVisiblePosition = mFirstPosition + childCount - 1;
+
+        final int listBottom = mScrollY + getHeight() - mListPadding.bottom;
+
+        return (lastVisiblePosition < mItemCount - 1)
+                         || (bottomOfBottomChild < listBottom);
+    }
+
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rect, boolean immediate) {
+
+        int rectTopWithinChild = rect.top;
+
+        // offset so rect is in coordinates of the this view
+        rect.offset(child.getLeft(), child.getTop());
+        rect.offset(-child.getScrollX(), -child.getScrollY());
+
+        final int height = getHeight();
+        int listUnfadedTop = getScrollY();
+        int listUnfadedBottom = listUnfadedTop + height;
+        final int fadingEdge = getVerticalFadingEdgeLength();
+
+        if (showingTopFadingEdge()) {
+            // leave room for top fading edge as long as rect isn't at very top
+            if ((mSelectedPosition > 0) || (rectTopWithinChild > fadingEdge)) {
+                listUnfadedTop += fadingEdge;
+            }
+        }
+
+        int childCount = getChildCount();
+        int bottomOfBottomChild = getChildAt(childCount - 1).getBottom();
+
+        if (showingBottomFadingEdge()) {
+            // leave room for bottom fading edge as long as rect isn't at very bottom
+            if ((mSelectedPosition < mItemCount - 1)
+                    || (rect.bottom < (bottomOfBottomChild - fadingEdge))) {
+                listUnfadedBottom -= fadingEdge;
+            }
+        }
+
+        int scrollYDelta = 0;
+
+        if (rect.bottom > listUnfadedBottom && rect.top > listUnfadedTop) {
+            // need to MOVE DOWN to get it in view: move down just enough so
+            // that the entire rectangle is in view (or at least the first
+            // screen size chunk).
+
+            if (rect.height() > height) {
+                // just enough to get screen size chunk on
+                scrollYDelta += (rect.top - listUnfadedTop);
+            } else {
+                // get entire rect at bottom of screen
+                scrollYDelta += (rect.bottom - listUnfadedBottom);
+            }
+
+            // make sure we aren't scrolling beyond the end of our children
+            int distanceToBottom = bottomOfBottomChild - listUnfadedBottom;
+            scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+        } else if (rect.top < listUnfadedTop && rect.bottom < listUnfadedBottom) {
+            // need to MOVE UP to get it in view: move up just enough so that
+            // entire rectangle is in view (or at least the first screen
+            // size chunk of it).
+
+            if (rect.height() > height) {
+                // screen size chunk
+                scrollYDelta -= (listUnfadedBottom - rect.bottom);
+            } else {
+                // entire rect at top
+                scrollYDelta -= (listUnfadedTop - rect.top);
+            }
+
+            // make sure we aren't scrolling any further than the top our children
+            int top = getChildAt(0).getTop();
+            int deltaToTop = top - listUnfadedTop;
+            scrollYDelta = Math.max(scrollYDelta, deltaToTop);
+        }
+
+        final boolean scroll = scrollYDelta != 0;
+        if (scroll) {
+            scrollListItemsBy(-scrollYDelta);
+            positionSelector(INVALID_POSITION, child);
+            mSelectedTop = child.getTop();
+            invalidate();
+        }
+        return scroll;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void fillGap(boolean down) {
+        final int count = getChildCount();
+        if (down) {
+            int paddingTop = 0;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                paddingTop = getListPaddingTop();
+            }
+            final int startOffset = count > 0 ? getChildAt(count - 1).getBottom() + mDividerHeight :
+                    paddingTop;
+            fillDown(mFirstPosition + count, startOffset);
+            correctTooHigh(getChildCount());
+        } else {
+            int paddingBottom = 0;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                paddingBottom = getListPaddingBottom();
+            }
+            final int startOffset = count > 0 ? getChildAt(0).getTop() - mDividerHeight :
+                    getHeight() - paddingBottom;
+            fillUp(mFirstPosition - 1, startOffset);
+            correctTooLow(getChildCount());
+        }
+    }
+
+    /**
+     * Fills the list from pos down to the end of the list view.
+     *
+     * @param pos The first position to put in the list
+     *
+     * @param nextTop The location where the top of the item associated with pos
+     *        should be drawn
+     *
+     * @return The view that is currently selected, if it happens to be in the
+     *         range that we draw.
+     */
+    private View fillDown(int pos, int nextTop) {
+        View selectedView = null;
+
+        int end = (mBottom - mTop);
+        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+            end -= mListPadding.bottom;
+        }
+
+        while (nextTop < end && pos < mItemCount) {
+            // is this the selected item?
+            boolean selected = pos == mSelectedPosition;
+            View child = makeAndAddView(pos, nextTop, true, mListPadding.left, selected);
+
+            nextTop = child.getBottom() + mDividerHeight;
+            if (selected) {
+                selectedView = child;
+            }
+            pos++;
+        }
+
+        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+        return selectedView;
+    }
+
+    /**
+     * Fills the list from pos up to the top of the list view.
+     *
+     * @param pos The first position to put in the list
+     *
+     * @param nextBottom The location where the bottom of the item associated
+     *        with pos should be drawn
+     *
+     * @return The view that is currently selected
+     */
+    private View fillUp(int pos, int nextBottom) {
+        View selectedView = null;
+
+        int end = 0;
+        if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+            end = mListPadding.top;
+        }
+
+        while (nextBottom > end && pos >= 0) {
+            // is this the selected item?
+            boolean selected = pos == mSelectedPosition;
+            View child = makeAndAddView(pos, nextBottom, false, mListPadding.left, selected);
+            nextBottom = child.getTop() - mDividerHeight;
+            if (selected) {
+                selectedView = child;
+            }
+            pos--;
+        }
+
+        mFirstPosition = pos + 1;
+        setVisibleRangeHint(mFirstPosition, mFirstPosition + getChildCount() - 1);
+        return selectedView;
+    }
+
+    /**
+     * Fills the list from top to bottom, starting with mFirstPosition
+     *
+     * @param nextTop The location where the top of the first item should be
+     *        drawn
+     *
+     * @return The view that is currently selected
+     */
+    private View fillFromTop(int nextTop) {
+        mFirstPosition = Math.min(mFirstPosition, mSelectedPosition);
+        mFirstPosition = Math.min(mFirstPosition, mItemCount - 1);
+        if (mFirstPosition < 0) {
+            mFirstPosition = 0;
+        }
+        return fillDown(mFirstPosition, nextTop);
+    }
+
+
+    /**
+     * Put mSelectedPosition in the middle of the screen and then build up and
+     * down from there. This method forces mSelectedPosition to the center.
+     *
+     * @param childrenTop Top of the area in which children can be drawn, as
+     *        measured in pixels
+     * @param childrenBottom Bottom of the area in which children can be drawn,
+     *        as measured in pixels
+     * @return Currently selected view
+     */
+    private View fillFromMiddle(int childrenTop, int childrenBottom) {
+        int height = childrenBottom - childrenTop;
+
+        int position = reconcileSelectedPosition();
+
+        View sel = makeAndAddView(position, childrenTop, true,
+                mListPadding.left, true);
+        mFirstPosition = position;
+
+        int selHeight = sel.getMeasuredHeight();
+        if (selHeight <= height) {
+            sel.offsetTopAndBottom((height - selHeight) / 2);
+        }
+
+        fillAboveAndBelow(sel, position);
+
+        if (!mStackFromBottom) {
+            correctTooHigh(getChildCount());
+        } else {
+            correctTooLow(getChildCount());
+        }
+
+        return sel;
+    }
+
+    /**
+     * Once the selected view as been placed, fill up the visible area above and
+     * below it.
+     *
+     * @param sel The selected view
+     * @param position The position corresponding to sel
+     */
+    private void fillAboveAndBelow(View sel, int position) {
+        final int dividerHeight = mDividerHeight;
+        if (!mStackFromBottom) {
+            fillUp(position - 1, sel.getTop() - dividerHeight);
+            adjustViewsUpOrDown();
+            fillDown(position + 1, sel.getBottom() + dividerHeight);
+        } else {
+            fillDown(position + 1, sel.getBottom() + dividerHeight);
+            adjustViewsUpOrDown();
+            fillUp(position - 1, sel.getTop() - dividerHeight);
+        }
+    }
+
+
+    /**
+     * Fills the grid based on positioning the new selection at a specific
+     * location. The selection may be moved so that it does not intersect the
+     * faded edges. The grid is then filled upwards and downwards from there.
+     *
+     * @param selectedTop Where the selected item should be
+     * @param childrenTop Where to start drawing children
+     * @param childrenBottom Last pixel where children can be drawn
+     * @return The view that currently has selection
+     */
+    private View fillFromSelection(int selectedTop, int childrenTop, int childrenBottom) {
+        int fadingEdgeLength = getVerticalFadingEdgeLength();
+        final int selectedPosition = mSelectedPosition;
+
+        View sel;
+
+        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
+                selectedPosition);
+        final int bottomSelectionPixel = getBottomSelectionPixel(childrenBottom, fadingEdgeLength,
+                selectedPosition);
+
+        sel = makeAndAddView(selectedPosition, selectedTop, true, mListPadding.left, true);
+
+
+        // Some of the newly selected item extends below the bottom of the list
+        if (sel.getBottom() > bottomSelectionPixel) {
+            // Find space available above the selection into which we can scroll
+            // upwards
+            final int spaceAbove = sel.getTop() - topSelectionPixel;
+
+            // Find space required to bring the bottom of the selected item
+            // fully into view
+            final int spaceBelow = sel.getBottom() - bottomSelectionPixel;
+            final int offset = Math.min(spaceAbove, spaceBelow);
+
+            // Now offset the selected item to get it into view
+            sel.offsetTopAndBottom(-offset);
+        } else if (sel.getTop() < topSelectionPixel) {
+            // Find space required to bring the top of the selected item fully
+            // into view
+            final int spaceAbove = topSelectionPixel - sel.getTop();
+
+            // Find space available below the selection into which we can scroll
+            // downwards
+            final int spaceBelow = bottomSelectionPixel - sel.getBottom();
+            final int offset = Math.min(spaceAbove, spaceBelow);
+
+            // Offset the selected item to get it into view
+            sel.offsetTopAndBottom(offset);
+        }
+
+        // Fill in views above and below
+        fillAboveAndBelow(sel, selectedPosition);
+
+        if (!mStackFromBottom) {
+            correctTooHigh(getChildCount());
+        } else {
+            correctTooLow(getChildCount());
+        }
+
+        return sel;
+    }
+
+    /**
+     * Calculate the bottom-most pixel we can draw the selection into
+     *
+     * @param childrenBottom Bottom pixel were children can be drawn
+     * @param fadingEdgeLength Length of the fading edge in pixels, if present
+     * @param selectedPosition The position that will be selected
+     * @return The bottom-most pixel we can draw the selection into
+     */
+    private int getBottomSelectionPixel(int childrenBottom, int fadingEdgeLength,
+            int selectedPosition) {
+        int bottomSelectionPixel = childrenBottom;
+        if (selectedPosition != mItemCount - 1) {
+            bottomSelectionPixel -= fadingEdgeLength;
+        }
+        return bottomSelectionPixel;
+    }
+
+    /**
+     * Calculate the top-most pixel we can draw the selection into
+     *
+     * @param childrenTop Top pixel were children can be drawn
+     * @param fadingEdgeLength Length of the fading edge in pixels, if present
+     * @param selectedPosition The position that will be selected
+     * @return The top-most pixel we can draw the selection into
+     */
+    private int getTopSelectionPixel(int childrenTop, int fadingEdgeLength, int selectedPosition) {
+        // first pixel we can draw the selection into
+        int topSelectionPixel = childrenTop;
+        if (selectedPosition > 0) {
+            topSelectionPixel += fadingEdgeLength;
+        }
+        return topSelectionPixel;
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position. The view will
+     * scroll such that the indicated position is displayed.
+     * @param position Scroll to this adapter position.
+     */
+    @android.view.RemotableViewMethod
+    public void smoothScrollToPosition(int position) {
+        super.smoothScrollToPosition(position);
+    }
+
+    /**
+     * Smoothly scroll to the specified adapter position offset. The view will
+     * scroll such that the indicated position is displayed.
+     * @param offset The amount to offset from the adapter position to scroll to.
+     */
+    @android.view.RemotableViewMethod
+    public void smoothScrollByOffset(int offset) {
+        super.smoothScrollByOffset(offset);
+    }
+
+    /**
+     * Fills the list based on positioning the new selection relative to the old
+     * selection. The new selection will be placed at, above, or below the
+     * location of the new selection depending on how the selection is moving.
+     * The selection will then be pinned to the visible part of the screen,
+     * excluding the edges that are faded. The list is then filled upwards and
+     * downwards from there.
+     *
+     * @param oldSel The old selected view. Useful for trying to put the new
+     *        selection in the same place
+     * @param newSel The view that is to become selected. Useful for trying to
+     *        put the new selection in the same place
+     * @param delta Which way we are moving
+     * @param childrenTop Where to start drawing children
+     * @param childrenBottom Last pixel where children can be drawn
+     * @return The view that currently has selection
+     */
+    private View moveSelection(View oldSel, View newSel, int delta, int childrenTop,
+            int childrenBottom) {
+        int fadingEdgeLength = getVerticalFadingEdgeLength();
+        final int selectedPosition = mSelectedPosition;
+
+        View sel;
+
+        final int topSelectionPixel = getTopSelectionPixel(childrenTop, fadingEdgeLength,
+                selectedPosition);
+        final int bottomSelectionPixel = getBottomSelectionPixel(childrenTop, fadingEdgeLength,
+                selectedPosition);
+
+        if (delta > 0) {
+            /*
+             * Case 1: Scrolling down.
+             */
+
+            /*
+             *     Before           After
+             *    |       |        |       |
+             *    +-------+        +-------+
+             *    |   A   |        |   A   |
+             *    |   1   |   =>   +-------+
+             *    +-------+        |   B   |
+             *    |   B   |        |   2   |
+             *    +-------+        +-------+
+             *    |       |        |       |
+             *
+             *    Try to keep the top of the previously selected item where it was.
+             *    oldSel = A
+             *    sel = B
+             */
+
+            // Put oldSel (A) where it belongs
+            oldSel = makeAndAddView(selectedPosition - 1, oldSel.getTop(), true,
+                    mListPadding.left, false);
+
+            final int dividerHeight = mDividerHeight;
+
+            // Now put the new selection (B) below that
+            sel = makeAndAddView(selectedPosition, oldSel.getBottom() + dividerHeight, true,
+                    mListPadding.left, true);
+
+            // Some of the newly selected item extends below the bottom of the list
+            if (sel.getBottom() > bottomSelectionPixel) {
+
+                // Find space available above the selection into which we can scroll upwards
+                int spaceAbove = sel.getTop() - topSelectionPixel;
+
+                // Find space required to bring the bottom of the selected item fully into view
+                int spaceBelow = sel.getBottom() - bottomSelectionPixel;
+
+                // Don't scroll more than half the height of the list
+                int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
+                int offset = Math.min(spaceAbove, spaceBelow);
+                offset = Math.min(offset, halfVerticalSpace);
+
+                // We placed oldSel, so offset that item
+                oldSel.offsetTopAndBottom(-offset);
+                // Now offset the selected item to get it into view
+                sel.offsetTopAndBottom(-offset);
+            }
+
+            // Fill in views above and below
+            if (!mStackFromBottom) {
+                fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
+                adjustViewsUpOrDown();
+                fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
+            } else {
+                fillDown(mSelectedPosition + 1, sel.getBottom() + dividerHeight);
+                adjustViewsUpOrDown();
+                fillUp(mSelectedPosition - 2, sel.getTop() - dividerHeight);
+            }
+        } else if (delta < 0) {
+            /*
+             * Case 2: Scrolling up.
+             */
+
+            /*
+             *     Before           After
+             *    |       |        |       |
+             *    +-------+        +-------+
+             *    |   A   |        |   A   |
+             *    +-------+   =>   |   1   |
+             *    |   B   |        +-------+
+             *    |   2   |        |   B   |
+             *    +-------+        +-------+
+             *    |       |        |       |
+             *
+             *    Try to keep the top of the item about to become selected where it was.
+             *    newSel = A
+             *    olSel = B
+             */
+
+            if (newSel != null) {
+                // Try to position the top of newSel (A) where it was before it was selected
+                sel = makeAndAddView(selectedPosition, newSel.getTop(), true, mListPadding.left,
+                        true);
+            } else {
+                // If (A) was not on screen and so did not have a view, position
+                // it above the oldSel (B)
+                sel = makeAndAddView(selectedPosition, oldSel.getTop(), false, mListPadding.left,
+                        true);
+            }
+
+            // Some of the newly selected item extends above the top of the list
+            if (sel.getTop() < topSelectionPixel) {
+                // Find space required to bring the top of the selected item fully into view
+                int spaceAbove = topSelectionPixel - sel.getTop();
+
+               // Find space available below the selection into which we can scroll downwards
+                int spaceBelow = bottomSelectionPixel - sel.getBottom();
+
+                // Don't scroll more than half the height of the list
+                int halfVerticalSpace = (childrenBottom - childrenTop) / 2;
+                int offset = Math.min(spaceAbove, spaceBelow);
+                offset = Math.min(offset, halfVerticalSpace);
+
+                // Offset the selected item to get it into view
+                sel.offsetTopAndBottom(offset);
+            }
+
+            // Fill in views above and below
+            fillAboveAndBelow(sel, selectedPosition);
+        } else {
+
+            int oldTop = oldSel.getTop();
+
+            /*
+             * Case 3: Staying still
+             */
+            sel = makeAndAddView(selectedPosition, oldTop, true, mListPadding.left, true);
+
+            // We're staying still...
+            if (oldTop < childrenTop) {
+                // ... but the top of the old selection was off screen.
+                // (This can happen if the data changes size out from under us)
+                int newBottom = sel.getBottom();
+                if (newBottom < childrenTop + 20) {
+                    // Not enough visible -- bring it onscreen
+                    sel.offsetTopAndBottom(childrenTop - sel.getTop());
+                }
+            }
+
+            // Fill in views above and below
+            fillAboveAndBelow(sel, selectedPosition);
+        }
+
+        return sel;
+    }
+
+    private class FocusSelector implements Runnable {
+        // the selector is waiting to set selection on the list view
+        private static final int STATE_SET_SELECTION = 1;
+        // the selector set the selection on the list view, waiting for a layoutChildren pass
+        private static final int STATE_WAIT_FOR_LAYOUT = 2;
+        // the selector's selection has been honored and it is waiting to request focus on the
+        // target child.
+        private static final int STATE_REQUEST_FOCUS = 3;
+
+        private int mAction;
+        private int mPosition;
+        private int mPositionTop;
+
+        FocusSelector setupForSetSelection(int position, int top) {
+            mPosition = position;
+            mPositionTop = top;
+            mAction = STATE_SET_SELECTION;
+            return this;
+        }
+
+        public void run() {
+            if (mAction == STATE_SET_SELECTION) {
+                setSelectionFromTop(mPosition, mPositionTop);
+                mAction = STATE_WAIT_FOR_LAYOUT;
+            } else if (mAction == STATE_REQUEST_FOCUS) {
+                final int childIndex = mPosition - mFirstPosition;
+                final View child = getChildAt(childIndex);
+                if (child != null) {
+                    child.requestFocus();
+                }
+                mAction = -1;
+            }
+        }
+
+        @Nullable Runnable setupFocusIfValid(int position) {
+            if (mAction != STATE_WAIT_FOR_LAYOUT || position != mPosition) {
+                return null;
+            }
+            mAction = STATE_REQUEST_FOCUS;
+            return this;
+        }
+
+        void onLayoutComplete() {
+            if (mAction == STATE_WAIT_FOR_LAYOUT) {
+                mAction = -1;
+            }
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mFocusSelector != null) {
+            removeCallbacks(mFocusSelector);
+            mFocusSelector = null;
+        }
+        super.onDetachedFromWindow();
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        if (getChildCount() > 0) {
+            View focusedChild = getFocusedChild();
+            if (focusedChild != null) {
+                final int childPosition = mFirstPosition + indexOfChild(focusedChild);
+                final int childBottom = focusedChild.getBottom();
+                final int offset = Math.max(0, childBottom - (h - mPaddingTop));
+                final int top = focusedChild.getTop() - offset;
+                if (mFocusSelector == null) {
+                    mFocusSelector = new FocusSelector();
+                }
+                post(mFocusSelector.setupForSetSelection(childPosition, top));
+            }
+        }
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Sets up mListPadding
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        int childWidth = 0;
+        int childHeight = 0;
+        int childState = 0;
+
+        mItemCount = mAdapter == null ? 0 : mAdapter.getCount();
+        if (mItemCount > 0 && (widthMode == MeasureSpec.UNSPECIFIED
+                || heightMode == MeasureSpec.UNSPECIFIED)) {
+            final View child = obtainView(0, mIsScrap);
+
+            // Lay out child directly against the parent measure spec so that
+            // we can obtain exected minimum width and height.
+            measureScrapChild(child, 0, widthMeasureSpec, heightSize);
+
+            childWidth = child.getMeasuredWidth();
+            childHeight = child.getMeasuredHeight();
+            childState = combineMeasuredStates(childState, child.getMeasuredState());
+
+            if (recycleOnMeasure() && mRecycler.shouldRecycleViewType(
+                    ((LayoutParams) child.getLayoutParams()).viewType)) {
+                mRecycler.addScrapView(child, 0);
+            }
+        }
+
+        if (widthMode == MeasureSpec.UNSPECIFIED) {
+            widthSize = mListPadding.left + mListPadding.right + childWidth +
+                    getVerticalScrollbarWidth();
+        } else {
+            widthSize |= (childState & MEASURED_STATE_MASK);
+        }
+
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            heightSize = mListPadding.top + mListPadding.bottom + childHeight +
+                    getVerticalFadingEdgeLength() * 2;
+        }
+
+        if (heightMode == MeasureSpec.AT_MOST) {
+            // TODO: after first layout we should maybe start at the first visible position, not 0
+            heightSize = measureHeightOfChildren(widthMeasureSpec, 0, NO_POSITION, heightSize, -1);
+        }
+
+        setMeasuredDimension(widthSize, heightSize);
+
+        mWidthMeasureSpec = widthMeasureSpec;
+    }
+
+    private void measureScrapChild(View child, int position, int widthMeasureSpec, int heightHint) {
+        LayoutParams p = (LayoutParams) child.getLayoutParams();
+        if (p == null) {
+            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+            child.setLayoutParams(p);
+        }
+        p.viewType = mAdapter.getItemViewType(position);
+        p.isEnabled = mAdapter.isEnabled(position);
+        p.forceAdd = true;
+
+        final int childWidthSpec = ViewGroup.getChildMeasureSpec(widthMeasureSpec,
+                mListPadding.left + mListPadding.right, p.width);
+        final int lpHeight = p.height;
+        final int childHeightSpec;
+        if (lpHeight > 0) {
+            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+        } else {
+            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(heightHint, MeasureSpec.UNSPECIFIED);
+        }
+        child.measure(childWidthSpec, childHeightSpec);
+
+        // Since this view was measured directly aginst the parent measure
+        // spec, we must measure it again before reuse.
+        child.forceLayout();
+    }
+
+    /**
+     * @return True to recycle the views used to measure this ListView in
+     *         UNSPECIFIED/AT_MOST modes, false otherwise.
+     * @hide
+     */
+    @ViewDebug.ExportedProperty(category = "list")
+    protected boolean recycleOnMeasure() {
+        return true;
+    }
+
+    /**
+     * Measures the height of the given range of children (inclusive) and
+     * returns the height with this ListView's padding and divider heights
+     * included. If maxHeight is provided, the measuring will stop when the
+     * current height reaches maxHeight.
+     *
+     * @param widthMeasureSpec The width measure spec to be given to a child's
+     *            {@link View#measure(int, int)}.
+     * @param startPosition The position of the first child to be shown.
+     * @param endPosition The (inclusive) position of the last child to be
+     *            shown. Specify {@link #NO_POSITION} if the last child should be
+     *            the last available child from the adapter.
+     * @param maxHeight The maximum height that will be returned (if all the
+     *            children don't fit in this value, this value will be
+     *            returned).
+     * @param disallowPartialChildPosition In general, whether the returned
+     *            height should only contain entire children. This is more
+     *            powerful--it is the first inclusive position at which partial
+     *            children will not be allowed. Example: it looks nice to have
+     *            at least 3 completely visible children, and in portrait this
+     *            will most likely fit; but in landscape there could be times
+     *            when even 2 children can not be completely shown, so a value
+     *            of 2 (remember, inclusive) would be good (assuming
+     *            startPosition is 0).
+     * @return The height of this ListView with the given children.
+     */
+    final int measureHeightOfChildren(int widthMeasureSpec, int startPosition, int endPosition,
+            int maxHeight, int disallowPartialChildPosition) {
+        final ListAdapter adapter = mAdapter;
+        if (adapter == null) {
+            return mListPadding.top + mListPadding.bottom;
+        }
+
+        // Include the padding of the list
+        int returnedHeight = mListPadding.top + mListPadding.bottom;
+        final int dividerHeight = mDividerHeight;
+        // The previous height value that was less than maxHeight and contained
+        // no partial children
+        int prevHeightWithoutPartialChild = 0;
+        int i;
+        View child;
+
+        // mItemCount - 1 since endPosition parameter is inclusive
+        endPosition = (endPosition == NO_POSITION) ? adapter.getCount() - 1 : endPosition;
+        final AbsListView.RecycleBin recycleBin = mRecycler;
+        final boolean recyle = recycleOnMeasure();
+        final boolean[] isScrap = mIsScrap;
+
+        for (i = startPosition; i <= endPosition; ++i) {
+            child = obtainView(i, isScrap);
+
+            measureScrapChild(child, i, widthMeasureSpec, maxHeight);
+
+            if (i > 0) {
+                // Count the divider for all but one child
+                returnedHeight += dividerHeight;
+            }
+
+            // Recycle the view before we possibly return from the method
+            if (recyle && recycleBin.shouldRecycleViewType(
+                    ((LayoutParams) child.getLayoutParams()).viewType)) {
+                recycleBin.addScrapView(child, -1);
+            }
+
+            returnedHeight += child.getMeasuredHeight();
+
+            if (returnedHeight >= maxHeight) {
+                // We went over, figure out which height to return.  If returnedHeight > maxHeight,
+                // then the i'th position did not fit completely.
+                return (disallowPartialChildPosition >= 0) // Disallowing is enabled (> -1)
+                            && (i > disallowPartialChildPosition) // We've past the min pos
+                            && (prevHeightWithoutPartialChild > 0) // We have a prev height
+                            && (returnedHeight != maxHeight) // i'th child did not fit completely
+                        ? prevHeightWithoutPartialChild
+                        : maxHeight;
+            }
+
+            if ((disallowPartialChildPosition >= 0) && (i >= disallowPartialChildPosition)) {
+                prevHeightWithoutPartialChild = returnedHeight;
+            }
+        }
+
+        // At this point, we went through the range of children, and they each
+        // completely fit, so return the returnedHeight
+        return returnedHeight;
+    }
+
+    @Override
+    int findMotionRow(int y) {
+        int childCount = getChildCount();
+        if (childCount > 0) {
+            if (!mStackFromBottom) {
+                for (int i = 0; i < childCount; i++) {
+                    View v = getChildAt(i);
+                    if (y <= v.getBottom()) {
+                        return mFirstPosition + i;
+                    }
+                }
+            } else {
+                for (int i = childCount - 1; i >= 0; i--) {
+                    View v = getChildAt(i);
+                    if (y >= v.getTop()) {
+                        return mFirstPosition + i;
+                    }
+                }
+            }
+        }
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Put a specific item at a specific location on the screen and then build
+     * up and down from there.
+     *
+     * @param position The reference view to use as the starting point
+     * @param top Pixel offset from the top of this view to the top of the
+     *        reference view.
+     *
+     * @return The selected view, or null if the selected view is outside the
+     *         visible area.
+     */
+    private View fillSpecific(int position, int top) {
+        boolean tempIsSelected = position == mSelectedPosition;
+        View temp = makeAndAddView(position, top, true, mListPadding.left, tempIsSelected);
+        // Possibly changed again in fillUp if we add rows above this one.
+        mFirstPosition = position;
+
+        View above;
+        View below;
+
+        final int dividerHeight = mDividerHeight;
+        if (!mStackFromBottom) {
+            above = fillUp(position - 1, temp.getTop() - dividerHeight);
+            // This will correct for the top of the first view not touching the top of the list
+            adjustViewsUpOrDown();
+            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
+            int childCount = getChildCount();
+            if (childCount > 0) {
+                correctTooHigh(childCount);
+            }
+        } else {
+            below = fillDown(position + 1, temp.getBottom() + dividerHeight);
+            // This will correct for the bottom of the last view not touching the bottom of the list
+            adjustViewsUpOrDown();
+            above = fillUp(position - 1, temp.getTop() - dividerHeight);
+            int childCount = getChildCount();
+            if (childCount > 0) {
+                 correctTooLow(childCount);
+            }
+        }
+
+        if (tempIsSelected) {
+            return temp;
+        } else if (above != null) {
+            return above;
+        } else {
+            return below;
+        }
+    }
+
+    /**
+     * Check if we have dragged the bottom of the list too high (we have pushed the
+     * top element off the top of the screen when we did not need to). Correct by sliding
+     * everything back down.
+     *
+     * @param childCount Number of children
+     */
+    private void correctTooHigh(int childCount) {
+        // First see if the last item is visible. If it is not, it is OK for the
+        // top of the list to be pushed up.
+        int lastPosition = mFirstPosition + childCount - 1;
+        if (lastPosition == mItemCount - 1 && childCount > 0) {
+
+            // Get the last child ...
+            final View lastChild = getChildAt(childCount - 1);
+
+            // ... and its bottom edge
+            final int lastBottom = lastChild.getBottom();
+
+            // This is bottom of our drawable area
+            final int end = (mBottom - mTop) - mListPadding.bottom;
+
+            // This is how far the bottom edge of the last view is from the bottom of the
+            // drawable area
+            int bottomOffset = end - lastBottom;
+            View firstChild = getChildAt(0);
+            final int firstTop = firstChild.getTop();
+
+            // Make sure we are 1) Too high, and 2) Either there are more rows above the
+            // first row or the first row is scrolled off the top of the drawable area
+            if (bottomOffset > 0 && (mFirstPosition > 0 || firstTop < mListPadding.top))  {
+                if (mFirstPosition == 0) {
+                    // Don't pull the top too far down
+                    bottomOffset = Math.min(bottomOffset, mListPadding.top - firstTop);
+                }
+                // Move everything down
+                offsetChildrenTopAndBottom(bottomOffset);
+                if (mFirstPosition > 0) {
+                    // Fill the gap that was opened above mFirstPosition with more rows, if
+                    // possible
+                    fillUp(mFirstPosition - 1, firstChild.getTop() - mDividerHeight);
+                    // Close up the remaining gap
+                    adjustViewsUpOrDown();
+                }
+
+            }
+        }
+    }
+
+    /**
+     * Check if we have dragged the bottom of the list too low (we have pushed the
+     * bottom element off the bottom of the screen when we did not need to). Correct by sliding
+     * everything back up.
+     *
+     * @param childCount Number of children
+     */
+    private void correctTooLow(int childCount) {
+        // First see if the first item is visible. If it is not, it is OK for the
+        // bottom of the list to be pushed down.
+        if (mFirstPosition == 0 && childCount > 0) {
+
+            // Get the first child ...
+            final View firstChild = getChildAt(0);
+
+            // ... and its top edge
+            final int firstTop = firstChild.getTop();
+
+            // This is top of our drawable area
+            final int start = mListPadding.top;
+
+            // This is bottom of our drawable area
+            final int end = (mBottom - mTop) - mListPadding.bottom;
+
+            // This is how far the top edge of the first view is from the top of the
+            // drawable area
+            int topOffset = firstTop - start;
+            View lastChild = getChildAt(childCount - 1);
+            final int lastBottom = lastChild.getBottom();
+            int lastPosition = mFirstPosition + childCount - 1;
+
+            // Make sure we are 1) Too low, and 2) Either there are more rows below the
+            // last row or the last row is scrolled off the bottom of the drawable area
+            if (topOffset > 0) {
+                if (lastPosition < mItemCount - 1 || lastBottom > end)  {
+                    if (lastPosition == mItemCount - 1) {
+                        // Don't pull the bottom too far up
+                        topOffset = Math.min(topOffset, lastBottom - end);
+                    }
+                    // Move everything up
+                    offsetChildrenTopAndBottom(-topOffset);
+                    if (lastPosition < mItemCount - 1) {
+                        // Fill the gap that was opened below the last position with more rows, if
+                        // possible
+                        fillDown(lastPosition + 1, lastChild.getBottom() + mDividerHeight);
+                        // Close up the remaining gap
+                        adjustViewsUpOrDown();
+                    }
+                } else if (lastPosition == mItemCount - 1) {
+                    adjustViewsUpOrDown();
+                }
+            }
+        }
+    }
+
+    @Override
+    protected void layoutChildren() {
+        final boolean blockLayoutRequests = mBlockLayoutRequests;
+        if (blockLayoutRequests) {
+            return;
+        }
+
+        mBlockLayoutRequests = true;
+
+        try {
+            super.layoutChildren();
+
+            invalidate();
+
+            if (mAdapter == null) {
+                resetList();
+                invokeOnItemScrollListener();
+                return;
+            }
+
+            final int childrenTop = mListPadding.top;
+            final int childrenBottom = mBottom - mTop - mListPadding.bottom;
+            final int childCount = getChildCount();
+
+            int index = 0;
+            int delta = 0;
+
+            View sel;
+            View oldSel = null;
+            View oldFirst = null;
+            View newSel = null;
+
+            // Remember stuff we will need down below
+            switch (mLayoutMode) {
+            case LAYOUT_SET_SELECTION:
+                index = mNextSelectedPosition - mFirstPosition;
+                if (index >= 0 && index < childCount) {
+                    newSel = getChildAt(index);
+                }
+                break;
+            case LAYOUT_FORCE_TOP:
+            case LAYOUT_FORCE_BOTTOM:
+            case LAYOUT_SPECIFIC:
+            case LAYOUT_SYNC:
+                break;
+            case LAYOUT_MOVE_SELECTION:
+            default:
+                // Remember the previously selected view
+                index = mSelectedPosition - mFirstPosition;
+                if (index >= 0 && index < childCount) {
+                    oldSel = getChildAt(index);
+                }
+
+                // Remember the previous first child
+                oldFirst = getChildAt(0);
+
+                if (mNextSelectedPosition >= 0) {
+                    delta = mNextSelectedPosition - mSelectedPosition;
+                }
+
+                // Caution: newSel might be null
+                newSel = getChildAt(index + delta);
+            }
+
+
+            boolean dataChanged = mDataChanged;
+            if (dataChanged) {
+                handleDataChanged();
+            }
+
+            // Handle the empty set by removing all views that are visible
+            // and calling it a day
+            if (mItemCount == 0) {
+                resetList();
+                invokeOnItemScrollListener();
+                return;
+            } else if (mItemCount != mAdapter.getCount()) {
+                throw new IllegalStateException("The content of the adapter has changed but "
+                        + "ListView did not receive a notification. Make sure the content of "
+                        + "your adapter is not modified from a background thread, but only from "
+                        + "the UI thread. Make sure your adapter calls notifyDataSetChanged() "
+                        + "when its content changes. [in ListView(" + getId() + ", " + getClass()
+                        + ") with Adapter(" + mAdapter.getClass() + ")]");
+            }
+
+            setSelectedPositionInt(mNextSelectedPosition);
+
+            AccessibilityNodeInfo accessibilityFocusLayoutRestoreNode = null;
+            View accessibilityFocusLayoutRestoreView = null;
+            int accessibilityFocusPosition = INVALID_POSITION;
+
+            // Remember which child, if any, had accessibility focus. This must
+            // occur before recycling any views, since that will clear
+            // accessibility focus.
+            final ViewRootImpl viewRootImpl = getViewRootImpl();
+            if (viewRootImpl != null) {
+                final View focusHost = viewRootImpl.getAccessibilityFocusedHost();
+                if (focusHost != null) {
+                    final View focusChild = getAccessibilityFocusedChild(focusHost);
+                    if (focusChild != null) {
+                        if (!dataChanged || isDirectChildHeaderOrFooter(focusChild)
+                                || (focusChild.hasTransientState() && mAdapterHasStableIds)) {
+                            // The views won't be changing, so try to maintain
+                            // focus on the current host and virtual view.
+                            accessibilityFocusLayoutRestoreView = focusHost;
+                            accessibilityFocusLayoutRestoreNode = viewRootImpl
+                                    .getAccessibilityFocusedVirtualView();
+                        }
+
+                        // If all else fails, maintain focus at the same
+                        // position.
+                        accessibilityFocusPosition = getPositionForView(focusChild);
+                    }
+                }
+            }
+
+            View focusLayoutRestoreDirectChild = null;
+            View focusLayoutRestoreView = null;
+
+            // Take focus back to us temporarily to avoid the eventual call to
+            // clear focus when removing the focused child below from messing
+            // things up when ViewAncestor assigns focus back to someone else.
+            final View focusedChild = getFocusedChild();
+            if (focusedChild != null) {
+                // TODO: in some cases focusedChild.getParent() == null
+
+                // We can remember the focused view to restore after re-layout
+                // if the data hasn't changed, or if the focused position is a
+                // header or footer.
+                if (!dataChanged || isDirectChildHeaderOrFooter(focusedChild)
+                        || focusedChild.hasTransientState() || mAdapterHasStableIds) {
+                    focusLayoutRestoreDirectChild = focusedChild;
+                    // Remember the specific view that had focus.
+                    focusLayoutRestoreView = findFocus();
+                    if (focusLayoutRestoreView != null) {
+                        // Tell it we are going to mess with it.
+                        focusLayoutRestoreView.dispatchStartTemporaryDetach();
+                    }
+                }
+                requestFocus();
+            }
+
+            // Pull all children into the RecycleBin.
+            // These views will be reused if possible
+            final int firstPosition = mFirstPosition;
+            final RecycleBin recycleBin = mRecycler;
+            if (dataChanged) {
+                for (int i = 0; i < childCount; i++) {
+                    recycleBin.addScrapView(getChildAt(i), firstPosition+i);
+                }
+            } else {
+                recycleBin.fillActiveViews(childCount, firstPosition);
+            }
+
+            // Clear out old views
+            detachAllViewsFromParent();
+            recycleBin.removeSkippedScrap();
+
+            switch (mLayoutMode) {
+            case LAYOUT_SET_SELECTION:
+                if (newSel != null) {
+                    sel = fillFromSelection(newSel.getTop(), childrenTop, childrenBottom);
+                } else {
+                    sel = fillFromMiddle(childrenTop, childrenBottom);
+                }
+                break;
+            case LAYOUT_SYNC:
+                sel = fillSpecific(mSyncPosition, mSpecificTop);
+                break;
+            case LAYOUT_FORCE_BOTTOM:
+                sel = fillUp(mItemCount - 1, childrenBottom);
+                adjustViewsUpOrDown();
+                break;
+            case LAYOUT_FORCE_TOP:
+                mFirstPosition = 0;
+                sel = fillFromTop(childrenTop);
+                adjustViewsUpOrDown();
+                break;
+            case LAYOUT_SPECIFIC:
+                final int selectedPosition = reconcileSelectedPosition();
+                sel = fillSpecific(selectedPosition, mSpecificTop);
+                /**
+                 * When ListView is resized, FocusSelector requests an async selection for the
+                 * previously focused item to make sure it is still visible. If the item is not
+                 * selectable, it won't regain focus so instead we call FocusSelector
+                 * to directly request focus on the view after it is visible.
+                 */
+                if (sel == null && mFocusSelector != null) {
+                    final Runnable focusRunnable = mFocusSelector
+                            .setupFocusIfValid(selectedPosition);
+                    if (focusRunnable != null) {
+                        post(focusRunnable);
+                    }
+                }
+                break;
+            case LAYOUT_MOVE_SELECTION:
+                sel = moveSelection(oldSel, newSel, delta, childrenTop, childrenBottom);
+                break;
+            default:
+                if (childCount == 0) {
+                    if (!mStackFromBottom) {
+                        final int position = lookForSelectablePosition(0, true);
+                        setSelectedPositionInt(position);
+                        sel = fillFromTop(childrenTop);
+                    } else {
+                        final int position = lookForSelectablePosition(mItemCount - 1, false);
+                        setSelectedPositionInt(position);
+                        sel = fillUp(mItemCount - 1, childrenBottom);
+                    }
+                } else {
+                    if (mSelectedPosition >= 0 && mSelectedPosition < mItemCount) {
+                        sel = fillSpecific(mSelectedPosition,
+                                oldSel == null ? childrenTop : oldSel.getTop());
+                    } else if (mFirstPosition < mItemCount) {
+                        sel = fillSpecific(mFirstPosition,
+                                oldFirst == null ? childrenTop : oldFirst.getTop());
+                    } else {
+                        sel = fillSpecific(0, childrenTop);
+                    }
+                }
+                break;
+            }
+
+            // Flush any cached views that did not get reused above
+            recycleBin.scrapActiveViews();
+
+            // remove any header/footer that has been temp detached and not re-attached
+            removeUnusedFixedViews(mHeaderViewInfos);
+            removeUnusedFixedViews(mFooterViewInfos);
+
+            if (sel != null) {
+                // The current selected item should get focus if items are
+                // focusable.
+                if (mItemsCanFocus && hasFocus() && !sel.hasFocus()) {
+                    final boolean focusWasTaken = (sel == focusLayoutRestoreDirectChild &&
+                            focusLayoutRestoreView != null &&
+                            focusLayoutRestoreView.requestFocus()) || sel.requestFocus();
+                    if (!focusWasTaken) {
+                        // Selected item didn't take focus, but we still want to
+                        // make sure something else outside of the selected view
+                        // has focus.
+                        final View focused = getFocusedChild();
+                        if (focused != null) {
+                            focused.clearFocus();
+                        }
+                        positionSelector(INVALID_POSITION, sel);
+                    } else {
+                        sel.setSelected(false);
+                        mSelectorRect.setEmpty();
+                    }
+                } else {
+                    positionSelector(INVALID_POSITION, sel);
+                }
+                mSelectedTop = sel.getTop();
+            } else {
+                final boolean inTouchMode = mTouchMode == TOUCH_MODE_TAP
+                        || mTouchMode == TOUCH_MODE_DONE_WAITING;
+                if (inTouchMode) {
+                    // If the user's finger is down, select the motion position.
+                    final View child = getChildAt(mMotionPosition - mFirstPosition);
+                    if (child != null) {
+                        positionSelector(mMotionPosition, child);
+                    }
+                } else if (mSelectorPosition != INVALID_POSITION) {
+                    // If we had previously positioned the selector somewhere,
+                    // put it back there. It might not match up with the data,
+                    // but it's transitioning out so it's not a big deal.
+                    final View child = getChildAt(mSelectorPosition - mFirstPosition);
+                    if (child != null) {
+                        positionSelector(mSelectorPosition, child);
+                    }
+                } else {
+                    // Otherwise, clear selection.
+                    mSelectedTop = 0;
+                    mSelectorRect.setEmpty();
+                }
+
+                // Even if there is not selected position, we may need to
+                // restore focus (i.e. something focusable in touch mode).
+                if (hasFocus() && focusLayoutRestoreView != null) {
+                    focusLayoutRestoreView.requestFocus();
+                }
+            }
+
+            // Attempt to restore accessibility focus, if necessary.
+            if (viewRootImpl != null) {
+                final View newAccessibilityFocusedView = viewRootImpl.getAccessibilityFocusedHost();
+                if (newAccessibilityFocusedView == null) {
+                    if (accessibilityFocusLayoutRestoreView != null
+                            && accessibilityFocusLayoutRestoreView.isAttachedToWindow()) {
+                        final AccessibilityNodeProvider provider =
+                                accessibilityFocusLayoutRestoreView.getAccessibilityNodeProvider();
+                        if (accessibilityFocusLayoutRestoreNode != null && provider != null) {
+                            final int virtualViewId = AccessibilityNodeInfo.getVirtualDescendantId(
+                                    accessibilityFocusLayoutRestoreNode.getSourceNodeId());
+                            provider.performAction(virtualViewId,
+                                    AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+                        } else {
+                            accessibilityFocusLayoutRestoreView.requestAccessibilityFocus();
+                        }
+                    } else if (accessibilityFocusPosition != INVALID_POSITION) {
+                        // Bound the position within the visible children.
+                        final int position = MathUtils.constrain(
+                                accessibilityFocusPosition - mFirstPosition, 0,
+                                getChildCount() - 1);
+                        final View restoreView = getChildAt(position);
+                        if (restoreView != null) {
+                            restoreView.requestAccessibilityFocus();
+                        }
+                    }
+                }
+            }
+
+            // Tell focus view we are done mucking with it, if it is still in
+            // our view hierarchy.
+            if (focusLayoutRestoreView != null
+                    && focusLayoutRestoreView.getWindowToken() != null) {
+                focusLayoutRestoreView.dispatchFinishTemporaryDetach();
+            }
+
+            mLayoutMode = LAYOUT_NORMAL;
+            mDataChanged = false;
+            if (mPositionScrollAfterLayout != null) {
+                post(mPositionScrollAfterLayout);
+                mPositionScrollAfterLayout = null;
+            }
+            mNeedSync = false;
+            setNextSelectedPositionInt(mSelectedPosition);
+
+            updateScrollIndicators();
+
+            if (mItemCount > 0) {
+                checkSelectionChanged();
+            }
+
+            invokeOnItemScrollListener();
+        } finally {
+            if (mFocusSelector != null) {
+                mFocusSelector.onLayoutComplete();
+            }
+            if (!blockLayoutRequests) {
+                mBlockLayoutRequests = false;
+            }
+        }
+    }
+
+    @Override
+    boolean trackMotionScroll(int deltaY, int incrementalDeltaY) {
+        final boolean result = super.trackMotionScroll(deltaY, incrementalDeltaY);
+        removeUnusedFixedViews(mHeaderViewInfos);
+        removeUnusedFixedViews(mFooterViewInfos);
+        return result;
+    }
+
+    /**
+     * Header and Footer views are not scrapped / recycled like other views but they are still
+     * detached from the ViewGroup. After a layout operation, call this method to remove such views.
+     *
+     * @param infoList The info list to be traversed
+     */
+    private void removeUnusedFixedViews(@Nullable List<FixedViewInfo> infoList) {
+        if (infoList == null) {
+            return;
+        }
+        for (int i = infoList.size() - 1; i >= 0; i--) {
+            final FixedViewInfo fixedViewInfo = infoList.get(i);
+            final View view = fixedViewInfo.view;
+            final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+            if (view.getParent() == null && lp != null && lp.recycledHeaderFooter) {
+                removeDetachedView(view, false);
+                lp.recycledHeaderFooter = false;
+            }
+
+        }
+    }
+
+    /**
+     * @param child a direct child of this list.
+     * @return Whether child is a header or footer view.
+     */
+    private boolean isDirectChildHeaderOrFooter(View child) {
+        final ArrayList<FixedViewInfo> headers = mHeaderViewInfos;
+        final int numHeaders = headers.size();
+        for (int i = 0; i < numHeaders; i++) {
+            if (child == headers.get(i).view) {
+                return true;
+            }
+        }
+
+        final ArrayList<FixedViewInfo> footers = mFooterViewInfos;
+        final int numFooters = footers.size();
+        for (int i = 0; i < numFooters; i++) {
+            if (child == footers.get(i).view) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Obtains the view and adds it to our list of children. The view can be
+     * made fresh, converted from an unused view, or used as is if it was in
+     * the recycle bin.
+     *
+     * @param position logical position in the list
+     * @param y top or bottom edge of the view to add
+     * @param flow {@code true} to align top edge to y, {@code false} to align
+     *             bottom edge to y
+     * @param childrenLeft left edge where children should be positioned
+     * @param selected {@code true} if the position is selected, {@code false}
+     *                 otherwise
+     * @return the view that was added
+     */
+    private View makeAndAddView(int position, int y, boolean flow, int childrenLeft,
+            boolean selected) {
+        if (!mDataChanged) {
+            // Try to use an existing view for this position.
+            final View activeView = mRecycler.getActiveView(position);
+            if (activeView != null) {
+                // Found it. We're reusing an existing child, so it just needs
+                // to be positioned like a scrap view.
+                setupChild(activeView, position, y, flow, childrenLeft, selected, true);
+                return activeView;
+            }
+        }
+
+        // Make a new view for this position, or convert an unused view if
+        // possible.
+        final View child = obtainView(position, mIsScrap);
+
+        // This needs to be positioned and measured.
+        setupChild(child, position, y, flow, childrenLeft, selected, mIsScrap[0]);
+
+        return child;
+    }
+
+    /**
+     * Adds a view as a child and make sure it is measured (if necessary) and
+     * positioned properly.
+     *
+     * @param child the view to add
+     * @param position the position of this child
+     * @param y the y position relative to which this view will be positioned
+     * @param flowDown {@code true} to align top edge to y, {@code false} to
+     *                 align bottom edge to y
+     * @param childrenLeft left edge where children should be positioned
+     * @param selected {@code true} if the position is selected, {@code false}
+     *                 otherwise
+     * @param isAttachedToWindow {@code true} if the view is already attached
+     *                           to the window, e.g. whether it was reused, or
+     *                           {@code false} otherwise
+     */
+    private void setupChild(View child, int position, int y, boolean flowDown, int childrenLeft,
+            boolean selected, boolean isAttachedToWindow) {
+        Trace.traceBegin(Trace.TRACE_TAG_VIEW, "setupListItem");
+
+        final boolean isSelected = selected && shouldShowSelector();
+        final boolean updateChildSelected = isSelected != child.isSelected();
+        final int mode = mTouchMode;
+        final boolean isPressed = mode > TOUCH_MODE_DOWN && mode < TOUCH_MODE_SCROLL
+                && mMotionPosition == position;
+        final boolean updateChildPressed = isPressed != child.isPressed();
+        final boolean needToMeasure = !isAttachedToWindow || updateChildSelected
+                || child.isLayoutRequested();
+
+        // Respect layout params that are already in the view. Otherwise make
+        // some up...
+        AbsListView.LayoutParams p = (AbsListView.LayoutParams) child.getLayoutParams();
+        if (p == null) {
+            p = (AbsListView.LayoutParams) generateDefaultLayoutParams();
+        }
+        p.viewType = mAdapter.getItemViewType(position);
+        p.isEnabled = mAdapter.isEnabled(position);
+
+        // Set up view state before attaching the view, since we may need to
+        // rely on the jumpDrawablesToCurrentState() call that occurs as part
+        // of view attachment.
+        if (updateChildSelected) {
+            child.setSelected(isSelected);
+        }
+
+        if (updateChildPressed) {
+            child.setPressed(isPressed);
+        }
+
+        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null) {
+            if (child instanceof Checkable) {
+                ((Checkable) child).setChecked(mCheckStates.get(position));
+            } else if (getContext().getApplicationInfo().targetSdkVersion
+                    >= android.os.Build.VERSION_CODES.HONEYCOMB) {
+                child.setActivated(mCheckStates.get(position));
+            }
+        }
+
+        if ((isAttachedToWindow && !p.forceAdd) || (p.recycledHeaderFooter
+                && p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER)) {
+            attachViewToParent(child, flowDown ? -1 : 0, p);
+
+            // If the view was previously attached for a different position,
+            // then manually jump the drawables.
+            if (isAttachedToWindow
+                    && (((AbsListView.LayoutParams) child.getLayoutParams()).scrappedFromPosition)
+                            != position) {
+                child.jumpDrawablesToCurrentState();
+            }
+        } else {
+            p.forceAdd = false;
+            if (p.viewType == AdapterView.ITEM_VIEW_TYPE_HEADER_OR_FOOTER) {
+                p.recycledHeaderFooter = true;
+            }
+            addViewInLayout(child, flowDown ? -1 : 0, p, true);
+            // add view in layout will reset the RTL properties. We have to re-resolve them
+            child.resolveRtlPropertiesIfNeeded();
+        }
+
+        if (needToMeasure) {
+            final int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+                    mListPadding.left + mListPadding.right, p.width);
+            final int lpHeight = p.height;
+            final int childHeightSpec;
+            if (lpHeight > 0) {
+                childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+            } else {
+                childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
+                        MeasureSpec.UNSPECIFIED);
+            }
+            child.measure(childWidthSpec, childHeightSpec);
+        } else {
+            cleanupLayoutState(child);
+        }
+
+        final int w = child.getMeasuredWidth();
+        final int h = child.getMeasuredHeight();
+        final int childTop = flowDown ? y : y - h;
+
+        if (needToMeasure) {
+            final int childRight = childrenLeft + w;
+            final int childBottom = childTop + h;
+            child.layout(childrenLeft, childTop, childRight, childBottom);
+        } else {
+            child.offsetLeftAndRight(childrenLeft - child.getLeft());
+            child.offsetTopAndBottom(childTop - child.getTop());
+        }
+
+        if (mCachingStarted && !child.isDrawingCacheEnabled()) {
+            child.setDrawingCacheEnabled(true);
+        }
+
+        Trace.traceEnd(Trace.TRACE_TAG_VIEW);
+    }
+
+    @Override
+    protected boolean canAnimate() {
+        return super.canAnimate() && mItemCount > 0;
+    }
+
+    /**
+     * Sets the currently selected item. If in touch mode, the item will not be selected
+     * but it will still be positioned appropriately. If the specified selection position
+     * is less than 0, then the item at position 0 will be selected.
+     *
+     * @param position Index (starting at 0) of the data item to be selected.
+     */
+    @Override
+    public void setSelection(int position) {
+        setSelectionFromTop(position, 0);
+    }
+
+    /**
+     * Makes the item at the supplied position selected.
+     *
+     * @param position the position of the item to select
+     */
+    @Override
+    void setSelectionInt(int position) {
+        setNextSelectedPositionInt(position);
+        boolean awakeScrollbars = false;
+
+        final int selectedPosition = mSelectedPosition;
+
+        if (selectedPosition >= 0) {
+            if (position == selectedPosition - 1) {
+                awakeScrollbars = true;
+            } else if (position == selectedPosition + 1) {
+                awakeScrollbars = true;
+            }
+        }
+
+        if (mPositionScroller != null) {
+            mPositionScroller.stop();
+        }
+
+        layoutChildren();
+
+        if (awakeScrollbars) {
+            awakenScrollBars();
+        }
+    }
+
+    /**
+     * Find a position that can be selected (i.e., is not a separator).
+     *
+     * @param position The starting position to look at.
+     * @param lookDown Whether to look down for other positions.
+     * @return The next selectable position starting at position and then searching either up or
+     *         down. Returns {@link #INVALID_POSITION} if nothing can be found.
+     */
+    @Override
+    int lookForSelectablePosition(int position, boolean lookDown) {
+        final ListAdapter adapter = mAdapter;
+        if (adapter == null || isInTouchMode()) {
+            return INVALID_POSITION;
+        }
+
+        final int count = adapter.getCount();
+        if (!mAreAllItemsSelectable) {
+            if (lookDown) {
+                position = Math.max(0, position);
+                while (position < count && !adapter.isEnabled(position)) {
+                    position++;
+                }
+            } else {
+                position = Math.min(position, count - 1);
+                while (position >= 0 && !adapter.isEnabled(position)) {
+                    position--;
+                }
+            }
+        }
+
+        if (position < 0 || position >= count) {
+            return INVALID_POSITION;
+        }
+
+        return position;
+    }
+
+    /**
+     * Find a position that can be selected (i.e., is not a separator). If there
+     * are no selectable positions in the specified direction from the starting
+     * position, searches in the opposite direction from the starting position
+     * to the current position.
+     *
+     * @param current the current position
+     * @param position the starting position
+     * @param lookDown whether to look down for other positions
+     * @return the next selectable position, or {@link #INVALID_POSITION} if
+     *         nothing can be found
+     */
+    int lookForSelectablePositionAfter(int current, int position, boolean lookDown) {
+        final ListAdapter adapter = mAdapter;
+        if (adapter == null || isInTouchMode()) {
+            return INVALID_POSITION;
+        }
+
+        // First check after the starting position in the specified direction.
+        final int after = lookForSelectablePosition(position, lookDown);
+        if (after != INVALID_POSITION) {
+            return after;
+        }
+
+        // Then check between the starting position and the current position.
+        final int count = adapter.getCount();
+        current = MathUtils.constrain(current, -1, count - 1);
+        if (lookDown) {
+            position = Math.min(position - 1, count - 1);
+            while ((position > current) && !adapter.isEnabled(position)) {
+                position--;
+            }
+            if (position <= current) {
+                return INVALID_POSITION;
+            }
+        } else {
+            position = Math.max(0, position + 1);
+            while ((position < current) && !adapter.isEnabled(position)) {
+                position++;
+            }
+            if (position >= current) {
+                return INVALID_POSITION;
+            }
+        }
+
+        return position;
+    }
+
+    /**
+     * setSelectionAfterHeaderView set the selection to be the first list item
+     * after the header views.
+     */
+    public void setSelectionAfterHeaderView() {
+        final int count = getHeaderViewsCount();
+        if (count > 0) {
+            mNextSelectedPosition = 0;
+            return;
+        }
+
+        if (mAdapter != null) {
+            setSelection(count);
+        } else {
+            mNextSelectedPosition = count;
+            mLayoutMode = LAYOUT_SET_SELECTION;
+        }
+
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Dispatch in the normal way
+        boolean handled = super.dispatchKeyEvent(event);
+        if (!handled) {
+            // If we didn't handle it...
+            View focused = getFocusedChild();
+            if (focused != null && event.getAction() == KeyEvent.ACTION_DOWN) {
+                // ... and our focused child didn't handle it
+                // ... give it to ourselves so we can scroll if necessary
+                handled = onKeyDown(event.getKeyCode(), event);
+            }
+        }
+        return handled;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        return commonKey(keyCode, 1, event);
+    }
+
+    @Override
+    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+        return commonKey(keyCode, repeatCount, event);
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        return commonKey(keyCode, 1, event);
+    }
+
+    private boolean commonKey(int keyCode, int count, KeyEvent event) {
+        if (mAdapter == null || !isAttachedToWindow()) {
+            return false;
+        }
+
+        if (mDataChanged) {
+            layoutChildren();
+        }
+
+        boolean handled = false;
+        int action = event.getAction();
+        if (KeyEvent.isConfirmKey(keyCode)
+                && event.hasNoModifiers() && action != KeyEvent.ACTION_UP) {
+            handled = resurrectSelectionIfNeeded();
+            if (!handled && event.getRepeatCount() == 0 && getChildCount() > 0) {
+                keyPressed();
+                handled = true;
+            }
+        }
+
+
+        if (!handled && action != KeyEvent.ACTION_UP) {
+            switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_UP:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded();
+                    if (!handled) {
+                        while (count-- > 0) {
+                            if (arrowScroll(FOCUS_UP)) {
+                                handled = true;
+                            } else {
+                                break;
+                            }
+                        }
+                    }
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded();
+                    if (!handled) {
+                        while (count-- > 0) {
+                            if (arrowScroll(FOCUS_DOWN)) {
+                                handled = true;
+                            } else {
+                                break;
+                            }
+                        }
+                    }
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (event.hasNoModifiers()) {
+                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_LEFT);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (event.hasNoModifiers()) {
+                    handled = handleHorizontalFocusWithinListItem(View.FOCUS_RIGHT);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_PAGE_UP:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_UP);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_PAGE_DOWN:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded() || pageScroll(FOCUS_DOWN);
+                } else if (event.hasModifiers(KeyEvent.META_ALT_ON)) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_MOVE_HOME:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_UP);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_MOVE_END:
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded() || fullScroll(FOCUS_DOWN);
+                }
+                break;
+
+            case KeyEvent.KEYCODE_TAB:
+                // This creates an asymmetry in TAB navigation order. At some
+                // point in the future we may decide that it's preferable to
+                // force the list selection to the top or bottom when receiving
+                // TAB focus from another widget, but for now this is adequate.
+                if (event.hasNoModifiers()) {
+                    handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_DOWN);
+                } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                    handled = resurrectSelectionIfNeeded() || arrowScroll(FOCUS_UP);
+                }
+                break;
+            }
+        }
+
+        if (handled) {
+            return true;
+        }
+
+        if (sendToTextFilter(keyCode, count, event)) {
+            return true;
+        }
+
+        switch (action) {
+            case KeyEvent.ACTION_DOWN:
+                return super.onKeyDown(keyCode, event);
+
+            case KeyEvent.ACTION_UP:
+                return super.onKeyUp(keyCode, event);
+
+            case KeyEvent.ACTION_MULTIPLE:
+                return super.onKeyMultiple(keyCode, count, event);
+
+            default: // shouldn't happen
+                return false;
+        }
+    }
+
+    /**
+     * Scrolls up or down by the number of items currently present on screen.
+     *
+     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+     * @return whether selection was moved
+     */
+    boolean pageScroll(int direction) {
+        final int nextPage;
+        final boolean down;
+
+        if (direction == FOCUS_UP) {
+            nextPage = Math.max(0, mSelectedPosition - getChildCount() - 1);
+            down = false;
+        } else if (direction == FOCUS_DOWN) {
+            nextPage = Math.min(mItemCount - 1, mSelectedPosition + getChildCount() - 1);
+            down = true;
+        } else {
+            return false;
+        }
+
+        if (nextPage >= 0) {
+            final int position = lookForSelectablePositionAfter(mSelectedPosition, nextPage, down);
+            if (position >= 0) {
+                mLayoutMode = LAYOUT_SPECIFIC;
+                mSpecificTop = mPaddingTop + getVerticalFadingEdgeLength();
+
+                if (down && (position > (mItemCount - getChildCount()))) {
+                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
+                }
+
+                if (!down && (position < getChildCount())) {
+                    mLayoutMode = LAYOUT_FORCE_TOP;
+                }
+
+                setSelectionInt(position);
+                invokeOnItemScrollListener();
+                if (!awakenScrollBars()) {
+                    invalidate();
+                }
+
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Go to the last or first item if possible (not worrying about panning
+     * across or navigating within the internal focus of the currently selected
+     * item.)
+     *
+     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+     * @return whether selection was moved
+     */
+    boolean fullScroll(int direction) {
+        boolean moved = false;
+        if (direction == FOCUS_UP) {
+            if (mSelectedPosition != 0) {
+                final int position = lookForSelectablePositionAfter(mSelectedPosition, 0, true);
+                if (position >= 0) {
+                    mLayoutMode = LAYOUT_FORCE_TOP;
+                    setSelectionInt(position);
+                    invokeOnItemScrollListener();
+                }
+                moved = true;
+            }
+        } else if (direction == FOCUS_DOWN) {
+            final int lastItem = (mItemCount - 1);
+            if (mSelectedPosition < lastItem) {
+                final int position = lookForSelectablePositionAfter(
+                        mSelectedPosition, lastItem, false);
+                if (position >= 0) {
+                    mLayoutMode = LAYOUT_FORCE_BOTTOM;
+                    setSelectionInt(position);
+                    invokeOnItemScrollListener();
+                }
+                moved = true;
+            }
+        }
+
+        if (moved && !awakenScrollBars()) {
+            awakenScrollBars();
+            invalidate();
+        }
+
+        return moved;
+    }
+
+    /**
+     * To avoid horizontal focus searches changing the selected item, we
+     * manually focus search within the selected item (as applicable), and
+     * prevent focus from jumping to something within another item.
+     * @param direction one of {View.FOCUS_LEFT, View.FOCUS_RIGHT}
+     * @return Whether this consumes the key event.
+     */
+    private boolean handleHorizontalFocusWithinListItem(int direction) {
+        if (direction != View.FOCUS_LEFT && direction != View.FOCUS_RIGHT)  {
+            throw new IllegalArgumentException("direction must be one of"
+                    + " {View.FOCUS_LEFT, View.FOCUS_RIGHT}");
+        }
+
+        final int numChildren = getChildCount();
+        if (mItemsCanFocus && numChildren > 0 && mSelectedPosition != INVALID_POSITION) {
+            final View selectedView = getSelectedView();
+            if (selectedView != null && selectedView.hasFocus() &&
+                    selectedView instanceof ViewGroup) {
+
+                final View currentFocus = selectedView.findFocus();
+                final View nextFocus = FocusFinder.getInstance().findNextFocus(
+                        (ViewGroup) selectedView, currentFocus, direction);
+                if (nextFocus != null) {
+                    // do the math to get interesting rect in next focus' coordinates
+                    Rect focusedRect = mTempRect;
+                    if (currentFocus != null) {
+                        currentFocus.getFocusedRect(focusedRect);
+                        offsetDescendantRectToMyCoords(currentFocus, focusedRect);
+                        offsetRectIntoDescendantCoords(nextFocus, focusedRect);
+                    } else {
+                        focusedRect = null;
+                    }
+                    if (nextFocus.requestFocus(direction, focusedRect)) {
+                        return true;
+                    }
+                }
+                // we are blocking the key from being handled (by returning true)
+                // if the global result is going to be some other view within this
+                // list.  this is to acheive the overall goal of having
+                // horizontal d-pad navigation remain in the current item.
+                final View globalNextFocus = FocusFinder.getInstance().findNextFocus(
+                        (ViewGroup) getRootView(), currentFocus, direction);
+                if (globalNextFocus != null) {
+                    return isViewAncestorOf(globalNextFocus, this);
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Scrolls to the next or previous item if possible.
+     *
+     * @param direction either {@link View#FOCUS_UP} or {@link View#FOCUS_DOWN}
+     *
+     * @return whether selection was moved
+     */
+    boolean arrowScroll(int direction) {
+        try {
+            mInLayout = true;
+            final boolean handled = arrowScrollImpl(direction);
+            if (handled) {
+                playSoundEffect(SoundEffectConstants.getContantForFocusDirection(direction));
+            }
+            return handled;
+        } finally {
+            mInLayout = false;
+        }
+    }
+
+    /**
+     * Used by {@link #arrowScrollImpl(int)} to help determine the next selected position
+     * to move to. This return a position in the direction given if the selected item
+     * is fully visible.
+     *
+     * @param selectedView Current selected view to move from
+     * @param selectedPos Current selected position to move from
+     * @param direction Direction to move in
+     * @return Desired selected position after moving in the given direction
+     */
+    private final int nextSelectedPositionForDirection(
+            View selectedView, int selectedPos, int direction) {
+        int nextSelected;
+
+        if (direction == View.FOCUS_DOWN) {
+            final int listBottom = getHeight() - mListPadding.bottom;
+            if (selectedView != null && selectedView.getBottom() <= listBottom) {
+                nextSelected = selectedPos != INVALID_POSITION && selectedPos >= mFirstPosition ?
+                        selectedPos + 1 :
+                        mFirstPosition;
+            } else {
+                return INVALID_POSITION;
+            }
+        } else {
+            final int listTop = mListPadding.top;
+            if (selectedView != null && selectedView.getTop() >= listTop) {
+                final int lastPos = mFirstPosition + getChildCount() - 1;
+                nextSelected = selectedPos != INVALID_POSITION && selectedPos <= lastPos ?
+                        selectedPos - 1 :
+                        lastPos;
+            } else {
+                return INVALID_POSITION;
+            }
+        }
+
+        if (nextSelected < 0 || nextSelected >= mAdapter.getCount()) {
+            return INVALID_POSITION;
+        }
+        return lookForSelectablePosition(nextSelected, direction == View.FOCUS_DOWN);
+    }
+
+    /**
+     * Handle an arrow scroll going up or down.  Take into account whether items are selectable,
+     * whether there are focusable items etc.
+     *
+     * @param direction Either {@link android.view.View#FOCUS_UP} or {@link android.view.View#FOCUS_DOWN}.
+     * @return Whether any scrolling, selection or focus change occured.
+     */
+    private boolean arrowScrollImpl(int direction) {
+        if (getChildCount() <= 0) {
+            return false;
+        }
+
+        View selectedView = getSelectedView();
+        int selectedPos = mSelectedPosition;
+
+        int nextSelectedPosition = nextSelectedPositionForDirection(selectedView, selectedPos, direction);
+        int amountToScroll = amountToScroll(direction, nextSelectedPosition);
+
+        // if we are moving focus, we may OVERRIDE the default behavior
+        final ArrowScrollFocusResult focusResult = mItemsCanFocus ? arrowScrollFocused(direction) : null;
+        if (focusResult != null) {
+            nextSelectedPosition = focusResult.getSelectedPosition();
+            amountToScroll = focusResult.getAmountToScroll();
+        }
+
+        boolean needToRedraw = focusResult != null;
+        if (nextSelectedPosition != INVALID_POSITION) {
+            handleNewSelectionChange(selectedView, direction, nextSelectedPosition, focusResult != null);
+            setSelectedPositionInt(nextSelectedPosition);
+            setNextSelectedPositionInt(nextSelectedPosition);
+            selectedView = getSelectedView();
+            selectedPos = nextSelectedPosition;
+            if (mItemsCanFocus && focusResult == null) {
+                // there was no new view found to take focus, make sure we
+                // don't leave focus with the old selection
+                final View focused = getFocusedChild();
+                if (focused != null) {
+                    focused.clearFocus();
+                }
+            }
+            needToRedraw = true;
+            checkSelectionChanged();
+        }
+
+        if (amountToScroll > 0) {
+            scrollListItemsBy((direction == View.FOCUS_UP) ? amountToScroll : -amountToScroll);
+            needToRedraw = true;
+        }
+
+        // if we didn't find a new focusable, make sure any existing focused
+        // item that was panned off screen gives up focus.
+        if (mItemsCanFocus && (focusResult == null)
+                && selectedView != null && selectedView.hasFocus()) {
+            final View focused = selectedView.findFocus();
+            if (focused != null) {
+                if (!isViewAncestorOf(focused, this) || distanceToView(focused) > 0) {
+                    focused.clearFocus();
+                }
+            }
+        }
+
+        // if  the current selection is panned off, we need to remove the selection
+        if (nextSelectedPosition == INVALID_POSITION && selectedView != null
+                && !isViewAncestorOf(selectedView, this)) {
+            selectedView = null;
+            hideSelector();
+
+            // but we don't want to set the ressurect position (that would make subsequent
+            // unhandled key events bring back the item we just scrolled off!)
+            mResurrectToPosition = INVALID_POSITION;
+        }
+
+        if (needToRedraw) {
+            if (selectedView != null) {
+                positionSelectorLikeFocus(selectedPos, selectedView);
+                mSelectedTop = selectedView.getTop();
+            }
+            if (!awakenScrollBars()) {
+                invalidate();
+            }
+            invokeOnItemScrollListener();
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * When selection changes, it is possible that the previously selected or the
+     * next selected item will change its size.  If so, we need to offset some folks,
+     * and re-layout the items as appropriate.
+     *
+     * @param selectedView The currently selected view (before changing selection).
+     *   should be <code>null</code> if there was no previous selection.
+     * @param direction Either {@link android.view.View#FOCUS_UP} or
+     *        {@link android.view.View#FOCUS_DOWN}.
+     * @param newSelectedPosition The position of the next selection.
+     * @param newFocusAssigned whether new focus was assigned.  This matters because
+     *        when something has focus, we don't want to show selection (ugh).
+     */
+    private void handleNewSelectionChange(View selectedView, int direction, int newSelectedPosition,
+            boolean newFocusAssigned) {
+        if (newSelectedPosition == INVALID_POSITION) {
+            throw new IllegalArgumentException("newSelectedPosition needs to be valid");
+        }
+
+        // whether or not we are moving down or up, we want to preserve the
+        // top of whatever view is on top:
+        // - moving down: the view that had selection
+        // - moving up: the view that is getting selection
+        View topView;
+        View bottomView;
+        int topViewIndex, bottomViewIndex;
+        boolean topSelected = false;
+        final int selectedIndex = mSelectedPosition - mFirstPosition;
+        final int nextSelectedIndex = newSelectedPosition - mFirstPosition;
+        if (direction == View.FOCUS_UP) {
+            topViewIndex = nextSelectedIndex;
+            bottomViewIndex = selectedIndex;
+            topView = getChildAt(topViewIndex);
+            bottomView = selectedView;
+            topSelected = true;
+        } else {
+            topViewIndex = selectedIndex;
+            bottomViewIndex = nextSelectedIndex;
+            topView = selectedView;
+            bottomView = getChildAt(bottomViewIndex);
+        }
+
+        final int numChildren = getChildCount();
+
+        // start with top view: is it changing size?
+        if (topView != null) {
+            topView.setSelected(!newFocusAssigned && topSelected);
+            measureAndAdjustDown(topView, topViewIndex, numChildren);
+        }
+
+        // is the bottom view changing size?
+        if (bottomView != null) {
+            bottomView.setSelected(!newFocusAssigned && !topSelected);
+            measureAndAdjustDown(bottomView, bottomViewIndex, numChildren);
+        }
+    }
+
+    /**
+     * Re-measure a child, and if its height changes, lay it out preserving its
+     * top, and adjust the children below it appropriately.
+     * @param child The child
+     * @param childIndex The view group index of the child.
+     * @param numChildren The number of children in the view group.
+     */
+    private void measureAndAdjustDown(View child, int childIndex, int numChildren) {
+        int oldHeight = child.getHeight();
+        measureItem(child);
+        if (child.getMeasuredHeight() != oldHeight) {
+            // lay out the view, preserving its top
+            relayoutMeasuredItem(child);
+
+            // adjust views below appropriately
+            final int heightDelta = child.getMeasuredHeight() - oldHeight;
+            for (int i = childIndex + 1; i < numChildren; i++) {
+                getChildAt(i).offsetTopAndBottom(heightDelta);
+            }
+        }
+    }
+
+    /**
+     * Measure a particular list child.
+     * TODO: unify with setUpChild.
+     * @param child The child.
+     */
+    private void measureItem(View child) {
+        ViewGroup.LayoutParams p = child.getLayoutParams();
+        if (p == null) {
+            p = new ViewGroup.LayoutParams(
+                    ViewGroup.LayoutParams.MATCH_PARENT,
+                    ViewGroup.LayoutParams.WRAP_CONTENT);
+        }
+
+        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+                mListPadding.left + mListPadding.right, p.width);
+        int lpHeight = p.height;
+        int childHeightSpec;
+        if (lpHeight > 0) {
+            childHeightSpec = MeasureSpec.makeMeasureSpec(lpHeight, MeasureSpec.EXACTLY);
+        } else {
+            childHeightSpec = MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(),
+                    MeasureSpec.UNSPECIFIED);
+        }
+        child.measure(childWidthSpec, childHeightSpec);
+    }
+
+    /**
+     * Layout a child that has been measured, preserving its top position.
+     * TODO: unify with setUpChild.
+     * @param child The child.
+     */
+    private void relayoutMeasuredItem(View child) {
+        final int w = child.getMeasuredWidth();
+        final int h = child.getMeasuredHeight();
+        final int childLeft = mListPadding.left;
+        final int childRight = childLeft + w;
+        final int childTop = child.getTop();
+        final int childBottom = childTop + h;
+        child.layout(childLeft, childTop, childRight, childBottom);
+    }
+
+    /**
+     * @return The amount to preview next items when arrow srolling.
+     */
+    private int getArrowScrollPreviewLength() {
+        return Math.max(MIN_SCROLL_PREVIEW_PIXELS, getVerticalFadingEdgeLength());
+    }
+
+    /**
+     * Determine how much we need to scroll in order to get the next selected view
+     * visible, with a fading edge showing below as applicable.  The amount is
+     * capped at {@link #getMaxScrollAmount()} .
+     *
+     * @param direction either {@link android.view.View#FOCUS_UP} or
+     *        {@link android.view.View#FOCUS_DOWN}.
+     * @param nextSelectedPosition The position of the next selection, or
+     *        {@link #INVALID_POSITION} if there is no next selectable position
+     * @return The amount to scroll. Note: this is always positive!  Direction
+     *         needs to be taken into account when actually scrolling.
+     */
+    private int amountToScroll(int direction, int nextSelectedPosition) {
+        final int listBottom = getHeight() - mListPadding.bottom;
+        final int listTop = mListPadding.top;
+
+        int numChildren = getChildCount();
+
+        if (direction == View.FOCUS_DOWN) {
+            int indexToMakeVisible = numChildren - 1;
+            if (nextSelectedPosition != INVALID_POSITION) {
+                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+            }
+            while (numChildren <= indexToMakeVisible) {
+                // Child to view is not attached yet.
+                addViewBelow(getChildAt(numChildren - 1), mFirstPosition + numChildren - 1);
+                numChildren++;
+            }
+            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
+            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
+
+            int goalBottom = listBottom;
+            if (positionToMakeVisible < mItemCount - 1) {
+                goalBottom -= getArrowScrollPreviewLength();
+            }
+
+            if (viewToMakeVisible.getBottom() <= goalBottom) {
+                // item is fully visible.
+                return 0;
+            }
+
+            if (nextSelectedPosition != INVALID_POSITION
+                    && (goalBottom - viewToMakeVisible.getTop()) >= getMaxScrollAmount()) {
+                // item already has enough of it visible, changing selection is good enough
+                return 0;
+            }
+
+            int amountToScroll = (viewToMakeVisible.getBottom() - goalBottom);
+
+            if ((mFirstPosition + numChildren) == mItemCount) {
+                // last is last in list -> make sure we don't scroll past it
+                final int max = getChildAt(numChildren - 1).getBottom() - listBottom;
+                amountToScroll = Math.min(amountToScroll, max);
+            }
+
+            return Math.min(amountToScroll, getMaxScrollAmount());
+        } else {
+            int indexToMakeVisible = 0;
+            if (nextSelectedPosition != INVALID_POSITION) {
+                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+            }
+            while (indexToMakeVisible < 0) {
+                // Child to view is not attached yet.
+                addViewAbove(getChildAt(0), mFirstPosition);
+                mFirstPosition--;
+                indexToMakeVisible = nextSelectedPosition - mFirstPosition;
+            }
+            final int positionToMakeVisible = mFirstPosition + indexToMakeVisible;
+            final View viewToMakeVisible = getChildAt(indexToMakeVisible);
+            int goalTop = listTop;
+            if (positionToMakeVisible > 0) {
+                goalTop += getArrowScrollPreviewLength();
+            }
+            if (viewToMakeVisible.getTop() >= goalTop) {
+                // item is fully visible.
+                return 0;
+            }
+
+            if (nextSelectedPosition != INVALID_POSITION &&
+                    (viewToMakeVisible.getBottom() - goalTop) >= getMaxScrollAmount()) {
+                // item already has enough of it visible, changing selection is good enough
+                return 0;
+            }
+
+            int amountToScroll = (goalTop - viewToMakeVisible.getTop());
+            if (mFirstPosition == 0) {
+                // first is first in list -> make sure we don't scroll past it
+                final int max = listTop - getChildAt(0).getTop();
+                amountToScroll = Math.min(amountToScroll,  max);
+            }
+            return Math.min(amountToScroll, getMaxScrollAmount());
+        }
+    }
+
+    /**
+     * Holds results of focus aware arrow scrolling.
+     */
+    static private class ArrowScrollFocusResult {
+        private int mSelectedPosition;
+        private int mAmountToScroll;
+
+        /**
+         * How {@link android.widget.ListView#arrowScrollFocused} returns its values.
+         */
+        void populate(int selectedPosition, int amountToScroll) {
+            mSelectedPosition = selectedPosition;
+            mAmountToScroll = amountToScroll;
+        }
+
+        public int getSelectedPosition() {
+            return mSelectedPosition;
+        }
+
+        public int getAmountToScroll() {
+            return mAmountToScroll;
+        }
+    }
+
+    /**
+     * @param direction either {@link android.view.View#FOCUS_UP} or
+     *        {@link android.view.View#FOCUS_DOWN}.
+     * @return The position of the next selectable position of the views that
+     *         are currently visible, taking into account the fact that there might
+     *         be no selection.  Returns {@link #INVALID_POSITION} if there is no
+     *         selectable view on screen in the given direction.
+     */
+    private int lookForSelectablePositionOnScreen(int direction) {
+        final int firstPosition = mFirstPosition;
+        if (direction == View.FOCUS_DOWN) {
+            int startPos = (mSelectedPosition != INVALID_POSITION) ?
+                    mSelectedPosition + 1 :
+                    firstPosition;
+            if (startPos >= mAdapter.getCount()) {
+                return INVALID_POSITION;
+            }
+            if (startPos < firstPosition) {
+                startPos = firstPosition;
+            }
+
+            final int lastVisiblePos = getLastVisiblePosition();
+            final ListAdapter adapter = getAdapter();
+            for (int pos = startPos; pos <= lastVisiblePos; pos++) {
+                if (adapter.isEnabled(pos)
+                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
+                    return pos;
+                }
+            }
+        } else {
+            int last = firstPosition + getChildCount() - 1;
+            int startPos = (mSelectedPosition != INVALID_POSITION) ?
+                    mSelectedPosition - 1 :
+                    firstPosition + getChildCount() - 1;
+            if (startPos < 0 || startPos >= mAdapter.getCount()) {
+                return INVALID_POSITION;
+            }
+            if (startPos > last) {
+                startPos = last;
+            }
+
+            final ListAdapter adapter = getAdapter();
+            for (int pos = startPos; pos >= firstPosition; pos--) {
+                if (adapter.isEnabled(pos)
+                        && getChildAt(pos - firstPosition).getVisibility() == View.VISIBLE) {
+                    return pos;
+                }
+            }
+        }
+        return INVALID_POSITION;
+    }
+
+    /**
+     * Do an arrow scroll based on focus searching.  If a new view is
+     * given focus, return the selection delta and amount to scroll via
+     * an {@link ArrowScrollFocusResult}, otherwise, return null.
+     *
+     * @param direction either {@link android.view.View#FOCUS_UP} or
+     *        {@link android.view.View#FOCUS_DOWN}.
+     * @return The result if focus has changed, or <code>null</code>.
+     */
+    private ArrowScrollFocusResult arrowScrollFocused(final int direction) {
+        final View selectedView = getSelectedView();
+        View newFocus;
+        if (selectedView != null && selectedView.hasFocus()) {
+            View oldFocus = selectedView.findFocus();
+            newFocus = FocusFinder.getInstance().findNextFocus(this, oldFocus, direction);
+        } else {
+            if (direction == View.FOCUS_DOWN) {
+                final boolean topFadingEdgeShowing = (mFirstPosition > 0);
+                final int listTop = mListPadding.top +
+                        (topFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
+                final int ySearchPoint =
+                        (selectedView != null && selectedView.getTop() > listTop) ?
+                                selectedView.getTop() :
+                                listTop;
+                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
+            } else {
+                final boolean bottomFadingEdgeShowing =
+                        (mFirstPosition + getChildCount() - 1) < mItemCount;
+                final int listBottom = getHeight() - mListPadding.bottom -
+                        (bottomFadingEdgeShowing ? getArrowScrollPreviewLength() : 0);
+                final int ySearchPoint =
+                        (selectedView != null && selectedView.getBottom() < listBottom) ?
+                                selectedView.getBottom() :
+                                listBottom;
+                mTempRect.set(0, ySearchPoint, 0, ySearchPoint);
+            }
+            newFocus = FocusFinder.getInstance().findNextFocusFromRect(this, mTempRect, direction);
+        }
+
+        if (newFocus != null) {
+            final int positionOfNewFocus = positionOfNewFocus(newFocus);
+
+            // if the focus change is in a different new position, make sure
+            // we aren't jumping over another selectable position
+            if (mSelectedPosition != INVALID_POSITION && positionOfNewFocus != mSelectedPosition) {
+                final int selectablePosition = lookForSelectablePositionOnScreen(direction);
+                if (selectablePosition != INVALID_POSITION &&
+                        ((direction == View.FOCUS_DOWN && selectablePosition < positionOfNewFocus) ||
+                        (direction == View.FOCUS_UP && selectablePosition > positionOfNewFocus))) {
+                    return null;
+                }
+            }
+
+            int focusScroll = amountToScrollToNewFocus(direction, newFocus, positionOfNewFocus);
+
+            final int maxScrollAmount = getMaxScrollAmount();
+            if (focusScroll < maxScrollAmount) {
+                // not moving too far, safe to give next view focus
+                newFocus.requestFocus(direction);
+                mArrowScrollFocusResult.populate(positionOfNewFocus, focusScroll);
+                return mArrowScrollFocusResult;
+            } else if (distanceToView(newFocus) < maxScrollAmount){
+                // Case to consider:
+                // too far to get entire next focusable on screen, but by going
+                // max scroll amount, we are getting it at least partially in view,
+                // so give it focus and scroll the max ammount.
+                newFocus.requestFocus(direction);
+                mArrowScrollFocusResult.populate(positionOfNewFocus, maxScrollAmount);
+                return mArrowScrollFocusResult;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @param newFocus The view that would have focus.
+     * @return the position that contains newFocus
+     */
+    private int positionOfNewFocus(View newFocus) {
+        final int numChildren = getChildCount();
+        for (int i = 0; i < numChildren; i++) {
+            final View child = getChildAt(i);
+            if (isViewAncestorOf(newFocus, child)) {
+                return mFirstPosition + i;
+            }
+        }
+        throw new IllegalArgumentException("newFocus is not a child of any of the"
+                + " children of the list!");
+    }
+
+    /**
+     * Return true if child is an ancestor of parent, (or equal to the parent).
+     */
+    private boolean isViewAncestorOf(View child, View parent) {
+        if (child == parent) {
+            return true;
+        }
+
+        final ViewParent theParent = child.getParent();
+        return (theParent instanceof ViewGroup) && isViewAncestorOf((View) theParent, parent);
+    }
+
+    /**
+     * Determine how much we need to scroll in order to get newFocus in view.
+     * @param direction either {@link android.view.View#FOCUS_UP} or
+     *        {@link android.view.View#FOCUS_DOWN}.
+     * @param newFocus The view that would take focus.
+     * @param positionOfNewFocus The position of the list item containing newFocus
+     * @return The amount to scroll.  Note: this is always positive!  Direction
+     *   needs to be taken into account when actually scrolling.
+     */
+    private int amountToScrollToNewFocus(int direction, View newFocus, int positionOfNewFocus) {
+        int amountToScroll = 0;
+        newFocus.getDrawingRect(mTempRect);
+        offsetDescendantRectToMyCoords(newFocus, mTempRect);
+        if (direction == View.FOCUS_UP) {
+            if (mTempRect.top < mListPadding.top) {
+                amountToScroll = mListPadding.top - mTempRect.top;
+                if (positionOfNewFocus > 0) {
+                    amountToScroll += getArrowScrollPreviewLength();
+                }
+            }
+        } else {
+            final int listBottom = getHeight() - mListPadding.bottom;
+            if (mTempRect.bottom > listBottom) {
+                amountToScroll = mTempRect.bottom - listBottom;
+                if (positionOfNewFocus < mItemCount - 1) {
+                    amountToScroll += getArrowScrollPreviewLength();
+                }
+            }
+        }
+        return amountToScroll;
+    }
+
+    /**
+     * Determine the distance to the nearest edge of a view in a particular
+     * direction.
+     *
+     * @param descendant A descendant of this list.
+     * @return The distance, or 0 if the nearest edge is already on screen.
+     */
+    private int distanceToView(View descendant) {
+        int distance = 0;
+        descendant.getDrawingRect(mTempRect);
+        offsetDescendantRectToMyCoords(descendant, mTempRect);
+        final int listBottom = mBottom - mTop - mListPadding.bottom;
+        if (mTempRect.bottom < mListPadding.top) {
+            distance = mListPadding.top - mTempRect.bottom;
+        } else if (mTempRect.top > listBottom) {
+            distance = mTempRect.top - listBottom;
+        }
+        return distance;
+    }
+
+
+    /**
+     * Scroll the children by amount, adding a view at the end and removing
+     * views that fall off as necessary.
+     *
+     * @param amount The amount (positive or negative) to scroll.
+     */
+    private void scrollListItemsBy(int amount) {
+        offsetChildrenTopAndBottom(amount);
+
+        final int listBottom = getHeight() - mListPadding.bottom;
+        final int listTop = mListPadding.top;
+        final AbsListView.RecycleBin recycleBin = mRecycler;
+
+        if (amount < 0) {
+            // shifted items up
+
+            // may need to pan views into the bottom space
+            int numChildren = getChildCount();
+            View last = getChildAt(numChildren - 1);
+            while (last.getBottom() < listBottom) {
+                final int lastVisiblePosition = mFirstPosition + numChildren - 1;
+                if (lastVisiblePosition < mItemCount - 1) {
+                    last = addViewBelow(last, lastVisiblePosition);
+                    numChildren++;
+                } else {
+                    break;
+                }
+            }
+
+            // may have brought in the last child of the list that is skinnier
+            // than the fading edge, thereby leaving space at the end.  need
+            // to shift back
+            if (last.getBottom() < listBottom) {
+                offsetChildrenTopAndBottom(listBottom - last.getBottom());
+            }
+
+            // top views may be panned off screen
+            View first = getChildAt(0);
+            while (first.getBottom() < listTop) {
+                AbsListView.LayoutParams layoutParams = (LayoutParams) first.getLayoutParams();
+                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
+                    recycleBin.addScrapView(first, mFirstPosition);
+                }
+                detachViewFromParent(first);
+                first = getChildAt(0);
+                mFirstPosition++;
+            }
+        } else {
+            // shifted items down
+            View first = getChildAt(0);
+
+            // may need to pan views into top
+            while ((first.getTop() > listTop) && (mFirstPosition > 0)) {
+                first = addViewAbove(first, mFirstPosition);
+                mFirstPosition--;
+            }
+
+            // may have brought the very first child of the list in too far and
+            // need to shift it back
+            if (first.getTop() > listTop) {
+                offsetChildrenTopAndBottom(listTop - first.getTop());
+            }
+
+            int lastIndex = getChildCount() - 1;
+            View last = getChildAt(lastIndex);
+
+            // bottom view may be panned off screen
+            while (last.getTop() > listBottom) {
+                AbsListView.LayoutParams layoutParams = (LayoutParams) last.getLayoutParams();
+                if (recycleBin.shouldRecycleViewType(layoutParams.viewType)) {
+                    recycleBin.addScrapView(last, mFirstPosition+lastIndex);
+                }
+                detachViewFromParent(last);
+                last = getChildAt(--lastIndex);
+            }
+        }
+        recycleBin.fullyDetachScrapViews();
+        removeUnusedFixedViews(mHeaderViewInfos);
+        removeUnusedFixedViews(mFooterViewInfos);
+    }
+
+    private View addViewAbove(View theView, int position) {
+        int abovePosition = position - 1;
+        View view = obtainView(abovePosition, mIsScrap);
+        int edgeOfNewChild = theView.getTop() - mDividerHeight;
+        setupChild(view, abovePosition, edgeOfNewChild, false, mListPadding.left,
+                false, mIsScrap[0]);
+        return view;
+    }
+
+    private View addViewBelow(View theView, int position) {
+        int belowPosition = position + 1;
+        View view = obtainView(belowPosition, mIsScrap);
+        int edgeOfNewChild = theView.getBottom() + mDividerHeight;
+        setupChild(view, belowPosition, edgeOfNewChild, true, mListPadding.left,
+                false, mIsScrap[0]);
+        return view;
+    }
+
+    /**
+     * Indicates that the views created by the ListAdapter can contain focusable
+     * items.
+     *
+     * @param itemsCanFocus true if items can get focus, false otherwise
+     */
+    public void setItemsCanFocus(boolean itemsCanFocus) {
+        mItemsCanFocus = itemsCanFocus;
+        if (!itemsCanFocus) {
+            setDescendantFocusability(ViewGroup.FOCUS_BLOCK_DESCENDANTS);
+        }
+    }
+
+    /**
+     * @return Whether the views created by the ListAdapter can contain focusable
+     * items.
+     */
+    public boolean getItemsCanFocus() {
+        return mItemsCanFocus;
+    }
+
+    @Override
+    public boolean isOpaque() {
+        boolean retValue = (mCachingActive && mIsCacheColorOpaque && mDividerIsOpaque &&
+                hasOpaqueScrollbars()) || super.isOpaque();
+        if (retValue) {
+            // only return true if the list items cover the entire area of the view
+            final int listTop = mListPadding != null ? mListPadding.top : mPaddingTop;
+            View first = getChildAt(0);
+            if (first == null || first.getTop() > listTop) {
+                return false;
+            }
+            final int listBottom = getHeight() -
+                    (mListPadding != null ? mListPadding.bottom : mPaddingBottom);
+            View last = getChildAt(getChildCount() - 1);
+            if (last == null || last.getBottom() < listBottom) {
+                return false;
+            }
+        }
+        return retValue;
+    }
+
+    @Override
+    public void setCacheColorHint(int color) {
+        final boolean opaque = (color >>> 24) == 0xFF;
+        mIsCacheColorOpaque = opaque;
+        if (opaque) {
+            if (mDividerPaint == null) {
+                mDividerPaint = new Paint();
+            }
+            mDividerPaint.setColor(color);
+        }
+        super.setCacheColorHint(color);
+    }
+
+    void drawOverscrollHeader(Canvas canvas, Drawable drawable, Rect bounds) {
+        final int height = drawable.getMinimumHeight();
+
+        canvas.save();
+        canvas.clipRect(bounds);
+
+        final int span = bounds.bottom - bounds.top;
+        if (span < height) {
+            bounds.top = bounds.bottom - height;
+        }
+
+        drawable.setBounds(bounds);
+        drawable.draw(canvas);
+
+        canvas.restore();
+    }
+
+    void drawOverscrollFooter(Canvas canvas, Drawable drawable, Rect bounds) {
+        final int height = drawable.getMinimumHeight();
+
+        canvas.save();
+        canvas.clipRect(bounds);
+
+        final int span = bounds.bottom - bounds.top;
+        if (span < height) {
+            bounds.bottom = bounds.top + height;
+        }
+
+        drawable.setBounds(bounds);
+        drawable.draw(canvas);
+
+        canvas.restore();
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        if (mCachingStarted) {
+            mCachingActive = true;
+        }
+
+        // Draw the dividers
+        final int dividerHeight = mDividerHeight;
+        final Drawable overscrollHeader = mOverScrollHeader;
+        final Drawable overscrollFooter = mOverScrollFooter;
+        final boolean drawOverscrollHeader = overscrollHeader != null;
+        final boolean drawOverscrollFooter = overscrollFooter != null;
+        final boolean drawDividers = dividerHeight > 0 && mDivider != null;
+
+        if (drawDividers || drawOverscrollHeader || drawOverscrollFooter) {
+            // Only modify the top and bottom in the loop, we set the left and right here
+            final Rect bounds = mTempRect;
+            bounds.left = mPaddingLeft;
+            bounds.right = mRight - mLeft - mPaddingRight;
+
+            final int count = getChildCount();
+            final int headerCount = getHeaderViewsCount();
+            final int itemCount = mItemCount;
+            final int footerLimit = (itemCount - mFooterViewInfos.size());
+            final boolean headerDividers = mHeaderDividersEnabled;
+            final boolean footerDividers = mFooterDividersEnabled;
+            final int first = mFirstPosition;
+            final boolean areAllItemsSelectable = mAreAllItemsSelectable;
+            final ListAdapter adapter = mAdapter;
+            // If the list is opaque *and* the background is not, we want to
+            // fill a rect where the dividers would be for non-selectable items
+            // If the list is opaque and the background is also opaque, we don't
+            // need to draw anything since the background will do it for us
+            final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
+
+            if (fillForMissingDividers && mDividerPaint == null && mIsCacheColorOpaque) {
+                mDividerPaint = new Paint();
+                mDividerPaint.setColor(getCacheColorHint());
+            }
+            final Paint paint = mDividerPaint;
+
+            int effectivePaddingTop = 0;
+            int effectivePaddingBottom = 0;
+            if ((mGroupFlags & CLIP_TO_PADDING_MASK) == CLIP_TO_PADDING_MASK) {
+                effectivePaddingTop = mListPadding.top;
+                effectivePaddingBottom = mListPadding.bottom;
+            }
+
+            final int listBottom = mBottom - mTop - effectivePaddingBottom + mScrollY;
+            if (!mStackFromBottom) {
+                int bottom = 0;
+
+                // Draw top divider or header for overscroll
+                final int scrollY = mScrollY;
+                if (count > 0 && scrollY < 0) {
+                    if (drawOverscrollHeader) {
+                        bounds.bottom = 0;
+                        bounds.top = scrollY;
+                        drawOverscrollHeader(canvas, overscrollHeader, bounds);
+                    } else if (drawDividers) {
+                        bounds.bottom = 0;
+                        bounds.top = -dividerHeight;
+                        drawDivider(canvas, bounds, -1);
+                    }
+                }
+
+                for (int i = 0; i < count; i++) {
+                    final int itemIndex = (first + i);
+                    final boolean isHeader = (itemIndex < headerCount);
+                    final boolean isFooter = (itemIndex >= footerLimit);
+                    if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+                        final View child = getChildAt(i);
+                        bottom = child.getBottom();
+                        final boolean isLastItem = (i == (count - 1));
+
+                        if (drawDividers && (bottom < listBottom)
+                                && !(drawOverscrollFooter && isLastItem)) {
+                            final int nextIndex = (itemIndex + 1);
+                            // Draw dividers between enabled items, headers
+                            // and/or footers when enabled and requested, and
+                            // after the last enabled item.
+                            if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                    && (nextIndex >= headerCount)) && (isLastItem
+                                    || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
+                                            && (nextIndex < footerLimit)))) {
+                                bounds.top = bottom;
+                                bounds.bottom = bottom + dividerHeight;
+                                drawDivider(canvas, bounds, i);
+                            } else if (fillForMissingDividers) {
+                                bounds.top = bottom;
+                                bounds.bottom = bottom + dividerHeight;
+                                canvas.drawRect(bounds, paint);
+                            }
+                        }
+                    }
+                }
+
+                final int overFooterBottom = mBottom + mScrollY;
+                if (drawOverscrollFooter && first + count == itemCount &&
+                        overFooterBottom > bottom) {
+                    bounds.top = bottom;
+                    bounds.bottom = overFooterBottom;
+                    drawOverscrollFooter(canvas, overscrollFooter, bounds);
+                }
+            } else {
+                int top;
+
+                final int scrollY = mScrollY;
+
+                if (count > 0 && drawOverscrollHeader) {
+                    bounds.top = scrollY;
+                    bounds.bottom = getChildAt(0).getTop();
+                    drawOverscrollHeader(canvas, overscrollHeader, bounds);
+                }
+
+                final int start = drawOverscrollHeader ? 1 : 0;
+                for (int i = start; i < count; i++) {
+                    final int itemIndex = (first + i);
+                    final boolean isHeader = (itemIndex < headerCount);
+                    final boolean isFooter = (itemIndex >= footerLimit);
+                    if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+                        final View child = getChildAt(i);
+                        top = child.getTop();
+                        if (drawDividers && (top > effectivePaddingTop)) {
+                            final boolean isFirstItem = (i == start);
+                            final int previousIndex = (itemIndex - 1);
+                            // Draw dividers between enabled items, headers
+                            // and/or footers when enabled and requested, and
+                            // before the first enabled item.
+                            if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                    && (previousIndex >= headerCount)) && (isFirstItem ||
+                                    adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
+                                            && (previousIndex < footerLimit)))) {
+                                bounds.top = top - dividerHeight;
+                                bounds.bottom = top;
+                                // Give the method the child ABOVE the divider,
+                                // so we subtract one from our child position.
+                                // Give -1 when there is no child above the
+                                // divider.
+                                drawDivider(canvas, bounds, i - 1);
+                            } else if (fillForMissingDividers) {
+                                bounds.top = top - dividerHeight;
+                                bounds.bottom = top;
+                                canvas.drawRect(bounds, paint);
+                            }
+                        }
+                    }
+                }
+
+                if (count > 0 && scrollY > 0) {
+                    if (drawOverscrollFooter) {
+                        final int absListBottom = mBottom;
+                        bounds.top = absListBottom;
+                        bounds.bottom = absListBottom + scrollY;
+                        drawOverscrollFooter(canvas, overscrollFooter, bounds);
+                    } else if (drawDividers) {
+                        bounds.top = listBottom;
+                        bounds.bottom = listBottom + dividerHeight;
+                        drawDivider(canvas, bounds, -1);
+                    }
+                }
+            }
+        }
+
+        // Draw the indicators (these should be drawn above the dividers) and children
+        super.dispatchDraw(canvas);
+    }
+
+    @Override
+    protected boolean drawChild(Canvas canvas, View child, long drawingTime) {
+        boolean more = super.drawChild(canvas, child, drawingTime);
+        if (mCachingActive && child.mCachingFailed) {
+            mCachingActive = false;
+        }
+        return more;
+    }
+
+    /**
+     * Draws a divider for the given child in the given bounds.
+     *
+     * @param canvas The canvas to draw to.
+     * @param bounds The bounds of the divider.
+     * @param childIndex The index of child (of the View) above the divider.
+     *            This will be -1 if there is no child above the divider to be
+     *            drawn.
+     */
+    void drawDivider(Canvas canvas, Rect bounds, int childIndex) {
+        // This widget draws the same divider for all children
+        final Drawable divider = mDivider;
+
+        divider.setBounds(bounds);
+        divider.draw(canvas);
+    }
+
+    /**
+     * Returns the drawable that will be drawn between each item in the list.
+     *
+     * @return the current drawable drawn between list elements
+     * @attr ref R.styleable#ListView_divider
+     */
+    @Nullable
+    public Drawable getDivider() {
+        return mDivider;
+    }
+
+    /**
+     * Sets the drawable that will be drawn between each item in the list.
+     * <p>
+     * <strong>Note:</strong> If the drawable does not have an intrinsic
+     * height, you should also call {@link #setDividerHeight(int)}.
+     *
+     * @param divider the drawable to use
+     * @attr ref R.styleable#ListView_divider
+     */
+    public void setDivider(@Nullable Drawable divider) {
+        if (divider != null) {
+            mDividerHeight = divider.getIntrinsicHeight();
+        } else {
+            mDividerHeight = 0;
+        }
+        mDivider = divider;
+        mDividerIsOpaque = divider == null || divider.getOpacity() == PixelFormat.OPAQUE;
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * @return Returns the height of the divider that will be drawn between each item in the list.
+     */
+    public int getDividerHeight() {
+        return mDividerHeight;
+    }
+
+    /**
+     * Sets the height of the divider that will be drawn between each item in the list. Calling
+     * this will override the intrinsic height as set by {@link #setDivider(Drawable)}
+     *
+     * @param height The new height of the divider in pixels.
+     */
+    public void setDividerHeight(int height) {
+        mDividerHeight = height;
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Enables or disables the drawing of the divider for header views.
+     *
+     * @param headerDividersEnabled True to draw the headers, false otherwise.
+     *
+     * @see #setFooterDividersEnabled(boolean)
+     * @see #areHeaderDividersEnabled()
+     * @see #addHeaderView(android.view.View)
+     */
+    public void setHeaderDividersEnabled(boolean headerDividersEnabled) {
+        mHeaderDividersEnabled = headerDividersEnabled;
+        invalidate();
+    }
+
+    /**
+     * @return Whether the drawing of the divider for header views is enabled
+     *
+     * @see #setHeaderDividersEnabled(boolean)
+     */
+    public boolean areHeaderDividersEnabled() {
+        return mHeaderDividersEnabled;
+    }
+
+    /**
+     * Enables or disables the drawing of the divider for footer views.
+     *
+     * @param footerDividersEnabled True to draw the footers, false otherwise.
+     *
+     * @see #setHeaderDividersEnabled(boolean)
+     * @see #areFooterDividersEnabled()
+     * @see #addFooterView(android.view.View)
+     */
+    public void setFooterDividersEnabled(boolean footerDividersEnabled) {
+        mFooterDividersEnabled = footerDividersEnabled;
+        invalidate();
+    }
+
+    /**
+     * @return Whether the drawing of the divider for footer views is enabled
+     *
+     * @see #setFooterDividersEnabled(boolean)
+     */
+    public boolean areFooterDividersEnabled() {
+        return mFooterDividersEnabled;
+    }
+
+    /**
+     * Sets the drawable that will be drawn above all other list content.
+     * This area can become visible when the user overscrolls the list.
+     *
+     * @param header The drawable to use
+     */
+    public void setOverscrollHeader(Drawable header) {
+        mOverScrollHeader = header;
+        if (mScrollY < 0) {
+            invalidate();
+        }
+    }
+
+    /**
+     * @return The drawable that will be drawn above all other list content
+     */
+    public Drawable getOverscrollHeader() {
+        return mOverScrollHeader;
+    }
+
+    /**
+     * Sets the drawable that will be drawn below all other list content.
+     * This area can become visible when the user overscrolls the list,
+     * or when the list's content does not fully fill the container area.
+     *
+     * @param footer The drawable to use
+     */
+    public void setOverscrollFooter(Drawable footer) {
+        mOverScrollFooter = footer;
+        invalidate();
+    }
+
+    /**
+     * @return The drawable that will be drawn below all other list content
+     */
+    public Drawable getOverscrollFooter() {
+        return mOverScrollFooter;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, int direction, Rect previouslyFocusedRect) {
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+
+        final ListAdapter adapter = mAdapter;
+        int closetChildIndex = -1;
+        int closestChildTop = 0;
+        if (adapter != null && gainFocus && previouslyFocusedRect != null) {
+            previouslyFocusedRect.offset(mScrollX, mScrollY);
+
+            // Don't cache the result of getChildCount or mFirstPosition here,
+            // it could change in layoutChildren.
+            if (adapter.getCount() < getChildCount() + mFirstPosition) {
+                mLayoutMode = LAYOUT_NORMAL;
+                layoutChildren();
+            }
+
+            // figure out which item should be selected based on previously
+            // focused rect
+            Rect otherRect = mTempRect;
+            int minDistance = Integer.MAX_VALUE;
+            final int childCount = getChildCount();
+            final int firstPosition = mFirstPosition;
+
+            for (int i = 0; i < childCount; i++) {
+                // only consider selectable views
+                if (!adapter.isEnabled(firstPosition + i)) {
+                    continue;
+                }
+
+                View other = getChildAt(i);
+                other.getDrawingRect(otherRect);
+                offsetDescendantRectToMyCoords(other, otherRect);
+                int distance = getDistance(previouslyFocusedRect, otherRect, direction);
+
+                if (distance < minDistance) {
+                    minDistance = distance;
+                    closetChildIndex = i;
+                    closestChildTop = other.getTop();
+                }
+            }
+        }
+
+        if (closetChildIndex >= 0) {
+            setSelectionFromTop(closetChildIndex + mFirstPosition, closestChildTop);
+        } else {
+            requestLayout();
+        }
+    }
+
+
+    /*
+     * (non-Javadoc)
+     *
+     * Children specified in XML are assumed to be header views. After we have
+     * parsed them move them out of the children list and into mHeaderViews.
+     */
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        int count = getChildCount();
+        if (count > 0) {
+            for (int i = 0; i < count; ++i) {
+                addHeaderView(getChildAt(i));
+            }
+            removeAllViews();
+        }
+    }
+
+    /**
+     * @see android.view.View#findViewById(int)
+     * @removed For internal use only. This should have been hidden.
+     */
+    @Override
+    protected <T extends View> T findViewTraversal(@IdRes int id) {
+        // First look in our children, then in any header and footer views that
+        // may be scrolled off.
+        View v = super.findViewTraversal(id);
+        if (v == null) {
+            v = findViewInHeadersOrFooters(mHeaderViewInfos, id);
+            if (v != null) {
+                return (T) v;
+            }
+            v = findViewInHeadersOrFooters(mFooterViewInfos, id);
+            if (v != null) {
+                return (T) v;
+            }
+        }
+        return (T) v;
+    }
+
+    View findViewInHeadersOrFooters(ArrayList<FixedViewInfo> where, int id) {
+        // Look in the passed in list of headers or footers for the view.
+        if (where != null) {
+            int len = where.size();
+            View v;
+
+            for (int i = 0; i < len; i++) {
+                v = where.get(i).view;
+
+                if (!v.isRootNamespace()) {
+                    v = v.findViewById(id);
+
+                    if (v != null) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * @see android.view.View#findViewWithTag(Object)
+     * @removed For internal use only. This should have been hidden.
+     */
+    @Override
+    protected <T extends View> T findViewWithTagTraversal(Object tag) {
+        // First look in our children, then in any header and footer views that
+        // may be scrolled off.
+        View v = super.findViewWithTagTraversal(tag);
+        if (v == null) {
+            v = findViewWithTagInHeadersOrFooters(mHeaderViewInfos, tag);
+            if (v != null) {
+                return (T) v;
+            }
+
+            v = findViewWithTagInHeadersOrFooters(mFooterViewInfos, tag);
+            if (v != null) {
+                return (T) v;
+            }
+        }
+        return (T) v;
+    }
+
+    View findViewWithTagInHeadersOrFooters(ArrayList<FixedViewInfo> where, Object tag) {
+        // Look in the passed in list of headers or footers for the view with
+        // the tag.
+        if (where != null) {
+            int len = where.size();
+            View v;
+
+            for (int i = 0; i < len; i++) {
+                v = where.get(i).view;
+
+                if (!v.isRootNamespace()) {
+                    v = v.findViewWithTag(tag);
+
+                    if (v != null) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * First look in our children, then in any header and footer views that may
+     * be scrolled off.
+     *
+     * @see android.view.View#findViewByPredicate(Predicate)
+     * @hide
+     */
+    @Override
+    protected <T extends View> T findViewByPredicateTraversal(
+            Predicate<View> predicate, View childToSkip) {
+        View v = super.findViewByPredicateTraversal(predicate, childToSkip);
+        if (v == null) {
+            v = findViewByPredicateInHeadersOrFooters(mHeaderViewInfos, predicate, childToSkip);
+            if (v != null) {
+                return (T) v;
+            }
+
+            v = findViewByPredicateInHeadersOrFooters(mFooterViewInfos, predicate, childToSkip);
+            if (v != null) {
+                return (T) v;
+            }
+        }
+        return (T) v;
+    }
+
+    /**
+     * Look in the passed in list of headers or footers for the first view that
+     * matches the predicate.
+     */
+    View findViewByPredicateInHeadersOrFooters(ArrayList<FixedViewInfo> where,
+            Predicate<View> predicate, View childToSkip) {
+        if (where != null) {
+            int len = where.size();
+            View v;
+
+            for (int i = 0; i < len; i++) {
+                v = where.get(i).view;
+
+                if (v != childToSkip && !v.isRootNamespace()) {
+                    v = v.findViewByPredicate(predicate);
+
+                    if (v != null) {
+                        return v;
+                    }
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Returns the set of checked items ids. The result is only valid if the
+     * choice mode has not been set to {@link #CHOICE_MODE_NONE}.
+     *
+     * @return A new array which contains the id of each checked item in the
+     *         list.
+     *
+     * @deprecated Use {@link #getCheckedItemIds()} instead.
+     */
+    @Deprecated
+    public long[] getCheckItemIds() {
+        // Use new behavior that correctly handles stable ID mapping.
+        if (mAdapter != null && mAdapter.hasStableIds()) {
+            return getCheckedItemIds();
+        }
+
+        // Old behavior was buggy, but would sort of work for adapters without stable IDs.
+        // Fall back to it to support legacy apps.
+        if (mChoiceMode != CHOICE_MODE_NONE && mCheckStates != null && mAdapter != null) {
+            final SparseBooleanArray states = mCheckStates;
+            final int count = states.size();
+            final long[] ids = new long[count];
+            final ListAdapter adapter = mAdapter;
+
+            int checkedCount = 0;
+            for (int i = 0; i < count; i++) {
+                if (states.valueAt(i)) {
+                    ids[checkedCount++] = adapter.getItemId(states.keyAt(i));
+                }
+            }
+
+            // Trim array if needed. mCheckStates may contain false values
+            // resulting in checkedCount being smaller than count.
+            if (checkedCount == count) {
+                return ids;
+            } else {
+                final long[] result = new long[checkedCount];
+                System.arraycopy(ids, 0, result, 0, checkedCount);
+
+                return result;
+            }
+        }
+        return new long[0];
+    }
+
+    @Override
+    int getHeightForPosition(int position) {
+        final int height = super.getHeightForPosition(position);
+        if (shouldAdjustHeightForDivider(position)) {
+            return height + mDividerHeight;
+        }
+        return height;
+    }
+
+    private boolean shouldAdjustHeightForDivider(int itemIndex) {
+        final int dividerHeight = mDividerHeight;
+        final Drawable overscrollHeader = mOverScrollHeader;
+        final Drawable overscrollFooter = mOverScrollFooter;
+        final boolean drawOverscrollHeader = overscrollHeader != null;
+        final boolean drawOverscrollFooter = overscrollFooter != null;
+        final boolean drawDividers = dividerHeight > 0 && mDivider != null;
+
+        if (drawDividers) {
+            final boolean fillForMissingDividers = isOpaque() && !super.isOpaque();
+            final int itemCount = mItemCount;
+            final int headerCount = getHeaderViewsCount();
+            final int footerLimit = (itemCount - mFooterViewInfos.size());
+            final boolean isHeader = (itemIndex < headerCount);
+            final boolean isFooter = (itemIndex >= footerLimit);
+            final boolean headerDividers = mHeaderDividersEnabled;
+            final boolean footerDividers = mFooterDividersEnabled;
+            if ((headerDividers || !isHeader) && (footerDividers || !isFooter)) {
+                final ListAdapter adapter = mAdapter;
+                if (!mStackFromBottom) {
+                    final boolean isLastItem = (itemIndex == (itemCount - 1));
+                    if (!drawOverscrollFooter || !isLastItem) {
+                        final int nextIndex = itemIndex + 1;
+                        // Draw dividers between enabled items, headers
+                        // and/or footers when enabled and requested, and
+                        // after the last enabled item.
+                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                && (nextIndex >= headerCount)) && (isLastItem
+                                || adapter.isEnabled(nextIndex) && (footerDividers || !isFooter
+                                                && (nextIndex < footerLimit)))) {
+                            return true;
+                        } else if (fillForMissingDividers) {
+                            return true;
+                        }
+                    }
+                } else {
+                    final int start = drawOverscrollHeader ? 1 : 0;
+                    final boolean isFirstItem = (itemIndex == start);
+                    if (!isFirstItem) {
+                        final int previousIndex = (itemIndex - 1);
+                        // Draw dividers between enabled items, headers
+                        // and/or footers when enabled and requested, and
+                        // before the first enabled item.
+                        if (adapter.isEnabled(itemIndex) && (headerDividers || !isHeader
+                                && (previousIndex >= headerCount)) && (isFirstItem ||
+                                adapter.isEnabled(previousIndex) && (footerDividers || !isFooter
+                                        && (previousIndex < footerLimit)))) {
+                            return true;
+                        } else if (fillForMissingDividers) {
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ListView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        final int rowsCount = getCount();
+        final int selectionMode = getSelectionModeForAccessibility();
+        final CollectionInfo collectionInfo = CollectionInfo.obtain(
+                rowsCount, 1, false, selectionMode);
+        info.setCollectionInfo(collectionInfo);
+
+        if (rowsCount > 0) {
+            info.addAction(AccessibilityAction.ACTION_SCROLL_TO_POSITION);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+
+        switch (action) {
+            case R.id.accessibilityActionScrollToPosition: {
+                final int row = arguments.getInt(AccessibilityNodeInfo.ACTION_ARGUMENT_ROW_INT, -1);
+                final int position = Math.min(row, getCount() - 1);
+                if (row >= 0) {
+                    // The accessibility service gets data asynchronously, so
+                    // we'll be a little lenient by clamping the last position.
+                    smoothScrollToPosition(position);
+                    return true;
+                }
+            } break;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void onInitializeAccessibilityNodeInfoForItem(
+            View view, int position, AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoForItem(view, position, info);
+
+        final LayoutParams lp = (LayoutParams) view.getLayoutParams();
+        final boolean isHeading = lp != null && lp.viewType == ITEM_VIEW_TYPE_HEADER_OR_FOOTER;
+        final boolean isSelected = isItemChecked(position);
+        final CollectionItemInfo itemInfo = CollectionItemInfo.obtain(
+                position, 1, 0, 1, isHeading, isSelected);
+        info.setCollectionItemInfo(itemInfo);
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+
+        encoder.addProperty("recycleOnMeasure", recycleOnMeasure());
+    }
+
+    /** @hide */
+    protected HeaderViewListAdapter wrapHeaderListAdapterInternal(
+            ArrayList<ListView.FixedViewInfo> headerViewInfos,
+            ArrayList<ListView.FixedViewInfo> footerViewInfos,
+            ListAdapter adapter) {
+        return new HeaderViewListAdapter(headerViewInfos, footerViewInfos, adapter);
+    }
+
+    /** @hide */
+    protected void wrapHeaderListAdapterInternal() {
+        mAdapter = wrapHeaderListAdapterInternal(mHeaderViewInfos, mFooterViewInfos, mAdapter);
+    }
+
+    /** @hide */
+    protected void dispatchDataSetObserverOnChangedInternal() {
+        if (mDataSetObserver != null) {
+            mDataSetObserver.onChanged();
+        }
+    }
+}
diff --git a/android/widget/MediaController.java b/android/widget/MediaController.java
new file mode 100644
index 0000000..8e04f1c
--- /dev/null
+++ b/android/widget/MediaController.java
@@ -0,0 +1,716 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.media.AudioManager;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.Window;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityManager;
+import android.widget.SeekBar.OnSeekBarChangeListener;
+
+import com.android.internal.policy.PhoneWindow;
+
+import java.util.Formatter;
+import java.util.Locale;
+
+/**
+ * A view containing controls for a MediaPlayer. Typically contains the
+ * buttons like "Play/Pause", "Rewind", "Fast Forward" and a progress
+ * slider. It takes care of synchronizing the controls with the state
+ * of the MediaPlayer.
+ * <p>
+ * The way to use this class is to instantiate it programmatically.
+ * The MediaController will create a default set of controls
+ * and put them in a window floating above your application. Specifically,
+ * the controls will float above the view specified with setAnchorView().
+ * The window will disappear if left idle for three seconds and reappear
+ * when the user touches the anchor view.
+ * <p>
+ * Functions like show() and hide() have no effect when MediaController
+ * is created in an xml layout.
+ *
+ * MediaController will hide and
+ * show the buttons according to these rules:
+ * <ul>
+ * <li> The "previous" and "next" buttons are hidden until setPrevNextListeners()
+ *   has been called
+ * <li> The "previous" and "next" buttons are visible but disabled if
+ *   setPrevNextListeners() was called with null listeners
+ * <li> The "rewind" and "fastforward" buttons are shown unless requested
+ *   otherwise by using the MediaController(Context, boolean) constructor
+ *   with the boolean set to false
+ * </ul>
+ */
+public class MediaController extends FrameLayout {
+
+    private MediaPlayerControl mPlayer;
+    private final Context mContext;
+    private View mAnchor;
+    private View mRoot;
+    private WindowManager mWindowManager;
+    private Window mWindow;
+    private View mDecor;
+    private WindowManager.LayoutParams mDecorLayoutParams;
+    private ProgressBar mProgress;
+    private TextView mEndTime, mCurrentTime;
+    private boolean mShowing;
+    private boolean mDragging;
+    private static final int sDefaultTimeout = 3000;
+    private final boolean mUseFastForward;
+    private boolean mFromXml;
+    private boolean mListenersSet;
+    private View.OnClickListener mNextListener, mPrevListener;
+    StringBuilder mFormatBuilder;
+    Formatter mFormatter;
+    private ImageButton mPauseButton;
+    private ImageButton mFfwdButton;
+    private ImageButton mRewButton;
+    private ImageButton mNextButton;
+    private ImageButton mPrevButton;
+    private CharSequence mPlayDescription;
+    private CharSequence mPauseDescription;
+    private final AccessibilityManager mAccessibilityManager;
+
+    public MediaController(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        mRoot = this;
+        mContext = context;
+        mUseFastForward = true;
+        mFromXml = true;
+        mAccessibilityManager = AccessibilityManager.getInstance(context);
+    }
+
+    @Override
+    public void onFinishInflate() {
+        if (mRoot != null)
+            initControllerView(mRoot);
+    }
+
+    public MediaController(Context context, boolean useFastForward) {
+        super(context);
+        mContext = context;
+        mUseFastForward = useFastForward;
+        initFloatingWindowLayout();
+        initFloatingWindow();
+        mAccessibilityManager = AccessibilityManager.getInstance(context);
+    }
+
+    public MediaController(Context context) {
+        this(context, true);
+    }
+
+    private void initFloatingWindow() {
+        mWindowManager = (WindowManager)mContext.getSystemService(Context.WINDOW_SERVICE);
+        mWindow = new PhoneWindow(mContext);
+        mWindow.setWindowManager(mWindowManager, null, null);
+        mWindow.requestFeature(Window.FEATURE_NO_TITLE);
+        mDecor = mWindow.getDecorView();
+        mDecor.setOnTouchListener(mTouchListener);
+        mWindow.setContentView(this);
+        mWindow.setBackgroundDrawableResource(android.R.color.transparent);
+
+        // While the media controller is up, the volume control keys should
+        // affect the media stream type
+        mWindow.setVolumeControlStream(AudioManager.STREAM_MUSIC);
+
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+        setDescendantFocusability(ViewGroup.FOCUS_AFTER_DESCENDANTS);
+        requestFocus();
+    }
+
+    // Allocate and initialize the static parts of mDecorLayoutParams. Must
+    // also call updateFloatingWindowLayout() to fill in the dynamic parts
+    // (y and width) before mDecorLayoutParams can be used.
+    private void initFloatingWindowLayout() {
+        mDecorLayoutParams = new WindowManager.LayoutParams();
+        WindowManager.LayoutParams p = mDecorLayoutParams;
+        p.gravity = Gravity.TOP | Gravity.LEFT;
+        p.height = LayoutParams.WRAP_CONTENT;
+        p.x = 0;
+        p.format = PixelFormat.TRANSLUCENT;
+        p.type = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+        p.flags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM
+                | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL
+                | WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+        p.token = null;
+        p.windowAnimations = 0; // android.R.style.DropDownAnimationDown;
+    }
+
+    // Update the dynamic parts of mDecorLayoutParams
+    // Must be called with mAnchor != NULL.
+    private void updateFloatingWindowLayout() {
+        int [] anchorPos = new int[2];
+        mAnchor.getLocationOnScreen(anchorPos);
+
+        // we need to know the size of the controller so we can properly position it
+        // within its space
+        mDecor.measure(MeasureSpec.makeMeasureSpec(mAnchor.getWidth(), MeasureSpec.AT_MOST),
+                MeasureSpec.makeMeasureSpec(mAnchor.getHeight(), MeasureSpec.AT_MOST));
+
+        WindowManager.LayoutParams p = mDecorLayoutParams;
+        p.width = mAnchor.getWidth();
+        p.x = anchorPos[0] + (mAnchor.getWidth() - p.width) / 2;
+        p.y = anchorPos[1] + mAnchor.getHeight() - mDecor.getMeasuredHeight();
+    }
+
+    // This is called whenever mAnchor's layout bound changes
+    private final OnLayoutChangeListener mLayoutChangeListener =
+            new OnLayoutChangeListener() {
+        @Override
+        public void onLayoutChange(View v, int left, int top, int right,
+                int bottom, int oldLeft, int oldTop, int oldRight,
+                int oldBottom) {
+            updateFloatingWindowLayout();
+            if (mShowing) {
+                mWindowManager.updateViewLayout(mDecor, mDecorLayoutParams);
+            }
+        }
+    };
+
+    private final OnTouchListener mTouchListener = new OnTouchListener() {
+        @Override
+        public boolean onTouch(View v, MotionEvent event) {
+            if (event.getAction() == MotionEvent.ACTION_DOWN) {
+                if (mShowing) {
+                    hide();
+                }
+            }
+            return false;
+        }
+    };
+
+    public void setMediaPlayer(MediaPlayerControl player) {
+        mPlayer = player;
+        updatePausePlay();
+    }
+
+    /**
+     * Set the view that acts as the anchor for the control view.
+     * This can for example be a VideoView, or your Activity's main view.
+     * When VideoView calls this method, it will use the VideoView's parent
+     * as the anchor.
+     * @param view The view to which to anchor the controller when it is visible.
+     */
+    public void setAnchorView(View view) {
+        if (mAnchor != null) {
+            mAnchor.removeOnLayoutChangeListener(mLayoutChangeListener);
+        }
+        mAnchor = view;
+        if (mAnchor != null) {
+            mAnchor.addOnLayoutChangeListener(mLayoutChangeListener);
+        }
+
+        FrameLayout.LayoutParams frameParams = new FrameLayout.LayoutParams(
+                ViewGroup.LayoutParams.MATCH_PARENT,
+                ViewGroup.LayoutParams.MATCH_PARENT
+        );
+
+        removeAllViews();
+        View v = makeControllerView();
+        addView(v, frameParams);
+    }
+
+    /**
+     * Create the view that holds the widgets that control playback.
+     * Derived classes can override this to create their own.
+     * @return The controller view.
+     * @hide This doesn't work as advertised
+     */
+    protected View makeControllerView() {
+        LayoutInflater inflate = (LayoutInflater) mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mRoot = inflate.inflate(com.android.internal.R.layout.media_controller, null);
+
+        initControllerView(mRoot);
+
+        return mRoot;
+    }
+
+    private void initControllerView(View v) {
+        Resources res = mContext.getResources();
+        mPlayDescription = res
+                .getText(com.android.internal.R.string.lockscreen_transport_play_description);
+        mPauseDescription = res
+                .getText(com.android.internal.R.string.lockscreen_transport_pause_description);
+        mPauseButton = v.findViewById(com.android.internal.R.id.pause);
+        if (mPauseButton != null) {
+            mPauseButton.requestFocus();
+            mPauseButton.setOnClickListener(mPauseListener);
+        }
+
+        mFfwdButton = v.findViewById(com.android.internal.R.id.ffwd);
+        if (mFfwdButton != null) {
+            mFfwdButton.setOnClickListener(mFfwdListener);
+            if (!mFromXml) {
+                mFfwdButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
+            }
+        }
+
+        mRewButton = v.findViewById(com.android.internal.R.id.rew);
+        if (mRewButton != null) {
+            mRewButton.setOnClickListener(mRewListener);
+            if (!mFromXml) {
+                mRewButton.setVisibility(mUseFastForward ? View.VISIBLE : View.GONE);
+            }
+        }
+
+        // By default these are hidden. They will be enabled when setPrevNextListeners() is called
+        mNextButton = v.findViewById(com.android.internal.R.id.next);
+        if (mNextButton != null && !mFromXml && !mListenersSet) {
+            mNextButton.setVisibility(View.GONE);
+        }
+        mPrevButton = v.findViewById(com.android.internal.R.id.prev);
+        if (mPrevButton != null && !mFromXml && !mListenersSet) {
+            mPrevButton.setVisibility(View.GONE);
+        }
+
+        mProgress = v.findViewById(com.android.internal.R.id.mediacontroller_progress);
+        if (mProgress != null) {
+            if (mProgress instanceof SeekBar) {
+                SeekBar seeker = (SeekBar) mProgress;
+                seeker.setOnSeekBarChangeListener(mSeekListener);
+            }
+            mProgress.setMax(1000);
+        }
+
+        mEndTime = v.findViewById(com.android.internal.R.id.time);
+        mCurrentTime = v.findViewById(com.android.internal.R.id.time_current);
+        mFormatBuilder = new StringBuilder();
+        mFormatter = new Formatter(mFormatBuilder, Locale.getDefault());
+
+        installPrevNextListeners();
+    }
+
+    /**
+     * Show the controller on screen. It will go away
+     * automatically after 3 seconds of inactivity.
+     */
+    public void show() {
+        show(sDefaultTimeout);
+    }
+
+    /**
+     * Disable pause or seek buttons if the stream cannot be paused or seeked.
+     * This requires the control interface to be a MediaPlayerControlExt
+     */
+    private void disableUnsupportedButtons() {
+        try {
+            if (mPauseButton != null && !mPlayer.canPause()) {
+                mPauseButton.setEnabled(false);
+            }
+            if (mRewButton != null && !mPlayer.canSeekBackward()) {
+                mRewButton.setEnabled(false);
+            }
+            if (mFfwdButton != null && !mPlayer.canSeekForward()) {
+                mFfwdButton.setEnabled(false);
+            }
+            // TODO What we really should do is add a canSeek to the MediaPlayerControl interface;
+            // this scheme can break the case when applications want to allow seek through the
+            // progress bar but disable forward/backward buttons.
+            //
+            // However, currently the flags SEEK_BACKWARD_AVAILABLE, SEEK_FORWARD_AVAILABLE,
+            // and SEEK_AVAILABLE are all (un)set together; as such the aforementioned issue
+            // shouldn't arise in existing applications.
+            if (mProgress != null && !mPlayer.canSeekBackward() && !mPlayer.canSeekForward()) {
+                mProgress.setEnabled(false);
+            }
+        } catch (IncompatibleClassChangeError ex) {
+            // We were given an old version of the interface, that doesn't have
+            // the canPause/canSeekXYZ methods. This is OK, it just means we
+            // assume the media can be paused and seeked, and so we don't disable
+            // the buttons.
+        }
+    }
+
+    /**
+     * Show the controller on screen. It will go away
+     * automatically after 'timeout' milliseconds of inactivity.
+     * @param timeout The timeout in milliseconds. Use 0 to show
+     * the controller until hide() is called.
+     */
+    public void show(int timeout) {
+        if (!mShowing && mAnchor != null) {
+            setProgress();
+            if (mPauseButton != null) {
+                mPauseButton.requestFocus();
+            }
+            disableUnsupportedButtons();
+            updateFloatingWindowLayout();
+            mWindowManager.addView(mDecor, mDecorLayoutParams);
+            mShowing = true;
+        }
+        updatePausePlay();
+
+        // cause the progress bar to be updated even if mShowing
+        // was already true.  This happens, for example, if we're
+        // paused with the progress bar showing the user hits play.
+        post(mShowProgress);
+
+        if (timeout != 0 && !mAccessibilityManager.isTouchExplorationEnabled()) {
+            removeCallbacks(mFadeOut);
+            postDelayed(mFadeOut, timeout);
+        }
+    }
+
+    public boolean isShowing() {
+        return mShowing;
+    }
+
+    /**
+     * Remove the controller from the screen.
+     */
+    public void hide() {
+        if (mAnchor == null)
+            return;
+
+        if (mShowing) {
+            try {
+                removeCallbacks(mShowProgress);
+                mWindowManager.removeView(mDecor);
+            } catch (IllegalArgumentException ex) {
+                Log.w("MediaController", "already removed");
+            }
+            mShowing = false;
+        }
+    }
+
+    private final Runnable mFadeOut = new Runnable() {
+        @Override
+        public void run() {
+            hide();
+        }
+    };
+
+    private final Runnable mShowProgress = new Runnable() {
+        @Override
+        public void run() {
+            int pos = setProgress();
+            if (!mDragging && mShowing && mPlayer.isPlaying()) {
+                postDelayed(mShowProgress, 1000 - (pos % 1000));
+            }
+        }
+    };
+
+    private String stringForTime(int timeMs) {
+        int totalSeconds = timeMs / 1000;
+
+        int seconds = totalSeconds % 60;
+        int minutes = (totalSeconds / 60) % 60;
+        int hours   = totalSeconds / 3600;
+
+        mFormatBuilder.setLength(0);
+        if (hours > 0) {
+            return mFormatter.format("%d:%02d:%02d", hours, minutes, seconds).toString();
+        } else {
+            return mFormatter.format("%02d:%02d", minutes, seconds).toString();
+        }
+    }
+
+    private int setProgress() {
+        if (mPlayer == null || mDragging) {
+            return 0;
+        }
+        int position = mPlayer.getCurrentPosition();
+        int duration = mPlayer.getDuration();
+        if (mProgress != null) {
+            if (duration > 0) {
+                // use long to avoid overflow
+                long pos = 1000L * position / duration;
+                mProgress.setProgress( (int) pos);
+            }
+            int percent = mPlayer.getBufferPercentage();
+            mProgress.setSecondaryProgress(percent * 10);
+        }
+
+        if (mEndTime != null)
+            mEndTime.setText(stringForTime(duration));
+        if (mCurrentTime != null)
+            mCurrentTime.setText(stringForTime(position));
+
+        return position;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_DOWN:
+                show(0); // show until hide is called
+                break;
+            case MotionEvent.ACTION_UP:
+                show(sDefaultTimeout); // start timeout
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                hide();
+                break;
+            default:
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent ev) {
+        show(sDefaultTimeout);
+        return false;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        final boolean uniqueDown = event.getRepeatCount() == 0
+                && event.getAction() == KeyEvent.ACTION_DOWN;
+        if (keyCode ==  KeyEvent.KEYCODE_HEADSETHOOK
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE
+                || keyCode == KeyEvent.KEYCODE_SPACE) {
+            if (uniqueDown) {
+                doPauseResume();
+                show(sDefaultTimeout);
+                if (mPauseButton != null) {
+                    mPauseButton.requestFocus();
+                }
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+            if (uniqueDown && !mPlayer.isPlaying()) {
+                mPlayer.start();
+                updatePausePlay();
+                show(sDefaultTimeout);
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+                || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+            if (uniqueDown && mPlayer.isPlaying()) {
+                mPlayer.pause();
+                updatePausePlay();
+                show(sDefaultTimeout);
+            }
+            return true;
+        } else if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
+                || keyCode == KeyEvent.KEYCODE_VOLUME_UP
+                || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE
+                || keyCode == KeyEvent.KEYCODE_CAMERA) {
+            // don't show the controls for volume adjustment
+            return super.dispatchKeyEvent(event);
+        } else if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_MENU) {
+            if (uniqueDown) {
+                hide();
+            }
+            return true;
+        }
+
+        show(sDefaultTimeout);
+        return super.dispatchKeyEvent(event);
+    }
+
+    private final View.OnClickListener mPauseListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            doPauseResume();
+            show(sDefaultTimeout);
+        }
+    };
+
+    private void updatePausePlay() {
+        if (mRoot == null || mPauseButton == null)
+            return;
+
+        if (mPlayer.isPlaying()) {
+            mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_pause);
+            mPauseButton.setContentDescription(mPauseDescription);
+        } else {
+            mPauseButton.setImageResource(com.android.internal.R.drawable.ic_media_play);
+            mPauseButton.setContentDescription(mPlayDescription);
+        }
+    }
+
+    private void doPauseResume() {
+        if (mPlayer.isPlaying()) {
+            mPlayer.pause();
+        } else {
+            mPlayer.start();
+        }
+        updatePausePlay();
+    }
+
+    // There are two scenarios that can trigger the seekbar listener to trigger:
+    //
+    // The first is the user using the touchpad to adjust the posititon of the
+    // seekbar's thumb. In this case onStartTrackingTouch is called followed by
+    // a number of onProgressChanged notifications, concluded by onStopTrackingTouch.
+    // We're setting the field "mDragging" to true for the duration of the dragging
+    // session to avoid jumps in the position in case of ongoing playback.
+    //
+    // The second scenario involves the user operating the scroll ball, in this
+    // case there WON'T BE onStartTrackingTouch/onStopTrackingTouch notifications,
+    // we will simply apply the updated position without suspending regular updates.
+    private final OnSeekBarChangeListener mSeekListener = new OnSeekBarChangeListener() {
+        @Override
+        public void onStartTrackingTouch(SeekBar bar) {
+            show(3600000);
+
+            mDragging = true;
+
+            // By removing these pending progress messages we make sure
+            // that a) we won't update the progress while the user adjusts
+            // the seekbar and b) once the user is done dragging the thumb
+            // we will post one of these messages to the queue again and
+            // this ensures that there will be exactly one message queued up.
+            removeCallbacks(mShowProgress);
+        }
+
+        @Override
+        public void onProgressChanged(SeekBar bar, int progress, boolean fromuser) {
+            if (!fromuser) {
+                // We're not interested in programmatically generated changes to
+                // the progress bar's position.
+                return;
+            }
+
+            long duration = mPlayer.getDuration();
+            long newposition = (duration * progress) / 1000L;
+            mPlayer.seekTo( (int) newposition);
+            if (mCurrentTime != null)
+                mCurrentTime.setText(stringForTime( (int) newposition));
+        }
+
+        @Override
+        public void onStopTrackingTouch(SeekBar bar) {
+            mDragging = false;
+            setProgress();
+            updatePausePlay();
+            show(sDefaultTimeout);
+
+            // Ensure that progress is properly updated in the future,
+            // the call to show() does not guarantee this because it is a
+            // no-op if we are already showing.
+            post(mShowProgress);
+        }
+    };
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (mPauseButton != null) {
+            mPauseButton.setEnabled(enabled);
+        }
+        if (mFfwdButton != null) {
+            mFfwdButton.setEnabled(enabled);
+        }
+        if (mRewButton != null) {
+            mRewButton.setEnabled(enabled);
+        }
+        if (mNextButton != null) {
+            mNextButton.setEnabled(enabled && mNextListener != null);
+        }
+        if (mPrevButton != null) {
+            mPrevButton.setEnabled(enabled && mPrevListener != null);
+        }
+        if (mProgress != null) {
+            mProgress.setEnabled(enabled);
+        }
+        disableUnsupportedButtons();
+        super.setEnabled(enabled);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return MediaController.class.getName();
+    }
+
+    private final View.OnClickListener mRewListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = mPlayer.getCurrentPosition();
+            pos -= 5000; // milliseconds
+            mPlayer.seekTo(pos);
+            setProgress();
+
+            show(sDefaultTimeout);
+        }
+    };
+
+    private final View.OnClickListener mFfwdListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+            int pos = mPlayer.getCurrentPosition();
+            pos += 15000; // milliseconds
+            mPlayer.seekTo(pos);
+            setProgress();
+
+            show(sDefaultTimeout);
+        }
+    };
+
+    private void installPrevNextListeners() {
+        if (mNextButton != null) {
+            mNextButton.setOnClickListener(mNextListener);
+            mNextButton.setEnabled(mNextListener != null);
+        }
+
+        if (mPrevButton != null) {
+            mPrevButton.setOnClickListener(mPrevListener);
+            mPrevButton.setEnabled(mPrevListener != null);
+        }
+    }
+
+    public void setPrevNextListeners(View.OnClickListener next, View.OnClickListener prev) {
+        mNextListener = next;
+        mPrevListener = prev;
+        mListenersSet = true;
+
+        if (mRoot != null) {
+            installPrevNextListeners();
+
+            if (mNextButton != null && !mFromXml) {
+                mNextButton.setVisibility(View.VISIBLE);
+            }
+            if (mPrevButton != null && !mFromXml) {
+                mPrevButton.setVisibility(View.VISIBLE);
+            }
+        }
+    }
+
+    public interface MediaPlayerControl {
+        void    start();
+        void    pause();
+        int     getDuration();
+        int     getCurrentPosition();
+        void    seekTo(int pos);
+        boolean isPlaying();
+        int     getBufferPercentage();
+        boolean canPause();
+        boolean canSeekBackward();
+        boolean canSeekForward();
+
+        /**
+         * Get the audio session id for the player used by this VideoView. This can be used to
+         * apply audio effects to the audio track of a video.
+         * @return The audio session, or 0 if there was an error.
+         */
+        int     getAudioSessionId();
+    }
+}
diff --git a/android/widget/MenuItemHoverListener.java b/android/widget/MenuItemHoverListener.java
new file mode 100644
index 0000000..835e4cd
--- /dev/null
+++ b/android/widget/MenuItemHoverListener.java
@@ -0,0 +1,33 @@
+package android.widget;
+
+import android.annotation.NonNull;
+import android.view.MenuItem;
+
+import com.android.internal.view.menu.MenuBuilder;
+
+/**
+ * An interface notified when a menu item is hovered. Useful for cases when hover should trigger
+ * some behavior at a higher level, like managing the opening and closing of submenus.
+ *
+ * @hide
+ */
+public interface MenuItemHoverListener {
+    /**
+     * Called when hover exits a menu item.
+     * <p>
+     * If hover is moving to another item, this method will be called before
+     * {@link #onItemHoverEnter(MenuBuilder, MenuItem)} for the newly-hovered item.
+     *
+     * @param menu the item's parent menu
+     * @param item the hovered menu item
+     */
+    void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item);
+
+    /**
+     * Called when hover enters a menu item.
+     *
+     * @param menu the item's parent menu
+     * @param item the hovered menu item
+     */
+    void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item);
+}
diff --git a/android/widget/MenuPopupWindow.java b/android/widget/MenuPopupWindow.java
new file mode 100644
index 0000000..85e26d0
--- /dev/null
+++ b/android/widget/MenuPopupWindow.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.transition.Transition;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+
+import com.android.internal.view.menu.ListMenuItemView;
+import com.android.internal.view.menu.MenuAdapter;
+import com.android.internal.view.menu.MenuBuilder;
+
+/**
+ * A MenuPopupWindow represents the popup window for menu.
+ *
+ * MenuPopupWindow is mostly same as ListPopupWindow, but it has customized
+ * behaviors specific to menus,
+ *
+ * @hide
+ */
+public class MenuPopupWindow extends ListPopupWindow implements MenuItemHoverListener {
+    private MenuItemHoverListener mHoverListener;
+
+    public MenuPopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    DropDownListView createDropDownListView(Context context, boolean hijackFocus) {
+        MenuDropDownListView view = new MenuDropDownListView(context, hijackFocus);
+        view.setHoverListener(this);
+        return view;
+    }
+
+    public void setEnterTransition(Transition enterTransition) {
+        mPopup.setEnterTransition(enterTransition);
+    }
+
+    public void setExitTransition(Transition exitTransition) {
+        mPopup.setExitTransition(exitTransition);
+    }
+
+    public void setHoverListener(MenuItemHoverListener hoverListener) {
+        mHoverListener = hoverListener;
+    }
+
+    /**
+     * Set whether this window is touch modal or if outside touches will be sent to
+     * other windows behind it.
+     */
+    public void setTouchModal(boolean touchModal) {
+        mPopup.setTouchModal(touchModal);
+    }
+
+    @Override
+    public void onItemHoverEnter(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
+        // Forward up the chain
+        if (mHoverListener != null) {
+            mHoverListener.onItemHoverEnter(menu, item);
+        }
+    }
+
+    @Override
+    public void onItemHoverExit(@NonNull MenuBuilder menu, @NonNull MenuItem item) {
+        // Forward up the chain
+        if (mHoverListener != null) {
+            mHoverListener.onItemHoverExit(menu, item);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static class MenuDropDownListView extends DropDownListView {
+        final int mAdvanceKey;
+        final int mRetreatKey;
+
+        private MenuItemHoverListener mHoverListener;
+        private MenuItem mHoveredMenuItem;
+
+        public MenuDropDownListView(Context context, boolean hijackFocus) {
+            super(context, hijackFocus);
+
+            final Resources res = context.getResources();
+            final Configuration config = res.getConfiguration();
+            if (config.getLayoutDirection() == View.LAYOUT_DIRECTION_RTL) {
+                mAdvanceKey = KeyEvent.KEYCODE_DPAD_LEFT;
+                mRetreatKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+            } else {
+                mAdvanceKey = KeyEvent.KEYCODE_DPAD_RIGHT;
+                mRetreatKey = KeyEvent.KEYCODE_DPAD_LEFT;
+            }
+        }
+
+        public void setHoverListener(MenuItemHoverListener hoverListener) {
+            mHoverListener = hoverListener;
+        }
+
+        public void clearSelection() {
+            setSelectedPositionInt(INVALID_POSITION);
+            setNextSelectedPositionInt(INVALID_POSITION);
+        }
+
+        @Override
+        public boolean onKeyDown(int keyCode, KeyEvent event) {
+            ListMenuItemView selectedItem = (ListMenuItemView) getSelectedView();
+            if (selectedItem != null && keyCode == mAdvanceKey) {
+                if (selectedItem.isEnabled() && selectedItem.getItemData().hasSubMenu()) {
+                    performItemClick(
+                            selectedItem,
+                            getSelectedItemPosition(),
+                            getSelectedItemId());
+                }
+                return true;
+            } else if (selectedItem != null && keyCode == mRetreatKey) {
+                setSelectedPositionInt(INVALID_POSITION);
+                setNextSelectedPositionInt(INVALID_POSITION);
+
+                // Close only the top-level menu.
+                ((MenuAdapter) getAdapter()).getAdapterMenu().close(false /* closeAllMenus */);
+                return true;
+            }
+            return super.onKeyDown(keyCode, event);
+        }
+
+        @Override
+        public boolean onHoverEvent(MotionEvent ev) {
+            // Dispatch any changes in hovered item index to the listener.
+            if (mHoverListener != null) {
+                // The adapter may be wrapped. Adjust the index if necessary.
+                final int headersCount;
+                final MenuAdapter menuAdapter;
+                final ListAdapter adapter = getAdapter();
+                if (adapter instanceof HeaderViewListAdapter) {
+                    final HeaderViewListAdapter headerAdapter = (HeaderViewListAdapter) adapter;
+                    headersCount = headerAdapter.getHeadersCount();
+                    menuAdapter = (MenuAdapter) headerAdapter.getWrappedAdapter();
+                } else {
+                    headersCount = 0;
+                    menuAdapter = (MenuAdapter) adapter;
+                }
+
+                // Find the menu item for the view at the event coordinates.
+                MenuItem menuItem = null;
+                if (ev.getAction() != MotionEvent.ACTION_HOVER_EXIT) {
+                    final int position = pointToPosition((int) ev.getX(), (int) ev.getY());
+                    if (position != INVALID_POSITION) {
+                        final int itemPosition = position - headersCount;
+                        if (itemPosition >= 0 && itemPosition < menuAdapter.getCount()) {
+                            menuItem = menuAdapter.getItem(itemPosition);
+                        }
+                    }
+                }
+
+                final MenuItem oldMenuItem = mHoveredMenuItem;
+                if (oldMenuItem != menuItem) {
+                    final MenuBuilder menu = menuAdapter.getAdapterMenu();
+                    if (oldMenuItem != null) {
+                        mHoverListener.onItemHoverExit(menu, oldMenuItem);
+                    }
+
+                    mHoveredMenuItem = menuItem;
+
+                    if (menuItem != null) {
+                        mHoverListener.onItemHoverEnter(menu, menuItem);
+                    }
+                }
+            }
+
+            return super.onHoverEvent(ev);
+        }
+    }
+}
\ No newline at end of file
diff --git a/android/widget/MultiAutoCompleteTextView.java b/android/widget/MultiAutoCompleteTextView.java
new file mode 100644
index 0000000..f348d73
--- /dev/null
+++ b/android/widget/MultiAutoCompleteTextView.java
@@ -0,0 +1,282 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.SpannableString;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.QwertyKeyListener;
+import android.util.AttributeSet;
+
+/**
+ * An editable text view, extending {@link AutoCompleteTextView}, that
+ * can show completion suggestions for the substring of the text where
+ * the user is typing instead of necessarily for the entire thing.
+ * <p>
+ * You must provide a {@link Tokenizer} to distinguish the
+ * various substrings.
+ *
+ * <p>The following code snippet shows how to create a text view which suggests
+ * various countries names while the user is typing:</p>
+ *
+ * <pre class="prettyprint">
+ * public class CountriesActivity extends Activity {
+ *     protected void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *         setContentView(R.layout.autocomplete_7);
+ *
+ *         ArrayAdapter&lt;String&gt; adapter = new ArrayAdapter&lt;String&gt;(this,
+ *                 android.R.layout.simple_dropdown_item_1line, COUNTRIES);
+ *         MultiAutoCompleteTextView textView = findViewById(R.id.edit);
+ *         textView.setAdapter(adapter);
+ *         textView.setTokenizer(new MultiAutoCompleteTextView.CommaTokenizer());
+ *     }
+ *
+ *     private static final String[] COUNTRIES = new String[] {
+ *         "Belgium", "France", "Italy", "Germany", "Spain"
+ *     };
+ * }</pre>
+ */
+
+public class MultiAutoCompleteTextView extends AutoCompleteTextView {
+    private Tokenizer mTokenizer;
+
+    public MultiAutoCompleteTextView(Context context) {
+        this(context, null);
+    }
+
+    public MultiAutoCompleteTextView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.autoCompleteTextViewStyle);
+    }
+
+    public MultiAutoCompleteTextView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public MultiAutoCompleteTextView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /* package */ void finishInit() { }
+
+    /**
+     * Sets the Tokenizer that will be used to determine the relevant
+     * range of the text where the user is typing.
+     */
+    public void setTokenizer(Tokenizer t) {
+        mTokenizer = t;
+    }
+
+    /**
+     * Instead of filtering on the entire contents of the edit box,
+     * this subclass method filters on the range from
+     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
+     * if the length of that range meets or exceeds {@link #getThreshold}.
+     */
+    @Override
+    protected void performFiltering(CharSequence text, int keyCode) {
+        if (enoughToFilter()) {
+            int end = getSelectionEnd();
+            int start = mTokenizer.findTokenStart(text, end);
+
+            performFiltering(text, start, end, keyCode);
+        } else {
+            dismissDropDown();
+
+            Filter f = getFilter();
+            if (f != null) {
+                f.filter(null);
+            }
+        }
+    }
+
+    /**
+     * Instead of filtering whenever the total length of the text
+     * exceeds the threshhold, this subclass filters only when the
+     * length of the range from
+     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd}
+     * meets or exceeds {@link #getThreshold}.
+     */
+    @Override
+    public boolean enoughToFilter() {
+        Editable text = getText();
+
+        int end = getSelectionEnd();
+        if (end < 0 || mTokenizer == null) {
+            return false;
+        }
+
+        int start = mTokenizer.findTokenStart(text, end);
+
+        if (end - start >= getThreshold()) {
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Instead of validating the entire text, this subclass method validates
+     * each token of the text individually.  Empty tokens are removed.
+     */
+    @Override
+    public void performValidation() {
+        Validator v = getValidator();
+
+        if (v == null || mTokenizer == null) {
+            return;
+        }
+
+        Editable e = getText();
+        int i = getText().length();
+        while (i > 0) {
+            int start = mTokenizer.findTokenStart(e, i);
+            int end = mTokenizer.findTokenEnd(e, start);
+
+            CharSequence sub = e.subSequence(start, end);
+            if (TextUtils.isEmpty(sub)) {
+                e.replace(start, i, "");
+            } else if (!v.isValid(sub)) {
+                e.replace(start, i,
+                          mTokenizer.terminateToken(v.fixText(sub)));
+            }
+
+            i = start;
+        }
+    }
+
+    /**
+     * <p>Starts filtering the content of the drop down list. The filtering
+     * pattern is the specified range of text from the edit box. Subclasses may
+     * override this method to filter with a different pattern, for
+     * instance a smaller substring of <code>text</code>.</p>
+     */
+    protected void performFiltering(CharSequence text, int start, int end,
+                                    int keyCode) {
+        getFilter().filter(text.subSequence(start, end), this);
+    }
+
+    /**
+     * <p>Performs the text completion by replacing the range from
+     * {@link Tokenizer#findTokenStart} to {@link #getSelectionEnd} by the
+     * the result of passing <code>text</code> through
+     * {@link Tokenizer#terminateToken}.
+     * In addition, the replaced region will be marked as an AutoText
+     * substition so that if the user immediately presses DEL, the
+     * completion will be undone.
+     * Subclasses may override this method to do some different
+     * insertion of the content into the edit box.</p>
+     *
+     * @param text the selected suggestion in the drop down list
+     */
+    @Override
+    protected void replaceText(CharSequence text) {
+        clearComposingText();
+
+        int end = getSelectionEnd();
+        int start = mTokenizer.findTokenStart(getText(), end);
+
+        Editable editable = getText();
+        String original = TextUtils.substring(editable, start, end);
+
+        QwertyKeyListener.markAsReplaced(editable, start, end, original);
+        editable.replace(start, end, mTokenizer.terminateToken(text));
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return MultiAutoCompleteTextView.class.getName();
+    }
+
+    public static interface Tokenizer {
+        /**
+         * Returns the start of the token that ends at offset
+         * <code>cursor</code> within <code>text</code>.
+         */
+        public int findTokenStart(CharSequence text, int cursor);
+
+        /**
+         * Returns the end of the token (minus trailing punctuation)
+         * that begins at offset <code>cursor</code> within <code>text</code>.
+         */
+        public int findTokenEnd(CharSequence text, int cursor);
+
+        /**
+         * Returns <code>text</code>, modified, if necessary, to ensure that
+         * it ends with a token terminator (for example a space or comma).
+         */
+        public CharSequence terminateToken(CharSequence text);
+    }
+
+    /**
+     * This simple Tokenizer can be used for lists where the items are
+     * separated by a comma and one or more spaces.
+     */
+    public static class CommaTokenizer implements Tokenizer {
+        public int findTokenStart(CharSequence text, int cursor) {
+            int i = cursor;
+
+            while (i > 0 && text.charAt(i - 1) != ',') {
+                i--;
+            }
+            while (i < cursor && text.charAt(i) == ' ') {
+                i++;
+            }
+
+            return i;
+        }
+
+        public int findTokenEnd(CharSequence text, int cursor) {
+            int i = cursor;
+            int len = text.length();
+
+            while (i < len) {
+                if (text.charAt(i) == ',') {
+                    return i;
+                } else {
+                    i++;
+                }
+            }
+
+            return len;
+        }
+
+        public CharSequence terminateToken(CharSequence text) {
+            int i = text.length();
+
+            while (i > 0 && text.charAt(i - 1) == ' ') {
+                i--;
+            }
+
+            if (i > 0 && text.charAt(i - 1) == ',') {
+                return text;
+            } else {
+                if (text instanceof Spanned) {
+                    SpannableString sp = new SpannableString(text + ", ");
+                    TextUtils.copySpansFrom((Spanned) text, 0, text.length(),
+                                            Object.class, sp, 0);
+                    return sp;
+                } else {
+                    return text + ", ";
+                }
+            }
+        }
+    }
+}
diff --git a/android/widget/NumberPicker.java b/android/widget/NumberPicker.java
new file mode 100644
index 0000000..4d3189e
--- /dev/null
+++ b/android/widget/NumberPicker.java
@@ -0,0 +1,2817 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.annotation.CallSuper;
+import android.annotation.IntDef;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.NumberKeyListener;
+import android.util.AttributeSet;
+import android.util.SparseArray;
+import android.util.TypedValue;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.LayoutInflater.Filter;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeProvider;
+import android.view.animation.DecelerateInterpolator;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/**
+ * A widget that enables the user to select a number from a predefined range.
+ * There are two flavors of this widget and which one is presented to the user
+ * depends on the current theme.
+ * <ul>
+ * <li>
+ * If the current theme is derived from {@link android.R.style#Theme} the widget
+ * presents the current value as an editable input field with an increment button
+ * above and a decrement button below. Long pressing the buttons allows for a quick
+ * change of the current value. Tapping on the input field allows to type in
+ * a desired value.
+ * </li>
+ * <li>
+ * If the current theme is derived from {@link android.R.style#Theme_Holo} or
+ * {@link android.R.style#Theme_Holo_Light} the widget presents the current
+ * value as an editable input field with a lesser value above and a greater
+ * value below. Tapping on the lesser or greater value selects it by animating
+ * the number axis up or down to make the chosen value current. Flinging up
+ * or down allows for multiple increments or decrements of the current value.
+ * Long pressing on the lesser and greater values also allows for a quick change
+ * of the current value. Tapping on the current value allows to type in a
+ * desired value.
+ * </li>
+ * </ul>
+ * <p>
+ * For an example of using this widget, see {@link android.widget.TimePicker}.
+ * </p>
+ */
+@Widget
+public class NumberPicker extends LinearLayout {
+
+    /**
+     * The number of items show in the selector wheel.
+     */
+    private static final int SELECTOR_WHEEL_ITEM_COUNT = 3;
+
+    /**
+     * The default update interval during long press.
+     */
+    private static final long DEFAULT_LONG_PRESS_UPDATE_INTERVAL = 300;
+
+    /**
+     * The index of the middle selector item.
+     */
+    private static final int SELECTOR_MIDDLE_ITEM_INDEX = SELECTOR_WHEEL_ITEM_COUNT / 2;
+
+    /**
+     * The coefficient by which to adjust (divide) the max fling velocity.
+     */
+    private static final int SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT = 8;
+
+    /**
+     * The the duration for adjusting the selector wheel.
+     */
+    private static final int SELECTOR_ADJUSTMENT_DURATION_MILLIS = 800;
+
+    /**
+     * The duration of scrolling while snapping to a given position.
+     */
+    private static final int SNAP_SCROLL_DURATION = 300;
+
+    /**
+     * The strength of fading in the top and bottom while drawing the selector.
+     */
+    private static final float TOP_AND_BOTTOM_FADING_EDGE_STRENGTH = 0.9f;
+
+    /**
+     * The default unscaled height of the selection divider.
+     */
+    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT = 2;
+
+    /**
+     * The default unscaled distance between the selection dividers.
+     */
+    private static final int UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE = 48;
+
+    /**
+     * The resource id for the default layout.
+     */
+    private static final int DEFAULT_LAYOUT_RESOURCE_ID = R.layout.number_picker;
+
+    /**
+     * Constant for unspecified size.
+     */
+    private static final int SIZE_UNSPECIFIED = -1;
+
+    /**
+     * User choice on whether the selector wheel should be wrapped.
+     */
+    private boolean mWrapSelectorWheelPreferred = true;
+
+    /**
+     * Use a custom NumberPicker formatting callback to use two-digit minutes
+     * strings like "01". Keeping a static formatter etc. is the most efficient
+     * way to do this; it avoids creating temporary objects on every call to
+     * format().
+     */
+    private static class TwoDigitFormatter implements NumberPicker.Formatter {
+        final StringBuilder mBuilder = new StringBuilder();
+
+        char mZeroDigit;
+        java.util.Formatter mFmt;
+
+        final Object[] mArgs = new Object[1];
+
+        TwoDigitFormatter() {
+            final Locale locale = Locale.getDefault();
+            init(locale);
+        }
+
+        private void init(Locale locale) {
+            mFmt = createFormatter(locale);
+            mZeroDigit = getZeroDigit(locale);
+        }
+
+        public String format(int value) {
+            final Locale currentLocale = Locale.getDefault();
+            if (mZeroDigit != getZeroDigit(currentLocale)) {
+                init(currentLocale);
+            }
+            mArgs[0] = value;
+            mBuilder.delete(0, mBuilder.length());
+            mFmt.format("%02d", mArgs);
+            return mFmt.toString();
+        }
+
+        private static char getZeroDigit(Locale locale) {
+            return LocaleData.get(locale).zeroDigit;
+        }
+
+        private java.util.Formatter createFormatter(Locale locale) {
+            return new java.util.Formatter(mBuilder, locale);
+        }
+    }
+
+    private static final TwoDigitFormatter sTwoDigitFormatter = new TwoDigitFormatter();
+
+    /**
+     * @hide
+     */
+    public static final Formatter getTwoDigitFormatter() {
+        return sTwoDigitFormatter;
+    }
+
+    /**
+     * The increment button.
+     */
+    private final ImageButton mIncrementButton;
+
+    /**
+     * The decrement button.
+     */
+    private final ImageButton mDecrementButton;
+
+    /**
+     * The text for showing the current value.
+     */
+    private final EditText mInputText;
+
+    /**
+     * The distance between the two selection dividers.
+     */
+    private final int mSelectionDividersDistance;
+
+    /**
+     * The min height of this widget.
+     */
+    private final int mMinHeight;
+
+    /**
+     * The max height of this widget.
+     */
+    private final int mMaxHeight;
+
+    /**
+     * The max width of this widget.
+     */
+    private final int mMinWidth;
+
+    /**
+     * The max width of this widget.
+     */
+    private int mMaxWidth;
+
+    /**
+     * Flag whether to compute the max width.
+     */
+    private final boolean mComputeMaxWidth;
+
+    /**
+     * The height of the text.
+     */
+    private final int mTextSize;
+
+    /**
+     * The height of the gap between text elements if the selector wheel.
+     */
+    private int mSelectorTextGapHeight;
+
+    /**
+     * The values to be displayed instead the indices.
+     */
+    private String[] mDisplayedValues;
+
+    /**
+     * Lower value of the range of numbers allowed for the NumberPicker
+     */
+    private int mMinValue;
+
+    /**
+     * Upper value of the range of numbers allowed for the NumberPicker
+     */
+    private int mMaxValue;
+
+    /**
+     * Current value of this NumberPicker
+     */
+    private int mValue;
+
+    /**
+     * Listener to be notified upon current value change.
+     */
+    private OnValueChangeListener mOnValueChangeListener;
+
+    /**
+     * Listener to be notified upon scroll state change.
+     */
+    private OnScrollListener mOnScrollListener;
+
+    /**
+     * Formatter for for displaying the current value.
+     */
+    private Formatter mFormatter;
+
+    /**
+     * The speed for updating the value form long press.
+     */
+    private long mLongPressUpdateInterval = DEFAULT_LONG_PRESS_UPDATE_INTERVAL;
+
+    /**
+     * Cache for the string representation of selector indices.
+     */
+    private final SparseArray<String> mSelectorIndexToStringCache = new SparseArray<String>();
+
+    /**
+     * The selector indices whose value are show by the selector.
+     */
+    private final int[] mSelectorIndices = new int[SELECTOR_WHEEL_ITEM_COUNT];
+
+    /**
+     * The {@link Paint} for drawing the selector.
+     */
+    private final Paint mSelectorWheelPaint;
+
+    /**
+     * The {@link Drawable} for pressed virtual (increment/decrement) buttons.
+     */
+    private final Drawable mVirtualButtonPressedDrawable;
+
+    /**
+     * The height of a selector element (text + gap).
+     */
+    private int mSelectorElementHeight;
+
+    /**
+     * The initial offset of the scroll selector.
+     */
+    private int mInitialScrollOffset = Integer.MIN_VALUE;
+
+    /**
+     * The current offset of the scroll selector.
+     */
+    private int mCurrentScrollOffset;
+
+    /**
+     * The {@link Scroller} responsible for flinging the selector.
+     */
+    private final Scroller mFlingScroller;
+
+    /**
+     * The {@link Scroller} responsible for adjusting the selector.
+     */
+    private final Scroller mAdjustScroller;
+
+    /**
+     * The previous Y coordinate while scrolling the selector.
+     */
+    private int mPreviousScrollerY;
+
+    /**
+     * Handle to the reusable command for setting the input text selection.
+     */
+    private SetSelectionCommand mSetSelectionCommand;
+
+    /**
+     * Handle to the reusable command for changing the current value from long
+     * press by one.
+     */
+    private ChangeCurrentByOneFromLongPressCommand mChangeCurrentByOneFromLongPressCommand;
+
+    /**
+     * Command for beginning an edit of the current value via IME on long press.
+     */
+    private BeginSoftInputOnLongPressCommand mBeginSoftInputOnLongPressCommand;
+
+    /**
+     * The Y position of the last down event.
+     */
+    private float mLastDownEventY;
+
+    /**
+     * The time of the last down event.
+     */
+    private long mLastDownEventTime;
+
+    /**
+     * The Y position of the last down or move event.
+     */
+    private float mLastDownOrMoveEventY;
+
+    /**
+     * Determines speed during touch scrolling.
+     */
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * @see ViewConfiguration#getScaledTouchSlop()
+     */
+    private int mTouchSlop;
+
+    /**
+     * @see ViewConfiguration#getScaledMinimumFlingVelocity()
+     */
+    private int mMinimumFlingVelocity;
+
+    /**
+     * @see ViewConfiguration#getScaledMaximumFlingVelocity()
+     */
+    private int mMaximumFlingVelocity;
+
+    /**
+     * Flag whether the selector should wrap around.
+     */
+    private boolean mWrapSelectorWheel;
+
+    /**
+     * The back ground color used to optimize scroller fading.
+     */
+    private final int mSolidColor;
+
+    /**
+     * Flag whether this widget has a selector wheel.
+     */
+    private final boolean mHasSelectorWheel;
+
+    /**
+     * Divider for showing item to be selected while scrolling
+     */
+    private final Drawable mSelectionDivider;
+
+    /**
+     * The height of the selection divider.
+     */
+    private final int mSelectionDividerHeight;
+
+    /**
+     * The current scroll state of the number picker.
+     */
+    private int mScrollState = OnScrollListener.SCROLL_STATE_IDLE;
+
+    /**
+     * Flag whether to ignore move events - we ignore such when we show in IME
+     * to prevent the content from scrolling.
+     */
+    private boolean mIgnoreMoveEvents;
+
+    /**
+     * Flag whether to perform a click on tap.
+     */
+    private boolean mPerformClickOnTap;
+
+    /**
+     * The top of the top selection divider.
+     */
+    private int mTopSelectionDividerTop;
+
+    /**
+     * The bottom of the bottom selection divider.
+     */
+    private int mBottomSelectionDividerBottom;
+
+    /**
+     * The virtual id of the last hovered child.
+     */
+    private int mLastHoveredChildVirtualViewId;
+
+    /**
+     * Whether the increment virtual button is pressed.
+     */
+    private boolean mIncrementVirtualButtonPressed;
+
+    /**
+     * Whether the decrement virtual button is pressed.
+     */
+    private boolean mDecrementVirtualButtonPressed;
+
+    /**
+     * Provider to report to clients the semantic structure of this widget.
+     */
+    private AccessibilityNodeProviderImpl mAccessibilityNodeProvider;
+
+    /**
+     * Helper class for managing pressed state of the virtual buttons.
+     */
+    private final PressedStateHelper mPressedStateHelper;
+
+    /**
+     * The keycode of the last handled DPAD down event.
+     */
+    private int mLastHandledDownDpadKeyCode = -1;
+
+    /**
+     * If true then the selector wheel is hidden until the picker has focus.
+     */
+    private boolean mHideWheelUntilFocused;
+
+    /**
+     * Interface to listen for changes of the current value.
+     */
+    public interface OnValueChangeListener {
+
+        /**
+         * Called upon a change of the current value.
+         *
+         * @param picker The NumberPicker associated with this listener.
+         * @param oldVal The previous value.
+         * @param newVal The new value.
+         */
+        void onValueChange(NumberPicker picker, int oldVal, int newVal);
+    }
+
+    /**
+     * Interface to listen for the picker scroll state.
+     */
+    public interface OnScrollListener {
+        /** @hide */
+        @IntDef({SCROLL_STATE_IDLE, SCROLL_STATE_TOUCH_SCROLL, SCROLL_STATE_FLING})
+        @Retention(RetentionPolicy.SOURCE)
+        public @interface ScrollState {}
+
+        /**
+         * The view is not scrolling.
+         */
+        public static int SCROLL_STATE_IDLE = 0;
+
+        /**
+         * The user is scrolling using touch, and his finger is still on the screen.
+         */
+        public static int SCROLL_STATE_TOUCH_SCROLL = 1;
+
+        /**
+         * The user had previously been scrolling using touch and performed a fling.
+         */
+        public static int SCROLL_STATE_FLING = 2;
+
+        /**
+         * Callback invoked while the number picker scroll state has changed.
+         *
+         * @param view The view whose scroll state is being reported.
+         * @param scrollState The current scroll state. One of
+         *            {@link #SCROLL_STATE_IDLE},
+         *            {@link #SCROLL_STATE_TOUCH_SCROLL} or
+         *            {@link #SCROLL_STATE_IDLE}.
+         */
+        public void onScrollStateChange(NumberPicker view, @ScrollState int scrollState);
+    }
+
+    /**
+     * Interface used to format current value into a string for presentation.
+     */
+    public interface Formatter {
+
+        /**
+         * Formats a string representation of the current value.
+         *
+         * @param value The currently selected value.
+         * @return A formatted string representation.
+         */
+        public String format(int value);
+    }
+
+    /**
+     * Create a new number picker.
+     *
+     * @param context The application environment.
+     */
+    public NumberPicker(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a new number picker.
+     *
+     * @param context The application environment.
+     * @param attrs A collection of attributes.
+     */
+    public NumberPicker(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.numberPickerStyle);
+    }
+
+    /**
+     * Create a new number picker
+     *
+     * @param context the application environment.
+     * @param attrs a collection of attributes.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     */
+    public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * Create a new number picker
+     *
+     * @param context the application environment.
+     * @param attrs a collection of attributes.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *        supplies default values for the view, used only if
+     *        defStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public NumberPicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // process style attributes
+        final TypedArray attributesArray = context.obtainStyledAttributes(
+                attrs, R.styleable.NumberPicker, defStyleAttr, defStyleRes);
+        final int layoutResId = attributesArray.getResourceId(
+                R.styleable.NumberPicker_internalLayout, DEFAULT_LAYOUT_RESOURCE_ID);
+
+        mHasSelectorWheel = (layoutResId != DEFAULT_LAYOUT_RESOURCE_ID);
+
+        mHideWheelUntilFocused = attributesArray.getBoolean(
+            R.styleable.NumberPicker_hideWheelUntilFocused, false);
+
+        mSolidColor = attributesArray.getColor(R.styleable.NumberPicker_solidColor, 0);
+
+        final Drawable selectionDivider = attributesArray.getDrawable(
+                R.styleable.NumberPicker_selectionDivider);
+        if (selectionDivider != null) {
+            selectionDivider.setCallback(this);
+            selectionDivider.setLayoutDirection(getLayoutDirection());
+            if (selectionDivider.isStateful()) {
+                selectionDivider.setState(getDrawableState());
+            }
+        }
+        mSelectionDivider = selectionDivider;
+
+        final int defSelectionDividerHeight = (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDER_HEIGHT,
+                getResources().getDisplayMetrics());
+        mSelectionDividerHeight = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_selectionDividerHeight, defSelectionDividerHeight);
+
+        final int defSelectionDividerDistance = (int) TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP, UNSCALED_DEFAULT_SELECTION_DIVIDERS_DISTANCE,
+                getResources().getDisplayMetrics());
+        mSelectionDividersDistance = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_selectionDividersDistance, defSelectionDividerDistance);
+
+        mMinHeight = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_internalMinHeight, SIZE_UNSPECIFIED);
+
+        mMaxHeight = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_internalMaxHeight, SIZE_UNSPECIFIED);
+        if (mMinHeight != SIZE_UNSPECIFIED && mMaxHeight != SIZE_UNSPECIFIED
+                && mMinHeight > mMaxHeight) {
+            throw new IllegalArgumentException("minHeight > maxHeight");
+        }
+
+        mMinWidth = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_internalMinWidth, SIZE_UNSPECIFIED);
+
+        mMaxWidth = attributesArray.getDimensionPixelSize(
+                R.styleable.NumberPicker_internalMaxWidth, SIZE_UNSPECIFIED);
+        if (mMinWidth != SIZE_UNSPECIFIED && mMaxWidth != SIZE_UNSPECIFIED
+                && mMinWidth > mMaxWidth) {
+            throw new IllegalArgumentException("minWidth > maxWidth");
+        }
+
+        mComputeMaxWidth = (mMaxWidth == SIZE_UNSPECIFIED);
+
+        mVirtualButtonPressedDrawable = attributesArray.getDrawable(
+                R.styleable.NumberPicker_virtualButtonPressedDrawable);
+
+        attributesArray.recycle();
+
+        mPressedStateHelper = new PressedStateHelper();
+
+        // By default Linearlayout that we extend is not drawn. This is
+        // its draw() method is not called but dispatchDraw() is called
+        // directly (see ViewGroup.drawChild()). However, this class uses
+        // the fading edge effect implemented by View and we need our
+        // draw() method to be called. Therefore, we declare we will draw.
+        setWillNotDraw(!mHasSelectorWheel);
+
+        LayoutInflater inflater = (LayoutInflater) getContext().getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(layoutResId, this, true);
+
+        OnClickListener onClickListener = new OnClickListener() {
+            public void onClick(View v) {
+                hideSoftInput();
+                mInputText.clearFocus();
+                if (v.getId() == R.id.increment) {
+                    changeValueByOne(true);
+                } else {
+                    changeValueByOne(false);
+                }
+            }
+        };
+
+        OnLongClickListener onLongClickListener = new OnLongClickListener() {
+            public boolean onLongClick(View v) {
+                hideSoftInput();
+                mInputText.clearFocus();
+                if (v.getId() == R.id.increment) {
+                    postChangeCurrentByOneFromLongPress(true, 0);
+                } else {
+                    postChangeCurrentByOneFromLongPress(false, 0);
+                }
+                return true;
+            }
+        };
+
+        // increment button
+        if (!mHasSelectorWheel) {
+            mIncrementButton = findViewById(R.id.increment);
+            mIncrementButton.setOnClickListener(onClickListener);
+            mIncrementButton.setOnLongClickListener(onLongClickListener);
+        } else {
+            mIncrementButton = null;
+        }
+
+        // decrement button
+        if (!mHasSelectorWheel) {
+            mDecrementButton = findViewById(R.id.decrement);
+            mDecrementButton.setOnClickListener(onClickListener);
+            mDecrementButton.setOnLongClickListener(onLongClickListener);
+        } else {
+            mDecrementButton = null;
+        }
+
+        // input text
+        mInputText = findViewById(R.id.numberpicker_input);
+        mInputText.setOnFocusChangeListener(new OnFocusChangeListener() {
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (hasFocus) {
+                    mInputText.selectAll();
+                } else {
+                    mInputText.setSelection(0, 0);
+                    validateInputTextView(v);
+                }
+            }
+        });
+        mInputText.setFilters(new InputFilter[] {
+            new InputTextFilter()
+        });
+        mInputText.setAccessibilityLiveRegion(View.ACCESSIBILITY_LIVE_REGION_POLITE);
+
+        mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+        mInputText.setImeOptions(EditorInfo.IME_ACTION_DONE);
+
+        // initialize constants
+        ViewConfiguration configuration = ViewConfiguration.get(context);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumFlingVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumFlingVelocity = configuration.getScaledMaximumFlingVelocity()
+                / SELECTOR_MAX_FLING_VELOCITY_ADJUSTMENT;
+        mTextSize = (int) mInputText.getTextSize();
+
+        // create the selector wheel paint
+        Paint paint = new Paint();
+        paint.setAntiAlias(true);
+        paint.setTextAlign(Align.CENTER);
+        paint.setTextSize(mTextSize);
+        paint.setTypeface(mInputText.getTypeface());
+        ColorStateList colors = mInputText.getTextColors();
+        int color = colors.getColorForState(ENABLED_STATE_SET, Color.WHITE);
+        paint.setColor(color);
+        mSelectorWheelPaint = paint;
+
+        // create the fling and adjust scrollers
+        mFlingScroller = new Scroller(getContext(), null, true);
+        mAdjustScroller = new Scroller(getContext(), new DecelerateInterpolator(2.5f));
+
+        updateInputTextView();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        // Should be focusable by default, as the text view whose visibility changes is focusable
+        if (getFocusable() == View.FOCUSABLE_AUTO) {
+            setFocusable(View.FOCUSABLE);
+            setFocusableInTouchMode(true);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (!mHasSelectorWheel) {
+            super.onLayout(changed, left, top, right, bottom);
+            return;
+        }
+        final int msrdWdth = getMeasuredWidth();
+        final int msrdHght = getMeasuredHeight();
+
+        // Input text centered horizontally.
+        final int inptTxtMsrdWdth = mInputText.getMeasuredWidth();
+        final int inptTxtMsrdHght = mInputText.getMeasuredHeight();
+        final int inptTxtLeft = (msrdWdth - inptTxtMsrdWdth) / 2;
+        final int inptTxtTop = (msrdHght - inptTxtMsrdHght) / 2;
+        final int inptTxtRight = inptTxtLeft + inptTxtMsrdWdth;
+        final int inptTxtBottom = inptTxtTop + inptTxtMsrdHght;
+        mInputText.layout(inptTxtLeft, inptTxtTop, inptTxtRight, inptTxtBottom);
+
+        if (changed) {
+            // need to do all this when we know our size
+            initializeSelectorWheel();
+            initializeFadingEdges();
+            mTopSelectionDividerTop = (getHeight() - mSelectionDividersDistance) / 2
+                    - mSelectionDividerHeight;
+            mBottomSelectionDividerBottom = mTopSelectionDividerTop + 2 * mSelectionDividerHeight
+                    + mSelectionDividersDistance;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (!mHasSelectorWheel) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+        // Try greedily to fit the max width and height.
+        final int newWidthMeasureSpec = makeMeasureSpec(widthMeasureSpec, mMaxWidth);
+        final int newHeightMeasureSpec = makeMeasureSpec(heightMeasureSpec, mMaxHeight);
+        super.onMeasure(newWidthMeasureSpec, newHeightMeasureSpec);
+        // Flag if we are measured with width or height less than the respective min.
+        final int widthSize = resolveSizeAndStateRespectingMinSize(mMinWidth, getMeasuredWidth(),
+                widthMeasureSpec);
+        final int heightSize = resolveSizeAndStateRespectingMinSize(mMinHeight, getMeasuredHeight(),
+                heightMeasureSpec);
+        setMeasuredDimension(widthSize, heightSize);
+    }
+
+    /**
+     * Move to the final position of a scroller. Ensures to force finish the scroller
+     * and if it is not at its final position a scroll of the selector wheel is
+     * performed to fast forward to the final position.
+     *
+     * @param scroller The scroller to whose final position to get.
+     * @return True of the a move was performed, i.e. the scroller was not in final position.
+     */
+    private boolean moveToFinalScrollerPosition(Scroller scroller) {
+        scroller.forceFinished(true);
+        int amountToScroll = scroller.getFinalY() - scroller.getCurrY();
+        int futureScrollOffset = (mCurrentScrollOffset + amountToScroll) % mSelectorElementHeight;
+        int overshootAdjustment = mInitialScrollOffset - futureScrollOffset;
+        if (overshootAdjustment != 0) {
+            if (Math.abs(overshootAdjustment) > mSelectorElementHeight / 2) {
+                if (overshootAdjustment > 0) {
+                    overshootAdjustment -= mSelectorElementHeight;
+                } else {
+                    overshootAdjustment += mSelectorElementHeight;
+                }
+            }
+            amountToScroll += overshootAdjustment;
+            scrollBy(0, amountToScroll);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (!mHasSelectorWheel || !isEnabled()) {
+            return false;
+        }
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                removeAllCallbacks();
+                hideSoftInput();
+                mLastDownOrMoveEventY = mLastDownEventY = event.getY();
+                mLastDownEventTime = event.getEventTime();
+                mIgnoreMoveEvents = false;
+                mPerformClickOnTap = false;
+                // Handle pressed state before any state change.
+                if (mLastDownEventY < mTopSelectionDividerTop) {
+                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+                        mPressedStateHelper.buttonPressDelayed(
+                                PressedStateHelper.BUTTON_DECREMENT);
+                    }
+                } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
+                    if (mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+                        mPressedStateHelper.buttonPressDelayed(
+                                PressedStateHelper.BUTTON_INCREMENT);
+                    }
+                }
+                // Make sure we support flinging inside scrollables.
+                getParent().requestDisallowInterceptTouchEvent(true);
+                if (!mFlingScroller.isFinished()) {
+                    mFlingScroller.forceFinished(true);
+                    mAdjustScroller.forceFinished(true);
+                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                } else if (!mAdjustScroller.isFinished()) {
+                    mFlingScroller.forceFinished(true);
+                    mAdjustScroller.forceFinished(true);
+                } else if (mLastDownEventY < mTopSelectionDividerTop) {
+                    postChangeCurrentByOneFromLongPress(
+                            false, ViewConfiguration.getLongPressTimeout());
+                } else if (mLastDownEventY > mBottomSelectionDividerBottom) {
+                    postChangeCurrentByOneFromLongPress(
+                            true, ViewConfiguration.getLongPressTimeout());
+                } else {
+                    mPerformClickOnTap = true;
+                    postBeginSoftInputOnLongPressCommand();
+                }
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!isEnabled() || !mHasSelectorWheel) {
+            return false;
+        }
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(event);
+        int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_MOVE: {
+                if (mIgnoreMoveEvents) {
+                    break;
+                }
+                float currentMoveY = event.getY();
+                if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+                    int deltaDownY = (int) Math.abs(currentMoveY - mLastDownEventY);
+                    if (deltaDownY > mTouchSlop) {
+                        removeAllCallbacks();
+                        onScrollStateChange(OnScrollListener.SCROLL_STATE_TOUCH_SCROLL);
+                    }
+                } else {
+                    int deltaMoveY = (int) ((currentMoveY - mLastDownOrMoveEventY));
+                    scrollBy(0, deltaMoveY);
+                    invalidate();
+                }
+                mLastDownOrMoveEventY = currentMoveY;
+            } break;
+            case MotionEvent.ACTION_UP: {
+                removeBeginSoftInputCommand();
+                removeChangeCurrentByOneFromLongPress();
+                mPressedStateHelper.cancel();
+                VelocityTracker velocityTracker = mVelocityTracker;
+                velocityTracker.computeCurrentVelocity(1000, mMaximumFlingVelocity);
+                int initialVelocity = (int) velocityTracker.getYVelocity();
+                if (Math.abs(initialVelocity) > mMinimumFlingVelocity) {
+                    fling(initialVelocity);
+                    onScrollStateChange(OnScrollListener.SCROLL_STATE_FLING);
+                } else {
+                    int eventY = (int) event.getY();
+                    int deltaMoveY = (int) Math.abs(eventY - mLastDownEventY);
+                    long deltaTime = event.getEventTime() - mLastDownEventTime;
+                    if (deltaMoveY <= mTouchSlop && deltaTime < ViewConfiguration.getTapTimeout()) {
+                        if (mPerformClickOnTap) {
+                            mPerformClickOnTap = false;
+                            performClick();
+                        } else {
+                            int selectorIndexOffset = (eventY / mSelectorElementHeight)
+                                    - SELECTOR_MIDDLE_ITEM_INDEX;
+                            if (selectorIndexOffset > 0) {
+                                changeValueByOne(true);
+                                mPressedStateHelper.buttonTapped(
+                                        PressedStateHelper.BUTTON_INCREMENT);
+                            } else if (selectorIndexOffset < 0) {
+                                changeValueByOne(false);
+                                mPressedStateHelper.buttonTapped(
+                                        PressedStateHelper.BUTTON_DECREMENT);
+                            }
+                        }
+                    } else {
+                        ensureScrollWheelAdjusted();
+                    }
+                    onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+                }
+                mVelocityTracker.recycle();
+                mVelocityTracker = null;
+            } break;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean dispatchTouchEvent(MotionEvent event) {
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                removeAllCallbacks();
+                break;
+        }
+        return super.dispatchTouchEvent(event);
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        final int keyCode = event.getKeyCode();
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_ENTER:
+                removeAllCallbacks();
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_UP:
+                if (!mHasSelectorWheel) {
+                    break;
+                }
+                switch (event.getAction()) {
+                    case KeyEvent.ACTION_DOWN:
+                        if (mWrapSelectorWheel || ((keyCode == KeyEvent.KEYCODE_DPAD_DOWN)
+                                ? getValue() < getMaxValue() : getValue() > getMinValue())) {
+                            requestFocus();
+                            mLastHandledDownDpadKeyCode = keyCode;
+                            removeAllCallbacks();
+                            if (mFlingScroller.isFinished()) {
+                                changeValueByOne(keyCode == KeyEvent.KEYCODE_DPAD_DOWN);
+                            }
+                            return true;
+                        }
+                        break;
+                    case KeyEvent.ACTION_UP:
+                        if (mLastHandledDownDpadKeyCode == keyCode) {
+                            mLastHandledDownDpadKeyCode = -1;
+                            return true;
+                        }
+                        break;
+                }
+        }
+        return super.dispatchKeyEvent(event);
+    }
+
+    @Override
+    public boolean dispatchTrackballEvent(MotionEvent event) {
+        final int action = event.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                removeAllCallbacks();
+                break;
+        }
+        return super.dispatchTrackballEvent(event);
+    }
+
+    @Override
+    protected boolean dispatchHoverEvent(MotionEvent event) {
+        if (!mHasSelectorWheel) {
+            return super.dispatchHoverEvent(event);
+        }
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            final int eventY = (int) event.getY();
+            final int hoveredVirtualViewId;
+            if (eventY < mTopSelectionDividerTop) {
+                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_DECREMENT;
+            } else if (eventY > mBottomSelectionDividerBottom) {
+                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INCREMENT;
+            } else {
+                hoveredVirtualViewId = AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT;
+            }
+            final int action = event.getActionMasked();
+            AccessibilityNodeProviderImpl provider =
+                (AccessibilityNodeProviderImpl) getAccessibilityNodeProvider();
+            switch (action) {
+                case MotionEvent.ACTION_HOVER_ENTER: {
+                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+                            AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+                    mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
+                    provider.performAction(hoveredVirtualViewId,
+                            AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+                } break;
+                case MotionEvent.ACTION_HOVER_MOVE: {
+                    if (mLastHoveredChildVirtualViewId != hoveredVirtualViewId
+                            && mLastHoveredChildVirtualViewId != View.NO_ID) {
+                        provider.sendAccessibilityEventForVirtualView(
+                                mLastHoveredChildVirtualViewId,
+                                AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+                        provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+                                AccessibilityEvent.TYPE_VIEW_HOVER_ENTER);
+                        mLastHoveredChildVirtualViewId = hoveredVirtualViewId;
+                        provider.performAction(hoveredVirtualViewId,
+                                AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS, null);
+                    }
+                } break;
+                case MotionEvent.ACTION_HOVER_EXIT: {
+                    provider.sendAccessibilityEventForVirtualView(hoveredVirtualViewId,
+                            AccessibilityEvent.TYPE_VIEW_HOVER_EXIT);
+                    mLastHoveredChildVirtualViewId = View.NO_ID;
+                } break;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public void computeScroll() {
+        Scroller scroller = mFlingScroller;
+        if (scroller.isFinished()) {
+            scroller = mAdjustScroller;
+            if (scroller.isFinished()) {
+                return;
+            }
+        }
+        scroller.computeScrollOffset();
+        int currentScrollerY = scroller.getCurrY();
+        if (mPreviousScrollerY == 0) {
+            mPreviousScrollerY = scroller.getStartY();
+        }
+        scrollBy(0, currentScrollerY - mPreviousScrollerY);
+        mPreviousScrollerY = currentScrollerY;
+        if (scroller.isFinished()) {
+            onScrollerFinished(scroller);
+        } else {
+            invalidate();
+        }
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        if (!mHasSelectorWheel) {
+            mIncrementButton.setEnabled(enabled);
+        }
+        if (!mHasSelectorWheel) {
+            mDecrementButton.setEnabled(enabled);
+        }
+        mInputText.setEnabled(enabled);
+    }
+
+    @Override
+    public void scrollBy(int x, int y) {
+        int[] selectorIndices = mSelectorIndices;
+        int startScrollOffset = mCurrentScrollOffset;
+        if (!mWrapSelectorWheel && y > 0
+                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
+            mCurrentScrollOffset = mInitialScrollOffset;
+            return;
+        }
+        if (!mWrapSelectorWheel && y < 0
+                && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
+            mCurrentScrollOffset = mInitialScrollOffset;
+            return;
+        }
+        mCurrentScrollOffset += y;
+        while (mCurrentScrollOffset - mInitialScrollOffset > mSelectorTextGapHeight) {
+            mCurrentScrollOffset -= mSelectorElementHeight;
+            decrementSelectorIndices(selectorIndices);
+            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
+            if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] <= mMinValue) {
+                mCurrentScrollOffset = mInitialScrollOffset;
+            }
+        }
+        while (mCurrentScrollOffset - mInitialScrollOffset < -mSelectorTextGapHeight) {
+            mCurrentScrollOffset += mSelectorElementHeight;
+            incrementSelectorIndices(selectorIndices);
+            setValueInternal(selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX], true);
+            if (!mWrapSelectorWheel && selectorIndices[SELECTOR_MIDDLE_ITEM_INDEX] >= mMaxValue) {
+                mCurrentScrollOffset = mInitialScrollOffset;
+            }
+        }
+        if (startScrollOffset != mCurrentScrollOffset) {
+            onScrollChanged(0, mCurrentScrollOffset, 0, startScrollOffset);
+        }
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        return mCurrentScrollOffset;
+    }
+
+    @Override
+    protected int computeVerticalScrollRange() {
+        return (mMaxValue - mMinValue + 1) * mSelectorElementHeight;
+    }
+
+    @Override
+    protected int computeVerticalScrollExtent() {
+        return getHeight();
+    }
+
+    @Override
+    public int getSolidColor() {
+        return mSolidColor;
+    }
+
+    /**
+     * Sets the listener to be notified on change of the current value.
+     *
+     * @param onValueChangedListener The listener.
+     */
+    public void setOnValueChangedListener(OnValueChangeListener onValueChangedListener) {
+        mOnValueChangeListener = onValueChangedListener;
+    }
+
+    /**
+     * Set listener to be notified for scroll state changes.
+     *
+     * @param onScrollListener The listener.
+     */
+    public void setOnScrollListener(OnScrollListener onScrollListener) {
+        mOnScrollListener = onScrollListener;
+    }
+
+    /**
+     * Set the formatter to be used for formatting the current value.
+     * <p>
+     * Note: If you have provided alternative values for the values this
+     * formatter is never invoked.
+     * </p>
+     *
+     * @param formatter The formatter object. If formatter is <code>null</code>,
+     *            {@link String#valueOf(int)} will be used.
+     *@see #setDisplayedValues(String[])
+     */
+    public void setFormatter(Formatter formatter) {
+        if (formatter == mFormatter) {
+            return;
+        }
+        mFormatter = formatter;
+        initializeSelectorWheelIndices();
+        updateInputTextView();
+    }
+
+    /**
+     * Set the current value for the number picker.
+     * <p>
+     * If the argument is less than the {@link NumberPicker#getMinValue()} and
+     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
+     * current value is set to the {@link NumberPicker#getMinValue()} value.
+     * </p>
+     * <p>
+     * If the argument is less than the {@link NumberPicker#getMinValue()} and
+     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
+     * current value is set to the {@link NumberPicker#getMaxValue()} value.
+     * </p>
+     * <p>
+     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
+     * {@link NumberPicker#getWrapSelectorWheel()} is <code>false</code> the
+     * current value is set to the {@link NumberPicker#getMaxValue()} value.
+     * </p>
+     * <p>
+     * If the argument is less than the {@link NumberPicker#getMaxValue()} and
+     * {@link NumberPicker#getWrapSelectorWheel()} is <code>true</code> the
+     * current value is set to the {@link NumberPicker#getMinValue()} value.
+     * </p>
+     *
+     * @param value The current value.
+     * @see #setWrapSelectorWheel(boolean)
+     * @see #setMinValue(int)
+     * @see #setMaxValue(int)
+     */
+    public void setValue(int value) {
+        setValueInternal(value, false);
+    }
+
+    @Override
+    public boolean performClick() {
+        if (!mHasSelectorWheel) {
+            return super.performClick();
+        } else if (!super.performClick()) {
+            showSoftInput();
+        }
+        return true;
+    }
+
+    @Override
+    public boolean performLongClick() {
+        if (!mHasSelectorWheel) {
+            return super.performLongClick();
+        } else if (!super.performLongClick()) {
+            showSoftInput();
+            mIgnoreMoveEvents = true;
+        }
+        return true;
+    }
+
+    /**
+     * Shows the soft input for its input text.
+     */
+    private void showSoftInput() {
+        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+        if (inputMethodManager != null) {
+            if (mHasSelectorWheel) {
+                mInputText.setVisibility(View.VISIBLE);
+            }
+            mInputText.requestFocus();
+            inputMethodManager.showSoftInput(mInputText, 0);
+        }
+    }
+
+    /**
+     * Hides the soft input if it is active for the input text.
+     */
+    private void hideSoftInput() {
+        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+        if (inputMethodManager != null && inputMethodManager.isActive(mInputText)) {
+            inputMethodManager.hideSoftInputFromWindow(getWindowToken(), 0);
+        }
+        if (mHasSelectorWheel) {
+            mInputText.setVisibility(View.INVISIBLE);
+        }
+    }
+
+    /**
+     * Computes the max width if no such specified as an attribute.
+     */
+    private void tryComputeMaxWidth() {
+        if (!mComputeMaxWidth) {
+            return;
+        }
+        int maxTextWidth = 0;
+        if (mDisplayedValues == null) {
+            float maxDigitWidth = 0;
+            for (int i = 0; i <= 9; i++) {
+                final float digitWidth = mSelectorWheelPaint.measureText(formatNumberWithLocale(i));
+                if (digitWidth > maxDigitWidth) {
+                    maxDigitWidth = digitWidth;
+                }
+            }
+            int numberOfDigits = 0;
+            int current = mMaxValue;
+            while (current > 0) {
+                numberOfDigits++;
+                current = current / 10;
+            }
+            maxTextWidth = (int) (numberOfDigits * maxDigitWidth);
+        } else {
+            final int valueCount = mDisplayedValues.length;
+            for (int i = 0; i < valueCount; i++) {
+                final float textWidth = mSelectorWheelPaint.measureText(mDisplayedValues[i]);
+                if (textWidth > maxTextWidth) {
+                    maxTextWidth = (int) textWidth;
+                }
+            }
+        }
+        maxTextWidth += mInputText.getPaddingLeft() + mInputText.getPaddingRight();
+        if (mMaxWidth != maxTextWidth) {
+            if (maxTextWidth > mMinWidth) {
+                mMaxWidth = maxTextWidth;
+            } else {
+                mMaxWidth = mMinWidth;
+            }
+            invalidate();
+        }
+    }
+
+    /**
+     * Gets whether the selector wheel wraps when reaching the min/max value.
+     *
+     * @return True if the selector wheel wraps.
+     *
+     * @see #getMinValue()
+     * @see #getMaxValue()
+     */
+    public boolean getWrapSelectorWheel() {
+        return mWrapSelectorWheel;
+    }
+
+    /**
+     * Sets whether the selector wheel shown during flinging/scrolling should
+     * wrap around the {@link NumberPicker#getMinValue()} and
+     * {@link NumberPicker#getMaxValue()} values.
+     * <p>
+     * By default if the range (max - min) is more than the number of items shown
+     * on the selector wheel the selector wheel wrapping is enabled.
+     * </p>
+     * <p>
+     * <strong>Note:</strong> If the number of items, i.e. the range (
+     * {@link #getMaxValue()} - {@link #getMinValue()}) is less than
+     * the number of items shown on the selector wheel, the selector wheel will
+     * not wrap. Hence, in such a case calling this method is a NOP.
+     * </p>
+     *
+     * @param wrapSelectorWheel Whether to wrap.
+     */
+    public void setWrapSelectorWheel(boolean wrapSelectorWheel) {
+        mWrapSelectorWheelPreferred = wrapSelectorWheel;
+        updateWrapSelectorWheel();
+
+    }
+
+    /**
+     * Whether or not the selector wheel should be wrapped is determined by user choice and whether
+     * the choice is allowed. The former comes from {@link #setWrapSelectorWheel(boolean)}, the
+     * latter is calculated based on min & max value set vs selector's visual length. Therefore,
+     * this method should be called any time any of the 3 values (i.e. user choice, min and max
+     * value) gets updated.
+     */
+    private void updateWrapSelectorWheel() {
+        final boolean wrappingAllowed = (mMaxValue - mMinValue) >= mSelectorIndices.length;
+        mWrapSelectorWheel = wrappingAllowed && mWrapSelectorWheelPreferred;
+    }
+
+    /**
+     * Sets the speed at which the numbers be incremented and decremented when
+     * the up and down buttons are long pressed respectively.
+     * <p>
+     * The default value is 300 ms.
+     * </p>
+     *
+     * @param intervalMillis The speed (in milliseconds) at which the numbers
+     *            will be incremented and decremented.
+     */
+    public void setOnLongPressUpdateInterval(long intervalMillis) {
+        mLongPressUpdateInterval = intervalMillis;
+    }
+
+    /**
+     * Returns the value of the picker.
+     *
+     * @return The value.
+     */
+    public int getValue() {
+        return mValue;
+    }
+
+    /**
+     * Returns the min value of the picker.
+     *
+     * @return The min value
+     */
+    public int getMinValue() {
+        return mMinValue;
+    }
+
+    /**
+     * Sets the min value of the picker.
+     *
+     * @param minValue The min value inclusive.
+     *
+     * <strong>Note:</strong> The length of the displayed values array
+     * set via {@link #setDisplayedValues(String[])} must be equal to the
+     * range of selectable numbers which is equal to
+     * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+     */
+    public void setMinValue(int minValue) {
+        if (mMinValue == minValue) {
+            return;
+        }
+        if (minValue < 0) {
+            throw new IllegalArgumentException("minValue must be >= 0");
+        }
+        mMinValue = minValue;
+        if (mMinValue > mValue) {
+            mValue = mMinValue;
+        }
+        updateWrapSelectorWheel();
+        initializeSelectorWheelIndices();
+        updateInputTextView();
+        tryComputeMaxWidth();
+        invalidate();
+    }
+
+    /**
+     * Returns the max value of the picker.
+     *
+     * @return The max value.
+     */
+    public int getMaxValue() {
+        return mMaxValue;
+    }
+
+    /**
+     * Sets the max value of the picker.
+     *
+     * @param maxValue The max value inclusive.
+     *
+     * <strong>Note:</strong> The length of the displayed values array
+     * set via {@link #setDisplayedValues(String[])} must be equal to the
+     * range of selectable numbers which is equal to
+     * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+     */
+    public void setMaxValue(int maxValue) {
+        if (mMaxValue == maxValue) {
+            return;
+        }
+        if (maxValue < 0) {
+            throw new IllegalArgumentException("maxValue must be >= 0");
+        }
+        mMaxValue = maxValue;
+        if (mMaxValue < mValue) {
+            mValue = mMaxValue;
+        }
+        updateWrapSelectorWheel();
+        initializeSelectorWheelIndices();
+        updateInputTextView();
+        tryComputeMaxWidth();
+        invalidate();
+    }
+
+    /**
+     * Gets the values to be displayed instead of string values.
+     *
+     * @return The displayed values.
+     */
+    public String[] getDisplayedValues() {
+        return mDisplayedValues;
+    }
+
+    /**
+     * Sets the values to be displayed.
+     *
+     * @param displayedValues The displayed values.
+     *
+     * <strong>Note:</strong> The length of the displayed values array
+     * must be equal to the range of selectable numbers which is equal to
+     * {@link #getMaxValue()} - {@link #getMinValue()} + 1.
+     */
+    public void setDisplayedValues(String[] displayedValues) {
+        if (mDisplayedValues == displayedValues) {
+            return;
+        }
+        mDisplayedValues = displayedValues;
+        if (mDisplayedValues != null) {
+            // Allow text entry rather than strictly numeric entry.
+            mInputText.setRawInputType(InputType.TYPE_CLASS_TEXT
+                    | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS);
+        } else {
+            mInputText.setRawInputType(InputType.TYPE_CLASS_NUMBER);
+        }
+        updateInputTextView();
+        initializeSelectorWheelIndices();
+        tryComputeMaxWidth();
+    }
+
+    /**
+     * Retrieves the displayed value for the current selection in this picker.
+     *
+     * @hide
+     */
+    @TestApi
+    public CharSequence getDisplayedValueForCurrentSelection() {
+        // The cache field itself is initialized at declaration time, and since it's final, it
+        // can't be null here. The cache is updated in ensureCachedScrollSelectorValue which is
+        // called, directly or indirectly, on every call to setDisplayedValues, setFormatter,
+        // setMinValue, setMaxValue and setValue, as well as user-driven interaction with the
+        // picker. As such, the contents of the cache are always synced to the latest state of
+        // the widget.
+        return mSelectorIndexToStringCache.get(getValue());
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
+    }
+
+    @Override
+    protected float getBottomFadingEdgeStrength() {
+        return TOP_AND_BOTTOM_FADING_EDGE_STRENGTH;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        removeAllCallbacks();
+    }
+
+    @CallSuper
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable selectionDivider = mSelectionDivider;
+        if (selectionDivider != null && selectionDivider.isStateful()
+                && selectionDivider.setState(getDrawableState())) {
+            invalidateDrawable(selectionDivider);
+        }
+    }
+
+    @CallSuper
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+
+        if (mSelectionDivider != null) {
+            mSelectionDivider.jumpToCurrentState();
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onResolveDrawables(@ResolvedLayoutDir int layoutDirection) {
+        super.onResolveDrawables(layoutDirection);
+
+        if (mSelectionDivider != null) {
+            mSelectionDivider.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        if (!mHasSelectorWheel) {
+            super.onDraw(canvas);
+            return;
+        }
+        final boolean showSelectorWheel = mHideWheelUntilFocused ? hasFocus() : true;
+        float x = (mRight - mLeft) / 2;
+        float y = mCurrentScrollOffset;
+
+        // draw the virtual buttons pressed state if needed
+        if (showSelectorWheel && mVirtualButtonPressedDrawable != null
+                && mScrollState == OnScrollListener.SCROLL_STATE_IDLE) {
+            if (mDecrementVirtualButtonPressed) {
+                mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
+                mVirtualButtonPressedDrawable.setBounds(0, 0, mRight, mTopSelectionDividerTop);
+                mVirtualButtonPressedDrawable.draw(canvas);
+            }
+            if (mIncrementVirtualButtonPressed) {
+                mVirtualButtonPressedDrawable.setState(PRESSED_STATE_SET);
+                mVirtualButtonPressedDrawable.setBounds(0, mBottomSelectionDividerBottom, mRight,
+                        mBottom);
+                mVirtualButtonPressedDrawable.draw(canvas);
+            }
+        }
+
+        // draw the selector wheel
+        int[] selectorIndices = mSelectorIndices;
+        for (int i = 0; i < selectorIndices.length; i++) {
+            int selectorIndex = selectorIndices[i];
+            String scrollSelectorValue = mSelectorIndexToStringCache.get(selectorIndex);
+            // Do not draw the middle item if input is visible since the input
+            // is shown only if the wheel is static and it covers the middle
+            // item. Otherwise, if the user starts editing the text via the
+            // IME he may see a dimmed version of the old value intermixed
+            // with the new one.
+            if ((showSelectorWheel && i != SELECTOR_MIDDLE_ITEM_INDEX) ||
+                (i == SELECTOR_MIDDLE_ITEM_INDEX && mInputText.getVisibility() != VISIBLE)) {
+                canvas.drawText(scrollSelectorValue, x, y, mSelectorWheelPaint);
+            }
+            y += mSelectorElementHeight;
+        }
+
+        // draw the selection dividers
+        if (showSelectorWheel && mSelectionDivider != null) {
+            // draw the top divider
+            int topOfTopDivider = mTopSelectionDividerTop;
+            int bottomOfTopDivider = topOfTopDivider + mSelectionDividerHeight;
+            mSelectionDivider.setBounds(0, topOfTopDivider, mRight, bottomOfTopDivider);
+            mSelectionDivider.draw(canvas);
+
+            // draw the bottom divider
+            int bottomOfBottomDivider = mBottomSelectionDividerBottom;
+            int topOfBottomDivider = bottomOfBottomDivider - mSelectionDividerHeight;
+            mSelectionDivider.setBounds(0, topOfBottomDivider, mRight, bottomOfBottomDivider);
+            mSelectionDivider.draw(canvas);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setClassName(NumberPicker.class.getName());
+        event.setScrollable(true);
+        event.setScrollY((mMinValue + mValue) * mSelectorElementHeight);
+        event.setMaxScrollY((mMaxValue - mMinValue) * mSelectorElementHeight);
+    }
+
+    @Override
+    public AccessibilityNodeProvider getAccessibilityNodeProvider() {
+        if (!mHasSelectorWheel) {
+            return super.getAccessibilityNodeProvider();
+        }
+        if (mAccessibilityNodeProvider == null) {
+            mAccessibilityNodeProvider = new AccessibilityNodeProviderImpl();
+        }
+        return mAccessibilityNodeProvider;
+    }
+
+    /**
+     * Makes a measure spec that tries greedily to use the max value.
+     *
+     * @param measureSpec The measure spec.
+     * @param maxSize The max value for the size.
+     * @return A measure spec greedily imposing the max size.
+     */
+    private int makeMeasureSpec(int measureSpec, int maxSize) {
+        if (maxSize == SIZE_UNSPECIFIED) {
+            return measureSpec;
+        }
+        final int size = MeasureSpec.getSize(measureSpec);
+        final int mode = MeasureSpec.getMode(measureSpec);
+        switch (mode) {
+            case MeasureSpec.EXACTLY:
+                return measureSpec;
+            case MeasureSpec.AT_MOST:
+                return MeasureSpec.makeMeasureSpec(Math.min(size, maxSize), MeasureSpec.EXACTLY);
+            case MeasureSpec.UNSPECIFIED:
+                return MeasureSpec.makeMeasureSpec(maxSize, MeasureSpec.EXACTLY);
+            default:
+                throw new IllegalArgumentException("Unknown measure mode: " + mode);
+        }
+    }
+
+    /**
+     * Utility to reconcile a desired size and state, with constraints imposed
+     * by a MeasureSpec. Tries to respect the min size, unless a different size
+     * is imposed by the constraints.
+     *
+     * @param minSize The minimal desired size.
+     * @param measuredSize The currently measured size.
+     * @param measureSpec The current measure spec.
+     * @return The resolved size and state.
+     */
+    private int resolveSizeAndStateRespectingMinSize(
+            int minSize, int measuredSize, int measureSpec) {
+        if (minSize != SIZE_UNSPECIFIED) {
+            final int desiredWidth = Math.max(minSize, measuredSize);
+            return resolveSizeAndState(desiredWidth, measureSpec, 0);
+        } else {
+            return measuredSize;
+        }
+    }
+
+    /**
+     * Resets the selector indices and clear the cached string representation of
+     * these indices.
+     */
+    private void initializeSelectorWheelIndices() {
+        mSelectorIndexToStringCache.clear();
+        int[] selectorIndices = mSelectorIndices;
+        int current = getValue();
+        for (int i = 0; i < mSelectorIndices.length; i++) {
+            int selectorIndex = current + (i - SELECTOR_MIDDLE_ITEM_INDEX);
+            if (mWrapSelectorWheel) {
+                selectorIndex = getWrappedSelectorIndex(selectorIndex);
+            }
+            selectorIndices[i] = selectorIndex;
+            ensureCachedScrollSelectorValue(selectorIndices[i]);
+        }
+    }
+
+    /**
+     * Sets the current value of this NumberPicker.
+     *
+     * @param current The new value of the NumberPicker.
+     * @param notifyChange Whether to notify if the current value changed.
+     */
+    private void setValueInternal(int current, boolean notifyChange) {
+        if (mValue == current) {
+            return;
+        }
+        // Wrap around the values if we go past the start or end
+        if (mWrapSelectorWheel) {
+            current = getWrappedSelectorIndex(current);
+        } else {
+            current = Math.max(current, mMinValue);
+            current = Math.min(current, mMaxValue);
+        }
+        int previous = mValue;
+        mValue = current;
+        // If we're flinging, we'll update the text view at the end when it becomes visible
+        if (mScrollState != OnScrollListener.SCROLL_STATE_FLING) {
+            updateInputTextView();
+        }
+        if (notifyChange) {
+            notifyChange(previous, current);
+        }
+        initializeSelectorWheelIndices();
+        invalidate();
+    }
+
+    /**
+     * Changes the current value by one which is increment or
+     * decrement based on the passes argument.
+     * decrement the current value.
+     *
+     * @param increment True to increment, false to decrement.
+     */
+     private void changeValueByOne(boolean increment) {
+        if (mHasSelectorWheel) {
+            hideSoftInput();
+            if (!moveToFinalScrollerPosition(mFlingScroller)) {
+                moveToFinalScrollerPosition(mAdjustScroller);
+            }
+            mPreviousScrollerY = 0;
+            if (increment) {
+                mFlingScroller.startScroll(0, 0, 0, -mSelectorElementHeight, SNAP_SCROLL_DURATION);
+            } else {
+                mFlingScroller.startScroll(0, 0, 0, mSelectorElementHeight, SNAP_SCROLL_DURATION);
+            }
+            invalidate();
+        } else {
+            if (increment) {
+                setValueInternal(mValue + 1, true);
+            } else {
+                setValueInternal(mValue - 1, true);
+            }
+        }
+    }
+
+    private void initializeSelectorWheel() {
+        initializeSelectorWheelIndices();
+        int[] selectorIndices = mSelectorIndices;
+        int totalTextHeight = selectorIndices.length * mTextSize;
+        float totalTextGapHeight = (mBottom - mTop) - totalTextHeight;
+        float textGapCount = selectorIndices.length;
+        mSelectorTextGapHeight = (int) (totalTextGapHeight / textGapCount + 0.5f);
+        mSelectorElementHeight = mTextSize + mSelectorTextGapHeight;
+        // Ensure that the middle item is positioned the same as the text in
+        // mInputText
+        int editTextTextPosition = mInputText.getBaseline() + mInputText.getTop();
+        mInitialScrollOffset = editTextTextPosition
+                - (mSelectorElementHeight * SELECTOR_MIDDLE_ITEM_INDEX);
+        mCurrentScrollOffset = mInitialScrollOffset;
+        updateInputTextView();
+    }
+
+    private void initializeFadingEdges() {
+        setVerticalFadingEdgeEnabled(true);
+        setFadingEdgeLength((mBottom - mTop - mTextSize) / 2);
+    }
+
+    /**
+     * Callback invoked upon completion of a given <code>scroller</code>.
+     */
+    private void onScrollerFinished(Scroller scroller) {
+        if (scroller == mFlingScroller) {
+            ensureScrollWheelAdjusted();
+            updateInputTextView();
+            onScrollStateChange(OnScrollListener.SCROLL_STATE_IDLE);
+        } else {
+            if (mScrollState != OnScrollListener.SCROLL_STATE_TOUCH_SCROLL) {
+                updateInputTextView();
+            }
+        }
+    }
+
+    /**
+     * Handles transition to a given <code>scrollState</code>
+     */
+    private void onScrollStateChange(int scrollState) {
+        if (mScrollState == scrollState) {
+            return;
+        }
+        mScrollState = scrollState;
+        if (mOnScrollListener != null) {
+            mOnScrollListener.onScrollStateChange(this, scrollState);
+        }
+    }
+
+    /**
+     * Flings the selector with the given <code>velocityY</code>.
+     */
+    private void fling(int velocityY) {
+        mPreviousScrollerY = 0;
+
+        if (velocityY > 0) {
+            mFlingScroller.fling(0, 0, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
+        } else {
+            mFlingScroller.fling(0, Integer.MAX_VALUE, 0, velocityY, 0, 0, 0, Integer.MAX_VALUE);
+        }
+
+        invalidate();
+    }
+
+    /**
+     * @return The wrapped index <code>selectorIndex</code> value.
+     */
+    private int getWrappedSelectorIndex(int selectorIndex) {
+        if (selectorIndex > mMaxValue) {
+            return mMinValue + (selectorIndex - mMaxValue) % (mMaxValue - mMinValue) - 1;
+        } else if (selectorIndex < mMinValue) {
+            return mMaxValue - (mMinValue - selectorIndex) % (mMaxValue - mMinValue) + 1;
+        }
+        return selectorIndex;
+    }
+
+    /**
+     * Increments the <code>selectorIndices</code> whose string representations
+     * will be displayed in the selector.
+     */
+    private void incrementSelectorIndices(int[] selectorIndices) {
+        for (int i = 0; i < selectorIndices.length - 1; i++) {
+            selectorIndices[i] = selectorIndices[i + 1];
+        }
+        int nextScrollSelectorIndex = selectorIndices[selectorIndices.length - 2] + 1;
+        if (mWrapSelectorWheel && nextScrollSelectorIndex > mMaxValue) {
+            nextScrollSelectorIndex = mMinValue;
+        }
+        selectorIndices[selectorIndices.length - 1] = nextScrollSelectorIndex;
+        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
+    }
+
+    /**
+     * Decrements the <code>selectorIndices</code> whose string representations
+     * will be displayed in the selector.
+     */
+    private void decrementSelectorIndices(int[] selectorIndices) {
+        for (int i = selectorIndices.length - 1; i > 0; i--) {
+            selectorIndices[i] = selectorIndices[i - 1];
+        }
+        int nextScrollSelectorIndex = selectorIndices[1] - 1;
+        if (mWrapSelectorWheel && nextScrollSelectorIndex < mMinValue) {
+            nextScrollSelectorIndex = mMaxValue;
+        }
+        selectorIndices[0] = nextScrollSelectorIndex;
+        ensureCachedScrollSelectorValue(nextScrollSelectorIndex);
+    }
+
+    /**
+     * Ensures we have a cached string representation of the given <code>
+     * selectorIndex</code> to avoid multiple instantiations of the same string.
+     */
+    private void ensureCachedScrollSelectorValue(int selectorIndex) {
+        SparseArray<String> cache = mSelectorIndexToStringCache;
+        String scrollSelectorValue = cache.get(selectorIndex);
+        if (scrollSelectorValue != null) {
+            return;
+        }
+        if (selectorIndex < mMinValue || selectorIndex > mMaxValue) {
+            scrollSelectorValue = "";
+        } else {
+            if (mDisplayedValues != null) {
+                int displayedValueIndex = selectorIndex - mMinValue;
+                scrollSelectorValue = mDisplayedValues[displayedValueIndex];
+            } else {
+                scrollSelectorValue = formatNumber(selectorIndex);
+            }
+        }
+        cache.put(selectorIndex, scrollSelectorValue);
+    }
+
+    private String formatNumber(int value) {
+        return (mFormatter != null) ? mFormatter.format(value) : formatNumberWithLocale(value);
+    }
+
+    private void validateInputTextView(View v) {
+        String str = String.valueOf(((TextView) v).getText());
+        if (TextUtils.isEmpty(str)) {
+            // Restore to the old value as we don't allow empty values
+            updateInputTextView();
+        } else {
+            // Check the new value and ensure it's in range
+            int current = getSelectedPos(str.toString());
+            setValueInternal(current, true);
+        }
+    }
+
+    /**
+     * Updates the view of this NumberPicker. If displayValues were specified in
+     * the string corresponding to the index specified by the current value will
+     * be returned. Otherwise, the formatter specified in {@link #setFormatter}
+     * will be used to format the number.
+     *
+     * @return Whether the text was updated.
+     */
+    private boolean updateInputTextView() {
+        /*
+         * If we don't have displayed values then use the current number else
+         * find the correct value in the displayed values for the current
+         * number.
+         */
+        String text = (mDisplayedValues == null) ? formatNumber(mValue)
+                : mDisplayedValues[mValue - mMinValue];
+        if (!TextUtils.isEmpty(text)) {
+            CharSequence beforeText = mInputText.getText();
+            if (!text.equals(beforeText.toString())) {
+                mInputText.setText(text);
+                if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                    AccessibilityEvent event = AccessibilityEvent.obtain(
+                            AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+                    mInputText.onInitializeAccessibilityEvent(event);
+                    mInputText.onPopulateAccessibilityEvent(event);
+                    event.setFromIndex(0);
+                    event.setRemovedCount(beforeText.length());
+                    event.setAddedCount(text.length());
+                    event.setBeforeText(beforeText);
+                    event.setSource(NumberPicker.this,
+                            AccessibilityNodeProviderImpl.VIRTUAL_VIEW_ID_INPUT);
+                    requestSendAccessibilityEvent(NumberPicker.this, event);
+                }
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Notifies the listener, if registered, of a change of the value of this
+     * NumberPicker.
+     */
+    private void notifyChange(int previous, int current) {
+        if (mOnValueChangeListener != null) {
+            mOnValueChangeListener.onValueChange(this, previous, mValue);
+        }
+    }
+
+    /**
+     * Posts a command for changing the current value by one.
+     *
+     * @param increment Whether to increment or decrement the value.
+     */
+    private void postChangeCurrentByOneFromLongPress(boolean increment, long delayMillis) {
+        if (mChangeCurrentByOneFromLongPressCommand == null) {
+            mChangeCurrentByOneFromLongPressCommand = new ChangeCurrentByOneFromLongPressCommand();
+        } else {
+            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+        }
+        mChangeCurrentByOneFromLongPressCommand.setStep(increment);
+        postDelayed(mChangeCurrentByOneFromLongPressCommand, delayMillis);
+    }
+
+    /**
+     * Removes the command for changing the current value by one.
+     */
+    private void removeChangeCurrentByOneFromLongPress() {
+        if (mChangeCurrentByOneFromLongPressCommand != null) {
+            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+        }
+    }
+
+    /**
+     * Posts a command for beginning an edit of the current value via IME on
+     * long press.
+     */
+    private void postBeginSoftInputOnLongPressCommand() {
+        if (mBeginSoftInputOnLongPressCommand == null) {
+            mBeginSoftInputOnLongPressCommand = new BeginSoftInputOnLongPressCommand();
+        } else {
+            removeCallbacks(mBeginSoftInputOnLongPressCommand);
+        }
+        postDelayed(mBeginSoftInputOnLongPressCommand, ViewConfiguration.getLongPressTimeout());
+    }
+
+    /**
+     * Removes the command for beginning an edit of the current value via IME.
+     */
+    private void removeBeginSoftInputCommand() {
+        if (mBeginSoftInputOnLongPressCommand != null) {
+            removeCallbacks(mBeginSoftInputOnLongPressCommand);
+        }
+    }
+
+    /**
+     * Removes all pending callback from the message queue.
+     */
+    private void removeAllCallbacks() {
+        if (mChangeCurrentByOneFromLongPressCommand != null) {
+            removeCallbacks(mChangeCurrentByOneFromLongPressCommand);
+        }
+        if (mSetSelectionCommand != null) {
+            mSetSelectionCommand.cancel();
+        }
+        if (mBeginSoftInputOnLongPressCommand != null) {
+            removeCallbacks(mBeginSoftInputOnLongPressCommand);
+        }
+        mPressedStateHelper.cancel();
+    }
+
+    /**
+     * @return The selected index given its displayed <code>value</code>.
+     */
+    private int getSelectedPos(String value) {
+        if (mDisplayedValues == null) {
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+                // Ignore as if it's not a number we don't care
+            }
+        } else {
+            for (int i = 0; i < mDisplayedValues.length; i++) {
+                // Don't force the user to type in jan when ja will do
+                value = value.toLowerCase();
+                if (mDisplayedValues[i].toLowerCase().startsWith(value)) {
+                    return mMinValue + i;
+                }
+            }
+
+            /*
+             * The user might have typed in a number into the month field i.e.
+             * 10 instead of OCT so support that too.
+             */
+            try {
+                return Integer.parseInt(value);
+            } catch (NumberFormatException e) {
+
+                // Ignore as if it's not a number we don't care
+            }
+        }
+        return mMinValue;
+    }
+
+    /**
+     * Posts a {@link SetSelectionCommand} from the given
+     * {@code selectionStart} to {@code selectionEnd}.
+     */
+    private void postSetSelectionCommand(int selectionStart, int selectionEnd) {
+        if (mSetSelectionCommand == null) {
+            mSetSelectionCommand = new SetSelectionCommand(mInputText);
+        }
+        mSetSelectionCommand.post(selectionStart, selectionEnd);
+    }
+
+    /**
+     * The numbers accepted by the input text's {@link Filter}
+     */
+    private static final char[] DIGIT_CHARACTERS = new char[] {
+            // Latin digits are the common case
+            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
+            // Arabic-Indic
+            '\u0660', '\u0661', '\u0662', '\u0663', '\u0664', '\u0665', '\u0666', '\u0667', '\u0668'
+            , '\u0669',
+            // Extended Arabic-Indic
+            '\u06f0', '\u06f1', '\u06f2', '\u06f3', '\u06f4', '\u06f5', '\u06f6', '\u06f7', '\u06f8'
+            , '\u06f9',
+            // Hindi and Marathi (Devanagari script)
+            '\u0966', '\u0967', '\u0968', '\u0969', '\u096a', '\u096b', '\u096c', '\u096d', '\u096e'
+            , '\u096f',
+            // Bengali
+            '\u09e6', '\u09e7', '\u09e8', '\u09e9', '\u09ea', '\u09eb', '\u09ec', '\u09ed', '\u09ee'
+            , '\u09ef',
+            // Kannada
+            '\u0ce6', '\u0ce7', '\u0ce8', '\u0ce9', '\u0cea', '\u0ceb', '\u0cec', '\u0ced', '\u0cee'
+            , '\u0cef'
+    };
+
+    /**
+     * Filter for accepting only valid indices or prefixes of the string
+     * representation of valid indices.
+     */
+    class InputTextFilter extends NumberKeyListener {
+
+        // XXX This doesn't allow for range limits when controlled by a
+        // soft input method!
+        public int getInputType() {
+            return InputType.TYPE_CLASS_TEXT;
+        }
+
+        @Override
+        protected char[] getAcceptedChars() {
+            return DIGIT_CHARACTERS;
+        }
+
+        @Override
+        public CharSequence filter(
+                CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
+            // We don't know what the output will be, so always cancel any
+            // pending set selection command.
+            if (mSetSelectionCommand != null) {
+                mSetSelectionCommand.cancel();
+            }
+
+            if (mDisplayedValues == null) {
+                CharSequence filtered = super.filter(source, start, end, dest, dstart, dend);
+                if (filtered == null) {
+                    filtered = source.subSequence(start, end);
+                }
+
+                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+                        + dest.subSequence(dend, dest.length());
+
+                if ("".equals(result)) {
+                    return result;
+                }
+                int val = getSelectedPos(result);
+
+                /*
+                 * Ensure the user can't type in a value greater than the max
+                 * allowed. We have to allow less than min as the user might
+                 * want to delete some numbers and then type a new number.
+                 * And prevent multiple-"0" that exceeds the length of upper
+                 * bound number.
+                 */
+                if (val > mMaxValue || result.length() > String.valueOf(mMaxValue).length()) {
+                    return "";
+                } else {
+                    return filtered;
+                }
+            } else {
+                CharSequence filtered = String.valueOf(source.subSequence(start, end));
+                if (TextUtils.isEmpty(filtered)) {
+                    return "";
+                }
+                String result = String.valueOf(dest.subSequence(0, dstart)) + filtered
+                        + dest.subSequence(dend, dest.length());
+                String str = String.valueOf(result).toLowerCase();
+                for (String val : mDisplayedValues) {
+                    String valLowerCase = val.toLowerCase();
+                    if (valLowerCase.startsWith(str)) {
+                        postSetSelectionCommand(result.length(), val.length());
+                        return val.subSequence(dstart, val.length());
+                    }
+                }
+                return "";
+            }
+        }
+    }
+
+    /**
+     * Ensures that the scroll wheel is adjusted i.e. there is no offset and the
+     * middle element is in the middle of the widget.
+     *
+     * @return Whether an adjustment has been made.
+     */
+    private boolean ensureScrollWheelAdjusted() {
+        // adjust to the closest value
+        int deltaY = mInitialScrollOffset - mCurrentScrollOffset;
+        if (deltaY != 0) {
+            mPreviousScrollerY = 0;
+            if (Math.abs(deltaY) > mSelectorElementHeight / 2) {
+                deltaY += (deltaY > 0) ? -mSelectorElementHeight : mSelectorElementHeight;
+            }
+            mAdjustScroller.startScroll(0, 0, 0, deltaY, SELECTOR_ADJUSTMENT_DURATION_MILLIS);
+            invalidate();
+            return true;
+        }
+        return false;
+    }
+
+    class PressedStateHelper implements Runnable {
+        public static final int BUTTON_INCREMENT = 1;
+        public static final int BUTTON_DECREMENT = 2;
+
+        private final int MODE_PRESS = 1;
+        private final int MODE_TAPPED = 2;
+
+        private int mManagedButton;
+        private int mMode;
+
+        public void cancel() {
+            mMode = 0;
+            mManagedButton = 0;
+            NumberPicker.this.removeCallbacks(this);
+            if (mIncrementVirtualButtonPressed) {
+                mIncrementVirtualButtonPressed = false;
+                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+            }
+            mDecrementVirtualButtonPressed = false;
+            if (mDecrementVirtualButtonPressed) {
+                invalidate(0, 0, mRight, mTopSelectionDividerTop);
+            }
+        }
+
+        public void buttonPressDelayed(int button) {
+            cancel();
+            mMode = MODE_PRESS;
+            mManagedButton = button;
+            NumberPicker.this.postDelayed(this, ViewConfiguration.getTapTimeout());
+        }
+
+        public void buttonTapped(int button) {
+            cancel();
+            mMode = MODE_TAPPED;
+            mManagedButton = button;
+            NumberPicker.this.post(this);
+        }
+
+        @Override
+        public void run() {
+            switch (mMode) {
+                case MODE_PRESS: {
+                    switch (mManagedButton) {
+                        case BUTTON_INCREMENT: {
+                            mIncrementVirtualButtonPressed = true;
+                            invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+                        } break;
+                        case BUTTON_DECREMENT: {
+                            mDecrementVirtualButtonPressed = true;
+                            invalidate(0, 0, mRight, mTopSelectionDividerTop);
+                        }
+                    }
+                } break;
+                case MODE_TAPPED: {
+                    switch (mManagedButton) {
+                        case BUTTON_INCREMENT: {
+                            if (!mIncrementVirtualButtonPressed) {
+                                NumberPicker.this.postDelayed(this,
+                                        ViewConfiguration.getPressedStateDuration());
+                            }
+                            mIncrementVirtualButtonPressed ^= true;
+                            invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+                        } break;
+                        case BUTTON_DECREMENT: {
+                            if (!mDecrementVirtualButtonPressed) {
+                                NumberPicker.this.postDelayed(this,
+                                        ViewConfiguration.getPressedStateDuration());
+                            }
+                            mDecrementVirtualButtonPressed ^= true;
+                            invalidate(0, 0, mRight, mTopSelectionDividerTop);
+                        }
+                    }
+                } break;
+            }
+        }
+    }
+
+    /**
+     * Command for setting the input text selection.
+     */
+    private static class SetSelectionCommand implements Runnable {
+        private final EditText mInputText;
+
+        private int mSelectionStart;
+        private int mSelectionEnd;
+
+        /** Whether this runnable is currently posted. */
+        private boolean mPosted;
+
+        public SetSelectionCommand(EditText inputText) {
+            mInputText = inputText;
+        }
+
+        public void post(int selectionStart, int selectionEnd) {
+            mSelectionStart = selectionStart;
+            mSelectionEnd = selectionEnd;
+
+            if (!mPosted) {
+                mInputText.post(this);
+                mPosted = true;
+            }
+        }
+
+        public void cancel() {
+            if (mPosted) {
+                mInputText.removeCallbacks(this);
+                mPosted = false;
+            }
+        }
+
+        @Override
+        public void run() {
+            mPosted = false;
+            mInputText.setSelection(mSelectionStart, mSelectionEnd);
+        }
+    }
+
+    /**
+     * Command for changing the current value from a long press by one.
+     */
+    class ChangeCurrentByOneFromLongPressCommand implements Runnable {
+        private boolean mIncrement;
+
+        private void setStep(boolean increment) {
+            mIncrement = increment;
+        }
+
+        @Override
+        public void run() {
+            changeValueByOne(mIncrement);
+            postDelayed(this, mLongPressUpdateInterval);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public static class CustomEditText extends EditText {
+
+        public CustomEditText(Context context, AttributeSet attrs) {
+            super(context, attrs);
+        }
+
+        @Override
+        public void onEditorAction(int actionCode) {
+            super.onEditorAction(actionCode);
+            if (actionCode == EditorInfo.IME_ACTION_DONE) {
+                clearFocus();
+            }
+        }
+    }
+
+    /**
+     * Command for beginning soft input on long press.
+     */
+    class BeginSoftInputOnLongPressCommand implements Runnable {
+
+        @Override
+        public void run() {
+            performLongClick();
+        }
+    }
+
+    /**
+     * Class for managing virtual view tree rooted at this picker.
+     */
+    class AccessibilityNodeProviderImpl extends AccessibilityNodeProvider {
+        private static final int UNDEFINED = Integer.MIN_VALUE;
+
+        private static final int VIRTUAL_VIEW_ID_INCREMENT = 1;
+
+        private static final int VIRTUAL_VIEW_ID_INPUT = 2;
+
+        private static final int VIRTUAL_VIEW_ID_DECREMENT = 3;
+
+        private final Rect mTempRect = new Rect();
+
+        private final int[] mTempArray = new int[2];
+
+        private int mAccessibilityFocusedView = UNDEFINED;
+
+        @Override
+        public AccessibilityNodeInfo createAccessibilityNodeInfo(int virtualViewId) {
+            switch (virtualViewId) {
+                case View.NO_ID:
+                    return createAccessibilityNodeInfoForNumberPicker( mScrollX, mScrollY,
+                            mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
+                case VIRTUAL_VIEW_ID_DECREMENT:
+                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_DECREMENT,
+                            getVirtualDecrementButtonText(), mScrollX, mScrollY,
+                            mScrollX + (mRight - mLeft),
+                            mTopSelectionDividerTop + mSelectionDividerHeight);
+                case VIRTUAL_VIEW_ID_INPUT:
+                    return createAccessibiltyNodeInfoForInputText(mScrollX,
+                            mTopSelectionDividerTop + mSelectionDividerHeight,
+                            mScrollX + (mRight - mLeft),
+                            mBottomSelectionDividerBottom - mSelectionDividerHeight);
+                case VIRTUAL_VIEW_ID_INCREMENT:
+                    return createAccessibilityNodeInfoForVirtualButton(VIRTUAL_VIEW_ID_INCREMENT,
+                            getVirtualIncrementButtonText(), mScrollX,
+                            mBottomSelectionDividerBottom - mSelectionDividerHeight,
+                            mScrollX + (mRight - mLeft), mScrollY + (mBottom - mTop));
+            }
+            return super.createAccessibilityNodeInfo(virtualViewId);
+        }
+
+        @Override
+        public List<AccessibilityNodeInfo> findAccessibilityNodeInfosByText(String searched,
+                int virtualViewId) {
+            if (TextUtils.isEmpty(searched)) {
+                return Collections.emptyList();
+            }
+            String searchedLowerCase = searched.toLowerCase();
+            List<AccessibilityNodeInfo> result = new ArrayList<AccessibilityNodeInfo>();
+            switch (virtualViewId) {
+                case View.NO_ID: {
+                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+                            VIRTUAL_VIEW_ID_DECREMENT, result);
+                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+                            VIRTUAL_VIEW_ID_INPUT, result);
+                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase,
+                            VIRTUAL_VIEW_ID_INCREMENT, result);
+                    return result;
+                }
+                case VIRTUAL_VIEW_ID_DECREMENT:
+                case VIRTUAL_VIEW_ID_INCREMENT:
+                case VIRTUAL_VIEW_ID_INPUT: {
+                    findAccessibilityNodeInfosByTextInChild(searchedLowerCase, virtualViewId,
+                            result);
+                    return result;
+                }
+            }
+            return super.findAccessibilityNodeInfosByText(searched, virtualViewId);
+        }
+
+        @Override
+        public boolean performAction(int virtualViewId, int action, Bundle arguments) {
+            switch (virtualViewId) {
+                case View.NO_ID: {
+                    switch (action) {
+                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView != virtualViewId) {
+                                mAccessibilityFocusedView = virtualViewId;
+                                requestAccessibilityFocus();
+                                return true;
+                            }
+                        } return false;
+                        case AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView == virtualViewId) {
+                                mAccessibilityFocusedView = UNDEFINED;
+                                clearAccessibilityFocus();
+                                return true;
+                            }
+                            return false;
+                        }
+                        case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+                            if (NumberPicker.this.isEnabled()
+                                    && (getWrapSelectorWheel() || getValue() < getMaxValue())) {
+                                changeValueByOne(true);
+                                return true;
+                            }
+                        } return false;
+                        case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+                            if (NumberPicker.this.isEnabled()
+                                    && (getWrapSelectorWheel() || getValue() > getMinValue())) {
+                                changeValueByOne(false);
+                                return true;
+                            }
+                        } return false;
+                    }
+                } break;
+                case VIRTUAL_VIEW_ID_INPUT: {
+                    switch (action) {
+                        case AccessibilityNodeInfo.ACTION_FOCUS: {
+                            if (NumberPicker.this.isEnabled() && !mInputText.isFocused()) {
+                                return mInputText.requestFocus();
+                            }
+                        } break;
+                        case AccessibilityNodeInfo.ACTION_CLEAR_FOCUS: {
+                            if (NumberPicker.this.isEnabled() && mInputText.isFocused()) {
+                                mInputText.clearFocus();
+                                return true;
+                            }
+                            return false;
+                        }
+                        case AccessibilityNodeInfo.ACTION_CLICK: {
+                            if (NumberPicker.this.isEnabled()) {
+                                performClick();
+                                return true;
+                            }
+                            return false;
+                        }
+                        case AccessibilityNodeInfo.ACTION_LONG_CLICK: {
+                            if (NumberPicker.this.isEnabled()) {
+                                performLongClick();
+                                return true;
+                            }
+                            return false;
+                        }
+                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView != virtualViewId) {
+                                mAccessibilityFocusedView = virtualViewId;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+                                mInputText.invalidate();
+                                return true;
+                            }
+                        } return false;
+                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView == virtualViewId) {
+                                mAccessibilityFocusedView = UNDEFINED;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+                                mInputText.invalidate();
+                                return true;
+                            }
+                        } return false;
+                        default: {
+                            return mInputText.performAccessibilityAction(action, arguments);
+                        }
+                    }
+                } return false;
+                case VIRTUAL_VIEW_ID_INCREMENT: {
+                    switch (action) {
+                        case AccessibilityNodeInfo.ACTION_CLICK: {
+                            if (NumberPicker.this.isEnabled()) {
+                                NumberPicker.this.changeValueByOne(true);
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
+                                return true;
+                            }
+                        } return false;
+                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView != virtualViewId) {
+                                mAccessibilityFocusedView = virtualViewId;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+                                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+                                return true;
+                            }
+                        } return false;
+                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView == virtualViewId) {
+                                mAccessibilityFocusedView = UNDEFINED;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+                                invalidate(0, mBottomSelectionDividerBottom, mRight, mBottom);
+                                return true;
+                            }
+                        } return false;
+                    }
+                } return false;
+                case VIRTUAL_VIEW_ID_DECREMENT: {
+                    switch (action) {
+                        case AccessibilityNodeInfo.ACTION_CLICK: {
+                            if (NumberPicker.this.isEnabled()) {
+                                final boolean increment = (virtualViewId == VIRTUAL_VIEW_ID_INCREMENT);
+                                NumberPicker.this.changeValueByOne(increment);
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_CLICKED);
+                                return true;
+                            }
+                        } return false;
+                        case AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView != virtualViewId) {
+                                mAccessibilityFocusedView = virtualViewId;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED);
+                                invalidate(0, 0, mRight, mTopSelectionDividerTop);
+                                return true;
+                            }
+                        } return false;
+                        case  AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS: {
+                            if (mAccessibilityFocusedView == virtualViewId) {
+                                mAccessibilityFocusedView = UNDEFINED;
+                                sendAccessibilityEventForVirtualView(virtualViewId,
+                                        AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED);
+                                invalidate(0, 0, mRight, mTopSelectionDividerTop);
+                                return true;
+                            }
+                        } return false;
+                    }
+                } return false;
+            }
+            return super.performAction(virtualViewId, action, arguments);
+        }
+
+        public void sendAccessibilityEventForVirtualView(int virtualViewId, int eventType) {
+            switch (virtualViewId) {
+                case VIRTUAL_VIEW_ID_DECREMENT: {
+                    if (hasVirtualDecrementButton()) {
+                        sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
+                                getVirtualDecrementButtonText());
+                    }
+                } break;
+                case VIRTUAL_VIEW_ID_INPUT: {
+                    sendAccessibilityEventForVirtualText(eventType);
+                } break;
+                case VIRTUAL_VIEW_ID_INCREMENT: {
+                    if (hasVirtualIncrementButton()) {
+                        sendAccessibilityEventForVirtualButton(virtualViewId, eventType,
+                                getVirtualIncrementButtonText());
+                    }
+                } break;
+            }
+        }
+
+        private void sendAccessibilityEventForVirtualText(int eventType) {
+            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+                mInputText.onInitializeAccessibilityEvent(event);
+                mInputText.onPopulateAccessibilityEvent(event);
+                event.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+                requestSendAccessibilityEvent(NumberPicker.this, event);
+            }
+        }
+
+        private void sendAccessibilityEventForVirtualButton(int virtualViewId, int eventType,
+                String text) {
+            if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+                AccessibilityEvent event = AccessibilityEvent.obtain(eventType);
+                event.setClassName(Button.class.getName());
+                event.setPackageName(mContext.getPackageName());
+                event.getText().add(text);
+                event.setEnabled(NumberPicker.this.isEnabled());
+                event.setSource(NumberPicker.this, virtualViewId);
+                requestSendAccessibilityEvent(NumberPicker.this, event);
+            }
+        }
+
+        private void findAccessibilityNodeInfosByTextInChild(String searchedLowerCase,
+                int virtualViewId, List<AccessibilityNodeInfo> outResult) {
+            switch (virtualViewId) {
+                case VIRTUAL_VIEW_ID_DECREMENT: {
+                    String text = getVirtualDecrementButtonText();
+                    if (!TextUtils.isEmpty(text)
+                            && text.toString().toLowerCase().contains(searchedLowerCase)) {
+                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_DECREMENT));
+                    }
+                } return;
+                case VIRTUAL_VIEW_ID_INPUT: {
+                    CharSequence text = mInputText.getText();
+                    if (!TextUtils.isEmpty(text) &&
+                            text.toString().toLowerCase().contains(searchedLowerCase)) {
+                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
+                        return;
+                    }
+                    CharSequence contentDesc = mInputText.getText();
+                    if (!TextUtils.isEmpty(contentDesc) &&
+                            contentDesc.toString().toLowerCase().contains(searchedLowerCase)) {
+                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INPUT));
+                        return;
+                    }
+                } break;
+                case VIRTUAL_VIEW_ID_INCREMENT: {
+                    String text = getVirtualIncrementButtonText();
+                    if (!TextUtils.isEmpty(text)
+                            && text.toString().toLowerCase().contains(searchedLowerCase)) {
+                        outResult.add(createAccessibilityNodeInfo(VIRTUAL_VIEW_ID_INCREMENT));
+                    }
+                } return;
+            }
+        }
+
+        private AccessibilityNodeInfo createAccessibiltyNodeInfoForInputText(
+                int left, int top, int right, int bottom) {
+            AccessibilityNodeInfo info = mInputText.createAccessibilityNodeInfo();
+            info.setSource(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+            if (mAccessibilityFocusedView != VIRTUAL_VIEW_ID_INPUT) {
+                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+            }
+            if (mAccessibilityFocusedView == VIRTUAL_VIEW_ID_INPUT) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            }
+            Rect boundsInParent = mTempRect;
+            boundsInParent.set(left, top, right, bottom);
+            info.setVisibleToUser(isVisibleToUser(boundsInParent));
+            info.setBoundsInParent(boundsInParent);
+            Rect boundsInScreen = boundsInParent;
+            int[] locationOnScreen = mTempArray;
+            getLocationOnScreen(locationOnScreen);
+            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+            info.setBoundsInScreen(boundsInScreen);
+            return info;
+        }
+
+        private AccessibilityNodeInfo createAccessibilityNodeInfoForVirtualButton(int virtualViewId,
+                String text, int left, int top, int right, int bottom) {
+            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+            info.setClassName(Button.class.getName());
+            info.setPackageName(mContext.getPackageName());
+            info.setSource(NumberPicker.this, virtualViewId);
+            info.setParent(NumberPicker.this);
+            info.setText(text);
+            info.setClickable(true);
+            info.setLongClickable(true);
+            info.setEnabled(NumberPicker.this.isEnabled());
+            Rect boundsInParent = mTempRect;
+            boundsInParent.set(left, top, right, bottom);
+            info.setVisibleToUser(isVisibleToUser(boundsInParent));
+            info.setBoundsInParent(boundsInParent);
+            Rect boundsInScreen = boundsInParent;
+            int[] locationOnScreen = mTempArray;
+            getLocationOnScreen(locationOnScreen);
+            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+            info.setBoundsInScreen(boundsInScreen);
+
+            if (mAccessibilityFocusedView != virtualViewId) {
+                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+            }
+            if (mAccessibilityFocusedView == virtualViewId) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            }
+            if (NumberPicker.this.isEnabled()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CLICK);
+            }
+
+            return info;
+        }
+
+        private AccessibilityNodeInfo createAccessibilityNodeInfoForNumberPicker(int left, int top,
+                int right, int bottom) {
+            AccessibilityNodeInfo info = AccessibilityNodeInfo.obtain();
+            info.setClassName(NumberPicker.class.getName());
+            info.setPackageName(mContext.getPackageName());
+            info.setSource(NumberPicker.this);
+
+            if (hasVirtualDecrementButton()) {
+                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_DECREMENT);
+            }
+            info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INPUT);
+            if (hasVirtualIncrementButton()) {
+                info.addChild(NumberPicker.this, VIRTUAL_VIEW_ID_INCREMENT);
+            }
+
+            info.setParent((View) getParentForAccessibility());
+            info.setEnabled(NumberPicker.this.isEnabled());
+            info.setScrollable(true);
+
+            final float applicationScale =
+                getContext().getResources().getCompatibilityInfo().applicationScale;
+
+            Rect boundsInParent = mTempRect;
+            boundsInParent.set(left, top, right, bottom);
+            boundsInParent.scale(applicationScale);
+            info.setBoundsInParent(boundsInParent);
+
+            info.setVisibleToUser(isVisibleToUser());
+
+            Rect boundsInScreen = boundsInParent;
+            int[] locationOnScreen = mTempArray;
+            getLocationOnScreen(locationOnScreen);
+            boundsInScreen.offset(locationOnScreen[0], locationOnScreen[1]);
+            boundsInScreen.scale(applicationScale);
+            info.setBoundsInScreen(boundsInScreen);
+
+            if (mAccessibilityFocusedView != View.NO_ID) {
+                info.addAction(AccessibilityNodeInfo.ACTION_ACCESSIBILITY_FOCUS);
+            }
+            if (mAccessibilityFocusedView == View.NO_ID) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CLEAR_ACCESSIBILITY_FOCUS);
+            }
+            if (NumberPicker.this.isEnabled()) {
+                if (getWrapSelectorWheel() || getValue() < getMaxValue()) {
+                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+                }
+                if (getWrapSelectorWheel() || getValue() > getMinValue()) {
+                    info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+                }
+            }
+
+            return info;
+        }
+
+        private boolean hasVirtualDecrementButton() {
+            return getWrapSelectorWheel() || getValue() > getMinValue();
+        }
+
+        private boolean hasVirtualIncrementButton() {
+            return getWrapSelectorWheel() || getValue() < getMaxValue();
+        }
+
+        private String getVirtualDecrementButtonText() {
+            int value = mValue - 1;
+            if (mWrapSelectorWheel) {
+                value = getWrappedSelectorIndex(value);
+            }
+            if (value >= mMinValue) {
+                return (mDisplayedValues == null) ? formatNumber(value)
+                        : mDisplayedValues[value - mMinValue];
+            }
+            return null;
+        }
+
+        private String getVirtualIncrementButtonText() {
+            int value = mValue + 1;
+            if (mWrapSelectorWheel) {
+                value = getWrappedSelectorIndex(value);
+            }
+            if (value <= mMaxValue) {
+                return (mDisplayedValues == null) ? formatNumber(value)
+                        : mDisplayedValues[value - mMinValue];
+            }
+            return null;
+        }
+    }
+
+    static private String formatNumberWithLocale(int value) {
+        return String.format(Locale.getDefault(), "%d", value);
+    }
+}
diff --git a/android/widget/OnDateChangedListener.java b/android/widget/OnDateChangedListener.java
new file mode 100644
index 0000000..29be888
--- /dev/null
+++ b/android/widget/OnDateChangedListener.java
@@ -0,0 +1,27 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+/**
+ * The callback used to notify other date picker components of a change in the selected date.
+ *
+ */
+interface OnDateChangedListener {
+
+    public void onDateChanged();
+}
+
diff --git a/android/widget/OverScroller.java b/android/widget/OverScroller.java
new file mode 100644
index 0000000..9938789
--- /dev/null
+++ b/android/widget/OverScroller.java
@@ -0,0 +1,961 @@
+/*
+ * 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 android.widget;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.util.Log;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+/**
+ * This class encapsulates scrolling with the ability to overshoot the bounds
+ * of a scrolling operation. This class is a drop-in replacement for
+ * {@link android.widget.Scroller} in most cases.
+ */
+public class OverScroller {
+    private int mMode;
+
+    private final SplineOverScroller mScrollerX;
+    private final SplineOverScroller mScrollerY;
+
+    private Interpolator mInterpolator;
+
+    private final boolean mFlywheel;
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    /**
+     * Creates an OverScroller with a viscous fluid scroll interpolator and flywheel.
+     * @param context
+     */
+    public OverScroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     */
+    public OverScroller(Context context, Interpolator interpolator) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * @hide
+     */
+    public OverScroller(Context context, Interpolator interpolator, boolean flywheel) {
+        if (interpolator == null) {
+            mInterpolator = new Scroller.ViscousFluidInterpolator();
+        } else {
+            mInterpolator = interpolator;
+        }
+        mFlywheel = flywheel;
+        mScrollerX = new SplineOverScroller(context);
+        mScrollerY = new SplineOverScroller(context);
+    }
+
+    /**
+     * Creates an OverScroller with flywheel enabled.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
+     */
+    @Deprecated
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY) {
+        this(context, interpolator, true);
+    }
+
+    /**
+     * Creates an OverScroller.
+     * @param context The context of this application.
+     * @param interpolator The scroll interpolator. If null, a default (viscous) interpolator will
+     * be used.
+     * @param bounceCoefficientX A value between 0 and 1 that will determine the proportion of the
+     * velocity which is preserved in the bounce when the horizontal edge is reached. A null value
+     * means no bounce. This behavior is no longer supported and this coefficient has no effect.
+     * @param bounceCoefficientY Same as bounceCoefficientX but for the vertical direction. This
+     * behavior is no longer supported and this coefficient has no effect.
+     * @param flywheel If true, successive fling motions will keep on increasing scroll speed.
+     * @deprecated Use {@link #OverScroller(Context, Interpolator)} instead.
+     */
+    @Deprecated
+    public OverScroller(Context context, Interpolator interpolator,
+            float bounceCoefficientX, float bounceCoefficientY, boolean flywheel) {
+        this(context, interpolator, flywheel);
+    }
+
+    void setInterpolator(Interpolator interpolator) {
+        if (interpolator == null) {
+            mInterpolator = new Scroller.ViscousFluidInterpolator();
+        } else {
+            mInterpolator = interpolator;
+        }
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     *
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mScrollerX.setFriction(friction);
+        mScrollerY.setFriction(friction);
+    }
+
+    /**
+     *
+     * Returns whether the scroller has finished scrolling.
+     *
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mScrollerX.mFinished && mScrollerY.mFinished;
+    }
+
+    /**
+     * Force the finished field to a particular value. Contrary to
+     * {@link #abortAnimation()}, forcing the animation to finished
+     * does NOT cause the scroller to move to the final x and y
+     * position.
+     *
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mScrollerX.mFinished = mScrollerY.mFinished = finished;
+    }
+
+    /**
+     * Returns the current X offset in the scroll.
+     *
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mScrollerX.mCurrentPosition;
+    }
+
+    /**
+     * Returns the current Y offset in the scroll.
+     *
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mScrollerY.mCurrentPosition;
+    }
+
+    /**
+     * Returns the absolute value of the current velocity.
+     *
+     * @return The original velocity less the deceleration, norm of the X and Y velocity vector.
+     */
+    public float getCurrVelocity() {
+        return (float) Math.hypot(mScrollerX.mCurrVelocity, mScrollerY.mCurrVelocity);
+    }
+
+    /**
+     * Returns the start X offset in the scroll.
+     *
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mScrollerX.mStart;
+    }
+
+    /**
+     * Returns the start Y offset in the scroll.
+     *
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mScrollerY.mStart;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mScrollerX.mFinal;
+    }
+
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     *
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mScrollerY.mFinal;
+    }
+
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     *
+     * @return The duration of the scroll in milliseconds.
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             This function will lie to the best of its ability.
+     */
+    @Deprecated
+    public final int getDuration() {
+        return Math.max(mScrollerX.mDuration, mScrollerY.mDuration);
+    }
+
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScrollers don't necessarily have a fixed duration.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void extendDuration(int extend) {
+        mScrollerX.extendDuration(extend);
+        mScrollerY.extendDuration(extend);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalX(int newX) {
+        mScrollerX.setFinalPosition(newX);
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     *
+     * @hide Pending removal once nothing depends on it
+     * @deprecated OverScroller's final position may change during an animation.
+     *             Instead of setting a new final position and extending
+     *             the duration of an existing scroll, use startScroll
+     *             to begin a new animation.
+     */
+    @Deprecated
+    public void setFinalY(int newY) {
+        mScrollerY.setFinalPosition(newY);
+    }
+
+    /**
+     * Call this when you want to know the new location. If it returns true, the
+     * animation is not yet finished.
+     */
+    public boolean computeScrollOffset() {
+        if (isFinished()) {
+            return false;
+        }
+
+        switch (mMode) {
+            case SCROLL_MODE:
+                long time = AnimationUtils.currentAnimationTimeMillis();
+                // Any scroller can be used for time, since they were started
+                // together in scroll mode. We use X here.
+                final long elapsedTime = time - mScrollerX.mStartTime;
+
+                final int duration = mScrollerX.mDuration;
+                if (elapsedTime < duration) {
+                    final float q = mInterpolator.getInterpolation(elapsedTime / (float) duration);
+                    mScrollerX.updateScroll(q);
+                    mScrollerY.updateScroll(q);
+                } else {
+                    abortAnimation();
+                }
+                break;
+
+            case FLING_MODE:
+                if (!mScrollerX.mFinished) {
+                    if (!mScrollerX.update()) {
+                        if (!mScrollerX.continueWhenFinished()) {
+                            mScrollerX.finish();
+                        }
+                    }
+                }
+
+                if (!mScrollerY.mFinished) {
+                    if (!mScrollerY.update()) {
+                        if (!mScrollerY.continueWhenFinished()) {
+                            mScrollerY.finish();
+                        }
+                    }
+                }
+
+                break;
+        }
+
+        return true;
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     *
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mScrollerX.startScroll(startX, dx, duration);
+        mScrollerY.startScroll(startY, dy, duration);
+    }
+
+    /**
+     * Call this when you want to 'spring back' into a valid coordinate range.
+     *
+     * @param startX Starting X coordinate
+     * @param startY Starting Y coordinate
+     * @param minX Minimum valid X value
+     * @param maxX Maximum valid X value
+     * @param minY Minimum valid Y value
+     * @param maxY Minimum valid Y value
+     * @return true if a springback was initiated, false if startX and startY were
+     *          already within the valid range.
+     */
+    public boolean springBack(int startX, int startY, int minX, int maxX, int minY, int maxY) {
+        mMode = FLING_MODE;
+
+        // Make sure both methods are called.
+        final boolean spingbackX = mScrollerX.springback(startX, minX, maxX);
+        final boolean spingbackY = mScrollerY.springback(startY, minY, maxY);
+        return spingbackX || spingbackY;
+    }
+
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY, 0, 0);
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance traveled will
+     * depend on the initial velocity of the fling.
+     *
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *            second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *            second
+     * @param minX Minimum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use minX as
+     *            a springback boundary.
+     * @param maxX Maximum X value. The scroller will not scroll past this point
+     *            unless overX > 0. If overfling is allowed, it will use maxX as
+     *            a springback boundary.
+     * @param minY Minimum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use minY as
+     *            a springback boundary.
+     * @param maxY Maximum Y value. The scroller will not scroll past this point
+     *            unless overY > 0. If overfling is allowed, it will use maxY as
+     *            a springback boundary.
+     * @param overX Overfling range. If > 0, horizontal overfling in either
+     *            direction will be possible.
+     * @param overY Overfling range. If > 0, vertical overfling in either
+     *            direction will be possible.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY, int overX, int overY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !isFinished()) {
+            float oldVelocityX = mScrollerX.mCurrVelocity;
+            float oldVelocityY = mScrollerY.mCurrVelocity;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mScrollerX.fling(startX, velocityX, minX, maxX, overX);
+        mScrollerY.fling(startY, velocityY, minY, maxY, overY);
+    }
+
+    /**
+     * Notify the scroller that we've reached a horizontal boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will transition the current motion and
+     * animate from startX to finalX as appropriate.
+     *
+     * @param startX Starting/current X position
+     * @param finalX Desired final X position
+     * @param overX Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalX. Absolute value - must be positive.
+     */
+    public void notifyHorizontalEdgeReached(int startX, int finalX, int overX) {
+        mScrollerX.notifyEdgeReached(startX, finalX, overX);
+    }
+
+    /**
+     * Notify the scroller that we've reached a vertical boundary.
+     * Normally the information to handle this will already be known
+     * when the animation is started, such as in a call to one of the
+     * fling functions. However there are cases where this cannot be known
+     * in advance. This function will animate a parabolic motion from
+     * startY to finalY.
+     *
+     * @param startY Starting/current Y position
+     * @param finalY Desired final Y position
+     * @param overY Magnitude of overscroll allowed. This should be the maximum
+     *              desired distance from finalY. Absolute value - must be positive.
+     */
+    public void notifyVerticalEdgeReached(int startY, int finalY, int overY) {
+        mScrollerY.notifyEdgeReached(startY, finalY, overY);
+    }
+
+    /**
+     * Returns whether the current Scroller is currently returning to a valid position.
+     * Valid bounds were provided by the
+     * {@link #fling(int, int, int, int, int, int, int, int, int, int)} method.
+     *
+     * One should check this value before calling
+     * {@link #startScroll(int, int, int, int)} as the interpolation currently in progress
+     * to restore a valid position will then be stopped. The caller has to take into account
+     * the fact that the started scroll will start from an overscrolled position.
+     *
+     * @return true when the current position is overscrolled and in the process of
+     *         interpolating back to a valid value.
+     */
+    public boolean isOverScrolled() {
+        return ((!mScrollerX.mFinished &&
+                mScrollerX.mState != SplineOverScroller.SPLINE) ||
+                (!mScrollerY.mFinished &&
+                        mScrollerY.mState != SplineOverScroller.SPLINE));
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating causes the scroller to move to the final x and y
+     * positions.
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mScrollerX.finish();
+        mScrollerY.finish();
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     *
+     * @hide
+     */
+    public int timePassed() {
+        final long time = AnimationUtils.currentAnimationTimeMillis();
+        final long startTime = Math.min(mScrollerX.mStartTime, mScrollerY.mStartTime);
+        return (int) (time - startTime);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        final int dx = mScrollerX.mFinal - mScrollerX.mStart;
+        final int dy = mScrollerY.mFinal - mScrollerY.mStart;
+        return !isFinished() && Math.signum(xvel) == Math.signum(dx) &&
+                Math.signum(yvel) == Math.signum(dy);
+    }
+
+    static class SplineOverScroller {
+        // Initial position
+        private int mStart;
+
+        // Current position
+        private int mCurrentPosition;
+
+        // Final position
+        private int mFinal;
+
+        // Initial velocity
+        private int mVelocity;
+
+        // Current velocity
+        private float mCurrVelocity;
+
+        // Constant current deceleration
+        private float mDeceleration;
+
+        // Animation starting time, in system milliseconds
+        private long mStartTime;
+
+        // Animation duration, in milliseconds
+        private int mDuration;
+
+        // Duration to complete spline component of animation
+        private int mSplineDuration;
+
+        // Distance to travel along spline animation
+        private int mSplineDistance;
+
+        // Whether the animation is currently in progress
+        private boolean mFinished;
+
+        // The allowed overshot distance before boundary is reached.
+        private int mOver;
+
+        // Fling friction
+        private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+        // Current state of the animation.
+        private int mState = SPLINE;
+
+        // Constant gravity value, used in the deceleration phase.
+        private static final float GRAVITY = 2000.0f;
+
+        // A context-specific coefficient adjusted to physical values.
+        private float mPhysicalCoeff;
+
+        private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+        private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+        private static final float START_TENSION = 0.5f;
+        private static final float END_TENSION = 1.0f;
+        private static final float P1 = START_TENSION * INFLEXION;
+        private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+        private static final int NB_SAMPLES = 100;
+        private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+        private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+        private static final int SPLINE = 0;
+        private static final int CUBIC = 1;
+        private static final int BALLISTIC = 2;
+
+        static {
+            float x_min = 0.0f;
+            float y_min = 0.0f;
+            for (int i = 0; i < NB_SAMPLES; i++) {
+                final float alpha = (float) i / NB_SAMPLES;
+
+                float x_max = 1.0f;
+                float x, tx, coef;
+                while (true) {
+                    x = x_min + (x_max - x_min) / 2.0f;
+                    coef = 3.0f * x * (1.0f - x);
+                    tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+                    if (Math.abs(tx - alpha) < 1E-5) break;
+                    if (tx > alpha) x_max = x;
+                    else x_min = x;
+                }
+                SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+                float y_max = 1.0f;
+                float y, dy;
+                while (true) {
+                    y = y_min + (y_max - y_min) / 2.0f;
+                    coef = 3.0f * y * (1.0f - y);
+                    dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+                    if (Math.abs(dy - alpha) < 1E-5) break;
+                    if (dy > alpha) y_max = y;
+                    else y_min = y;
+                }
+                SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+            }
+            SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+        }
+
+        void setFriction(float friction) {
+            mFlingFriction = friction;
+        }
+
+        SplineOverScroller(Context context) {
+            mFinished = true;
+            final float ppi = context.getResources().getDisplayMetrics().density * 160.0f;
+            mPhysicalCoeff = SensorManager.GRAVITY_EARTH // g (m/s^2)
+                    * 39.37f // inch/meter
+                    * ppi
+                    * 0.84f; // look and feel tuning
+        }
+
+        void updateScroll(float q) {
+            mCurrentPosition = mStart + Math.round(q * (mFinal - mStart));
+        }
+
+        /*
+         * Get a signed deceleration that will reduce the velocity.
+         */
+        static private float getDeceleration(int velocity) {
+            return velocity > 0 ? -GRAVITY : GRAVITY;
+        }
+
+        /*
+         * Modifies mDuration to the duration it takes to get from start to newFinal using the
+         * spline interpolation. The previous duration was needed to get to oldFinal.
+         */
+        private void adjustDuration(int start, int oldFinal, int newFinal) {
+            final int oldDistance = oldFinal - start;
+            final int newDistance = newFinal - start;
+            final float x = Math.abs((float) newDistance / oldDistance);
+            final int index = (int) (NB_SAMPLES * x);
+            if (index < NB_SAMPLES) {
+                final float x_inf = (float) index / NB_SAMPLES;
+                final float x_sup = (float) (index + 1) / NB_SAMPLES;
+                final float t_inf = SPLINE_TIME[index];
+                final float t_sup = SPLINE_TIME[index + 1];
+                final float timeCoef = t_inf + (x - x_inf) / (x_sup - x_inf) * (t_sup - t_inf);
+                mDuration *= timeCoef;
+            }
+        }
+
+        void startScroll(int start, int distance, int duration) {
+            mFinished = false;
+
+            mCurrentPosition = mStart = start;
+            mFinal = start + distance;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = duration;
+
+            // Unused
+            mDeceleration = 0.0f;
+            mVelocity = 0;
+        }
+
+        void finish() {
+            mCurrentPosition = mFinal;
+            // Not reset since WebView relies on this value for fast fling.
+            // TODO: restore when WebView uses the fast fling implemented in this class.
+            // mCurrVelocity = 0.0f;
+            mFinished = true;
+        }
+
+        void setFinalPosition(int position) {
+            mFinal = position;
+            mFinished = false;
+        }
+
+        void extendDuration(int extend) {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final int elapsedTime = (int) (time - mStartTime);
+            mDuration = elapsedTime + extend;
+            mFinished = false;
+        }
+
+        boolean springback(int start, int min, int max) {
+            mFinished = true;
+
+            mCurrentPosition = mStart = mFinal = start;
+            mVelocity = 0;
+
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mDuration = 0;
+
+            if (start < min) {
+                startSpringback(start, min, 0);
+            } else if (start > max) {
+                startSpringback(start, max, 0);
+            }
+
+            return !mFinished;
+        }
+
+        private void startSpringback(int start, int end, int velocity) {
+            // mStartTime has been set
+            mFinished = false;
+            mState = CUBIC;
+            mCurrentPosition = mStart = start;
+            mFinal = end;
+            final int delta = start - end;
+            mDeceleration = getDeceleration(delta);
+            // TODO take velocity into account
+            mVelocity = -delta; // only sign is used
+            mOver = Math.abs(delta);
+            mDuration = (int) (1000.0 * Math.sqrt(-2.0 * delta / mDeceleration));
+        }
+
+        void fling(int start, int velocity, int min, int max, int over) {
+            mOver = over;
+            mFinished = false;
+            mCurrVelocity = mVelocity = velocity;
+            mDuration = mSplineDuration = 0;
+            mStartTime = AnimationUtils.currentAnimationTimeMillis();
+            mCurrentPosition = mStart = start;
+
+            if (start > max || start < min) {
+                startAfterEdge(start, min, max, velocity);
+                return;
+            }
+
+            mState = SPLINE;
+            double totalDistance = 0.0;
+
+            if (velocity != 0) {
+                mDuration = mSplineDuration = getSplineFlingDuration(velocity);
+                totalDistance = getSplineFlingDistance(velocity);
+            }
+
+            mSplineDistance = (int) (totalDistance * Math.signum(velocity));
+            mFinal = start + mSplineDistance;
+
+            // Clamp to a valid final position
+            if (mFinal < min) {
+                adjustDuration(mStart, mFinal, min);
+                mFinal = min;
+            }
+
+            if (mFinal > max) {
+                adjustDuration(mStart, mFinal, max);
+                mFinal = max;
+            }
+        }
+
+        private double getSplineDeceleration(int velocity) {
+            return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+        }
+
+        private double getSplineFlingDistance(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+        }
+
+        /* Returns the duration, expressed in milliseconds */
+        private int getSplineFlingDuration(int velocity) {
+            final double l = getSplineDeceleration(velocity);
+            final double decelMinusOne = DECELERATION_RATE - 1.0;
+            return (int) (1000.0 * Math.exp(l / decelMinusOne));
+        }
+
+        private void fitOnBounceCurve(int start, int end, int velocity) {
+            // Simulate a bounce that started from edge
+            final float durationToApex = - velocity / mDeceleration;
+            // The float cast below is necessary to avoid integer overflow.
+            final float velocitySquared = (float) velocity * velocity;
+            final float distanceToApex = velocitySquared / 2.0f / Math.abs(mDeceleration);
+            final float distanceToEdge = Math.abs(end - start);
+            final float totalDuration = (float) Math.sqrt(
+                    2.0 * (distanceToApex + distanceToEdge) / Math.abs(mDeceleration));
+            mStartTime -= (int) (1000.0f * (totalDuration - durationToApex));
+            mCurrentPosition = mStart = end;
+            mVelocity = (int) (- mDeceleration * totalDuration);
+        }
+
+        private void startBounceAfterEdge(int start, int end, int velocity) {
+            mDeceleration = getDeceleration(velocity == 0 ? start - end : velocity);
+            fitOnBounceCurve(start, end, velocity);
+            onEdgeReached();
+        }
+
+        private void startAfterEdge(int start, int min, int max, int velocity) {
+            if (start > min && start < max) {
+                Log.e("OverScroller", "startAfterEdge called from a valid position");
+                mFinished = true;
+                return;
+            }
+            final boolean positive = start > max;
+            final int edge = positive ? max : min;
+            final int overDistance = start - edge;
+            boolean keepIncreasing = overDistance * velocity >= 0;
+            if (keepIncreasing) {
+                // Will result in a bounce or a to_boundary depending on velocity.
+                startBounceAfterEdge(start, edge, velocity);
+            } else {
+                final double totalDistance = getSplineFlingDistance(velocity);
+                if (totalDistance > Math.abs(overDistance)) {
+                    fling(start, velocity, positive ? min : start, positive ? start : max, mOver);
+                } else {
+                    startSpringback(start, edge, velocity);
+                }
+            }
+        }
+
+        void notifyEdgeReached(int start, int end, int over) {
+            // mState is used to detect successive notifications 
+            if (mState == SPLINE) {
+                mOver = over;
+                mStartTime = AnimationUtils.currentAnimationTimeMillis();
+                // We were in fling/scroll mode before: current velocity is such that distance to
+                // edge is increasing. This ensures that startAfterEdge will not start a new fling.
+                startAfterEdge(start, end, end, (int) mCurrVelocity);
+            }
+        }
+
+        private void onEdgeReached() {
+            // mStart, mVelocity and mStartTime were adjusted to their values when edge was reached.
+            // The float cast below is necessary to avoid integer overflow.
+            final float velocitySquared = (float) mVelocity * mVelocity;
+            float distance = velocitySquared / (2.0f * Math.abs(mDeceleration));
+            final float sign = Math.signum(mVelocity);
+
+            if (distance > mOver) {
+                // Default deceleration is not sufficient to slow us down before boundary
+                 mDeceleration = - sign * velocitySquared / (2.0f * mOver);
+                 distance = mOver;
+            }
+
+            mOver = (int) distance;
+            mState = BALLISTIC;
+            mFinal = mStart + (int) (mVelocity > 0 ? distance : -distance);
+            mDuration = - (int) (1000.0f * mVelocity / mDeceleration);
+        }
+
+        boolean continueWhenFinished() {
+            switch (mState) {
+                case SPLINE:
+                    // Duration from start to null velocity
+                    if (mDuration < mSplineDuration) {
+                        // If the animation was clamped, we reached the edge
+                        mCurrentPosition = mStart = mFinal;
+                        // TODO Better compute speed when edge was reached
+                        mVelocity = (int) mCurrVelocity;
+                        mDeceleration = getDeceleration(mVelocity);
+                        mStartTime += mDuration;
+                        onEdgeReached();
+                    } else {
+                        // Normal stop, no need to continue
+                        return false;
+                    }
+                    break;
+                case BALLISTIC:
+                    mStartTime += mDuration;
+                    startSpringback(mFinal, mStart, 0);
+                    break;
+                case CUBIC:
+                    return false;
+            }
+
+            update();
+            return true;
+        }
+
+        /*
+         * Update the current position and velocity for current time. Returns
+         * true if update has been done and false if animation duration has been
+         * reached.
+         */
+        boolean update() {
+            final long time = AnimationUtils.currentAnimationTimeMillis();
+            final long currentTime = time - mStartTime;
+
+            if (currentTime == 0) {
+                // Skip work but report that we're still going if we have a nonzero duration.
+                return mDuration > 0;
+            }
+            if (currentTime > mDuration) {
+                return false;
+            }
+
+            double distance = 0.0;
+            switch (mState) {
+                case SPLINE: {
+                    final float t = (float) currentTime / mSplineDuration;
+                    final int index = (int) (NB_SAMPLES * t);
+                    float distanceCoef = 1.f;
+                    float velocityCoef = 0.f;
+                    if (index < NB_SAMPLES) {
+                        final float t_inf = (float) index / NB_SAMPLES;
+                        final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                        final float d_inf = SPLINE_POSITION[index];
+                        final float d_sup = SPLINE_POSITION[index + 1];
+                        velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+                        distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+                    }
+
+                    distance = distanceCoef * mSplineDistance;
+                    mCurrVelocity = velocityCoef * mSplineDistance / mSplineDuration * 1000.0f;
+                    break;
+                }
+
+                case BALLISTIC: {
+                    final float t = currentTime / 1000.0f;
+                    mCurrVelocity = mVelocity + mDeceleration * t;
+                    distance = mVelocity * t + mDeceleration * t * t / 2.0f;
+                    break;
+                }
+
+                case CUBIC: {
+                    final float t = (float) (currentTime) / mDuration;
+                    final float t2 = t * t;
+                    final float sign = Math.signum(mVelocity);
+                    distance = sign * mOver * (3.0f * t2 - 2.0f * t * t2); 
+                    mCurrVelocity = sign * mOver * 6.0f * (- t + t2); 
+                    break;
+                }
+            }
+
+            mCurrentPosition = mStart + (int) Math.round(distance);
+
+            return true;
+        }
+    }
+}
diff --git a/android/widget/PopupMenu.java b/android/widget/PopupMenu.java
new file mode 100644
index 0000000..59bbc3b
--- /dev/null
+++ b/android/widget/PopupMenu.java
@@ -0,0 +1,300 @@
+/*
+ * 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 android.widget;
+
+import android.annotation.MenuRes;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.View;
+import android.view.View.OnTouchListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuPopupHelper;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * A PopupMenu displays a {@link Menu} in a modal popup window anchored to a
+ * {@link View}. The popup will appear below the anchor view if there is room,
+ * or above it if there is not. If the IME is visible the popup will not
+ * overlap it until it is touched. Touching outside of the popup will dismiss
+ * it.
+ */
+public class PopupMenu {
+    private final Context mContext;
+    private final MenuBuilder mMenu;
+    private final View mAnchor;
+    private final MenuPopupHelper mPopup;
+
+    private OnMenuItemClickListener mMenuItemClickListener;
+    private OnDismissListener mOnDismissListener;
+    private OnTouchListener mDragListener;
+
+    /**
+     * Constructor to create a new popup menu with an anchor view.
+     *
+     * @param context Context the popup menu is running in, through which it
+     *        can access the current theme, resources, etc.
+     * @param anchor Anchor view for this popup. The popup will appear below
+     *        the anchor if there is room, or above it if there is not.
+     */
+    public PopupMenu(Context context, View anchor) {
+        this(context, anchor, Gravity.NO_GRAVITY);
+    }
+
+    /**
+     * Constructor to create a new popup menu with an anchor view and alignment
+     * gravity.
+     *
+     * @param context Context the popup menu is running in, through which it
+     *        can access the current theme, resources, etc.
+     * @param anchor Anchor view for this popup. The popup will appear below
+     *        the anchor if there is room, or above it if there is not.
+     * @param gravity The {@link Gravity} value for aligning the popup with its
+     *        anchor.
+     */
+    public PopupMenu(Context context, View anchor, int gravity) {
+        this(context, anchor, gravity, R.attr.popupMenuStyle, 0);
+    }
+
+    /**
+     * Constructor a create a new popup menu with a specific style.
+     *
+     * @param context Context the popup menu is running in, through which it
+     *        can access the current theme, resources, etc.
+     * @param anchor Anchor view for this popup. The popup will appear below
+     *        the anchor if there is room, or above it if there is not.
+     * @param gravity The {@link Gravity} value for aligning the popup with its
+     *        anchor.
+     * @param popupStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the popup window. Can be 0 to not look for defaults.
+     * @param popupStyleRes A resource identifier of a style resource that
+     *        supplies default values for the popup window, used only if
+     *        popupStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public PopupMenu(Context context, View anchor, int gravity, int popupStyleAttr,
+            int popupStyleRes) {
+        mContext = context;
+        mAnchor = anchor;
+
+        mMenu = new MenuBuilder(context);
+        mMenu.setCallback(new MenuBuilder.Callback() {
+            @Override
+            public boolean onMenuItemSelected(MenuBuilder menu, MenuItem item) {
+                if (mMenuItemClickListener != null) {
+                    return mMenuItemClickListener.onMenuItemClick(item);
+                }
+                return false;
+            }
+
+            @Override
+            public void onMenuModeChange(MenuBuilder menu) {
+            }
+        });
+
+        mPopup = new MenuPopupHelper(context, mMenu, anchor, false, popupStyleAttr, popupStyleRes);
+        mPopup.setGravity(gravity);
+        mPopup.setOnDismissListener(new PopupWindow.OnDismissListener() {
+            @Override
+            public void onDismiss() {
+                if (mOnDismissListener != null) {
+                    mOnDismissListener.onDismiss(PopupMenu.this);
+                }
+            }
+        });
+    }
+
+    /**
+     * Sets the gravity used to align the popup window to its anchor view.
+     * <p>
+     * If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown.
+     *
+     * @param gravity the gravity used to align the popup window
+     * @see #getGravity()
+     */
+    public void setGravity(int gravity) {
+        mPopup.setGravity(gravity);
+    }
+
+    /**
+     * @return the gravity used to align the popup window to its anchor view
+     * @see #setGravity(int)
+     */
+    public int getGravity() {
+        return mPopup.getGravity();
+    }
+
+    /**
+     * Returns an {@link OnTouchListener} that can be added to the anchor view
+     * to implement drag-to-open behavior.
+     * <p>
+     * When the listener is set on a view, touching that view and dragging
+     * outside of its bounds will open the popup window. Lifting will select
+     * the currently touched list item.
+     * <p>
+     * Example usage:
+     * <pre>
+     * PopupMenu myPopup = new PopupMenu(context, myAnchor);
+     * myAnchor.setOnTouchListener(myPopup.getDragToOpenListener());
+     * </pre>
+     *
+     * @return a touch listener that controls drag-to-open behavior
+     */
+    public OnTouchListener getDragToOpenListener() {
+        if (mDragListener == null) {
+            mDragListener = new ForwardingListener(mAnchor) {
+                @Override
+                protected boolean onForwardingStarted() {
+                    show();
+                    return true;
+                }
+
+                @Override
+                protected boolean onForwardingStopped() {
+                    dismiss();
+                    return true;
+                }
+
+                @Override
+                public ShowableListMenu getPopup() {
+                    // This will be null until show() is called.
+                    return mPopup.getPopup();
+                }
+            };
+        }
+
+        return mDragListener;
+    }
+
+    /**
+     * Returns the {@link Menu} associated with this popup. Populate the
+     * returned Menu with items before calling {@link #show()}.
+     *
+     * @return the {@link Menu} associated with this popup
+     * @see #show()
+     * @see #getMenuInflater()
+     */
+    public Menu getMenu() {
+        return mMenu;
+    }
+
+    /**
+     * @return a {@link MenuInflater} that can be used to inflate menu items
+     *         from XML into the menu returned by {@link #getMenu()}
+     * @see #getMenu()
+     */
+    public MenuInflater getMenuInflater() {
+        return new MenuInflater(mContext);
+    }
+
+    /**
+     * Inflate a menu resource into this PopupMenu. This is equivalent to
+     * calling {@code popupMenu.getMenuInflater().inflate(menuRes, popupMenu.getMenu())}.
+     *
+     * @param menuRes Menu resource to inflate
+     */
+    public void inflate(@MenuRes int menuRes) {
+        getMenuInflater().inflate(menuRes, mMenu);
+    }
+
+    /**
+     * Show the menu popup anchored to the view specified during construction.
+     *
+     * @see #dismiss()
+     */
+    public void show() {
+        mPopup.show();
+    }
+
+    /**
+     * Dismiss the menu popup.
+     *
+     * @see #show()
+     */
+    public void dismiss() {
+        mPopup.dismiss();
+    }
+
+    /**
+     * Sets a listener that will be notified when the user selects an item from
+     * the menu.
+     *
+     * @param listener the listener to notify
+     */
+    public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+        mMenuItemClickListener = listener;
+    }
+
+    /**
+     * Sets a listener that will be notified when this menu is dismissed.
+     *
+     * @param listener the listener to notify
+     */
+    public void setOnDismissListener(OnDismissListener listener) {
+        mOnDismissListener = listener;
+    }
+
+    /**
+     * Interface responsible for receiving menu item click events if the items
+     * themselves do not have individual item click listeners.
+     */
+    public interface OnMenuItemClickListener {
+        /**
+         * This method will be invoked when a menu item is clicked if the item
+         * itself did not already handle the event.
+         *
+         * @param item the menu item that was clicked
+         * @return {@code true} if the event was handled, {@code false}
+         *         otherwise
+         */
+        boolean onMenuItemClick(MenuItem item);
+    }
+
+    /**
+     * Callback interface used to notify the application that the menu has closed.
+     */
+    public interface OnDismissListener {
+        /**
+         * Called when the associated menu has been dismissed.
+         *
+         * @param menu the popup menu that was dismissed
+         */
+        void onDismiss(PopupMenu menu);
+    }
+
+    /**
+     * Returns the {@link ListView} representing the list of menu items in the currently showing
+     * menu.
+     *
+     * @return The view representing the list of menu items.
+     * @hide
+     */
+    @TestApi
+    public ListView getMenuListView() {
+        if (!mPopup.isShowing()) {
+            return null;
+        }
+        return mPopup.getPopup().getListView();
+    }
+}
diff --git a/android/widget/PopupWindow.java b/android/widget/PopupWindow.java
new file mode 100644
index 0000000..bf25915
--- /dev/null
+++ b/android/widget/PopupWindow.java
@@ -0,0 +1,2591 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
+import static android.view.ViewGroup.LayoutParams.WRAP_CONTENT;
+import static android.view.WindowManager.LayoutParams
+        .PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
+import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.StateListDrawable;
+import android.os.Build;
+import android.os.IBinder;
+import android.transition.Transition;
+import android.transition.Transition.EpicenterCallback;
+import android.transition.Transition.TransitionListener;
+import android.transition.TransitionInflater;
+import android.transition.TransitionListenerAdapter;
+import android.transition.TransitionManager;
+import android.transition.TransitionSet;
+import android.util.AttributeSet;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.KeyboardShortcutGroup;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnAttachStateChangeListener;
+import android.view.View.OnTouchListener;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.ViewTreeObserver.OnScrollChangedListener;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+import android.view.WindowManager.LayoutParams.SoftInputModeFlags;
+import android.view.WindowManagerGlobal;
+
+import com.android.internal.R;
+
+import java.lang.ref.WeakReference;
+import java.util.List;
+
+/**
+ * <p>
+ * This class represents a popup window that can be used to display an
+ * arbitrary view. The popup window is a floating container that appears on top
+ * of the current activity.
+ * </p>
+ * <a name="Animation"></a>
+ * <h3>Animation</h3>
+ * <p>
+ * On all versions of Android, popup window enter and exit animations may be
+ * specified by calling {@link #setAnimationStyle(int)} and passing the
+ * resource ID for an animation style that defines {@code windowEnterAnimation}
+ * and {@code windowExitAnimation}. For example, passing
+ * {@link android.R.style#Animation_Dialog} will give a scale and alpha
+ * animation.
+ * </br>
+ * A window animation style may also be specified in the popup window's style
+ * XML via the {@link android.R.styleable#PopupWindow_popupAnimationStyle popupAnimationStyle}
+ * attribute.
+ * </p>
+ * <p>
+ * Starting with API 23, more complex popup window enter and exit transitions
+ * may be specified by calling either {@link #setEnterTransition(Transition)}
+ * or {@link #setExitTransition(Transition)} and passing a  {@link Transition}.
+ * </br>
+ * Popup enter and exit transitions may also be specified in the popup window's
+ * style XML via the {@link android.R.styleable#PopupWindow_popupEnterTransition popupEnterTransition}
+ * and {@link android.R.styleable#PopupWindow_popupExitTransition popupExitTransition}
+ * attributes, respectively.
+ * </p>
+ *
+ * @attr ref android.R.styleable#PopupWindow_overlapAnchor
+ * @attr ref android.R.styleable#PopupWindow_popupAnimationStyle
+ * @attr ref android.R.styleable#PopupWindow_popupBackground
+ * @attr ref android.R.styleable#PopupWindow_popupElevation
+ * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+ * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+ *
+ * @see android.widget.AutoCompleteTextView
+ * @see android.widget.Spinner
+ */
+public class PopupWindow {
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: the requirements for the
+     * input method should be based on the focusability of the popup.  That is
+     * if it is focusable than it needs to work with the input method, else
+     * it doesn't.
+     */
+    public static final int INPUT_METHOD_FROM_FOCUSABLE = 0;
+
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: this popup always needs to
+     * work with an input method, regardless of whether it is focusable.  This
+     * means that it will always be displayed so that the user can also operate
+     * the input method while it is shown.
+     */
+    public static final int INPUT_METHOD_NEEDED = 1;
+
+    /**
+     * Mode for {@link #setInputMethodMode(int)}: this popup never needs to
+     * work with an input method, regardless of whether it is focusable.  This
+     * means that it will always be displayed to use as much space on the
+     * screen as needed, regardless of whether this covers the input method.
+     */
+    public static final int INPUT_METHOD_NOT_NEEDED = 2;
+
+    private static final int DEFAULT_ANCHORED_GRAVITY = Gravity.TOP | Gravity.START;
+
+    /**
+     * Default animation style indicating that separate animations should be
+     * used for top/bottom anchoring states.
+     */
+    private static final int ANIMATION_STYLE_DEFAULT = -1;
+
+    private final int[] mTmpDrawingLocation = new int[2];
+    private final int[] mTmpScreenLocation = new int[2];
+    private final int[] mTmpAppLocation = new int[2];
+    private final Rect mTempRect = new Rect();
+
+    private Context mContext;
+    private WindowManager mWindowManager;
+
+    /**
+     * Keeps track of popup's parent's decor view. This is needed to dispatch
+     * requestKeyboardShortcuts to the owning Activity.
+     */
+    private WeakReference<View> mParentRootView;
+
+    private boolean mIsShowing;
+    private boolean mIsTransitioningToDismiss;
+    private boolean mIsDropdown;
+
+    /** View that handles event dispatch and content transitions. */
+    private PopupDecorView mDecorView;
+
+    /** View that holds the background and may animate during a transition. */
+    private View mBackgroundView;
+
+    /** The contents of the popup. May be identical to the background view. */
+    private View mContentView;
+
+    private boolean mFocusable;
+    private int mInputMethodMode = INPUT_METHOD_FROM_FOCUSABLE;
+    @SoftInputModeFlags
+    private int mSoftInputMode = WindowManager.LayoutParams.SOFT_INPUT_STATE_UNCHANGED;
+    private boolean mTouchable = true;
+    private boolean mOutsideTouchable = false;
+    private boolean mClippingEnabled = true;
+    private int mSplitTouchEnabled = -1;
+    private boolean mLayoutInScreen;
+    private boolean mClipToScreen;
+    private boolean mAllowScrollingAnchorParent = true;
+    private boolean mLayoutInsetDecor = false;
+    private boolean mNotTouchModal;
+    private boolean mAttachedInDecor = true;
+    private boolean mAttachedInDecorSet = false;
+
+    private OnTouchListener mTouchInterceptor;
+
+    private int mWidthMode;
+    private int mWidth = LayoutParams.WRAP_CONTENT;
+    private int mLastWidth;
+    private int mHeightMode;
+    private int mHeight = LayoutParams.WRAP_CONTENT;
+    private int mLastHeight;
+
+    private float mElevation;
+
+    private Drawable mBackground;
+    private Drawable mAboveAnchorBackgroundDrawable;
+    private Drawable mBelowAnchorBackgroundDrawable;
+
+    private Transition mEnterTransition;
+    private Transition mExitTransition;
+    private Rect mEpicenterBounds;
+
+    private boolean mAboveAnchor;
+    private int mWindowLayoutType = WindowManager.LayoutParams.TYPE_APPLICATION_PANEL;
+
+    private OnDismissListener mOnDismissListener;
+    private boolean mIgnoreCheekPress = false;
+
+    private int mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+
+    private int mGravity = Gravity.NO_GRAVITY;
+
+    private static final int[] ABOVE_ANCHOR_STATE_SET = new int[] {
+            com.android.internal.R.attr.state_above_anchor
+    };
+
+    private final OnAttachStateChangeListener mOnAnchorDetachedListener =
+            new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {
+                    // Anchor might have been reattached in a different position.
+                    alignToAnchor();
+                }
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    // Leave the popup in its current position.
+                    // The anchor might become attached again.
+                }
+            };
+
+    private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+            new OnAttachStateChangeListener() {
+                @Override
+                public void onViewAttachedToWindow(View v) {}
+
+                @Override
+                public void onViewDetachedFromWindow(View v) {
+                    mIsAnchorRootAttached = false;
+                }
+            };
+
+    private WeakReference<View> mAnchor;
+    private WeakReference<View> mAnchorRoot;
+    private boolean mIsAnchorRootAttached;
+
+    private final OnScrollChangedListener mOnScrollChangedListener = this::alignToAnchor;
+
+    private final View.OnLayoutChangeListener mOnLayoutChangeListener =
+            (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> alignToAnchor();
+
+    private int mAnchorXoff;
+    private int mAnchorYoff;
+    private int mAnchoredGravity;
+    private boolean mOverlapAnchor;
+
+    private boolean mPopupViewInitialLayoutDirectionInherited;
+
+    /**
+     * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+     *
+     * <p>The popup does provide a background.</p>
+     */
+    public PopupWindow(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+     *
+     * <p>The popup does provide a background.</p>
+     */
+    public PopupWindow(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.popupWindowStyle);
+    }
+
+    /**
+     * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+     *
+     * <p>The popup does provide a background.</p>
+     */
+    public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * <p>Create a new, empty, non focusable popup window of dimension (0,0).</p>
+     *
+     * <p>The popup does not provide a background.</p>
+     */
+    public PopupWindow(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        mContext = context;
+        mWindowManager = (WindowManager) context.getSystemService(Context.WINDOW_SERVICE);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.PopupWindow, defStyleAttr, defStyleRes);
+        final Drawable bg = a.getDrawable(R.styleable.PopupWindow_popupBackground);
+        mElevation = a.getDimension(R.styleable.PopupWindow_popupElevation, 0);
+        mOverlapAnchor = a.getBoolean(R.styleable.PopupWindow_overlapAnchor, false);
+
+        // Preserve default behavior from Gingerbread. If the animation is
+        // undefined or explicitly specifies the Gingerbread animation style,
+        // use a sentinel value.
+        if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupAnimationStyle)) {
+            final int animStyle = a.getResourceId(R.styleable.PopupWindow_popupAnimationStyle, 0);
+            if (animStyle == R.style.Animation_PopupWindow) {
+                mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+            } else {
+                mAnimationStyle = animStyle;
+            }
+        } else {
+            mAnimationStyle = ANIMATION_STYLE_DEFAULT;
+        }
+
+        final Transition enterTransition = getTransition(a.getResourceId(
+                R.styleable.PopupWindow_popupEnterTransition, 0));
+        final Transition exitTransition;
+        if (a.hasValueOrEmpty(R.styleable.PopupWindow_popupExitTransition)) {
+            exitTransition = getTransition(a.getResourceId(
+                    R.styleable.PopupWindow_popupExitTransition, 0));
+        } else {
+            exitTransition = enterTransition == null ? null : enterTransition.clone();
+        }
+
+        a.recycle();
+
+        setEnterTransition(enterTransition);
+        setExitTransition(exitTransition);
+        setBackgroundDrawable(bg);
+    }
+
+    /**
+     * <p>Create a new empty, non focusable popup window of dimension (0,0).</p>
+     *
+     * <p>The popup does not provide any background. This should be handled
+     * by the content view.</p>
+     */
+    public PopupWindow() {
+        this(null, 0, 0);
+    }
+
+    /**
+     * <p>Create a new non focusable popup window which can display the
+     * <tt>contentView</tt>. The dimension of the window are (0,0).</p>
+     *
+     * <p>The popup does not provide any background. This should be handled
+     * by the content view.</p>
+     *
+     * @param contentView the popup's content
+     */
+    public PopupWindow(View contentView) {
+        this(contentView, 0, 0);
+    }
+
+    /**
+     * <p>Create a new empty, non focusable popup window. The dimension of the
+     * window must be passed to this constructor.</p>
+     *
+     * <p>The popup does not provide any background. This should be handled
+     * by the content view.</p>
+     *
+     * @param width the popup's width
+     * @param height the popup's height
+     */
+    public PopupWindow(int width, int height) {
+        this(null, width, height);
+    }
+
+    /**
+     * <p>Create a new non focusable popup window which can display the
+     * <tt>contentView</tt>. The dimension of the window must be passed to
+     * this constructor.</p>
+     *
+     * <p>The popup does not provide any background. This should be handled
+     * by the content view.</p>
+     *
+     * @param contentView the popup's content
+     * @param width the popup's width
+     * @param height the popup's height
+     */
+    public PopupWindow(View contentView, int width, int height) {
+        this(contentView, width, height, false);
+    }
+
+    /**
+     * <p>Create a new popup window which can display the <tt>contentView</tt>.
+     * The dimension of the window must be passed to this constructor.</p>
+     *
+     * <p>The popup does not provide any background. This should be handled
+     * by the content view.</p>
+     *
+     * @param contentView the popup's content
+     * @param width the popup's width
+     * @param height the popup's height
+     * @param focusable true if the popup can be focused, false otherwise
+     */
+    public PopupWindow(View contentView, int width, int height, boolean focusable) {
+        if (contentView != null) {
+            mContext = contentView.getContext();
+            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        }
+
+        setContentView(contentView);
+        setWidth(width);
+        setHeight(height);
+        setFocusable(focusable);
+    }
+
+    /**
+     * Sets the enter transition to be used when the popup window is shown.
+     *
+     * @param enterTransition the enter transition, or {@code null} to clear
+     * @see #getEnterTransition()
+     * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+     */
+    public void setEnterTransition(@Nullable Transition enterTransition) {
+        mEnterTransition = enterTransition;
+    }
+
+    /**
+     * Returns the enter transition to be used when the popup window is shown.
+     *
+     * @return the enter transition, or {@code null} if not set
+     * @see #setEnterTransition(Transition)
+     * @attr ref android.R.styleable#PopupWindow_popupEnterTransition
+     */
+    @Nullable
+    public Transition getEnterTransition() {
+        return mEnterTransition;
+    }
+
+    /**
+     * Sets the exit transition to be used when the popup window is dismissed.
+     *
+     * @param exitTransition the exit transition, or {@code null} to clear
+     * @see #getExitTransition()
+     * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+     */
+    public void setExitTransition(@Nullable Transition exitTransition) {
+        mExitTransition = exitTransition;
+    }
+
+    /**
+     * Returns the exit transition to be used when the popup window is
+     * dismissed.
+     *
+     * @return the exit transition, or {@code null} if not set
+     * @see #setExitTransition(Transition)
+     * @attr ref android.R.styleable#PopupWindow_popupExitTransition
+     */
+    @Nullable
+    public Transition getExitTransition() {
+        return mExitTransition;
+    }
+
+    /**
+     * Sets the bounds used as the epicenter of the enter and exit transitions.
+     * <p>
+     * Transitions use a point or Rect, referred to as the epicenter, to orient
+     * the direction of travel. For popup windows, the anchor view bounds are
+     * used as the default epicenter.
+     * <p>
+     * See {@link Transition#setEpicenterCallback(EpicenterCallback)} for more
+     * information about how transition epicenters.
+     *
+     * @param bounds the epicenter bounds relative to the anchor view, or
+     *               {@code null} to use the default epicenter
+     * @see #getTransitionEpicenter()
+     * @hide
+     */
+    public void setEpicenterBounds(Rect bounds) {
+        mEpicenterBounds = bounds;
+    }
+
+    private Transition getTransition(int resId) {
+        if (resId != 0 && resId != R.transition.no_transition) {
+            final TransitionInflater inflater = TransitionInflater.from(mContext);
+            final Transition transition = inflater.inflateTransition(resId);
+            if (transition != null) {
+                final boolean isEmpty = transition instanceof TransitionSet
+                        && ((TransitionSet) transition).getTransitionCount() == 0;
+                if (!isEmpty) {
+                    return transition;
+                }
+            }
+        }
+        return null;
+    }
+
+    /**
+     * Return the drawable used as the popup window's background.
+     *
+     * @return the background drawable or {@code null} if not set
+     * @see #setBackgroundDrawable(Drawable)
+     * @attr ref android.R.styleable#PopupWindow_popupBackground
+     */
+    public Drawable getBackground() {
+        return mBackground;
+    }
+
+    /**
+     * Specifies the background drawable for this popup window. The background
+     * can be set to {@code null}.
+     *
+     * @param background the popup's background
+     * @see #getBackground()
+     * @attr ref android.R.styleable#PopupWindow_popupBackground
+     */
+    public void setBackgroundDrawable(Drawable background) {
+        mBackground = background;
+
+        // If this is a StateListDrawable, try to find and store the drawable to be
+        // used when the drop-down is placed above its anchor view, and the one to be
+        // used when the drop-down is placed below its anchor view. We extract
+        // the drawables ourselves to work around a problem with using refreshDrawableState
+        // that it will take into account the padding of all drawables specified in a
+        // StateListDrawable, thus adding superfluous padding to drop-down views.
+        //
+        // We assume a StateListDrawable will have a drawable for ABOVE_ANCHOR_STATE_SET and
+        // at least one other drawable, intended for the 'below-anchor state'.
+        if (mBackground instanceof StateListDrawable) {
+            StateListDrawable stateList = (StateListDrawable) mBackground;
+
+            // Find the above-anchor view - this one's easy, it should be labeled as such.
+            int aboveAnchorStateIndex = stateList.getStateDrawableIndex(ABOVE_ANCHOR_STATE_SET);
+
+            // Now, for the below-anchor view, look for any other drawable specified in the
+            // StateListDrawable which is not for the above-anchor state and use that.
+            int count = stateList.getStateCount();
+            int belowAnchorStateIndex = -1;
+            for (int i = 0; i < count; i++) {
+                if (i != aboveAnchorStateIndex) {
+                    belowAnchorStateIndex = i;
+                    break;
+                }
+            }
+
+            // Store the drawables we found, if we found them. Otherwise, set them both
+            // to null so that we'll just use refreshDrawableState.
+            if (aboveAnchorStateIndex != -1 && belowAnchorStateIndex != -1) {
+                mAboveAnchorBackgroundDrawable = stateList.getStateDrawable(aboveAnchorStateIndex);
+                mBelowAnchorBackgroundDrawable = stateList.getStateDrawable(belowAnchorStateIndex);
+            } else {
+                mBelowAnchorBackgroundDrawable = null;
+                mAboveAnchorBackgroundDrawable = null;
+            }
+        }
+    }
+
+    /**
+     * @return the elevation for this popup window in pixels
+     * @see #setElevation(float)
+     * @attr ref android.R.styleable#PopupWindow_popupElevation
+     */
+    public float getElevation() {
+        return mElevation;
+    }
+
+    /**
+     * Specifies the elevation for this popup window.
+     *
+     * @param elevation the popup's elevation in pixels
+     * @see #getElevation()
+     * @attr ref android.R.styleable#PopupWindow_popupElevation
+     */
+    public void setElevation(float elevation) {
+        mElevation = elevation;
+    }
+
+    /**
+     * <p>Return the animation style to use the popup appears and disappears</p>
+     *
+     * @return the animation style to use the popup appears and disappears
+     */
+    public int getAnimationStyle() {
+        return mAnimationStyle;
+    }
+
+    /**
+     * Set the flag on popup to ignore cheek press events; by default this flag
+     * is set to false
+     * which means the popup will not ignore cheek press dispatch events.
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @see #update()
+     */
+    public void setIgnoreCheekPress() {
+        mIgnoreCheekPress = true;
+    }
+
+    /**
+     * <p>Change the animation style resource for this popup.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @param animationStyle animation style to use when the popup appears
+     *      and disappears.  Set to -1 for the default animation, 0 for no
+     *      animation, or a resource identifier for an explicit animation.
+     *
+     * @see #update()
+     */
+    public void setAnimationStyle(int animationStyle) {
+        mAnimationStyle = animationStyle;
+    }
+
+    /**
+     * <p>Return the view used as the content of the popup window.</p>
+     *
+     * @return a {@link android.view.View} representing the popup's content
+     *
+     * @see #setContentView(android.view.View)
+     */
+    public View getContentView() {
+        return mContentView;
+    }
+
+    /**
+     * <p>Change the popup's content. The content is represented by an instance
+     * of {@link android.view.View}.</p>
+     *
+     * <p>This method has no effect if called when the popup is showing.</p>
+     *
+     * @param contentView the new content for the popup
+     *
+     * @see #getContentView()
+     * @see #isShowing()
+     */
+    public void setContentView(View contentView) {
+        if (isShowing()) {
+            return;
+        }
+
+        mContentView = contentView;
+
+        if (mContext == null && mContentView != null) {
+            mContext = mContentView.getContext();
+        }
+
+        if (mWindowManager == null && mContentView != null) {
+            mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        }
+
+        // Setting the default for attachedInDecor based on SDK version here
+        // instead of in the constructor since we might not have the context
+        // object in the constructor. We only want to set default here if the
+        // app hasn't already set the attachedInDecor.
+        if (mContext != null && !mAttachedInDecorSet) {
+            // Attach popup window in decor frame of parent window by default for
+            // {@link Build.VERSION_CODES.LOLLIPOP_MR1} or greater. Keep current
+            // behavior of not attaching to decor frame for older SDKs.
+            setAttachedInDecor(mContext.getApplicationInfo().targetSdkVersion
+                    >= Build.VERSION_CODES.LOLLIPOP_MR1);
+        }
+
+    }
+
+    /**
+     * Set a callback for all touch events being dispatched to the popup
+     * window.
+     */
+    public void setTouchInterceptor(OnTouchListener l) {
+        mTouchInterceptor = l;
+    }
+
+    /**
+     * <p>Indicate whether the popup window can grab the focus.</p>
+     *
+     * @return true if the popup is focusable, false otherwise
+     *
+     * @see #setFocusable(boolean)
+     */
+    public boolean isFocusable() {
+        return mFocusable;
+    }
+
+    /**
+     * <p>Changes the focusability of the popup window. When focusable, the
+     * window will grab the focus from the current focused widget if the popup
+     * contains a focusable {@link android.view.View}.  By default a popup
+     * window is not focusable.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @param focusable true if the popup should grab focus, false otherwise.
+     *
+     * @see #isFocusable()
+     * @see #isShowing()
+     * @see #update()
+     */
+    public void setFocusable(boolean focusable) {
+        mFocusable = focusable;
+    }
+
+    /**
+     * Return the current value in {@link #setInputMethodMode(int)}.
+     *
+     * @see #setInputMethodMode(int)
+     */
+    public int getInputMethodMode() {
+        return mInputMethodMode;
+
+    }
+
+    /**
+     * Control how the popup operates with an input method: one of
+     * {@link #INPUT_METHOD_FROM_FOCUSABLE}, {@link #INPUT_METHOD_NEEDED},
+     * or {@link #INPUT_METHOD_NOT_NEEDED}.
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @see #getInputMethodMode()
+     * @see #update()
+     */
+    public void setInputMethodMode(int mode) {
+        mInputMethodMode = mode;
+    }
+
+    /**
+     * Sets the operating mode for the soft input area.
+     *
+     * @param mode The desired mode, see
+     *        {@link android.view.WindowManager.LayoutParams#softInputMode}
+     *        for the full list
+     *
+     * @see android.view.WindowManager.LayoutParams#softInputMode
+     * @see #getSoftInputMode()
+     */
+    public void setSoftInputMode(@SoftInputModeFlags int mode) {
+        mSoftInputMode = mode;
+    }
+
+    /**
+     * Returns the current value in {@link #setSoftInputMode(int)}.
+     *
+     * @see #setSoftInputMode(int)
+     * @see android.view.WindowManager.LayoutParams#softInputMode
+     */
+    @SoftInputModeFlags
+    public int getSoftInputMode() {
+        return mSoftInputMode;
+    }
+
+    /**
+     * <p>Indicates whether the popup window receives touch events.</p>
+     *
+     * @return true if the popup is touchable, false otherwise
+     *
+     * @see #setTouchable(boolean)
+     */
+    public boolean isTouchable() {
+        return mTouchable;
+    }
+
+    /**
+     * <p>Changes the touchability of the popup window. When touchable, the
+     * window will receive touch events, otherwise touch events will go to the
+     * window below it. By default the window is touchable.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @param touchable true if the popup should receive touch events, false otherwise
+     *
+     * @see #isTouchable()
+     * @see #isShowing()
+     * @see #update()
+     */
+    public void setTouchable(boolean touchable) {
+        mTouchable = touchable;
+    }
+
+    /**
+     * <p>Indicates whether the popup window will be informed of touch events
+     * outside of its window.</p>
+     *
+     * @return true if the popup is outside touchable, false otherwise
+     *
+     * @see #setOutsideTouchable(boolean)
+     */
+    public boolean isOutsideTouchable() {
+        return mOutsideTouchable;
+    }
+
+    /**
+     * <p>Controls whether the pop-up will be informed of touch events outside
+     * of its window.  This only makes sense for pop-ups that are touchable
+     * but not focusable, which means touches outside of the window will
+     * be delivered to the window behind.  The default is false.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @param touchable true if the popup should receive outside
+     * touch events, false otherwise
+     *
+     * @see #isOutsideTouchable()
+     * @see #isShowing()
+     * @see #update()
+     */
+    public void setOutsideTouchable(boolean touchable) {
+        mOutsideTouchable = touchable;
+    }
+
+    /**
+     * <p>Indicates whether clipping of the popup window is enabled.</p>
+     *
+     * @return true if the clipping is enabled, false otherwise
+     *
+     * @see #setClippingEnabled(boolean)
+     */
+    public boolean isClippingEnabled() {
+        return mClippingEnabled;
+    }
+
+    /**
+     * <p>Allows the popup window to extend beyond the bounds of the screen. By default the
+     * window is clipped to the screen boundaries. Setting this to false will allow windows to be
+     * accurately positioned.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown or through a manual call to one of
+     * the {@link #update()} methods.</p>
+     *
+     * @param enabled false if the window should be allowed to extend outside of the screen
+     * @see #isShowing()
+     * @see #isClippingEnabled()
+     * @see #update()
+     */
+    public void setClippingEnabled(boolean enabled) {
+        mClippingEnabled = enabled;
+    }
+
+    /**
+     * Clip this popup window to the screen, but not to the containing window.
+     *
+     * @param enabled True to clip to the screen.
+     * @hide
+     */
+    public void setClipToScreenEnabled(boolean enabled) {
+        mClipToScreen = enabled;
+    }
+
+    /**
+     * Allow PopupWindow to scroll the anchor's parent to provide more room
+     * for the popup. Enabled by default.
+     *
+     * @param enabled True to scroll the anchor's parent when more room is desired by the popup.
+     */
+    void setAllowScrollingAnchorParent(boolean enabled) {
+        mAllowScrollingAnchorParent = enabled;
+    }
+
+    /** @hide */
+    protected final boolean getAllowScrollingAnchorParent() {
+        return mAllowScrollingAnchorParent;
+    }
+
+    /**
+     * <p>Indicates whether the popup window supports splitting touches.</p>
+     *
+     * @return true if the touch splitting is enabled, false otherwise
+     *
+     * @see #setSplitTouchEnabled(boolean)
+     */
+    public boolean isSplitTouchEnabled() {
+        if (mSplitTouchEnabled < 0 && mContext != null) {
+            return mContext.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB;
+        }
+        return mSplitTouchEnabled == 1;
+    }
+
+    /**
+     * <p>Allows the popup window to split touches across other windows that also
+     * support split touch.  When this flag is false, the first pointer
+     * that goes down determines the window to which all subsequent touches
+     * go until all pointers go up.  When this flag is true, each pointer
+     * (not necessarily the first) that goes down determines the window
+     * to which all subsequent touches of that pointer will go until that
+     * pointer goes up thereby enabling touches with multiple pointers
+     * to be split across multiple windows.</p>
+     *
+     * @param enabled true if the split touches should be enabled, false otherwise
+     * @see #isSplitTouchEnabled()
+     */
+    public void setSplitTouchEnabled(boolean enabled) {
+        mSplitTouchEnabled = enabled ? 1 : 0;
+    }
+
+    /**
+     * <p>Indicates whether the popup window will be forced into using absolute screen coordinates
+     * for positioning.</p>
+     *
+     * @return true if the window will always be positioned in screen coordinates.
+     * @hide
+     */
+    public boolean isLayoutInScreenEnabled() {
+        return mLayoutInScreen;
+    }
+
+    /**
+     * <p>Allows the popup window to force the flag
+     * {@link WindowManager.LayoutParams#FLAG_LAYOUT_IN_SCREEN}, overriding default behavior.
+     * This will cause the popup to be positioned in absolute screen coordinates.</p>
+     *
+     * @param enabled true if the popup should always be positioned in screen coordinates
+     * @hide
+     */
+    public void setLayoutInScreenEnabled(boolean enabled) {
+        mLayoutInScreen = enabled;
+    }
+
+    /**
+     * <p>Indicates whether the popup window will be attached in the decor frame of its parent
+     * window.
+     *
+     * @return true if the window will be attached to the decor frame of its parent window.
+     *
+     * @see #setAttachedInDecor(boolean)
+     * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR
+     */
+    public boolean isAttachedInDecor() {
+        return mAttachedInDecor;
+    }
+
+    /**
+     * <p>This will attach the popup window to the decor frame of the parent window to avoid
+     * overlaping with screen decorations like the navigation bar. Overrides the default behavior of
+     * the flag {@link WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR}.
+     *
+     * <p>By default the flag is set on SDK version {@link Build.VERSION_CODES#LOLLIPOP_MR1} or
+     * greater and cleared on lesser SDK versions.
+     *
+     * @param enabled true if the popup should be attached to the decor frame of its parent window.
+     *
+     * @see WindowManager.LayoutParams#FLAG_LAYOUT_ATTACHED_IN_DECOR
+     */
+    public void setAttachedInDecor(boolean enabled) {
+        mAttachedInDecor = enabled;
+        mAttachedInDecorSet = true;
+    }
+
+    /**
+     * Allows the popup window to force the flag
+     * {@link WindowManager.LayoutParams#FLAG_LAYOUT_INSET_DECOR}, overriding default behavior.
+     * This will cause the popup to inset its content to account for system windows overlaying
+     * the screen, such as the status bar.
+     *
+     * <p>This will often be combined with {@link #setLayoutInScreenEnabled(boolean)}.
+     *
+     * @param enabled true if the popup's views should inset content to account for system windows,
+     *                the way that decor views behave for full-screen windows.
+     * @hide
+     */
+    public void setLayoutInsetDecor(boolean enabled) {
+        mLayoutInsetDecor = enabled;
+    }
+
+    /** @hide */
+    protected final boolean isLayoutInsetDecor() {
+        return mLayoutInsetDecor;
+    }
+
+    /**
+     * Set the layout type for this window.
+     * <p>
+     * See {@link WindowManager.LayoutParams#type} for possible values.
+     *
+     * @param layoutType Layout type for this window.
+     *
+     * @see WindowManager.LayoutParams#type
+     */
+    public void setWindowLayoutType(int layoutType) {
+        mWindowLayoutType = layoutType;
+    }
+
+    /**
+     * Returns the layout type for this window.
+     *
+     * @see #setWindowLayoutType(int)
+     */
+    public int getWindowLayoutType() {
+        return mWindowLayoutType;
+    }
+
+    /**
+     * Set whether this window is touch modal or if outside touches will be sent to
+     * other windows behind it.
+     * @hide
+     */
+    public void setTouchModal(boolean touchModal) {
+        mNotTouchModal = !touchModal;
+    }
+
+    /**
+     * <p>Change the width and height measure specs that are given to the
+     * window manager by the popup.  By default these are 0, meaning that
+     * the current width or height is requested as an explicit size from
+     * the window manager.  You can supply
+     * {@link ViewGroup.LayoutParams#WRAP_CONTENT} or
+     * {@link ViewGroup.LayoutParams#MATCH_PARENT} to have that measure
+     * spec supplied instead, replacing the absolute width and height that
+     * has been set in the popup.</p>
+     *
+     * <p>If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown.</p>
+     *
+     * @param widthSpec an explicit width measure spec mode, either
+     * {@link ViewGroup.LayoutParams#WRAP_CONTENT},
+     * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute
+     * width.
+     * @param heightSpec an explicit height measure spec mode, either
+     * {@link ViewGroup.LayoutParams#WRAP_CONTENT},
+     * {@link ViewGroup.LayoutParams#MATCH_PARENT}, or 0 to use the absolute
+     * height.
+     *
+     * @deprecated Use {@link #setWidth(int)} and {@link #setHeight(int)}.
+     */
+    @Deprecated
+    public void setWindowLayoutMode(int widthSpec, int heightSpec) {
+        mWidthMode = widthSpec;
+        mHeightMode = heightSpec;
+    }
+
+    /**
+     * Returns the popup's requested height. May be a layout constant such as
+     * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+     * <p>
+     * The actual size of the popup may depend on other factors such as
+     * clipping and window layout.
+     *
+     * @return the popup height in pixels or a layout constant
+     * @see #setHeight(int)
+     */
+    public int getHeight() {
+        return mHeight;
+    }
+
+    /**
+     * Sets the popup's requested height. May be a layout constant such as
+     * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+     * <p>
+     * The actual size of the popup may depend on other factors such as
+     * clipping and window layout.
+     * <p>
+     * If the popup is showing, calling this method will take effect the next
+     * time the popup is shown.
+     *
+     * @param height the popup height in pixels or a layout constant
+     * @see #getHeight()
+     * @see #isShowing()
+     */
+    public void setHeight(int height) {
+        mHeight = height;
+    }
+
+    /**
+     * Returns the popup's requested width. May be a layout constant such as
+     * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+     * <p>
+     * The actual size of the popup may depend on other factors such as
+     * clipping and window layout.
+     *
+     * @return the popup width in pixels or a layout constant
+     * @see #setWidth(int)
+     */
+    public int getWidth() {
+        return mWidth;
+    }
+
+    /**
+     * Sets the popup's requested width. May be a layout constant such as
+     * {@link LayoutParams#WRAP_CONTENT} or {@link LayoutParams#MATCH_PARENT}.
+     * <p>
+     * The actual size of the popup may depend on other factors such as
+     * clipping and window layout.
+     * <p>
+     * If the popup is showing, calling this method will take effect the next
+     * time the popup is shown.
+     *
+     * @param width the popup width in pixels or a layout constant
+     * @see #getWidth()
+     * @see #isShowing()
+     */
+    public void setWidth(int width) {
+        mWidth = width;
+    }
+
+    /**
+     * Sets whether the popup window should overlap its anchor view when
+     * displayed as a drop-down.
+     * <p>
+     * If the popup is showing, calling this method will take effect only
+     * the next time the popup is shown.
+     *
+     * @param overlapAnchor Whether the popup should overlap its anchor.
+     *
+     * @see #getOverlapAnchor()
+     * @see #isShowing()
+     */
+    public void setOverlapAnchor(boolean overlapAnchor) {
+        mOverlapAnchor = overlapAnchor;
+    }
+
+    /**
+     * Returns whether the popup window should overlap its anchor view when
+     * displayed as a drop-down.
+     *
+     * @return Whether the popup should overlap its anchor.
+     *
+     * @see #setOverlapAnchor(boolean)
+     */
+    public boolean getOverlapAnchor() {
+        return mOverlapAnchor;
+    }
+
+    /**
+     * <p>Indicate whether this popup window is showing on screen.</p>
+     *
+     * @return true if the popup is showing, false otherwise
+     */
+    public boolean isShowing() {
+        return mIsShowing;
+    }
+
+    /** @hide */
+    protected final void setShowing(boolean isShowing) {
+        mIsShowing = isShowing;
+    }
+
+    /** @hide */
+    protected final void setDropDown(boolean isDropDown) {
+        mIsDropdown = isDropDown;
+    }
+
+    /** @hide */
+    protected final void setTransitioningToDismiss(boolean transitioningToDismiss) {
+        mIsTransitioningToDismiss = transitioningToDismiss;
+    }
+
+    /** @hide */
+    protected final boolean isTransitioningToDismiss() {
+        return mIsTransitioningToDismiss;
+    }
+
+    /**
+     * <p>
+     * Display the content view in a popup window at the specified location. If the popup window
+     * cannot fit on screen, it will be clipped. See {@link android.view.WindowManager.LayoutParams}
+     * for more information on how gravity and the x and y parameters are related. Specifying
+     * a gravity of {@link android.view.Gravity#NO_GRAVITY} is similar to specifying
+     * <code>Gravity.LEFT | Gravity.TOP</code>.
+     * </p>
+     *
+     * @param parent a parent view to get the {@link android.view.View#getWindowToken()} token from
+     * @param gravity the gravity which controls the placement of the popup window
+     * @param x the popup's x location offset
+     * @param y the popup's y location offset
+     */
+    public void showAtLocation(View parent, int gravity, int x, int y) {
+        mParentRootView = new WeakReference<>(parent.getRootView());
+        showAtLocation(parent.getWindowToken(), gravity, x, y);
+    }
+
+    /**
+     * Display the content view in a popup window at the specified location.
+     *
+     * @param token Window token to use for creating the new window
+     * @param gravity the gravity which controls the placement of the popup window
+     * @param x the popup's x location offset
+     * @param y the popup's y location offset
+     *
+     * @hide Internal use only. Applications should use
+     *       {@link #showAtLocation(View, int, int, int)} instead.
+     */
+    public void showAtLocation(IBinder token, int gravity, int x, int y) {
+        if (isShowing() || mContentView == null) {
+            return;
+        }
+
+        TransitionManager.endTransitions(mDecorView);
+
+        detachFromAnchor();
+
+        mIsShowing = true;
+        mIsDropdown = false;
+        mGravity = gravity;
+
+        final WindowManager.LayoutParams p = createPopupLayoutParams(token);
+        preparePopup(p);
+
+        p.x = x;
+        p.y = y;
+
+        invokePopup(p);
+    }
+
+    /**
+     * Display the content view in a popup window anchored to the bottom-left
+     * corner of the anchor view. If there is not enough room on screen to show
+     * the popup in its entirety, this method tries to find a parent scroll
+     * view to scroll. If no parent scroll view can be scrolled, the
+     * bottom-left corner of the popup is pinned at the top left corner of the
+     * anchor view.
+     *
+     * @param anchor the view on which to pin the popup window
+     *
+     * @see #dismiss()
+     */
+    public void showAsDropDown(View anchor) {
+        showAsDropDown(anchor, 0, 0);
+    }
+
+    /**
+     * Display the content view in a popup window anchored to the bottom-left
+     * corner of the anchor view offset by the specified x and y coordinates.
+     * If there is not enough room on screen to show the popup in its entirety,
+     * this method tries to find a parent scroll view to scroll. If no parent
+     * scroll view can be scrolled, the bottom-left corner of the popup is
+     * pinned at the top left corner of the anchor view.
+     * <p>
+     * If the view later scrolls to move <code>anchor</code> to a different
+     * location, the popup will be moved correspondingly.
+     *
+     * @param anchor the view on which to pin the popup window
+     * @param xoff A horizontal offset from the anchor in pixels
+     * @param yoff A vertical offset from the anchor in pixels
+     *
+     * @see #dismiss()
+     */
+    public void showAsDropDown(View anchor, int xoff, int yoff) {
+        showAsDropDown(anchor, xoff, yoff, DEFAULT_ANCHORED_GRAVITY);
+    }
+
+    /**
+     * Displays the content view in a popup window anchored to the corner of
+     * another view. The window is positioned according to the specified
+     * gravity and offset by the specified x and y coordinates.
+     * <p>
+     * If there is not enough room on screen to show the popup in its entirety,
+     * this method tries to find a parent scroll view to scroll. If no parent
+     * view can be scrolled, the specified vertical gravity will be ignored and
+     * the popup will anchor itself such that it is visible.
+     * <p>
+     * If the view later scrolls to move <code>anchor</code> to a different
+     * location, the popup will be moved correspondingly.
+     *
+     * @param anchor the view on which to pin the popup window
+     * @param xoff A horizontal offset from the anchor in pixels
+     * @param yoff A vertical offset from the anchor in pixels
+     * @param gravity Alignment of the popup relative to the anchor
+     *
+     * @see #dismiss()
+     */
+    public void showAsDropDown(View anchor, int xoff, int yoff, int gravity) {
+        if (isShowing() || !hasContentView()) {
+            return;
+        }
+
+        TransitionManager.endTransitions(mDecorView);
+
+        attachToAnchor(anchor, xoff, yoff, gravity);
+
+        mIsShowing = true;
+        mIsDropdown = true;
+
+        final WindowManager.LayoutParams p =
+                createPopupLayoutParams(anchor.getApplicationWindowToken());
+        preparePopup(p);
+
+        final boolean aboveAnchor = findDropDownPosition(anchor, p, xoff, yoff,
+                p.width, p.height, gravity, mAllowScrollingAnchorParent);
+        updateAboveAnchor(aboveAnchor);
+        p.accessibilityIdOfAnchor = (anchor != null) ? anchor.getAccessibilityViewId() : -1;
+
+        invokePopup(p);
+    }
+
+    /** @hide */
+    protected final void updateAboveAnchor(boolean aboveAnchor) {
+        if (aboveAnchor != mAboveAnchor) {
+            mAboveAnchor = aboveAnchor;
+
+            if (mBackground != null && mBackgroundView != null) {
+                // If the background drawable provided was a StateListDrawable
+                // with above-anchor and below-anchor states, use those.
+                // Otherwise, rely on refreshDrawableState to do the job.
+                if (mAboveAnchorBackgroundDrawable != null) {
+                    if (mAboveAnchor) {
+                        mBackgroundView.setBackground(mAboveAnchorBackgroundDrawable);
+                    } else {
+                        mBackgroundView.setBackground(mBelowAnchorBackgroundDrawable);
+                    }
+                } else {
+                    mBackgroundView.refreshDrawableState();
+                }
+            }
+        }
+    }
+
+    /**
+     * Indicates whether the popup is showing above (the y coordinate of the popup's bottom
+     * is less than the y coordinate of the anchor) or below the anchor view (the y coordinate
+     * of the popup is greater than y coordinate of the anchor's bottom).
+     *
+     * The value returned
+     * by this method is meaningful only after {@link #showAsDropDown(android.view.View)}
+     * or {@link #showAsDropDown(android.view.View, int, int)} was invoked.
+     *
+     * @return True if this popup is showing above the anchor view, false otherwise.
+     */
+    public boolean isAboveAnchor() {
+        return mAboveAnchor;
+    }
+
+    /**
+     * Prepare the popup by embedding it into a new ViewGroup if the background
+     * drawable is not null. If embedding is required, the layout parameters'
+     * height is modified to take into account the background's padding.
+     *
+     * @param p the layout parameters of the popup's content view
+     */
+    private void preparePopup(WindowManager.LayoutParams p) {
+        if (mContentView == null || mContext == null || mWindowManager == null) {
+            throw new IllegalStateException("You must specify a valid content view by "
+                    + "calling setContentView() before attempting to show the popup.");
+        }
+
+        if (p.accessibilityTitle == null) {
+            p.accessibilityTitle = mContext.getString(R.string.popup_window_default_title);
+        }
+
+        // The old decor view may be transitioning out. Make sure it finishes
+        // and cleans up before we try to create another one.
+        if (mDecorView != null) {
+            mDecorView.cancelTransitions();
+        }
+
+        // When a background is available, we embed the content view within
+        // another view that owns the background drawable.
+        if (mBackground != null) {
+            mBackgroundView = createBackgroundView(mContentView);
+            mBackgroundView.setBackground(mBackground);
+        } else {
+            mBackgroundView = mContentView;
+        }
+
+        mDecorView = createDecorView(mBackgroundView);
+
+        // The background owner should be elevated so that it casts a shadow.
+        mBackgroundView.setElevation(mElevation);
+
+        // We may wrap that in another view, so we'll need to manually specify
+        // the surface insets.
+        p.setSurfaceInsets(mBackgroundView, true /*manual*/, true /*preservePrevious*/);
+
+        mPopupViewInitialLayoutDirectionInherited =
+                (mContentView.getRawLayoutDirection() == View.LAYOUT_DIRECTION_INHERIT);
+    }
+
+    /**
+     * Wraps a content view in a PopupViewContainer.
+     *
+     * @param contentView the content view to wrap
+     * @return a PopupViewContainer that wraps the content view
+     */
+    private PopupBackgroundView createBackgroundView(View contentView) {
+        final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
+        final int height;
+        if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
+            height = WRAP_CONTENT;
+        } else {
+            height = MATCH_PARENT;
+        }
+
+        final PopupBackgroundView backgroundView = new PopupBackgroundView(mContext);
+        final PopupBackgroundView.LayoutParams listParams = new PopupBackgroundView.LayoutParams(
+                MATCH_PARENT, height);
+        backgroundView.addView(contentView, listParams);
+
+        return backgroundView;
+    }
+
+    /**
+     * Wraps a content view in a FrameLayout.
+     *
+     * @param contentView the content view to wrap
+     * @return a FrameLayout that wraps the content view
+     */
+    private PopupDecorView createDecorView(View contentView) {
+        final ViewGroup.LayoutParams layoutParams = mContentView.getLayoutParams();
+        final int height;
+        if (layoutParams != null && layoutParams.height == WRAP_CONTENT) {
+            height = WRAP_CONTENT;
+        } else {
+            height = MATCH_PARENT;
+        }
+
+        final PopupDecorView decorView = new PopupDecorView(mContext);
+        decorView.addView(contentView, MATCH_PARENT, height);
+        decorView.setClipChildren(false);
+        decorView.setClipToPadding(false);
+
+        return decorView;
+    }
+
+    /**
+     * <p>Invoke the popup window by adding the content view to the window
+     * manager.</p>
+     *
+     * <p>The content view must be non-null when this method is invoked.</p>
+     *
+     * @param p the layout parameters of the popup's content view
+     */
+    private void invokePopup(WindowManager.LayoutParams p) {
+        if (mContext != null) {
+            p.packageName = mContext.getPackageName();
+        }
+
+        final PopupDecorView decorView = mDecorView;
+        decorView.setFitsSystemWindows(mLayoutInsetDecor);
+
+        setLayoutDirectionFromAnchor();
+
+        mWindowManager.addView(decorView, p);
+
+        if (mEnterTransition != null) {
+            decorView.requestEnterTransition(mEnterTransition);
+        }
+    }
+
+    private void setLayoutDirectionFromAnchor() {
+        if (mAnchor != null) {
+            View anchor = mAnchor.get();
+            if (anchor != null && mPopupViewInitialLayoutDirectionInherited) {
+                mDecorView.setLayoutDirection(anchor.getLayoutDirection());
+            }
+        }
+    }
+
+    private int computeGravity() {
+        int gravity = mGravity == Gravity.NO_GRAVITY ?  Gravity.START | Gravity.TOP : mGravity;
+        if (mIsDropdown && (mClipToScreen || mClippingEnabled)) {
+            gravity |= Gravity.DISPLAY_CLIP_VERTICAL;
+        }
+        return gravity;
+    }
+
+    /**
+     * <p>Generate the layout parameters for the popup window.</p>
+     *
+     * @param token the window token used to bind the popup's window
+     *
+     * @return the layout parameters to pass to the window manager
+     *
+     * @hide
+     */
+    protected final WindowManager.LayoutParams createPopupLayoutParams(IBinder token) {
+        final WindowManager.LayoutParams p = new WindowManager.LayoutParams();
+
+        // These gravity settings put the view at the top left corner of the
+        // screen. The view is then positioned to the appropriate location by
+        // setting the x and y offsets to match the anchor's bottom-left
+        // corner.
+        p.gravity = computeGravity();
+        p.flags = computeFlags(p.flags);
+        p.type = mWindowLayoutType;
+        p.token = token;
+        p.softInputMode = mSoftInputMode;
+        p.windowAnimations = computeAnimationResource();
+
+        if (mBackground != null) {
+            p.format = mBackground.getOpacity();
+        } else {
+            p.format = PixelFormat.TRANSLUCENT;
+        }
+
+        if (mHeightMode < 0) {
+            p.height = mLastHeight = mHeightMode;
+        } else {
+            p.height = mLastHeight = mHeight;
+        }
+
+        if (mWidthMode < 0) {
+            p.width = mLastWidth = mWidthMode;
+        } else {
+            p.width = mLastWidth = mWidth;
+        }
+
+        p.privateFlags = PRIVATE_FLAG_WILL_NOT_REPLACE_ON_RELAUNCH
+                | PRIVATE_FLAG_LAYOUT_CHILD_WINDOW_IN_PARENT_FRAME;
+
+        // Used for debugging.
+        p.setTitle("PopupWindow:" + Integer.toHexString(hashCode()));
+
+        return p;
+    }
+
+    private int computeFlags(int curFlags) {
+        curFlags &= ~(
+                WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES |
+                WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE |
+                WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE |
+                WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH |
+                WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS |
+                WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
+                WindowManager.LayoutParams.FLAG_SPLIT_TOUCH);
+        if(mIgnoreCheekPress) {
+            curFlags |= WindowManager.LayoutParams.FLAG_IGNORE_CHEEK_PRESSES;
+        }
+        if (!mFocusable) {
+            curFlags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
+            if (mInputMethodMode == INPUT_METHOD_NEEDED) {
+                curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+            }
+        } else if (mInputMethodMode == INPUT_METHOD_NOT_NEEDED) {
+            curFlags |= WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        }
+        if (!mTouchable) {
+            curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+        }
+        if (mOutsideTouchable) {
+            curFlags |= WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH;
+        }
+        if (!mClippingEnabled || mClipToScreen) {
+            curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_NO_LIMITS;
+        }
+        if (isSplitTouchEnabled()) {
+            curFlags |= WindowManager.LayoutParams.FLAG_SPLIT_TOUCH;
+        }
+        if (mLayoutInScreen) {
+            curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN;
+        }
+        if (mLayoutInsetDecor) {
+            curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR;
+        }
+        if (mNotTouchModal) {
+            curFlags |= WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL;
+        }
+        if (mAttachedInDecor) {
+            curFlags |= WindowManager.LayoutParams.FLAG_LAYOUT_ATTACHED_IN_DECOR;
+        }
+        return curFlags;
+    }
+
+    private int computeAnimationResource() {
+        if (mAnimationStyle == ANIMATION_STYLE_DEFAULT) {
+            if (mIsDropdown) {
+                return mAboveAnchor
+                        ? com.android.internal.R.style.Animation_DropDownUp
+                        : com.android.internal.R.style.Animation_DropDownDown;
+            }
+            return 0;
+        }
+        return mAnimationStyle;
+    }
+
+    /**
+     * Positions the popup window on screen. When the popup window is too tall
+     * to fit under the anchor, a parent scroll view is seeked and scrolled up
+     * to reclaim space. If scrolling is not possible or not enough, the popup
+     * window gets moved on top of the anchor.
+     * <p>
+     * The results of positioning are placed in {@code outParams}.
+     *
+     * @param anchor the view on which the popup window must be anchored
+     * @param outParams the layout parameters used to display the drop down
+     * @param xOffset absolute horizontal offset from the left of the anchor
+     * @param yOffset absolute vertical offset from the top of the anchor
+     * @param gravity horizontal gravity specifying popup alignment
+     * @param allowScroll whether the anchor view's parent may be scrolled
+     *                    when the popup window doesn't fit on screen
+     * @return true if the popup is translated upwards to fit on screen
+     *
+     * @hide
+     */
+    protected final boolean findDropDownPosition(View anchor, WindowManager.LayoutParams outParams,
+            int xOffset, int yOffset, int width, int height, int gravity, boolean allowScroll) {
+        final int anchorHeight = anchor.getHeight();
+        final int anchorWidth = anchor.getWidth();
+        if (mOverlapAnchor) {
+            yOffset -= anchorHeight;
+        }
+
+        // Initially, align to the bottom-left corner of the anchor plus offsets.
+        final int[] appScreenLocation = mTmpAppLocation;
+        final View appRootView = getAppRootView(anchor);
+        appRootView.getLocationOnScreen(appScreenLocation);
+
+        final int[] screenLocation = mTmpScreenLocation;
+        anchor.getLocationOnScreen(screenLocation);
+
+        final int[] drawingLocation = mTmpDrawingLocation;
+        drawingLocation[0] = screenLocation[0] - appScreenLocation[0];
+        drawingLocation[1] = screenLocation[1] - appScreenLocation[1];
+        outParams.x = drawingLocation[0] + xOffset;
+        outParams.y = drawingLocation[1] + anchorHeight + yOffset;
+
+        final Rect displayFrame = new Rect();
+        appRootView.getWindowVisibleDisplayFrame(displayFrame);
+        if (width == MATCH_PARENT) {
+            width = displayFrame.right - displayFrame.left;
+        }
+        if (height == MATCH_PARENT) {
+            height = displayFrame.bottom - displayFrame.top;
+        }
+
+        // Let the window manager know to align the top to y.
+        outParams.gravity = computeGravity();
+        outParams.width = width;
+        outParams.height = height;
+
+        // If we need to adjust for gravity RIGHT, align to the bottom-right
+        // corner of the anchor (still accounting for offsets).
+        final int hgrav = Gravity.getAbsoluteGravity(gravity, anchor.getLayoutDirection())
+                & Gravity.HORIZONTAL_GRAVITY_MASK;
+        if (hgrav == Gravity.RIGHT) {
+            outParams.x -= width - anchorWidth;
+        }
+
+        // First, attempt to fit the popup vertically without resizing.
+        final boolean fitsVertical = tryFitVertical(outParams, yOffset, height,
+                anchorHeight, drawingLocation[1], screenLocation[1], displayFrame.top,
+                displayFrame.bottom, false);
+
+        // Next, attempt to fit the popup horizontally without resizing.
+        final boolean fitsHorizontal = tryFitHorizontal(outParams, xOffset, width,
+                anchorWidth, drawingLocation[0], screenLocation[0], displayFrame.left,
+                displayFrame.right, false);
+
+        // If the popup still doesn't fit, attempt to scroll the parent.
+        if (!fitsVertical || !fitsHorizontal) {
+            final int scrollX = anchor.getScrollX();
+            final int scrollY = anchor.getScrollY();
+            final Rect r = new Rect(scrollX, scrollY, scrollX + width + xOffset,
+                    scrollY + height + anchorHeight + yOffset);
+            if (allowScroll && anchor.requestRectangleOnScreen(r, true)) {
+                // Reset for the new anchor position.
+                anchor.getLocationOnScreen(screenLocation);
+                drawingLocation[0] = screenLocation[0] - appScreenLocation[0];
+                drawingLocation[1] = screenLocation[1] - appScreenLocation[1];
+                outParams.x = drawingLocation[0] + xOffset;
+                outParams.y = drawingLocation[1] + anchorHeight + yOffset;
+
+                // Preserve the gravity adjustment.
+                if (hgrav == Gravity.RIGHT) {
+                    outParams.x -= width - anchorWidth;
+                }
+            }
+
+            // Try to fit the popup again and allowing resizing.
+            tryFitVertical(outParams, yOffset, height, anchorHeight, drawingLocation[1],
+                    screenLocation[1], displayFrame.top, displayFrame.bottom, mClipToScreen);
+            tryFitHorizontal(outParams, xOffset, width, anchorWidth, drawingLocation[0],
+                    screenLocation[0], displayFrame.left, displayFrame.right, mClipToScreen);
+        }
+
+        // Return whether the popup's top edge is above the anchor's top edge.
+        return outParams.y < drawingLocation[1];
+    }
+
+    private boolean tryFitVertical(@NonNull LayoutParams outParams, int yOffset, int height,
+            int anchorHeight, int drawingLocationY, int screenLocationY, int displayFrameTop,
+            int displayFrameBottom, boolean allowResize) {
+        final int winOffsetY = screenLocationY - drawingLocationY;
+        final int anchorTopInScreen = outParams.y + winOffsetY;
+        final int spaceBelow = displayFrameBottom - anchorTopInScreen;
+        if (anchorTopInScreen >= 0 && height <= spaceBelow) {
+            return true;
+        }
+
+        final int spaceAbove = anchorTopInScreen - anchorHeight - displayFrameTop;
+        if (height <= spaceAbove) {
+            // Move everything up.
+            if (mOverlapAnchor) {
+                yOffset += anchorHeight;
+            }
+            outParams.y = drawingLocationY - height + yOffset;
+
+            return true;
+        }
+
+        if (positionInDisplayVertical(outParams, height, drawingLocationY, screenLocationY,
+                displayFrameTop, displayFrameBottom, allowResize)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean positionInDisplayVertical(@NonNull LayoutParams outParams, int height,
+            int drawingLocationY, int screenLocationY, int displayFrameTop, int displayFrameBottom,
+            boolean canResize) {
+        boolean fitsInDisplay = true;
+
+        final int winOffsetY = screenLocationY - drawingLocationY;
+        outParams.y += winOffsetY;
+        outParams.height = height;
+
+        final int bottom = outParams.y + height;
+        if (bottom > displayFrameBottom) {
+            // The popup is too far down, move it back in.
+            outParams.y -= bottom - displayFrameBottom;
+        }
+
+        if (outParams.y < displayFrameTop) {
+            // The popup is too far up, move it back in and clip if
+            // it's still too large.
+            outParams.y = displayFrameTop;
+
+            final int displayFrameHeight = displayFrameBottom - displayFrameTop;
+            if (canResize && height > displayFrameHeight) {
+                outParams.height = displayFrameHeight;
+            } else {
+                fitsInDisplay = false;
+            }
+        }
+
+        outParams.y -= winOffsetY;
+
+        return fitsInDisplay;
+    }
+
+    private boolean tryFitHorizontal(@NonNull LayoutParams outParams, int xOffset, int width,
+            int anchorWidth, int drawingLocationX, int screenLocationX, int displayFrameLeft,
+            int displayFrameRight, boolean allowResize) {
+        final int winOffsetX = screenLocationX - drawingLocationX;
+        final int anchorLeftInScreen = outParams.x + winOffsetX;
+        final int spaceRight = displayFrameRight - anchorLeftInScreen;
+        if (anchorLeftInScreen >= 0 && width <= spaceRight) {
+            return true;
+        }
+
+        if (positionInDisplayHorizontal(outParams, width, drawingLocationX, screenLocationX,
+                displayFrameLeft, displayFrameRight, allowResize)) {
+            return true;
+        }
+
+        return false;
+    }
+
+    private boolean positionInDisplayHorizontal(@NonNull LayoutParams outParams, int width,
+            int drawingLocationX, int screenLocationX, int displayFrameLeft, int displayFrameRight,
+            boolean canResize) {
+        boolean fitsInDisplay = true;
+
+        // Use screen coordinates for comparison against display frame.
+        final int winOffsetX = screenLocationX - drawingLocationX;
+        outParams.x += winOffsetX;
+
+        final int right = outParams.x + width;
+        if (right > displayFrameRight) {
+            // The popup is too far right, move it back in.
+            outParams.x -= right - displayFrameRight;
+        }
+
+        if (outParams.x < displayFrameLeft) {
+            // The popup is too far left, move it back in and clip if it's
+            // still too large.
+            outParams.x = displayFrameLeft;
+
+            final int displayFrameWidth = displayFrameRight - displayFrameLeft;
+            if (canResize && width > displayFrameWidth) {
+                outParams.width = displayFrameWidth;
+            } else {
+                fitsInDisplay = false;
+            }
+        }
+
+        outParams.x -= winOffsetX;
+
+        return fitsInDisplay;
+    }
+
+    /**
+     * Returns the maximum height that is available for the popup to be
+     * completely shown. It is recommended that this height be the maximum for
+     * the popup's height, otherwise it is possible that the popup will be
+     * clipped.
+     *
+     * @param anchor The view on which the popup window must be anchored.
+     * @return The maximum available height for the popup to be completely
+     *         shown.
+     */
+    public int getMaxAvailableHeight(@NonNull View anchor) {
+        return getMaxAvailableHeight(anchor, 0);
+    }
+
+    /**
+     * Returns the maximum height that is available for the popup to be
+     * completely shown. It is recommended that this height be the maximum for
+     * the popup's height, otherwise it is possible that the popup will be
+     * clipped.
+     *
+     * @param anchor The view on which the popup window must be anchored.
+     * @param yOffset y offset from the view's bottom edge
+     * @return The maximum available height for the popup to be completely
+     *         shown.
+     */
+    public int getMaxAvailableHeight(@NonNull View anchor, int yOffset) {
+        return getMaxAvailableHeight(anchor, yOffset, false);
+    }
+
+    /**
+     * Returns the maximum height that is available for the popup to be
+     * completely shown, optionally ignoring any bottom decorations such as
+     * the input method. It is recommended that this height be the maximum for
+     * the popup's height, otherwise it is possible that the popup will be
+     * clipped.
+     *
+     * @param anchor The view on which the popup window must be anchored.
+     * @param yOffset y offset from the view's bottom edge
+     * @param ignoreBottomDecorations if true, the height returned will be
+     *        all the way to the bottom of the display, ignoring any
+     *        bottom decorations
+     * @return The maximum available height for the popup to be completely
+     *         shown.
+     */
+    public int getMaxAvailableHeight(
+            @NonNull View anchor, int yOffset, boolean ignoreBottomDecorations) {
+        Rect displayFrame = null;
+        final Rect visibleDisplayFrame = new Rect();
+
+        final View appView = getAppRootView(anchor);
+        appView.getWindowVisibleDisplayFrame(visibleDisplayFrame);
+        if (ignoreBottomDecorations) {
+            // In the ignore bottom decorations case we want to
+            // still respect all other decorations so we use the inset visible
+            // frame on the top right and left and take the bottom
+            // value from the full frame.
+            displayFrame = new Rect();
+            anchor.getWindowDisplayFrame(displayFrame);
+            displayFrame.top = visibleDisplayFrame.top;
+            displayFrame.right = visibleDisplayFrame.right;
+            displayFrame.left = visibleDisplayFrame.left;
+        } else {
+            displayFrame = visibleDisplayFrame;
+        }
+
+        final int[] anchorPos = mTmpDrawingLocation;
+        anchor.getLocationOnScreen(anchorPos);
+
+        final int bottomEdge = displayFrame.bottom;
+
+        final int distanceToBottom;
+        if (mOverlapAnchor) {
+            distanceToBottom = bottomEdge - anchorPos[1] - yOffset;
+        } else {
+            distanceToBottom = bottomEdge - (anchorPos[1] + anchor.getHeight()) - yOffset;
+        }
+        final int distanceToTop = anchorPos[1] - displayFrame.top + yOffset;
+
+        // anchorPos[1] is distance from anchor to top of screen
+        int returnedHeight = Math.max(distanceToBottom, distanceToTop);
+        if (mBackground != null) {
+            mBackground.getPadding(mTempRect);
+            returnedHeight -= mTempRect.top + mTempRect.bottom;
+        }
+
+        return returnedHeight;
+    }
+
+    /**
+     * Disposes of the popup window. This method can be invoked only after
+     * {@link #showAsDropDown(android.view.View)} has been executed. Failing
+     * that, calling this method will have no effect.
+     *
+     * @see #showAsDropDown(android.view.View)
+     */
+    public void dismiss() {
+        if (!isShowing() || isTransitioningToDismiss()) {
+            return;
+        }
+
+        final PopupDecorView decorView = mDecorView;
+        final View contentView = mContentView;
+
+        final ViewGroup contentHolder;
+        final ViewParent contentParent = contentView.getParent();
+        if (contentParent instanceof ViewGroup) {
+            contentHolder = ((ViewGroup) contentParent);
+        } else {
+            contentHolder = null;
+        }
+
+        // Ensure any ongoing or pending transitions are canceled.
+        decorView.cancelTransitions();
+
+        mIsShowing = false;
+        mIsTransitioningToDismiss = true;
+
+        // This method may be called as part of window detachment, in which
+        // case the anchor view (and its root) will still return true from
+        // isAttachedToWindow() during execution of this method; however, we
+        // can expect the OnAttachStateChangeListener to have been called prior
+        // to executing this method, so we can rely on that instead.
+        final Transition exitTransition = mExitTransition;
+        if (exitTransition != null && decorView.isLaidOut()
+                && (mIsAnchorRootAttached || mAnchorRoot == null)) {
+            // The decor view is non-interactive and non-IME-focusable during exit transitions.
+            final LayoutParams p = (LayoutParams) decorView.getLayoutParams();
+            p.flags |= LayoutParams.FLAG_NOT_TOUCHABLE;
+            p.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+            p.flags &= ~LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+            mWindowManager.updateViewLayout(decorView, p);
+
+            final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
+            final Rect epicenter = getTransitionEpicenter();
+
+            // Once we start dismissing the decor view, all state (including
+            // the anchor root) needs to be moved to the decor view since we
+            // may open another popup while it's busy exiting.
+            decorView.startExitTransition(exitTransition, anchorRoot, epicenter,
+                    new TransitionListenerAdapter() {
+                        @Override
+                        public void onTransitionEnd(Transition transition) {
+                            dismissImmediate(decorView, contentHolder, contentView);
+                        }
+                    });
+        } else {
+            dismissImmediate(decorView, contentHolder, contentView);
+        }
+
+        // Clears the anchor view.
+        detachFromAnchor();
+
+        if (mOnDismissListener != null) {
+            mOnDismissListener.onDismiss();
+        }
+    }
+
+    /**
+     * Returns the window-relative epicenter bounds to be used by enter and
+     * exit transitions.
+     * <p>
+     * <strong>Note:</strong> This is distinct from the rect passed to
+     * {@link #setEpicenterBounds(Rect)}, which is anchor-relative.
+     *
+     * @return the window-relative epicenter bounds to be used by enter and
+     *         exit transitions
+     *
+     * @hide
+     */
+    protected final Rect getTransitionEpicenter() {
+        final View anchor = mAnchor != null ? mAnchor.get() : null;
+        final View decor = mDecorView;
+        if (anchor == null || decor == null) {
+            return null;
+        }
+
+        final int[] anchorLocation = anchor.getLocationOnScreen();
+        final int[] popupLocation = mDecorView.getLocationOnScreen();
+
+        // Compute the position of the anchor relative to the popup.
+        final Rect bounds = new Rect(0, 0, anchor.getWidth(), anchor.getHeight());
+        bounds.offset(anchorLocation[0] - popupLocation[0], anchorLocation[1] - popupLocation[1]);
+
+        // Use anchor-relative epicenter, if specified.
+        if (mEpicenterBounds != null) {
+            final int offsetX = bounds.left;
+            final int offsetY = bounds.top;
+            bounds.set(mEpicenterBounds);
+            bounds.offset(offsetX, offsetY);
+        }
+
+        return bounds;
+    }
+
+    /**
+     * Removes the popup from the window manager and tears down the supporting
+     * view hierarchy, if necessary.
+     */
+    private void dismissImmediate(View decorView, ViewGroup contentHolder, View contentView) {
+        // If this method gets called and the decor view doesn't have a parent,
+        // then it was either never added or was already removed. That should
+        // never happen, but it's worth checking to avoid potential crashes.
+        if (decorView.getParent() != null) {
+            mWindowManager.removeViewImmediate(decorView);
+        }
+
+        if (contentHolder != null) {
+            contentHolder.removeView(contentView);
+        }
+
+        // This needs to stay until after all transitions have ended since we
+        // need the reference to cancel transitions in preparePopup().
+        mDecorView = null;
+        mBackgroundView = null;
+        mIsTransitioningToDismiss = false;
+    }
+
+    /**
+     * Sets the listener to be called when the window is dismissed.
+     *
+     * @param onDismissListener The listener.
+     */
+    public void setOnDismissListener(OnDismissListener onDismissListener) {
+        mOnDismissListener = onDismissListener;
+    }
+
+    /** @hide */
+    protected final OnDismissListener getOnDismissListener() {
+        return mOnDismissListener;
+    }
+
+    /**
+     * Updates the state of the popup window, if it is currently being displayed,
+     * from the currently set state.
+     * <p>
+     * This includes:
+     * <ul>
+     *     <li>{@link #setClippingEnabled(boolean)}</li>
+     *     <li>{@link #setFocusable(boolean)}</li>
+     *     <li>{@link #setIgnoreCheekPress()}</li>
+     *     <li>{@link #setInputMethodMode(int)}</li>
+     *     <li>{@link #setTouchable(boolean)}</li>
+     *     <li>{@link #setAnimationStyle(int)}</li>
+     * </ul>
+     */
+    public void update() {
+        if (!isShowing() || !hasContentView()) {
+            return;
+        }
+
+        final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+        boolean update = false;
+
+        final int newAnim = computeAnimationResource();
+        if (newAnim != p.windowAnimations) {
+            p.windowAnimations = newAnim;
+            update = true;
+        }
+
+        final int newFlags = computeFlags(p.flags);
+        if (newFlags != p.flags) {
+            p.flags = newFlags;
+            update = true;
+        }
+
+        final int newGravity = computeGravity();
+        if (newGravity != p.gravity) {
+            p.gravity = newGravity;
+            update = true;
+        }
+
+        if (update) {
+            update(mAnchor != null ? mAnchor.get() : null, p);
+        }
+    }
+
+    /** @hide */
+    protected void update(View anchor, WindowManager.LayoutParams params) {
+        setLayoutDirectionFromAnchor();
+        mWindowManager.updateViewLayout(mDecorView, params);
+    }
+
+    /**
+     * Updates the dimension of the popup window.
+     * <p>
+     * Calling this function also updates the window with the current popup
+     * state as described for {@link #update()}.
+     *
+     * @param width the new width in pixels, must be >= 0 or -1 to ignore
+     * @param height the new height in pixels, must be >= 0 or -1 to ignore
+     */
+    public void update(int width, int height) {
+        final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+        update(p.x, p.y, width, height, false);
+    }
+
+    /**
+     * Updates the position and the dimension of the popup window.
+     * <p>
+     * Width and height can be set to -1 to update location only. Calling this
+     * function also updates the window with the current popup state as
+     * described for {@link #update()}.
+     *
+     * @param x the new x location
+     * @param y the new y location
+     * @param width the new width in pixels, must be >= 0 or -1 to ignore
+     * @param height the new height in pixels, must be >= 0 or -1 to ignore
+     */
+    public void update(int x, int y, int width, int height) {
+        update(x, y, width, height, false);
+    }
+
+    /**
+     * Updates the position and the dimension of the popup window.
+     * <p>
+     * Width and height can be set to -1 to update location only. Calling this
+     * function also updates the window with the current popup state as
+     * described for {@link #update()}.
+     *
+     * @param x the new x location
+     * @param y the new y location
+     * @param width the new width in pixels, must be >= 0 or -1 to ignore
+     * @param height the new height in pixels, must be >= 0 or -1 to ignore
+     * @param force {@code true} to reposition the window even if the specified
+     *              position already seems to correspond to the LayoutParams,
+     *              {@code false} to only reposition if needed
+     */
+    public void update(int x, int y, int width, int height, boolean force) {
+        if (width >= 0) {
+            mLastWidth = width;
+            setWidth(width);
+        }
+
+        if (height >= 0) {
+            mLastHeight = height;
+            setHeight(height);
+        }
+
+        if (!isShowing() || !hasContentView()) {
+            return;
+        }
+
+        final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+        boolean update = force;
+
+        final int finalWidth = mWidthMode < 0 ? mWidthMode : mLastWidth;
+        if (width != -1 && p.width != finalWidth) {
+            p.width = mLastWidth = finalWidth;
+            update = true;
+        }
+
+        final int finalHeight = mHeightMode < 0 ? mHeightMode : mLastHeight;
+        if (height != -1 && p.height != finalHeight) {
+            p.height = mLastHeight = finalHeight;
+            update = true;
+        }
+
+        if (p.x != x) {
+            p.x = x;
+            update = true;
+        }
+
+        if (p.y != y) {
+            p.y = y;
+            update = true;
+        }
+
+        final int newAnim = computeAnimationResource();
+        if (newAnim != p.windowAnimations) {
+            p.windowAnimations = newAnim;
+            update = true;
+        }
+
+        final int newFlags = computeFlags(p.flags);
+        if (newFlags != p.flags) {
+            p.flags = newFlags;
+            update = true;
+        }
+
+        final int newGravity = computeGravity();
+        if (newGravity != p.gravity) {
+            p.gravity = newGravity;
+            update = true;
+        }
+
+        View anchor = null;
+        int newAccessibilityIdOfAnchor = -1;
+
+        if (mAnchor != null && mAnchor.get() != null) {
+            anchor = mAnchor.get();
+            newAccessibilityIdOfAnchor = anchor.getAccessibilityViewId();
+        }
+
+        if (newAccessibilityIdOfAnchor != p.accessibilityIdOfAnchor) {
+            p.accessibilityIdOfAnchor = newAccessibilityIdOfAnchor;
+            update = true;
+        }
+
+        if (update) {
+            update(anchor, p);
+        }
+    }
+
+    /** @hide */
+    protected boolean hasContentView() {
+        return mContentView != null;
+    }
+
+    /** @hide */
+    protected boolean hasDecorView() {
+        return mDecorView != null;
+    }
+
+    /** @hide */
+    protected WindowManager.LayoutParams getDecorViewLayoutParams() {
+        return (WindowManager.LayoutParams) mDecorView.getLayoutParams();
+    }
+
+    /**
+     * Updates the position and the dimension of the popup window.
+     * <p>
+     * Calling this function also updates the window with the current popup
+     * state as described for {@link #update()}.
+     *
+     * @param anchor the popup's anchor view
+     * @param width the new width in pixels, must be >= 0 or -1 to ignore
+     * @param height the new height in pixels, must be >= 0 or -1 to ignore
+     */
+    public void update(View anchor, int width, int height) {
+        update(anchor, false, 0, 0, width, height);
+    }
+
+    /**
+     * Updates the position and the dimension of the popup window.
+     * <p>
+     * Width and height can be set to -1 to update location only. Calling this
+     * function also updates the window with the current popup state as
+     * described for {@link #update()}.
+     * <p>
+     * If the view later scrolls to move {@code anchor} to a different
+     * location, the popup will be moved correspondingly.
+     *
+     * @param anchor the popup's anchor view
+     * @param xoff x offset from the view's left edge
+     * @param yoff y offset from the view's bottom edge
+     * @param width the new width in pixels, must be >= 0 or -1 to ignore
+     * @param height the new height in pixels, must be >= 0 or -1 to ignore
+     */
+    public void update(View anchor, int xoff, int yoff, int width, int height) {
+        update(anchor, true, xoff, yoff, width, height);
+    }
+
+    private void update(View anchor, boolean updateLocation, int xoff, int yoff,
+            int width, int height) {
+
+        if (!isShowing() || !hasContentView()) {
+            return;
+        }
+
+        final WeakReference<View> oldAnchor = mAnchor;
+        final int gravity = mAnchoredGravity;
+
+        final boolean needsUpdate = updateLocation && (mAnchorXoff != xoff || mAnchorYoff != yoff);
+        if (oldAnchor == null || oldAnchor.get() != anchor || (needsUpdate && !mIsDropdown)) {
+            attachToAnchor(anchor, xoff, yoff, gravity);
+        } else if (needsUpdate) {
+            // No need to register again if this is a DropDown, showAsDropDown already did.
+            mAnchorXoff = xoff;
+            mAnchorYoff = yoff;
+        }
+
+        final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+        final int oldGravity = p.gravity;
+        final int oldWidth = p.width;
+        final int oldHeight = p.height;
+        final int oldX = p.x;
+        final int oldY = p.y;
+
+        // If an explicit width/height has not specified, use the most recent
+        // explicitly specified value (either from setWidth/Height or update).
+        if (width < 0) {
+            width = mWidth;
+        }
+        if (height < 0) {
+            height = mHeight;
+        }
+
+        final boolean aboveAnchor = findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
+                width, height, gravity, mAllowScrollingAnchorParent);
+        updateAboveAnchor(aboveAnchor);
+
+        final boolean paramsChanged = oldGravity != p.gravity || oldX != p.x || oldY != p.y
+                || oldWidth != p.width || oldHeight != p.height;
+
+        // If width and mWidth were both < 0 then we have a MATCH_PARENT or
+        // WRAP_CONTENT case. findDropDownPosition will have resolved this to
+        // absolute values, but we don't want to update mWidth/mHeight to these
+        // absolute values.
+        final int newWidth = width < 0 ? width : p.width;
+        final int newHeight = height < 0 ? height : p.height;
+        update(p.x, p.y, newWidth, newHeight, paramsChanged);
+    }
+
+    /**
+     * Listener that is called when this popup window is dismissed.
+     */
+    public interface OnDismissListener {
+        /**
+         * Called when this popup window is dismissed.
+         */
+        public void onDismiss();
+    }
+
+    /** @hide */
+    protected final void detachFromAnchor() {
+        final View anchor = mAnchor != null ? mAnchor.get() : null;
+        if (anchor != null) {
+            final ViewTreeObserver vto = anchor.getViewTreeObserver();
+            vto.removeOnScrollChangedListener(mOnScrollChangedListener);
+            anchor.removeOnAttachStateChangeListener(mOnAnchorDetachedListener);
+        }
+
+        final View anchorRoot = mAnchorRoot != null ? mAnchorRoot.get() : null;
+        if (anchorRoot != null) {
+            anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+            anchorRoot.removeOnLayoutChangeListener(mOnLayoutChangeListener);
+        }
+
+        mAnchor = null;
+        mAnchorRoot = null;
+        mIsAnchorRootAttached = false;
+    }
+
+    /** @hide */
+    protected final void attachToAnchor(View anchor, int xoff, int yoff, int gravity) {
+        detachFromAnchor();
+
+        final ViewTreeObserver vto = anchor.getViewTreeObserver();
+        if (vto != null) {
+            vto.addOnScrollChangedListener(mOnScrollChangedListener);
+        }
+        anchor.addOnAttachStateChangeListener(mOnAnchorDetachedListener);
+
+        final View anchorRoot = anchor.getRootView();
+        anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+        anchorRoot.addOnLayoutChangeListener(mOnLayoutChangeListener);
+
+        mAnchor = new WeakReference<>(anchor);
+        mAnchorRoot = new WeakReference<>(anchorRoot);
+        mIsAnchorRootAttached = anchorRoot.isAttachedToWindow();
+        mParentRootView = mAnchorRoot;
+
+        mAnchorXoff = xoff;
+        mAnchorYoff = yoff;
+        mAnchoredGravity = gravity;
+    }
+
+    private void alignToAnchor() {
+        final View anchor = mAnchor != null ? mAnchor.get() : null;
+        if (anchor != null && anchor.isAttachedToWindow() && hasDecorView()) {
+            final WindowManager.LayoutParams p = getDecorViewLayoutParams();
+
+            updateAboveAnchor(findDropDownPosition(anchor, p, mAnchorXoff, mAnchorYoff,
+                    p.width, p.height, mAnchoredGravity, false));
+            update(p.x, p.y, -1, -1, true);
+        }
+    }
+
+    private View getAppRootView(View anchor) {
+        final View appWindowView = WindowManagerGlobal.getInstance().getWindowView(
+                anchor.getApplicationWindowToken());
+        if (appWindowView != null) {
+            return appWindowView;
+        }
+        return anchor.getRootView();
+    }
+
+    private class PopupDecorView extends FrameLayout {
+        /** Runnable used to clean up listeners after exit transition. */
+        private Runnable mCleanupAfterExit;
+
+        public PopupDecorView(Context context) {
+            super(context);
+        }
+
+        @Override
+        public boolean dispatchKeyEvent(KeyEvent event) {
+            if (event.getKeyCode() == KeyEvent.KEYCODE_BACK) {
+                if (getKeyDispatcherState() == null) {
+                    return super.dispatchKeyEvent(event);
+                }
+
+                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
+                    if (state != null) {
+                        state.startTracking(event, this);
+                    }
+                    return true;
+                } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                    final KeyEvent.DispatcherState state = getKeyDispatcherState();
+                    if (state != null && state.isTracking(event) && !event.isCanceled()) {
+                        dismiss();
+                        return true;
+                    }
+                }
+                return super.dispatchKeyEvent(event);
+            } else {
+                return super.dispatchKeyEvent(event);
+            }
+        }
+
+        @Override
+        public boolean dispatchTouchEvent(MotionEvent ev) {
+            if (mTouchInterceptor != null && mTouchInterceptor.onTouch(this, ev)) {
+                return true;
+            }
+            return super.dispatchTouchEvent(ev);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            final int x = (int) event.getX();
+            final int y = (int) event.getY();
+
+            if ((event.getAction() == MotionEvent.ACTION_DOWN)
+                    && ((x < 0) || (x >= getWidth()) || (y < 0) || (y >= getHeight()))) {
+                dismiss();
+                return true;
+            } else if (event.getAction() == MotionEvent.ACTION_OUTSIDE) {
+                dismiss();
+                return true;
+            } else {
+                return super.onTouchEvent(event);
+            }
+        }
+
+        /**
+         * Requests that an enter transition run after the next layout pass.
+         */
+        public void requestEnterTransition(Transition transition) {
+            final ViewTreeObserver observer = getViewTreeObserver();
+            if (observer != null && transition != null) {
+                final Transition enterTransition = transition.clone();
+
+                // Postpone the enter transition after the first layout pass.
+                observer.addOnGlobalLayoutListener(new OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        final ViewTreeObserver observer = getViewTreeObserver();
+                        if (observer != null) {
+                            observer.removeOnGlobalLayoutListener(this);
+                        }
+
+                        final Rect epicenter = getTransitionEpicenter();
+                        enterTransition.setEpicenterCallback(new EpicenterCallback() {
+                            @Override
+                            public Rect onGetEpicenter(Transition transition) {
+                                return epicenter;
+                            }
+                        });
+                        startEnterTransition(enterTransition);
+                    }
+                });
+            }
+        }
+
+        /**
+         * Starts the pending enter transition, if one is set.
+         */
+        private void startEnterTransition(Transition enterTransition) {
+            final int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                enterTransition.addTarget(child);
+                child.setVisibility(View.INVISIBLE);
+            }
+
+            TransitionManager.beginDelayedTransition(this, enterTransition);
+
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                child.setVisibility(View.VISIBLE);
+            }
+        }
+
+        /**
+         * Starts an exit transition immediately.
+         * <p>
+         * <strong>Note:</strong> The transition listener is guaranteed to have
+         * its {@code onTransitionEnd} method called even if the transition
+         * never starts.
+         */
+        public void startExitTransition(@NonNull Transition transition,
+                @Nullable final View anchorRoot, @Nullable final Rect epicenter,
+                @NonNull final TransitionListener listener) {
+            if (transition == null) {
+                return;
+            }
+
+            // The anchor view's window may go away while we're executing our
+            // transition, in which case we need to end the transition
+            // immediately and execute the listener to remove the popup.
+            if (anchorRoot != null) {
+                anchorRoot.addOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+            }
+
+            // The cleanup runnable MUST be called even if the transition is
+            // canceled before it starts (and thus can't call onTransitionEnd).
+            mCleanupAfterExit = () -> {
+                listener.onTransitionEnd(transition);
+
+                if (anchorRoot != null) {
+                    anchorRoot.removeOnAttachStateChangeListener(mOnAnchorRootDetachedListener);
+                }
+
+                // The listener was called. Our job here is done.
+                mCleanupAfterExit = null;
+            };
+
+            final Transition exitTransition = transition.clone();
+            exitTransition.addListener(new TransitionListenerAdapter() {
+                @Override
+                public void onTransitionEnd(Transition t) {
+                    t.removeListener(this);
+
+                    // This null check shouldn't be necessary, but it's easier
+                    // to check here than it is to test every possible case.
+                    if (mCleanupAfterExit != null) {
+                        mCleanupAfterExit.run();
+                    }
+                }
+            });
+            exitTransition.setEpicenterCallback(new EpicenterCallback() {
+                @Override
+                public Rect onGetEpicenter(Transition transition) {
+                    return epicenter;
+                }
+            });
+
+            final int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                exitTransition.addTarget(child);
+            }
+
+            TransitionManager.beginDelayedTransition(this, exitTransition);
+
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                child.setVisibility(View.INVISIBLE);
+            }
+        }
+
+        /**
+         * Cancels all pending or current transitions.
+         */
+        public void cancelTransitions() {
+            TransitionManager.endTransitions(this);
+
+            // If the cleanup runnable is still around, that means the
+            // transition never started. We should run it now to clean up.
+            if (mCleanupAfterExit != null) {
+                mCleanupAfterExit.run();
+            }
+        }
+
+        private final OnAttachStateChangeListener mOnAnchorRootDetachedListener =
+                new OnAttachStateChangeListener() {
+                    @Override
+                    public void onViewAttachedToWindow(View v) {}
+
+                    @Override
+                    public void onViewDetachedFromWindow(View v) {
+                        v.removeOnAttachStateChangeListener(this);
+
+                        TransitionManager.endTransitions(PopupDecorView.this);
+                    }
+                };
+
+        @Override
+        public void requestKeyboardShortcuts(List<KeyboardShortcutGroup> list, int deviceId) {
+            if (mParentRootView != null) {
+                View parentRoot = mParentRootView.get();
+                if (parentRoot != null) {
+                    parentRoot.requestKeyboardShortcuts(list, deviceId);
+                }
+            }
+        }
+    }
+
+    private class PopupBackgroundView extends FrameLayout {
+        public PopupBackgroundView(Context context) {
+            super(context);
+        }
+
+        @Override
+        protected int[] onCreateDrawableState(int extraSpace) {
+            if (mAboveAnchor) {
+                final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+                View.mergeDrawableStates(drawableState, ABOVE_ANCHOR_STATE_SET);
+                return drawableState;
+            } else {
+                return super.onCreateDrawableState(extraSpace);
+            }
+        }
+    }
+}
diff --git a/android/widget/ProgressBar.java b/android/widget/ProgressBar.java
new file mode 100644
index 0000000..ced66cd
--- /dev/null
+++ b/android/widget/ProgressBar.java
@@ -0,0 +1,2086 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.animation.ObjectAnimator;
+import android.annotation.InterpolatorRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Shader;
+import android.graphics.drawable.Animatable;
+import android.graphics.drawable.AnimationDrawable;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.ClipDrawable;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.graphics.drawable.StateListDrawable;
+import android.graphics.drawable.shapes.RoundRectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.util.Pools.SynchronizedPool;
+import android.view.Gravity;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AlphaAnimation;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+import android.view.animation.DecelerateInterpolator;
+import android.view.animation.Interpolator;
+import android.view.animation.LinearInterpolator;
+import android.view.animation.Transformation;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+
+/**
+ * <p>
+ * A user interface element that indicates the progress of an operation.
+ * Progress bar supports two modes to represent progress: determinate, and indeterminate. For
+ * a visual overview of the difference between determinate and indeterminate progress modes, see
+ * <a href="https://material.io/guidelines/components/progress-activity.html#progress-activity-types-of-indicators">
+ * Progress & activity</a>.
+ * Display progress bars to a user in a non-interruptive way.
+ * Show the progress bar in your app's user interface or in a notification
+ * instead of within a dialog.
+ * </p>
+ * <h3>Indeterminate Progress</h3>
+ * <p>
+ * Use indeterminate mode for the progress bar when you do not know how long an
+ * operation will take.
+ * Indeterminate mode is the default for progress bar and shows a cyclic animation without a
+ * specific amount of progress indicated.
+ * The following example shows an indeterminate progress bar:
+ * <pre>
+ * &lt;ProgressBar
+ *      android:id="@+id/indeterminateBar"
+ *      android:layout_width="wrap_content"
+ *      android:layout_height="wrap_content"
+ *      /&gt;
+ * </pre>
+ * </p>
+ * <h3>Determinate Progress</h3>
+ * <p>
+ * Use determinate mode for the progress bar when you want to show that a specific quantity of
+ * progress has occurred.
+ * For example, the percent remaining of a file being retrieved, the amount records in
+ * a batch written to database, or the percent remaining of an audio file that is playing.
+ * <p>
+ * <p>
+ * To indicate determinate progress, you set the style of the progress bar to
+ * {@link android.R.style#Widget_ProgressBar_Horizontal} and set the amount of progress.
+ * The following example shows a determinate progress bar that is 25% complete:
+ * <pre>
+ * &lt;ProgressBar
+ *      android:id="@+id/determinateBar"
+ *      style="@android:style/Widget.ProgressBar.Horizontal"
+ *      android:layout_width="wrap_content"
+ *      android:layout_height="wrap_content"
+ *      android:progress="25"/&gt;
+ * </pre>
+ * You can update the percentage of progress displayed by using the
+ * {@link #setProgress(int)} method, or by calling
+ * {@link #incrementProgressBy(int)} to increase the current progress completed
+ * by a specified amount.
+ * By default, the progress bar is full when the progress value reaches 100.
+ * You can adjust this default by setting the
+ * {@link android.R.styleable#ProgressBar_max android:max} attribute.
+ * </p>
+ * <p>Other progress bar styles provided by the system include:</p>
+ * <ul>
+ * <li>{@link android.R.style#Widget_ProgressBar_Horizontal Widget.ProgressBar.Horizontal}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small Widget.ProgressBar.Small}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large Widget.ProgressBar.Large}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Inverse Widget.ProgressBar.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Small_Inverse
+ * Widget.ProgressBar.Small.Inverse}</li>
+ * <li>{@link android.R.style#Widget_ProgressBar_Large_Inverse
+ * Widget.ProgressBar.Large.Inverse}</li>
+ * </ul>
+ * <p>The "inverse" styles provide an inverse color scheme for the spinner, which may be necessary
+ * if your application uses a light colored theme (a white background).</p>
+ *
+ * <p><strong>XML attributes</b></strong>
+ * <p>
+ * See {@link android.R.styleable#ProgressBar ProgressBar Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ *
+ * @attr ref android.R.styleable#ProgressBar_animationResolution
+ * @attr ref android.R.styleable#ProgressBar_indeterminate
+ * @attr ref android.R.styleable#ProgressBar_indeterminateBehavior
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDrawable
+ * @attr ref android.R.styleable#ProgressBar_indeterminateDuration
+ * @attr ref android.R.styleable#ProgressBar_indeterminateOnly
+ * @attr ref android.R.styleable#ProgressBar_interpolator
+ * @attr ref android.R.styleable#ProgressBar_min
+ * @attr ref android.R.styleable#ProgressBar_max
+ * @attr ref android.R.styleable#ProgressBar_maxHeight
+ * @attr ref android.R.styleable#ProgressBar_maxWidth
+ * @attr ref android.R.styleable#ProgressBar_minHeight
+ * @attr ref android.R.styleable#ProgressBar_minWidth
+ * @attr ref android.R.styleable#ProgressBar_mirrorForRtl
+ * @attr ref android.R.styleable#ProgressBar_progress
+ * @attr ref android.R.styleable#ProgressBar_progressDrawable
+ * @attr ref android.R.styleable#ProgressBar_secondaryProgress
+ */
+@RemoteView
+public class ProgressBar extends View {
+
+    private static final int MAX_LEVEL = 10000;
+    private static final int TIMEOUT_SEND_ACCESSIBILITY_EVENT = 200;
+
+    /** Interpolator used for smooth progress animations. */
+    private static final DecelerateInterpolator PROGRESS_ANIM_INTERPOLATOR =
+            new DecelerateInterpolator();
+
+    /** Duration of smooth progress animations. */
+    private static final int PROGRESS_ANIM_DURATION = 80;
+
+    int mMinWidth;
+    int mMaxWidth;
+    int mMinHeight;
+    int mMaxHeight;
+
+    private int mProgress;
+    private int mSecondaryProgress;
+    private int mMin;
+    private boolean mMinInitialized;
+    private int mMax;
+    private boolean mMaxInitialized;
+
+    private int mBehavior;
+    private int mDuration;
+    private boolean mIndeterminate;
+    private boolean mOnlyIndeterminate;
+    private Transformation mTransformation;
+    private AlphaAnimation mAnimation;
+    private boolean mHasAnimation;
+
+    private Drawable mIndeterminateDrawable;
+    private Drawable mProgressDrawable;
+    private Drawable mCurrentDrawable;
+    private ProgressTintInfo mProgressTintInfo;
+
+    int mSampleWidth = 0;
+    private boolean mNoInvalidate;
+    private Interpolator mInterpolator;
+    private RefreshProgressRunnable mRefreshProgressRunnable;
+    private long mUiThreadId;
+    private boolean mShouldStartAnimationDrawable;
+
+    private boolean mInDrawing;
+    private boolean mAttached;
+    private boolean mRefreshIsPosted;
+
+    /** Value used to track progress animation, in the range [0...1]. */
+    private float mVisualProgress;
+
+    boolean mMirrorForRtl = false;
+
+    private boolean mAggregatedIsVisible;
+
+    private final ArrayList<RefreshData> mRefreshData = new ArrayList<RefreshData>();
+
+    private AccessibilityEventSender mAccessibilityEventSender;
+
+    /**
+     * Create a new progress bar with range 0...100 and initial progress of 0.
+     * @param context the application environment
+     */
+    public ProgressBar(Context context) {
+        this(context, null);
+    }
+
+    public ProgressBar(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.progressBarStyle);
+    }
+
+    public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ProgressBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mUiThreadId = Thread.currentThread().getId();
+        initProgressBar();
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.ProgressBar, defStyleAttr, defStyleRes);
+
+        mNoInvalidate = true;
+
+        final Drawable progressDrawable = a.getDrawable(R.styleable.ProgressBar_progressDrawable);
+        if (progressDrawable != null) {
+            // Calling setProgressDrawable can set mMaxHeight, so make sure the
+            // corresponding XML attribute for mMaxHeight is read after calling
+            // this method.
+            if (needsTileify(progressDrawable)) {
+                setProgressDrawableTiled(progressDrawable);
+            } else {
+                setProgressDrawable(progressDrawable);
+            }
+        }
+
+
+        mDuration = a.getInt(R.styleable.ProgressBar_indeterminateDuration, mDuration);
+
+        mMinWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_minWidth, mMinWidth);
+        mMaxWidth = a.getDimensionPixelSize(R.styleable.ProgressBar_maxWidth, mMaxWidth);
+        mMinHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_minHeight, mMinHeight);
+        mMaxHeight = a.getDimensionPixelSize(R.styleable.ProgressBar_maxHeight, mMaxHeight);
+
+        mBehavior = a.getInt(R.styleable.ProgressBar_indeterminateBehavior, mBehavior);
+
+        final int resID = a.getResourceId(
+                com.android.internal.R.styleable.ProgressBar_interpolator,
+                android.R.anim.linear_interpolator); // default to linear interpolator
+        if (resID > 0) {
+            setInterpolator(context, resID);
+        }
+
+        setMin(a.getInt(R.styleable.ProgressBar_min, mMin));
+        setMax(a.getInt(R.styleable.ProgressBar_max, mMax));
+
+        setProgress(a.getInt(R.styleable.ProgressBar_progress, mProgress));
+
+        setSecondaryProgress(a.getInt(
+                R.styleable.ProgressBar_secondaryProgress, mSecondaryProgress));
+
+        final Drawable indeterminateDrawable = a.getDrawable(
+                R.styleable.ProgressBar_indeterminateDrawable);
+        if (indeterminateDrawable != null) {
+            if (needsTileify(indeterminateDrawable)) {
+                setIndeterminateDrawableTiled(indeterminateDrawable);
+            } else {
+                setIndeterminateDrawable(indeterminateDrawable);
+            }
+        }
+
+        mOnlyIndeterminate = a.getBoolean(
+                R.styleable.ProgressBar_indeterminateOnly, mOnlyIndeterminate);
+
+        mNoInvalidate = false;
+
+        setIndeterminate(mOnlyIndeterminate || a.getBoolean(
+                R.styleable.ProgressBar_indeterminate, mIndeterminate));
+
+        mMirrorForRtl = a.getBoolean(R.styleable.ProgressBar_mirrorForRtl, mMirrorForRtl);
+
+        if (a.hasValue(R.styleable.ProgressBar_progressTintMode)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mProgressTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.ProgressBar_progressTintMode, -1), null);
+            mProgressTintInfo.mHasProgressTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_progressTint)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mProgressTintList = a.getColorStateList(
+                    R.styleable.ProgressBar_progressTint);
+            mProgressTintInfo.mHasProgressTint = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_progressBackgroundTintMode)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mProgressBackgroundTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.ProgressBar_progressBackgroundTintMode, -1), null);
+            mProgressTintInfo.mHasProgressBackgroundTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_progressBackgroundTint)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mProgressBackgroundTintList = a.getColorStateList(
+                    R.styleable.ProgressBar_progressBackgroundTint);
+            mProgressTintInfo.mHasProgressBackgroundTint = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_secondaryProgressTintMode)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mSecondaryProgressTintMode = Drawable.parseTintMode(
+                    a.getInt(R.styleable.ProgressBar_secondaryProgressTintMode, -1), null);
+            mProgressTintInfo.mHasSecondaryProgressTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_secondaryProgressTint)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mSecondaryProgressTintList = a.getColorStateList(
+                    R.styleable.ProgressBar_secondaryProgressTint);
+            mProgressTintInfo.mHasSecondaryProgressTint = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_indeterminateTintMode)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mIndeterminateTintMode = Drawable.parseTintMode(a.getInt(
+                    R.styleable.ProgressBar_indeterminateTintMode, -1), null);
+            mProgressTintInfo.mHasIndeterminateTintMode = true;
+        }
+
+        if (a.hasValue(R.styleable.ProgressBar_indeterminateTint)) {
+            if (mProgressTintInfo == null) {
+                mProgressTintInfo = new ProgressTintInfo();
+            }
+            mProgressTintInfo.mIndeterminateTintList = a.getColorStateList(
+                    R.styleable.ProgressBar_indeterminateTint);
+            mProgressTintInfo.mHasIndeterminateTint = true;
+        }
+
+        a.recycle();
+
+        applyProgressTints();
+        applyIndeterminateTint();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (getImportantForAccessibility() == View.IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(View.IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    /**
+     * Returns {@code true} if the target drawable needs to be tileified.
+     *
+     * @param dr the drawable to check
+     * @return {@code true} if the target drawable needs to be tileified,
+     *         {@code false} otherwise
+     */
+    private static boolean needsTileify(Drawable dr) {
+        if (dr instanceof LayerDrawable) {
+            final LayerDrawable orig = (LayerDrawable) dr;
+            final int N = orig.getNumberOfLayers();
+            for (int i = 0; i < N; i++) {
+                if (needsTileify(orig.getDrawable(i))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        if (dr instanceof StateListDrawable) {
+            final StateListDrawable in = (StateListDrawable) dr;
+            final int N = in.getStateCount();
+            for (int i = 0; i < N; i++) {
+                if (needsTileify(in.getStateDrawable(i))) {
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        // If there's a bitmap that's not wrapped with a ClipDrawable or
+        // ScaleDrawable, we'll need to wrap it and apply tiling.
+        if (dr instanceof BitmapDrawable) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * Converts a drawable to a tiled version of itself. It will recursively
+     * traverse layer and state list drawables.
+     */
+    private Drawable tileify(Drawable drawable, boolean clip) {
+        // TODO: This is a terrible idea that potentially destroys any drawable
+        // that extends any of these classes. We *really* need to remove this.
+
+        if (drawable instanceof LayerDrawable) {
+            final LayerDrawable orig = (LayerDrawable) drawable;
+            final int N = orig.getNumberOfLayers();
+            final Drawable[] outDrawables = new Drawable[N];
+
+            for (int i = 0; i < N; i++) {
+                final int id = orig.getId(i);
+                outDrawables[i] = tileify(orig.getDrawable(i),
+                        (id == R.id.progress || id == R.id.secondaryProgress));
+            }
+
+            final LayerDrawable clone = new LayerDrawable(outDrawables);
+            for (int i = 0; i < N; i++) {
+                clone.setId(i, orig.getId(i));
+                clone.setLayerGravity(i, orig.getLayerGravity(i));
+                clone.setLayerWidth(i, orig.getLayerWidth(i));
+                clone.setLayerHeight(i, orig.getLayerHeight(i));
+                clone.setLayerInsetLeft(i, orig.getLayerInsetLeft(i));
+                clone.setLayerInsetRight(i, orig.getLayerInsetRight(i));
+                clone.setLayerInsetTop(i, orig.getLayerInsetTop(i));
+                clone.setLayerInsetBottom(i, orig.getLayerInsetBottom(i));
+                clone.setLayerInsetStart(i, orig.getLayerInsetStart(i));
+                clone.setLayerInsetEnd(i, orig.getLayerInsetEnd(i));
+            }
+
+            return clone;
+        }
+
+        if (drawable instanceof StateListDrawable) {
+            final StateListDrawable in = (StateListDrawable) drawable;
+            final StateListDrawable out = new StateListDrawable();
+            final int N = in.getStateCount();
+            for (int i = 0; i < N; i++) {
+                out.addState(in.getStateSet(i), tileify(in.getStateDrawable(i), clip));
+            }
+
+            return out;
+        }
+
+        if (drawable instanceof BitmapDrawable) {
+            final Drawable.ConstantState cs = drawable.getConstantState();
+            final BitmapDrawable clone = (BitmapDrawable) cs.newDrawable(getResources());
+            clone.setTileModeXY(Shader.TileMode.REPEAT, Shader.TileMode.CLAMP);
+
+            if (mSampleWidth <= 0) {
+                mSampleWidth = clone.getIntrinsicWidth();
+            }
+
+            if (clip) {
+                return new ClipDrawable(clone, Gravity.LEFT, ClipDrawable.HORIZONTAL);
+            } else {
+                return clone;
+            }
+        }
+
+        return drawable;
+    }
+
+    Shape getDrawableShape() {
+        final float[] roundedCorners = new float[] { 5, 5, 5, 5, 5, 5, 5, 5 };
+        return new RoundRectShape(roundedCorners, null, null);
+    }
+
+    /**
+     * Convert a AnimationDrawable for use as a barberpole animation.
+     * Each frame of the animation is wrapped in a ClipDrawable and
+     * given a tiling BitmapShader.
+     */
+    private Drawable tileifyIndeterminate(Drawable drawable) {
+        if (drawable instanceof AnimationDrawable) {
+            AnimationDrawable background = (AnimationDrawable) drawable;
+            final int N = background.getNumberOfFrames();
+            AnimationDrawable newBg = new AnimationDrawable();
+            newBg.setOneShot(background.isOneShot());
+
+            for (int i = 0; i < N; i++) {
+                Drawable frame = tileify(background.getFrame(i), true);
+                frame.setLevel(10000);
+                newBg.addFrame(frame, background.getDuration(i));
+            }
+            newBg.setLevel(10000);
+            drawable = newBg;
+        }
+        return drawable;
+    }
+
+    /**
+     * <p>
+     * Initialize the progress bar's default values:
+     * </p>
+     * <ul>
+     * <li>progress = 0</li>
+     * <li>max = 100</li>
+     * <li>animation duration = 4000 ms</li>
+     * <li>indeterminate = false</li>
+     * <li>behavior = repeat</li>
+     * </ul>
+     */
+    private void initProgressBar() {
+        mMin = 0;
+        mMax = 100;
+        mProgress = 0;
+        mSecondaryProgress = 0;
+        mIndeterminate = false;
+        mOnlyIndeterminate = false;
+        mDuration = 4000;
+        mBehavior = AlphaAnimation.RESTART;
+        mMinWidth = 24;
+        mMaxWidth = 48;
+        mMinHeight = 24;
+        mMaxHeight = 48;
+    }
+
+    /**
+     * <p>Indicate whether this progress bar is in indeterminate mode.</p>
+     *
+     * @return true if the progress bar is in indeterminate mode
+     */
+    @ViewDebug.ExportedProperty(category = "progress")
+    public synchronized boolean isIndeterminate() {
+        return mIndeterminate;
+    }
+
+    /**
+     * <p>Change the indeterminate mode for this progress bar. In indeterminate
+     * mode, the progress is ignored and the progress bar shows an infinite
+     * animation instead.</p>
+     *
+     * If this progress bar's style only supports indeterminate mode (such as the circular
+     * progress bars), then this will be ignored.
+     *
+     * @param indeterminate true to enable the indeterminate mode
+     */
+    @android.view.RemotableViewMethod
+    public synchronized void setIndeterminate(boolean indeterminate) {
+        if ((!mOnlyIndeterminate || !mIndeterminate) && indeterminate != mIndeterminate) {
+            mIndeterminate = indeterminate;
+
+            if (indeterminate) {
+                // swap between indeterminate and regular backgrounds
+                swapCurrentDrawable(mIndeterminateDrawable);
+                startAnimation();
+            } else {
+                swapCurrentDrawable(mProgressDrawable);
+                stopAnimation();
+            }
+        }
+    }
+
+    private void swapCurrentDrawable(Drawable newDrawable) {
+        final Drawable oldDrawable = mCurrentDrawable;
+        mCurrentDrawable = newDrawable;
+
+        if (oldDrawable != mCurrentDrawable) {
+            if (oldDrawable != null) {
+                oldDrawable.setVisible(false, false);
+            }
+            if (mCurrentDrawable != null) {
+                mCurrentDrawable.setVisible(getWindowVisibility() == VISIBLE && isShown(), false);
+            }
+        }
+    }
+
+    /**
+     * <p>Get the drawable used to draw the progress bar in
+     * indeterminate mode.</p>
+     *
+     * @return a {@link android.graphics.drawable.Drawable} instance
+     *
+     * @see #setIndeterminateDrawable(android.graphics.drawable.Drawable)
+     * @see #setIndeterminate(boolean)
+     */
+    public Drawable getIndeterminateDrawable() {
+        return mIndeterminateDrawable;
+    }
+
+    /**
+     * Define the drawable used to draw the progress bar in indeterminate mode.
+     *
+     * @param d the new drawable
+     * @see #getIndeterminateDrawable()
+     * @see #setIndeterminate(boolean)
+     */
+    public void setIndeterminateDrawable(Drawable d) {
+        if (mIndeterminateDrawable != d) {
+            if (mIndeterminateDrawable != null) {
+                mIndeterminateDrawable.setCallback(null);
+                unscheduleDrawable(mIndeterminateDrawable);
+            }
+
+            mIndeterminateDrawable = d;
+
+            if (d != null) {
+                d.setCallback(this);
+                d.setLayoutDirection(getLayoutDirection());
+                if (d.isStateful()) {
+                    d.setState(getDrawableState());
+                }
+                applyIndeterminateTint();
+            }
+
+            if (mIndeterminate) {
+                swapCurrentDrawable(d);
+                postInvalidate();
+            }
+        }
+    }
+
+    /**
+     * Applies a tint to the indeterminate drawable. Does not modify the
+     * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setIndeterminateDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and
+     * tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#ProgressBar_indeterminateTint
+     * @see #getIndeterminateTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    @RemotableViewMethod
+    public void setIndeterminateTintList(@Nullable ColorStateList tint) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mIndeterminateTintList = tint;
+        mProgressTintInfo.mHasIndeterminateTint = true;
+
+        applyIndeterminateTint();
+    }
+
+    /**
+     * @return the tint applied to the indeterminate drawable
+     * @attr ref android.R.styleable#ProgressBar_indeterminateTint
+     * @see #setIndeterminateTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getIndeterminateTintList() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mIndeterminateTintList : null;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setIndeterminateTintList(ColorStateList)} to the indeterminate
+     * drawable. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#ProgressBar_indeterminateTintMode
+     * @see #setIndeterminateTintList(ColorStateList)
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setIndeterminateTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mIndeterminateTintMode = tintMode;
+        mProgressTintInfo.mHasIndeterminateTintMode = true;
+
+        applyIndeterminateTint();
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the indeterminate
+     * drawable, if specified.
+     *
+     * @return the blending mode used to apply the tint to the indeterminate
+     *         drawable
+     * @attr ref android.R.styleable#ProgressBar_indeterminateTintMode
+     * @see #setIndeterminateTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getIndeterminateTintMode() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mIndeterminateTintMode : null;
+    }
+
+    private void applyIndeterminateTint() {
+        if (mIndeterminateDrawable != null && mProgressTintInfo != null) {
+            final ProgressTintInfo tintInfo = mProgressTintInfo;
+            if (tintInfo.mHasIndeterminateTint || tintInfo.mHasIndeterminateTintMode) {
+                mIndeterminateDrawable = mIndeterminateDrawable.mutate();
+
+                if (tintInfo.mHasIndeterminateTint) {
+                    mIndeterminateDrawable.setTintList(tintInfo.mIndeterminateTintList);
+                }
+
+                if (tintInfo.mHasIndeterminateTintMode) {
+                    mIndeterminateDrawable.setTintMode(tintInfo.mIndeterminateTintMode);
+                }
+
+                // The drawable (or one of its children) may not have been
+                // stateful before applying the tint, so let's try again.
+                if (mIndeterminateDrawable.isStateful()) {
+                    mIndeterminateDrawable.setState(getDrawableState());
+                }
+            }
+        }
+    }
+
+    /**
+     * Define the tileable drawable used to draw the progress bar in
+     * indeterminate mode.
+     * <p>
+     * If the drawable is a BitmapDrawable or contains BitmapDrawables, a
+     * tiled copy will be generated for display as a progress bar.
+     *
+     * @param d the new drawable
+     * @see #getIndeterminateDrawable()
+     * @see #setIndeterminate(boolean)
+     */
+    public void setIndeterminateDrawableTiled(Drawable d) {
+        if (d != null) {
+            d = tileifyIndeterminate(d);
+        }
+
+        setIndeterminateDrawable(d);
+    }
+
+    /**
+     * <p>Get the drawable used to draw the progress bar in
+     * progress mode.</p>
+     *
+     * @return a {@link android.graphics.drawable.Drawable} instance
+     *
+     * @see #setProgressDrawable(android.graphics.drawable.Drawable)
+     * @see #setIndeterminate(boolean)
+     */
+    public Drawable getProgressDrawable() {
+        return mProgressDrawable;
+    }
+
+    /**
+     * Define the drawable used to draw the progress bar in progress mode.
+     *
+     * @param d the new drawable
+     * @see #getProgressDrawable()
+     * @see #setIndeterminate(boolean)
+     */
+    public void setProgressDrawable(Drawable d) {
+        if (mProgressDrawable != d) {
+            if (mProgressDrawable != null) {
+                mProgressDrawable.setCallback(null);
+                unscheduleDrawable(mProgressDrawable);
+            }
+
+            mProgressDrawable = d;
+
+            if (d != null) {
+                d.setCallback(this);
+                d.setLayoutDirection(getLayoutDirection());
+                if (d.isStateful()) {
+                    d.setState(getDrawableState());
+                }
+
+                // Make sure the ProgressBar is always tall enough
+                int drawableHeight = d.getMinimumHeight();
+                if (mMaxHeight < drawableHeight) {
+                    mMaxHeight = drawableHeight;
+                    requestLayout();
+                }
+
+                applyProgressTints();
+            }
+
+            if (!mIndeterminate) {
+                swapCurrentDrawable(d);
+                postInvalidate();
+            }
+
+            updateDrawableBounds(getWidth(), getHeight());
+            updateDrawableState();
+
+            doRefreshProgress(R.id.progress, mProgress, false, false, false);
+            doRefreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false, false);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public boolean getMirrorForRtl() {
+        return mMirrorForRtl;
+    }
+
+    /**
+     * Applies the progress tints in order of increasing specificity.
+     */
+    private void applyProgressTints() {
+        if (mProgressDrawable != null && mProgressTintInfo != null) {
+            applyPrimaryProgressTint();
+            applyProgressBackgroundTint();
+            applySecondaryProgressTint();
+        }
+    }
+
+    /**
+     * Should only be called if we've already verified that mProgressDrawable
+     * and mProgressTintInfo are non-null.
+     */
+    private void applyPrimaryProgressTint() {
+        if (mProgressTintInfo.mHasProgressTint
+                || mProgressTintInfo.mHasProgressTintMode) {
+            final Drawable target = getTintTarget(R.id.progress, true);
+            if (target != null) {
+                if (mProgressTintInfo.mHasProgressTint) {
+                    target.setTintList(mProgressTintInfo.mProgressTintList);
+                }
+                if (mProgressTintInfo.mHasProgressTintMode) {
+                    target.setTintMode(mProgressTintInfo.mProgressTintMode);
+                }
+
+                // The drawable (or one of its children) may not have been
+                // stateful before applying the tint, so let's try again.
+                if (target.isStateful()) {
+                    target.setState(getDrawableState());
+                }
+            }
+        }
+    }
+
+    /**
+     * Should only be called if we've already verified that mProgressDrawable
+     * and mProgressTintInfo are non-null.
+     */
+    private void applyProgressBackgroundTint() {
+        if (mProgressTintInfo.mHasProgressBackgroundTint
+                || mProgressTintInfo.mHasProgressBackgroundTintMode) {
+            final Drawable target = getTintTarget(R.id.background, false);
+            if (target != null) {
+                if (mProgressTintInfo.mHasProgressBackgroundTint) {
+                    target.setTintList(mProgressTintInfo.mProgressBackgroundTintList);
+                }
+                if (mProgressTintInfo.mHasProgressBackgroundTintMode) {
+                    target.setTintMode(mProgressTintInfo.mProgressBackgroundTintMode);
+                }
+
+                // The drawable (or one of its children) may not have been
+                // stateful before applying the tint, so let's try again.
+                if (target.isStateful()) {
+                    target.setState(getDrawableState());
+                }
+            }
+        }
+    }
+
+    /**
+     * Should only be called if we've already verified that mProgressDrawable
+     * and mProgressTintInfo are non-null.
+     */
+    private void applySecondaryProgressTint() {
+        if (mProgressTintInfo.mHasSecondaryProgressTint
+                || mProgressTintInfo.mHasSecondaryProgressTintMode) {
+            final Drawable target = getTintTarget(R.id.secondaryProgress, false);
+            if (target != null) {
+                if (mProgressTintInfo.mHasSecondaryProgressTint) {
+                    target.setTintList(mProgressTintInfo.mSecondaryProgressTintList);
+                }
+                if (mProgressTintInfo.mHasSecondaryProgressTintMode) {
+                    target.setTintMode(mProgressTintInfo.mSecondaryProgressTintMode);
+                }
+
+                // The drawable (or one of its children) may not have been
+                // stateful before applying the tint, so let's try again.
+                if (target.isStateful()) {
+                    target.setState(getDrawableState());
+                }
+            }
+        }
+    }
+
+    /**
+     * Applies a tint to the progress indicator, if one exists, or to the
+     * entire progress drawable otherwise. Does not modify the current tint
+     * mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * The progress indicator should be specified as a layer with
+     * id {@link android.R.id#progress} in a {@link LayerDrawable}
+     * used as the progress drawable.
+     * <p>
+     * Subsequent calls to {@link #setProgressDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and
+     * tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#ProgressBar_progressTint
+     * @see #getProgressTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    @RemotableViewMethod
+    public void setProgressTintList(@Nullable ColorStateList tint) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mProgressTintList = tint;
+        mProgressTintInfo.mHasProgressTint = true;
+
+        if (mProgressDrawable != null) {
+            applyPrimaryProgressTint();
+        }
+    }
+
+    /**
+     * Returns the tint applied to the progress drawable, if specified.
+     *
+     * @return the tint applied to the progress drawable
+     * @attr ref android.R.styleable#ProgressBar_progressTint
+     * @see #setProgressTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getProgressTintList() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintList : null;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setProgressTintList(ColorStateList)}} to the progress
+     * indicator. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#ProgressBar_progressTintMode
+     * @see #getProgressTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mProgressTintMode = tintMode;
+        mProgressTintInfo.mHasProgressTintMode = true;
+
+        if (mProgressDrawable != null) {
+            applyPrimaryProgressTint();
+        }
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the progress
+     * drawable, if specified.
+     *
+     * @return the blending mode used to apply the tint to the progress
+     *         drawable
+     * @attr ref android.R.styleable#ProgressBar_progressTintMode
+     * @see #setProgressTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getProgressTintMode() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mProgressTintMode : null;
+    }
+
+    /**
+     * Applies a tint to the progress background, if one exists. Does not
+     * modify the current tint mode, which is
+     * {@link PorterDuff.Mode#SRC_ATOP} by default.
+     * <p>
+     * The progress background must be specified as a layer with
+     * id {@link android.R.id#background} in a {@link LayerDrawable}
+     * used as the progress drawable.
+     * <p>
+     * Subsequent calls to {@link #setProgressDrawable(Drawable)} where the
+     * drawable contains a progress background will automatically mutate the
+     * drawable and apply the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#ProgressBar_progressBackgroundTint
+     * @see #getProgressBackgroundTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    @RemotableViewMethod
+    public void setProgressBackgroundTintList(@Nullable ColorStateList tint) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mProgressBackgroundTintList = tint;
+        mProgressTintInfo.mHasProgressBackgroundTint = true;
+
+        if (mProgressDrawable != null) {
+            applyProgressBackgroundTint();
+        }
+    }
+
+    /**
+     * Returns the tint applied to the progress background, if specified.
+     *
+     * @return the tint applied to the progress background
+     * @attr ref android.R.styleable#ProgressBar_progressBackgroundTint
+     * @see #setProgressBackgroundTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getProgressBackgroundTintList() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintList : null;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setProgressBackgroundTintList(ColorStateList)}} to the progress
+     * background. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#ProgressBar_progressBackgroundTintMode
+     * @see #setProgressBackgroundTintList(ColorStateList)
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setProgressBackgroundTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mProgressBackgroundTintMode = tintMode;
+        mProgressTintInfo.mHasProgressBackgroundTintMode = true;
+
+        if (mProgressDrawable != null) {
+            applyProgressBackgroundTint();
+        }
+    }
+
+    /**
+     * @return the blending mode used to apply the tint to the progress
+     *         background
+     * @attr ref android.R.styleable#ProgressBar_progressBackgroundTintMode
+     * @see #setProgressBackgroundTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getProgressBackgroundTintMode() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mProgressBackgroundTintMode : null;
+    }
+
+    /**
+     * Applies a tint to the secondary progress indicator, if one exists.
+     * Does not modify the current tint mode, which is
+     * {@link PorterDuff.Mode#SRC_ATOP} by default.
+     * <p>
+     * The secondary progress indicator must be specified as a layer with
+     * id {@link android.R.id#secondaryProgress} in a {@link LayerDrawable}
+     * used as the progress drawable.
+     * <p>
+     * Subsequent calls to {@link #setProgressDrawable(Drawable)} where the
+     * drawable contains a secondary progress indicator will automatically
+     * mutate the drawable and apply the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#ProgressBar_secondaryProgressTint
+     * @see #getSecondaryProgressTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setSecondaryProgressTintList(@Nullable ColorStateList tint) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mSecondaryProgressTintList = tint;
+        mProgressTintInfo.mHasSecondaryProgressTint = true;
+
+        if (mProgressDrawable != null) {
+            applySecondaryProgressTint();
+        }
+    }
+
+    /**
+     * Returns the tint applied to the secondary progress drawable, if
+     * specified.
+     *
+     * @return the tint applied to the secondary progress drawable
+     * @attr ref android.R.styleable#ProgressBar_secondaryProgressTint
+     * @see #setSecondaryProgressTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getSecondaryProgressTintList() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mSecondaryProgressTintList : null;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setSecondaryProgressTintList(ColorStateList)}} to the secondary
+     * progress indicator. The default mode is
+     * {@link PorterDuff.Mode#SRC_ATOP}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#ProgressBar_secondaryProgressTintMode
+     * @see #setSecondaryProgressTintList(ColorStateList)
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setSecondaryProgressTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mProgressTintInfo == null) {
+            mProgressTintInfo = new ProgressTintInfo();
+        }
+        mProgressTintInfo.mSecondaryProgressTintMode = tintMode;
+        mProgressTintInfo.mHasSecondaryProgressTintMode = true;
+
+        if (mProgressDrawable != null) {
+            applySecondaryProgressTint();
+        }
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the secondary
+     * progress drawable, if specified.
+     *
+     * @return the blending mode used to apply the tint to the secondary
+     *         progress drawable
+     * @attr ref android.R.styleable#ProgressBar_secondaryProgressTintMode
+     * @see #setSecondaryProgressTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getSecondaryProgressTintMode() {
+        return mProgressTintInfo != null ? mProgressTintInfo.mSecondaryProgressTintMode : null;
+    }
+
+    /**
+     * Returns the drawable to which a tint or tint mode should be applied.
+     *
+     * @param layerId id of the layer to modify
+     * @param shouldFallback whether the base drawable should be returned
+     *                       if the id does not exist
+     * @return the drawable to modify
+     */
+    @Nullable
+    private Drawable getTintTarget(int layerId, boolean shouldFallback) {
+        Drawable layer = null;
+
+        final Drawable d = mProgressDrawable;
+        if (d != null) {
+            mProgressDrawable = d.mutate();
+
+            if (d instanceof LayerDrawable) {
+                layer = ((LayerDrawable) d).findDrawableByLayerId(layerId);
+            }
+
+            if (shouldFallback && layer == null) {
+                layer = d;
+            }
+        }
+
+        return layer;
+    }
+
+    /**
+     * Define the tileable drawable used to draw the progress bar in
+     * progress mode.
+     * <p>
+     * If the drawable is a BitmapDrawable or contains BitmapDrawables, a
+     * tiled copy will be generated for display as a progress bar.
+     *
+     * @param d the new drawable
+     * @see #getProgressDrawable()
+     * @see #setIndeterminate(boolean)
+     */
+    public void setProgressDrawableTiled(Drawable d) {
+        if (d != null) {
+            d = tileify(d, false);
+        }
+
+        setProgressDrawable(d);
+    }
+
+    /**
+     * @return The drawable currently used to draw the progress bar
+     */
+    Drawable getCurrentDrawable() {
+        return mCurrentDrawable;
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return who == mProgressDrawable || who == mIndeterminateDrawable
+                || super.verifyDrawable(who);
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mProgressDrawable != null) mProgressDrawable.jumpToCurrentState();
+        if (mIndeterminateDrawable != null) mIndeterminateDrawable.jumpToCurrentState();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onResolveDrawables(int layoutDirection) {
+        final Drawable d = mCurrentDrawable;
+        if (d != null) {
+            d.setLayoutDirection(layoutDirection);
+        }
+        if (mIndeterminateDrawable != null) {
+            mIndeterminateDrawable.setLayoutDirection(layoutDirection);
+        }
+        if (mProgressDrawable != null) {
+            mProgressDrawable.setLayoutDirection(layoutDirection);
+        }
+    }
+
+    @Override
+    public void postInvalidate() {
+        if (!mNoInvalidate) {
+            super.postInvalidate();
+        }
+    }
+
+    private class RefreshProgressRunnable implements Runnable {
+        public void run() {
+            synchronized (ProgressBar.this) {
+                final int count = mRefreshData.size();
+                for (int i = 0; i < count; i++) {
+                    final RefreshData rd = mRefreshData.get(i);
+                    doRefreshProgress(rd.id, rd.progress, rd.fromUser, true, rd.animate);
+                    rd.recycle();
+                }
+                mRefreshData.clear();
+                mRefreshIsPosted = false;
+            }
+        }
+    }
+
+    private static class RefreshData {
+        private static final int POOL_MAX = 24;
+        private static final SynchronizedPool<RefreshData> sPool =
+                new SynchronizedPool<RefreshData>(POOL_MAX);
+
+        public int id;
+        public int progress;
+        public boolean fromUser;
+        public boolean animate;
+
+        public static RefreshData obtain(int id, int progress, boolean fromUser, boolean animate) {
+            RefreshData rd = sPool.acquire();
+            if (rd == null) {
+                rd = new RefreshData();
+            }
+            rd.id = id;
+            rd.progress = progress;
+            rd.fromUser = fromUser;
+            rd.animate = animate;
+            return rd;
+        }
+
+        public void recycle() {
+            sPool.release(this);
+        }
+    }
+
+    private synchronized void doRefreshProgress(int id, int progress, boolean fromUser,
+            boolean callBackToApp, boolean animate) {
+        int range = mMax - mMin;
+        final float scale = range > 0 ? (progress - mMin) / (float) range : 0;
+        final boolean isPrimary = id == R.id.progress;
+
+        if (isPrimary && animate) {
+            final ObjectAnimator animator = ObjectAnimator.ofFloat(this, VISUAL_PROGRESS, scale);
+            animator.setAutoCancel(true);
+            animator.setDuration(PROGRESS_ANIM_DURATION);
+            animator.setInterpolator(PROGRESS_ANIM_INTERPOLATOR);
+            animator.start();
+        } else {
+            setVisualProgress(id, scale);
+        }
+
+        if (isPrimary && callBackToApp) {
+            onProgressRefresh(scale, fromUser, progress);
+        }
+    }
+
+    void onProgressRefresh(float scale, boolean fromUser, int progress) {
+        if (AccessibilityManager.getInstance(mContext).isEnabled()) {
+            scheduleAccessibilityEventSender();
+        }
+    }
+
+    /**
+     * Sets the visual state of a progress indicator.
+     *
+     * @param id the identifier of the progress indicator
+     * @param progress the visual progress in the range [0...1]
+     */
+    private void setVisualProgress(int id, float progress) {
+        mVisualProgress = progress;
+
+        Drawable d = mCurrentDrawable;
+
+        if (d instanceof LayerDrawable) {
+            d = ((LayerDrawable) d).findDrawableByLayerId(id);
+            if (d == null) {
+                // If we can't find the requested layer, fall back to setting
+                // the level of the entire drawable. This will break if
+                // progress is set on multiple elements, but the theme-default
+                // drawable will always have all layer IDs present.
+                d = mCurrentDrawable;
+            }
+        }
+
+        if (d != null) {
+            final int level = (int) (progress * MAX_LEVEL);
+            d.setLevel(level);
+        } else {
+            invalidate();
+        }
+
+        onVisualProgressChanged(id, progress);
+    }
+
+    /**
+     * Called when the visual state of a progress indicator changes.
+     *
+     * @param id the identifier of the progress indicator
+     * @param progress the visual progress in the range [0...1]
+     */
+    void onVisualProgressChanged(int id, float progress) {
+        // Stub method.
+    }
+
+    private synchronized void refreshProgress(int id, int progress, boolean fromUser,
+            boolean animate) {
+        if (mUiThreadId == Thread.currentThread().getId()) {
+            doRefreshProgress(id, progress, fromUser, true, animate);
+        } else {
+            if (mRefreshProgressRunnable == null) {
+                mRefreshProgressRunnable = new RefreshProgressRunnable();
+            }
+
+            final RefreshData rd = RefreshData.obtain(id, progress, fromUser, animate);
+            mRefreshData.add(rd);
+            if (mAttached && !mRefreshIsPosted) {
+                post(mRefreshProgressRunnable);
+                mRefreshIsPosted = true;
+            }
+        }
+    }
+
+    /**
+     * Sets the current progress to the specified value. Does not do anything
+     * if the progress bar is in indeterminate mode.
+     * <p>
+     * This method will immediately update the visual position of the progress
+     * indicator. To animate the visual position to the target value, use
+     * {@link #setProgress(int, boolean)}}.
+     *
+     * @param progress the new progress, between 0 and {@link #getMax()}
+     *
+     * @see #setIndeterminate(boolean)
+     * @see #isIndeterminate()
+     * @see #getProgress()
+     * @see #incrementProgressBy(int)
+     */
+    @android.view.RemotableViewMethod
+    public synchronized void setProgress(int progress) {
+        setProgressInternal(progress, false, false);
+    }
+
+    /**
+     * Sets the current progress to the specified value, optionally animating
+     * the visual position between the current and target values.
+     * <p>
+     * Animation does not affect the result of {@link #getProgress()}, which
+     * will return the target value immediately after this method is called.
+     *
+     * @param progress the new progress value, between 0 and {@link #getMax()}
+     * @param animate {@code true} to animate between the current and target
+     *                values or {@code false} to not animate
+     */
+    public void setProgress(int progress, boolean animate) {
+        setProgressInternal(progress, false, animate);
+    }
+
+    @android.view.RemotableViewMethod
+    synchronized boolean setProgressInternal(int progress, boolean fromUser, boolean animate) {
+        if (mIndeterminate) {
+            // Not applicable.
+            return false;
+        }
+
+        progress = MathUtils.constrain(progress, mMin, mMax);
+
+        if (progress == mProgress) {
+            // No change from current.
+            return false;
+        }
+
+        mProgress = progress;
+        refreshProgress(R.id.progress, mProgress, fromUser, animate);
+        return true;
+    }
+
+    /**
+     * <p>
+     * Set the current secondary progress to the specified value. Does not do
+     * anything if the progress bar is in indeterminate mode.
+     * </p>
+     *
+     * @param secondaryProgress the new secondary progress, between 0 and {@link #getMax()}
+     * @see #setIndeterminate(boolean)
+     * @see #isIndeterminate()
+     * @see #getSecondaryProgress()
+     * @see #incrementSecondaryProgressBy(int)
+     */
+    @android.view.RemotableViewMethod
+    public synchronized void setSecondaryProgress(int secondaryProgress) {
+        if (mIndeterminate) {
+            return;
+        }
+
+        if (secondaryProgress < mMin) {
+            secondaryProgress = mMin;
+        }
+
+        if (secondaryProgress > mMax) {
+            secondaryProgress = mMax;
+        }
+
+        if (secondaryProgress != mSecondaryProgress) {
+            mSecondaryProgress = secondaryProgress;
+            refreshProgress(R.id.secondaryProgress, mSecondaryProgress, false, false);
+        }
+    }
+
+    /**
+     * <p>Get the progress bar's current level of progress. Return 0 when the
+     * progress bar is in indeterminate mode.</p>
+     *
+     * @return the current progress, between 0 and {@link #getMax()}
+     *
+     * @see #setIndeterminate(boolean)
+     * @see #isIndeterminate()
+     * @see #setProgress(int)
+     * @see #setMax(int)
+     * @see #getMax()
+     */
+    @ViewDebug.ExportedProperty(category = "progress")
+    public synchronized int getProgress() {
+        return mIndeterminate ? 0 : mProgress;
+    }
+
+    /**
+     * <p>Get the progress bar's current level of secondary progress. Return 0 when the
+     * progress bar is in indeterminate mode.</p>
+     *
+     * @return the current secondary progress, between 0 and {@link #getMax()}
+     *
+     * @see #setIndeterminate(boolean)
+     * @see #isIndeterminate()
+     * @see #setSecondaryProgress(int)
+     * @see #setMax(int)
+     * @see #getMax()
+     */
+    @ViewDebug.ExportedProperty(category = "progress")
+    public synchronized int getSecondaryProgress() {
+        return mIndeterminate ? 0 : mSecondaryProgress;
+    }
+
+    /**
+     * <p>Return the lower limit of this progress bar's range.</p>
+     *
+     * @return a positive integer
+     *
+     * @see #setMin(int)
+     * @see #getProgress()
+     * @see #getSecondaryProgress()
+     */
+    @ViewDebug.ExportedProperty(category = "progress")
+    public synchronized int getMin() {
+        return mMin;
+    }
+
+    /**
+     * <p>Return the upper limit of this progress bar's range.</p>
+     *
+     * @return a positive integer
+     *
+     * @see #setMax(int)
+     * @see #getProgress()
+     * @see #getSecondaryProgress()
+     */
+    @ViewDebug.ExportedProperty(category = "progress")
+    public synchronized int getMax() {
+        return mMax;
+    }
+
+    /**
+     * <p>Set the lower range of the progress bar to <tt>min</tt>.</p>
+     *
+     * @param min the lower range of this progress bar
+     *
+     * @see #getMin()
+     * @see #setProgress(int)
+     * @see #setSecondaryProgress(int)
+     */
+    @android.view.RemotableViewMethod
+    public synchronized void setMin(int min) {
+        if (mMaxInitialized) {
+            if (min > mMax) {
+                min = mMax;
+            }
+        }
+        mMinInitialized = true;
+        if (mMaxInitialized && min != mMin) {
+            mMin = min;
+            postInvalidate();
+
+            if (mProgress < min) {
+                mProgress = min;
+            }
+            refreshProgress(R.id.progress, mProgress, false, false);
+        } else {
+            mMin = min;
+        }
+    }
+
+    /**
+     * <p>Set the upper range of the progress bar <tt>max</tt>.</p>
+     *
+     * @param max the upper range of this progress bar
+     *
+     * @see #getMax()
+     * @see #setProgress(int)
+     * @see #setSecondaryProgress(int)
+     */
+    @android.view.RemotableViewMethod
+    public synchronized void setMax(int max) {
+        if (mMinInitialized) {
+            if (max < mMin) {
+                max = mMin;
+            }
+        }
+        mMaxInitialized = true;
+        if (mMinInitialized && max != mMax) {
+            mMax = max;
+            postInvalidate();
+
+            if (mProgress > max) {
+                mProgress = max;
+            }
+            refreshProgress(R.id.progress, mProgress, false, false);
+        } else {
+            mMax = max;
+        }
+    }
+
+    /**
+     * <p>Increase the progress bar's progress by the specified amount.</p>
+     *
+     * @param diff the amount by which the progress must be increased
+     *
+     * @see #setProgress(int)
+     */
+    public synchronized final void incrementProgressBy(int diff) {
+        setProgress(mProgress + diff);
+    }
+
+    /**
+     * <p>Increase the progress bar's secondary progress by the specified amount.</p>
+     *
+     * @param diff the amount by which the secondary progress must be increased
+     *
+     * @see #setSecondaryProgress(int)
+     */
+    public synchronized final void incrementSecondaryProgressBy(int diff) {
+        setSecondaryProgress(mSecondaryProgress + diff);
+    }
+
+    /**
+     * <p>Start the indeterminate progress animation.</p>
+     */
+    void startAnimation() {
+        if (getVisibility() != VISIBLE || getWindowVisibility() != VISIBLE) {
+            return;
+        }
+
+        if (mIndeterminateDrawable instanceof Animatable) {
+            mShouldStartAnimationDrawable = true;
+            mHasAnimation = false;
+        } else {
+            mHasAnimation = true;
+
+            if (mInterpolator == null) {
+                mInterpolator = new LinearInterpolator();
+            }
+
+            if (mTransformation == null) {
+                mTransformation = new Transformation();
+            } else {
+                mTransformation.clear();
+            }
+
+            if (mAnimation == null) {
+                mAnimation = new AlphaAnimation(0.0f, 1.0f);
+            } else {
+                mAnimation.reset();
+            }
+
+            mAnimation.setRepeatMode(mBehavior);
+            mAnimation.setRepeatCount(Animation.INFINITE);
+            mAnimation.setDuration(mDuration);
+            mAnimation.setInterpolator(mInterpolator);
+            mAnimation.setStartTime(Animation.START_ON_FIRST_FRAME);
+        }
+        postInvalidate();
+    }
+
+    /**
+     * <p>Stop the indeterminate progress animation.</p>
+     */
+    void stopAnimation() {
+        mHasAnimation = false;
+        if (mIndeterminateDrawable instanceof Animatable) {
+            ((Animatable) mIndeterminateDrawable).stop();
+            mShouldStartAnimationDrawable = false;
+        }
+        postInvalidate();
+    }
+
+    /**
+     * Sets the acceleration curve for the indeterminate animation.
+     * The interpolator is loaded as a resource from the specified context.
+     *
+     * @param context The application environment
+     * @param resID The resource identifier of the interpolator to load
+     */
+    public void setInterpolator(Context context, @InterpolatorRes int resID) {
+        setInterpolator(AnimationUtils.loadInterpolator(context, resID));
+    }
+
+    /**
+     * Sets the acceleration curve for the indeterminate animation.
+     * Defaults to a linear interpolation.
+     *
+     * @param interpolator The interpolator which defines the acceleration curve
+     */
+    public void setInterpolator(Interpolator interpolator) {
+        mInterpolator = interpolator;
+    }
+
+    /**
+     * Gets the acceleration curve type for the indeterminate animation.
+     *
+     * @return the {@link Interpolator} associated to this animation
+     */
+    public Interpolator getInterpolator() {
+        return mInterpolator;
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+
+        if (isVisible != mAggregatedIsVisible) {
+            mAggregatedIsVisible = isVisible;
+
+            if (mIndeterminate) {
+                // let's be nice with the UI thread
+                if (isVisible) {
+                    startAnimation();
+                } else {
+                    stopAnimation();
+                }
+            }
+
+            if (mCurrentDrawable != null) {
+                mCurrentDrawable.setVisible(isVisible, false);
+            }
+        }
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable dr) {
+        if (!mInDrawing) {
+            if (verifyDrawable(dr)) {
+                final Rect dirty = dr.getBounds();
+                final int scrollX = mScrollX + mPaddingLeft;
+                final int scrollY = mScrollY + mPaddingTop;
+
+                invalidate(dirty.left + scrollX, dirty.top + scrollY,
+                        dirty.right + scrollX, dirty.bottom + scrollY);
+            } else {
+                super.invalidateDrawable(dr);
+            }
+        }
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        updateDrawableBounds(w, h);
+    }
+
+    private void updateDrawableBounds(int w, int h) {
+        // onDraw will translate the canvas so we draw starting at 0,0.
+        // Subtract out padding for the purposes of the calculations below.
+        w -= mPaddingRight + mPaddingLeft;
+        h -= mPaddingTop + mPaddingBottom;
+
+        int right = w;
+        int bottom = h;
+        int top = 0;
+        int left = 0;
+
+        if (mIndeterminateDrawable != null) {
+            // Aspect ratio logic does not apply to AnimationDrawables
+            if (mOnlyIndeterminate && !(mIndeterminateDrawable instanceof AnimationDrawable)) {
+                // Maintain aspect ratio. Certain kinds of animated drawables
+                // get very confused otherwise.
+                final int intrinsicWidth = mIndeterminateDrawable.getIntrinsicWidth();
+                final int intrinsicHeight = mIndeterminateDrawable.getIntrinsicHeight();
+                final float intrinsicAspect = (float) intrinsicWidth / intrinsicHeight;
+                final float boundAspect = (float) w / h;
+                if (intrinsicAspect != boundAspect) {
+                    if (boundAspect > intrinsicAspect) {
+                        // New width is larger. Make it smaller to match height.
+                        final int width = (int) (h * intrinsicAspect);
+                        left = (w - width) / 2;
+                        right = left + width;
+                    } else {
+                        // New height is larger. Make it smaller to match width.
+                        final int height = (int) (w * (1 / intrinsicAspect));
+                        top = (h - height) / 2;
+                        bottom = top + height;
+                    }
+                }
+            }
+            if (isLayoutRtl() && mMirrorForRtl) {
+                int tempLeft = left;
+                left = w - right;
+                right = w - tempLeft;
+            }
+            mIndeterminateDrawable.setBounds(left, top, right, bottom);
+        }
+
+        if (mProgressDrawable != null) {
+            mProgressDrawable.setBounds(0, 0, right, bottom);
+        }
+    }
+
+    @Override
+    protected synchronized void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        drawTrack(canvas);
+    }
+
+    /**
+     * Draws the progress bar track.
+     */
+    void drawTrack(Canvas canvas) {
+        final Drawable d = mCurrentDrawable;
+        if (d != null) {
+            // Translate canvas so a indeterminate circular progress bar with padding
+            // rotates properly in its animation
+            final int saveCount = canvas.save();
+
+            if (isLayoutRtl() && mMirrorForRtl) {
+                canvas.translate(getWidth() - mPaddingRight, mPaddingTop);
+                canvas.scale(-1.0f, 1.0f);
+            } else {
+                canvas.translate(mPaddingLeft, mPaddingTop);
+            }
+
+            final long time = getDrawingTime();
+            if (mHasAnimation) {
+                mAnimation.getTransformation(time, mTransformation);
+                final float scale = mTransformation.getAlpha();
+                try {
+                    mInDrawing = true;
+                    d.setLevel((int) (scale * MAX_LEVEL));
+                } finally {
+                    mInDrawing = false;
+                }
+                postInvalidateOnAnimation();
+            }
+
+            d.draw(canvas);
+            canvas.restoreToCount(saveCount);
+
+            if (mShouldStartAnimationDrawable && d instanceof Animatable) {
+                ((Animatable) d).start();
+                mShouldStartAnimationDrawable = false;
+            }
+        }
+    }
+
+    @Override
+    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int dw = 0;
+        int dh = 0;
+
+        final Drawable d = mCurrentDrawable;
+        if (d != null) {
+            dw = Math.max(mMinWidth, Math.min(mMaxWidth, d.getIntrinsicWidth()));
+            dh = Math.max(mMinHeight, Math.min(mMaxHeight, d.getIntrinsicHeight()));
+        }
+
+        updateDrawableState();
+
+        dw += mPaddingLeft + mPaddingRight;
+        dh += mPaddingTop + mPaddingBottom;
+
+        final int measuredWidth = resolveSizeAndState(dw, widthMeasureSpec, 0);
+        final int measuredHeight = resolveSizeAndState(dh, heightMeasureSpec, 0);
+        setMeasuredDimension(measuredWidth, measuredHeight);
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        updateDrawableState();
+    }
+
+    private void updateDrawableState() {
+        final int[] state = getDrawableState();
+        boolean changed = false;
+
+        final Drawable progressDrawable = mProgressDrawable;
+        if (progressDrawable != null && progressDrawable.isStateful()) {
+            changed |= progressDrawable.setState(state);
+        }
+
+        final Drawable indeterminateDrawable = mIndeterminateDrawable;
+        if (indeterminateDrawable != null && indeterminateDrawable.isStateful()) {
+            changed |= indeterminateDrawable.setState(state);
+        }
+
+        if (changed) {
+            invalidate();
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mProgressDrawable != null) {
+            mProgressDrawable.setHotspot(x, y);
+        }
+
+        if (mIndeterminateDrawable != null) {
+            mIndeterminateDrawable.setHotspot(x, y);
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        int progress;
+        int secondaryProgress;
+
+        /**
+         * Constructor called from {@link ProgressBar#onSaveInstanceState()}
+         */
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        /**
+         * Constructor called from {@link #CREATOR}
+         */
+        private SavedState(Parcel in) {
+            super(in);
+            progress = in.readInt();
+            secondaryProgress = in.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(progress);
+            out.writeInt(secondaryProgress);
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        // Force our ancestor class to save its state
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+
+        ss.progress = mProgress;
+        ss.secondaryProgress = mSecondaryProgress;
+
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        setProgress(ss.progress);
+        setSecondaryProgress(ss.secondaryProgress);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+        if (mIndeterminate) {
+            startAnimation();
+        }
+        if (mRefreshData != null) {
+            synchronized (this) {
+                final int count = mRefreshData.size();
+                for (int i = 0; i < count; i++) {
+                    final RefreshData rd = mRefreshData.get(i);
+                    doRefreshProgress(rd.id, rd.progress, rd.fromUser, true, rd.animate);
+                    rd.recycle();
+                }
+                mRefreshData.clear();
+            }
+        }
+        mAttached = true;
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        if (mIndeterminate) {
+            stopAnimation();
+        }
+        if (mRefreshProgressRunnable != null) {
+            removeCallbacks(mRefreshProgressRunnable);
+            mRefreshIsPosted = false;
+        }
+        if (mAccessibilityEventSender != null) {
+            removeCallbacks(mAccessibilityEventSender);
+        }
+        // This should come after stopAnimation(), otherwise an invalidate message remains in the
+        // queue, which can prevent the entire view hierarchy from being GC'ed during a rotation
+        super.onDetachedFromWindow();
+        mAttached = false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ProgressBar.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setItemCount(mMax - mMin);
+        event.setCurrentItemIndex(mProgress);
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (!isIndeterminate()) {
+            AccessibilityNodeInfo.RangeInfo rangeInfo = AccessibilityNodeInfo.RangeInfo.obtain(
+                    AccessibilityNodeInfo.RangeInfo.RANGE_TYPE_INT, 0, getMax(), getProgress());
+            info.setRangeInfo(rangeInfo);
+        }
+    }
+
+    /**
+     * Schedule a command for sending an accessibility event.
+     * </br>
+     * Note: A command is used to ensure that accessibility events
+     *       are sent at most one in a given time frame to save
+     *       system resources while the progress changes quickly.
+     */
+    private void scheduleAccessibilityEventSender() {
+        if (mAccessibilityEventSender == null) {
+            mAccessibilityEventSender = new AccessibilityEventSender();
+        } else {
+            removeCallbacks(mAccessibilityEventSender);
+        }
+        postDelayed(mAccessibilityEventSender, TIMEOUT_SEND_ACCESSIBILITY_EVENT);
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+
+        stream.addProperty("progress:max", getMax());
+        stream.addProperty("progress:progress", getProgress());
+        stream.addProperty("progress:secondaryProgress", getSecondaryProgress());
+        stream.addProperty("progress:indeterminate", isIndeterminate());
+    }
+
+    /**
+     * Returns whether the ProgressBar is animating or not. This is essentially the same
+     * as whether the ProgressBar is {@link #isIndeterminate() indeterminate} and visible,
+     * as indeterminate ProgressBars are always animating, and non-indeterminate
+     * ProgressBars are not animating.
+     *
+     * @return true if the ProgressBar is animating, false otherwise.
+     */
+    public boolean isAnimating() {
+        return isIndeterminate() && getWindowVisibility() == VISIBLE && isShown();
+    }
+
+    /**
+     * Command for sending an accessibility event.
+     */
+    private class AccessibilityEventSender implements Runnable {
+        public void run() {
+            sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        }
+    }
+
+    private static class ProgressTintInfo {
+        ColorStateList mIndeterminateTintList;
+        PorterDuff.Mode mIndeterminateTintMode;
+        boolean mHasIndeterminateTint;
+        boolean mHasIndeterminateTintMode;
+
+        ColorStateList mProgressTintList;
+        PorterDuff.Mode mProgressTintMode;
+        boolean mHasProgressTint;
+        boolean mHasProgressTintMode;
+
+        ColorStateList mProgressBackgroundTintList;
+        PorterDuff.Mode mProgressBackgroundTintMode;
+        boolean mHasProgressBackgroundTint;
+        boolean mHasProgressBackgroundTintMode;
+
+        ColorStateList mSecondaryProgressTintList;
+        PorterDuff.Mode mSecondaryProgressTintMode;
+        boolean mHasSecondaryProgressTint;
+        boolean mHasSecondaryProgressTintMode;
+    }
+
+    /**
+     * Property wrapper around the visual state of the {@code progress} functionality
+     * handled by the {@link ProgressBar#setProgress(int, boolean)} method. This does
+     * not correspond directly to the actual progress -- only the visual state.
+     */
+    private final FloatProperty<ProgressBar> VISUAL_PROGRESS =
+            new FloatProperty<ProgressBar>("visual_progress") {
+                @Override
+                public void setValue(ProgressBar object, float value) {
+                    object.setVisualProgress(R.id.progress, value);
+                    object.mVisualProgress = value;
+                }
+
+                @Override
+                public Float get(ProgressBar object) {
+                    return object.mVisualProgress;
+                }
+            };
+}
diff --git a/android/widget/QuickContactBadge.java b/android/widget/QuickContactBadge.java
new file mode 100644
index 0000000..8f6b0d5
--- /dev/null
+++ b/android/widget/QuickContactBadge.java
@@ -0,0 +1,404 @@
+/*
+ * Copyright (C) 2009 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 android.widget;
+
+import android.content.AsyncQueryHandler;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Canvas;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.provider.ContactsContract.CommonDataKinds.Email;
+import android.provider.ContactsContract.Contacts;
+import android.provider.ContactsContract.Intents;
+import android.provider.ContactsContract.PhoneLookup;
+import android.provider.ContactsContract.QuickContact;
+import android.provider.ContactsContract.RawContacts;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.View.OnClickListener;
+
+import com.android.internal.R;
+
+/**
+ * Widget used to show an image with the standard QuickContact badge
+ * and on-click behavior.
+ */
+public class QuickContactBadge extends ImageView implements OnClickListener {
+    private Uri mContactUri;
+    private String mContactEmail;
+    private String mContactPhone;
+    private Drawable mOverlay;
+    private QueryHandler mQueryHandler;
+    private Drawable mDefaultAvatar;
+    private Bundle mExtras = null;
+    private String mPrioritizedMimeType;
+
+    protected String[] mExcludeMimes = null;
+
+    static final private int TOKEN_EMAIL_LOOKUP = 0;
+    static final private int TOKEN_PHONE_LOOKUP = 1;
+    static final private int TOKEN_EMAIL_LOOKUP_AND_TRIGGER = 2;
+    static final private int TOKEN_PHONE_LOOKUP_AND_TRIGGER = 3;
+
+    static final private String EXTRA_URI_CONTENT = "uri_content";
+
+    static final String[] EMAIL_LOOKUP_PROJECTION = new String[] {
+        RawContacts.CONTACT_ID,
+        Contacts.LOOKUP_KEY,
+    };
+    static final int EMAIL_ID_COLUMN_INDEX = 0;
+    static final int EMAIL_LOOKUP_STRING_COLUMN_INDEX = 1;
+
+    static final String[] PHONE_LOOKUP_PROJECTION = new String[] {
+        PhoneLookup._ID,
+        PhoneLookup.LOOKUP_KEY,
+    };
+    static final int PHONE_ID_COLUMN_INDEX = 0;
+    static final int PHONE_LOOKUP_STRING_COLUMN_INDEX = 1;
+
+    public QuickContactBadge(Context context) {
+        this(context, null);
+    }
+
+    public QuickContactBadge(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public QuickContactBadge(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public QuickContactBadge(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        TypedArray styledAttributes = mContext.obtainStyledAttributes(R.styleable.Theme);
+        mOverlay = styledAttributes.getDrawable(
+                com.android.internal.R.styleable.Theme_quickContactBadgeOverlay);
+        styledAttributes.recycle();
+
+        setOnClickListener(this);
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (!isInEditMode()) {
+            mQueryHandler = new QueryHandler(mContext.getContentResolver());
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final Drawable overlay = mOverlay;
+        if (overlay != null && overlay.isStateful()
+                && overlay.setState(getDrawableState())) {
+            invalidateDrawable(overlay);
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mOverlay != null) {
+            mOverlay.setHotspot(x, y);
+        }
+    }
+
+    /** This call has no effect anymore, as there is only one QuickContact mode */
+    @SuppressWarnings("unused")
+    public void setMode(int size) {
+    }
+
+    /**
+     * Set which mimetype should be prioritized in the QuickContacts UI. For example, passing the
+     * value {@link Email#CONTENT_ITEM_TYPE} can cause emails to be displayed more prominently in
+     * QuickContacts.
+     */
+    public void setPrioritizedMimeType(String prioritizedMimeType) {
+        mPrioritizedMimeType = prioritizedMimeType;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        if (!isEnabled()) {
+            // not clickable? don't show triangle
+            return;
+        }
+
+        if (mOverlay == null || mOverlay.getIntrinsicWidth() == 0 ||
+                mOverlay.getIntrinsicHeight() == 0) {
+            // nothing to draw
+            return;
+        }
+
+        mOverlay.setBounds(0, 0, getWidth(), getHeight());
+
+        if (mPaddingTop == 0 && mPaddingLeft == 0) {
+            mOverlay.draw(canvas);
+        } else {
+            int saveCount = canvas.getSaveCount();
+            canvas.save();
+            canvas.translate(mPaddingLeft, mPaddingTop);
+            mOverlay.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    /** True if a contact, an email address or a phone number has been assigned */
+    private boolean isAssigned() {
+        return mContactUri != null || mContactEmail != null || mContactPhone != null;
+    }
+
+    /**
+     * Resets the contact photo to the default state.
+     */
+    public void setImageToDefault() {
+        if (mDefaultAvatar == null) {
+            mDefaultAvatar = mContext.getDrawable(R.drawable.ic_contact_picture);
+        }
+        setImageDrawable(mDefaultAvatar);
+    }
+
+    /**
+     * Assign the contact uri that this QuickContactBadge should be associated
+     * with. Note that this is only used for displaying the QuickContact window and
+     * won't bind the contact's photo for you. Call {@link #setImageDrawable(Drawable)} to set the
+     * photo.
+     *
+     * @param contactUri Either a {@link Contacts#CONTENT_URI} or
+     *            {@link Contacts#CONTENT_LOOKUP_URI} style URI.
+     */
+    public void assignContactUri(Uri contactUri) {
+        mContactUri = contactUri;
+        mContactEmail = null;
+        mContactPhone = null;
+        onContactUriChanged();
+    }
+
+    /**
+     * Assign a contact based on an email address. This should only be used when
+     * the contact's URI is not available, as an extra query will have to be
+     * performed to lookup the URI based on the email.
+     *
+     * @param emailAddress The email address of the contact.
+     * @param lazyLookup If this is true, the lookup query will not be performed
+     * until this view is clicked.
+     */
+    public void assignContactFromEmail(String emailAddress, boolean lazyLookup) {
+        assignContactFromEmail(emailAddress, lazyLookup, null);
+    }
+
+    /**
+     * Assign a contact based on an email address. This should only be used when
+     * the contact's URI is not available, as an extra query will have to be
+     * performed to lookup the URI based on the email.
+
+     @param emailAddress The email address of the contact.
+     @param lazyLookup If this is true, the lookup query will not be performed
+     until this view is clicked.
+     @param extras A bundle of extras to populate the contact edit page with if the contact
+     is not found and the user chooses to add the email address to an existing contact or
+     create a new contact. Uses the same string constants as those found in
+     {@link android.provider.ContactsContract.Intents.Insert}
+    */
+
+    public void assignContactFromEmail(String emailAddress, boolean lazyLookup, Bundle extras) {
+        mContactEmail = emailAddress;
+        mExtras = extras;
+        if (!lazyLookup && mQueryHandler != null) {
+            mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP, null,
+                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
+                    EMAIL_LOOKUP_PROJECTION, null, null, null);
+        } else {
+            mContactUri = null;
+            onContactUriChanged();
+        }
+    }
+
+
+    /**
+     * Assign a contact based on a phone number. This should only be used when
+     * the contact's URI is not available, as an extra query will have to be
+     * performed to lookup the URI based on the phone number.
+     *
+     * @param phoneNumber The phone number of the contact.
+     * @param lazyLookup If this is true, the lookup query will not be performed
+     * until this view is clicked.
+     */
+    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup) {
+        assignContactFromPhone(phoneNumber, lazyLookup, new Bundle());
+    }
+
+    /**
+     * Assign a contact based on a phone number. This should only be used when
+     * the contact's URI is not available, as an extra query will have to be
+     * performed to lookup the URI based on the phone number.
+     *
+     * @param phoneNumber The phone number of the contact.
+     * @param lazyLookup If this is true, the lookup query will not be performed
+     * until this view is clicked.
+     * @param extras A bundle of extras to populate the contact edit page with if the contact
+     * is not found and the user chooses to add the phone number to an existing contact or
+     * create a new contact. Uses the same string constants as those found in
+     * {@link android.provider.ContactsContract.Intents.Insert}
+     */
+    public void assignContactFromPhone(String phoneNumber, boolean lazyLookup, Bundle extras) {
+        mContactPhone = phoneNumber;
+        mExtras = extras;
+        if (!lazyLookup && mQueryHandler != null) {
+            mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP, null,
+                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
+                    PHONE_LOOKUP_PROJECTION, null, null, null);
+        } else {
+            mContactUri = null;
+            onContactUriChanged();
+        }
+    }
+
+    /**
+     * Assigns the drawable that is to be drawn on top of the assigned contact photo.
+     *
+     * @param overlay Drawable to be drawn over the assigned contact photo. Must have a non-zero
+     *         instrinsic width and height.
+     */
+    public void setOverlay(Drawable overlay) {
+        mOverlay = overlay;
+    }
+
+    private void onContactUriChanged() {
+        setEnabled(isAssigned());
+    }
+
+    @Override
+    public void onClick(View v) {
+        // If contact has been assigned, mExtras should no longer be null, but do a null check
+        // anyway just in case assignContactFromPhone or Email was called with a null bundle or
+        // wasn't assigned previously.
+        final Bundle extras = (mExtras == null) ? new Bundle() : mExtras;
+        if (mContactUri != null) {
+            QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
+                    mExcludeMimes, mPrioritizedMimeType);
+        } else if (mContactEmail != null && mQueryHandler != null) {
+            extras.putString(EXTRA_URI_CONTENT, mContactEmail);
+            mQueryHandler.startQuery(TOKEN_EMAIL_LOOKUP_AND_TRIGGER, extras,
+                    Uri.withAppendedPath(Email.CONTENT_LOOKUP_URI, Uri.encode(mContactEmail)),
+                    EMAIL_LOOKUP_PROJECTION, null, null, null);
+        } else if (mContactPhone != null && mQueryHandler != null) {
+            extras.putString(EXTRA_URI_CONTENT, mContactPhone);
+            mQueryHandler.startQuery(TOKEN_PHONE_LOOKUP_AND_TRIGGER, extras,
+                    Uri.withAppendedPath(PhoneLookup.CONTENT_FILTER_URI, mContactPhone),
+                    PHONE_LOOKUP_PROJECTION, null, null, null);
+        } else {
+            // If a contact hasn't been assigned, don't react to click.
+            return;
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return QuickContactBadge.class.getName();
+    }
+
+    /**
+     * Set a list of specific MIME-types to exclude and not display. For
+     * example, this can be used to hide the {@link Contacts#CONTENT_ITEM_TYPE}
+     * profile icon.
+     */
+    public void setExcludeMimes(String[] excludeMimes) {
+        mExcludeMimes = excludeMimes;
+    }
+
+    private class QueryHandler extends AsyncQueryHandler {
+
+        public QueryHandler(ContentResolver cr) {
+            super(cr);
+        }
+
+        @Override
+        protected void onQueryComplete(int token, Object cookie, Cursor cursor) {
+            Uri lookupUri = null;
+            Uri createUri = null;
+            boolean trigger = false;
+            Bundle extras = (cookie != null) ? (Bundle) cookie : new Bundle();
+            try {
+                switch(token) {
+                    case TOKEN_PHONE_LOOKUP_AND_TRIGGER:
+                        trigger = true;
+                        createUri = Uri.fromParts("tel", extras.getString(EXTRA_URI_CONTENT), null);
+
+                        //$FALL-THROUGH$
+                    case TOKEN_PHONE_LOOKUP: {
+                        if (cursor != null && cursor.moveToFirst()) {
+                            long contactId = cursor.getLong(PHONE_ID_COLUMN_INDEX);
+                            String lookupKey = cursor.getString(PHONE_LOOKUP_STRING_COLUMN_INDEX);
+                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+                        }
+
+                        break;
+                    }
+                    case TOKEN_EMAIL_LOOKUP_AND_TRIGGER:
+                        trigger = true;
+                        createUri = Uri.fromParts("mailto",
+                                extras.getString(EXTRA_URI_CONTENT), null);
+
+                        //$FALL-THROUGH$
+                    case TOKEN_EMAIL_LOOKUP: {
+                        if (cursor != null && cursor.moveToFirst()) {
+                            long contactId = cursor.getLong(EMAIL_ID_COLUMN_INDEX);
+                            String lookupKey = cursor.getString(EMAIL_LOOKUP_STRING_COLUMN_INDEX);
+                            lookupUri = Contacts.getLookupUri(contactId, lookupKey);
+                        }
+                        break;
+                    }
+                }
+            } finally {
+                if (cursor != null) {
+                    cursor.close();
+                }
+            }
+
+            mContactUri = lookupUri;
+            onContactUriChanged();
+
+            if (trigger && mContactUri != null) {
+                // Found contact, so trigger QuickContact
+                QuickContact.showQuickContact(getContext(), QuickContactBadge.this, mContactUri,
+                        mExcludeMimes, mPrioritizedMimeType);
+            } else if (createUri != null) {
+                // Prompt user to add this person to contacts
+                final Intent intent = new Intent(Intents.SHOW_OR_CREATE_CONTACT, createUri);
+                if (extras != null) {
+                    extras.remove(EXTRA_URI_CONTENT);
+                    intent.putExtras(extras);
+                }
+                getContext().startActivity(intent);
+            }
+        }
+    }
+}
diff --git a/android/widget/RadialTimePickerView.java b/android/widget/RadialTimePickerView.java
new file mode 100644
index 0000000..757a4ca
--- /dev/null
+++ b/android/widget/RadialTimePickerView.java
@@ -0,0 +1,1381 @@
+/*
+ * Copyright (C) 2013 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 android.widget;
+
+import android.animation.ObjectAnimator;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.Rect;
+import android.graphics.Region;
+import android.graphics.Typeface;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.MathUtils;
+import android.util.StateSet;
+import android.util.TypedValue;
+import android.view.HapticFeedbackConstants;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import com.android.internal.R;
+import com.android.internal.widget.ExploreByTouchHelper;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+import java.util.Locale;
+
+/**
+ * View to show a clock circle picker (with one or two picking circles)
+ *
+ * @hide
+ */
+public class RadialTimePickerView extends View {
+    private static final String TAG = "RadialTimePickerView";
+
+    public static final int HOURS = 0;
+    public static final int MINUTES = 1;
+
+    /** @hide */
+    @IntDef({HOURS, MINUTES})
+    @Retention(RetentionPolicy.SOURCE)
+    @interface PickerType {}
+
+    private static final int HOURS_INNER = 2;
+
+    private static final int SELECTOR_CIRCLE = 0;
+    private static final int SELECTOR_DOT = 1;
+    private static final int SELECTOR_LINE = 2;
+
+    private static final int AM = 0;
+    private static final int PM = 1;
+
+    private static final int HOURS_IN_CIRCLE = 12;
+    private static final int MINUTES_IN_CIRCLE = 60;
+    private static final int DEGREES_FOR_ONE_HOUR = 360 / HOURS_IN_CIRCLE;
+    private static final int DEGREES_FOR_ONE_MINUTE = 360 / MINUTES_IN_CIRCLE;
+
+    private static final int[] HOURS_NUMBERS = {12, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};
+    private static final int[] HOURS_NUMBERS_24 = {0, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23};
+    private static final int[] MINUTES_NUMBERS = {0, 5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55};
+
+    private static final int ANIM_DURATION_NORMAL = 500;
+    private static final int ANIM_DURATION_TOUCH = 60;
+
+    private static final int[] SNAP_PREFER_30S_MAP = new int[361];
+
+    private static final int NUM_POSITIONS = 12;
+    private static final float[] COS_30 = new float[NUM_POSITIONS];
+    private static final float[] SIN_30 = new float[NUM_POSITIONS];
+
+    /** "Something is wrong" color used when a color attribute is missing. */
+    private static final int MISSING_COLOR = Color.MAGENTA;
+
+    static {
+        // Prepare mapping to snap touchable degrees to selectable degrees.
+        preparePrefer30sMap();
+
+        final double increment = 2.0 * Math.PI / NUM_POSITIONS;
+        double angle = Math.PI / 2.0;
+        for (int i = 0; i < NUM_POSITIONS; i++) {
+            COS_30[i] = (float) Math.cos(angle);
+            SIN_30[i] = (float) Math.sin(angle);
+            angle += increment;
+        }
+    }
+
+    private final FloatProperty<RadialTimePickerView> HOURS_TO_MINUTES =
+            new FloatProperty<RadialTimePickerView>("hoursToMinutes") {
+                @Override
+                public Float get(RadialTimePickerView radialTimePickerView) {
+                    return radialTimePickerView.mHoursToMinutes;
+                }
+
+                @Override
+                public void setValue(RadialTimePickerView object, float value) {
+                    object.mHoursToMinutes = value;
+                    object.invalidate();
+                }
+            };
+
+    private final String[] mHours12Texts = new String[12];
+    private final String[] mOuterHours24Texts = new String[12];
+    private final String[] mInnerHours24Texts = new String[12];
+    private final String[] mMinutesTexts = new String[12];
+
+    private final Paint[] mPaint = new Paint[2];
+    private final Paint mPaintCenter = new Paint();
+    private final Paint[] mPaintSelector = new Paint[3];
+    private final Paint mPaintBackground = new Paint();
+
+    private final Typeface mTypeface;
+
+    private final ColorStateList[] mTextColor = new ColorStateList[3];
+    private final int[] mTextSize = new int[3];
+    private final int[] mTextInset = new int[3];
+
+    private final float[][] mOuterTextX = new float[2][12];
+    private final float[][] mOuterTextY = new float[2][12];
+
+    private final float[] mInnerTextX = new float[12];
+    private final float[] mInnerTextY = new float[12];
+
+    private final int[] mSelectionDegrees = new int[2];
+
+    private final RadialPickerTouchHelper mTouchHelper;
+
+    private final Path mSelectorPath = new Path();
+
+    private boolean mIs24HourMode;
+    private boolean mShowHours;
+
+    private ObjectAnimator mHoursToMinutesAnimator;
+    private float mHoursToMinutes;
+
+    /**
+     * When in 24-hour mode, indicates that the current hour is between
+     * 1 and 12 (inclusive).
+     */
+    private boolean mIsOnInnerCircle;
+
+    private int mSelectorRadius;
+    private int mSelectorStroke;
+    private int mSelectorDotRadius;
+    private int mCenterDotRadius;
+
+    private int mSelectorColor;
+    private int mSelectorDotColor;
+
+    private int mXCenter;
+    private int mYCenter;
+    private int mCircleRadius;
+
+    private int mMinDistForInnerNumber;
+    private int mMaxDistForOuterNumber;
+    private int mHalfwayDist;
+
+    private String[] mOuterTextHours;
+    private String[] mInnerTextHours;
+    private String[] mMinutesText;
+
+    private int mAmOrPm;
+
+    private float mDisabledAlpha;
+
+    private OnValueSelectedListener mListener;
+
+    private boolean mInputEnabled = true;
+
+    interface OnValueSelectedListener {
+        /**
+         * Called when the selected value at a given picker index has changed.
+         *
+         * @param pickerType the type of value that has changed, one of:
+         *                   <ul>
+         *                       <li>{@link #MINUTES}
+         *                       <li>{@link #HOURS}
+         *                   </ul>
+         * @param newValue the new value as minute in hour (0-59) or hour in
+         *                 day (0-23)
+         * @param autoAdvance when the picker type is {@link #HOURS},
+         *                    {@code true} to switch to the {@link #MINUTES}
+         *                    picker or {@code false} to stay on the current
+         *                    picker. No effect when picker type is
+         *                    {@link #MINUTES}.
+         */
+        void onValueSelected(@PickerType int pickerType, int newValue, boolean autoAdvance);
+    }
+
+    /**
+     * Split up the 360 degrees of the circle among the 60 selectable values. Assigns a larger
+     * selectable area to each of the 12 visible values, such that the ratio of space apportioned
+     * to a visible value : space apportioned to a non-visible value will be 14 : 4.
+     * E.g. the output of 30 degrees should have a higher range of input associated with it than
+     * the output of 24 degrees, because 30 degrees corresponds to a visible number on the clock
+     * circle (5 on the minutes, 1 or 13 on the hours).
+     */
+    private static void preparePrefer30sMap() {
+        // We'll split up the visible output and the non-visible output such that each visible
+        // output will correspond to a range of 14 associated input degrees, and each non-visible
+        // output will correspond to a range of 4 associate input degrees, so visible numbers
+        // are more than 3 times easier to get than non-visible numbers:
+        // {354-359,0-7}:0, {8-11}:6, {12-15}:12, {16-19}:18, {20-23}:24, {24-37}:30, etc.
+        //
+        // If an output of 30 degrees should correspond to a range of 14 associated degrees, then
+        // we'll need any input between 24 - 37 to snap to 30. Working out from there, 20-23 should
+        // snap to 24, while 38-41 should snap to 36. This is somewhat counter-intuitive, that you
+        // can be touching 36 degrees but have the selection snapped to 30 degrees; however, this
+        // inconsistency isn't noticeable at such fine-grained degrees, and it affords us the
+        // ability to aggressively prefer the visible values by a factor of more than 3:1, which
+        // greatly contributes to the selectability of these values.
+
+        // The first output is 0, and each following output will increment by 6 {0, 6, 12, ...}.
+        int snappedOutputDegrees = 0;
+        // Count of how many inputs we've designated to the specified output.
+        int count = 1;
+        // How many input we expect for a specified output. This will be 14 for output divisible
+        // by 30, and 4 for the remaining output. We'll special case the outputs of 0 and 360, so
+        // the caller can decide which they need.
+        int expectedCount = 8;
+        // Iterate through the input.
+        for (int degrees = 0; degrees < 361; degrees++) {
+            // Save the input-output mapping.
+            SNAP_PREFER_30S_MAP[degrees] = snappedOutputDegrees;
+            // If this is the last input for the specified output, calculate the next output and
+            // the next expected count.
+            if (count == expectedCount) {
+                snappedOutputDegrees += 6;
+                if (snappedOutputDegrees == 360) {
+                    expectedCount = 7;
+                } else if (snappedOutputDegrees % 30 == 0) {
+                    expectedCount = 14;
+                } else {
+                    expectedCount = 4;
+                }
+                count = 1;
+            } else {
+                count++;
+            }
+        }
+    }
+
+    /**
+     * Returns mapping of any input degrees (0 to 360) to one of 60 selectable output degrees,
+     * where the degrees corresponding to visible numbers (i.e. those divisible by 30) will be
+     * weighted heavier than the degrees corresponding to non-visible numbers.
+     * See {@link #preparePrefer30sMap()} documentation for the rationale and generation of the
+     * mapping.
+     */
+    private static int snapPrefer30s(int degrees) {
+        if (SNAP_PREFER_30S_MAP == null) {
+            return -1;
+        }
+        return SNAP_PREFER_30S_MAP[degrees];
+    }
+
+    /**
+     * Returns mapping of any input degrees (0 to 360) to one of 12 visible output degrees (all
+     * multiples of 30), where the input will be "snapped" to the closest visible degrees.
+     * @param degrees The input degrees
+     * @param forceHigherOrLower The output may be forced to either the higher or lower step, or may
+     * be allowed to snap to whichever is closer. Use 1 to force strictly higher, -1 to force
+     * strictly lower, and 0 to snap to the closer one.
+     * @return output degrees, will be a multiple of 30
+     */
+    private static int snapOnly30s(int degrees, int forceHigherOrLower) {
+        final int stepSize = DEGREES_FOR_ONE_HOUR;
+        int floor = (degrees / stepSize) * stepSize;
+        final int ceiling = floor + stepSize;
+        if (forceHigherOrLower == 1) {
+            degrees = ceiling;
+        } else if (forceHigherOrLower == -1) {
+            if (degrees == floor) {
+                floor -= stepSize;
+            }
+            degrees = floor;
+        } else {
+            if ((degrees - floor) < (ceiling - degrees)) {
+                degrees = floor;
+            } else {
+                degrees = ceiling;
+            }
+        }
+        return degrees;
+    }
+
+    @SuppressWarnings("unused")
+    public RadialTimePickerView(Context context)  {
+        this(context, null);
+    }
+
+    public RadialTimePickerView(Context context, AttributeSet attrs)  {
+        this(context, attrs, R.attr.timePickerStyle);
+    }
+
+    public RadialTimePickerView(Context context, AttributeSet attrs, int defStyleAttr)  {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RadialTimePickerView(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes)  {
+        super(context, attrs);
+
+        applyAttributes(attrs, defStyleAttr, defStyleRes);
+
+        // Pull disabled alpha from theme.
+        final TypedValue outValue = new TypedValue();
+        context.getTheme().resolveAttribute(android.R.attr.disabledAlpha, outValue, true);
+        mDisabledAlpha = outValue.getFloat();
+
+        mTypeface = Typeface.create("sans-serif", Typeface.NORMAL);
+
+        mPaint[HOURS] = new Paint();
+        mPaint[HOURS].setAntiAlias(true);
+        mPaint[HOURS].setTextAlign(Paint.Align.CENTER);
+
+        mPaint[MINUTES] = new Paint();
+        mPaint[MINUTES].setAntiAlias(true);
+        mPaint[MINUTES].setTextAlign(Paint.Align.CENTER);
+
+        mPaintCenter.setAntiAlias(true);
+
+        mPaintSelector[SELECTOR_CIRCLE] = new Paint();
+        mPaintSelector[SELECTOR_CIRCLE].setAntiAlias(true);
+
+        mPaintSelector[SELECTOR_DOT] = new Paint();
+        mPaintSelector[SELECTOR_DOT].setAntiAlias(true);
+
+        mPaintSelector[SELECTOR_LINE] = new Paint();
+        mPaintSelector[SELECTOR_LINE].setAntiAlias(true);
+        mPaintSelector[SELECTOR_LINE].setStrokeWidth(2);
+
+        mPaintBackground.setAntiAlias(true);
+
+        final Resources res = getResources();
+        mSelectorRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_radius);
+        mSelectorStroke = res.getDimensionPixelSize(R.dimen.timepicker_selector_stroke);
+        mSelectorDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_selector_dot_radius);
+        mCenterDotRadius = res.getDimensionPixelSize(R.dimen.timepicker_center_dot_radius);
+
+        mTextSize[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
+        mTextSize[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_normal);
+        mTextSize[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_size_inner);
+
+        mTextInset[HOURS] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
+        mTextInset[MINUTES] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_normal);
+        mTextInset[HOURS_INNER] = res.getDimensionPixelSize(R.dimen.timepicker_text_inset_inner);
+
+        mShowHours = true;
+        mHoursToMinutes = HOURS;
+        mIs24HourMode = false;
+        mAmOrPm = AM;
+
+        // Set up accessibility components.
+        mTouchHelper = new RadialPickerTouchHelper();
+        setAccessibilityDelegate(mTouchHelper);
+
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        initHoursAndMinutesText();
+        initData();
+
+        // Initial values
+        final Calendar calendar = Calendar.getInstance(Locale.getDefault());
+        final int currentHour = calendar.get(Calendar.HOUR_OF_DAY);
+        final int currentMinute = calendar.get(Calendar.MINUTE);
+
+        setCurrentHourInternal(currentHour, false, false);
+        setCurrentMinuteInternal(currentMinute, false);
+
+        setHapticFeedbackEnabled(true);
+    }
+
+    void applyAttributes(AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        final Context context = getContext();
+        final TypedArray a = getContext().obtainStyledAttributes(attrs,
+                R.styleable.TimePicker, defStyleAttr, defStyleRes);
+
+        final ColorStateList numbersTextColor = a.getColorStateList(
+                R.styleable.TimePicker_numbersTextColor);
+        final ColorStateList numbersInnerTextColor = a.getColorStateList(
+                R.styleable.TimePicker_numbersInnerTextColor);
+        mTextColor[HOURS] = numbersTextColor == null ?
+                ColorStateList.valueOf(MISSING_COLOR) : numbersTextColor;
+        mTextColor[HOURS_INNER] = numbersInnerTextColor == null ?
+                ColorStateList.valueOf(MISSING_COLOR) : numbersInnerTextColor;
+        mTextColor[MINUTES] = mTextColor[HOURS];
+
+        // Set up various colors derived from the selector "activated" state.
+        final ColorStateList selectorColors = a.getColorStateList(
+                R.styleable.TimePicker_numbersSelectorColor);
+        final int selectorActivatedColor;
+        if (selectorColors != null) {
+            final int[] stateSetEnabledActivated = StateSet.get(
+                    StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
+            selectorActivatedColor = selectorColors.getColorForState(
+                    stateSetEnabledActivated, 0);
+        }  else {
+            selectorActivatedColor = MISSING_COLOR;
+        }
+
+        mPaintCenter.setColor(selectorActivatedColor);
+
+        final int[] stateSetActivated = StateSet.get(
+                StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED);
+
+        mSelectorColor = selectorActivatedColor;
+        mSelectorDotColor = mTextColor[HOURS].getColorForState(stateSetActivated, 0);
+
+        mPaintBackground.setColor(a.getColor(R.styleable.TimePicker_numbersBackgroundColor,
+                context.getColor(R.color.timepicker_default_numbers_background_color_material)));
+
+        a.recycle();
+    }
+
+    public void initialize(int hour, int minute, boolean is24HourMode) {
+        if (mIs24HourMode != is24HourMode) {
+            mIs24HourMode = is24HourMode;
+            initData();
+        }
+
+        setCurrentHourInternal(hour, false, false);
+        setCurrentMinuteInternal(minute, false);
+    }
+
+    public void setCurrentItemShowing(int item, boolean animate) {
+        switch (item){
+            case HOURS:
+                showHours(animate);
+                break;
+            case MINUTES:
+                showMinutes(animate);
+                break;
+            default:
+                Log.e(TAG, "ClockView does not support showing item " + item);
+        }
+    }
+
+    public int getCurrentItemShowing() {
+        return mShowHours ? HOURS : MINUTES;
+    }
+
+    public void setOnValueSelectedListener(OnValueSelectedListener listener) {
+        mListener = listener;
+    }
+
+    /**
+     * Sets the current hour in 24-hour time.
+     *
+     * @param hour the current hour between 0 and 23 (inclusive)
+     */
+    public void setCurrentHour(int hour) {
+        setCurrentHourInternal(hour, true, false);
+    }
+
+    /**
+     * Sets the current hour.
+     *
+     * @param hour The current hour
+     * @param callback Whether the value listener should be invoked
+     * @param autoAdvance Whether the listener should auto-advance to the next
+     *                    selection mode, e.g. hour to minutes
+     */
+    private void setCurrentHourInternal(int hour, boolean callback, boolean autoAdvance) {
+        final int degrees = (hour % 12) * DEGREES_FOR_ONE_HOUR;
+        mSelectionDegrees[HOURS] = degrees;
+
+        // 0 is 12 AM (midnight) and 12 is 12 PM (noon).
+        final int amOrPm = (hour == 0 || (hour % 24) < 12) ? AM : PM;
+        final boolean isOnInnerCircle = getInnerCircleForHour(hour);
+        if (mAmOrPm != amOrPm || mIsOnInnerCircle != isOnInnerCircle) {
+            mAmOrPm = amOrPm;
+            mIsOnInnerCircle = isOnInnerCircle;
+
+            initData();
+            mTouchHelper.invalidateRoot();
+        }
+
+        invalidate();
+
+        if (callback && mListener != null) {
+            mListener.onValueSelected(HOURS, hour, autoAdvance);
+        }
+    }
+
+    /**
+     * Returns the current hour in 24-hour time.
+     *
+     * @return the current hour between 0 and 23 (inclusive)
+     */
+    public int getCurrentHour() {
+        return getHourForDegrees(mSelectionDegrees[HOURS], mIsOnInnerCircle);
+    }
+
+    private int getHourForDegrees(int degrees, boolean innerCircle) {
+        int hour = (degrees / DEGREES_FOR_ONE_HOUR) % 12;
+        if (mIs24HourMode) {
+            // Convert the 12-hour value into 24-hour time based on where the
+            // selector is positioned.
+            if (!innerCircle && hour == 0) {
+                // Outer circle is 1 through 12.
+                hour = 12;
+            } else if (innerCircle && hour != 0) {
+                // Inner circle is 13 through 23 and 0.
+                hour += 12;
+            }
+        } else if (mAmOrPm == PM) {
+            hour += 12;
+        }
+        return hour;
+    }
+
+    /**
+     * @param hour the hour in 24-hour time or 12-hour time
+     */
+    private int getDegreesForHour(int hour) {
+        // Convert to be 0-11.
+        if (mIs24HourMode) {
+            if (hour >= 12) {
+                hour -= 12;
+            }
+        } else if (hour == 12) {
+            hour = 0;
+        }
+        return hour * DEGREES_FOR_ONE_HOUR;
+    }
+
+    /**
+     * @param hour the hour in 24-hour time or 12-hour time
+     */
+    private boolean getInnerCircleForHour(int hour) {
+        return mIs24HourMode && (hour == 0 || hour > 12);
+    }
+
+    public void setCurrentMinute(int minute) {
+        setCurrentMinuteInternal(minute, true);
+    }
+
+    private void setCurrentMinuteInternal(int minute, boolean callback) {
+        mSelectionDegrees[MINUTES] = (minute % MINUTES_IN_CIRCLE) * DEGREES_FOR_ONE_MINUTE;
+
+        invalidate();
+
+        if (callback && mListener != null) {
+            mListener.onValueSelected(MINUTES, minute, false);
+        }
+    }
+
+    // Returns minutes in 0-59 range
+    public int getCurrentMinute() {
+        return getMinuteForDegrees(mSelectionDegrees[MINUTES]);
+    }
+
+    private int getMinuteForDegrees(int degrees) {
+        return degrees / DEGREES_FOR_ONE_MINUTE;
+    }
+
+    private int getDegreesForMinute(int minute) {
+        return minute * DEGREES_FOR_ONE_MINUTE;
+    }
+
+    /**
+     * Sets whether the picker is showing AM or PM hours. Has no effect when
+     * in 24-hour mode.
+     *
+     * @param amOrPm {@link #AM} or {@link #PM}
+     * @return {@code true} if the value changed from what was previously set,
+     *         or {@code false} otherwise
+     */
+    public boolean setAmOrPm(int amOrPm) {
+        if (mAmOrPm == amOrPm || mIs24HourMode) {
+            return false;
+        }
+
+        mAmOrPm = amOrPm;
+        invalidate();
+        mTouchHelper.invalidateRoot();
+        return true;
+    }
+
+    public int getAmOrPm() {
+        return mAmOrPm;
+    }
+
+    public void showHours(boolean animate) {
+        showPicker(true, animate);
+    }
+
+    public void showMinutes(boolean animate) {
+        showPicker(false, animate);
+    }
+
+    private void initHoursAndMinutesText() {
+        // Initialize the hours and minutes numbers.
+        for (int i = 0; i < 12; i++) {
+            mHours12Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+            mInnerHours24Texts[i] = String.format("%02d", HOURS_NUMBERS_24[i]);
+            mOuterHours24Texts[i] = String.format("%d", HOURS_NUMBERS[i]);
+            mMinutesTexts[i] = String.format("%02d", MINUTES_NUMBERS[i]);
+        }
+    }
+
+    private void initData() {
+        if (mIs24HourMode) {
+            mOuterTextHours = mOuterHours24Texts;
+            mInnerTextHours = mInnerHours24Texts;
+        } else {
+            mOuterTextHours = mHours12Texts;
+            mInnerTextHours = mHours12Texts;
+        }
+
+        mMinutesText = mMinutesTexts;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (!changed) {
+            return;
+        }
+
+        mXCenter = getWidth() / 2;
+        mYCenter = getHeight() / 2;
+        mCircleRadius = Math.min(mXCenter, mYCenter);
+
+        mMinDistForInnerNumber = mCircleRadius - mTextInset[HOURS_INNER] - mSelectorRadius;
+        mMaxDistForOuterNumber = mCircleRadius - mTextInset[HOURS] + mSelectorRadius;
+        mHalfwayDist = mCircleRadius - (mTextInset[HOURS] + mTextInset[HOURS_INNER]) / 2;
+
+        calculatePositionsHours();
+        calculatePositionsMinutes();
+
+        mTouchHelper.invalidateRoot();
+    }
+
+    @Override
+    public void onDraw(Canvas canvas) {
+        final float alphaMod = mInputEnabled ? 1 : mDisabledAlpha;
+
+        drawCircleBackground(canvas);
+
+        final Path selectorPath = mSelectorPath;
+        drawSelector(canvas, selectorPath);
+        drawHours(canvas, selectorPath, alphaMod);
+        drawMinutes(canvas, selectorPath, alphaMod);
+        drawCenter(canvas, alphaMod);
+    }
+
+    private void showPicker(boolean hours, boolean animate) {
+        if (mShowHours == hours) {
+            return;
+        }
+
+        mShowHours = hours;
+
+        if (animate) {
+            animatePicker(hours, ANIM_DURATION_NORMAL);
+        } else {
+            // If we have a pending or running animator, cancel it.
+            if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
+                mHoursToMinutesAnimator.cancel();
+                mHoursToMinutesAnimator = null;
+            }
+            mHoursToMinutes = hours ? 0.0f : 1.0f;
+        }
+
+        initData();
+        invalidate();
+        mTouchHelper.invalidateRoot();
+    }
+
+    private void animatePicker(boolean hoursToMinutes, long duration) {
+        final float target = hoursToMinutes ? HOURS : MINUTES;
+        if (mHoursToMinutes == target) {
+            // If we have a pending or running animator, cancel it.
+            if (mHoursToMinutesAnimator != null && mHoursToMinutesAnimator.isStarted()) {
+                mHoursToMinutesAnimator.cancel();
+                mHoursToMinutesAnimator = null;
+            }
+
+            // We're already showing the correct picker.
+            return;
+        }
+
+        mHoursToMinutesAnimator = ObjectAnimator.ofFloat(this, HOURS_TO_MINUTES, target);
+        mHoursToMinutesAnimator.setAutoCancel(true);
+        mHoursToMinutesAnimator.setDuration(duration);
+        mHoursToMinutesAnimator.start();
+    }
+
+    private void drawCircleBackground(Canvas canvas) {
+        canvas.drawCircle(mXCenter, mYCenter, mCircleRadius, mPaintBackground);
+    }
+
+    private void drawHours(Canvas canvas, Path selectorPath, float alphaMod) {
+        final int hoursAlpha = (int) (255f * (1f - mHoursToMinutes) * alphaMod + 0.5f);
+        if (hoursAlpha > 0) {
+            // Exclude the selector region, then draw inner/outer hours with no
+            // activated states.
+            canvas.save(Canvas.CLIP_SAVE_FLAG);
+            canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
+            drawHoursClipped(canvas, hoursAlpha, false);
+            canvas.restore();
+
+            // Intersect the selector region, then draw minutes with only
+            // activated states.
+            canvas.save(Canvas.CLIP_SAVE_FLAG);
+            canvas.clipPath(selectorPath, Region.Op.INTERSECT);
+            drawHoursClipped(canvas, hoursAlpha, true);
+            canvas.restore();
+        }
+    }
+
+    private void drawHoursClipped(Canvas canvas, int hoursAlpha, boolean showActivated) {
+        // Draw outer hours.
+        drawTextElements(canvas, mTextSize[HOURS], mTypeface, mTextColor[HOURS], mOuterTextHours,
+                mOuterTextX[HOURS], mOuterTextY[HOURS], mPaint[HOURS], hoursAlpha,
+                showActivated && !mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
+
+        // Draw inner hours (13-00) for 24-hour time.
+        if (mIs24HourMode && mInnerTextHours != null) {
+            drawTextElements(canvas, mTextSize[HOURS_INNER], mTypeface, mTextColor[HOURS_INNER],
+                    mInnerTextHours, mInnerTextX, mInnerTextY, mPaint[HOURS], hoursAlpha,
+                    showActivated && mIsOnInnerCircle, mSelectionDegrees[HOURS], showActivated);
+        }
+    }
+
+    private void drawMinutes(Canvas canvas, Path selectorPath, float alphaMod) {
+        final int minutesAlpha = (int) (255f * mHoursToMinutes * alphaMod + 0.5f);
+        if (minutesAlpha > 0) {
+            // Exclude the selector region, then draw minutes with no
+            // activated states.
+            canvas.save(Canvas.CLIP_SAVE_FLAG);
+            canvas.clipPath(selectorPath, Region.Op.DIFFERENCE);
+            drawMinutesClipped(canvas, minutesAlpha, false);
+            canvas.restore();
+
+            // Intersect the selector region, then draw minutes with only
+            // activated states.
+            canvas.save(Canvas.CLIP_SAVE_FLAG);
+            canvas.clipPath(selectorPath, Region.Op.INTERSECT);
+            drawMinutesClipped(canvas, minutesAlpha, true);
+            canvas.restore();
+        }
+    }
+
+    private void drawMinutesClipped(Canvas canvas, int minutesAlpha, boolean showActivated) {
+        drawTextElements(canvas, mTextSize[MINUTES], mTypeface, mTextColor[MINUTES], mMinutesText,
+                mOuterTextX[MINUTES], mOuterTextY[MINUTES], mPaint[MINUTES], minutesAlpha,
+                showActivated, mSelectionDegrees[MINUTES], showActivated);
+    }
+
+    private void drawCenter(Canvas canvas, float alphaMod) {
+        mPaintCenter.setAlpha((int) (255 * alphaMod + 0.5f));
+        canvas.drawCircle(mXCenter, mYCenter, mCenterDotRadius, mPaintCenter);
+    }
+
+    private int getMultipliedAlpha(int argb, int alpha) {
+        return (int) (Color.alpha(argb) * (alpha / 255.0) + 0.5);
+    }
+
+    private void drawSelector(Canvas canvas, Path selectorPath) {
+        // Determine the current length, angle, and dot scaling factor.
+        final int hoursIndex = mIsOnInnerCircle ? HOURS_INNER : HOURS;
+        final int hoursInset = mTextInset[hoursIndex];
+        final int hoursAngleDeg = mSelectionDegrees[hoursIndex % 2];
+        final float hoursDotScale = mSelectionDegrees[hoursIndex % 2] % 30 != 0 ? 1 : 0;
+
+        final int minutesIndex = MINUTES;
+        final int minutesInset = mTextInset[minutesIndex];
+        final int minutesAngleDeg = mSelectionDegrees[minutesIndex];
+        final float minutesDotScale = mSelectionDegrees[minutesIndex] % 30 != 0 ? 1 : 0;
+
+        // Calculate the current radius at which to place the selection circle.
+        final int selRadius = mSelectorRadius;
+        final float selLength =
+                mCircleRadius - MathUtils.lerp(hoursInset, minutesInset, mHoursToMinutes);
+        final double selAngleRad =
+                Math.toRadians(MathUtils.lerpDeg(hoursAngleDeg, minutesAngleDeg, mHoursToMinutes));
+        final float selCenterX = mXCenter + selLength * (float) Math.sin(selAngleRad);
+        final float selCenterY = mYCenter - selLength * (float) Math.cos(selAngleRad);
+
+        // Draw the selection circle.
+        final Paint paint = mPaintSelector[SELECTOR_CIRCLE];
+        paint.setColor(mSelectorColor);
+        canvas.drawCircle(selCenterX, selCenterY, selRadius, paint);
+
+        // If needed, set up the clip path for later.
+        if (selectorPath != null) {
+            selectorPath.reset();
+            selectorPath.addCircle(selCenterX, selCenterY, selRadius, Path.Direction.CCW);
+        }
+
+        // Draw the dot if we're between two items.
+        final float dotScale = MathUtils.lerp(hoursDotScale, minutesDotScale, mHoursToMinutes);
+        if (dotScale > 0) {
+            final Paint dotPaint = mPaintSelector[SELECTOR_DOT];
+            dotPaint.setColor(mSelectorDotColor);
+            canvas.drawCircle(selCenterX, selCenterY, mSelectorDotRadius * dotScale, dotPaint);
+        }
+
+        // Shorten the line to only go from the edge of the center dot to the
+        // edge of the selection circle.
+        final double sin = Math.sin(selAngleRad);
+        final double cos = Math.cos(selAngleRad);
+        final float lineLength = selLength - selRadius;
+        final int centerX = mXCenter + (int) (mCenterDotRadius * sin);
+        final int centerY = mYCenter - (int) (mCenterDotRadius * cos);
+        final float linePointX = centerX + (int) (lineLength * sin);
+        final float linePointY = centerY - (int) (lineLength * cos);
+
+        // Draw the line.
+        final Paint linePaint = mPaintSelector[SELECTOR_LINE];
+        linePaint.setColor(mSelectorColor);
+        linePaint.setStrokeWidth(mSelectorStroke);
+        canvas.drawLine(mXCenter, mYCenter, linePointX, linePointY, linePaint);
+    }
+
+    private void calculatePositionsHours() {
+        // Calculate the text positions
+        final float numbersRadius = mCircleRadius - mTextInset[HOURS];
+
+        // Calculate the positions for the 12 numbers in the main circle.
+        calculatePositions(mPaint[HOURS], numbersRadius, mXCenter, mYCenter,
+                mTextSize[HOURS], mOuterTextX[HOURS], mOuterTextY[HOURS]);
+
+        // If we have an inner circle, calculate those positions too.
+        if (mIs24HourMode) {
+            final int innerNumbersRadius = mCircleRadius - mTextInset[HOURS_INNER];
+            calculatePositions(mPaint[HOURS], innerNumbersRadius, mXCenter, mYCenter,
+                    mTextSize[HOURS_INNER], mInnerTextX, mInnerTextY);
+        }
+    }
+
+    private void calculatePositionsMinutes() {
+        // Calculate the text positions
+        final float numbersRadius = mCircleRadius - mTextInset[MINUTES];
+
+        // Calculate the positions for the 12 numbers in the main circle.
+        calculatePositions(mPaint[MINUTES], numbersRadius, mXCenter, mYCenter,
+                mTextSize[MINUTES], mOuterTextX[MINUTES], mOuterTextY[MINUTES]);
+    }
+
+    /**
+     * Using the trigonometric Unit Circle, calculate the positions that the text will need to be
+     * drawn at based on the specified circle radius. Place the values in the textGridHeights and
+     * textGridWidths parameters.
+     */
+    private static void calculatePositions(Paint paint, float radius, float xCenter, float yCenter,
+            float textSize, float[] x, float[] y) {
+        // Adjust yCenter to account for the text's baseline.
+        paint.setTextSize(textSize);
+        yCenter -= (paint.descent() + paint.ascent()) / 2;
+
+        for (int i = 0; i < NUM_POSITIONS; i++) {
+            x[i] = xCenter - radius * COS_30[i];
+            y[i] = yCenter - radius * SIN_30[i];
+        }
+    }
+
+    /**
+     * Draw the 12 text values at the positions specified by the textGrid parameters.
+     */
+    private void drawTextElements(Canvas canvas, float textSize, Typeface typeface,
+            ColorStateList textColor, String[] texts, float[] textX, float[] textY, Paint paint,
+            int alpha, boolean showActivated, int activatedDegrees, boolean activatedOnly) {
+        paint.setTextSize(textSize);
+        paint.setTypeface(typeface);
+
+        // The activated index can touch a range of elements.
+        final float activatedIndex = activatedDegrees / (360.0f / NUM_POSITIONS);
+        final int activatedFloor = (int) activatedIndex;
+        final int activatedCeil = ((int) Math.ceil(activatedIndex)) % NUM_POSITIONS;
+
+        for (int i = 0; i < 12; i++) {
+            final boolean activated = (activatedFloor == i || activatedCeil == i);
+            if (activatedOnly && !activated) {
+                continue;
+            }
+
+            final int stateMask = StateSet.VIEW_STATE_ENABLED
+                    | (showActivated && activated ? StateSet.VIEW_STATE_ACTIVATED : 0);
+            final int color = textColor.getColorForState(StateSet.get(stateMask), 0);
+            paint.setColor(color);
+            paint.setAlpha(getMultipliedAlpha(color, alpha));
+
+            canvas.drawText(texts[i], textX[i], textY[i], paint);
+        }
+    }
+
+    private int getDegreesFromXY(float x, float y, boolean constrainOutside) {
+        // Ensure the point is inside the touchable area.
+        final int innerBound;
+        final int outerBound;
+        if (mIs24HourMode && mShowHours) {
+            innerBound = mMinDistForInnerNumber;
+            outerBound = mMaxDistForOuterNumber;
+        } else {
+            final int index = mShowHours ? HOURS : MINUTES;
+            final int center = mCircleRadius - mTextInset[index];
+            innerBound = center - mSelectorRadius;
+            outerBound = center + mSelectorRadius;
+        }
+
+        final double dX = x - mXCenter;
+        final double dY = y - mYCenter;
+        final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
+        if (distFromCenter < innerBound || constrainOutside && distFromCenter > outerBound) {
+            return -1;
+        }
+
+        // Convert to degrees.
+        final int degrees = (int) (Math.toDegrees(Math.atan2(dY, dX) + Math.PI / 2) + 0.5);
+        if (degrees < 0) {
+            return degrees + 360;
+        } else {
+            return degrees;
+        }
+    }
+
+    private boolean getInnerCircleFromXY(float x, float y) {
+        if (mIs24HourMode && mShowHours) {
+            final double dX = x - mXCenter;
+            final double dY = y - mYCenter;
+            final double distFromCenter = Math.sqrt(dX * dX + dY * dY);
+            return distFromCenter <= mHalfwayDist;
+        }
+        return false;
+    }
+
+    boolean mChangedDuringTouch = false;
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (!mInputEnabled) {
+            return true;
+        }
+
+        final int action = event.getActionMasked();
+        if (action == MotionEvent.ACTION_MOVE
+                || action == MotionEvent.ACTION_UP
+                || action == MotionEvent.ACTION_DOWN) {
+            boolean forceSelection = false;
+            boolean autoAdvance = false;
+
+            if (action == MotionEvent.ACTION_DOWN) {
+                // This is a new event stream, reset whether the value changed.
+                mChangedDuringTouch = false;
+            } else if (action == MotionEvent.ACTION_UP) {
+                autoAdvance = true;
+
+                // If we saw a down/up pair without the value changing, assume
+                // this is a single-tap selection and force a change.
+                if (!mChangedDuringTouch) {
+                    forceSelection = true;
+                }
+            }
+
+            mChangedDuringTouch |= handleTouchInput(
+                    event.getX(), event.getY(), forceSelection, autoAdvance);
+        }
+
+        return true;
+    }
+
+    private boolean handleTouchInput(
+            float x, float y, boolean forceSelection, boolean autoAdvance) {
+        final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
+        final int degrees = getDegreesFromXY(x, y, false);
+        if (degrees == -1) {
+            return false;
+        }
+
+        // Ensure we're showing the correct picker.
+        animatePicker(mShowHours, ANIM_DURATION_TOUCH);
+
+        final @PickerType int type;
+        final int newValue;
+        final boolean valueChanged;
+
+        if (mShowHours) {
+            final int snapDegrees = snapOnly30s(degrees, 0) % 360;
+            valueChanged = mIsOnInnerCircle != isOnInnerCircle
+                    || mSelectionDegrees[HOURS] != snapDegrees;
+            mIsOnInnerCircle = isOnInnerCircle;
+            mSelectionDegrees[HOURS] = snapDegrees;
+            type = HOURS;
+            newValue = getCurrentHour();
+        } else {
+            final int snapDegrees = snapPrefer30s(degrees) % 360;
+            valueChanged = mSelectionDegrees[MINUTES] != snapDegrees;
+            mSelectionDegrees[MINUTES] = snapDegrees;
+            type = MINUTES;
+            newValue = getCurrentMinute();
+        }
+
+        if (valueChanged || forceSelection || autoAdvance) {
+            // Fire the listener even if we just need to auto-advance.
+            if (mListener != null) {
+                mListener.onValueSelected(type, newValue, autoAdvance);
+            }
+
+            // Only provide feedback if the value actually changed.
+            if (valueChanged || forceSelection) {
+                performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+                invalidate();
+            }
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // First right-of-refusal goes the touch exploration helper.
+        if (mTouchHelper.dispatchHoverEvent(event)) {
+            return true;
+        }
+        return super.dispatchHoverEvent(event);
+    }
+
+    public void setInputEnabled(boolean inputEnabled) {
+        mInputEnabled = inputEnabled;
+        invalidate();
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (!isEnabled()) {
+            return null;
+        }
+        final int degrees = getDegreesFromXY(event.getX(), event.getY(), false);
+        if (degrees != -1) {
+            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    private class RadialPickerTouchHelper extends ExploreByTouchHelper {
+        private final Rect mTempRect = new Rect();
+
+        private final int TYPE_HOUR = 1;
+        private final int TYPE_MINUTE = 2;
+
+        private final int SHIFT_TYPE = 0;
+        private final int MASK_TYPE = 0xF;
+
+        private final int SHIFT_VALUE = 8;
+        private final int MASK_VALUE = 0xFF;
+
+        /** Increment in which virtual views are exposed for minutes. */
+        private final int MINUTE_INCREMENT = 5;
+
+        public RadialPickerTouchHelper() {
+            super(RadialTimePickerView.this);
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+        }
+
+        @Override
+        public boolean performAccessibilityAction(View host, int action, Bundle arguments) {
+            if (super.performAccessibilityAction(host, action, arguments)) {
+                return true;
+            }
+
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+                    adjustPicker(1);
+                    return true;
+                case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+                    adjustPicker(-1);
+                    return true;
+            }
+
+            return false;
+        }
+
+        private void adjustPicker(int step) {
+            final int stepSize;
+            final int initialStep;
+            final int maxValue;
+            final int minValue;
+            if (mShowHours) {
+                stepSize = 1;
+
+                final int currentHour24 = getCurrentHour();
+                if (mIs24HourMode) {
+                    initialStep = currentHour24;
+                    minValue = 0;
+                    maxValue = 23;
+                } else {
+                    initialStep = hour24To12(currentHour24);
+                    minValue = 1;
+                    maxValue = 12;
+                }
+            } else {
+                stepSize = 5;
+                initialStep = getCurrentMinute() / stepSize;
+                minValue = 0;
+                maxValue = 55;
+            }
+
+            final int nextValue = (initialStep + step) * stepSize;
+            final int clampedValue = MathUtils.constrain(nextValue, minValue, maxValue);
+            if (mShowHours) {
+                setCurrentHour(clampedValue);
+            } else {
+                setCurrentMinute(clampedValue);
+            }
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            final int id;
+            final int degrees = getDegreesFromXY(x, y, true);
+            if (degrees != -1) {
+                final int snapDegrees = snapOnly30s(degrees, 0) % 360;
+                if (mShowHours) {
+                    final boolean isOnInnerCircle = getInnerCircleFromXY(x, y);
+                    final int hour24 = getHourForDegrees(snapDegrees, isOnInnerCircle);
+                    final int hour = mIs24HourMode ? hour24 : hour24To12(hour24);
+                    id = makeId(TYPE_HOUR, hour);
+                } else {
+                    final int current = getCurrentMinute();
+                    final int touched = getMinuteForDegrees(degrees);
+                    final int snapped = getMinuteForDegrees(snapDegrees);
+
+                    // If the touched minute is closer to the current minute
+                    // than it is to the snapped minute, return current.
+                    final int currentOffset = getCircularDiff(current, touched, MINUTES_IN_CIRCLE);
+                    final int snappedOffset = getCircularDiff(snapped, touched, MINUTES_IN_CIRCLE);
+                    final int minute;
+                    if (currentOffset < snappedOffset) {
+                        minute = current;
+                    } else {
+                        minute = snapped;
+                    }
+                    id = makeId(TYPE_MINUTE, minute);
+                }
+            } else {
+                id = INVALID_ID;
+            }
+
+            return id;
+        }
+
+        /**
+         * Returns the difference in degrees between two values along a circle.
+         *
+         * @param first value in the range [0,max]
+         * @param second value in the range [0,max]
+         * @param max the maximum value along the circle
+         * @return the difference in between the two values
+         */
+        private int getCircularDiff(int first, int second, int max) {
+            final int diff = Math.abs(first - second);
+            final int midpoint = max / 2;
+            return (diff > midpoint) ? (max - diff) : diff;
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+            if (mShowHours) {
+                final int min = mIs24HourMode ? 0 : 1;
+                final int max = mIs24HourMode ? 23 : 12;
+                for (int i = min; i <= max ; i++) {
+                    virtualViewIds.add(makeId(TYPE_HOUR, i));
+                }
+            } else {
+                final int current = getCurrentMinute();
+                for (int i = 0; i < MINUTES_IN_CIRCLE; i += MINUTE_INCREMENT) {
+                    virtualViewIds.add(makeId(TYPE_MINUTE, i));
+
+                    // If the current minute falls between two increments,
+                    // insert an extra node for it.
+                    if (current > i && current < i + MINUTE_INCREMENT) {
+                        virtualViewIds.add(makeId(TYPE_MINUTE, current));
+                    }
+                }
+            }
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            event.setClassName(getClass().getName());
+
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final CharSequence description = getVirtualViewDescription(type, value);
+            event.setContentDescription(description);
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+            node.setClassName(getClass().getName());
+            node.addAction(AccessibilityAction.ACTION_CLICK);
+
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final CharSequence description = getVirtualViewDescription(type, value);
+            node.setContentDescription(description);
+
+            getBoundsForVirtualView(virtualViewId, mTempRect);
+            node.setBoundsInParent(mTempRect);
+
+            final boolean selected = isVirtualViewSelected(type, value);
+            node.setSelected(selected);
+
+            final int nextId = getVirtualViewIdAfter(type, value);
+            if (nextId != INVALID_ID) {
+                node.setTraversalBefore(RadialTimePickerView.this, nextId);
+            }
+        }
+
+        private int getVirtualViewIdAfter(int type, int value) {
+            if (type == TYPE_HOUR) {
+                final int nextValue = value + 1;
+                final int max = mIs24HourMode ? 23 : 12;
+                if (nextValue <= max) {
+                    return makeId(type, nextValue);
+                }
+            } else if (type == TYPE_MINUTE) {
+                final int current = getCurrentMinute();
+                final int snapValue = value - (value % MINUTE_INCREMENT);
+                final int nextValue = snapValue + MINUTE_INCREMENT;
+                if (value < current && nextValue > current) {
+                    // The current value is between two snap values.
+                    return makeId(type, current);
+                } else if (nextValue < MINUTES_IN_CIRCLE) {
+                    return makeId(type, nextValue);
+                }
+            }
+            return INVALID_ID;
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+                Bundle arguments) {
+            if (action == AccessibilityNodeInfo.ACTION_CLICK) {
+                final int type = getTypeFromId(virtualViewId);
+                final int value = getValueFromId(virtualViewId);
+                if (type == TYPE_HOUR) {
+                    final int hour = mIs24HourMode ? value : hour12To24(value, mAmOrPm);
+                    setCurrentHour(hour);
+                    return true;
+                } else if (type == TYPE_MINUTE) {
+                    setCurrentMinute(value);
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        private int hour12To24(int hour12, int amOrPm) {
+            int hour24 = hour12;
+            if (hour12 == 12) {
+                if (amOrPm == AM) {
+                    hour24 = 0;
+                }
+            } else if (amOrPm == PM) {
+                hour24 += 12;
+            }
+            return hour24;
+        }
+
+        private int hour24To12(int hour24) {
+            if (hour24 == 0) {
+                return 12;
+            } else if (hour24 > 12) {
+                return hour24 - 12;
+            } else {
+                return hour24;
+            }
+        }
+
+        private void getBoundsForVirtualView(int virtualViewId, Rect bounds) {
+            final float radius;
+            final int type = getTypeFromId(virtualViewId);
+            final int value = getValueFromId(virtualViewId);
+            final float centerRadius;
+            final float degrees;
+            if (type == TYPE_HOUR) {
+                final boolean innerCircle = getInnerCircleForHour(value);
+                if (innerCircle) {
+                    centerRadius = mCircleRadius - mTextInset[HOURS_INNER];
+                    radius = mSelectorRadius;
+                } else {
+                    centerRadius = mCircleRadius - mTextInset[HOURS];
+                    radius = mSelectorRadius;
+                }
+
+                degrees = getDegreesForHour(value);
+            } else if (type == TYPE_MINUTE) {
+                centerRadius = mCircleRadius - mTextInset[MINUTES];
+                degrees = getDegreesForMinute(value);
+                radius = mSelectorRadius;
+            } else {
+                // This should never happen.
+                centerRadius = 0;
+                degrees = 0;
+                radius = 0;
+            }
+
+            final double radians = Math.toRadians(degrees);
+            final float xCenter = mXCenter + centerRadius * (float) Math.sin(radians);
+            final float yCenter = mYCenter - centerRadius * (float) Math.cos(radians);
+
+            bounds.set((int) (xCenter - radius), (int) (yCenter - radius),
+                    (int) (xCenter + radius), (int) (yCenter + radius));
+        }
+
+        private CharSequence getVirtualViewDescription(int type, int value) {
+            final CharSequence description;
+            if (type == TYPE_HOUR || type == TYPE_MINUTE) {
+                description = Integer.toString(value);
+            } else {
+                description = null;
+            }
+            return description;
+        }
+
+        private boolean isVirtualViewSelected(int type, int value) {
+            final boolean selected;
+            if (type == TYPE_HOUR) {
+                selected = getCurrentHour() == value;
+            } else if (type == TYPE_MINUTE) {
+                selected = getCurrentMinute() == value;
+            } else {
+                selected = false;
+            }
+            return selected;
+        }
+
+        private int makeId(int type, int value) {
+            return type << SHIFT_TYPE | value << SHIFT_VALUE;
+        }
+
+        private int getTypeFromId(int id) {
+            return id >>> SHIFT_TYPE & MASK_TYPE;
+        }
+
+        private int getValueFromId(int id) {
+            return id >>> SHIFT_VALUE & MASK_VALUE;
+        }
+    }
+}
diff --git a/android/widget/RadioButton.java b/android/widget/RadioButton.java
new file mode 100644
index 0000000..d44fbd7
--- /dev/null
+++ b/android/widget/RadioButton.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+
+
+/**
+ * <p>
+ * A radio button is a two-states button that can be either checked or
+ * unchecked. When the radio button is unchecked, the user can press or click it
+ * to check it. However, contrary to a {@link android.widget.CheckBox}, a radio
+ * button cannot be unchecked by the user once checked.
+ * </p>
+ *
+ * <p>
+ * Radio buttons are normally used together in a
+ * {@link android.widget.RadioGroup}. When several radio buttons live inside
+ * a radio group, checking one radio button unchecks all the others.</p>
+ * </p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/radiobutton.html">Radio Buttons</a>
+ * guide.</p>
+ *
+ * <p><strong>XML attributes</strong></p>
+ * <p> 
+ * See {@link android.R.styleable#CompoundButton CompoundButton Attributes}, 
+ * {@link android.R.styleable#Button Button Attributes}, 
+ * {@link android.R.styleable#TextView TextView Attributes}, 
+ * {@link android.R.styleable#View View Attributes}
+ * </p>
+ */
+public class RadioButton extends CompoundButton {
+    
+    public RadioButton(Context context) {
+        this(context, null);
+    }
+    
+    public RadioButton(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.radioButtonStyle);
+    }
+
+    public RadioButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RadioButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    /**
+     * {@inheritDoc}
+     * <p>
+     * If the radio button is already checked, this method will not toggle the radio button.
+     */
+    @Override
+    public void toggle() {
+        // we override to prevent toggle when the radio is already
+        // checked (as opposed to check boxes widgets)
+        if (!isChecked()) {
+            super.toggle();
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return RadioButton.class.getName();
+    }
+}
diff --git a/android/widget/RadioGroup.java b/android/widget/RadioGroup.java
new file mode 100644
index 0000000..5c4d4d2
--- /dev/null
+++ b/android/widget/RadioGroup.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.IdRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewStructure;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+
+/**
+ * <p>This class is used to create a multiple-exclusion scope for a set of radio
+ * buttons. Checking one radio button that belongs to a radio group unchecks
+ * any previously checked radio button within the same group.</p>
+ *
+ * <p>Intially, all of the radio buttons are unchecked. While it is not possible
+ * to uncheck a particular radio button, the radio group can be cleared to
+ * remove the checked state.</p>
+ *
+ * <p>The selection is identified by the unique id of the radio button as defined
+ * in the XML layout file.</p>
+ *
+ * <p><strong>XML Attributes</strong></p>
+ * <p>See {@link android.R.styleable#RadioGroup RadioGroup Attributes},
+ * {@link android.R.styleable#LinearLayout LinearLayout Attributes},
+ * {@link android.R.styleable#ViewGroup ViewGroup Attributes},
+ * {@link android.R.styleable#View View Attributes}</p>
+ * <p>Also see
+ * {@link android.widget.LinearLayout.LayoutParams LinearLayout.LayoutParams}
+ * for layout attributes.</p>
+ *
+ * @see RadioButton
+ *
+ */
+public class RadioGroup extends LinearLayout {
+    private static final String LOG_TAG = RadioGroup.class.getSimpleName();
+
+    // holds the checked id; the selection is empty by default
+    private int mCheckedId = -1;
+    // tracks children radio buttons checked state
+    private CompoundButton.OnCheckedChangeListener mChildOnCheckedChangeListener;
+    // when true, mOnCheckedChangeListener discards events
+    private boolean mProtectFromCheckedChange = false;
+    private OnCheckedChangeListener mOnCheckedChangeListener;
+    private PassThroughHierarchyChangeListener mPassThroughListener;
+
+    // Indicates whether the child was set from resources or dynamically, so it can be used
+    // to sanitize autofill requests.
+    private int mInitialCheckedId = View.NO_ID;
+
+    /**
+     * {@inheritDoc}
+     */
+    public RadioGroup(Context context) {
+        super(context);
+        setOrientation(VERTICAL);
+        init();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public RadioGroup(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        // RadioGroup is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        // retrieve selected radio button as requested by the user in the
+        // XML layout file
+        TypedArray attributes = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.RadioGroup, com.android.internal.R.attr.radioButtonStyle, 0);
+
+        int value = attributes.getResourceId(R.styleable.RadioGroup_checkedButton, View.NO_ID);
+        if (value != View.NO_ID) {
+            mCheckedId = value;
+            mInitialCheckedId = value;
+        }
+        final int index = attributes.getInt(com.android.internal.R.styleable.RadioGroup_orientation, VERTICAL);
+        setOrientation(index);
+
+        attributes.recycle();
+        init();
+    }
+
+    private void init() {
+        mChildOnCheckedChangeListener = new CheckedStateTracker();
+        mPassThroughListener = new PassThroughHierarchyChangeListener();
+        super.setOnHierarchyChangeListener(mPassThroughListener);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+        // the user listener is delegated to our pass-through listener
+        mPassThroughListener.mOnHierarchyChangeListener = listener;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        // checks the appropriate radio button as requested in the XML file
+        if (mCheckedId != -1) {
+            mProtectFromCheckedChange = true;
+            setCheckedStateForView(mCheckedId, true);
+            mProtectFromCheckedChange = false;
+            setCheckedId(mCheckedId);
+        }
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (child instanceof RadioButton) {
+            final RadioButton button = (RadioButton) child;
+            if (button.isChecked()) {
+                mProtectFromCheckedChange = true;
+                if (mCheckedId != -1) {
+                    setCheckedStateForView(mCheckedId, false);
+                }
+                mProtectFromCheckedChange = false;
+                setCheckedId(button.getId());
+            }
+        }
+
+        super.addView(child, index, params);
+    }
+
+    /**
+     * <p>Sets the selection to the radio button whose identifier is passed in
+     * parameter. Using -1 as the selection identifier clears the selection;
+     * such an operation is equivalent to invoking {@link #clearCheck()}.</p>
+     *
+     * @param id the unique id of the radio button to select in this group
+     *
+     * @see #getCheckedRadioButtonId()
+     * @see #clearCheck()
+     */
+    public void check(@IdRes int id) {
+        // don't even bother
+        if (id != -1 && (id == mCheckedId)) {
+            return;
+        }
+
+        if (mCheckedId != -1) {
+            setCheckedStateForView(mCheckedId, false);
+        }
+
+        if (id != -1) {
+            setCheckedStateForView(id, true);
+        }
+
+        setCheckedId(id);
+    }
+
+    private void setCheckedId(@IdRes int id) {
+        mCheckedId = id;
+        if (mOnCheckedChangeListener != null) {
+            mOnCheckedChangeListener.onCheckedChanged(this, mCheckedId);
+        }
+        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+        if (afm != null) {
+            afm.notifyValueChanged(this);
+        }
+    }
+
+    private void setCheckedStateForView(int viewId, boolean checked) {
+        View checkedView = findViewById(viewId);
+        if (checkedView != null && checkedView instanceof RadioButton) {
+            ((RadioButton) checkedView).setChecked(checked);
+        }
+    }
+
+    /**
+     * <p>Returns the identifier of the selected radio button in this group.
+     * Upon empty selection, the returned value is -1.</p>
+     *
+     * @return the unique id of the selected radio button in this group
+     *
+     * @see #check(int)
+     * @see #clearCheck()
+     *
+     * @attr ref android.R.styleable#RadioGroup_checkedButton
+     */
+    @IdRes
+    public int getCheckedRadioButtonId() {
+        return mCheckedId;
+    }
+
+    /**
+     * <p>Clears the selection. When the selection is cleared, no radio button
+     * in this group is selected and {@link #getCheckedRadioButtonId()} returns
+     * null.</p>
+     *
+     * @see #check(int)
+     * @see #getCheckedRadioButtonId()
+     */
+    public void clearCheck() {
+        check(-1);
+    }
+
+    /**
+     * <p>Register a callback to be invoked when the checked radio button
+     * changes in this group.</p>
+     *
+     * @param listener the callback to call on checked state change
+     */
+    public void setOnCheckedChangeListener(OnCheckedChangeListener listener) {
+        mOnCheckedChangeListener = listener;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new RadioGroup.LayoutParams(getContext(), attrs);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof RadioGroup.LayoutParams;
+    }
+
+    @Override
+    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return RadioGroup.class.getName();
+    }
+
+    /**
+     * <p>This set of layout parameters defaults the width and the height of
+     * the children to {@link #WRAP_CONTENT} when they are not specified in the
+     * XML file. Otherwise, this class ussed the value read from the XML file.</p>
+     *
+     * <p>See
+     * {@link android.R.styleable#LinearLayout_Layout LinearLayout Attributes}
+     * for a list of all child view attributes that this class supports.</p>
+     *
+     */
+    public static class LayoutParams extends LinearLayout.LayoutParams {
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int w, int h) {
+            super(w, h);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int w, int h, float initWeight) {
+            super(w, h, initWeight);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams p) {
+            super(p);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * <p>Fixes the child's width to
+         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and the child's
+         * height to  {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}
+         * when not specified in the XML file.</p>
+         *
+         * @param a the styled attributes set
+         * @param widthAttr the width attribute to fetch
+         * @param heightAttr the height attribute to fetch
+         */
+        @Override
+        protected void setBaseAttributes(TypedArray a,
+                int widthAttr, int heightAttr) {
+
+            if (a.hasValue(widthAttr)) {
+                width = a.getLayoutDimension(widthAttr, "layout_width");
+            } else {
+                width = WRAP_CONTENT;
+            }
+
+            if (a.hasValue(heightAttr)) {
+                height = a.getLayoutDimension(heightAttr, "layout_height");
+            } else {
+                height = WRAP_CONTENT;
+            }
+        }
+    }
+
+    /**
+     * <p>Interface definition for a callback to be invoked when the checked
+     * radio button changed in this group.</p>
+     */
+    public interface OnCheckedChangeListener {
+        /**
+         * <p>Called when the checked radio button has changed. When the
+         * selection is cleared, checkedId is -1.</p>
+         *
+         * @param group the group in which the checked radio button has changed
+         * @param checkedId the unique identifier of the newly checked radio button
+         */
+        public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
+    }
+
+    private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
+        @Override
+        public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) {
+            // prevents from infinite recursion
+            if (mProtectFromCheckedChange) {
+                return;
+            }
+
+            mProtectFromCheckedChange = true;
+            if (mCheckedId != -1) {
+                setCheckedStateForView(mCheckedId, false);
+            }
+            mProtectFromCheckedChange = false;
+
+            int id = buttonView.getId();
+            setCheckedId(id);
+        }
+    }
+
+    /**
+     * <p>A pass-through listener acts upon the events and dispatches them
+     * to another listener. This allows the table layout to set its own internal
+     * hierarchy change listener without preventing the user to setup his.</p>
+     */
+    private class PassThroughHierarchyChangeListener implements
+            ViewGroup.OnHierarchyChangeListener {
+        private ViewGroup.OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void onChildViewAdded(View parent, View child) {
+            if (parent == RadioGroup.this && child instanceof RadioButton) {
+                int id = child.getId();
+                // generates an id if it's missing
+                if (id == View.NO_ID) {
+                    id = View.generateViewId();
+                    child.setId(id);
+                }
+                ((RadioButton) child).setOnCheckedChangeWidgetListener(
+                        mChildOnCheckedChangeListener);
+            }
+
+            if (mOnHierarchyChangeListener != null) {
+                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        @Override
+        public void onChildViewRemoved(View parent, View child) {
+            if (parent == RadioGroup.this && child instanceof RadioButton) {
+                ((RadioButton) child).setOnCheckedChangeWidgetListener(null);
+            }
+
+            if (mOnHierarchyChangeListener != null) {
+                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
+            }
+        }
+    }
+
+    @Override
+    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+        super.onProvideAutofillStructure(structure, flags);
+        structure.setDataIsSensitive(mCheckedId != mInitialCheckedId);
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!isEnabled()) return;
+
+        if (!value.isList()) {
+            Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+            return;
+        }
+
+        final int index = value.getListValue();
+        final View child = getChildAt(index);
+        if (child == null) {
+            Log.w(VIEW_LOG_TAG, "RadioGroup.autoFill(): no child with index " + index);
+            return;
+        }
+
+        check(child.getId());
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isEnabled() ? AUTOFILL_TYPE_LIST : AUTOFILL_TYPE_NONE;
+    }
+
+    @Override
+    public AutofillValue getAutofillValue() {
+        if (!isEnabled()) return null;
+
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getId() == mCheckedId) {
+                return AutofillValue.forList(i);
+            }
+        }
+        return null;
+    }
+}
diff --git a/android/widget/RatingBar.java b/android/widget/RatingBar.java
new file mode 100644
index 0000000..70b70bc
--- /dev/null
+++ b/android/widget/RatingBar.java
@@ -0,0 +1,354 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.shapes.RectShape;
+import android.graphics.drawable.shapes.Shape;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * A RatingBar is an extension of SeekBar and ProgressBar that shows a rating in
+ * stars. The user can touch/drag or use arrow keys to set the rating when using
+ * the default size RatingBar. The smaller RatingBar style (
+ * {@link android.R.attr#ratingBarStyleSmall}) and the larger indicator-only
+ * style ({@link android.R.attr#ratingBarStyleIndicator}) do not support user
+ * interaction and should only be used as indicators.
+ * <p>
+ * When using a RatingBar that supports user interaction, placing widgets to the
+ * left or right of the RatingBar is discouraged.
+ * <p>
+ * The number of stars set (via {@link #setNumStars(int)} or in an XML layout)
+ * will be shown when the layout width is set to wrap content (if another layout
+ * width is set, the results may be unpredictable).
+ * <p>
+ * The secondary progress should not be modified by the client as it is used
+ * internally as the background for a fractionally filled star.
+ *
+ * @attr ref android.R.styleable#RatingBar_numStars
+ * @attr ref android.R.styleable#RatingBar_rating
+ * @attr ref android.R.styleable#RatingBar_stepSize
+ * @attr ref android.R.styleable#RatingBar_isIndicator
+ */
+public class RatingBar extends AbsSeekBar {
+
+    /**
+     * A callback that notifies clients when the rating has been changed. This
+     * includes changes that were initiated by the user through a touch gesture
+     * or arrow key/trackball as well as changes that were initiated
+     * programmatically.
+     */
+    public interface OnRatingBarChangeListener {
+
+        /**
+         * Notification that the rating has changed. Clients can use the
+         * fromUser parameter to distinguish user-initiated changes from those
+         * that occurred programmatically. This will not be called continuously
+         * while the user is dragging, only when the user finalizes a rating by
+         * lifting the touch.
+         *
+         * @param ratingBar The RatingBar whose rating has changed.
+         * @param rating The current rating. This will be in the range
+         *            0..numStars.
+         * @param fromUser True if the rating change was initiated by a user's
+         *            touch gesture or arrow key/horizontal trackbell movement.
+         */
+        void onRatingChanged(RatingBar ratingBar, float rating, boolean fromUser);
+
+    }
+
+    private int mNumStars = 5;
+
+    private int mProgressOnStartTracking;
+
+    private OnRatingBarChangeListener mOnRatingBarChangeListener;
+
+    public RatingBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RatingBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.RatingBar, defStyleAttr, defStyleRes);
+        final int numStars = a.getInt(R.styleable.RatingBar_numStars, mNumStars);
+        setIsIndicator(a.getBoolean(R.styleable.RatingBar_isIndicator, !mIsUserSeekable));
+        final float rating = a.getFloat(R.styleable.RatingBar_rating, -1);
+        final float stepSize = a.getFloat(R.styleable.RatingBar_stepSize, -1);
+        a.recycle();
+
+        if (numStars > 0 && numStars != mNumStars) {
+            setNumStars(numStars);
+        }
+
+        if (stepSize >= 0) {
+            setStepSize(stepSize);
+        } else {
+            setStepSize(0.5f);
+        }
+
+        if (rating >= 0) {
+            setRating(rating);
+        }
+
+        // A touch inside a star fill up to that fractional area (slightly more
+        // than 0.5 so boundaries round up).
+        mTouchProgressOffset = 0.6f;
+    }
+
+    public RatingBar(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.ratingBarStyle);
+    }
+
+    public RatingBar(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Sets the listener to be called when the rating changes.
+     *
+     * @param listener The listener.
+     */
+    public void setOnRatingBarChangeListener(OnRatingBarChangeListener listener) {
+        mOnRatingBarChangeListener = listener;
+    }
+
+    /**
+     * @return The listener (may be null) that is listening for rating change
+     *         events.
+     */
+    public OnRatingBarChangeListener getOnRatingBarChangeListener() {
+        return mOnRatingBarChangeListener;
+    }
+
+    /**
+     * Whether this rating bar should only be an indicator (thus non-changeable
+     * by the user).
+     *
+     * @param isIndicator Whether it should be an indicator.
+     *
+     * @attr ref android.R.styleable#RatingBar_isIndicator
+     */
+    public void setIsIndicator(boolean isIndicator) {
+        mIsUserSeekable = !isIndicator;
+        if (isIndicator) {
+            setFocusable(FOCUSABLE_AUTO);
+        } else {
+            setFocusable(FOCUSABLE);
+        }
+    }
+
+    /**
+     * @return Whether this rating bar is only an indicator.
+     *
+     * @attr ref android.R.styleable#RatingBar_isIndicator
+     */
+    public boolean isIndicator() {
+        return !mIsUserSeekable;
+    }
+
+    /**
+     * Sets the number of stars to show. In order for these to be shown
+     * properly, it is recommended the layout width of this widget be wrap
+     * content.
+     *
+     * @param numStars The number of stars.
+     */
+    public void setNumStars(final int numStars) {
+        if (numStars <= 0) {
+            return;
+        }
+
+        mNumStars = numStars;
+
+        // This causes the width to change, so re-layout
+        requestLayout();
+    }
+
+    /**
+     * Returns the number of stars shown.
+     * @return The number of stars shown.
+     */
+    public int getNumStars() {
+        return mNumStars;
+    }
+
+    /**
+     * Sets the rating (the number of stars filled).
+     *
+     * @param rating The rating to set.
+     */
+    public void setRating(float rating) {
+        setProgress(Math.round(rating * getProgressPerStar()));
+    }
+
+    /**
+     * Gets the current rating (number of stars filled).
+     *
+     * @return The current rating.
+     */
+    public float getRating() {
+        return getProgress() / getProgressPerStar();
+    }
+
+    /**
+     * Sets the step size (granularity) of this rating bar.
+     *
+     * @param stepSize The step size of this rating bar. For example, if
+     *            half-star granularity is wanted, this would be 0.5.
+     */
+    public void setStepSize(float stepSize) {
+        if (stepSize <= 0) {
+            return;
+        }
+
+        final float newMax = mNumStars / stepSize;
+        final int newProgress = (int) (newMax / getMax() * getProgress());
+        setMax((int) newMax);
+        setProgress(newProgress);
+    }
+
+    /**
+     * Gets the step size of this rating bar.
+     *
+     * @return The step size.
+     */
+    public float getStepSize() {
+        return (float) getNumStars() / getMax();
+    }
+
+    /**
+     * @return The amount of progress that fits into a star
+     */
+    private float getProgressPerStar() {
+        if (mNumStars > 0) {
+            return 1f * getMax() / mNumStars;
+        } else {
+            return 1;
+        }
+    }
+
+    @Override
+    Shape getDrawableShape() {
+        // TODO: Once ProgressBar's TODOs are fixed, this won't be needed
+        return new RectShape();
+    }
+
+    @Override
+    void onProgressRefresh(float scale, boolean fromUser, int progress) {
+        super.onProgressRefresh(scale, fromUser, progress);
+
+        // Keep secondary progress in sync with primary
+        updateSecondaryProgress(progress);
+
+        if (!fromUser) {
+            // Callback for non-user rating changes
+            dispatchRatingChange(false);
+        }
+    }
+
+    /**
+     * The secondary progress is used to differentiate the background of a
+     * partially filled star. This method keeps the secondary progress in sync
+     * with the progress.
+     *
+     * @param progress The primary progress level.
+     */
+    private void updateSecondaryProgress(int progress) {
+        final float ratio = getProgressPerStar();
+        if (ratio > 0) {
+            final float progressInStars = progress / ratio;
+            final int secondaryProgress = (int) (Math.ceil(progressInStars) * ratio);
+            setSecondaryProgress(secondaryProgress);
+        }
+    }
+
+    @Override
+    protected synchronized void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (mSampleWidth > 0) {
+            final int width = mSampleWidth * mNumStars;
+            setMeasuredDimension(resolveSizeAndState(width, widthMeasureSpec, 0),
+                    getMeasuredHeight());
+        }
+    }
+
+    @Override
+    void onStartTrackingTouch() {
+        mProgressOnStartTracking = getProgress();
+
+        super.onStartTrackingTouch();
+    }
+
+    @Override
+    void onStopTrackingTouch() {
+        super.onStopTrackingTouch();
+
+        if (getProgress() != mProgressOnStartTracking) {
+            dispatchRatingChange(true);
+        }
+    }
+
+    @Override
+    void onKeyChange() {
+        super.onKeyChange();
+        dispatchRatingChange(true);
+    }
+
+    void dispatchRatingChange(boolean fromUser) {
+        if (mOnRatingBarChangeListener != null) {
+            mOnRatingBarChangeListener.onRatingChanged(this, getRating(),
+                    fromUser);
+        }
+    }
+
+    @Override
+    public synchronized void setMax(int max) {
+        // Disallow max progress = 0
+        if (max <= 0) {
+            return;
+        }
+
+        super.setMax(max);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return RatingBar.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (canUserSetProgress()) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
+        }
+    }
+
+    @Override
+    boolean canUserSetProgress() {
+        return super.canUserSetProgress() && !isIndicator();
+    }
+}
diff --git a/android/widget/RelativeLayout.java b/android/widget/RelativeLayout.java
new file mode 100644
index 0000000..75fc538
--- /dev/null
+++ b/android/widget/RelativeLayout.java
@@ -0,0 +1,1909 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import static android.os.Build.VERSION_CODES.JELLY_BEAN_MR1;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Rect;
+import android.os.Build;
+import android.util.ArrayMap;
+import android.util.AttributeSet;
+import android.util.Pools.SynchronizedPool;
+import android.util.SparseArray;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.accessibility.AccessibilityEvent;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.R;
+
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/**
+ * A Layout where the positions of the children can be described in relation to each other or to the
+ * parent.
+ *
+ * <p>
+ * Note that you cannot have a circular dependency between the size of the RelativeLayout and the
+ * position of its children. For example, you cannot have a RelativeLayout whose height is set to
+ * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT WRAP_CONTENT} and a child set to
+ * {@link #ALIGN_PARENT_BOTTOM}.
+ * </p>
+ *
+ * <p><strong>Note:</strong> In platform version 17 and lower, RelativeLayout was affected by
+ * a measurement bug that could cause child views to be measured with incorrect
+ * {@link android.view.View.MeasureSpec MeasureSpec} values. (See
+ * {@link android.view.View.MeasureSpec#makeMeasureSpec(int, int) MeasureSpec.makeMeasureSpec}
+ * for more details.) This was triggered when a RelativeLayout container was placed in
+ * a scrolling container, such as a ScrollView or HorizontalScrollView. If a custom view
+ * not equipped to properly measure with the MeasureSpec mode
+ * {@link android.view.View.MeasureSpec#UNSPECIFIED UNSPECIFIED} was placed in a RelativeLayout,
+ * this would silently work anyway as RelativeLayout would pass a very large
+ * {@link android.view.View.MeasureSpec#AT_MOST AT_MOST} MeasureSpec instead.</p>
+ *
+ * <p>This behavior has been preserved for apps that set <code>android:targetSdkVersion="17"</code>
+ * or older in their manifest's <code>uses-sdk</code> tag for compatibility. Apps targeting SDK
+ * version 18 or newer will receive the correct behavior</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/layout/relative.html">Relative
+ * Layout</a> guide.</p>
+ *
+ * <p>
+ * Also see {@link android.widget.RelativeLayout.LayoutParams RelativeLayout.LayoutParams} for
+ * layout attributes
+ * </p>
+ *
+ * @attr ref android.R.styleable#RelativeLayout_gravity
+ * @attr ref android.R.styleable#RelativeLayout_ignoreGravity
+ */
+@RemoteView
+public class RelativeLayout extends ViewGroup {
+    public static final int TRUE = -1;
+
+    /**
+     * Rule that aligns a child's right edge with another child's left edge.
+     */
+    public static final int LEFT_OF                  = 0;
+    /**
+     * Rule that aligns a child's left edge with another child's right edge.
+     */
+    public static final int RIGHT_OF                 = 1;
+    /**
+     * Rule that aligns a child's bottom edge with another child's top edge.
+     */
+    public static final int ABOVE                    = 2;
+    /**
+     * Rule that aligns a child's top edge with another child's bottom edge.
+     */
+    public static final int BELOW                    = 3;
+
+    /**
+     * Rule that aligns a child's baseline with another child's baseline.
+     */
+    public static final int ALIGN_BASELINE           = 4;
+    /**
+     * Rule that aligns a child's left edge with another child's left edge.
+     */
+    public static final int ALIGN_LEFT               = 5;
+    /**
+     * Rule that aligns a child's top edge with another child's top edge.
+     */
+    public static final int ALIGN_TOP                = 6;
+    /**
+     * Rule that aligns a child's right edge with another child's right edge.
+     */
+    public static final int ALIGN_RIGHT              = 7;
+    /**
+     * Rule that aligns a child's bottom edge with another child's bottom edge.
+     */
+    public static final int ALIGN_BOTTOM             = 8;
+
+    /**
+     * Rule that aligns the child's left edge with its RelativeLayout
+     * parent's left edge.
+     */
+    public static final int ALIGN_PARENT_LEFT        = 9;
+    /**
+     * Rule that aligns the child's top edge with its RelativeLayout
+     * parent's top edge.
+     */
+    public static final int ALIGN_PARENT_TOP         = 10;
+    /**
+     * Rule that aligns the child's right edge with its RelativeLayout
+     * parent's right edge.
+     */
+    public static final int ALIGN_PARENT_RIGHT       = 11;
+    /**
+     * Rule that aligns the child's bottom edge with its RelativeLayout
+     * parent's bottom edge.
+     */
+    public static final int ALIGN_PARENT_BOTTOM      = 12;
+
+    /**
+     * Rule that centers the child with respect to the bounds of its
+     * RelativeLayout parent.
+     */
+    public static final int CENTER_IN_PARENT         = 13;
+    /**
+     * Rule that centers the child horizontally with respect to the
+     * bounds of its RelativeLayout parent.
+     */
+    public static final int CENTER_HORIZONTAL        = 14;
+    /**
+     * Rule that centers the child vertically with respect to the
+     * bounds of its RelativeLayout parent.
+     */
+    public static final int CENTER_VERTICAL          = 15;
+    /**
+     * Rule that aligns a child's end edge with another child's start edge.
+     */
+    public static final int START_OF                 = 16;
+    /**
+     * Rule that aligns a child's start edge with another child's end edge.
+     */
+    public static final int END_OF                   = 17;
+    /**
+     * Rule that aligns a child's start edge with another child's start edge.
+     */
+    public static final int ALIGN_START              = 18;
+    /**
+     * Rule that aligns a child's end edge with another child's end edge.
+     */
+    public static final int ALIGN_END                = 19;
+    /**
+     * Rule that aligns the child's start edge with its RelativeLayout
+     * parent's start edge.
+     */
+    public static final int ALIGN_PARENT_START       = 20;
+    /**
+     * Rule that aligns the child's end edge with its RelativeLayout
+     * parent's end edge.
+     */
+    public static final int ALIGN_PARENT_END         = 21;
+
+    private static final int VERB_COUNT              = 22;
+
+
+    private static final int[] RULES_VERTICAL = {
+            ABOVE, BELOW, ALIGN_BASELINE, ALIGN_TOP, ALIGN_BOTTOM
+    };
+
+    private static final int[] RULES_HORIZONTAL = {
+            LEFT_OF, RIGHT_OF, ALIGN_LEFT, ALIGN_RIGHT, START_OF, END_OF, ALIGN_START, ALIGN_END
+    };
+
+    /**
+     * Used to indicate left/right/top/bottom should be inferred from constraints
+     */
+    private static final int VALUE_NOT_SET = Integer.MIN_VALUE;
+
+    private View mBaselineView = null;
+
+    private int mGravity = Gravity.START | Gravity.TOP;
+    private final Rect mContentBounds = new Rect();
+    private final Rect mSelfBounds = new Rect();
+    private int mIgnoreGravity;
+
+    private SortedSet<View> mTopToBottomLeftToRightSet = null;
+
+    private boolean mDirtyHierarchy;
+    private View[] mSortedHorizontalChildren;
+    private View[] mSortedVerticalChildren;
+    private final DependencyGraph mGraph = new DependencyGraph();
+
+    // Compatibility hack. Old versions of the platform had problems
+    // with MeasureSpec value overflow and RelativeLayout was one source of them.
+    // Some apps came to rely on them. :(
+    private boolean mAllowBrokenMeasureSpecs = false;
+    // Compatibility hack. Old versions of the platform would not take
+    // margins and padding into account when generating the height measure spec
+    // for children during the horizontal measure pass.
+    private boolean mMeasureVerticalWithPaddingMargin = false;
+
+    // A default width used for RTL measure pass
+    /**
+     * Value reduced so as not to interfere with View's measurement spec. flags. See:
+     * {@link View#MEASURED_SIZE_MASK}.
+     * {@link View#MEASURED_STATE_TOO_SMALL}.
+     **/
+    private static final int DEFAULT_WIDTH = 0x00010000;
+
+    public RelativeLayout(Context context) {
+        this(context, null);
+    }
+
+    public RelativeLayout(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public RelativeLayout(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initFromAttributes(context, attrs, defStyleAttr, defStyleRes);
+        queryCompatibilityModes(context);
+    }
+
+    private void initFromAttributes(
+            Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.RelativeLayout, defStyleAttr, defStyleRes);
+        mIgnoreGravity = a.getResourceId(R.styleable.RelativeLayout_ignoreGravity, View.NO_ID);
+        mGravity = a.getInt(R.styleable.RelativeLayout_gravity, mGravity);
+        a.recycle();
+    }
+
+    private void queryCompatibilityModes(Context context) {
+        int version = context.getApplicationInfo().targetSdkVersion;
+        mAllowBrokenMeasureSpecs = version <= Build.VERSION_CODES.JELLY_BEAN_MR1;
+        mMeasureVerticalWithPaddingMargin = version >= Build.VERSION_CODES.JELLY_BEAN_MR2;
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return false;
+    }
+
+    /**
+     * Defines which View is ignored when the gravity is applied. This setting has no
+     * effect if the gravity is <code>Gravity.START | Gravity.TOP</code>.
+     *
+     * @param viewId The id of the View to be ignored by gravity, or 0 if no View
+     *        should be ignored.
+     *
+     * @see #setGravity(int)
+     *
+     * @attr ref android.R.styleable#RelativeLayout_ignoreGravity
+     */
+    @android.view.RemotableViewMethod
+    public void setIgnoreGravity(int viewId) {
+        mIgnoreGravity = viewId;
+    }
+
+    /**
+     * Describes how the child views are positioned.
+     *
+     * @return the gravity.
+     *
+     * @see #setGravity(int)
+     * @see android.view.Gravity
+     *
+     * @attr ref android.R.styleable#RelativeLayout_gravity
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    /**
+     * Describes how the child views are positioned. Defaults to
+     * <code>Gravity.START | Gravity.TOP</code>.
+     *
+     * <p>Note that since RelativeLayout considers the positioning of each child
+     * relative to one another to be significant, setting gravity will affect
+     * the positioning of all children as a single unit within the parent.
+     * This happens after children have been relatively positioned.</p>
+     *
+     * @param gravity See {@link android.view.Gravity}
+     *
+     * @see #setHorizontalGravity(int)
+     * @see #setVerticalGravity(int)
+     *
+     * @attr ref android.R.styleable#RelativeLayout_gravity
+     */
+    @android.view.RemotableViewMethod
+    public void setGravity(int gravity) {
+        if (mGravity != gravity) {
+            if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+                gravity |= Gravity.START;
+            }
+
+            if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+                gravity |= Gravity.TOP;
+            }
+
+            mGravity = gravity;
+            requestLayout();
+        }
+    }
+
+    @android.view.RemotableViewMethod
+    public void setHorizontalGravity(int horizontalGravity) {
+        final int gravity = horizontalGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+        if ((mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) != gravity) {
+            mGravity = (mGravity & ~Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) | gravity;
+            requestLayout();
+        }
+    }
+
+    @android.view.RemotableViewMethod
+    public void setVerticalGravity(int verticalGravity) {
+        final int gravity = verticalGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != gravity) {
+            mGravity = (mGravity & ~Gravity.VERTICAL_GRAVITY_MASK) | gravity;
+            requestLayout();
+        }
+    }
+
+    @Override
+    public int getBaseline() {
+        return mBaselineView != null ? mBaselineView.getBaseline() : super.getBaseline();
+    }
+
+    @Override
+    public void requestLayout() {
+        super.requestLayout();
+        mDirtyHierarchy = true;
+    }
+
+    private void sortChildren() {
+        final int count = getChildCount();
+        if (mSortedVerticalChildren == null || mSortedVerticalChildren.length != count) {
+            mSortedVerticalChildren = new View[count];
+        }
+
+        if (mSortedHorizontalChildren == null || mSortedHorizontalChildren.length != count) {
+            mSortedHorizontalChildren = new View[count];
+        }
+
+        final DependencyGraph graph = mGraph;
+        graph.clear();
+
+        for (int i = 0; i < count; i++) {
+            graph.add(getChildAt(i));
+        }
+
+        graph.getSortedViews(mSortedVerticalChildren, RULES_VERTICAL);
+        graph.getSortedViews(mSortedHorizontalChildren, RULES_HORIZONTAL);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mDirtyHierarchy) {
+            mDirtyHierarchy = false;
+            sortChildren();
+        }
+
+        int myWidth = -1;
+        int myHeight = -1;
+
+        int width = 0;
+        int height = 0;
+
+        final int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        final int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        final int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        // Record our dimensions if they are known;
+        if (widthMode != MeasureSpec.UNSPECIFIED) {
+            myWidth = widthSize;
+        }
+
+        if (heightMode != MeasureSpec.UNSPECIFIED) {
+            myHeight = heightSize;
+        }
+
+        if (widthMode == MeasureSpec.EXACTLY) {
+            width = myWidth;
+        }
+
+        if (heightMode == MeasureSpec.EXACTLY) {
+            height = myHeight;
+        }
+
+        View ignore = null;
+        int gravity = mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK;
+        final boolean horizontalGravity = gravity != Gravity.START && gravity != 0;
+        gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        final boolean verticalGravity = gravity != Gravity.TOP && gravity != 0;
+
+        int left = Integer.MAX_VALUE;
+        int top = Integer.MAX_VALUE;
+        int right = Integer.MIN_VALUE;
+        int bottom = Integer.MIN_VALUE;
+
+        boolean offsetHorizontalAxis = false;
+        boolean offsetVerticalAxis = false;
+
+        if ((horizontalGravity || verticalGravity) && mIgnoreGravity != View.NO_ID) {
+            ignore = findViewById(mIgnoreGravity);
+        }
+
+        final boolean isWrapContentWidth = widthMode != MeasureSpec.EXACTLY;
+        final boolean isWrapContentHeight = heightMode != MeasureSpec.EXACTLY;
+
+        // We need to know our size for doing the correct computation of children positioning in RTL
+        // mode but there is no practical way to get it instead of running the code below.
+        // So, instead of running the code twice, we just set the width to a "default display width"
+        // before the computation and then, as a last pass, we will update their real position with
+        // an offset equals to "DEFAULT_WIDTH - width".
+        final int layoutDirection = getLayoutDirection();
+        if (isLayoutRtl() && myWidth == -1) {
+            myWidth = DEFAULT_WIDTH;
+        }
+
+        View[] views = mSortedHorizontalChildren;
+        int count = views.length;
+
+        for (int i = 0; i < count; i++) {
+            View child = views[i];
+            if (child.getVisibility() != GONE) {
+                LayoutParams params = (LayoutParams) child.getLayoutParams();
+                int[] rules = params.getRules(layoutDirection);
+
+                applyHorizontalSizeRules(params, myWidth, rules);
+                measureChildHorizontal(child, params, myWidth, myHeight);
+
+                if (positionChildHorizontal(child, params, myWidth, isWrapContentWidth)) {
+                    offsetHorizontalAxis = true;
+                }
+            }
+        }
+
+        views = mSortedVerticalChildren;
+        count = views.length;
+        final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+
+        for (int i = 0; i < count; i++) {
+            final View child = views[i];
+            if (child.getVisibility() != GONE) {
+                final LayoutParams params = (LayoutParams) child.getLayoutParams();
+
+                applyVerticalSizeRules(params, myHeight, child.getBaseline());
+                measureChild(child, params, myWidth, myHeight);
+                if (positionChildVertical(child, params, myHeight, isWrapContentHeight)) {
+                    offsetVerticalAxis = true;
+                }
+
+                if (isWrapContentWidth) {
+                    if (isLayoutRtl()) {
+                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+                            width = Math.max(width, myWidth - params.mLeft);
+                        } else {
+                            width = Math.max(width, myWidth - params.mLeft + params.leftMargin);
+                        }
+                    } else {
+                        if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+                            width = Math.max(width, params.mRight);
+                        } else {
+                            width = Math.max(width, params.mRight + params.rightMargin);
+                        }
+                    }
+                }
+
+                if (isWrapContentHeight) {
+                    if (targetSdkVersion < Build.VERSION_CODES.KITKAT) {
+                        height = Math.max(height, params.mBottom);
+                    } else {
+                        height = Math.max(height, params.mBottom + params.bottomMargin);
+                    }
+                }
+
+                if (child != ignore || verticalGravity) {
+                    left = Math.min(left, params.mLeft - params.leftMargin);
+                    top = Math.min(top, params.mTop - params.topMargin);
+                }
+
+                if (child != ignore || horizontalGravity) {
+                    right = Math.max(right, params.mRight + params.rightMargin);
+                    bottom = Math.max(bottom, params.mBottom + params.bottomMargin);
+                }
+            }
+        }
+
+        // Use the top-start-most laid out view as the baseline. RTL offsets are
+        // applied later, so we can use the left-most edge as the starting edge.
+        View baselineView = null;
+        LayoutParams baselineParams = null;
+        for (int i = 0; i < count; i++) {
+            final View child = views[i];
+            if (child.getVisibility() != GONE) {
+                final LayoutParams childParams = (LayoutParams) child.getLayoutParams();
+                if (baselineView == null || baselineParams == null
+                        || compareLayoutPosition(childParams, baselineParams) < 0) {
+                    baselineView = child;
+                    baselineParams = childParams;
+                }
+            }
+        }
+        mBaselineView = baselineView;
+
+        if (isWrapContentWidth) {
+            // Width already has left padding in it since it was calculated by looking at
+            // the right of each child view
+            width += mPaddingRight;
+
+            if (mLayoutParams != null && mLayoutParams.width >= 0) {
+                width = Math.max(width, mLayoutParams.width);
+            }
+
+            width = Math.max(width, getSuggestedMinimumWidth());
+            width = resolveSize(width, widthMeasureSpec);
+
+            if (offsetHorizontalAxis) {
+                for (int i = 0; i < count; i++) {
+                    final View child = views[i];
+                    if (child.getVisibility() != GONE) {
+                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
+                        final int[] rules = params.getRules(layoutDirection);
+                        if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
+                            centerHorizontal(child, params, width);
+                        } else if (rules[ALIGN_PARENT_RIGHT] != 0) {
+                            final int childWidth = child.getMeasuredWidth();
+                            params.mLeft = width - mPaddingRight - childWidth;
+                            params.mRight = params.mLeft + childWidth;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (isWrapContentHeight) {
+            // Height already has top padding in it since it was calculated by looking at
+            // the bottom of each child view
+            height += mPaddingBottom;
+
+            if (mLayoutParams != null && mLayoutParams.height >= 0) {
+                height = Math.max(height, mLayoutParams.height);
+            }
+
+            height = Math.max(height, getSuggestedMinimumHeight());
+            height = resolveSize(height, heightMeasureSpec);
+
+            if (offsetVerticalAxis) {
+                for (int i = 0; i < count; i++) {
+                    final View child = views[i];
+                    if (child.getVisibility() != GONE) {
+                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
+                        final int[] rules = params.getRules(layoutDirection);
+                        if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
+                            centerVertical(child, params, height);
+                        } else if (rules[ALIGN_PARENT_BOTTOM] != 0) {
+                            final int childHeight = child.getMeasuredHeight();
+                            params.mTop = height - mPaddingBottom - childHeight;
+                            params.mBottom = params.mTop + childHeight;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (horizontalGravity || verticalGravity) {
+            final Rect selfBounds = mSelfBounds;
+            selfBounds.set(mPaddingLeft, mPaddingTop, width - mPaddingRight,
+                    height - mPaddingBottom);
+
+            final Rect contentBounds = mContentBounds;
+            Gravity.apply(mGravity, right - left, bottom - top, selfBounds, contentBounds,
+                    layoutDirection);
+
+            final int horizontalOffset = contentBounds.left - left;
+            final int verticalOffset = contentBounds.top - top;
+            if (horizontalOffset != 0 || verticalOffset != 0) {
+                for (int i = 0; i < count; i++) {
+                    final View child = views[i];
+                    if (child.getVisibility() != GONE && child != ignore) {
+                        final LayoutParams params = (LayoutParams) child.getLayoutParams();
+                        if (horizontalGravity) {
+                            params.mLeft += horizontalOffset;
+                            params.mRight += horizontalOffset;
+                        }
+                        if (verticalGravity) {
+                            params.mTop += verticalOffset;
+                            params.mBottom += verticalOffset;
+                        }
+                    }
+                }
+            }
+        }
+
+        if (isLayoutRtl()) {
+            final int offsetWidth = myWidth - width;
+            for (int i = 0; i < count; i++) {
+                final View child = views[i];
+                if (child.getVisibility() != GONE) {
+                    final LayoutParams params = (LayoutParams) child.getLayoutParams();
+                    params.mLeft -= offsetWidth;
+                    params.mRight -= offsetWidth;
+                }
+            }
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    /**
+     * @return a negative number if the top of {@code p1} is above the top of
+     *         {@code p2} or if they have identical top values and the left of
+     *         {@code p1} is to the left of {@code p2}, or a positive number
+     *         otherwise
+     */
+    private int compareLayoutPosition(LayoutParams p1, LayoutParams p2) {
+        final int topDiff = p1.mTop - p2.mTop;
+        if (topDiff != 0) {
+            return topDiff;
+        }
+        return p1.mLeft - p2.mLeft;
+    }
+
+    /**
+     * Measure a child. The child should have left, top, right and bottom information
+     * stored in its LayoutParams. If any of these values is VALUE_NOT_SET it means
+     * that the view can extend up to the corresponding edge.
+     *
+     * @param child Child to measure
+     * @param params LayoutParams associated with child
+     * @param myWidth Width of the the RelativeLayout
+     * @param myHeight Height of the RelativeLayout
+     */
+    private void measureChild(View child, LayoutParams params, int myWidth, int myHeight) {
+        int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft,
+                params.mRight, params.width,
+                params.leftMargin, params.rightMargin,
+                mPaddingLeft, mPaddingRight,
+                myWidth);
+        int childHeightMeasureSpec = getChildMeasureSpec(params.mTop,
+                params.mBottom, params.height,
+                params.topMargin, params.bottomMargin,
+                mPaddingTop, mPaddingBottom,
+                myHeight);
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    private void measureChildHorizontal(
+            View child, LayoutParams params, int myWidth, int myHeight) {
+        final int childWidthMeasureSpec = getChildMeasureSpec(params.mLeft, params.mRight,
+                params.width, params.leftMargin, params.rightMargin, mPaddingLeft, mPaddingRight,
+                myWidth);
+
+        final int childHeightMeasureSpec;
+        if (myHeight < 0 && !mAllowBrokenMeasureSpecs) {
+            if (params.height >= 0) {
+                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        params.height, MeasureSpec.EXACTLY);
+            } else {
+                // Negative values in a mySize/myWidth/myWidth value in
+                // RelativeLayout measurement is code for, "we got an
+                // unspecified mode in the RelativeLayout's measure spec."
+                // Carry it forward.
+                childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
+            }
+        } else {
+            final int maxHeight;
+            if (mMeasureVerticalWithPaddingMargin) {
+                maxHeight = Math.max(0, myHeight - mPaddingTop - mPaddingBottom
+                        - params.topMargin - params.bottomMargin);
+            } else {
+                maxHeight = Math.max(0, myHeight);
+            }
+
+            final int heightMode;
+            if (params.height == LayoutParams.MATCH_PARENT) {
+                heightMode = MeasureSpec.EXACTLY;
+            } else {
+                heightMode = MeasureSpec.AT_MOST;
+            }
+            childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(maxHeight, heightMode);
+        }
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    /**
+     * Get a measure spec that accounts for all of the constraints on this view.
+     * This includes size constraints imposed by the RelativeLayout as well as
+     * the View's desired dimension.
+     *
+     * @param childStart The left or top field of the child's layout params
+     * @param childEnd The right or bottom field of the child's layout params
+     * @param childSize The child's desired size (the width or height field of
+     *        the child's layout params)
+     * @param startMargin The left or top margin
+     * @param endMargin The right or bottom margin
+     * @param startPadding mPaddingLeft or mPaddingTop
+     * @param endPadding mPaddingRight or mPaddingBottom
+     * @param mySize The width or height of this view (the RelativeLayout)
+     * @return MeasureSpec for the child
+     */
+    private int getChildMeasureSpec(int childStart, int childEnd,
+            int childSize, int startMargin, int endMargin, int startPadding,
+            int endPadding, int mySize) {
+        int childSpecMode = 0;
+        int childSpecSize = 0;
+
+        // Negative values in a mySize value in RelativeLayout
+        // measurement is code for, "we got an unspecified mode in the
+        // RelativeLayout's measure spec."
+        final boolean isUnspecified = mySize < 0;
+        if (isUnspecified && !mAllowBrokenMeasureSpecs) {
+            if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
+                // Constraints fixed both edges, so child has an exact size.
+                childSpecSize = Math.max(0, childEnd - childStart);
+                childSpecMode = MeasureSpec.EXACTLY;
+            } else if (childSize >= 0) {
+                // The child specified an exact size.
+                childSpecSize = childSize;
+                childSpecMode = MeasureSpec.EXACTLY;
+            } else {
+                // Allow the child to be whatever size it wants.
+                childSpecSize = 0;
+                childSpecMode = MeasureSpec.UNSPECIFIED;
+            }
+
+            return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
+        }
+
+        // Figure out start and end bounds.
+        int tempStart = childStart;
+        int tempEnd = childEnd;
+
+        // If the view did not express a layout constraint for an edge, use
+        // view's margins and our padding
+        if (tempStart == VALUE_NOT_SET) {
+            tempStart = startPadding + startMargin;
+        }
+        if (tempEnd == VALUE_NOT_SET) {
+            tempEnd = mySize - endPadding - endMargin;
+        }
+
+        // Figure out maximum size available to this view
+        final int maxAvailable = tempEnd - tempStart;
+
+        if (childStart != VALUE_NOT_SET && childEnd != VALUE_NOT_SET) {
+            // Constraints fixed both edges, so child must be an exact size.
+            childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
+            childSpecSize = Math.max(0, maxAvailable);
+        } else {
+            if (childSize >= 0) {
+                // Child wanted an exact size. Give as much as possible.
+                childSpecMode = MeasureSpec.EXACTLY;
+
+                if (maxAvailable >= 0) {
+                    // We have a maximum size in this dimension.
+                    childSpecSize = Math.min(maxAvailable, childSize);
+                } else {
+                    // We can grow in this dimension.
+                    childSpecSize = childSize;
+                }
+            } else if (childSize == LayoutParams.MATCH_PARENT) {
+                // Child wanted to be as big as possible. Give all available
+                // space.
+                childSpecMode = isUnspecified ? MeasureSpec.UNSPECIFIED : MeasureSpec.EXACTLY;
+                childSpecSize = Math.max(0, maxAvailable);
+            } else if (childSize == LayoutParams.WRAP_CONTENT) {
+                // Child wants to wrap content. Use AT_MOST to communicate
+                // available space if we know our max size.
+                if (maxAvailable >= 0) {
+                    // We have a maximum size in this dimension.
+                    childSpecMode = MeasureSpec.AT_MOST;
+                    childSpecSize = maxAvailable;
+                } else {
+                    // We can grow in this dimension. Child can be as big as it
+                    // wants.
+                    childSpecMode = MeasureSpec.UNSPECIFIED;
+                    childSpecSize = 0;
+                }
+            }
+        }
+
+        return MeasureSpec.makeMeasureSpec(childSpecSize, childSpecMode);
+    }
+
+    private boolean positionChildHorizontal(View child, LayoutParams params, int myWidth,
+            boolean wrapContent) {
+
+        final int layoutDirection = getLayoutDirection();
+        int[] rules = params.getRules(layoutDirection);
+
+        if (params.mLeft == VALUE_NOT_SET && params.mRight != VALUE_NOT_SET) {
+            // Right is fixed, but left varies
+            params.mLeft = params.mRight - child.getMeasuredWidth();
+        } else if (params.mLeft != VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
+            // Left is fixed, but right varies
+            params.mRight = params.mLeft + child.getMeasuredWidth();
+        } else if (params.mLeft == VALUE_NOT_SET && params.mRight == VALUE_NOT_SET) {
+            // Both left and right vary
+            if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_HORIZONTAL] != 0) {
+                if (!wrapContent) {
+                    centerHorizontal(child, params, myWidth);
+                } else {
+                    positionAtEdge(child, params, myWidth);
+                }
+                return true;
+            } else {
+                // This is the default case. For RTL we start from the right and for LTR we start
+                // from the left. This will give LEFT/TOP for LTR and RIGHT/TOP for RTL.
+                positionAtEdge(child, params, myWidth);
+            }
+        }
+        return rules[ALIGN_PARENT_END] != 0;
+    }
+
+    private void positionAtEdge(View child, LayoutParams params, int myWidth) {
+        if (isLayoutRtl()) {
+            params.mRight = myWidth - mPaddingRight - params.rightMargin;
+            params.mLeft = params.mRight - child.getMeasuredWidth();
+        } else {
+            params.mLeft = mPaddingLeft + params.leftMargin;
+            params.mRight = params.mLeft + child.getMeasuredWidth();
+        }
+    }
+
+    private boolean positionChildVertical(View child, LayoutParams params, int myHeight,
+            boolean wrapContent) {
+
+        int[] rules = params.getRules();
+
+        if (params.mTop == VALUE_NOT_SET && params.mBottom != VALUE_NOT_SET) {
+            // Bottom is fixed, but top varies
+            params.mTop = params.mBottom - child.getMeasuredHeight();
+        } else if (params.mTop != VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
+            // Top is fixed, but bottom varies
+            params.mBottom = params.mTop + child.getMeasuredHeight();
+        } else if (params.mTop == VALUE_NOT_SET && params.mBottom == VALUE_NOT_SET) {
+            // Both top and bottom vary
+            if (rules[CENTER_IN_PARENT] != 0 || rules[CENTER_VERTICAL] != 0) {
+                if (!wrapContent) {
+                    centerVertical(child, params, myHeight);
+                } else {
+                    params.mTop = mPaddingTop + params.topMargin;
+                    params.mBottom = params.mTop + child.getMeasuredHeight();
+                }
+                return true;
+            } else {
+                params.mTop = mPaddingTop + params.topMargin;
+                params.mBottom = params.mTop + child.getMeasuredHeight();
+            }
+        }
+        return rules[ALIGN_PARENT_BOTTOM] != 0;
+    }
+
+    private void applyHorizontalSizeRules(LayoutParams childParams, int myWidth, int[] rules) {
+        RelativeLayout.LayoutParams anchorParams;
+
+        // VALUE_NOT_SET indicates a "soft requirement" in that direction. For example:
+        // left=10, right=VALUE_NOT_SET means the view must start at 10, but can go as far as it
+        // wants to the right
+        // left=VALUE_NOT_SET, right=10 means the view must end at 10, but can go as far as it
+        // wants to the left
+        // left=10, right=20 means the left and right ends are both fixed
+        childParams.mLeft = VALUE_NOT_SET;
+        childParams.mRight = VALUE_NOT_SET;
+
+        anchorParams = getRelatedViewParams(rules, LEFT_OF);
+        if (anchorParams != null) {
+            childParams.mRight = anchorParams.mLeft - (anchorParams.leftMargin +
+                    childParams.rightMargin);
+        } else if (childParams.alignWithParent && rules[LEFT_OF] != 0) {
+            if (myWidth >= 0) {
+                childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+            }
+        }
+
+        anchorParams = getRelatedViewParams(rules, RIGHT_OF);
+        if (anchorParams != null) {
+            childParams.mLeft = anchorParams.mRight + (anchorParams.rightMargin +
+                    childParams.leftMargin);
+        } else if (childParams.alignWithParent && rules[RIGHT_OF] != 0) {
+            childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+        }
+
+        anchorParams = getRelatedViewParams(rules, ALIGN_LEFT);
+        if (anchorParams != null) {
+            childParams.mLeft = anchorParams.mLeft + childParams.leftMargin;
+        } else if (childParams.alignWithParent && rules[ALIGN_LEFT] != 0) {
+            childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+        }
+
+        anchorParams = getRelatedViewParams(rules, ALIGN_RIGHT);
+        if (anchorParams != null) {
+            childParams.mRight = anchorParams.mRight - childParams.rightMargin;
+        } else if (childParams.alignWithParent && rules[ALIGN_RIGHT] != 0) {
+            if (myWidth >= 0) {
+                childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+            }
+        }
+
+        if (0 != rules[ALIGN_PARENT_LEFT]) {
+            childParams.mLeft = mPaddingLeft + childParams.leftMargin;
+        }
+
+        if (0 != rules[ALIGN_PARENT_RIGHT]) {
+            if (myWidth >= 0) {
+                childParams.mRight = myWidth - mPaddingRight - childParams.rightMargin;
+            }
+        }
+    }
+
+    private void applyVerticalSizeRules(LayoutParams childParams, int myHeight, int myBaseline) {
+        final int[] rules = childParams.getRules();
+
+        // Baseline alignment overrides any explicitly specified top or bottom.
+        int baselineOffset = getRelatedViewBaselineOffset(rules);
+        if (baselineOffset != -1) {
+            if (myBaseline != -1) {
+                baselineOffset -= myBaseline;
+            }
+            childParams.mTop = baselineOffset;
+            childParams.mBottom = VALUE_NOT_SET;
+            return;
+        }
+
+        RelativeLayout.LayoutParams anchorParams;
+
+        childParams.mTop = VALUE_NOT_SET;
+        childParams.mBottom = VALUE_NOT_SET;
+
+        anchorParams = getRelatedViewParams(rules, ABOVE);
+        if (anchorParams != null) {
+            childParams.mBottom = anchorParams.mTop - (anchorParams.topMargin +
+                    childParams.bottomMargin);
+        } else if (childParams.alignWithParent && rules[ABOVE] != 0) {
+            if (myHeight >= 0) {
+                childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+            }
+        }
+
+        anchorParams = getRelatedViewParams(rules, BELOW);
+        if (anchorParams != null) {
+            childParams.mTop = anchorParams.mBottom + (anchorParams.bottomMargin +
+                    childParams.topMargin);
+        } else if (childParams.alignWithParent && rules[BELOW] != 0) {
+            childParams.mTop = mPaddingTop + childParams.topMargin;
+        }
+
+        anchorParams = getRelatedViewParams(rules, ALIGN_TOP);
+        if (anchorParams != null) {
+            childParams.mTop = anchorParams.mTop + childParams.topMargin;
+        } else if (childParams.alignWithParent && rules[ALIGN_TOP] != 0) {
+            childParams.mTop = mPaddingTop + childParams.topMargin;
+        }
+
+        anchorParams = getRelatedViewParams(rules, ALIGN_BOTTOM);
+        if (anchorParams != null) {
+            childParams.mBottom = anchorParams.mBottom - childParams.bottomMargin;
+        } else if (childParams.alignWithParent && rules[ALIGN_BOTTOM] != 0) {
+            if (myHeight >= 0) {
+                childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+            }
+        }
+
+        if (0 != rules[ALIGN_PARENT_TOP]) {
+            childParams.mTop = mPaddingTop + childParams.topMargin;
+        }
+
+        if (0 != rules[ALIGN_PARENT_BOTTOM]) {
+            if (myHeight >= 0) {
+                childParams.mBottom = myHeight - mPaddingBottom - childParams.bottomMargin;
+            }
+        }
+    }
+
+    private View getRelatedView(int[] rules, int relation) {
+        int id = rules[relation];
+        if (id != 0) {
+            DependencyGraph.Node node = mGraph.mKeyNodes.get(id);
+            if (node == null) return null;
+            View v = node.view;
+
+            // Find the first non-GONE view up the chain
+            while (v.getVisibility() == View.GONE) {
+                rules = ((LayoutParams) v.getLayoutParams()).getRules(v.getLayoutDirection());
+                node = mGraph.mKeyNodes.get((rules[relation]));
+                // ignore self dependency. for more info look in git commit: da3003
+                if (node == null || v == node.view) return null;
+                v = node.view;
+            }
+
+            return v;
+        }
+
+        return null;
+    }
+
+    private LayoutParams getRelatedViewParams(int[] rules, int relation) {
+        View v = getRelatedView(rules, relation);
+        if (v != null) {
+            ViewGroup.LayoutParams params = v.getLayoutParams();
+            if (params instanceof LayoutParams) {
+                return (LayoutParams) v.getLayoutParams();
+            }
+        }
+        return null;
+    }
+
+    private int getRelatedViewBaselineOffset(int[] rules) {
+        final View v = getRelatedView(rules, ALIGN_BASELINE);
+        if (v != null) {
+            final int baseline = v.getBaseline();
+            if (baseline != -1) {
+                final ViewGroup.LayoutParams params = v.getLayoutParams();
+                if (params instanceof LayoutParams) {
+                    final LayoutParams anchorParams = (LayoutParams) v.getLayoutParams();
+                    return anchorParams.mTop + baseline;
+                }
+            }
+        }
+        return -1;
+    }
+
+    private static void centerHorizontal(View child, LayoutParams params, int myWidth) {
+        int childWidth = child.getMeasuredWidth();
+        int left = (myWidth - childWidth) / 2;
+
+        params.mLeft = left;
+        params.mRight = left + childWidth;
+    }
+
+    private static void centerVertical(View child, LayoutParams params, int myHeight) {
+        int childHeight = child.getMeasuredHeight();
+        int top = (myHeight - childHeight) / 2;
+
+        params.mTop = top;
+        params.mBottom = top + childHeight;
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        //  The layout has actually already been performed and the positions
+        //  cached.  Apply the cached values to the children.
+        final int count = getChildCount();
+
+        for (int i = 0; i < count; i++) {
+            View child = getChildAt(i);
+            if (child.getVisibility() != GONE) {
+                RelativeLayout.LayoutParams st =
+                        (RelativeLayout.LayoutParams) child.getLayoutParams();
+                child.layout(st.mLeft, st.mTop, st.mRight, st.mBottom);
+            }
+        }
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new RelativeLayout.LayoutParams(getContext(), attrs);
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT},
+     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
+     */
+    @Override
+    protected ViewGroup.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    // Override to allow type-checking of LayoutParams.
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof RelativeLayout.LayoutParams;
+    }
+
+    @Override
+    protected ViewGroup.LayoutParams generateLayoutParams(ViewGroup.LayoutParams lp) {
+        if (sPreserveMarginParamsInLayoutParamConversion) {
+            if (lp instanceof LayoutParams) {
+                return new LayoutParams((LayoutParams) lp);
+            } else if (lp instanceof MarginLayoutParams) {
+                return new LayoutParams((MarginLayoutParams) lp);
+            }
+        }
+        return new LayoutParams(lp);
+    }
+
+    /** @hide */
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        if (mTopToBottomLeftToRightSet == null) {
+            mTopToBottomLeftToRightSet = new TreeSet<View>(new TopToBottomLeftToRightComparator());
+        }
+
+        // sort children top-to-bottom and left-to-right
+        for (int i = 0, count = getChildCount(); i < count; i++) {
+            mTopToBottomLeftToRightSet.add(getChildAt(i));
+        }
+
+        for (View view : mTopToBottomLeftToRightSet) {
+            if (view.getVisibility() == View.VISIBLE
+                    && view.dispatchPopulateAccessibilityEvent(event)) {
+                mTopToBottomLeftToRightSet.clear();
+                return true;
+            }
+        }
+
+        mTopToBottomLeftToRightSet.clear();
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return RelativeLayout.class.getName();
+    }
+
+    /**
+     * Compares two views in left-to-right and top-to-bottom fashion.
+     */
+     private class TopToBottomLeftToRightComparator implements Comparator<View> {
+        public int compare(View first, View second) {
+            // top - bottom
+            int topDifference = first.getTop() - second.getTop();
+            if (topDifference != 0) {
+                return topDifference;
+            }
+            // left - right
+            int leftDifference = first.getLeft() - second.getLeft();
+            if (leftDifference != 0) {
+                return leftDifference;
+            }
+            // break tie by height
+            int heightDiference = first.getHeight() - second.getHeight();
+            if (heightDiference != 0) {
+                return heightDiference;
+            }
+            // break tie by width
+            int widthDiference = first.getWidth() - second.getWidth();
+            if (widthDiference != 0) {
+                return widthDiference;
+            }
+            return 0;
+        }
+    }
+
+    /**
+     * Specifies how a view is positioned within a {@link RelativeLayout}.
+     * The relative layout containing the view uses the value of these layout parameters to
+     * determine where to position the view on the screen.  If the view is not contained
+     * within a relative layout, these attributes are ignored.
+     *
+     * See the <a href=“https://developer.android.com/guide/topics/ui/layout/relative.html”>
+     * Relative Layout</a> guide for example code demonstrating how to use relative layout’s
+     * layout parameters in a layout XML.
+     *
+     * To learn more about layout parameters and how they differ from typical view attributes,
+     * see the <a href=“https://developer.android.com/guide/topics/ui/declaring-layout.html#attributes”>
+     *     Layouts guide</a>.
+     *
+     *
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignWithParentIfMissing
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toLeftOf
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toRightOf
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_above
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_below
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBaseline
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignLeft
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignTop
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignRight
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignBottom
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentLeft
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentTop
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentRight
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentBottom
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerInParent
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerHorizontal
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_centerVertical
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toStartOf
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_toEndOf
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignStart
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignEnd
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentStart
+     * @attr ref android.R.styleable#RelativeLayout_Layout_layout_alignParentEnd
+     */
+    public static class LayoutParams extends ViewGroup.MarginLayoutParams {
+        @ViewDebug.ExportedProperty(category = "layout", resolveId = true, indexMapping = {
+            @ViewDebug.IntToString(from = ABOVE,               to = "above"),
+            @ViewDebug.IntToString(from = ALIGN_BASELINE,      to = "alignBaseline"),
+            @ViewDebug.IntToString(from = ALIGN_BOTTOM,        to = "alignBottom"),
+            @ViewDebug.IntToString(from = ALIGN_LEFT,          to = "alignLeft"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_BOTTOM, to = "alignParentBottom"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_LEFT,   to = "alignParentLeft"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_RIGHT,  to = "alignParentRight"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_TOP,    to = "alignParentTop"),
+            @ViewDebug.IntToString(from = ALIGN_RIGHT,         to = "alignRight"),
+            @ViewDebug.IntToString(from = ALIGN_TOP,           to = "alignTop"),
+            @ViewDebug.IntToString(from = BELOW,               to = "below"),
+            @ViewDebug.IntToString(from = CENTER_HORIZONTAL,   to = "centerHorizontal"),
+            @ViewDebug.IntToString(from = CENTER_IN_PARENT,    to = "center"),
+            @ViewDebug.IntToString(from = CENTER_VERTICAL,     to = "centerVertical"),
+            @ViewDebug.IntToString(from = LEFT_OF,             to = "leftOf"),
+            @ViewDebug.IntToString(from = RIGHT_OF,            to = "rightOf"),
+            @ViewDebug.IntToString(from = ALIGN_START,         to = "alignStart"),
+            @ViewDebug.IntToString(from = ALIGN_END,           to = "alignEnd"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_START,  to = "alignParentStart"),
+            @ViewDebug.IntToString(from = ALIGN_PARENT_END,    to = "alignParentEnd"),
+            @ViewDebug.IntToString(from = START_OF,            to = "startOf"),
+            @ViewDebug.IntToString(from = END_OF,              to = "endOf")
+        }, mapping = {
+            @ViewDebug.IntToString(from = TRUE, to = "true"),
+            @ViewDebug.IntToString(from = 0,    to = "false/NO_ID")
+        })
+
+        private int[] mRules = new int[VERB_COUNT];
+        private int[] mInitialRules = new int[VERB_COUNT];
+
+        private int mLeft, mTop, mRight, mBottom;
+
+        /**
+         * Whether this view had any relative rules modified following the most
+         * recent resolution of layout direction.
+         */
+        private boolean mNeedsLayoutResolution;
+
+        private boolean mRulesChanged = false;
+        private boolean mIsRtlCompatibilityMode = false;
+
+        /**
+         * When true, uses the parent as the anchor if the anchor doesn't exist or if
+         * the anchor's visibility is GONE.
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public boolean alignWithParent;
+
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            TypedArray a = c.obtainStyledAttributes(attrs,
+                    com.android.internal.R.styleable.RelativeLayout_Layout);
+
+            final int targetSdkVersion = c.getApplicationInfo().targetSdkVersion;
+            mIsRtlCompatibilityMode = (targetSdkVersion < JELLY_BEAN_MR1 ||
+                    !c.getApplicationInfo().hasRtlSupport());
+
+            final int[] rules = mRules;
+            //noinspection MismatchedReadAndWriteOfArray
+            final int[] initialRules = mInitialRules;
+
+            final int N = a.getIndexCount();
+            for (int i = 0; i < N; i++) {
+                int attr = a.getIndex(i);
+                switch (attr) {
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignWithParentIfMissing:
+                        alignWithParent = a.getBoolean(attr, false);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toLeftOf:
+                        rules[LEFT_OF] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toRightOf:
+                        rules[RIGHT_OF] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_above:
+                        rules[ABOVE] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_below:
+                        rules[BELOW] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBaseline:
+                        rules[ALIGN_BASELINE] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignLeft:
+                        rules[ALIGN_LEFT] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignTop:
+                        rules[ALIGN_TOP] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignRight:
+                        rules[ALIGN_RIGHT] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignBottom:
+                        rules[ALIGN_BOTTOM] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentLeft:
+                        rules[ALIGN_PARENT_LEFT] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentTop:
+                        rules[ALIGN_PARENT_TOP] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentRight:
+                        rules[ALIGN_PARENT_RIGHT] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentBottom:
+                        rules[ALIGN_PARENT_BOTTOM] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerInParent:
+                        rules[CENTER_IN_PARENT] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerHorizontal:
+                        rules[CENTER_HORIZONTAL] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_centerVertical:
+                        rules[CENTER_VERTICAL] = a.getBoolean(attr, false) ? TRUE : 0;
+                       break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toStartOf:
+                        rules[START_OF] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_toEndOf:
+                        rules[END_OF] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignStart:
+                        rules[ALIGN_START] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignEnd:
+                        rules[ALIGN_END] = a.getResourceId(attr, 0);
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentStart:
+                        rules[ALIGN_PARENT_START] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                    case com.android.internal.R.styleable.RelativeLayout_Layout_layout_alignParentEnd:
+                        rules[ALIGN_PARENT_END] = a.getBoolean(attr, false) ? TRUE : 0;
+                        break;
+                }
+            }
+            mRulesChanged = true;
+            System.arraycopy(rules, LEFT_OF, initialRules, LEFT_OF, VERB_COUNT);
+
+            a.recycle();
+        }
+
+        public LayoutParams(int w, int h) {
+            super(w, h);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.MarginLayoutParams source) {
+            super(source);
+        }
+
+        /**
+         * Copy constructor. Clones the width, height, margin values, and rules
+         * of the source.
+         *
+         * @param source The layout params to copy from.
+         */
+        public LayoutParams(LayoutParams source) {
+            super(source);
+
+            this.mIsRtlCompatibilityMode = source.mIsRtlCompatibilityMode;
+            this.mRulesChanged = source.mRulesChanged;
+            this.alignWithParent = source.alignWithParent;
+
+            System.arraycopy(source.mRules, LEFT_OF, this.mRules, LEFT_OF, VERB_COUNT);
+            System.arraycopy(
+                    source.mInitialRules, LEFT_OF, this.mInitialRules, LEFT_OF, VERB_COUNT);
+        }
+
+        @Override
+        public String debug(String output) {
+            return output + "ViewGroup.LayoutParams={ width=" + sizeToString(width) +
+                    ", height=" + sizeToString(height) + " }";
+        }
+
+        /**
+         * Adds a layout rule to be interpreted by the RelativeLayout.
+         * <p>
+         * This method should only be used for verbs that don't refer to a
+         * sibling (ex. {@link #ALIGN_RIGHT}) or take a boolean
+         * value ({@link #TRUE} for true or 0 for false). To
+         * specify a verb that takes a subject, use {@link #addRule(int, int)}.
+         * <p>
+         * If the rule is relative to the layout direction (ex.
+         * {@link #ALIGN_PARENT_START}), then the layout direction must be
+         * resolved using {@link #resolveLayoutDirection(int)} before calling
+         * {@link #getRule(int)} an absolute rule (ex.
+         * {@link #ALIGN_PARENT_LEFT}.
+         *
+         * @param verb a layout verb, such as {@link #ALIGN_PARENT_LEFT}
+         * @see #addRule(int, int)
+         * @see #removeRule(int)
+         * @see #getRule(int)
+         */
+        public void addRule(int verb) {
+            addRule(verb, TRUE);
+        }
+
+        /**
+         * Adds a layout rule to be interpreted by the RelativeLayout.
+         * <p>
+         * Use this for verbs that refer to a sibling (ex.
+         * {@link #ALIGN_RIGHT}) or take a boolean value (ex.
+         * {@link #CENTER_IN_PARENT}).
+         * <p>
+         * If the rule is relative to the layout direction (ex.
+         * {@link #START_OF}), then the layout direction must be resolved using
+         * {@link #resolveLayoutDirection(int)} before calling
+         * {@link #getRule(int)} with an absolute rule (ex. {@link #LEFT_OF}.
+         *
+         * @param verb a layout verb, such as {@link #ALIGN_RIGHT}
+         * @param subject the ID of another view to use as an anchor, or a
+         *                boolean value (represented as {@link #TRUE} for true
+         *                or 0 for false)
+         * @see #addRule(int)
+         * @see #removeRule(int)
+         * @see #getRule(int)
+         */
+        public void addRule(int verb, int subject) {
+            // If we're removing a relative rule, we'll need to force layout
+            // resolution the next time it's requested.
+            if (!mNeedsLayoutResolution && isRelativeRule(verb)
+                    && mInitialRules[verb] != 0 && subject == 0) {
+                mNeedsLayoutResolution = true;
+            }
+
+            mRules[verb] = subject;
+            mInitialRules[verb] = subject;
+            mRulesChanged = true;
+        }
+
+        /**
+         * Removes a layout rule to be interpreted by the RelativeLayout.
+         * <p>
+         * If the rule is relative to the layout direction (ex.
+         * {@link #START_OF}, {@link #ALIGN_PARENT_START}, etc.) then the
+         * layout direction must be resolved using
+         * {@link #resolveLayoutDirection(int)} before before calling
+         * {@link #getRule(int)} with an absolute rule (ex. {@link #LEFT_OF}.
+         *
+         * @param verb One of the verbs defined by
+         *        {@link android.widget.RelativeLayout RelativeLayout}, such as
+         *         ALIGN_WITH_PARENT_LEFT.
+         * @see #addRule(int)
+         * @see #addRule(int, int)
+         * @see #getRule(int)
+         */
+        public void removeRule(int verb) {
+            addRule(verb, 0);
+        }
+
+        /**
+         * Returns the layout rule associated with a specific verb.
+         *
+         * @param verb one of the verbs defined by {@link RelativeLayout}, such
+         *             as ALIGN_WITH_PARENT_LEFT
+         * @return the id of another view to use as an anchor, a boolean value
+         *         (represented as {@link RelativeLayout#TRUE} for true
+         *         or 0 for false), or -1 for verbs that don't refer to another
+         *         sibling (for example, ALIGN_WITH_PARENT_BOTTOM)
+         * @see #addRule(int)
+         * @see #addRule(int, int)
+         */
+        public int getRule(int verb) {
+            return mRules[verb];
+        }
+
+        private boolean hasRelativeRules() {
+            return (mInitialRules[START_OF] != 0 || mInitialRules[END_OF] != 0 ||
+                    mInitialRules[ALIGN_START] != 0 || mInitialRules[ALIGN_END] != 0 ||
+                    mInitialRules[ALIGN_PARENT_START] != 0 || mInitialRules[ALIGN_PARENT_END] != 0);
+        }
+
+        private boolean isRelativeRule(int rule) {
+            return rule == START_OF || rule == END_OF
+                    || rule == ALIGN_START || rule == ALIGN_END
+                    || rule == ALIGN_PARENT_START || rule == ALIGN_PARENT_END;
+        }
+
+        // The way we are resolving rules depends on the layout direction and if we are pre JB MR1
+        // or not.
+        //
+        // If we are pre JB MR1 (said as "RTL compatibility mode"), "left"/"right" rules are having
+        // predominance over any "start/end" rules that could have been defined. A special case:
+        // if no "left"/"right" rule has been defined and "start"/"end" rules are defined then we
+        // resolve those "start"/"end" rules to "left"/"right" respectively.
+        //
+        // If we are JB MR1+, then "start"/"end" rules are having predominance over "left"/"right"
+        // rules. If no "start"/"end" rule is defined then we use "left"/"right" rules.
+        //
+        // In all cases, the result of the resolution should clear the "start"/"end" rules to leave
+        // only the "left"/"right" rules at the end.
+        private void resolveRules(int layoutDirection) {
+            final boolean isLayoutRtl = (layoutDirection == View.LAYOUT_DIRECTION_RTL);
+
+            // Reset to initial state
+            System.arraycopy(mInitialRules, LEFT_OF, mRules, LEFT_OF, VERB_COUNT);
+
+            // Apply rules depending on direction and if we are in RTL compatibility mode
+            if (mIsRtlCompatibilityMode) {
+                if (mRules[ALIGN_START] != 0) {
+                    if (mRules[ALIGN_LEFT] == 0) {
+                        // "left" rule is not defined but "start" rule is: use the "start" rule as
+                        // the "left" rule
+                        mRules[ALIGN_LEFT] = mRules[ALIGN_START];
+                    }
+                    mRules[ALIGN_START] = 0;
+                }
+
+                if (mRules[ALIGN_END] != 0) {
+                    if (mRules[ALIGN_RIGHT] == 0) {
+                        // "right" rule is not defined but "end" rule is: use the "end" rule as the
+                        // "right" rule
+                        mRules[ALIGN_RIGHT] = mRules[ALIGN_END];
+                    }
+                    mRules[ALIGN_END] = 0;
+                }
+
+                if (mRules[START_OF] != 0) {
+                    if (mRules[LEFT_OF] == 0) {
+                        // "left" rule is not defined but "start" rule is: use the "start" rule as
+                        // the "left" rule
+                        mRules[LEFT_OF] = mRules[START_OF];
+                    }
+                    mRules[START_OF] = 0;
+                }
+
+                if (mRules[END_OF] != 0) {
+                    if (mRules[RIGHT_OF] == 0) {
+                        // "right" rule is not defined but "end" rule is: use the "end" rule as the
+                        // "right" rule
+                        mRules[RIGHT_OF] = mRules[END_OF];
+                    }
+                    mRules[END_OF] = 0;
+                }
+
+                if (mRules[ALIGN_PARENT_START] != 0) {
+                    if (mRules[ALIGN_PARENT_LEFT] == 0) {
+                        // "left" rule is not defined but "start" rule is: use the "start" rule as
+                        // the "left" rule
+                        mRules[ALIGN_PARENT_LEFT] = mRules[ALIGN_PARENT_START];
+                    }
+                    mRules[ALIGN_PARENT_START] = 0;
+                }
+
+                if (mRules[ALIGN_PARENT_END] != 0) {
+                    if (mRules[ALIGN_PARENT_RIGHT] == 0) {
+                        // "right" rule is not defined but "end" rule is: use the "end" rule as the
+                        // "right" rule
+                        mRules[ALIGN_PARENT_RIGHT] = mRules[ALIGN_PARENT_END];
+                    }
+                    mRules[ALIGN_PARENT_END] = 0;
+                }
+            } else {
+                // JB MR1+ case
+                if ((mRules[ALIGN_START] != 0 || mRules[ALIGN_END] != 0) &&
+                        (mRules[ALIGN_LEFT] != 0 || mRules[ALIGN_RIGHT] != 0)) {
+                    // "start"/"end" rules take precedence over "left"/"right" rules
+                    mRules[ALIGN_LEFT] = 0;
+                    mRules[ALIGN_RIGHT] = 0;
+                }
+                if (mRules[ALIGN_START] != 0) {
+                    // "start" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? ALIGN_RIGHT : ALIGN_LEFT] = mRules[ALIGN_START];
+                    mRules[ALIGN_START] = 0;
+                }
+                if (mRules[ALIGN_END] != 0) {
+                    // "end" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? ALIGN_LEFT : ALIGN_RIGHT] = mRules[ALIGN_END];
+                    mRules[ALIGN_END] = 0;
+                }
+
+                if ((mRules[START_OF] != 0 || mRules[END_OF] != 0) &&
+                        (mRules[LEFT_OF] != 0 || mRules[RIGHT_OF] != 0)) {
+                    // "start"/"end" rules take precedence over "left"/"right" rules
+                    mRules[LEFT_OF] = 0;
+                    mRules[RIGHT_OF] = 0;
+                }
+                if (mRules[START_OF] != 0) {
+                    // "start" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? RIGHT_OF : LEFT_OF] = mRules[START_OF];
+                    mRules[START_OF] = 0;
+                }
+                if (mRules[END_OF] != 0) {
+                    // "end" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? LEFT_OF : RIGHT_OF] = mRules[END_OF];
+                    mRules[END_OF] = 0;
+                }
+
+                if ((mRules[ALIGN_PARENT_START] != 0 || mRules[ALIGN_PARENT_END] != 0) &&
+                        (mRules[ALIGN_PARENT_LEFT] != 0 || mRules[ALIGN_PARENT_RIGHT] != 0)) {
+                    // "start"/"end" rules take precedence over "left"/"right" rules
+                    mRules[ALIGN_PARENT_LEFT] = 0;
+                    mRules[ALIGN_PARENT_RIGHT] = 0;
+                }
+                if (mRules[ALIGN_PARENT_START] != 0) {
+                    // "start" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? ALIGN_PARENT_RIGHT : ALIGN_PARENT_LEFT] = mRules[ALIGN_PARENT_START];
+                    mRules[ALIGN_PARENT_START] = 0;
+                }
+                if (mRules[ALIGN_PARENT_END] != 0) {
+                    // "end" rule resolved to "left" or "right" depending on the direction
+                    mRules[isLayoutRtl ? ALIGN_PARENT_LEFT : ALIGN_PARENT_RIGHT] = mRules[ALIGN_PARENT_END];
+                    mRules[ALIGN_PARENT_END] = 0;
+                }
+            }
+
+            mRulesChanged = false;
+            mNeedsLayoutResolution = false;
+        }
+
+        /**
+         * Retrieves a complete list of all supported rules, where the index is the rule
+         * verb, and the element value is the value specified, or "false" if it was never
+         * set. If there are relative rules defined (*_START / *_END), they will be resolved
+         * depending on the layout direction.
+         *
+         * @param layoutDirection the direction of the layout.
+         *                        Should be either {@link View#LAYOUT_DIRECTION_LTR}
+         *                        or {@link View#LAYOUT_DIRECTION_RTL}
+         * @return the supported rules
+         * @see #addRule(int, int)
+         *
+         * @hide
+         */
+        public int[] getRules(int layoutDirection) {
+            resolveLayoutDirection(layoutDirection);
+            return mRules;
+        }
+
+        /**
+         * Retrieves a complete list of all supported rules, where the index is the rule
+         * verb, and the element value is the value specified, or "false" if it was never
+         * set. There will be no resolution of relative rules done.
+         *
+         * @return the supported rules
+         * @see #addRule(int, int)
+         */
+        public int[] getRules() {
+            return mRules;
+        }
+
+        /**
+         * This will be called by {@link android.view.View#requestLayout()} to
+         * resolve layout parameters that are relative to the layout direction.
+         * <p>
+         * After this method is called, any rules using layout-relative verbs
+         * (ex. {@link #START_OF}) previously added via {@link #addRule(int)}
+         * may only be accessed via their resolved absolute verbs (ex.
+         * {@link #LEFT_OF}).
+         */
+        @Override
+        public void resolveLayoutDirection(int layoutDirection) {
+            if (shouldResolveLayoutDirection(layoutDirection)) {
+                resolveRules(layoutDirection);
+            }
+
+            // This will set the layout direction.
+            super.resolveLayoutDirection(layoutDirection);
+        }
+
+        private boolean shouldResolveLayoutDirection(int layoutDirection) {
+            return (mNeedsLayoutResolution || hasRelativeRules())
+                    && (mRulesChanged || layoutDirection != getLayoutDirection());
+        }
+
+        /** @hide */
+        @Override
+        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+            super.encodeProperties(encoder);
+            encoder.addProperty("layout:alignWithParent", alignWithParent);
+        }
+    }
+
+    private static class DependencyGraph {
+        /**
+         * List of all views in the graph.
+         */
+        private ArrayList<Node> mNodes = new ArrayList<Node>();
+
+        /**
+         * List of nodes in the graph. Each node is identified by its
+         * view id (see View#getId()).
+         */
+        private SparseArray<Node> mKeyNodes = new SparseArray<Node>();
+
+        /**
+         * Temporary data structure used to build the list of roots
+         * for this graph.
+         */
+        private ArrayDeque<Node> mRoots = new ArrayDeque<Node>();
+
+        /**
+         * Clears the graph.
+         */
+        void clear() {
+            final ArrayList<Node> nodes = mNodes;
+            final int count = nodes.size();
+
+            for (int i = 0; i < count; i++) {
+                nodes.get(i).release();
+            }
+            nodes.clear();
+
+            mKeyNodes.clear();
+            mRoots.clear();
+        }
+
+        /**
+         * Adds a view to the graph.
+         *
+         * @param view The view to be added as a node to the graph.
+         */
+        void add(View view) {
+            final int id = view.getId();
+            final Node node = Node.acquire(view);
+
+            if (id != View.NO_ID) {
+                mKeyNodes.put(id, node);
+            }
+
+            mNodes.add(node);
+        }
+
+        /**
+         * Builds a sorted list of views. The sorting order depends on the dependencies
+         * between the view. For instance, if view C needs view A to be processed first
+         * and view A needs view B to be processed first, the dependency graph
+         * is: B -> A -> C. The sorted array will contain views B, A and C in this order.
+         *
+         * @param sorted The sorted list of views. The length of this array must
+         *        be equal to getChildCount().
+         * @param rules The list of rules to take into account.
+         */
+        void getSortedViews(View[] sorted, int... rules) {
+            final ArrayDeque<Node> roots = findRoots(rules);
+            int index = 0;
+
+            Node node;
+            while ((node = roots.pollLast()) != null) {
+                final View view = node.view;
+                final int key = view.getId();
+
+                sorted[index++] = view;
+
+                final ArrayMap<Node, DependencyGraph> dependents = node.dependents;
+                final int count = dependents.size();
+                for (int i = 0; i < count; i++) {
+                    final Node dependent = dependents.keyAt(i);
+                    final SparseArray<Node> dependencies = dependent.dependencies;
+
+                    dependencies.remove(key);
+                    if (dependencies.size() == 0) {
+                        roots.add(dependent);
+                    }
+                }
+            }
+
+            if (index < sorted.length) {
+                throw new IllegalStateException("Circular dependencies cannot exist"
+                        + " in RelativeLayout");
+            }
+        }
+
+        /**
+         * Finds the roots of the graph. A root is a node with no dependency and
+         * with [0..n] dependents.
+         *
+         * @param rulesFilter The list of rules to consider when building the
+         *        dependencies
+         *
+         * @return A list of node, each being a root of the graph
+         */
+        private ArrayDeque<Node> findRoots(int[] rulesFilter) {
+            final SparseArray<Node> keyNodes = mKeyNodes;
+            final ArrayList<Node> nodes = mNodes;
+            final int count = nodes.size();
+
+            // Find roots can be invoked several times, so make sure to clear
+            // all dependents and dependencies before running the algorithm
+            for (int i = 0; i < count; i++) {
+                final Node node = nodes.get(i);
+                node.dependents.clear();
+                node.dependencies.clear();
+            }
+
+            // Builds up the dependents and dependencies for each node of the graph
+            for (int i = 0; i < count; i++) {
+                final Node node = nodes.get(i);
+
+                final LayoutParams layoutParams = (LayoutParams) node.view.getLayoutParams();
+                final int[] rules = layoutParams.mRules;
+                final int rulesCount = rulesFilter.length;
+
+                // Look only the the rules passed in parameter, this way we build only the
+                // dependencies for a specific set of rules
+                for (int j = 0; j < rulesCount; j++) {
+                    final int rule = rules[rulesFilter[j]];
+                    if (rule > 0) {
+                        // The node this node depends on
+                        final Node dependency = keyNodes.get(rule);
+                        // Skip unknowns and self dependencies
+                        if (dependency == null || dependency == node) {
+                            continue;
+                        }
+                        // Add the current node as a dependent
+                        dependency.dependents.put(node, this);
+                        // Add a dependency to the current node
+                        node.dependencies.put(rule, dependency);
+                    }
+                }
+            }
+
+            final ArrayDeque<Node> roots = mRoots;
+            roots.clear();
+
+            // Finds all the roots in the graph: all nodes with no dependencies
+            for (int i = 0; i < count; i++) {
+                final Node node = nodes.get(i);
+                if (node.dependencies.size() == 0) roots.addLast(node);
+            }
+
+            return roots;
+        }
+
+        /**
+         * A node in the dependency graph. A node is a view, its list of dependencies
+         * and its list of dependents.
+         *
+         * A node with no dependent is considered a root of the graph.
+         */
+        static class Node {
+            /**
+             * The view representing this node in the layout.
+             */
+            View view;
+
+            /**
+             * The list of dependents for this node; a dependent is a node
+             * that needs this node to be processed first.
+             */
+            final ArrayMap<Node, DependencyGraph> dependents =
+                    new ArrayMap<Node, DependencyGraph>();
+
+            /**
+             * The list of dependencies for this node.
+             */
+            final SparseArray<Node> dependencies = new SparseArray<Node>();
+
+            /*
+             * START POOL IMPLEMENTATION
+             */
+            // The pool is static, so all nodes instances are shared across
+            // activities, that's why we give it a rather high limit
+            private static final int POOL_LIMIT = 100;
+            private static final SynchronizedPool<Node> sPool =
+                    new SynchronizedPool<Node>(POOL_LIMIT);
+
+            static Node acquire(View view) {
+                Node node = sPool.acquire();
+                if (node == null) {
+                    node = new Node();
+                }
+                node.view = view;
+                return node;
+            }
+
+            void release() {
+                view = null;
+                dependents.clear();
+                dependencies.clear();
+
+                sPool.release(this);
+            }
+            /*
+             * END POOL IMPLEMENTATION
+             */
+        }
+    }
+}
diff --git a/android/widget/RemoteViews.java b/android/widget/RemoteViews.java
new file mode 100644
index 0000000..bc85fad
--- /dev/null
+++ b/android/widget/RemoteViews.java
@@ -0,0 +1,3875 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.ColorInt;
+import android.annotation.DimenRes;
+import android.app.ActivityManager.StackId;
+import android.app.ActivityOptions;
+import android.app.ActivityThread;
+import android.app.Application;
+import android.app.PendingIntent;
+import android.app.RemoteInput;
+import android.appwidget.AppWidgetHostView;
+import android.content.Context;
+import android.content.ContextWrapper;
+import android.content.Intent;
+import android.content.IntentSender;
+import android.content.pm.ApplicationInfo;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.Icon;
+import android.net.Uri;
+import android.os.AsyncTask;
+import android.os.Binder;
+import android.os.Build;
+import android.os.Bundle;
+import android.os.CancellationSignal;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.Process;
+import android.os.StrictMode;
+import android.os.UserHandle;
+import android.text.TextUtils;
+import android.util.ArrayMap;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.LayoutInflater.Filter;
+import android.view.RemotableViewMethod;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+import android.view.ViewStub;
+import android.widget.AdapterView.OnItemClickListener;
+
+import com.android.internal.R;
+import com.android.internal.util.NotificationColorUtil;
+import com.android.internal.util.Preconditions;
+
+import libcore.util.Objects;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.lang.invoke.MethodHandle;
+import java.lang.invoke.MethodHandles;
+import java.lang.invoke.MethodType;
+import java.lang.reflect.Method;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.Stack;
+import java.util.concurrent.Executor;
+
+/**
+ * A class that describes a view hierarchy that can be displayed in
+ * another process. The hierarchy is inflated from a layout resource
+ * file, and this class provides some basic operations for modifying
+ * the content of the inflated hierarchy.
+ */
+public class RemoteViews implements Parcelable, Filter {
+
+    private static final String LOG_TAG = "RemoteViews";
+
+    /**
+     * The intent extra that contains the appWidgetId.
+     * @hide
+     */
+    static final String EXTRA_REMOTEADAPTER_APPWIDGET_ID = "remoteAdapterAppWidgetId";
+
+    /**
+     * Maximum depth of nested views calls from {@link #addView(int, RemoteViews)} and
+     * {@link #RemoteViews(RemoteViews, RemoteViews)}.
+     */
+    private static final int MAX_NESTED_VIEWS = 10;
+
+    // The unique identifiers for each custom {@link Action}.
+    private static final int SET_ON_CLICK_PENDING_INTENT_TAG = 1;
+    private static final int REFLECTION_ACTION_TAG = 2;
+    private static final int SET_DRAWABLE_PARAMETERS_TAG = 3;
+    private static final int VIEW_GROUP_ACTION_ADD_TAG = 4;
+    private static final int SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG = 5;
+    private static final int SET_EMPTY_VIEW_ACTION_TAG = 6;
+    private static final int VIEW_GROUP_ACTION_REMOVE_TAG = 7;
+    private static final int SET_PENDING_INTENT_TEMPLATE_TAG = 8;
+    private static final int SET_ON_CLICK_FILL_IN_INTENT_TAG = 9;
+    private static final int SET_REMOTE_VIEW_ADAPTER_INTENT_TAG = 10;
+    private static final int TEXT_VIEW_DRAWABLE_ACTION_TAG = 11;
+    private static final int BITMAP_REFLECTION_ACTION_TAG = 12;
+    private static final int TEXT_VIEW_SIZE_ACTION_TAG = 13;
+    private static final int VIEW_PADDING_ACTION_TAG = 14;
+    private static final int SET_REMOTE_VIEW_ADAPTER_LIST_TAG = 15;
+    private static final int TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG = 17;
+    private static final int SET_REMOTE_INPUTS_ACTION_TAG = 18;
+    private static final int LAYOUT_PARAM_ACTION_TAG = 19;
+    private static final int OVERRIDE_TEXT_COLORS_TAG = 20;
+
+    /**
+     * Application that hosts the remote views.
+     *
+     * @hide
+     */
+    private ApplicationInfo mApplication;
+
+    /**
+     * The resource ID of the layout file. (Added to the parcel)
+     */
+    private final int mLayoutId;
+
+    /**
+     * An array of actions to perform on the view tree once it has been
+     * inflated
+     */
+    private ArrayList<Action> mActions;
+
+    /**
+     * Maps bitmaps to unique indicies to avoid Bitmap duplication.
+     */
+    private BitmapCache mBitmapCache;
+
+    /**
+     * Indicates whether or not this RemoteViews object is contained as a child of any other
+     * RemoteViews.
+     */
+    private boolean mIsRoot = true;
+
+    /**
+     * Whether reapply is disallowed on this remoteview. This maybe be true if some actions modify
+     * the layout in a way that isn't recoverable, since views are being removed.
+     */
+    private boolean mReapplyDisallowed;
+
+    /**
+     * Constants to whether or not this RemoteViews is composed of a landscape and portrait
+     * RemoteViews.
+     */
+    private static final int MODE_NORMAL = 0;
+    private static final int MODE_HAS_LANDSCAPE_AND_PORTRAIT = 1;
+
+    /**
+     * Used in conjunction with the special constructor
+     * {@link #RemoteViews(RemoteViews, RemoteViews)} to keep track of the landscape and portrait
+     * RemoteViews.
+     */
+    private RemoteViews mLandscape = null;
+    private RemoteViews mPortrait = null;
+
+    /**
+     * This flag indicates whether this RemoteViews object is being created from a
+     * RemoteViewsService for use as a child of a widget collection. This flag is used
+     * to determine whether or not certain features are available, in particular,
+     * setting on click extras and setting on click pending intents. The former is enabled,
+     * and the latter disabled when this flag is true.
+     */
+    private boolean mIsWidgetCollectionChild = false;
+
+    private static final OnClickHandler DEFAULT_ON_CLICK_HANDLER = new OnClickHandler();
+
+    private static final ArrayMap<MethodKey, MethodArgs> sMethods = new ArrayMap<>();
+
+    /**
+     * This key is used to perform lookups in sMethods without causing allocations.
+     */
+    private static final MethodKey sLookupKey = new MethodKey();
+
+    /**
+     * @hide
+     */
+    public void setRemoteInputs(int viewId, RemoteInput[] remoteInputs) {
+        mActions.add(new SetRemoteInputsAction(viewId, remoteInputs));
+    }
+
+    /**
+     * Reduces all images and ensures that they are all below the given sizes.
+     *
+     * @param maxWidth the maximum width allowed
+     * @param maxHeight the maximum height allowed
+     *
+     * @hide
+     */
+    public void reduceImageSizes(int maxWidth, int maxHeight) {
+        ArrayList<Bitmap> cache = mBitmapCache.mBitmaps;
+        for (int i = 0; i < cache.size(); i++) {
+            Bitmap bitmap = cache.get(i);
+            cache.set(i, Icon.scaleDownIfNecessary(bitmap, maxWidth, maxHeight));
+        }
+    }
+
+    /**
+     * Override all text colors in this layout and replace them by the given text color.
+     *
+     * @param textColor The color to use.
+     *
+     * @hide
+     */
+    public void overrideTextColors(int textColor) {
+        addAction(new OverrideTextColorsAction(textColor));
+    }
+
+    /**
+     * Set that it is disallowed to reapply another remoteview with the same layout as this view.
+     * This should be done if an action is destroying the view tree of the base layout.
+     *
+     * @hide
+     */
+    public void setReapplyDisallowed() {
+        mReapplyDisallowed = true;
+    }
+
+    /**
+     * @return Whether it is disallowed to reapply another remoteview with the same layout as this
+     * view. True if this remoteview has actions that destroyed view tree of the base layout.
+     *
+     * @hide
+     */
+    public boolean isReapplyDisallowed() {
+        return mReapplyDisallowed;
+    }
+
+    /**
+     * Stores information related to reflection method lookup.
+     */
+    static class MethodKey {
+        public Class targetClass;
+        public Class paramClass;
+        public String methodName;
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof MethodKey)) {
+                return false;
+            }
+            MethodKey p = (MethodKey) o;
+            return Objects.equal(p.targetClass, targetClass)
+                    && Objects.equal(p.paramClass, paramClass)
+                    && Objects.equal(p.methodName, methodName);
+        }
+
+        @Override
+        public int hashCode() {
+            return Objects.hashCode(targetClass) ^ Objects.hashCode(paramClass)
+                    ^ Objects.hashCode(methodName);
+        }
+
+        public void set(Class targetClass, Class paramClass, String methodName) {
+            this.targetClass = targetClass;
+            this.paramClass = paramClass;
+            this.methodName = methodName;
+        }
+    }
+
+
+    /**
+     * Stores information related to reflection method lookup result.
+     */
+    static class MethodArgs {
+        public MethodHandle syncMethod;
+        public MethodHandle asyncMethod;
+        public String asyncMethodName;
+    }
+
+    /**
+     * This annotation indicates that a subclass of View is allowed to be used
+     * with the {@link RemoteViews} mechanism.
+     */
+    @Target({ ElementType.TYPE })
+    @Retention(RetentionPolicy.RUNTIME)
+    public @interface RemoteView {
+    }
+
+    /**
+     * Exception to send when something goes wrong executing an action
+     *
+     */
+    public static class ActionException extends RuntimeException {
+        public ActionException(Exception ex) {
+            super(ex);
+        }
+        public ActionException(String message) {
+            super(message);
+        }
+        /**
+         * @hide
+         */
+        public ActionException(Throwable t) {
+            super(t);
+        }
+    }
+
+    /** @hide */
+    public static class OnClickHandler {
+
+        private int mEnterAnimationId;
+
+        public boolean onClickHandler(View view, PendingIntent pendingIntent,
+                Intent fillInIntent) {
+            return onClickHandler(view, pendingIntent, fillInIntent, StackId.INVALID_STACK_ID);
+        }
+
+        public boolean onClickHandler(View view, PendingIntent pendingIntent,
+                Intent fillInIntent, int launchStackId) {
+            try {
+                // TODO: Unregister this handler if PendingIntent.FLAG_ONE_SHOT?
+                Context context = view.getContext();
+                ActivityOptions opts;
+                if (mEnterAnimationId != 0) {
+                    opts = ActivityOptions.makeCustomAnimation(context, mEnterAnimationId, 0);
+                } else {
+                    opts = ActivityOptions.makeBasic();
+                }
+
+                if (launchStackId != StackId.INVALID_STACK_ID) {
+                    opts.setLaunchStackId(launchStackId);
+                }
+                context.startIntentSender(
+                        pendingIntent.getIntentSender(), fillInIntent,
+                        Intent.FLAG_ACTIVITY_NEW_TASK,
+                        Intent.FLAG_ACTIVITY_NEW_TASK, 0, opts.toBundle());
+            } catch (IntentSender.SendIntentException e) {
+                android.util.Log.e(LOG_TAG, "Cannot send pending intent: ", e);
+                return false;
+            } catch (Exception e) {
+                android.util.Log.e(LOG_TAG, "Cannot send pending intent due to " +
+                        "unknown exception: ", e);
+                return false;
+            }
+            return true;
+        }
+
+        public void setEnterAnimationId(int enterAnimationId) {
+            mEnterAnimationId = enterAnimationId;
+        }
+    }
+
+    /**
+     * Base class for all actions that can be performed on an
+     * inflated view.
+     *
+     *  SUBCLASSES MUST BE IMMUTABLE SO CLONE WORKS!!!!!
+     */
+    private abstract static class Action implements Parcelable {
+        public abstract void apply(View root, ViewGroup rootParent,
+                OnClickHandler handler) throws ActionException;
+
+        public static final int MERGE_REPLACE = 0;
+        public static final int MERGE_APPEND = 1;
+        public static final int MERGE_IGNORE = 2;
+
+        public int describeContents() {
+            return 0;
+        }
+
+        public void setBitmapCache(BitmapCache bitmapCache) {
+            // Do nothing
+        }
+
+        public int mergeBehavior() {
+            return MERGE_REPLACE;
+        }
+
+        public abstract String getActionName();
+
+        public String getUniqueKey() {
+            return (getActionName() + viewId);
+        }
+
+        /**
+         * This is called on the background thread. It should perform any non-ui computations
+         * and return the final action which will run on the UI thread.
+         * Override this if some of the tasks can be performed async.
+         */
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+            return this;
+        }
+
+        public boolean prefersAsyncApply() {
+            return false;
+        }
+
+        /**
+         * Overridden by subclasses which have (or inherit) an ApplicationInfo instance
+         * as member variable
+         */
+        public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
+            return true;
+        }
+
+        int viewId;
+    }
+
+    /**
+     * Action class used during async inflation of RemoteViews. Subclasses are not parcelable.
+     */
+    private static abstract class RuntimeAction extends Action {
+        @Override
+        public final String getActionName() {
+            return "RuntimeAction";
+        }
+
+        @Override
+        public final void writeToParcel(Parcel dest, int flags) {
+            throw new UnsupportedOperationException();
+        }
+    }
+
+    // Constant used during async execution. It is not parcelable.
+    private static final Action ACTION_NOOP = new RuntimeAction() {
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) { }
+    };
+
+    /**
+     * Merges the passed RemoteViews actions with this RemoteViews actions according to
+     * action-specific merge rules.
+     *
+     * @param newRv
+     *
+     * @hide
+     */
+    public void mergeRemoteViews(RemoteViews newRv) {
+        if (newRv == null) return;
+        // We first copy the new RemoteViews, as the process of merging modifies the way the actions
+        // reference the bitmap cache. We don't want to modify the object as it may need to
+        // be merged and applied multiple times.
+        RemoteViews copy = new RemoteViews(newRv);
+
+        HashMap<String, Action> map = new HashMap<String, Action>();
+        if (mActions == null) {
+            mActions = new ArrayList<Action>();
+        }
+
+        int count = mActions.size();
+        for (int i = 0; i < count; i++) {
+            Action a = mActions.get(i);
+            map.put(a.getUniqueKey(), a);
+        }
+
+        ArrayList<Action> newActions = copy.mActions;
+        if (newActions == null) return;
+        count = newActions.size();
+        for (int i = 0; i < count; i++) {
+            Action a = newActions.get(i);
+            String key = newActions.get(i).getUniqueKey();
+            int mergeBehavior = newActions.get(i).mergeBehavior();
+            if (map.containsKey(key) && mergeBehavior == Action.MERGE_REPLACE) {
+                mActions.remove(map.get(key));
+                map.remove(key);
+            }
+
+            // If the merge behavior is ignore, we don't bother keeping the extra action
+            if (mergeBehavior == Action.MERGE_REPLACE || mergeBehavior == Action.MERGE_APPEND) {
+                mActions.add(a);
+            }
+        }
+
+        // Because pruning can remove the need for bitmaps, we reconstruct the bitmap cache
+        mBitmapCache = new BitmapCache();
+        setBitmapCache(mBitmapCache);
+    }
+
+    private static class RemoteViewsContextWrapper extends ContextWrapper {
+        private final Context mContextForResources;
+
+        RemoteViewsContextWrapper(Context context, Context contextForResources) {
+            super(context);
+            mContextForResources = contextForResources;
+        }
+
+        @Override
+        public Resources getResources() {
+            return mContextForResources.getResources();
+        }
+
+        @Override
+        public Resources.Theme getTheme() {
+            return mContextForResources.getTheme();
+        }
+
+        @Override
+        public String getPackageName() {
+            return mContextForResources.getPackageName();
+        }
+    }
+
+    private class SetEmptyView extends Action {
+        int viewId;
+        int emptyViewId;
+
+        SetEmptyView(int viewId, int emptyViewId) {
+            this.viewId = viewId;
+            this.emptyViewId = emptyViewId;
+        }
+
+        SetEmptyView(Parcel in) {
+            this.viewId = in.readInt();
+            this.emptyViewId = in.readInt();
+        }
+
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(SET_EMPTY_VIEW_ACTION_TAG);
+            out.writeInt(this.viewId);
+            out.writeInt(this.emptyViewId);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View view = root.findViewById(viewId);
+            if (!(view instanceof AdapterView<?>)) return;
+
+            AdapterView<?> adapterView = (AdapterView<?>) view;
+
+            final View emptyView = root.findViewById(emptyViewId);
+            if (emptyView == null) return;
+
+            adapterView.setEmptyView(emptyView);
+        }
+
+        public String getActionName() {
+            return "SetEmptyView";
+        }
+    }
+
+    private class SetOnClickFillInIntent extends Action {
+        public SetOnClickFillInIntent(int id, Intent fillInIntent) {
+            this.viewId = id;
+            this.fillInIntent = fillInIntent;
+        }
+
+        public SetOnClickFillInIntent(Parcel parcel) {
+            viewId = parcel.readInt();
+            fillInIntent = Intent.CREATOR.createFromParcel(parcel);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_ON_CLICK_FILL_IN_INTENT_TAG);
+            dest.writeInt(viewId);
+            fillInIntent.writeToParcel(dest, 0 /* no flags */);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            if (!mIsWidgetCollectionChild) {
+                Log.e(LOG_TAG, "The method setOnClickFillInIntent is available " +
+                        "only from RemoteViewsFactory (ie. on collection items).");
+                return;
+            }
+            if (target == root) {
+                target.setTagInternal(com.android.internal.R.id.fillInIntent, fillInIntent);
+            } else if (fillInIntent != null) {
+                OnClickListener listener = new OnClickListener() {
+                    public void onClick(View v) {
+                        // Insure that this view is a child of an AdapterView
+                        View parent = (View) v.getParent();
+                        // Break the for loop on the first encounter of:
+                        //    1) an AdapterView,
+                        //    2) an AppWidgetHostView that is not a RemoteViewsFrameLayout, or
+                        //    3) a null parent.
+                        // 2) and 3) are unexpected and catch the case where a child is not
+                        // correctly parented in an AdapterView.
+                        while (parent != null && !(parent instanceof AdapterView<?>)
+                                && !((parent instanceof AppWidgetHostView) &&
+                                    !(parent instanceof RemoteViewsAdapter.RemoteViewsFrameLayout))) {
+                            parent = (View) parent.getParent();
+                        }
+
+                        if (!(parent instanceof AdapterView<?>)) {
+                            // Somehow they've managed to get this far without having
+                            // and AdapterView as a parent.
+                            Log.e(LOG_TAG, "Collection item doesn't have AdapterView parent");
+                            return;
+                        }
+
+                        // Insure that a template pending intent has been set on an ancestor
+                        if (!(parent.getTag() instanceof PendingIntent)) {
+                            Log.e(LOG_TAG, "Attempting setOnClickFillInIntent without" +
+                                    " calling setPendingIntentTemplate on parent.");
+                            return;
+                        }
+
+                        PendingIntent pendingIntent = (PendingIntent) parent.getTag();
+
+                        final Rect rect = getSourceBounds(v);
+
+                        fillInIntent.setSourceBounds(rect);
+                        handler.onClickHandler(v, pendingIntent, fillInIntent);
+                    }
+
+                };
+                target.setOnClickListener(listener);
+            }
+        }
+
+        public String getActionName() {
+            return "SetOnClickFillInIntent";
+        }
+
+        Intent fillInIntent;
+    }
+
+    private class SetPendingIntentTemplate extends Action {
+        public SetPendingIntentTemplate(int id, PendingIntent pendingIntentTemplate) {
+            this.viewId = id;
+            this.pendingIntentTemplate = pendingIntentTemplate;
+        }
+
+        public SetPendingIntentTemplate(Parcel parcel) {
+            viewId = parcel.readInt();
+            pendingIntentTemplate = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_PENDING_INTENT_TEMPLATE_TAG);
+            dest.writeInt(viewId);
+            pendingIntentTemplate.writeToParcel(dest, 0 /* no flags */);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // If the view isn't an AdapterView, setting a PendingIntent template doesn't make sense
+            if (target instanceof AdapterView<?>) {
+                AdapterView<?> av = (AdapterView<?>) target;
+                // The PendingIntent template is stored in the view's tag.
+                OnItemClickListener listener = new OnItemClickListener() {
+                    public void onItemClick(AdapterView<?> parent, View view,
+                            int position, long id) {
+                        // The view should be a frame layout
+                        if (view instanceof ViewGroup) {
+                            ViewGroup vg = (ViewGroup) view;
+
+                            // AdapterViews contain their children in a frame
+                            // so we need to go one layer deeper here.
+                            if (parent instanceof AdapterViewAnimator) {
+                                vg = (ViewGroup) vg.getChildAt(0);
+                            }
+                            if (vg == null) return;
+
+                            Intent fillInIntent = null;
+                            int childCount = vg.getChildCount();
+                            for (int i = 0; i < childCount; i++) {
+                                Object tag = vg.getChildAt(i).getTag(com.android.internal.R.id.fillInIntent);
+                                if (tag instanceof Intent) {
+                                    fillInIntent = (Intent) tag;
+                                    break;
+                                }
+                            }
+                            if (fillInIntent == null) return;
+
+                            final Rect rect = getSourceBounds(view);
+
+                            final Intent intent = new Intent();
+                            intent.setSourceBounds(rect);
+                            handler.onClickHandler(view, pendingIntentTemplate, fillInIntent);
+                        }
+                    }
+                };
+                av.setOnItemClickListener(listener);
+                av.setTag(pendingIntentTemplate);
+            } else {
+                Log.e(LOG_TAG, "Cannot setPendingIntentTemplate on a view which is not" +
+                        "an AdapterView (id: " + viewId + ")");
+                return;
+            }
+        }
+
+        public String getActionName() {
+            return "SetPendingIntentTemplate";
+        }
+
+        PendingIntent pendingIntentTemplate;
+    }
+
+    private class SetRemoteViewsAdapterList extends Action {
+        public SetRemoteViewsAdapterList(int id, ArrayList<RemoteViews> list, int viewTypeCount) {
+            this.viewId = id;
+            this.list = list;
+            this.viewTypeCount = viewTypeCount;
+        }
+
+        public SetRemoteViewsAdapterList(Parcel parcel) {
+            viewId = parcel.readInt();
+            viewTypeCount = parcel.readInt();
+            int count = parcel.readInt();
+            list = new ArrayList<RemoteViews>();
+
+            for (int i = 0; i < count; i++) {
+                RemoteViews rv = RemoteViews.CREATOR.createFromParcel(parcel);
+                list.add(rv);
+            }
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_REMOTE_VIEW_ADAPTER_LIST_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(viewTypeCount);
+
+            if (list == null || list.size() == 0) {
+                dest.writeInt(0);
+            } else {
+                int count = list.size();
+                dest.writeInt(count);
+                for (int i = 0; i < count; i++) {
+                    RemoteViews rv = list.get(i);
+                    rv.writeToParcel(dest, flags);
+                }
+            }
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // Ensure that we are applying to an AppWidget root
+            if (!(rootParent instanceof AppWidgetHostView)) {
+                Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " +
+                        "AppWidgets (root id: " + viewId + ")");
+                return;
+            }
+            // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it
+            if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) {
+                Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " +
+                        "an AbsListView or AdapterViewAnimator (id: " + viewId + ")");
+                return;
+            }
+
+            if (target instanceof AbsListView) {
+                AbsListView v = (AbsListView) target;
+                Adapter a = v.getAdapter();
+                if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
+                    ((RemoteViewsListAdapter) a).setViewsList(list);
+                } else {
+                    v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount));
+                }
+            } else if (target instanceof AdapterViewAnimator) {
+                AdapterViewAnimator v = (AdapterViewAnimator) target;
+                Adapter a = v.getAdapter();
+                if (a instanceof RemoteViewsListAdapter && viewTypeCount <= a.getViewTypeCount()) {
+                    ((RemoteViewsListAdapter) a).setViewsList(list);
+                } else {
+                    v.setAdapter(new RemoteViewsListAdapter(v.getContext(), list, viewTypeCount));
+                }
+            }
+        }
+
+        public String getActionName() {
+            return "SetRemoteViewsAdapterList";
+        }
+
+        int viewTypeCount;
+        ArrayList<RemoteViews> list;
+    }
+
+    private class SetRemoteViewsAdapterIntent extends Action {
+        public SetRemoteViewsAdapterIntent(int id, Intent intent) {
+            this.viewId = id;
+            this.intent = intent;
+        }
+
+        public SetRemoteViewsAdapterIntent(Parcel parcel) {
+            viewId = parcel.readInt();
+            intent = Intent.CREATOR.createFromParcel(parcel);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_REMOTE_VIEW_ADAPTER_INTENT_TAG);
+            dest.writeInt(viewId);
+            intent.writeToParcel(dest, flags);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // Ensure that we are applying to an AppWidget root
+            if (!(rootParent instanceof AppWidgetHostView)) {
+                Log.e(LOG_TAG, "SetRemoteViewsAdapterIntent action can only be used for " +
+                        "AppWidgets (root id: " + viewId + ")");
+                return;
+            }
+            // Ensure that we are calling setRemoteAdapter on an AdapterView that supports it
+            if (!(target instanceof AbsListView) && !(target instanceof AdapterViewAnimator)) {
+                Log.e(LOG_TAG, "Cannot setRemoteViewsAdapter on a view which is not " +
+                        "an AbsListView or AdapterViewAnimator (id: " + viewId + ")");
+                return;
+            }
+
+            // Embed the AppWidget Id for use in RemoteViewsAdapter when connecting to the intent
+            // RemoteViewsService
+            AppWidgetHostView host = (AppWidgetHostView) rootParent;
+            intent.putExtra(EXTRA_REMOTEADAPTER_APPWIDGET_ID, host.getAppWidgetId());
+            if (target instanceof AbsListView) {
+                AbsListView v = (AbsListView) target;
+                v.setRemoteViewsAdapter(intent, isAsync);
+                v.setRemoteViewsOnClickHandler(handler);
+            } else if (target instanceof AdapterViewAnimator) {
+                AdapterViewAnimator v = (AdapterViewAnimator) target;
+                v.setRemoteViewsAdapter(intent, isAsync);
+                v.setRemoteViewsOnClickHandler(handler);
+            }
+        }
+
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent,
+                OnClickHandler handler) {
+            SetRemoteViewsAdapterIntent copy = new SetRemoteViewsAdapterIntent(viewId, intent);
+            copy.isAsync = true;
+            return copy;
+        }
+
+        public String getActionName() {
+            return "SetRemoteViewsAdapterIntent";
+        }
+
+        Intent intent;
+        boolean isAsync = false;
+    }
+
+    /**
+     * Equivalent to calling
+     * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
+     * to launch the provided {@link PendingIntent}.
+     */
+    private class SetOnClickPendingIntent extends Action {
+        public SetOnClickPendingIntent(int id, PendingIntent pendingIntent) {
+            this.viewId = id;
+            this.pendingIntent = pendingIntent;
+        }
+
+        public SetOnClickPendingIntent(Parcel parcel) {
+            viewId = parcel.readInt();
+
+            // We check a flag to determine if the parcel contains a PendingIntent.
+            if (parcel.readInt() != 0) {
+                pendingIntent = PendingIntent.readPendingIntentOrNullFromParcel(parcel);
+            }
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_ON_CLICK_PENDING_INTENT_TAG);
+            dest.writeInt(viewId);
+
+            // We use a flag to indicate whether the parcel contains a valid object.
+            dest.writeInt(pendingIntent != null ? 1 : 0);
+            if (pendingIntent != null) {
+                pendingIntent.writeToParcel(dest, 0 /* no flags */);
+            }
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, final OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // If the view is an AdapterView, setting a PendingIntent on click doesn't make much
+            // sense, do they mean to set a PendingIntent template for the AdapterView's children?
+            if (mIsWidgetCollectionChild) {
+                Log.w(LOG_TAG, "Cannot setOnClickPendingIntent for collection item " +
+                        "(id: " + viewId + ")");
+                ApplicationInfo appInfo = root.getContext().getApplicationInfo();
+
+                // We let this slide for HC and ICS so as to not break compatibility. It should have
+                // been disabled from the outset, but was left open by accident.
+                if (appInfo != null &&
+                        appInfo.targetSdkVersion >= Build.VERSION_CODES.JELLY_BEAN) {
+                    return;
+                }
+            }
+
+            // If the pendingIntent is null, we clear the onClickListener
+            OnClickListener listener = null;
+            if (pendingIntent != null) {
+                listener = new OnClickListener() {
+                    public void onClick(View v) {
+                        // Find target view location in screen coordinates and
+                        // fill into PendingIntent before sending.
+                        final Rect rect = getSourceBounds(v);
+
+                        final Intent intent = new Intent();
+                        intent.setSourceBounds(rect);
+                        handler.onClickHandler(v, pendingIntent, intent);
+                    }
+                };
+            }
+            target.setOnClickListener(listener);
+        }
+
+        public String getActionName() {
+            return "SetOnClickPendingIntent";
+        }
+
+        PendingIntent pendingIntent;
+    }
+
+    private static Rect getSourceBounds(View v) {
+        final float appScale = v.getContext().getResources()
+                .getCompatibilityInfo().applicationScale;
+        final int[] pos = new int[2];
+        v.getLocationOnScreen(pos);
+
+        final Rect rect = new Rect();
+        rect.left = (int) (pos[0] * appScale + 0.5f);
+        rect.top = (int) (pos[1] * appScale + 0.5f);
+        rect.right = (int) ((pos[0] + v.getWidth()) * appScale + 0.5f);
+        rect.bottom = (int) ((pos[1] + v.getHeight()) * appScale + 0.5f);
+        return rect;
+    }
+
+    private MethodHandle getMethod(View view, String methodName, Class<?> paramType,
+            boolean async) {
+        MethodArgs result;
+        Class<? extends View> klass = view.getClass();
+
+        synchronized (sMethods) {
+            // The key is defined by the view class, param class and method name.
+            sLookupKey.set(klass, paramType, methodName);
+            result = sMethods.get(sLookupKey);
+
+            if (result == null) {
+                Method method;
+                try {
+                    if (paramType == null) {
+                        method = klass.getMethod(methodName);
+                    } else {
+                        method = klass.getMethod(methodName, paramType);
+                    }
+                    if (!method.isAnnotationPresent(RemotableViewMethod.class)) {
+                        throw new ActionException("view: " + klass.getName()
+                                + " can't use method with RemoteViews: "
+                                + methodName + getParameters(paramType));
+                    }
+
+                    result = new MethodArgs();
+                    result.syncMethod = MethodHandles.publicLookup().unreflect(method);
+                    result.asyncMethodName =
+                            method.getAnnotation(RemotableViewMethod.class).asyncImpl();
+                } catch (NoSuchMethodException | IllegalAccessException ex) {
+                    throw new ActionException("view: " + klass.getName() + " doesn't have method: "
+                            + methodName + getParameters(paramType));
+                }
+
+                MethodKey key = new MethodKey();
+                key.set(klass, paramType, methodName);
+                sMethods.put(key, result);
+            }
+
+            if (!async) {
+                return result.syncMethod;
+            }
+            // Check this so see if async method is implemented or not.
+            if (result.asyncMethodName.isEmpty()) {
+                return null;
+            }
+            // Async method is lazily loaded. If it is not yet loaded, load now.
+            if (result.asyncMethod == null) {
+                MethodType asyncType = result.syncMethod.type()
+                        .dropParameterTypes(0, 1).changeReturnType(Runnable.class);
+                try {
+                    result.asyncMethod = MethodHandles.publicLookup().findVirtual(
+                            klass, result.asyncMethodName, asyncType);
+                } catch (NoSuchMethodException | IllegalAccessException ex) {
+                    throw new ActionException("Async implementation declared as "
+                            + result.asyncMethodName + " but not defined for " + methodName
+                            + ": public Runnable " + result.asyncMethodName + " ("
+                            + TextUtils.join(",", asyncType.parameterArray()) + ")");
+                }
+            }
+            return result.asyncMethod;
+        }
+    }
+
+    private static String getParameters(Class<?> paramType) {
+        if (paramType == null) return "()";
+        return "(" + paramType + ")";
+    }
+
+    /**
+     * Equivalent to calling a combination of {@link Drawable#setAlpha(int)},
+     * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
+     * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given view.
+     * <p>
+     * These operations will be performed on the {@link Drawable} returned by the
+     * target {@link View#getBackground()} by default.  If targetBackground is false,
+     * we assume the target is an {@link ImageView} and try applying the operations
+     * to {@link ImageView#getDrawable()}.
+     * <p>
+     * You can omit specific calls by marking their values with null or -1.
+     */
+    private class SetDrawableParameters extends Action {
+        public SetDrawableParameters(int id, boolean targetBackground, int alpha,
+                int colorFilter, PorterDuff.Mode mode, int level) {
+            this.viewId = id;
+            this.targetBackground = targetBackground;
+            this.alpha = alpha;
+            this.colorFilter = colorFilter;
+            this.filterMode = mode;
+            this.level = level;
+        }
+
+        public SetDrawableParameters(Parcel parcel) {
+            viewId = parcel.readInt();
+            targetBackground = parcel.readInt() != 0;
+            alpha = parcel.readInt();
+            colorFilter = parcel.readInt();
+            boolean hasMode = parcel.readInt() != 0;
+            if (hasMode) {
+                filterMode = PorterDuff.Mode.valueOf(parcel.readString());
+            } else {
+                filterMode = null;
+            }
+            level = parcel.readInt();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_DRAWABLE_PARAMETERS_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(targetBackground ? 1 : 0);
+            dest.writeInt(alpha);
+            dest.writeInt(colorFilter);
+            if (filterMode != null) {
+                dest.writeInt(1);
+                dest.writeString(filterMode.toString());
+            } else {
+                dest.writeInt(0);
+            }
+            dest.writeInt(level);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            // Pick the correct drawable to modify for this view
+            Drawable targetDrawable = null;
+            if (targetBackground) {
+                targetDrawable = target.getBackground();
+            } else if (target instanceof ImageView) {
+                ImageView imageView = (ImageView) target;
+                targetDrawable = imageView.getDrawable();
+            }
+
+            if (targetDrawable != null) {
+                // Perform modifications only if values are set correctly
+                if (alpha != -1) {
+                    targetDrawable.mutate().setAlpha(alpha);
+                }
+                if (filterMode != null) {
+                    targetDrawable.mutate().setColorFilter(colorFilter, filterMode);
+                }
+                if (level != -1) {
+                    targetDrawable.mutate().setLevel(level);
+                }
+            }
+        }
+
+        public String getActionName() {
+            return "SetDrawableParameters";
+        }
+
+        boolean targetBackground;
+        int alpha;
+        int colorFilter;
+        PorterDuff.Mode filterMode;
+        int level;
+    }
+
+    private final class ReflectionActionWithoutParams extends Action {
+        final String methodName;
+
+        ReflectionActionWithoutParams(int viewId, String methodName) {
+            this.viewId = viewId;
+            this.methodName = methodName;
+        }
+
+        ReflectionActionWithoutParams(Parcel in) {
+            this.viewId = in.readInt();
+            this.methodName = in.readString();
+        }
+
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG);
+            out.writeInt(this.viewId);
+            out.writeString(this.methodName);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View view = root.findViewById(viewId);
+            if (view == null) return;
+
+            try {
+                getMethod(view, this.methodName, null, false /* async */).invoke(view);
+            } catch (Throwable ex) {
+                throw new ActionException(ex);
+            }
+        }
+
+        public int mergeBehavior() {
+            // we don't need to build up showNext or showPrevious calls
+            if (methodName.equals("showNext") || methodName.equals("showPrevious")) {
+                return MERGE_IGNORE;
+            } else {
+                return MERGE_REPLACE;
+            }
+        }
+
+        public String getActionName() {
+            return "ReflectionActionWithoutParams";
+        }
+    }
+
+    private static class BitmapCache {
+
+        ArrayList<Bitmap> mBitmaps;
+        int mBitmapMemory = -1;
+
+        public BitmapCache() {
+            mBitmaps = new ArrayList<>();
+        }
+
+        public BitmapCache(Parcel source) {
+            int count = source.readInt();
+            mBitmaps = new ArrayList<>(count);
+            for (int i = 0; i < count; i++) {
+                Bitmap b = Bitmap.CREATOR.createFromParcel(source);
+                mBitmaps.add(b);
+            }
+        }
+
+        public int getBitmapId(Bitmap b) {
+            if (b == null) {
+                return -1;
+            } else {
+                if (mBitmaps.contains(b)) {
+                    return mBitmaps.indexOf(b);
+                } else {
+                    mBitmaps.add(b);
+                    mBitmapMemory = -1;
+                    return (mBitmaps.size() - 1);
+                }
+            }
+        }
+
+        public Bitmap getBitmapForId(int id) {
+            if (id == -1 || id >= mBitmaps.size()) {
+                return null;
+            } else {
+                return mBitmaps.get(id);
+            }
+        }
+
+        public void writeBitmapsToParcel(Parcel dest, int flags) {
+            int count = mBitmaps.size();
+            dest.writeInt(count);
+            for (int i = 0; i < count; i++) {
+                mBitmaps.get(i).writeToParcel(dest, flags);
+            }
+        }
+
+        public int getBitmapMemory() {
+            if (mBitmapMemory < 0) {
+                mBitmapMemory = 0;
+                int count = mBitmaps.size();
+                for (int i = 0; i < count; i++) {
+                    mBitmapMemory += mBitmaps.get(i).getAllocationByteCount();
+                }
+            }
+            return mBitmapMemory;
+        }
+    }
+
+    private class BitmapReflectionAction extends Action {
+        int bitmapId;
+        Bitmap bitmap;
+        String methodName;
+
+        BitmapReflectionAction(int viewId, String methodName, Bitmap bitmap) {
+            this.bitmap = bitmap;
+            this.viewId = viewId;
+            this.methodName = methodName;
+            bitmapId = mBitmapCache.getBitmapId(bitmap);
+        }
+
+        BitmapReflectionAction(Parcel in) {
+            viewId = in.readInt();
+            methodName = in.readString();
+            bitmapId = in.readInt();
+            bitmap = mBitmapCache.getBitmapForId(bitmapId);
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(BITMAP_REFLECTION_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeString(methodName);
+            dest.writeInt(bitmapId);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent,
+                OnClickHandler handler) throws ActionException {
+            ReflectionAction ra = new ReflectionAction(viewId, methodName, ReflectionAction.BITMAP,
+                    bitmap);
+            ra.apply(root, rootParent, handler);
+        }
+
+        @Override
+        public void setBitmapCache(BitmapCache bitmapCache) {
+            bitmapId = bitmapCache.getBitmapId(bitmap);
+        }
+
+        public String getActionName() {
+            return "BitmapReflectionAction";
+        }
+    }
+
+    /**
+     * Base class for the reflection actions.
+     */
+    private final class ReflectionAction extends Action {
+        static final int BOOLEAN = 1;
+        static final int BYTE = 2;
+        static final int SHORT = 3;
+        static final int INT = 4;
+        static final int LONG = 5;
+        static final int FLOAT = 6;
+        static final int DOUBLE = 7;
+        static final int CHAR = 8;
+        static final int STRING = 9;
+        static final int CHAR_SEQUENCE = 10;
+        static final int URI = 11;
+        // BITMAP actions are never stored in the list of actions. They are only used locally
+        // to implement BitmapReflectionAction, which eliminates duplicates using BitmapCache.
+        static final int BITMAP = 12;
+        static final int BUNDLE = 13;
+        static final int INTENT = 14;
+        static final int COLOR_STATE_LIST = 15;
+        static final int ICON = 16;
+
+        String methodName;
+        int type;
+        Object value;
+
+        ReflectionAction(int viewId, String methodName, int type, Object value) {
+            this.viewId = viewId;
+            this.methodName = methodName;
+            this.type = type;
+            this.value = value;
+        }
+
+        ReflectionAction(Parcel in) {
+            this.viewId = in.readInt();
+            this.methodName = in.readString();
+            this.type = in.readInt();
+            //noinspection ConstantIfStatement
+            if (false) {
+                Log.d(LOG_TAG, "read viewId=0x" + Integer.toHexString(this.viewId)
+                        + " methodName=" + this.methodName + " type=" + this.type);
+            }
+
+            // For some values that may have been null, we first check a flag to see if they were
+            // written to the parcel.
+            switch (this.type) {
+                case BOOLEAN:
+                    this.value = in.readInt() != 0;
+                    break;
+                case BYTE:
+                    this.value = in.readByte();
+                    break;
+                case SHORT:
+                    this.value = (short)in.readInt();
+                    break;
+                case INT:
+                    this.value = in.readInt();
+                    break;
+                case LONG:
+                    this.value = in.readLong();
+                    break;
+                case FLOAT:
+                    this.value = in.readFloat();
+                    break;
+                case DOUBLE:
+                    this.value = in.readDouble();
+                    break;
+                case CHAR:
+                    this.value = (char)in.readInt();
+                    break;
+                case STRING:
+                    this.value = in.readString();
+                    break;
+                case CHAR_SEQUENCE:
+                    this.value = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+                    break;
+                case URI:
+                    if (in.readInt() != 0) {
+                        this.value = Uri.CREATOR.createFromParcel(in);
+                    }
+                    break;
+                case BITMAP:
+                    if (in.readInt() != 0) {
+                        this.value = Bitmap.CREATOR.createFromParcel(in);
+                    }
+                    break;
+                case BUNDLE:
+                    this.value = in.readBundle();
+                    break;
+                case INTENT:
+                    if (in.readInt() != 0) {
+                        this.value = Intent.CREATOR.createFromParcel(in);
+                    }
+                    break;
+                case COLOR_STATE_LIST:
+                    if (in.readInt() != 0) {
+                        this.value = ColorStateList.CREATOR.createFromParcel(in);
+                    }
+                    break;
+                case ICON:
+                    if (in.readInt() != 0) {
+                        this.value = Icon.CREATOR.createFromParcel(in);
+                    }
+                default:
+                    break;
+            }
+        }
+
+        public void writeToParcel(Parcel out, int flags) {
+            out.writeInt(REFLECTION_ACTION_TAG);
+            out.writeInt(this.viewId);
+            out.writeString(this.methodName);
+            out.writeInt(this.type);
+            //noinspection ConstantIfStatement
+            if (false) {
+                Log.d(LOG_TAG, "write viewId=0x" + Integer.toHexString(this.viewId)
+                        + " methodName=" + this.methodName + " type=" + this.type);
+            }
+
+            // For some values which are null, we record an integer flag to indicate whether
+            // we have written a valid value to the parcel.
+            switch (this.type) {
+                case BOOLEAN:
+                    out.writeInt((Boolean) this.value ? 1 : 0);
+                    break;
+                case BYTE:
+                    out.writeByte((Byte) this.value);
+                    break;
+                case SHORT:
+                    out.writeInt((Short) this.value);
+                    break;
+                case INT:
+                    out.writeInt((Integer) this.value);
+                    break;
+                case LONG:
+                    out.writeLong((Long) this.value);
+                    break;
+                case FLOAT:
+                    out.writeFloat((Float) this.value);
+                    break;
+                case DOUBLE:
+                    out.writeDouble((Double) this.value);
+                    break;
+                case CHAR:
+                    out.writeInt((int)((Character)this.value).charValue());
+                    break;
+                case STRING:
+                    out.writeString((String)this.value);
+                    break;
+                case CHAR_SEQUENCE:
+                    TextUtils.writeToParcel((CharSequence)this.value, out, flags);
+                    break;
+                case BUNDLE:
+                    out.writeBundle((Bundle) this.value);
+                    break;
+                case URI:
+                case BITMAP:
+                case INTENT:
+                case COLOR_STATE_LIST:
+                case ICON:
+                    out.writeInt(this.value != null ? 1 : 0);
+                    if (this.value != null) {
+                        ((Parcelable) this.value).writeToParcel(out, flags);
+                    }
+                    break;
+                default:
+                    break;
+            }
+        }
+
+        private Class<?> getParameterType() {
+            switch (this.type) {
+                case BOOLEAN:
+                    return boolean.class;
+                case BYTE:
+                    return byte.class;
+                case SHORT:
+                    return short.class;
+                case INT:
+                    return int.class;
+                case LONG:
+                    return long.class;
+                case FLOAT:
+                    return float.class;
+                case DOUBLE:
+                    return double.class;
+                case CHAR:
+                    return char.class;
+                case STRING:
+                    return String.class;
+                case CHAR_SEQUENCE:
+                    return CharSequence.class;
+                case URI:
+                    return Uri.class;
+                case BITMAP:
+                    return Bitmap.class;
+                case BUNDLE:
+                    return Bundle.class;
+                case INTENT:
+                    return Intent.class;
+                case COLOR_STATE_LIST:
+                    return ColorStateList.class;
+                case ICON:
+                    return Icon.class;
+                default:
+                    return null;
+            }
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View view = root.findViewById(viewId);
+            if (view == null) return;
+
+            Class<?> param = getParameterType();
+            if (param == null) {
+                throw new ActionException("bad type: " + this.type);
+            }
+            try {
+                getMethod(view, this.methodName, param, false /* async */).invoke(view, this.value);
+            } catch (Throwable ex) {
+                throw new ActionException(ex);
+            }
+        }
+
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+            final View view = root.findViewById(viewId);
+            if (view == null) return ACTION_NOOP;
+
+            Class<?> param = getParameterType();
+            if (param == null) {
+                throw new ActionException("bad type: " + this.type);
+            }
+
+            try {
+                MethodHandle method = getMethod(view, this.methodName, param, true /* async */);
+
+                if (method != null) {
+                    Runnable endAction = (Runnable) method.invoke(view, this.value);
+                    if (endAction == null) {
+                        return ACTION_NOOP;
+                    } else {
+                        // Special case view stub
+                        if (endAction instanceof ViewStub.ViewReplaceRunnable) {
+                            root.createTree();
+                            // Replace child tree
+                            root.findViewTreeById(viewId).replaceView(
+                                    ((ViewStub.ViewReplaceRunnable) endAction).view);
+                        }
+                        return new RunnableAction(endAction);
+                    }
+                }
+            } catch (Throwable ex) {
+                throw new ActionException(ex);
+            }
+
+            return this;
+        }
+
+        public int mergeBehavior() {
+            // smoothScrollBy is cumulative, everything else overwites.
+            if (methodName.equals("smoothScrollBy")) {
+                return MERGE_APPEND;
+            } else {
+                return MERGE_REPLACE;
+            }
+        }
+
+        public String getActionName() {
+            // Each type of reflection action corresponds to a setter, so each should be seen as
+            // unique from the standpoint of merging.
+            return "ReflectionAction" + this.methodName + this.type;
+        }
+
+        @Override
+        public boolean prefersAsyncApply() {
+            return this.type == URI || this.type == ICON;
+        }
+    }
+
+    /**
+     * This is only used for async execution of actions and it not parcelable.
+     */
+    private static final class RunnableAction extends RuntimeAction {
+        private final Runnable mRunnable;
+
+        RunnableAction(Runnable r) {
+            mRunnable = r;
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            mRunnable.run();
+        }
+    }
+
+    private void configureRemoteViewsAsChild(RemoteViews rv) {
+        rv.setBitmapCache(mBitmapCache);
+        rv.setNotRoot();
+    }
+
+    void setNotRoot() {
+        mIsRoot = false;
+    }
+
+    /**
+     * ViewGroup methods that are related to adding Views.
+     */
+    private class ViewGroupActionAdd extends Action {
+        private RemoteViews mNestedViews;
+        private int mIndex;
+
+        ViewGroupActionAdd(int viewId, RemoteViews nestedViews) {
+            this(viewId, nestedViews, -1 /* index */);
+        }
+
+        ViewGroupActionAdd(int viewId, RemoteViews nestedViews, int index) {
+            this.viewId = viewId;
+            mNestedViews = nestedViews;
+            mIndex = index;
+            if (nestedViews != null) {
+                configureRemoteViewsAsChild(nestedViews);
+            }
+        }
+
+        ViewGroupActionAdd(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info,
+                int depth) {
+            viewId = parcel.readInt();
+            mIndex = parcel.readInt();
+            mNestedViews = new RemoteViews(parcel, bitmapCache, info, depth);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(VIEW_GROUP_ACTION_ADD_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(mIndex);
+            mNestedViews.writeToParcel(dest, flags);
+        }
+
+        @Override
+        public boolean hasSameAppInfo(ApplicationInfo parentInfo) {
+            return mNestedViews.mApplication.packageName.equals(parentInfo.packageName)
+                    && mNestedViews.mApplication.uid == parentInfo.uid;
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final Context context = root.getContext();
+            final ViewGroup target = root.findViewById(viewId);
+
+            if (target == null) {
+                return;
+            }
+
+            // Inflate nested views and add as children
+            target.addView(mNestedViews.apply(context, target, handler), mIndex);
+        }
+
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+            // In the async implementation, update the view tree so that subsequent calls to
+            // findViewById return the current view.
+            root.createTree();
+            ViewTree target = root.findViewTreeById(viewId);
+            if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
+                return ACTION_NOOP;
+            }
+            final ViewGroup targetVg = (ViewGroup) target.mRoot;
+
+            // Inflate nested views and perform all the async tasks for the child remoteView.
+            final Context context = root.mRoot.getContext();
+            final AsyncApplyTask task = mNestedViews.getAsyncApplyTask(
+                    context, targetVg, null, handler);
+            final ViewTree tree = task.doInBackground();
+
+            if (tree == null) {
+                throw new ActionException(task.mError);
+            }
+
+            // Update the global view tree, so that next call to findViewTreeById
+            // goes through the subtree as well.
+            target.addChild(tree, mIndex);
+
+            return new RuntimeAction() {
+                @Override
+                public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
+                        throws ActionException {
+                    task.onPostExecute(tree);
+                    targetVg.addView(task.mResult, mIndex);
+                }
+            };
+        }
+
+        @Override
+        public void setBitmapCache(BitmapCache bitmapCache) {
+            mNestedViews.setBitmapCache(bitmapCache);
+        }
+
+        @Override
+        public int mergeBehavior() {
+            return MERGE_APPEND;
+        }
+
+        @Override
+        public boolean prefersAsyncApply() {
+            return mNestedViews.prefersAsyncApply();
+        }
+
+
+        @Override
+        public String getActionName() {
+            return "ViewGroupActionAdd";
+        }
+    }
+
+    /**
+     * ViewGroup methods related to removing child views.
+     */
+    private class ViewGroupActionRemove extends Action {
+        /**
+         * Id that indicates that all child views of the affected ViewGroup should be removed.
+         *
+         * <p>Using -2 because the default id is -1. This avoids accidentally matching that.
+         */
+        private static final int REMOVE_ALL_VIEWS_ID = -2;
+
+        private int mViewIdToKeep;
+
+        ViewGroupActionRemove(int viewId) {
+            this(viewId, REMOVE_ALL_VIEWS_ID);
+        }
+
+        ViewGroupActionRemove(int viewId, int viewIdToKeep) {
+            this.viewId = viewId;
+            mViewIdToKeep = viewIdToKeep;
+        }
+
+        ViewGroupActionRemove(Parcel parcel) {
+            viewId = parcel.readInt();
+            mViewIdToKeep = parcel.readInt();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(VIEW_GROUP_ACTION_REMOVE_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(mViewIdToKeep);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final ViewGroup target = root.findViewById(viewId);
+
+            if (target == null) {
+                return;
+            }
+
+            if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
+                target.removeAllViews();
+                return;
+            }
+
+            removeAllViewsExceptIdToKeep(target);
+        }
+
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+            // In the async implementation, update the view tree so that subsequent calls to
+            // findViewById return the current view.
+            root.createTree();
+            ViewTree target = root.findViewTreeById(viewId);
+
+            if ((target == null) || !(target.mRoot instanceof ViewGroup)) {
+                return ACTION_NOOP;
+            }
+
+            final ViewGroup targetVg = (ViewGroup) target.mRoot;
+
+            // Clear all children when nested views omitted
+            target.mChildren = null;
+            return new RuntimeAction() {
+                @Override
+                public void apply(View root, ViewGroup rootParent, OnClickHandler handler)
+                        throws ActionException {
+                    if (mViewIdToKeep == REMOVE_ALL_VIEWS_ID) {
+                        targetVg.removeAllViews();
+                        return;
+                    }
+
+                    removeAllViewsExceptIdToKeep(targetVg);
+                }
+            };
+        }
+
+        /**
+         * Iterates through the children in the given ViewGroup and removes all the views that
+         * do not have an id of {@link #mViewIdToKeep}.
+         */
+        private void removeAllViewsExceptIdToKeep(ViewGroup viewGroup) {
+            // Otherwise, remove all the views that do not match the id to keep.
+            int index = viewGroup.getChildCount() - 1;
+            while (index >= 0) {
+                if (viewGroup.getChildAt(index).getId() != mViewIdToKeep) {
+                    viewGroup.removeViewAt(index);
+                }
+                index--;
+            }
+        }
+
+        @Override
+        public String getActionName() {
+            return "ViewGroupActionRemove";
+        }
+
+        @Override
+        public int mergeBehavior() {
+            return MERGE_APPEND;
+        }
+    }
+
+    /**
+     * Helper action to set compound drawables on a TextView. Supports relative
+     * (s/t/e/b) or cardinal (l/t/r/b) arrangement.
+     */
+    private class TextViewDrawableAction extends Action {
+        public TextViewDrawableAction(int viewId, boolean isRelative, int d1, int d2, int d3, int d4) {
+            this.viewId = viewId;
+            this.isRelative = isRelative;
+            this.useIcons = false;
+            this.d1 = d1;
+            this.d2 = d2;
+            this.d3 = d3;
+            this.d4 = d4;
+        }
+
+        public TextViewDrawableAction(int viewId, boolean isRelative,
+                Icon i1, Icon i2, Icon i3, Icon i4) {
+            this.viewId = viewId;
+            this.isRelative = isRelative;
+            this.useIcons = true;
+            this.i1 = i1;
+            this.i2 = i2;
+            this.i3 = i3;
+            this.i4 = i4;
+        }
+
+        public TextViewDrawableAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            isRelative = (parcel.readInt() != 0);
+            useIcons = (parcel.readInt() != 0);
+            if (useIcons) {
+                if (parcel.readInt() != 0) {
+                    i1 = Icon.CREATOR.createFromParcel(parcel);
+                }
+                if (parcel.readInt() != 0) {
+                    i2 = Icon.CREATOR.createFromParcel(parcel);
+                }
+                if (parcel.readInt() != 0) {
+                    i3 = Icon.CREATOR.createFromParcel(parcel);
+                }
+                if (parcel.readInt() != 0) {
+                    i4 = Icon.CREATOR.createFromParcel(parcel);
+                }
+            } else {
+                d1 = parcel.readInt();
+                d2 = parcel.readInt();
+                d3 = parcel.readInt();
+                d4 = parcel.readInt();
+            }
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(TEXT_VIEW_DRAWABLE_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(isRelative ? 1 : 0);
+            dest.writeInt(useIcons ? 1 : 0);
+            if (useIcons) {
+                if (i1 != null) {
+                    dest.writeInt(1);
+                    i1.writeToParcel(dest, 0);
+                } else {
+                    dest.writeInt(0);
+                }
+                if (i2 != null) {
+                    dest.writeInt(1);
+                    i2.writeToParcel(dest, 0);
+                } else {
+                    dest.writeInt(0);
+                }
+                if (i3 != null) {
+                    dest.writeInt(1);
+                    i3.writeToParcel(dest, 0);
+                } else {
+                    dest.writeInt(0);
+                }
+                if (i4 != null) {
+                    dest.writeInt(1);
+                    i4.writeToParcel(dest, 0);
+                } else {
+                    dest.writeInt(0);
+                }
+            } else {
+                dest.writeInt(d1);
+                dest.writeInt(d2);
+                dest.writeInt(d3);
+                dest.writeInt(d4);
+            }
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final TextView target = root.findViewById(viewId);
+            if (target == null) return;
+            if (drawablesLoaded) {
+                if (isRelative) {
+                    target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
+                } else {
+                    target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
+                }
+            } else if (useIcons) {
+                final Context ctx = target.getContext();
+                final Drawable id1 = i1 == null ? null : i1.loadDrawable(ctx);
+                final Drawable id2 = i2 == null ? null : i2.loadDrawable(ctx);
+                final Drawable id3 = i3 == null ? null : i3.loadDrawable(ctx);
+                final Drawable id4 = i4 == null ? null : i4.loadDrawable(ctx);
+                if (isRelative) {
+                    target.setCompoundDrawablesRelativeWithIntrinsicBounds(id1, id2, id3, id4);
+                } else {
+                    target.setCompoundDrawablesWithIntrinsicBounds(id1, id2, id3, id4);
+                }
+            } else {
+                if (isRelative) {
+                    target.setCompoundDrawablesRelativeWithIntrinsicBounds(d1, d2, d3, d4);
+                } else {
+                    target.setCompoundDrawablesWithIntrinsicBounds(d1, d2, d3, d4);
+                }
+            }
+        }
+
+        @Override
+        public Action initActionAsync(ViewTree root, ViewGroup rootParent, OnClickHandler handler) {
+            final TextView target = root.findViewById(viewId);
+            if (target == null) return ACTION_NOOP;
+
+            TextViewDrawableAction copy = useIcons ?
+                    new TextViewDrawableAction(viewId, isRelative, i1, i2, i3, i4) :
+                    new TextViewDrawableAction(viewId, isRelative, d1, d2, d3, d4);
+
+            // Load the drawables on the background thread.
+            copy.drawablesLoaded = true;
+            final Context ctx = target.getContext();
+
+            if (useIcons) {
+                copy.id1 = i1 == null ? null : i1.loadDrawable(ctx);
+                copy.id2 = i2 == null ? null : i2.loadDrawable(ctx);
+                copy.id3 = i3 == null ? null : i3.loadDrawable(ctx);
+                copy.id4 = i4 == null ? null : i4.loadDrawable(ctx);
+            } else {
+                copy.id1 = d1 == 0 ? null : ctx.getDrawable(d1);
+                copy.id2 = d2 == 0 ? null : ctx.getDrawable(d2);
+                copy.id3 = d3 == 0 ? null : ctx.getDrawable(d3);
+                copy.id4 = d4 == 0 ? null : ctx.getDrawable(d4);
+            }
+            return copy;
+        }
+
+        @Override
+        public boolean prefersAsyncApply() {
+            return useIcons;
+        }
+
+        public String getActionName() {
+            return "TextViewDrawableAction";
+        }
+
+        boolean isRelative = false;
+        boolean useIcons = false;
+        int d1, d2, d3, d4;
+        Icon i1, i2, i3, i4;
+
+        boolean drawablesLoaded = false;
+        Drawable id1, id2, id3, id4;
+    }
+
+    /**
+     * Helper action to set text size on a TextView in any supported units.
+     */
+    private class TextViewSizeAction extends Action {
+        public TextViewSizeAction(int viewId, int units, float size) {
+            this.viewId = viewId;
+            this.units = units;
+            this.size = size;
+        }
+
+        public TextViewSizeAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            units = parcel.readInt();
+            size  = parcel.readFloat();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(TEXT_VIEW_SIZE_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(units);
+            dest.writeFloat(size);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final TextView target = root.findViewById(viewId);
+            if (target == null) return;
+            target.setTextSize(units, size);
+        }
+
+        public String getActionName() {
+            return "TextViewSizeAction";
+        }
+
+        int units;
+        float size;
+    }
+
+    /**
+     * Helper action to set padding on a View.
+     */
+    private class ViewPaddingAction extends Action {
+        public ViewPaddingAction(int viewId, int left, int top, int right, int bottom) {
+            this.viewId = viewId;
+            this.left = left;
+            this.top = top;
+            this.right = right;
+            this.bottom = bottom;
+        }
+
+        public ViewPaddingAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            left = parcel.readInt();
+            top = parcel.readInt();
+            right = parcel.readInt();
+            bottom = parcel.readInt();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(VIEW_PADDING_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(left);
+            dest.writeInt(top);
+            dest.writeInt(right);
+            dest.writeInt(bottom);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+            target.setPadding(left, top, right, bottom);
+        }
+
+        public String getActionName() {
+            return "ViewPaddingAction";
+        }
+
+        int left, top, right, bottom;
+    }
+
+    /**
+     * Helper action to set layout params on a View.
+     */
+    private static class LayoutParamAction extends Action {
+
+        /** Set marginEnd */
+        public static final int LAYOUT_MARGIN_END_DIMEN = 1;
+        /** Set width */
+        public static final int LAYOUT_WIDTH = 2;
+        public static final int LAYOUT_MARGIN_BOTTOM_DIMEN = 3;
+
+        /**
+         * @param viewId ID of the view alter
+         * @param property which layout parameter to alter
+         * @param value new value of the layout parameter
+         */
+        public LayoutParamAction(int viewId, int property, int value) {
+            this.viewId = viewId;
+            this.property = property;
+            this.value = value;
+        }
+
+        public LayoutParamAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            property = parcel.readInt();
+            value = parcel.readInt();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(LAYOUT_PARAM_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(property);
+            dest.writeInt(value);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) {
+                return;
+            }
+            ViewGroup.LayoutParams layoutParams = target.getLayoutParams();
+            if (layoutParams == null) {
+                return;
+            }
+            switch (property) {
+                case LAYOUT_MARGIN_END_DIMEN:
+                    if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
+                        int resolved = resolveDimenPixelOffset(target, value);
+                        ((ViewGroup.MarginLayoutParams) layoutParams).setMarginEnd(resolved);
+                        target.setLayoutParams(layoutParams);
+                    }
+                    break;
+                case LAYOUT_MARGIN_BOTTOM_DIMEN:
+                    if (layoutParams instanceof ViewGroup.MarginLayoutParams) {
+                        int resolved = resolveDimenPixelOffset(target, value);
+                        ((ViewGroup.MarginLayoutParams) layoutParams).bottomMargin = resolved;
+                        target.setLayoutParams(layoutParams);
+                    }
+                    break;
+                case LAYOUT_WIDTH:
+                    layoutParams.width = value;
+                    target.setLayoutParams(layoutParams);
+                    break;
+                default:
+                    throw new IllegalArgumentException("Unknown property " + property);
+            }
+        }
+
+        private static int resolveDimenPixelOffset(View target, int value) {
+            if (value == 0) {
+                return 0;
+            }
+            return target.getContext().getResources().getDimensionPixelOffset(value);
+        }
+
+        public String getActionName() {
+            return "LayoutParamAction" + property + ".";
+        }
+
+        int property;
+        int value;
+    }
+
+    /**
+     * Helper action to set a color filter on a compound drawable on a TextView. Supports relative
+     * (s/t/e/b) or cardinal (l/t/r/b) arrangement.
+     */
+    private class TextViewDrawableColorFilterAction extends Action {
+        public TextViewDrawableColorFilterAction(int viewId, boolean isRelative, int index,
+                int color, PorterDuff.Mode mode) {
+            this.viewId = viewId;
+            this.isRelative = isRelative;
+            this.index = index;
+            this.color = color;
+            this.mode = mode;
+        }
+
+        public TextViewDrawableColorFilterAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            isRelative = (parcel.readInt() != 0);
+            index = parcel.readInt();
+            color = parcel.readInt();
+            mode = readPorterDuffMode(parcel);
+        }
+
+        private PorterDuff.Mode readPorterDuffMode(Parcel parcel) {
+            int mode = parcel.readInt();
+            if (mode >= 0 && mode < PorterDuff.Mode.values().length) {
+                return PorterDuff.Mode.values()[mode];
+            } else {
+                return PorterDuff.Mode.CLEAR;
+            }
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeInt(isRelative ? 1 : 0);
+            dest.writeInt(index);
+            dest.writeInt(color);
+            dest.writeInt(mode.ordinal());
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final TextView target = root.findViewById(viewId);
+            if (target == null) return;
+            Drawable[] drawables = isRelative
+                    ? target.getCompoundDrawablesRelative()
+                    : target.getCompoundDrawables();
+            if (index < 0 || index >= 4) {
+                throw new IllegalStateException("index must be in range [0, 3].");
+            }
+            Drawable d = drawables[index];
+            if (d != null) {
+                d.mutate();
+                d.setColorFilter(color, mode);
+            }
+        }
+
+        public String getActionName() {
+            return "TextViewDrawableColorFilterAction";
+        }
+
+        final boolean isRelative;
+        final int index;
+        final int color;
+        final PorterDuff.Mode mode;
+    }
+
+    /**
+     * Helper action to add a view tag with RemoteInputs.
+     */
+    private class SetRemoteInputsAction extends Action {
+
+        public SetRemoteInputsAction(int viewId, RemoteInput[] remoteInputs) {
+            this.viewId = viewId;
+            this.remoteInputs = remoteInputs;
+        }
+
+        public SetRemoteInputsAction(Parcel parcel) {
+            viewId = parcel.readInt();
+            remoteInputs = parcel.createTypedArray(RemoteInput.CREATOR);
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(SET_REMOTE_INPUTS_ACTION_TAG);
+            dest.writeInt(viewId);
+            dest.writeTypedArray(remoteInputs, flags);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            final View target = root.findViewById(viewId);
+            if (target == null) return;
+
+            target.setTagInternal(R.id.remote_input_tag, remoteInputs);
+        }
+
+        public String getActionName() {
+            return "SetRemoteInputsAction";
+        }
+
+        final Parcelable[] remoteInputs;
+    }
+
+    /**
+     * Helper action to override all textViewColors
+     */
+    private class OverrideTextColorsAction extends Action {
+
+        private final int textColor;
+
+        public OverrideTextColorsAction(int textColor) {
+            this.textColor = textColor;
+        }
+
+        public OverrideTextColorsAction(Parcel parcel) {
+            textColor = parcel.readInt();
+        }
+
+        public void writeToParcel(Parcel dest, int flags) {
+            dest.writeInt(OVERRIDE_TEXT_COLORS_TAG);
+            dest.writeInt(textColor);
+        }
+
+        @Override
+        public void apply(View root, ViewGroup rootParent, OnClickHandler handler) {
+            // Let's traverse the viewtree and override all textColors!
+            Stack<View> viewsToProcess = new Stack<>();
+            viewsToProcess.add(root);
+            while (!viewsToProcess.isEmpty()) {
+                View v = viewsToProcess.pop();
+                if (v instanceof TextView) {
+                    TextView textView = (TextView) v;
+                    textView.setText(NotificationColorUtil.clearColorSpans(textView.getText()));
+                    textView.setTextColor(textColor);
+                }
+                if (v instanceof ViewGroup) {
+                    ViewGroup viewGroup = (ViewGroup) v;
+                    for (int i = 0; i < viewGroup.getChildCount(); i++) {
+                        viewsToProcess.push(viewGroup.getChildAt(i));
+                    }
+                }
+            }
+        }
+
+        public String getActionName() {
+            return "OverrideTextColorsAction";
+        }
+    }
+
+    /**
+     * Create a new RemoteViews object that will display the views contained
+     * in the specified layout file.
+     *
+     * @param packageName Name of the package that contains the layout resource
+     * @param layoutId The id of the layout resource
+     */
+    public RemoteViews(String packageName, int layoutId) {
+        this(getApplicationInfo(packageName, UserHandle.myUserId()), layoutId);
+    }
+
+    /**
+     * Create a new RemoteViews object that will display the views contained
+     * in the specified layout file.
+     *
+     * @param packageName Name of the package that contains the layout resource.
+     * @param userId The user under which the package is running.
+     * @param layoutId The id of the layout resource.
+     *
+     * @hide
+     */
+    public RemoteViews(String packageName, int userId, int layoutId) {
+        this(getApplicationInfo(packageName, userId), layoutId);
+    }
+
+    /**
+     * Create a new RemoteViews object that will display the views contained
+     * in the specified layout file.
+     *
+     * @param application The application whose content is shown by the views.
+     * @param layoutId The id of the layout resource.
+     *
+     * @hide
+     */
+    protected RemoteViews(ApplicationInfo application, int layoutId) {
+        mApplication = application;
+        mLayoutId = layoutId;
+        mBitmapCache = new BitmapCache();
+    }
+
+    private boolean hasLandscapeAndPortraitLayouts() {
+        return (mLandscape != null) && (mPortrait != null);
+    }
+
+    /**
+     * Create a new RemoteViews object that will inflate as the specified
+     * landspace or portrait RemoteViews, depending on the current configuration.
+     *
+     * @param landscape The RemoteViews to inflate in landscape configuration
+     * @param portrait The RemoteViews to inflate in portrait configuration
+     */
+    public RemoteViews(RemoteViews landscape, RemoteViews portrait) {
+        if (landscape == null || portrait == null) {
+            throw new RuntimeException("Both RemoteViews must be non-null");
+        }
+        if (landscape.mApplication.uid != portrait.mApplication.uid
+                || !landscape.mApplication.packageName.equals(portrait.mApplication.packageName)) {
+            throw new RuntimeException("Both RemoteViews must share the same package and user");
+        }
+        mApplication = portrait.mApplication;
+        mLayoutId = portrait.getLayoutId();
+
+        mLandscape = landscape;
+        mPortrait = portrait;
+
+        mBitmapCache = new BitmapCache();
+        configureRemoteViewsAsChild(landscape);
+        configureRemoteViewsAsChild(portrait);
+    }
+
+    /**
+     * Creates a copy of another RemoteViews.
+     */
+    public RemoteViews(RemoteViews src) {
+        mBitmapCache = src.mBitmapCache;
+        mApplication = src.mApplication;
+        mIsRoot = src.mIsRoot;
+        mLayoutId = src.mLayoutId;
+        mIsWidgetCollectionChild = src.mIsWidgetCollectionChild;
+        mReapplyDisallowed = src.mReapplyDisallowed;
+
+        if (src.hasLandscapeAndPortraitLayouts()) {
+            mLandscape = new RemoteViews(src.mLandscape);
+            mPortrait = new RemoteViews(src.mPortrait);
+
+        }
+
+        if (src.mActions != null) {
+            mActions = new ArrayList<>();
+
+            Parcel p = Parcel.obtain();
+            int count = src.mActions.size();
+            for (int i = 0; i < count; i++) {
+                p.setDataPosition(0);
+                Action a = src.mActions.get(i);
+                a.writeToParcel(
+                        p, a.hasSameAppInfo(mApplication) ? PARCELABLE_ELIDE_DUPLICATES : 0);
+                p.setDataPosition(0);
+                // Since src is already in memory, we do not care about stack overflow as it has
+                // already been read once.
+                mActions.add(getActionFromParcel(p, 0));
+            }
+            p.recycle();
+        }
+
+        // Now that everything is initialized and duplicated, setting a new BitmapCache will
+        // re-initialize the cache.
+        setBitmapCache(new BitmapCache());
+    }
+
+    /**
+     * Reads a RemoteViews object from a parcel.
+     *
+     * @param parcel
+     */
+    public RemoteViews(Parcel parcel) {
+        this(parcel, null, null, 0);
+    }
+
+    private RemoteViews(Parcel parcel, BitmapCache bitmapCache, ApplicationInfo info, int depth) {
+        if (depth > MAX_NESTED_VIEWS
+                && (UserHandle.getAppId(Binder.getCallingUid()) != Process.SYSTEM_UID)) {
+            throw new IllegalArgumentException("Too many nested views.");
+        }
+        depth++;
+
+        int mode = parcel.readInt();
+
+        // We only store a bitmap cache in the root of the RemoteViews.
+        if (bitmapCache == null) {
+            mBitmapCache = new BitmapCache(parcel);
+        } else {
+            setBitmapCache(bitmapCache);
+            setNotRoot();
+        }
+
+        if (mode == MODE_NORMAL) {
+            mApplication = parcel.readInt() == 0 ? info :
+                    ApplicationInfo.CREATOR.createFromParcel(parcel);
+            mLayoutId = parcel.readInt();
+            mIsWidgetCollectionChild = parcel.readInt() == 1;
+
+            int count = parcel.readInt();
+            if (count > 0) {
+                mActions = new ArrayList<>(count);
+                for (int i = 0; i < count; i++) {
+                    mActions.add(getActionFromParcel(parcel, depth));
+                }
+            }
+        } else {
+            // MODE_HAS_LANDSCAPE_AND_PORTRAIT
+            mLandscape = new RemoteViews(parcel, mBitmapCache, info, depth);
+            mPortrait = new RemoteViews(parcel, mBitmapCache, mLandscape.mApplication, depth);
+            mApplication = mPortrait.mApplication;
+            mLayoutId = mPortrait.getLayoutId();
+        }
+        mReapplyDisallowed = parcel.readInt() == 0;
+    }
+
+    private Action getActionFromParcel(Parcel parcel, int depth) {
+        int tag = parcel.readInt();
+        switch (tag) {
+            case SET_ON_CLICK_PENDING_INTENT_TAG:
+                return new SetOnClickPendingIntent(parcel);
+            case SET_DRAWABLE_PARAMETERS_TAG:
+                return new SetDrawableParameters(parcel);
+            case REFLECTION_ACTION_TAG:
+                return new ReflectionAction(parcel);
+            case VIEW_GROUP_ACTION_ADD_TAG:
+                return new ViewGroupActionAdd(parcel, mBitmapCache, mApplication, depth);
+            case VIEW_GROUP_ACTION_REMOVE_TAG:
+                return new ViewGroupActionRemove(parcel);
+            case SET_REFLECTION_ACTION_WITHOUT_PARAMS_TAG:
+                return new ReflectionActionWithoutParams(parcel);
+            case SET_EMPTY_VIEW_ACTION_TAG:
+                return new SetEmptyView(parcel);
+            case SET_PENDING_INTENT_TEMPLATE_TAG:
+                return new SetPendingIntentTemplate(parcel);
+            case SET_ON_CLICK_FILL_IN_INTENT_TAG:
+                return new SetOnClickFillInIntent(parcel);
+            case SET_REMOTE_VIEW_ADAPTER_INTENT_TAG:
+                return new SetRemoteViewsAdapterIntent(parcel);
+            case TEXT_VIEW_DRAWABLE_ACTION_TAG:
+                return new TextViewDrawableAction(parcel);
+            case TEXT_VIEW_SIZE_ACTION_TAG:
+                return new TextViewSizeAction(parcel);
+            case VIEW_PADDING_ACTION_TAG:
+                return new ViewPaddingAction(parcel);
+            case BITMAP_REFLECTION_ACTION_TAG:
+                return new BitmapReflectionAction(parcel);
+            case SET_REMOTE_VIEW_ADAPTER_LIST_TAG:
+                return new SetRemoteViewsAdapterList(parcel);
+            case TEXT_VIEW_DRAWABLE_COLOR_FILTER_ACTION_TAG:
+                return new TextViewDrawableColorFilterAction(parcel);
+            case SET_REMOTE_INPUTS_ACTION_TAG:
+                return new SetRemoteInputsAction(parcel);
+            case LAYOUT_PARAM_ACTION_TAG:
+                return new LayoutParamAction(parcel);
+            case OVERRIDE_TEXT_COLORS_TAG:
+                return new OverrideTextColorsAction(parcel);
+            default:
+                throw new ActionException("Tag " + tag + " not found");
+        }
+    };
+
+    /**
+     * Returns a deep copy of the RemoteViews object. The RemoteView may not be
+     * attached to another RemoteView -- it must be the root of a hierarchy.
+     *
+     * @deprecated use {@link #RemoteViews(RemoteViews)} instead.
+     * @throws IllegalStateException if this is not the root of a RemoteView
+     *         hierarchy
+     */
+    @Override
+    @Deprecated
+    public RemoteViews clone() {
+        Preconditions.checkState(mIsRoot, "RemoteView has been attached to another RemoteView. "
+                + "May only clone the root of a RemoteView hierarchy.");
+
+        return new RemoteViews(this);
+    }
+
+    public String getPackage() {
+        return (mApplication != null) ? mApplication.packageName : null;
+    }
+
+    /**
+     * Returns the layout id of the root layout associated with this RemoteViews. In the case
+     * that the RemoteViews has both a landscape and portrait root, this will return the layout
+     * id associated with the portrait layout.
+     *
+     * @return the layout id.
+     */
+    public int getLayoutId() {
+        return mLayoutId;
+    }
+
+    /*
+     * This flag indicates whether this RemoteViews object is being created from a
+     * RemoteViewsService for use as a child of a widget collection. This flag is used
+     * to determine whether or not certain features are available, in particular,
+     * setting on click extras and setting on click pending intents. The former is enabled,
+     * and the latter disabled when this flag is true.
+     */
+    void setIsWidgetCollectionChild(boolean isWidgetCollectionChild) {
+        mIsWidgetCollectionChild = isWidgetCollectionChild;
+    }
+
+    /**
+     * Recursively sets BitmapCache in the hierarchy and update the bitmap ids.
+     */
+    private void setBitmapCache(BitmapCache bitmapCache) {
+        mBitmapCache = bitmapCache;
+        if (!hasLandscapeAndPortraitLayouts()) {
+            if (mActions != null) {
+                final int count = mActions.size();
+                for (int i= 0; i < count; ++i) {
+                    mActions.get(i).setBitmapCache(bitmapCache);
+                }
+            }
+        } else {
+            mLandscape.setBitmapCache(bitmapCache);
+            mPortrait.setBitmapCache(bitmapCache);
+        }
+    }
+
+    /**
+     * Returns an estimate of the bitmap heap memory usage for this RemoteViews.
+     */
+    /** @hide */
+    public int estimateMemoryUsage() {
+        return mBitmapCache.getBitmapMemory();
+    }
+
+    /**
+     * Add an action to be executed on the remote side when apply is called.
+     *
+     * @param a The action to add
+     */
+    private void addAction(Action a) {
+        if (hasLandscapeAndPortraitLayouts()) {
+            throw new RuntimeException("RemoteViews specifying separate landscape and portrait" +
+                    " layouts cannot be modified. Instead, fully configure the landscape and" +
+                    " portrait layouts individually before constructing the combined layout.");
+        }
+        if (mActions == null) {
+            mActions = new ArrayList<>();
+        }
+        mActions.add(a);
+    }
+
+    /**
+     * Equivalent to calling {@link ViewGroup#addView(View)} after inflating the
+     * given {@link RemoteViews}. This allows users to build "nested"
+     * {@link RemoteViews}. In cases where consumers of {@link RemoteViews} may
+     * recycle layouts, use {@link #removeAllViews(int)} to clear any existing
+     * children.
+     *
+     * @param viewId The id of the parent {@link ViewGroup} to add child into.
+     * @param nestedView {@link RemoteViews} that describes the child.
+     */
+    public void addView(int viewId, RemoteViews nestedView) {
+        addAction(nestedView == null
+                ? new ViewGroupActionRemove(viewId)
+                : new ViewGroupActionAdd(viewId, nestedView));
+    }
+
+    /**
+     * Equivalent to calling {@link ViewGroup#addView(View, int)} after inflating the
+     * given {@link RemoteViews}.
+     *
+     * @param viewId The id of the parent {@link ViewGroup} to add the child into.
+     * @param nestedView {@link RemoteViews} of the child to add.
+     * @param index The position at which to add the child.
+     *
+     * @hide
+     */
+    public void addView(int viewId, RemoteViews nestedView, int index) {
+        addAction(new ViewGroupActionAdd(viewId, nestedView, index));
+    }
+
+    /**
+     * Equivalent to calling {@link ViewGroup#removeAllViews()}.
+     *
+     * @param viewId The id of the parent {@link ViewGroup} to remove all
+     *            children from.
+     */
+    public void removeAllViews(int viewId) {
+        addAction(new ViewGroupActionRemove(viewId));
+    }
+
+    /**
+     * Removes all views in the {@link ViewGroup} specified by the {@code viewId} except for any
+     * child that has the {@code viewIdToKeep} as its id.
+     *
+     * @param viewId The id of the parent {@link ViewGroup} to remove children from.
+     * @param viewIdToKeep The id of a child that should not be removed.
+     *
+     * @hide
+     */
+    public void removeAllViewsExceptId(int viewId, int viewIdToKeep) {
+        addAction(new ViewGroupActionRemove(viewId, viewIdToKeep));
+    }
+
+    /**
+     * Equivalent to calling {@link AdapterViewAnimator#showNext()}
+     *
+     * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showNext()}
+     */
+    public void showNext(int viewId) {
+        addAction(new ReflectionActionWithoutParams(viewId, "showNext"));
+    }
+
+    /**
+     * Equivalent to calling {@link AdapterViewAnimator#showPrevious()}
+     *
+     * @param viewId The id of the view on which to call {@link AdapterViewAnimator#showPrevious()}
+     */
+    public void showPrevious(int viewId) {
+        addAction(new ReflectionActionWithoutParams(viewId, "showPrevious"));
+    }
+
+    /**
+     * Equivalent to calling {@link AdapterViewAnimator#setDisplayedChild(int)}
+     *
+     * @param viewId The id of the view on which to call
+     *               {@link AdapterViewAnimator#setDisplayedChild(int)}
+     */
+    public void setDisplayedChild(int viewId, int childIndex) {
+        setInt(viewId, "setDisplayedChild", childIndex);
+    }
+
+    /**
+     * Equivalent to calling {@link View#setVisibility(int)}
+     *
+     * @param viewId The id of the view whose visibility should change
+     * @param visibility The new visibility for the view
+     */
+    public void setViewVisibility(int viewId, int visibility) {
+        setInt(viewId, "setVisibility", visibility);
+    }
+
+    /**
+     * Equivalent to calling {@link TextView#setText(CharSequence)}
+     *
+     * @param viewId The id of the view whose text should change
+     * @param text The new text for the view
+     */
+    public void setTextViewText(int viewId, CharSequence text) {
+        setCharSequence(viewId, "setText", text);
+    }
+
+    /**
+     * Equivalent to calling {@link TextView#setTextSize(int, float)}
+     *
+     * @param viewId The id of the view whose text size should change
+     * @param units The units of size (e.g. COMPLEX_UNIT_SP)
+     * @param size The size of the text
+     */
+    public void setTextViewTextSize(int viewId, int units, float size) {
+        addAction(new TextViewSizeAction(viewId, units, size));
+    }
+
+    /**
+     * Equivalent to calling
+     * {@link TextView#setCompoundDrawablesWithIntrinsicBounds(int, int, int, int)}.
+     *
+     * @param viewId The id of the view whose text should change
+     * @param left The id of a drawable to place to the left of the text, or 0
+     * @param top The id of a drawable to place above the text, or 0
+     * @param right The id of a drawable to place to the right of the text, or 0
+     * @param bottom The id of a drawable to place below the text, or 0
+     */
+    public void setTextViewCompoundDrawables(int viewId, int left, int top, int right, int bottom) {
+        addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
+    }
+
+    /**
+     * Equivalent to calling {@link
+     * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(int, int, int, int)}.
+     *
+     * @param viewId The id of the view whose text should change
+     * @param start The id of a drawable to place before the text (relative to the
+     * layout direction), or 0
+     * @param top The id of a drawable to place above the text, or 0
+     * @param end The id of a drawable to place after the text, or 0
+     * @param bottom The id of a drawable to place below the text, or 0
+     */
+    public void setTextViewCompoundDrawablesRelative(int viewId, int start, int top, int end, int bottom) {
+        addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
+    }
+
+    /**
+     * Equivalent to applying a color filter on one of the drawables in
+     * {@link android.widget.TextView#getCompoundDrawablesRelative()}.
+     *
+     * @param viewId The id of the view whose text should change.
+     * @param index  The index of the drawable in the array of
+     *               {@link android.widget.TextView#getCompoundDrawablesRelative()} to set the color
+     *               filter on. Must be in [0, 3].
+     * @param color  The color of the color filter. See
+     *               {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}.
+     * @param mode   The mode of the color filter. See
+     *               {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)}.
+     * @hide
+     */
+    public void setTextViewCompoundDrawablesRelativeColorFilter(int viewId,
+            int index, int color, PorterDuff.Mode mode) {
+        if (index < 0 || index >= 4) {
+            throw new IllegalArgumentException("index must be in range [0, 3].");
+        }
+        addAction(new TextViewDrawableColorFilterAction(viewId, true, index, color, mode));
+    }
+
+    /**
+     * Equivalent to calling {@link
+     * TextView#setCompoundDrawablesWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
+     * using the drawables yielded by {@link Icon#loadDrawable(Context)}.
+     *
+     * @param viewId The id of the view whose text should change
+     * @param left an Icon to place to the left of the text, or 0
+     * @param top an Icon to place above the text, or 0
+     * @param right an Icon to place to the right of the text, or 0
+     * @param bottom an Icon to place below the text, or 0
+     *
+     * @hide
+     */
+    public void setTextViewCompoundDrawables(int viewId, Icon left, Icon top, Icon right, Icon bottom) {
+        addAction(new TextViewDrawableAction(viewId, false, left, top, right, bottom));
+    }
+
+    /**
+     * Equivalent to calling {@link
+     * TextView#setCompoundDrawablesRelativeWithIntrinsicBounds(Drawable, Drawable, Drawable, Drawable)}
+     * using the drawables yielded by {@link Icon#loadDrawable(Context)}.
+     *
+     * @param viewId The id of the view whose text should change
+     * @param start an Icon to place before the text (relative to the
+     * layout direction), or 0
+     * @param top an Icon to place above the text, or 0
+     * @param end an Icon to place after the text, or 0
+     * @param bottom an Icon to place below the text, or 0
+     *
+     * @hide
+     */
+    public void setTextViewCompoundDrawablesRelative(int viewId, Icon start, Icon top, Icon end, Icon bottom) {
+        addAction(new TextViewDrawableAction(viewId, true, start, top, end, bottom));
+    }
+
+    /**
+     * Equivalent to calling {@link ImageView#setImageResource(int)}
+     *
+     * @param viewId The id of the view whose drawable should change
+     * @param srcId The new resource id for the drawable
+     */
+    public void setImageViewResource(int viewId, int srcId) {
+        setInt(viewId, "setImageResource", srcId);
+    }
+
+    /**
+     * Equivalent to calling {@link ImageView#setImageURI(Uri)}
+     *
+     * @param viewId The id of the view whose drawable should change
+     * @param uri The Uri for the image
+     */
+    public void setImageViewUri(int viewId, Uri uri) {
+        setUri(viewId, "setImageURI", uri);
+    }
+
+    /**
+     * Equivalent to calling {@link ImageView#setImageBitmap(Bitmap)}
+     *
+     * @param viewId The id of the view whose bitmap should change
+     * @param bitmap The new Bitmap for the drawable
+     */
+    public void setImageViewBitmap(int viewId, Bitmap bitmap) {
+        setBitmap(viewId, "setImageBitmap", bitmap);
+    }
+
+    /**
+     * Equivalent to calling {@link ImageView#setImageIcon(Icon)}
+     *
+     * @param viewId The id of the view whose bitmap should change
+     * @param icon The new Icon for the ImageView
+     */
+    public void setImageViewIcon(int viewId, Icon icon) {
+        setIcon(viewId, "setImageIcon", icon);
+    }
+
+    /**
+     * Equivalent to calling {@link AdapterView#setEmptyView(View)}
+     *
+     * @param viewId The id of the view on which to set the empty view
+     * @param emptyViewId The view id of the empty view
+     */
+    public void setEmptyView(int viewId, int emptyViewId) {
+        addAction(new SetEmptyView(viewId, emptyViewId));
+    }
+
+    /**
+     * Equivalent to calling {@link Chronometer#setBase Chronometer.setBase},
+     * {@link Chronometer#setFormat Chronometer.setFormat},
+     * and {@link Chronometer#start Chronometer.start()} or
+     * {@link Chronometer#stop Chronometer.stop()}.
+     *
+     * @param viewId The id of the {@link Chronometer} to change
+     * @param base The time at which the timer would have read 0:00.  This
+     *             time should be based off of
+     *             {@link android.os.SystemClock#elapsedRealtime SystemClock.elapsedRealtime()}.
+     * @param format The Chronometer format string, or null to
+     *               simply display the timer value.
+     * @param started True if you want the clock to be started, false if not.
+     *
+     * @see #setChronometerCountDown(int, boolean)
+     */
+    public void setChronometer(int viewId, long base, String format, boolean started) {
+        setLong(viewId, "setBase", base);
+        setString(viewId, "setFormat", format);
+        setBoolean(viewId, "setStarted", started);
+    }
+
+    /**
+     * Equivalent to calling {@link Chronometer#setCountDown(boolean) Chronometer.setCountDown} on
+     * the chronometer with the given viewId.
+     *
+     * @param viewId The id of the {@link Chronometer} to change
+     * @param isCountDown True if you want the chronometer to count down to base instead of
+     *                    counting up.
+     */
+    public void setChronometerCountDown(int viewId, boolean isCountDown) {
+        setBoolean(viewId, "setCountDown", isCountDown);
+    }
+
+    /**
+     * Equivalent to calling {@link ProgressBar#setMax ProgressBar.setMax},
+     * {@link ProgressBar#setProgress ProgressBar.setProgress}, and
+     * {@link ProgressBar#setIndeterminate ProgressBar.setIndeterminate}
+     *
+     * If indeterminate is true, then the values for max and progress are ignored.
+     *
+     * @param viewId The id of the {@link ProgressBar} to change
+     * @param max The 100% value for the progress bar
+     * @param progress The current value of the progress bar.
+     * @param indeterminate True if the progress bar is indeterminate,
+     *                false if not.
+     */
+    public void setProgressBar(int viewId, int max, int progress,
+            boolean indeterminate) {
+        setBoolean(viewId, "setIndeterminate", indeterminate);
+        if (!indeterminate) {
+            setInt(viewId, "setMax", max);
+            setInt(viewId, "setProgress", progress);
+        }
+    }
+
+    /**
+     * Equivalent to calling
+     * {@link android.view.View#setOnClickListener(android.view.View.OnClickListener)}
+     * to launch the provided {@link PendingIntent}.
+     *
+     * When setting the on-click action of items within collections (eg. {@link ListView},
+     * {@link StackView} etc.), this method will not work. Instead, use {@link
+     * RemoteViews#setPendingIntentTemplate(int, PendingIntent)} in conjunction with
+     * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
+     *
+     * @param viewId The id of the view that will trigger the {@link PendingIntent} when clicked
+     * @param pendingIntent The {@link PendingIntent} to send when user clicks
+     */
+    public void setOnClickPendingIntent(int viewId, PendingIntent pendingIntent) {
+        addAction(new SetOnClickPendingIntent(viewId, pendingIntent));
+    }
+
+    /**
+     * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
+     * costly to set PendingIntents on the individual items, and is hence not permitted. Instead
+     * this method should be used to set a single PendingIntent template on the collection, and
+     * individual items can differentiate their on-click behavior using
+     * {@link RemoteViews#setOnClickFillInIntent(int, Intent)}.
+     *
+     * @param viewId The id of the collection who's children will use this PendingIntent template
+     *          when clicked
+     * @param pendingIntentTemplate The {@link PendingIntent} to be combined with extras specified
+     *          by a child of viewId and executed when that child is clicked
+     */
+    public void setPendingIntentTemplate(int viewId, PendingIntent pendingIntentTemplate) {
+        addAction(new SetPendingIntentTemplate(viewId, pendingIntentTemplate));
+    }
+
+    /**
+     * When using collections (eg. {@link ListView}, {@link StackView} etc.) in widgets, it is very
+     * costly to set PendingIntents on the individual items, and is hence not permitted. Instead
+     * a single PendingIntent template can be set on the collection, see {@link
+     * RemoteViews#setPendingIntentTemplate(int, PendingIntent)}, and the individual on-click
+     * action of a given item can be distinguished by setting a fillInIntent on that item. The
+     * fillInIntent is then combined with the PendingIntent template in order to determine the final
+     * intent which will be executed when the item is clicked. This works as follows: any fields
+     * which are left blank in the PendingIntent template, but are provided by the fillInIntent
+     * will be overwritten, and the resulting PendingIntent will be used. The rest
+     * of the PendingIntent template will then be filled in with the associated fields that are
+     * set in fillInIntent. See {@link Intent#fillIn(Intent, int)} for more details.
+     *
+     * @param viewId The id of the view on which to set the fillInIntent
+     * @param fillInIntent The intent which will be combined with the parent's PendingIntent
+     *        in order to determine the on-click behavior of the view specified by viewId
+     */
+    public void setOnClickFillInIntent(int viewId, Intent fillInIntent) {
+        addAction(new SetOnClickFillInIntent(viewId, fillInIntent));
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling a combination of {@link Drawable#setAlpha(int)},
+     * {@link Drawable#setColorFilter(int, android.graphics.PorterDuff.Mode)},
+     * and/or {@link Drawable#setLevel(int)} on the {@link Drawable} of a given
+     * view.
+     * <p>
+     * You can omit specific calls by marking their values with null or -1.
+     *
+     * @param viewId The id of the view that contains the target
+     *            {@link Drawable}
+     * @param targetBackground If true, apply these parameters to the
+     *            {@link Drawable} returned by
+     *            {@link android.view.View#getBackground()}. Otherwise, assume
+     *            the target view is an {@link ImageView} and apply them to
+     *            {@link ImageView#getDrawable()}.
+     * @param alpha Specify an alpha value for the drawable, or -1 to leave
+     *            unchanged.
+     * @param colorFilter Specify a color for a
+     *            {@link android.graphics.ColorFilter} for this drawable. This will be ignored if
+     *            {@code mode} is {@code null}.
+     * @param mode Specify a PorterDuff mode for this drawable, or null to leave
+     *            unchanged.
+     * @param level Specify the level for the drawable, or -1 to leave
+     *            unchanged.
+     */
+    public void setDrawableParameters(int viewId, boolean targetBackground, int alpha,
+            int colorFilter, PorterDuff.Mode mode, int level) {
+        addAction(new SetDrawableParameters(viewId, targetBackground, alpha,
+                colorFilter, mode, level));
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling {@link android.widget.ProgressBar#setProgressTintList}.
+     *
+     * @param viewId The id of the view whose tint should change
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     */
+    public void setProgressTintList(int viewId, ColorStateList tint) {
+        addAction(new ReflectionAction(viewId, "setProgressTintList",
+                ReflectionAction.COLOR_STATE_LIST, tint));
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling {@link android.widget.ProgressBar#setProgressBackgroundTintList}.
+     *
+     * @param viewId The id of the view whose tint should change
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     */
+    public void setProgressBackgroundTintList(int viewId, ColorStateList tint) {
+        addAction(new ReflectionAction(viewId, "setProgressBackgroundTintList",
+                ReflectionAction.COLOR_STATE_LIST, tint));
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling {@link android.widget.ProgressBar#setIndeterminateTintList}.
+     *
+     * @param viewId The id of the view whose tint should change
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     */
+    public void setProgressIndeterminateTintList(int viewId, ColorStateList tint) {
+        addAction(new ReflectionAction(viewId, "setIndeterminateTintList",
+                ReflectionAction.COLOR_STATE_LIST, tint));
+    }
+
+    /**
+     * Equivalent to calling {@link android.widget.TextView#setTextColor(int)}.
+     *
+     * @param viewId The id of the view whose text color should change
+     * @param color Sets the text color for all the states (normal, selected,
+     *            focused) to be this color.
+     */
+    public void setTextColor(int viewId, @ColorInt int color) {
+        setInt(viewId, "setTextColor", color);
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling {@link android.widget.TextView#setTextColor(ColorStateList)}.
+     *
+     * @param viewId The id of the view whose text color should change
+     * @param colors the text colors to set
+     */
+    public void setTextColor(int viewId, @ColorInt ColorStateList colors) {
+        addAction(new ReflectionAction(viewId, "setTextColor", ReflectionAction.COLOR_STATE_LIST,
+                colors));
+    }
+
+    /**
+     * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
+     *
+     * @param appWidgetId The id of the app widget which contains the specified view. (This
+     *      parameter is ignored in this deprecated method)
+     * @param viewId The id of the {@link AdapterView}
+     * @param intent The intent of the service which will be
+     *            providing data to the RemoteViewsAdapter
+     * @deprecated This method has been deprecated. See
+     *      {@link android.widget.RemoteViews#setRemoteAdapter(int, Intent)}
+     */
+    @Deprecated
+    public void setRemoteAdapter(int appWidgetId, int viewId, Intent intent) {
+        setRemoteAdapter(viewId, intent);
+    }
+
+    /**
+     * Equivalent to calling {@link android.widget.AbsListView#setRemoteViewsAdapter(Intent)}.
+     * Can only be used for App Widgets.
+     *
+     * @param viewId The id of the {@link AdapterView}
+     * @param intent The intent of the service which will be
+     *            providing data to the RemoteViewsAdapter
+     */
+    public void setRemoteAdapter(int viewId, Intent intent) {
+        addAction(new SetRemoteViewsAdapterIntent(viewId, intent));
+    }
+
+    /**
+     * Creates a simple Adapter for the viewId specified. The viewId must point to an AdapterView,
+     * ie. {@link ListView}, {@link GridView}, {@link StackView} or {@link AdapterViewAnimator}.
+     * This is a simpler but less flexible approach to populating collection widgets. Its use is
+     * encouraged for most scenarios, as long as the total memory within the list of RemoteViews
+     * is relatively small (ie. doesn't contain large or numerous Bitmaps, see {@link
+     * RemoteViews#setImageViewBitmap}). In the case of numerous images, the use of API is still
+     * possible by setting image URIs instead of Bitmaps, see {@link RemoteViews#setImageViewUri}.
+     *
+     * This API is supported in the compatibility library for previous API levels, see
+     * RemoteViewsCompat.
+     *
+     * @param viewId The id of the {@link AdapterView}
+     * @param list The list of RemoteViews which will populate the view specified by viewId.
+     * @param viewTypeCount The maximum number of unique layout id's used to construct the list of
+     *      RemoteViews. This count cannot change during the life-cycle of a given widget, so this
+     *      parameter should account for the maximum possible number of types that may appear in the
+     *      See {@link Adapter#getViewTypeCount()}.
+     *
+     * @hide
+     */
+    public void setRemoteAdapter(int viewId, ArrayList<RemoteViews> list, int viewTypeCount) {
+        addAction(new SetRemoteViewsAdapterList(viewId, list, viewTypeCount));
+    }
+
+    /**
+     * Equivalent to calling {@link ListView#smoothScrollToPosition(int)}.
+     *
+     * @param viewId The id of the view to change
+     * @param position Scroll to this adapter position
+     */
+    public void setScrollPosition(int viewId, int position) {
+        setInt(viewId, "smoothScrollToPosition", position);
+    }
+
+    /**
+     * Equivalent to calling {@link ListView#smoothScrollByOffset(int)}.
+     *
+     * @param viewId The id of the view to change
+     * @param offset Scroll by this adapter position offset
+     */
+    public void setRelativeScrollPosition(int viewId, int offset) {
+        setInt(viewId, "smoothScrollByOffset", offset);
+    }
+
+    /**
+     * Equivalent to calling {@link android.view.View#setPadding(int, int, int, int)}.
+     *
+     * @param viewId The id of the view to change
+     * @param left the left padding in pixels
+     * @param top the top padding in pixels
+     * @param right the right padding in pixels
+     * @param bottom the bottom padding in pixels
+     */
+    public void setViewPadding(int viewId, int left, int top, int right, int bottom) {
+        addAction(new ViewPaddingAction(viewId, left, top, right, bottom));
+    }
+
+    /**
+     * @hide
+     * Equivalent to calling {@link android.view.ViewGroup.MarginLayoutParams#setMarginEnd(int)}.
+     * Only works if the {@link View#getLayoutParams()} supports margins.
+     * Hidden for now since we don't want to support this for all different layout margins yet.
+     *
+     * @param viewId The id of the view to change
+     * @param endMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
+     */
+    public void setViewLayoutMarginEndDimen(int viewId, @DimenRes int endMarginDimen) {
+        addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_END_DIMEN,
+                endMarginDimen));
+    }
+
+    /**
+     * Equivalent to setting {@link android.view.ViewGroup.MarginLayoutParams#bottomMargin}.
+     *
+     * @param bottomMarginDimen a dimen resource to read the margin from or 0 to clear the margin.
+     * @hide
+     */
+    public void setViewLayoutMarginBottomDimen(int viewId, @DimenRes int bottomMarginDimen) {
+        addAction(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_MARGIN_BOTTOM_DIMEN,
+                bottomMarginDimen));
+    }
+
+    /**
+     * Equivalent to setting {@link android.view.ViewGroup.LayoutParams#width}.
+     *
+     * @param layoutWidth one of 0, MATCH_PARENT or WRAP_CONTENT. Other sizes are not allowed
+     *                    because they behave poorly when the density changes.
+     * @hide
+     */
+    public void setViewLayoutWidth(int viewId, int layoutWidth) {
+        if (layoutWidth != 0 && layoutWidth != ViewGroup.LayoutParams.MATCH_PARENT
+                && layoutWidth != ViewGroup.LayoutParams.WRAP_CONTENT) {
+            throw new IllegalArgumentException("Only supports 0, WRAP_CONTENT and MATCH_PARENT");
+        }
+        mActions.add(new LayoutParamAction(viewId, LayoutParamAction.LAYOUT_WIDTH, layoutWidth));
+    }
+
+    /**
+     * Call a method taking one boolean on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setBoolean(int viewId, String methodName, boolean value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BOOLEAN, value));
+    }
+
+    /**
+     * Call a method taking one byte on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setByte(int viewId, String methodName, byte value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BYTE, value));
+    }
+
+    /**
+     * Call a method taking one short on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setShort(int viewId, String methodName, short value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.SHORT, value));
+    }
+
+    /**
+     * Call a method taking one int on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setInt(int viewId, String methodName, int value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INT, value));
+    }
+
+    /**
+     * Call a method taking one long on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setLong(int viewId, String methodName, long value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.LONG, value));
+    }
+
+    /**
+     * Call a method taking one float on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setFloat(int viewId, String methodName, float value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.FLOAT, value));
+    }
+
+    /**
+     * Call a method taking one double on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setDouble(int viewId, String methodName, double value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.DOUBLE, value));
+    }
+
+    /**
+     * Call a method taking one char on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setChar(int viewId, String methodName, char value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR, value));
+    }
+
+    /**
+     * Call a method taking one String on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setString(int viewId, String methodName, String value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.STRING, value));
+    }
+
+    /**
+     * Call a method taking one CharSequence on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setCharSequence(int viewId, String methodName, CharSequence value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.CHAR_SEQUENCE, value));
+    }
+
+    /**
+     * Call a method taking one Uri on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setUri(int viewId, String methodName, Uri value) {
+        if (value != null) {
+            // Resolve any filesystem path before sending remotely
+            value = value.getCanonicalUri();
+            if (StrictMode.vmFileUriExposureEnabled()) {
+                value.checkFileUriExposed("RemoteViews.setUri()");
+            }
+        }
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.URI, value));
+    }
+
+    /**
+     * Call a method taking one Bitmap on a view in the layout for this RemoteViews.
+     * @more
+     * <p class="note">The bitmap will be flattened into the parcel if this object is
+     * sent across processes, so it may end up using a lot of memory, and may be fairly slow.</p>
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setBitmap(int viewId, String methodName, Bitmap value) {
+        addAction(new BitmapReflectionAction(viewId, methodName, value));
+    }
+
+    /**
+     * Call a method taking one Bundle on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The value to pass to the method.
+     */
+    public void setBundle(int viewId, String methodName, Bundle value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.BUNDLE, value));
+    }
+
+    /**
+     * Call a method taking one Intent on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The {@link android.content.Intent} to pass the method.
+     */
+    public void setIntent(int viewId, String methodName, Intent value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.INTENT, value));
+    }
+
+    /**
+     * Call a method taking one Icon on a view in the layout for this RemoteViews.
+     *
+     * @param viewId The id of the view on which to call the method.
+     * @param methodName The name of the method to call.
+     * @param value The {@link android.graphics.drawable.Icon} to pass the method.
+     */
+    public void setIcon(int viewId, String methodName, Icon value) {
+        addAction(new ReflectionAction(viewId, methodName, ReflectionAction.ICON, value));
+    }
+
+    /**
+     * Equivalent to calling View.setContentDescription(CharSequence).
+     *
+     * @param viewId The id of the view whose content description should change.
+     * @param contentDescription The new content description for the view.
+     */
+    public void setContentDescription(int viewId, CharSequence contentDescription) {
+        setCharSequence(viewId, "setContentDescription", contentDescription);
+    }
+
+    /**
+     * Equivalent to calling {@link android.view.View#setAccessibilityTraversalBefore(int)}.
+     *
+     * @param viewId The id of the view whose before view in accessibility traversal to set.
+     * @param nextId The id of the next in the accessibility traversal.
+     **/
+    public void setAccessibilityTraversalBefore(int viewId, int nextId) {
+        setInt(viewId, "setAccessibilityTraversalBefore", nextId);
+    }
+
+    /**
+     * Equivalent to calling {@link android.view.View#setAccessibilityTraversalAfter(int)}.
+     *
+     * @param viewId The id of the view whose after view in accessibility traversal to set.
+     * @param nextId The id of the next in the accessibility traversal.
+     **/
+    public void setAccessibilityTraversalAfter(int viewId, int nextId) {
+        setInt(viewId, "setAccessibilityTraversalAfter", nextId);
+    }
+
+    /**
+     * Equivalent to calling {@link View#setLabelFor(int)}.
+     *
+     * @param viewId The id of the view whose property to set.
+     * @param labeledId The id of a view for which this view serves as a label.
+     */
+    public void setLabelFor(int viewId, int labeledId) {
+        setInt(viewId, "setLabelFor", labeledId);
+    }
+
+    private RemoteViews getRemoteViewsToApply(Context context) {
+        if (hasLandscapeAndPortraitLayouts()) {
+            int orientation = context.getResources().getConfiguration().orientation;
+            if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                return mLandscape;
+            } else {
+                return mPortrait;
+            }
+        }
+        return this;
+    }
+
+    /**
+     * Inflates the view hierarchy represented by this object and applies
+     * all of the actions.
+     *
+     * <p><strong>Caller beware: this may throw</strong>
+     *
+     * @param context Default context to use
+     * @param parent Parent that the resulting view hierarchy will be attached to. This method
+     * does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
+     * @return The inflated view hierarchy
+     */
+    public View apply(Context context, ViewGroup parent) {
+        return apply(context, parent, null);
+    }
+
+    /** @hide */
+    public View apply(Context context, ViewGroup parent, OnClickHandler handler) {
+        RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+        View result = inflateView(context, rvToApply, parent);
+        loadTransitionOverride(context, handler);
+
+        rvToApply.performApply(result, parent, handler);
+
+        return result;
+    }
+
+    private View inflateView(Context context, RemoteViews rv, ViewGroup parent) {
+        // RemoteViews may be built by an application installed in another
+        // user. So build a context that loads resources from that user but
+        // still returns the current users userId so settings like data / time formats
+        // are loaded without requiring cross user persmissions.
+        final Context contextForResources = getContextForResources(context);
+        Context inflationContext = new RemoteViewsContextWrapper(context, contextForResources);
+
+        LayoutInflater inflater = (LayoutInflater)
+                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        // Clone inflater so we load resources from correct context and
+        // we don't add a filter to the static version returned by getSystemService.
+        inflater = inflater.cloneInContext(inflationContext);
+        inflater.setFilter(this);
+        View v = inflater.inflate(rv.getLayoutId(), parent, false);
+        v.setTagInternal(R.id.widget_frame, rv.getLayoutId());
+        return v;
+    }
+
+    private static void loadTransitionOverride(Context context,
+            RemoteViews.OnClickHandler handler) {
+        if (handler != null && context.getResources().getBoolean(
+                com.android.internal.R.bool.config_overrideRemoteViewsActivityTransition)) {
+            TypedArray windowStyle = context.getTheme().obtainStyledAttributes(
+                    com.android.internal.R.styleable.Window);
+            int windowAnimations = windowStyle.getResourceId(
+                    com.android.internal.R.styleable.Window_windowAnimationStyle, 0);
+            TypedArray windowAnimationStyle = context.obtainStyledAttributes(
+                    windowAnimations, com.android.internal.R.styleable.WindowAnimation);
+            handler.setEnterAnimationId(windowAnimationStyle.getResourceId(
+                    com.android.internal.R.styleable.
+                            WindowAnimation_activityOpenRemoteViewsEnterAnimation, 0));
+            windowStyle.recycle();
+            windowAnimationStyle.recycle();
+        }
+    }
+
+    /**
+     * Implement this interface to receive a callback when
+     * {@link #applyAsync} or {@link #reapplyAsync} is finished.
+     * @hide
+     */
+    public interface OnViewAppliedListener {
+        void onViewApplied(View v);
+
+        void onError(Exception e);
+    }
+
+    /**
+     * Applies the views asynchronously, moving as much of the task on the background
+     * thread as possible.
+     *
+     * @see #apply(Context, ViewGroup)
+     * @param context Default context to use
+     * @param parent Parent that the resulting view hierarchy will be attached to. This method
+     * does <strong>not</strong> attach the hierarchy. The caller should do so when appropriate.
+     * @param listener the callback to run when all actions have been applied. May be null.
+     * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used.
+     * @return CancellationSignal
+     * @hide
+     */
+    public CancellationSignal applyAsync(
+            Context context, ViewGroup parent, Executor executor, OnViewAppliedListener listener) {
+        return applyAsync(context, parent, executor, listener, null);
+    }
+
+    private CancellationSignal startTaskOnExecutor(AsyncApplyTask task, Executor executor) {
+        CancellationSignal cancelSignal = new CancellationSignal();
+        cancelSignal.setOnCancelListener(task);
+
+        task.executeOnExecutor(executor == null ? AsyncTask.THREAD_POOL_EXECUTOR : executor);
+        return cancelSignal;
+    }
+
+    /** @hide */
+    public CancellationSignal applyAsync(Context context, ViewGroup parent,
+            Executor executor, OnViewAppliedListener listener, OnClickHandler handler) {
+        return startTaskOnExecutor(getAsyncApplyTask(context, parent, listener, handler), executor);
+    }
+
+    private AsyncApplyTask getAsyncApplyTask(Context context, ViewGroup parent,
+            OnViewAppliedListener listener, OnClickHandler handler) {
+        return new AsyncApplyTask(getRemoteViewsToApply(context), parent, context, listener,
+                handler, null);
+    }
+
+    private class AsyncApplyTask extends AsyncTask<Void, Void, ViewTree>
+            implements CancellationSignal.OnCancelListener {
+        final RemoteViews mRV;
+        final ViewGroup mParent;
+        final Context mContext;
+        final OnViewAppliedListener mListener;
+        final OnClickHandler mHandler;
+
+        private View mResult;
+        private ViewTree mTree;
+        private Action[] mActions;
+        private Exception mError;
+
+        private AsyncApplyTask(
+                RemoteViews rv, ViewGroup parent, Context context, OnViewAppliedListener listener,
+                OnClickHandler handler, View result) {
+            mRV = rv;
+            mParent = parent;
+            mContext = context;
+            mListener = listener;
+            mHandler = handler;
+
+            mResult = result;
+            loadTransitionOverride(context, handler);
+        }
+
+        @Override
+        protected ViewTree doInBackground(Void... params) {
+            try {
+                if (mResult == null) {
+                    mResult = inflateView(mContext, mRV, mParent);
+                }
+
+                mTree = new ViewTree(mResult);
+                if (mRV.mActions != null) {
+                    int count = mRV.mActions.size();
+                    mActions = new Action[count];
+                    for (int i = 0; i < count && !isCancelled(); i++) {
+                        // TODO: check if isCancelled in nested views.
+                        mActions[i] = mRV.mActions.get(i).initActionAsync(mTree, mParent, mHandler);
+                    }
+                } else {
+                    mActions = null;
+                }
+                return mTree;
+            } catch (Exception e) {
+                mError = e;
+                return null;
+            }
+        }
+
+        @Override
+        protected void onPostExecute(ViewTree viewTree) {
+            if (mError == null) {
+                try {
+                    if (mActions != null) {
+                        OnClickHandler handler = mHandler == null
+                                ? DEFAULT_ON_CLICK_HANDLER : mHandler;
+                        for (Action a : mActions) {
+                            a.apply(viewTree.mRoot, mParent, handler);
+                        }
+                    }
+                } catch (Exception e) {
+                    mError = e;
+                }
+            }
+
+            if (mListener != null) {
+                if (mError != null) {
+                    mListener.onError(mError);
+                } else {
+                    mListener.onViewApplied(viewTree.mRoot);
+                }
+            } else if (mError != null) {
+                if (mError instanceof ActionException) {
+                    throw (ActionException) mError;
+                } else {
+                    throw new ActionException(mError);
+                }
+            }
+        }
+
+        @Override
+        public void onCancel() {
+            cancel(true);
+        }
+    }
+
+    /**
+     * Applies all of the actions to the provided view.
+     *
+     * <p><strong>Caller beware: this may throw</strong>
+     *
+     * @param v The view to apply the actions to.  This should be the result of
+     * the {@link #apply(Context,ViewGroup)} call.
+     */
+    public void reapply(Context context, View v) {
+        reapply(context, v, null);
+    }
+
+    /** @hide */
+    public void reapply(Context context, View v, OnClickHandler handler) {
+        RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+        // In the case that a view has this RemoteViews applied in one orientation, is persisted
+        // across orientation change, and has the RemoteViews re-applied in the new orientation,
+        // we throw an exception, since the layouts may be completely unrelated.
+        if (hasLandscapeAndPortraitLayouts()) {
+            if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
+                throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
+                        " that does not share the same root layout id.");
+            }
+        }
+
+        rvToApply.performApply(v, (ViewGroup) v.getParent(), handler);
+    }
+
+    /**
+     * Applies all the actions to the provided view, moving as much of the task on the background
+     * thread as possible.
+     *
+     * @see #reapply(Context, View)
+     * @param context Default context to use
+     * @param v The view to apply the actions to.  This should be the result of
+     * the {@link #apply(Context,ViewGroup)} call.
+     * @param listener the callback to run when all actions have been applied. May be null.
+     * @param executor The executor to use. If null {@link AsyncTask#THREAD_POOL_EXECUTOR} is used
+     * @return CancellationSignal
+     * @hide
+     */
+    public CancellationSignal reapplyAsync(
+            Context context, View v, Executor executor, OnViewAppliedListener listener) {
+        return reapplyAsync(context, v, executor, listener, null);
+    }
+
+    /** @hide */
+    public CancellationSignal reapplyAsync(Context context, View v, Executor executor,
+            OnViewAppliedListener listener, OnClickHandler handler) {
+        RemoteViews rvToApply = getRemoteViewsToApply(context);
+
+        // In the case that a view has this RemoteViews applied in one orientation, is persisted
+        // across orientation change, and has the RemoteViews re-applied in the new orientation,
+        // we throw an exception, since the layouts may be completely unrelated.
+        if (hasLandscapeAndPortraitLayouts()) {
+            if ((Integer) v.getTag(R.id.widget_frame) != rvToApply.getLayoutId()) {
+                throw new RuntimeException("Attempting to re-apply RemoteViews to a view that" +
+                        " that does not share the same root layout id.");
+            }
+        }
+
+        return startTaskOnExecutor(new AsyncApplyTask(rvToApply, (ViewGroup) v.getParent(),
+                context, listener, handler, v), executor);
+    }
+
+    private void performApply(View v, ViewGroup parent, OnClickHandler handler) {
+        if (mActions != null) {
+            handler = handler == null ? DEFAULT_ON_CLICK_HANDLER : handler;
+            final int count = mActions.size();
+            for (int i = 0; i < count; i++) {
+                Action a = mActions.get(i);
+                a.apply(v, parent, handler);
+            }
+        }
+    }
+
+    /**
+     * Returns true if the RemoteViews contains potentially costly operations and should be
+     * applied asynchronously.
+     *
+     * @hide
+     */
+    public boolean prefersAsyncApply() {
+        if (mActions != null) {
+            final int count = mActions.size();
+            for (int i = 0; i < count; i++) {
+                if (mActions.get(i).prefersAsyncApply()) {
+                    return true;
+                }
+            }
+        }
+        return false;
+    }
+
+    private Context getContextForResources(Context context) {
+        if (mApplication != null) {
+            if (context.getUserId() == UserHandle.getUserId(mApplication.uid)
+                    && context.getPackageName().equals(mApplication.packageName)) {
+                return context;
+            }
+            try {
+                return context.createApplicationContext(mApplication,
+                        Context.CONTEXT_RESTRICTED);
+            } catch (NameNotFoundException e) {
+                Log.e(LOG_TAG, "Package name " + mApplication.packageName + " not found");
+            }
+        }
+
+        return context;
+    }
+
+    /**
+     * Returns the number of actions in this RemoteViews. Can be used as a sequence number.
+     *
+     * @hide
+     */
+    public int getSequenceNumber() {
+        return (mActions == null) ? 0 : mActions.size();
+    }
+
+    /* (non-Javadoc)
+     * Used to restrict the views which can be inflated
+     *
+     * @see android.view.LayoutInflater.Filter#onLoadClass(java.lang.Class)
+     */
+    public boolean onLoadClass(Class clazz) {
+        return clazz.isAnnotationPresent(RemoteView.class);
+    }
+
+    public int describeContents() {
+        return 0;
+    }
+
+    public void writeToParcel(Parcel dest, int flags) {
+        if (!hasLandscapeAndPortraitLayouts()) {
+            dest.writeInt(MODE_NORMAL);
+            // We only write the bitmap cache if we are the root RemoteViews, as this cache
+            // is shared by all children.
+            if (mIsRoot) {
+                mBitmapCache.writeBitmapsToParcel(dest, flags);
+            }
+            if (!mIsRoot && (flags & PARCELABLE_ELIDE_DUPLICATES) != 0) {
+                dest.writeInt(0);
+            } else {
+                dest.writeInt(1);
+                mApplication.writeToParcel(dest, flags);
+            }
+            dest.writeInt(mLayoutId);
+            dest.writeInt(mIsWidgetCollectionChild ? 1 : 0);
+            int count;
+            if (mActions != null) {
+                count = mActions.size();
+            } else {
+                count = 0;
+            }
+            dest.writeInt(count);
+            for (int i=0; i<count; i++) {
+                Action a = mActions.get(i);
+                a.writeToParcel(dest, a.hasSameAppInfo(mApplication)
+                        ? PARCELABLE_ELIDE_DUPLICATES : 0);
+            }
+        } else {
+            dest.writeInt(MODE_HAS_LANDSCAPE_AND_PORTRAIT);
+            // We only write the bitmap cache if we are the root RemoteViews, as this cache
+            // is shared by all children.
+            if (mIsRoot) {
+                mBitmapCache.writeBitmapsToParcel(dest, flags);
+            }
+            mLandscape.writeToParcel(dest, flags);
+            // Both RemoteViews already share the same package and user
+            mPortrait.writeToParcel(dest, flags | PARCELABLE_ELIDE_DUPLICATES);
+        }
+        dest.writeInt(mReapplyDisallowed ? 1 : 0);
+    }
+
+    private static ApplicationInfo getApplicationInfo(String packageName, int userId) {
+        if (packageName == null) {
+            return null;
+        }
+
+        // Get the application for the passed in package and user.
+        Application application = ActivityThread.currentApplication();
+        if (application == null) {
+            throw new IllegalStateException("Cannot create remote views out of an aplication.");
+        }
+
+        ApplicationInfo applicationInfo = application.getApplicationInfo();
+        if (UserHandle.getUserId(applicationInfo.uid) != userId
+                || !applicationInfo.packageName.equals(packageName)) {
+            try {
+                Context context = application.getBaseContext().createPackageContextAsUser(
+                        packageName, 0, new UserHandle(userId));
+                applicationInfo = context.getApplicationInfo();
+            } catch (NameNotFoundException nnfe) {
+                throw new IllegalArgumentException("No such package " + packageName);
+            }
+        }
+
+        return applicationInfo;
+    }
+
+    /**
+     * Parcelable.Creator that instantiates RemoteViews objects
+     */
+    public static final Parcelable.Creator<RemoteViews> CREATOR = new Parcelable.Creator<RemoteViews>() {
+        public RemoteViews createFromParcel(Parcel parcel) {
+            return new RemoteViews(parcel);
+        }
+
+        public RemoteViews[] newArray(int size) {
+            return new RemoteViews[size];
+        }
+    };
+
+    /**
+     * A representation of the view hierarchy. Only views which have a valid ID are added
+     * and can be searched.
+     */
+    private static class ViewTree {
+        private static final int INSERT_AT_END_INDEX = -1;
+        private View mRoot;
+        private ArrayList<ViewTree> mChildren;
+
+        private ViewTree(View root) {
+            mRoot = root;
+        }
+
+        public void createTree() {
+            if (mChildren != null) {
+                return;
+            }
+
+            mChildren = new ArrayList<>();
+            if (mRoot instanceof ViewGroup) {
+                ViewGroup vg = (ViewGroup) mRoot;
+                int count = vg.getChildCount();
+                for (int i = 0; i < count; i++) {
+                    addViewChild(vg.getChildAt(i));
+                }
+            }
+        }
+
+        public ViewTree findViewTreeById(int id) {
+            if (mRoot.getId() == id) {
+                return this;
+            }
+            if (mChildren == null) {
+                return null;
+            }
+            for (ViewTree tree : mChildren) {
+                ViewTree result = tree.findViewTreeById(id);
+                if (result != null) {
+                    return result;
+                }
+            }
+            return null;
+        }
+
+        public void replaceView(View v) {
+            mRoot = v;
+            mChildren = null;
+            createTree();
+        }
+
+        public <T extends View> T findViewById(int id) {
+            if (mChildren == null) {
+                return mRoot.findViewById(id);
+            }
+            ViewTree tree = findViewTreeById(id);
+            return tree == null ? null : (T) tree.mRoot;
+        }
+
+        public void addChild(ViewTree child) {
+            addChild(child, INSERT_AT_END_INDEX);
+        }
+
+        /**
+         * Adds the given {@link ViewTree} as a child at the given index.
+         *
+         * @param index The position at which to add the child or -1 to add last.
+         */
+        public void addChild(ViewTree child, int index) {
+            if (mChildren == null) {
+                mChildren = new ArrayList<>();
+            }
+            child.createTree();
+
+            if (index == INSERT_AT_END_INDEX) {
+                mChildren.add(child);
+                return;
+            }
+
+            mChildren.add(index, child);
+        }
+
+        private void addViewChild(View v) {
+            // ViewTree only contains Views which can be found using findViewById.
+            // If isRootNamespace is true, this view is skipped.
+            // @see ViewGroup#findViewTraversal(int)
+            if (v.isRootNamespace()) {
+                return;
+            }
+            final ViewTree target;
+
+            // If the view has a valid id, i.e., if can be found using findViewById, add it to the
+            // tree, otherwise skip this view and add its children instead.
+            if (v.getId() != 0) {
+                ViewTree tree = new ViewTree(v);
+                mChildren.add(tree);
+                target = tree;
+            } else {
+                target = this;
+            }
+
+            if (v instanceof ViewGroup) {
+                if (target.mChildren == null) {
+                    target.mChildren = new ArrayList<>();
+                    ViewGroup vg = (ViewGroup) v;
+                    int count = vg.getChildCount();
+                    for (int i = 0; i < count; i++) {
+                        target.addViewChild(vg.getChildAt(i));
+                    }
+                }
+            }
+        }
+    }
+}
diff --git a/android/widget/RemoteViewsAdapter.java b/android/widget/RemoteViewsAdapter.java
new file mode 100644
index 0000000..0968652
--- /dev/null
+++ b/android/widget/RemoteViewsAdapter.java
@@ -0,0 +1,1353 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.Manifest;
+import android.appwidget.AppWidgetHostView;
+import android.appwidget.AppWidgetManager;
+import android.content.Context;
+import android.content.Intent;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.util.Log;
+import android.util.Slog;
+import android.util.SparseArray;
+import android.util.SparseBooleanArray;
+import android.util.SparseIntArray;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.widget.RemoteViews.OnClickHandler;
+
+import com.android.internal.widget.IRemoteViewsAdapterConnection;
+import com.android.internal.widget.IRemoteViewsFactory;
+
+import java.lang.ref.WeakReference;
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.concurrent.Executor;
+
+/**
+ * An adapter to a RemoteViewsService which fetches and caches RemoteViews
+ * to be later inflated as child views.
+ */
+/** @hide */
+public class RemoteViewsAdapter extends BaseAdapter implements Handler.Callback {
+    private static final String MULTI_USER_PERM = Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+
+    private static final String TAG = "RemoteViewsAdapter";
+
+    // The max number of items in the cache
+    private static final int sDefaultCacheSize = 40;
+    // The delay (in millis) to wait until attempting to unbind from a service after a request.
+    // This ensures that we don't stay continually bound to the service and that it can be destroyed
+    // if we need the memory elsewhere in the system.
+    private static final int sUnbindServiceDelay = 5000;
+
+    // Default height for the default loading view, in case we cannot get inflate the first view
+    private static final int sDefaultLoadingViewHeight = 50;
+
+    // Type defs for controlling different messages across the main and worker message queues
+    private static final int sDefaultMessageType = 0;
+    private static final int sUnbindServiceMessageType = 1;
+
+    private final Context mContext;
+    private final Intent mIntent;
+    private final int mAppWidgetId;
+    private final Executor mAsyncViewLoadExecutor;
+
+    private RemoteViewsAdapterServiceConnection mServiceConnection;
+    private WeakReference<RemoteAdapterConnectionCallback> mCallback;
+    private OnClickHandler mRemoteViewsOnClickHandler;
+    private final FixedSizeRemoteViewsCache mCache;
+    private int mVisibleWindowLowerBound;
+    private int mVisibleWindowUpperBound;
+
+    // A flag to determine whether we should notify data set changed after we connect
+    private boolean mNotifyDataSetChangedAfterOnServiceConnected = false;
+
+    // The set of requested views that are to be notified when the associated RemoteViews are
+    // loaded.
+    private RemoteViewsFrameLayoutRefSet mRequestedViews;
+
+    private HandlerThread mWorkerThread;
+    // items may be interrupted within the normally processed queues
+    private Handler mWorkerQueue;
+    private Handler mMainQueue;
+
+    // We cache the FixedSizeRemoteViewsCaches across orientation. These are the related data
+    // structures;
+    private static final HashMap<RemoteViewsCacheKey, FixedSizeRemoteViewsCache>
+            sCachedRemoteViewsCaches = new HashMap<>();
+    private static final HashMap<RemoteViewsCacheKey, Runnable>
+            sRemoteViewsCacheRemoveRunnables = new HashMap<>();
+
+    private static HandlerThread sCacheRemovalThread;
+    private static Handler sCacheRemovalQueue;
+
+    // We keep the cache around for a duration after onSaveInstanceState for use on re-inflation.
+    // If a new RemoteViewsAdapter with the same intent / widget id isn't constructed within this
+    // duration, the cache is dropped.
+    private static final int REMOTE_VIEWS_CACHE_DURATION = 5000;
+
+    // Used to indicate to the AdapterView that it can use this Adapter immediately after
+    // construction (happens when we have a cached FixedSizeRemoteViewsCache).
+    private boolean mDataReady = false;
+
+    /**
+     * An interface for the RemoteAdapter to notify other classes when adapters
+     * are actually connected to/disconnected from their actual services.
+     */
+    public interface RemoteAdapterConnectionCallback {
+        /**
+         * @return whether the adapter was set or not.
+         */
+        boolean onRemoteAdapterConnected();
+
+        void onRemoteAdapterDisconnected();
+
+        /**
+         * This defers a notifyDataSetChanged on the pending RemoteViewsAdapter if it has not
+         * connected yet.
+         */
+        void deferNotifyDataSetChanged();
+
+        void setRemoteViewsAdapter(Intent intent, boolean isAsync);
+    }
+
+    public static class AsyncRemoteAdapterAction implements Runnable {
+
+        private final RemoteAdapterConnectionCallback mCallback;
+        private final Intent mIntent;
+
+        public AsyncRemoteAdapterAction(RemoteAdapterConnectionCallback callback, Intent intent) {
+            mCallback = callback;
+            mIntent = intent;
+        }
+
+        @Override
+        public void run() {
+            mCallback.setRemoteViewsAdapter(mIntent, true);
+        }
+    }
+
+    /**
+     * The service connection that gets populated when the RemoteViewsService is
+     * bound.  This must be a static inner class to ensure that no references to the outer
+     * RemoteViewsAdapter instance is retained (this would prevent the RemoteViewsAdapter from being
+     * garbage collected, and would cause us to leak activities due to the caching mechanism for
+     * FrameLayouts in the adapter).
+     */
+    private static class RemoteViewsAdapterServiceConnection extends
+            IRemoteViewsAdapterConnection.Stub {
+        private boolean mIsConnected;
+        private boolean mIsConnecting;
+        private WeakReference<RemoteViewsAdapter> mAdapter;
+        private IRemoteViewsFactory mRemoteViewsFactory;
+
+        public RemoteViewsAdapterServiceConnection(RemoteViewsAdapter adapter) {
+            mAdapter = new WeakReference<RemoteViewsAdapter>(adapter);
+        }
+
+        public synchronized void bind(Context context, int appWidgetId, Intent intent) {
+            if (!mIsConnecting) {
+                try {
+                    RemoteViewsAdapter adapter;
+                    final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
+                    if ((adapter = mAdapter.get()) != null) {
+                        mgr.bindRemoteViewsService(context.getOpPackageName(), appWidgetId,
+                                intent, asBinder());
+                    } else {
+                        Slog.w(TAG, "bind: adapter was null");
+                    }
+                    mIsConnecting = true;
+                } catch (Exception e) {
+                    Log.e("RVAServiceConnection", "bind(): " + e.getMessage());
+                    mIsConnecting = false;
+                    mIsConnected = false;
+                }
+            }
+        }
+
+        public synchronized void unbind(Context context, int appWidgetId, Intent intent) {
+            try {
+                RemoteViewsAdapter adapter;
+                final AppWidgetManager mgr = AppWidgetManager.getInstance(context);
+                if ((adapter = mAdapter.get()) != null) {
+                    mgr.unbindRemoteViewsService(context.getOpPackageName(), appWidgetId, intent);
+                } else {
+                    Slog.w(TAG, "unbind: adapter was null");
+                }
+                mIsConnecting = false;
+            } catch (Exception e) {
+                Log.e("RVAServiceConnection", "unbind(): " + e.getMessage());
+                mIsConnecting = false;
+                mIsConnected = false;
+            }
+        }
+
+        public synchronized void onServiceConnected(IBinder service) {
+            mRemoteViewsFactory = IRemoteViewsFactory.Stub.asInterface(service);
+
+            // Remove any deferred unbind messages
+            final RemoteViewsAdapter adapter = mAdapter.get();
+            if (adapter == null) return;
+
+            // Queue up work that we need to do for the callback to run
+            adapter.mWorkerQueue.post(new Runnable() {
+                @Override
+                public void run() {
+                    if (adapter.mNotifyDataSetChangedAfterOnServiceConnected) {
+                        // Handle queued notifyDataSetChanged() if necessary
+                        adapter.onNotifyDataSetChanged();
+                    } else {
+                        IRemoteViewsFactory factory =
+                            adapter.mServiceConnection.getRemoteViewsFactory();
+                        try {
+                            if (!factory.isCreated()) {
+                                // We only call onDataSetChanged() if this is the factory was just
+                                // create in response to this bind
+                                factory.onDataSetChanged();
+                            }
+                        } catch (RemoteException e) {
+                            Log.e(TAG, "Error notifying factory of data set changed in " +
+                                        "onServiceConnected(): " + e.getMessage());
+
+                            // Return early to prevent anything further from being notified
+                            // (effectively nothing has changed)
+                            return;
+                        } catch (RuntimeException e) {
+                            Log.e(TAG, "Error notifying factory of data set changed in " +
+                                    "onServiceConnected(): " + e.getMessage());
+                        }
+
+                        // Request meta data so that we have up to date data when calling back to
+                        // the remote adapter callback
+                        adapter.updateTemporaryMetaData();
+
+                        // Notify the host that we've connected
+                        adapter.mMainQueue.post(new Runnable() {
+                            @Override
+                            public void run() {
+                                synchronized (adapter.mCache) {
+                                    adapter.mCache.commitTemporaryMetaData();
+                                }
+
+                                final RemoteAdapterConnectionCallback callback =
+                                    adapter.mCallback.get();
+                                if (callback != null) {
+                                    callback.onRemoteAdapterConnected();
+                                }
+                            }
+                        });
+                    }
+
+                    // Enqueue unbind message
+                    adapter.enqueueDeferredUnbindServiceMessage();
+                    mIsConnected = true;
+                    mIsConnecting = false;
+                }
+            });
+        }
+
+        public synchronized void onServiceDisconnected() {
+            mIsConnected = false;
+            mIsConnecting = false;
+            mRemoteViewsFactory = null;
+
+            // Clear the main/worker queues
+            final RemoteViewsAdapter adapter = mAdapter.get();
+            if (adapter == null) return;
+
+            adapter.mMainQueue.post(new Runnable() {
+                @Override
+                public void run() {
+                    // Dequeue any unbind messages
+                    adapter.mMainQueue.removeMessages(sUnbindServiceMessageType);
+
+                    final RemoteAdapterConnectionCallback callback = adapter.mCallback.get();
+                    if (callback != null) {
+                        callback.onRemoteAdapterDisconnected();
+                    }
+                }
+            });
+        }
+
+        public synchronized IRemoteViewsFactory getRemoteViewsFactory() {
+            return mRemoteViewsFactory;
+        }
+
+        public synchronized boolean isConnected() {
+            return mIsConnected;
+        }
+    }
+
+    /**
+     * A FrameLayout which contains a loading view, and manages the re/applying of RemoteViews when
+     * they are loaded.
+     */
+    static class RemoteViewsFrameLayout extends AppWidgetHostView {
+        private final FixedSizeRemoteViewsCache mCache;
+
+        public RemoteViewsFrameLayout(Context context, FixedSizeRemoteViewsCache cache) {
+            super(context);
+            mCache = cache;
+        }
+
+        /**
+         * Updates this RemoteViewsFrameLayout depending on the view that was loaded.
+         * @param view the RemoteViews that was loaded. If null, the RemoteViews was not loaded
+         *             successfully.
+         * @param forceApplyAsync when true, the host will always try to inflate the view
+         *                        asynchronously (for eg, when we are already showing the loading
+         *                        view)
+         */
+        public void onRemoteViewsLoaded(RemoteViews view, OnClickHandler handler,
+                boolean forceApplyAsync) {
+            setOnClickHandler(handler);
+            applyRemoteViews(view, forceApplyAsync || ((view != null) && view.prefersAsyncApply()));
+        }
+
+        /**
+         * Creates a default loading view. Uses the size of the first row as a guide for the
+         * size of the loading view.
+         */
+        @Override
+        protected View getDefaultView() {
+            int viewHeight = mCache.getMetaData().getLoadingTemplate(getContext()).defaultHeight;
+            // Compose the loading view text
+            TextView loadingTextView = (TextView) LayoutInflater.from(getContext()).inflate(
+                    com.android.internal.R.layout.remote_views_adapter_default_loading_view,
+                    this, false);
+            loadingTextView.setHeight(viewHeight);
+            return loadingTextView;
+        }
+
+        @Override
+        protected Context getRemoteContext() {
+            return null;
+        }
+
+        @Override
+        protected View getErrorView() {
+            // Use the default loading view as the error view.
+            return getDefaultView();
+        }
+    }
+
+    /**
+     * Stores the references of all the RemoteViewsFrameLayouts that have been returned by the
+     * adapter that have not yet had their RemoteViews loaded.
+     */
+    private class RemoteViewsFrameLayoutRefSet {
+        private final SparseArray<LinkedList<RemoteViewsFrameLayout>> mReferences =
+                new SparseArray<>();
+        private final HashMap<RemoteViewsFrameLayout, LinkedList<RemoteViewsFrameLayout>>
+                mViewToLinkedList = new HashMap<>();
+
+        /**
+         * Adds a new reference to a RemoteViewsFrameLayout returned by the adapter.
+         */
+        public void add(int position, RemoteViewsFrameLayout layout) {
+            LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position);
+
+            // Create the list if necessary
+            if (refs == null) {
+                refs = new LinkedList<RemoteViewsFrameLayout>();
+                mReferences.put(position, refs);
+            }
+            mViewToLinkedList.put(layout, refs);
+
+            // Add the references to the list
+            refs.add(layout);
+        }
+
+        /**
+         * Notifies each of the RemoteViewsFrameLayouts associated with a particular position that
+         * the associated RemoteViews has loaded.
+         */
+        public void notifyOnRemoteViewsLoaded(int position, RemoteViews view) {
+            if (view == null) return;
+
+            final LinkedList<RemoteViewsFrameLayout> refs = mReferences.get(position);
+            if (refs != null) {
+                // Notify all the references for that position of the newly loaded RemoteViews
+                for (final RemoteViewsFrameLayout ref : refs) {
+                    ref.onRemoteViewsLoaded(view, mRemoteViewsOnClickHandler, true);
+                    if (mViewToLinkedList.containsKey(ref)) {
+                        mViewToLinkedList.remove(ref);
+                    }
+                }
+                refs.clear();
+                // Remove this set from the original mapping
+                mReferences.remove(position);
+            }
+        }
+
+        /**
+         * We need to remove views from this set if they have been recycled by the AdapterView.
+         */
+        public void removeView(RemoteViewsFrameLayout rvfl) {
+            if (mViewToLinkedList.containsKey(rvfl)) {
+                mViewToLinkedList.get(rvfl).remove(rvfl);
+                mViewToLinkedList.remove(rvfl);
+            }
+        }
+
+        /**
+         * Removes all references to all RemoteViewsFrameLayouts returned by the adapter.
+         */
+        public void clear() {
+            // We currently just clear the references, and leave all the previous layouts returned
+            // in their default state of the loading view.
+            mReferences.clear();
+            mViewToLinkedList.clear();
+        }
+    }
+
+    /**
+     * The meta-data associated with the cache in it's current state.
+     */
+    private static class RemoteViewsMetaData {
+        int count;
+        int viewTypeCount;
+        boolean hasStableIds;
+
+        // Used to determine how to construct loading views.  If a loading view is not specified
+        // by the user, then we try and load the first view, and use its height as the height for
+        // the default loading view.
+        LoadingViewTemplate loadingTemplate;
+
+        // A mapping from type id to a set of unique type ids
+        private final SparseIntArray mTypeIdIndexMap = new SparseIntArray();
+
+        public RemoteViewsMetaData() {
+            reset();
+        }
+
+        public void set(RemoteViewsMetaData d) {
+            synchronized (d) {
+                count = d.count;
+                viewTypeCount = d.viewTypeCount;
+                hasStableIds = d.hasStableIds;
+                loadingTemplate = d.loadingTemplate;
+            }
+        }
+
+        public void reset() {
+            count = 0;
+
+            // by default there is at least one dummy view type
+            viewTypeCount = 1;
+            hasStableIds = true;
+            loadingTemplate = null;
+            mTypeIdIndexMap.clear();
+        }
+
+        public int getMappedViewType(int typeId) {
+            int mappedTypeId = mTypeIdIndexMap.get(typeId, -1);
+            if (mappedTypeId == -1) {
+                // We +1 because the loading view always has view type id of 0
+                mappedTypeId = mTypeIdIndexMap.size() + 1;
+                mTypeIdIndexMap.put(typeId, mappedTypeId);
+            }
+            return mappedTypeId;
+        }
+
+        public boolean isViewTypeInRange(int typeId) {
+            int mappedType = getMappedViewType(typeId);
+            return (mappedType < viewTypeCount);
+        }
+
+        public synchronized LoadingViewTemplate getLoadingTemplate(Context context) {
+            if (loadingTemplate == null) {
+                loadingTemplate = new LoadingViewTemplate(null, context);
+            }
+            return loadingTemplate;
+        }
+    }
+
+    /**
+     * The meta-data associated with a single item in the cache.
+     */
+    private static class RemoteViewsIndexMetaData {
+        int typeId;
+        long itemId;
+
+        public RemoteViewsIndexMetaData(RemoteViews v, long itemId) {
+            set(v, itemId);
+        }
+
+        public void set(RemoteViews v, long id) {
+            itemId = id;
+            if (v != null) {
+                typeId = v.getLayoutId();
+            } else {
+                typeId = 0;
+            }
+        }
+    }
+
+    /**
+     *
+     */
+    private static class FixedSizeRemoteViewsCache {
+        private static final String TAG = "FixedSizeRemoteViewsCache";
+
+        // The meta data related to all the RemoteViews, ie. count, is stable, etc.
+        // The meta data objects are made final so that they can be locked on independently
+        // of the FixedSizeRemoteViewsCache. If we ever lock on both meta data objects, it is in
+        // the order mTemporaryMetaData followed by mMetaData.
+        private final RemoteViewsMetaData mMetaData = new RemoteViewsMetaData();
+        private final RemoteViewsMetaData mTemporaryMetaData = new RemoteViewsMetaData();
+
+        // The cache/mapping of position to RemoteViewsMetaData.  This set is guaranteed to be
+        // greater than or equal to the set of RemoteViews.
+        // Note: The reason that we keep this separate from the RemoteViews cache below is that this
+        // we still need to be able to access the mapping of position to meta data, without keeping
+        // the heavy RemoteViews around.  The RemoteViews cache is trimmed to fixed constraints wrt.
+        // memory and size, but this metadata cache will retain information until the data at the
+        // position is guaranteed as not being necessary any more (usually on notifyDataSetChanged).
+        private final SparseArray<RemoteViewsIndexMetaData> mIndexMetaData = new SparseArray<>();
+
+        // The cache of actual RemoteViews, which may be pruned if the cache gets too large, or uses
+        // too much memory.
+        private final SparseArray<RemoteViews> mIndexRemoteViews = new SparseArray<>();
+
+        // An array of indices to load, Indices which are explicitely requested are set to true,
+        // and those determined by the preloading algorithm to prefetch are set to false.
+        private final SparseBooleanArray mIndicesToLoad = new SparseBooleanArray();
+
+        // We keep a reference of the last requested index to determine which item to prune the
+        // farthest items from when we hit the memory limit
+        private int mLastRequestedIndex;
+
+
+        // The lower and upper bounds of the preloaded range
+        private int mPreloadLowerBound;
+        private int mPreloadUpperBound;
+
+        // The bounds of this fixed cache, we will try and fill as many items into the cache up to
+        // the maxCount number of items, or the maxSize memory usage.
+        // The maxCountSlack is used to determine if a new position in the cache to be loaded is
+        // sufficiently ouside the old set, prompting a shifting of the "window" of items to be
+        // preloaded.
+        private final int mMaxCount;
+        private final int mMaxCountSlack;
+        private static final float sMaxCountSlackPercent = 0.75f;
+        private static final int sMaxMemoryLimitInBytes = 2 * 1024 * 1024;
+
+        public FixedSizeRemoteViewsCache(int maxCacheSize) {
+            mMaxCount = maxCacheSize;
+            mMaxCountSlack = Math.round(sMaxCountSlackPercent * (mMaxCount / 2));
+            mPreloadLowerBound = 0;
+            mPreloadUpperBound = -1;
+            mLastRequestedIndex = -1;
+        }
+
+        public void insert(int position, RemoteViews v, long itemId, int[] visibleWindow) {
+            // Trim the cache if we go beyond the count
+            if (mIndexRemoteViews.size() >= mMaxCount) {
+                mIndexRemoteViews.remove(getFarthestPositionFrom(position, visibleWindow));
+            }
+
+            // Trim the cache if we go beyond the available memory size constraints
+            int pruneFromPosition = (mLastRequestedIndex > -1) ? mLastRequestedIndex : position;
+            while (getRemoteViewsBitmapMemoryUsage() >= sMaxMemoryLimitInBytes) {
+                // Note: This is currently the most naive mechanism for deciding what to prune when
+                // we hit the memory limit.  In the future, we may want to calculate which index to
+                // remove based on both its position as well as it's current memory usage, as well
+                // as whether it was directly requested vs. whether it was preloaded by our caching
+                // mechanism.
+                int trimIndex = getFarthestPositionFrom(pruneFromPosition, visibleWindow);
+
+                // Need to check that this is a valid index, to cover the case where you have only
+                // a single view in the cache, but it's larger than the max memory limit
+                if (trimIndex < 0) {
+                    break;
+                }
+
+                mIndexRemoteViews.remove(trimIndex);
+            }
+
+            // Update the metadata cache
+            final RemoteViewsIndexMetaData metaData = mIndexMetaData.get(position);
+            if (metaData != null) {
+                metaData.set(v, itemId);
+            } else {
+                mIndexMetaData.put(position, new RemoteViewsIndexMetaData(v, itemId));
+            }
+            mIndexRemoteViews.put(position, v);
+        }
+
+        public RemoteViewsMetaData getMetaData() {
+            return mMetaData;
+        }
+        public RemoteViewsMetaData getTemporaryMetaData() {
+            return mTemporaryMetaData;
+        }
+        public RemoteViews getRemoteViewsAt(int position) {
+            return mIndexRemoteViews.get(position);
+        }
+        public RemoteViewsIndexMetaData getMetaDataAt(int position) {
+            return mIndexMetaData.get(position);
+        }
+
+        public void commitTemporaryMetaData() {
+            synchronized (mTemporaryMetaData) {
+                synchronized (mMetaData) {
+                    mMetaData.set(mTemporaryMetaData);
+                }
+            }
+        }
+
+        private int getRemoteViewsBitmapMemoryUsage() {
+            // Calculate the memory usage of all the RemoteViews bitmaps being cached
+            int mem = 0;
+            for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) {
+                final RemoteViews v = mIndexRemoteViews.valueAt(i);
+                if (v != null) {
+                    mem += v.estimateMemoryUsage();
+                }
+            }
+            return mem;
+        }
+
+        private int getFarthestPositionFrom(int pos, int[] visibleWindow) {
+            // Find the index farthest away and remove that
+            int maxDist = 0;
+            int maxDistIndex = -1;
+            int maxDistNotVisible = 0;
+            int maxDistIndexNotVisible = -1;
+            for (int i = mIndexRemoteViews.size() - 1; i >= 0; i--) {
+                int index = mIndexRemoteViews.keyAt(i);
+                int dist = Math.abs(index-pos);
+                if (dist > maxDistNotVisible && Arrays.binarySearch(visibleWindow, index) < 0) {
+                    // maxDistNotVisible/maxDistIndexNotVisible will store the index of the
+                    // farthest non-visible position
+                    maxDistIndexNotVisible = index;
+                    maxDistNotVisible = dist;
+                }
+                if (dist >= maxDist) {
+                    // maxDist/maxDistIndex will store the index of the farthest position
+                    // regardless of whether it is visible or not
+                    maxDistIndex = index;
+                    maxDist = dist;
+                }
+            }
+            if (maxDistIndexNotVisible > -1) {
+                return maxDistIndexNotVisible;
+            }
+            return maxDistIndex;
+        }
+
+        public void queueRequestedPositionToLoad(int position) {
+            mLastRequestedIndex = position;
+            synchronized (mIndicesToLoad) {
+                mIndicesToLoad.put(position, true);
+            }
+        }
+        public boolean queuePositionsToBePreloadedFromRequestedPosition(int position) {
+            // Check if we need to preload any items
+            if (mPreloadLowerBound <= position && position <= mPreloadUpperBound) {
+                int center = (mPreloadUpperBound + mPreloadLowerBound) / 2;
+                if (Math.abs(position - center) < mMaxCountSlack) {
+                    return false;
+                }
+            }
+
+            int count = 0;
+            synchronized (mMetaData) {
+                count = mMetaData.count;
+            }
+            synchronized (mIndicesToLoad) {
+                // Remove all indices which have not been previously requested.
+                for (int i = mIndicesToLoad.size() - 1; i >= 0; i--) {
+                    if (!mIndicesToLoad.valueAt(i)) {
+                        mIndicesToLoad.removeAt(i);
+                    }
+                }
+
+                // Add all the preload indices
+                int halfMaxCount = mMaxCount / 2;
+                mPreloadLowerBound = position - halfMaxCount;
+                mPreloadUpperBound = position + halfMaxCount;
+                int effectiveLowerBound = Math.max(0, mPreloadLowerBound);
+                int effectiveUpperBound = Math.min(mPreloadUpperBound, count - 1);
+                for (int i = effectiveLowerBound; i <= effectiveUpperBound; ++i) {
+                    if (mIndexRemoteViews.indexOfKey(i) < 0 && !mIndicesToLoad.get(i)) {
+                        // If the index has not been requested, and has not been loaded.
+                        mIndicesToLoad.put(i, false);
+                    }
+                }
+            }
+            return true;
+        }
+        /** Returns the next index to load */
+        public int getNextIndexToLoad() {
+            // We try and prioritize items that have been requested directly, instead
+            // of items that are loaded as a result of the caching mechanism
+            synchronized (mIndicesToLoad) {
+                // Prioritize requested indices to be loaded first
+                int index = mIndicesToLoad.indexOfValue(true);
+                if (index < 0) {
+                    // Otherwise, preload other indices as necessary
+                    index = mIndicesToLoad.indexOfValue(false);
+                }
+                if (index < 0) {
+                    return -1;
+                } else {
+                    int key = mIndicesToLoad.keyAt(index);
+                    mIndicesToLoad.removeAt(index);
+                    return key;
+                }
+            }
+        }
+
+        public boolean containsRemoteViewAt(int position) {
+            return mIndexRemoteViews.indexOfKey(position) >= 0;
+        }
+        public boolean containsMetaDataAt(int position) {
+            return mIndexMetaData.indexOfKey(position) >= 0;
+        }
+
+        public void reset() {
+            // Note: We do not try and reset the meta data, since that information is still used by
+            // collection views to validate it's own contents (and will be re-requested if the data
+            // is invalidated through the notifyDataSetChanged() flow).
+
+            mPreloadLowerBound = 0;
+            mPreloadUpperBound = -1;
+            mLastRequestedIndex = -1;
+            mIndexRemoteViews.clear();
+            mIndexMetaData.clear();
+            synchronized (mIndicesToLoad) {
+                mIndicesToLoad.clear();
+            }
+        }
+    }
+
+    static class RemoteViewsCacheKey {
+        final Intent.FilterComparison filter;
+        final int widgetId;
+
+        RemoteViewsCacheKey(Intent.FilterComparison filter, int widgetId) {
+            this.filter = filter;
+            this.widgetId = widgetId;
+        }
+
+        @Override
+        public boolean equals(Object o) {
+            if (!(o instanceof RemoteViewsCacheKey)) {
+                return false;
+            }
+            RemoteViewsCacheKey other = (RemoteViewsCacheKey) o;
+            return other.filter.equals(filter) && other.widgetId == widgetId;
+        }
+
+        @Override
+        public int hashCode() {
+            return (filter == null ? 0 : filter.hashCode()) ^ (widgetId << 2);
+        }
+    }
+
+    public RemoteViewsAdapter(Context context, Intent intent,
+            RemoteAdapterConnectionCallback callback, boolean useAsyncLoader) {
+        mContext = context;
+        mIntent = intent;
+
+        if (mIntent == null) {
+            throw new IllegalArgumentException("Non-null Intent must be specified.");
+        }
+
+        mAppWidgetId = intent.getIntExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID, -1);
+        mRequestedViews = new RemoteViewsFrameLayoutRefSet();
+
+        // Strip the previously injected app widget id from service intent
+        if (intent.hasExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID)) {
+            intent.removeExtra(RemoteViews.EXTRA_REMOTEADAPTER_APPWIDGET_ID);
+        }
+
+        // Initialize the worker thread
+        mWorkerThread = new HandlerThread("RemoteViewsCache-loader");
+        mWorkerThread.start();
+        mWorkerQueue = new Handler(mWorkerThread.getLooper());
+        mMainQueue = new Handler(Looper.myLooper(), this);
+        mAsyncViewLoadExecutor = useAsyncLoader ? new HandlerThreadExecutor(mWorkerThread) : null;
+
+        if (sCacheRemovalThread == null) {
+            sCacheRemovalThread = new HandlerThread("RemoteViewsAdapter-cachePruner");
+            sCacheRemovalThread.start();
+            sCacheRemovalQueue = new Handler(sCacheRemovalThread.getLooper());
+        }
+
+        // Initialize the cache and the service connection on startup
+        mCallback = new WeakReference<RemoteAdapterConnectionCallback>(callback);
+        mServiceConnection = new RemoteViewsAdapterServiceConnection(this);
+
+        RemoteViewsCacheKey key = new RemoteViewsCacheKey(new Intent.FilterComparison(mIntent),
+                mAppWidgetId);
+
+        synchronized(sCachedRemoteViewsCaches) {
+            if (sCachedRemoteViewsCaches.containsKey(key)) {
+                mCache = sCachedRemoteViewsCaches.get(key);
+                synchronized (mCache.mMetaData) {
+                    if (mCache.mMetaData.count > 0) {
+                        // As a precautionary measure, we verify that the meta data indicates a
+                        // non-zero count before declaring that data is ready.
+                        mDataReady = true;
+                    }
+                }
+            } else {
+                mCache = new FixedSizeRemoteViewsCache(sDefaultCacheSize);
+            }
+            if (!mDataReady) {
+                requestBindService();
+            }
+        }
+    }
+
+    @Override
+    protected void finalize() throws Throwable {
+        try {
+            if (mWorkerThread != null) {
+                mWorkerThread.quit();
+            }
+        } finally {
+            super.finalize();
+        }
+    }
+
+    public boolean isDataReady() {
+        return mDataReady;
+    }
+
+    public void setRemoteViewsOnClickHandler(OnClickHandler handler) {
+        mRemoteViewsOnClickHandler = handler;
+    }
+
+    public void saveRemoteViewsCache() {
+        final RemoteViewsCacheKey key = new RemoteViewsCacheKey(
+                new Intent.FilterComparison(mIntent), mAppWidgetId);
+
+        synchronized(sCachedRemoteViewsCaches) {
+            // If we already have a remove runnable posted for this key, remove it.
+            if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+                sCacheRemovalQueue.removeCallbacks(sRemoteViewsCacheRemoveRunnables.get(key));
+                sRemoteViewsCacheRemoveRunnables.remove(key);
+            }
+
+            int metaDataCount = 0;
+            int numRemoteViewsCached = 0;
+            synchronized (mCache.mMetaData) {
+                metaDataCount = mCache.mMetaData.count;
+            }
+            synchronized (mCache) {
+                numRemoteViewsCached = mCache.mIndexRemoteViews.size();
+            }
+            if (metaDataCount > 0 && numRemoteViewsCached > 0) {
+                sCachedRemoteViewsCaches.put(key, mCache);
+            }
+
+            Runnable r = new Runnable() {
+                @Override
+                public void run() {
+                    synchronized (sCachedRemoteViewsCaches) {
+                        if (sCachedRemoteViewsCaches.containsKey(key)) {
+                            sCachedRemoteViewsCaches.remove(key);
+                        }
+                        if (sRemoteViewsCacheRemoveRunnables.containsKey(key)) {
+                            sRemoteViewsCacheRemoveRunnables.remove(key);
+                        }
+                    }
+                }
+            };
+            sRemoteViewsCacheRemoveRunnables.put(key, r);
+            sCacheRemovalQueue.postDelayed(r, REMOTE_VIEWS_CACHE_DURATION);
+        }
+    }
+
+    private void loadNextIndexInBackground() {
+        mWorkerQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                if (mServiceConnection.isConnected()) {
+                    // Get the next index to load
+                    int position = -1;
+                    synchronized (mCache) {
+                        position = mCache.getNextIndexToLoad();
+                    }
+                    if (position > -1) {
+                        // Load the item, and notify any existing RemoteViewsFrameLayouts
+                        updateRemoteViews(position, true);
+
+                        // Queue up for the next one to load
+                        loadNextIndexInBackground();
+                    } else {
+                        // No more items to load, so queue unbind
+                        enqueueDeferredUnbindServiceMessage();
+                    }
+                }
+            }
+        });
+    }
+
+    private void processException(String method, Exception e) {
+        Log.e("RemoteViewsAdapter", "Error in " + method + ": " + e.getMessage());
+
+        // If we encounter a crash when updating, we should reset the metadata & cache and trigger
+        // a notifyDataSetChanged to update the widget accordingly
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            metaData.reset();
+        }
+        synchronized (mCache) {
+            mCache.reset();
+        }
+        mMainQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                superNotifyDataSetChanged();
+            }
+        });
+    }
+
+    private void updateTemporaryMetaData() {
+        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+        try {
+            // get the properties/first view (so that we can use it to
+            // measure our dummy views)
+            boolean hasStableIds = factory.hasStableIds();
+            int viewTypeCount = factory.getViewTypeCount();
+            int count = factory.getCount();
+            LoadingViewTemplate loadingTemplate =
+                    new LoadingViewTemplate(factory.getLoadingView(), mContext);
+            if ((count > 0) && (loadingTemplate.remoteViews == null)) {
+                RemoteViews firstView = factory.getViewAt(0);
+                if (firstView != null) {
+                    loadingTemplate.loadFirstViewHeight(firstView, mContext,
+                            new HandlerThreadExecutor(mWorkerThread));
+                }
+            }
+            final RemoteViewsMetaData tmpMetaData = mCache.getTemporaryMetaData();
+            synchronized (tmpMetaData) {
+                tmpMetaData.hasStableIds = hasStableIds;
+                // We +1 because the base view type is the loading view
+                tmpMetaData.viewTypeCount = viewTypeCount + 1;
+                tmpMetaData.count = count;
+                tmpMetaData.loadingTemplate = loadingTemplate;
+            }
+        } catch(RemoteException e) {
+            processException("updateMetaData", e);
+        } catch(RuntimeException e) {
+            processException("updateMetaData", e);
+        }
+    }
+
+    private void updateRemoteViews(final int position, boolean notifyWhenLoaded) {
+        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+
+        // Load the item information from the remote service
+        RemoteViews remoteViews = null;
+        long itemId = 0;
+        try {
+            remoteViews = factory.getViewAt(position);
+            itemId = factory.getItemId(position);
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
+
+            // Return early to prevent additional work in re-centering the view cache, and
+            // swapping from the loading view
+            return;
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + e.getMessage());
+            return;
+        }
+
+        if (remoteViews == null) {
+            // If a null view was returned, we break early to prevent it from getting
+            // into our cache and causing problems later. The effect is that the child  at this
+            // position will remain as a loading view until it is updated.
+            Log.e(TAG, "Error in updateRemoteViews(" + position + "): " + " null RemoteViews " +
+                    "returned from RemoteViewsFactory.");
+            return;
+        }
+
+        int layoutId = remoteViews.getLayoutId();
+        RemoteViewsMetaData metaData = mCache.getMetaData();
+        boolean viewTypeInRange;
+        int cacheCount;
+        synchronized (metaData) {
+            viewTypeInRange = metaData.isViewTypeInRange(layoutId);
+            cacheCount = mCache.mMetaData.count;
+        }
+        synchronized (mCache) {
+            if (viewTypeInRange) {
+                int[] visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
+                        mVisibleWindowUpperBound, cacheCount);
+                // Cache the RemoteViews we loaded
+                mCache.insert(position, remoteViews, itemId, visibleWindow);
+
+                // Notify all the views that we have previously returned for this index that
+                // there is new data for it.
+                final RemoteViews rv = remoteViews;
+                if (notifyWhenLoaded) {
+                    mMainQueue.post(new Runnable() {
+                        @Override
+                        public void run() {
+                            mRequestedViews.notifyOnRemoteViewsLoaded(position, rv);
+                        }
+                    });
+                }
+            } else {
+                // We need to log an error here, as the the view type count specified by the
+                // factory is less than the number of view types returned. We don't return this
+                // view to the AdapterView, as this will cause an exception in the hosting process,
+                // which contains the associated AdapterView.
+                Log.e(TAG, "Error: widget's RemoteViewsFactory returns more view types than " +
+                        " indicated by getViewTypeCount() ");
+            }
+        }
+    }
+
+    public Intent getRemoteViewsServiceIntent() {
+        return mIntent;
+    }
+
+    public int getCount() {
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.count;
+        }
+    }
+
+    public Object getItem(int position) {
+        // Disallow arbitrary object to be associated with an item for the time being
+        return null;
+    }
+
+    public long getItemId(int position) {
+        synchronized (mCache) {
+            if (mCache.containsMetaDataAt(position)) {
+                return mCache.getMetaDataAt(position).itemId;
+            }
+            return 0;
+        }
+    }
+
+    public int getItemViewType(int position) {
+        int typeId = 0;
+        synchronized (mCache) {
+            if (mCache.containsMetaDataAt(position)) {
+                typeId = mCache.getMetaDataAt(position).typeId;
+            } else {
+                return 0;
+            }
+        }
+
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.getMappedViewType(typeId);
+        }
+    }
+
+    /**
+     * This method allows an AdapterView using this Adapter to provide information about which
+     * views are currently being displayed. This allows for certain optimizations and preloading
+     * which  wouldn't otherwise be possible.
+     */
+    public void setVisibleRangeHint(int lowerBound, int upperBound) {
+        mVisibleWindowLowerBound = lowerBound;
+        mVisibleWindowUpperBound = upperBound;
+    }
+
+    public View getView(int position, View convertView, ViewGroup parent) {
+        // "Request" an index so that we can queue it for loading, initiate subsequent
+        // preloading, etc.
+        synchronized (mCache) {
+            RemoteViews rv = mCache.getRemoteViewsAt(position);
+            boolean isInCache = (rv != null);
+            boolean isConnected = mServiceConnection.isConnected();
+            boolean hasNewItems = false;
+
+            if (convertView != null && convertView instanceof RemoteViewsFrameLayout) {
+                mRequestedViews.removeView((RemoteViewsFrameLayout) convertView);
+            }
+
+            if (!isInCache && !isConnected) {
+                // Requesting bind service will trigger a super.notifyDataSetChanged(), which will
+                // in turn trigger another request to getView()
+                requestBindService();
+            } else {
+                // Queue up other indices to be preloaded based on this position
+                hasNewItems = mCache.queuePositionsToBePreloadedFromRequestedPosition(position);
+            }
+
+            final RemoteViewsFrameLayout layout;
+            if (convertView instanceof RemoteViewsFrameLayout) {
+                layout = (RemoteViewsFrameLayout) convertView;
+            } else {
+                layout = new RemoteViewsFrameLayout(parent.getContext(), mCache);
+                layout.setExecutor(mAsyncViewLoadExecutor);
+            }
+
+            if (isInCache) {
+                // Apply the view synchronously if possible, to avoid flickering
+                layout.onRemoteViewsLoaded(rv, mRemoteViewsOnClickHandler, false);
+                if (hasNewItems) loadNextIndexInBackground();
+            } else {
+                // If the views is not loaded, apply the loading view. If the loading view doesn't
+                // exist, the layout will create a default view based on the firstView height.
+                layout.onRemoteViewsLoaded(
+                        mCache.getMetaData().getLoadingTemplate(mContext).remoteViews,
+                        mRemoteViewsOnClickHandler,
+                        false);
+                mRequestedViews.add(position, layout);
+                mCache.queueRequestedPositionToLoad(position);
+                loadNextIndexInBackground();
+            }
+            return layout;
+        }
+    }
+
+    public int getViewTypeCount() {
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.viewTypeCount;
+        }
+    }
+
+    public boolean hasStableIds() {
+        final RemoteViewsMetaData metaData = mCache.getMetaData();
+        synchronized (metaData) {
+            return metaData.hasStableIds;
+        }
+    }
+
+    public boolean isEmpty() {
+        return getCount() <= 0;
+    }
+
+    private void onNotifyDataSetChanged() {
+        // Complete the actual notifyDataSetChanged() call initiated earlier
+        IRemoteViewsFactory factory = mServiceConnection.getRemoteViewsFactory();
+        try {
+            factory.onDataSetChanged();
+        } catch (RemoteException e) {
+            Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+
+            // Return early to prevent from further being notified (since nothing has
+            // changed)
+            return;
+        } catch (RuntimeException e) {
+            Log.e(TAG, "Error in updateNotifyDataSetChanged(): " + e.getMessage());
+            return;
+        }
+
+        // Flush the cache so that we can reload new items from the service
+        synchronized (mCache) {
+            mCache.reset();
+        }
+
+        // Re-request the new metadata (only after the notification to the factory)
+        updateTemporaryMetaData();
+        int newCount;
+        int[] visibleWindow;
+        synchronized(mCache.getTemporaryMetaData()) {
+            newCount = mCache.getTemporaryMetaData().count;
+            visibleWindow = getVisibleWindow(mVisibleWindowLowerBound,
+                    mVisibleWindowUpperBound, newCount);
+        }
+
+        // Pre-load (our best guess of) the views which are currently visible in the AdapterView.
+        // This mitigates flashing and flickering of loading views when a widget notifies that
+        // its data has changed.
+        for (int i: visibleWindow) {
+            // Because temporary meta data is only ever modified from this thread (ie.
+            // mWorkerThread), it is safe to assume that count is a valid representation.
+            if (i < newCount) {
+                updateRemoteViews(i, false);
+            }
+        }
+
+        // Propagate the notification back to the base adapter
+        mMainQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                synchronized (mCache) {
+                    mCache.commitTemporaryMetaData();
+                }
+
+                superNotifyDataSetChanged();
+                enqueueDeferredUnbindServiceMessage();
+            }
+        });
+
+        // Reset the notify flagflag
+        mNotifyDataSetChangedAfterOnServiceConnected = false;
+    }
+
+    /**
+     * Returns a sorted array of all integers between lower and upper.
+     */
+    private int[] getVisibleWindow(int lower, int upper, int count) {
+        // In the case that the window is invalid or uninitialized, return an empty window.
+        if ((lower == 0 && upper == 0) || lower < 0 || upper < 0) {
+            return new int[0];
+        }
+
+        int[] window;
+        if (lower <= upper) {
+            window = new int[upper + 1 - lower];
+            for (int i = lower, j = 0;  i <= upper; i++, j++){
+                window[j] = i;
+            }
+        } else {
+            // If the upper bound is less than the lower bound it means that the visible window
+            // wraps around.
+            count = Math.max(count, lower);
+            window = new int[count - lower + upper + 1];
+            int j = 0;
+            // Add the entries in sorted order
+            for (int i = 0; i <= upper; i++, j++) {
+                window[j] = i;
+            }
+            for (int i = lower; i < count; i++, j++) {
+                window[j] = i;
+            }
+        }
+        return window;
+    }
+
+    public void notifyDataSetChanged() {
+        // Dequeue any unbind messages
+        mMainQueue.removeMessages(sUnbindServiceMessageType);
+
+        // If we are not connected, queue up the notifyDataSetChanged to be handled when we do
+        // connect
+        if (!mServiceConnection.isConnected()) {
+            mNotifyDataSetChangedAfterOnServiceConnected = true;
+            requestBindService();
+            return;
+        }
+
+        mWorkerQueue.post(new Runnable() {
+            @Override
+            public void run() {
+                onNotifyDataSetChanged();
+            }
+        });
+    }
+
+    void superNotifyDataSetChanged() {
+        super.notifyDataSetChanged();
+    }
+
+    @Override
+    public boolean handleMessage(Message msg) {
+        boolean result = false;
+        switch (msg.what) {
+        case sUnbindServiceMessageType:
+            if (mServiceConnection.isConnected()) {
+                mServiceConnection.unbind(mContext, mAppWidgetId, mIntent);
+            }
+            result = true;
+            break;
+        default:
+            break;
+        }
+        return result;
+    }
+
+    private void enqueueDeferredUnbindServiceMessage() {
+        // Remove any existing deferred-unbind messages
+        mMainQueue.removeMessages(sUnbindServiceMessageType);
+        mMainQueue.sendEmptyMessageDelayed(sUnbindServiceMessageType, sUnbindServiceDelay);
+    }
+
+    private boolean requestBindService() {
+        // Try binding the service (which will start it if it's not already running)
+        if (!mServiceConnection.isConnected()) {
+            mServiceConnection.bind(mContext, mAppWidgetId, mIntent);
+        }
+
+        // Remove any existing deferred-unbind messages
+        mMainQueue.removeMessages(sUnbindServiceMessageType);
+        return mServiceConnection.isConnected();
+    }
+
+    private static class HandlerThreadExecutor implements Executor {
+        private final HandlerThread mThread;
+
+        HandlerThreadExecutor(HandlerThread thread) {
+            mThread = thread;
+        }
+
+        @Override
+        public void execute(Runnable runnable) {
+            if (Thread.currentThread().getId() == mThread.getId()) {
+                runnable.run();
+            } else {
+                new Handler(mThread.getLooper()).post(runnable);
+            }
+        }
+    }
+
+    private static class LoadingViewTemplate {
+        public final RemoteViews remoteViews;
+        public int defaultHeight;
+
+        LoadingViewTemplate(RemoteViews views, Context context) {
+            remoteViews = views;
+
+            float density = context.getResources().getDisplayMetrics().density;
+            defaultHeight = Math.round(sDefaultLoadingViewHeight * density);
+        }
+
+        public void loadFirstViewHeight(
+                RemoteViews firstView, Context context, Executor executor) {
+            // Inflate the first view on the worker thread
+            firstView.applyAsync(context, new RemoteViewsFrameLayout(context, null), executor,
+                    new RemoteViews.OnViewAppliedListener() {
+                        @Override
+                        public void onViewApplied(View v) {
+                            try {
+                                v.measure(
+                                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED),
+                                        MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED));
+                                defaultHeight = v.getMeasuredHeight();
+                            } catch (Exception e) {
+                                onError(e);
+                            }
+                        }
+
+                        @Override
+                        public void onError(Exception e) {
+                            // Do nothing. The default height will stay the same.
+                            Log.w(TAG, "Error inflating first RemoteViews", e);
+                        }
+                    });
+        }
+    }
+}
diff --git a/android/widget/RemoteViewsListAdapter.java b/android/widget/RemoteViewsListAdapter.java
new file mode 100644
index 0000000..e490458
--- /dev/null
+++ b/android/widget/RemoteViewsListAdapter.java
@@ -0,0 +1,121 @@
+/*
+ * 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 android.widget;
+
+import android.content.Context;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+
+/**
+ * @hide
+ */
+public class RemoteViewsListAdapter extends BaseAdapter {
+
+    private Context mContext;
+    private ArrayList<RemoteViews> mRemoteViewsList;
+    private ArrayList<Integer> mViewTypes = new ArrayList<Integer>();
+    private int mViewTypeCount;
+
+    public RemoteViewsListAdapter(Context context, ArrayList<RemoteViews> remoteViews,
+            int viewTypeCount) {
+        mContext = context;
+        mRemoteViewsList = remoteViews;
+        mViewTypeCount = viewTypeCount;
+        init();
+    }
+
+    public void setViewsList(ArrayList<RemoteViews> remoteViews) {
+        mRemoteViewsList = remoteViews;
+        init();
+        notifyDataSetChanged();
+    }
+
+    private void init() {
+        if (mRemoteViewsList == null) return;
+
+        mViewTypes.clear();
+        for (RemoteViews rv: mRemoteViewsList) {
+            if (!mViewTypes.contains(rv.getLayoutId())) {
+                mViewTypes.add(rv.getLayoutId());
+            }
+        }
+
+        if (mViewTypes.size() > mViewTypeCount || mViewTypeCount < 1) {
+            throw new RuntimeException("Invalid view type count -- view type count must be >= 1" +
+                    "and must be as large as the total number of distinct view types");
+        }
+    }
+
+    @Override
+    public int getCount() {
+        if (mRemoteViewsList != null) {
+            return mRemoteViewsList.size();
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    public Object getItem(int position) {
+        return null;
+    }
+
+    @Override
+    public long getItemId(int position) {
+        return position;
+    }
+
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        if (position < getCount()) {
+            RemoteViews rv = mRemoteViewsList.get(position);
+            rv.setIsWidgetCollectionChild(true);
+            View v;
+            if (convertView != null && rv != null &&
+                    convertView.getId() == rv.getLayoutId()) {
+                v = convertView;
+                rv.reapply(mContext, v);
+            } else {
+                v = rv.apply(mContext, parent);
+            }
+            return v;
+        } else {
+            return null;
+        }
+    }
+
+    @Override
+    public int getItemViewType(int position) {
+        if (position < getCount()) {
+            int layoutId = mRemoteViewsList.get(position).getLayoutId();
+            return mViewTypes.indexOf(layoutId);
+        } else {
+            return 0;
+        }
+    }
+
+    public int getViewTypeCount() {
+        return mViewTypeCount;
+    }
+
+    @Override
+    public boolean hasStableIds() {
+        return false;
+    }
+}
diff --git a/android/widget/RemoteViewsService.java b/android/widget/RemoteViewsService.java
new file mode 100644
index 0000000..2827f63
--- /dev/null
+++ b/android/widget/RemoteViewsService.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.app.Service;
+import android.content.Intent;
+import android.os.IBinder;
+
+import com.android.internal.widget.IRemoteViewsFactory;
+
+import java.util.HashMap;
+
+/**
+ * The service to be connected to for a remote adapter to request RemoteViews.  Users should
+ * extend the RemoteViewsService to provide the appropriate RemoteViewsFactory's used to
+ * populate the remote collection view (ListView, GridView, etc).
+ */
+public abstract class RemoteViewsService extends Service {
+
+    private static final String LOG_TAG = "RemoteViewsService";
+
+    // Used for reference counting of RemoteViewsFactories
+    // Because we are now unbinding when we are not using the Service (to allow them to be
+    // reclaimed), the references to the factories that are created need to be stored and used when
+    // the service is restarted (in response to user input for example).  When the process is
+    // destroyed, so is this static cache of RemoteViewsFactories.
+    private static final HashMap<Intent.FilterComparison, RemoteViewsFactory> sRemoteViewFactories =
+            new HashMap<Intent.FilterComparison, RemoteViewsFactory>();
+    private static final Object sLock = new Object();
+
+    /**
+     * An interface for an adapter between a remote collection view (ListView, GridView, etc) and
+     * the underlying data for that view.  The implementor is responsible for making a RemoteView
+     * for each item in the data set. This interface is a thin wrapper around {@link Adapter}.
+     *
+     * @see android.widget.Adapter
+     * @see android.appwidget.AppWidgetManager
+     */
+    public interface RemoteViewsFactory {
+        /**
+         * Called when your factory is first constructed. The same factory may be shared across
+         * multiple RemoteViewAdapters depending on the intent passed.
+         */
+        public void onCreate();
+
+        /**
+         * Called when notifyDataSetChanged() is triggered on the remote adapter. This allows a
+         * RemoteViewsFactory to respond to data changes by updating any internal references.
+         *
+         * Note: expensive tasks can be safely performed synchronously within this method. In the
+         * interim, the old data will be displayed within the widget.
+         *
+         * @see android.appwidget.AppWidgetManager#notifyAppWidgetViewDataChanged(int[], int)
+         */
+        public void onDataSetChanged();
+
+        /**
+         * Called when the last RemoteViewsAdapter that is associated with this factory is
+         * unbound.
+         */
+        public void onDestroy();
+
+        /**
+         * See {@link Adapter#getCount()}
+         *
+         * @return Count of items.
+         */
+        public int getCount();
+
+        /**
+         * See {@link Adapter#getView(int, android.view.View, android.view.ViewGroup)}.
+         *
+         * Note: expensive tasks can be safely performed synchronously within this method, and a
+         * loading view will be displayed in the interim. See {@link #getLoadingView()}.
+         *
+         * @param position The position of the item within the Factory's data set of the item whose
+         *        view we want.
+         * @return A RemoteViews object corresponding to the data at the specified position.
+         */
+        public RemoteViews getViewAt(int position);
+
+        /**
+         * This allows for the use of a custom loading view which appears between the time that
+         * {@link #getViewAt(int)} is called and returns. If null is returned, a default loading
+         * view will be used.
+         *
+         * @return The RemoteViews representing the desired loading view.
+         */
+        public RemoteViews getLoadingView();
+
+        /**
+         * See {@link Adapter#getViewTypeCount()}.
+         *
+         * @return The number of types of Views that will be returned by this factory.
+         */
+        public int getViewTypeCount();
+
+        /**
+         * See {@link Adapter#getItemId(int)}.
+         *
+         * @param position The position of the item within the data set whose row id we want.
+         * @return The id of the item at the specified position.
+         */
+        public long getItemId(int position);
+
+        /**
+         * See {@link Adapter#hasStableIds()}.
+         *
+         * @return True if the same id always refers to the same object.
+         */
+        public boolean hasStableIds();
+    }
+
+    /**
+     * A private proxy class for the private IRemoteViewsFactory interface through the
+     * public RemoteViewsFactory interface.
+     */
+    private static class RemoteViewsFactoryAdapter extends IRemoteViewsFactory.Stub {
+        public RemoteViewsFactoryAdapter(RemoteViewsFactory factory, boolean isCreated) {
+            mFactory = factory;
+            mIsCreated = isCreated;
+        }
+        public synchronized boolean isCreated() {
+            return mIsCreated;
+        }
+        public synchronized void onDataSetChanged() {
+            try {
+                mFactory.onDataSetChanged();
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+        }
+        public synchronized void onDataSetChangedAsync() {
+            onDataSetChanged();
+        }
+        public synchronized int getCount() {
+            int count = 0;
+            try {
+                count = mFactory.getCount();
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return count;
+        }
+        public synchronized RemoteViews getViewAt(int position) {
+            RemoteViews rv = null;
+            try {
+                rv = mFactory.getViewAt(position);
+                if (rv != null) {
+                    rv.setIsWidgetCollectionChild(true);
+                }
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return rv;
+        }
+        public synchronized RemoteViews getLoadingView() {
+            RemoteViews rv = null;
+            try {
+                rv = mFactory.getLoadingView();
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return rv;
+        }
+        public synchronized int getViewTypeCount() {
+            int count = 0;
+            try {
+                count = mFactory.getViewTypeCount();
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return count;
+        }
+        public synchronized long getItemId(int position) {
+            long id = 0;
+            try {
+                id = mFactory.getItemId(position);
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return id;
+        }
+        public synchronized boolean hasStableIds() {
+            boolean hasStableIds = false;
+            try {
+                hasStableIds = mFactory.hasStableIds();
+            } catch (Exception ex) {
+                Thread t = Thread.currentThread();
+                Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+            }
+            return hasStableIds;
+        }
+        public void onDestroy(Intent intent) {
+            synchronized (sLock) {
+                Intent.FilterComparison fc = new Intent.FilterComparison(intent);
+                if (RemoteViewsService.sRemoteViewFactories.containsKey(fc)) {
+                    RemoteViewsFactory factory = RemoteViewsService.sRemoteViewFactories.get(fc);
+                    try {
+                        factory.onDestroy();
+                    } catch (Exception ex) {
+                        Thread t = Thread.currentThread();
+                        Thread.getDefaultUncaughtExceptionHandler().uncaughtException(t, ex);
+                    }
+                    RemoteViewsService.sRemoteViewFactories.remove(fc);
+                }
+            }
+        }
+
+        private RemoteViewsFactory mFactory;
+        private boolean mIsCreated;
+    }
+
+    @Override
+    public IBinder onBind(Intent intent) {
+        synchronized (sLock) {
+            Intent.FilterComparison fc = new Intent.FilterComparison(intent);
+            RemoteViewsFactory factory = null;
+            boolean isCreated = false;
+            if (!sRemoteViewFactories.containsKey(fc)) {
+                factory = onGetViewFactory(intent);
+                sRemoteViewFactories.put(fc, factory);
+                factory.onCreate();
+                isCreated = false;
+            } else {
+                factory = sRemoteViewFactories.get(fc);
+                isCreated = true;
+            }
+            return new RemoteViewsFactoryAdapter(factory, isCreated);
+        }
+    }
+
+    /**
+     * To be implemented by the derived service to generate appropriate factories for
+     * the data.
+     */
+    public abstract RemoteViewsFactory onGetViewFactory(Intent intent);
+}
diff --git a/android/widget/ResourceCursorAdapter.java b/android/widget/ResourceCursorAdapter.java
new file mode 100644
index 0000000..9732bb1
--- /dev/null
+++ b/android/widget/ResourceCursorAdapter.java
@@ -0,0 +1,160 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+
+/**
+ * An easy adapter that creates views defined in an XML file. You can specify
+ * the XML file that defines the appearance of the views.
+ */
+public abstract class ResourceCursorAdapter extends CursorAdapter {
+    private int mLayout;
+
+    private int mDropDownLayout;
+
+    private LayoutInflater mInflater;
+    private LayoutInflater mDropDownInflater;
+
+    /**
+     * Constructor the enables auto-requery.
+     *
+     * @deprecated This option is discouraged, as it results in Cursor queries
+     * being performed on the application's UI thread and thus can cause poor
+     * responsiveness or even Application Not Responding errors.  As an alternative,
+     * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+     *
+     * @param context The context where the ListView associated with this adapter is running
+     * @param layout resource identifier of a layout file that defines the views
+     *            for this list item.  Unless you override them later, this will
+     *            define both the item views and the drop down views.
+     */
+    @Deprecated
+    public ResourceCursorAdapter(Context context, int layout, Cursor c) {
+        super(context, c);
+        mLayout = mDropDownLayout = layout;
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mDropDownInflater = mInflater;
+    }
+
+    /**
+     * Constructor with default behavior as per
+     * {@link CursorAdapter#CursorAdapter(Context, Cursor, boolean)}; it is recommended
+     * you not use this, but instead {@link #ResourceCursorAdapter(Context, int, Cursor, int)}.
+     * When using this constructor, {@link #FLAG_REGISTER_CONTENT_OBSERVER}
+     * will always be set.
+     *
+     * @param context The context where the ListView associated with this adapter is running
+     * @param layout resource identifier of a layout file that defines the views
+     *            for this list item.  Unless you override them later, this will
+     *            define both the item views and the drop down views.
+     * @param c The cursor from which to get the data.
+     * @param autoRequery If true the adapter will call requery() on the
+     *                    cursor whenever it changes so the most recent
+     *                    data is always displayed.  Using true here is discouraged.
+     */
+    public ResourceCursorAdapter(Context context, int layout, Cursor c, boolean autoRequery) {
+        super(context, c, autoRequery);
+        mLayout = mDropDownLayout = layout;
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mDropDownInflater = mInflater;
+    }
+
+    /**
+     * Standard constructor.
+     *
+     * @param context The context where the ListView associated with this adapter is running
+     * @param layout Resource identifier of a layout file that defines the views
+     *            for this list item.  Unless you override them later, this will
+     *            define both the item views and the drop down views.
+     * @param c The cursor from which to get the data.
+     * @param flags Flags used to determine the behavior of the adapter,
+     * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+     */
+    public ResourceCursorAdapter(Context context, int layout, Cursor c, int flags) {
+        super(context, c, flags);
+        mLayout = mDropDownLayout = layout;
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        mDropDownInflater = mInflater;
+    }
+
+    /**
+     * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are
+     * inflated.
+     * <p>
+     * By default, drop-down views are inflated against the theme of the
+     * {@link Context} passed to the adapter's constructor.
+     *
+     * @param theme the theme against which to inflate drop-down views or
+     *              {@code null} to use the theme from the adapter's context
+     * @see #newDropDownView(Context, Cursor, ViewGroup)
+     */
+    @Override
+    public void setDropDownViewTheme(Resources.Theme theme) {
+        super.setDropDownViewTheme(theme);
+
+        if (theme == null) {
+            mDropDownInflater = null;
+        } else if (theme == mInflater.getContext().getTheme()) {
+            mDropDownInflater = mInflater;
+        } else {
+            final Context context = new ContextThemeWrapper(mContext, theme);
+            mDropDownInflater = LayoutInflater.from(context);
+        }
+    }
+
+    /**
+     * Inflates view(s) from the specified XML file.
+     *
+     * @see android.widget.CursorAdapter#newView(android.content.Context,
+     *      android.database.Cursor, ViewGroup)
+     */
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        return mInflater.inflate(mLayout, parent, false);
+    }
+
+    @Override
+    public View newDropDownView(Context context, Cursor cursor, ViewGroup parent) {
+        return mDropDownInflater.inflate(mDropDownLayout, parent, false);
+    }
+
+    /**
+     * <p>Sets the layout resource of the item views.</p>
+     *
+     * @param layout the layout resources used to create item views
+     */
+    public void setViewResource(int layout) {
+        mLayout = layout;
+    }
+
+    /**
+     * <p>Sets the layout resource of the drop down views.</p>
+     *
+     * @param dropDownLayout the layout resources used to create drop down views
+     */
+    public void setDropDownViewResource(int dropDownLayout) {
+        mDropDownLayout = dropDownLayout;
+    }
+}
diff --git a/android/widget/ResourceCursorTreeAdapter.java b/android/widget/ResourceCursorTreeAdapter.java
new file mode 100644
index 0000000..0894dba
--- /dev/null
+++ b/android/widget/ResourceCursorTreeAdapter.java
@@ -0,0 +1,109 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * A fairly simple ExpandableListAdapter that creates views defined in an XML
+ * file. You can specify the XML file that defines the appearance of the views.
+ */
+public abstract class ResourceCursorTreeAdapter extends CursorTreeAdapter {
+    private int mCollapsedGroupLayout;
+    private int mExpandedGroupLayout;
+    private int mChildLayout;
+    private int mLastChildLayout;
+    private LayoutInflater mInflater;
+
+    /**
+     * Constructor.
+     *
+     * @param context The context where the ListView associated with this
+     *            SimpleListItemFactory is running
+     * @param cursor The database cursor
+     * @param collapsedGroupLayout resource identifier of a layout file that
+     *            defines the views for collapsed groups.
+     * @param expandedGroupLayout resource identifier of a layout file that
+     *            defines the views for expanded groups.
+     * @param childLayout resource identifier of a layout file that defines the
+     *            views for all children but the last..
+     * @param lastChildLayout resource identifier of a layout file that defines
+     *            the views for the last child of a group.
+     */
+    public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+            int expandedGroupLayout, int childLayout, int lastChildLayout) {
+        super(cursor, context);
+
+        mCollapsedGroupLayout = collapsedGroupLayout;
+        mExpandedGroupLayout = expandedGroupLayout;
+        mChildLayout = childLayout;
+        mLastChildLayout = lastChildLayout;
+
+        mInflater = (LayoutInflater)context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context The context where the ListView associated with this
+     *            SimpleListItemFactory is running
+     * @param cursor The database cursor
+     * @param collapsedGroupLayout resource identifier of a layout file that
+     *            defines the views for collapsed groups.
+     * @param expandedGroupLayout resource identifier of a layout file that
+     *            defines the views for expanded groups.
+     * @param childLayout resource identifier of a layout file that defines the
+     *            views for all children.
+     */
+    public ResourceCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+            int expandedGroupLayout, int childLayout) {
+        this(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout, childLayout);
+    }
+
+    /**
+     * Constructor.
+     *
+     * @param context The context where the ListView associated with this
+     *            SimpleListItemFactory is running
+     * @param cursor The database cursor
+     * @param groupLayout resource identifier of a layout file that defines the
+     *            views for all groups.
+     * @param childLayout resource identifier of a layout file that defines the
+     *            views for all children.
+     */
+    public ResourceCursorTreeAdapter(Context context, Cursor cursor, int groupLayout,
+            int childLayout) {
+        this(context, cursor, groupLayout, groupLayout, childLayout, childLayout);
+    }
+
+    @Override
+    public View newChildView(Context context, Cursor cursor, boolean isLastChild,
+            ViewGroup parent) {
+        return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false);
+    }
+
+    @Override
+    public View newGroupView(Context context, Cursor cursor, boolean isExpanded, ViewGroup parent) {
+        return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout,
+                parent, false);
+    }
+
+}
diff --git a/android/widget/RtlSpacingHelper.java b/android/widget/RtlSpacingHelper.java
new file mode 100644
index 0000000..f6b116f
--- /dev/null
+++ b/android/widget/RtlSpacingHelper.java
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+/**
+ * RtlSpacingHelper manages the relationship between left/right and start/end for views
+ * that need to maintain both absolute and relative settings for a form of spacing similar
+ * to view padding.
+ */
+class RtlSpacingHelper {
+    public static final int UNDEFINED = Integer.MIN_VALUE;
+
+    private int mLeft = 0;
+    private int mRight = 0;
+    private int mStart = UNDEFINED;
+    private int mEnd = UNDEFINED;
+    private int mExplicitLeft = 0;
+    private int mExplicitRight = 0;
+
+    private boolean mIsRtl = false;
+    private boolean mIsRelative = false;
+
+    public int getLeft() {
+        return mLeft;
+    }
+
+    public int getRight() {
+        return mRight;
+    }
+
+    public int getStart() {
+        return mIsRtl ? mRight : mLeft;
+    }
+
+    public int getEnd() {
+        return mIsRtl ? mLeft : mRight;
+    }
+
+    public void setRelative(int start, int end) {
+        mStart = start;
+        mEnd = end;
+        mIsRelative = true;
+        if (mIsRtl) {
+            if (end != UNDEFINED) mLeft = end;
+            if (start != UNDEFINED) mRight = start;
+        } else {
+            if (start != UNDEFINED) mLeft = start;
+            if (end != UNDEFINED) mRight = end;
+        }
+    }
+
+    public void setAbsolute(int left, int right) {
+        mIsRelative = false;
+        if (left != UNDEFINED) mLeft = mExplicitLeft = left;
+        if (right != UNDEFINED) mRight = mExplicitRight = right;
+    }
+
+    public void setDirection(boolean isRtl) {
+        if (isRtl == mIsRtl) {
+            return;
+        }
+        mIsRtl = isRtl;
+        if (mIsRelative) {
+            if (isRtl) {
+                mLeft = mEnd != UNDEFINED ? mEnd : mExplicitLeft;
+                mRight = mStart != UNDEFINED ? mStart : mExplicitRight;
+            } else {
+                mLeft = mStart != UNDEFINED ? mStart : mExplicitLeft;
+                mRight = mEnd != UNDEFINED ? mEnd : mExplicitRight;
+            }
+        } else {
+            mLeft = mExplicitLeft;
+            mRight = mExplicitRight;
+        }
+    }
+}
diff --git a/android/widget/ScrollBarDrawable.java b/android/widget/ScrollBarDrawable.java
new file mode 100644
index 0000000..2ae38c9
--- /dev/null
+++ b/android/widget/ScrollBarDrawable.java
@@ -0,0 +1,387 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.NonNull;
+import android.graphics.Canvas;
+import android.graphics.ColorFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+
+import com.android.internal.widget.ScrollBarUtils;
+
+/**
+ * This is only used by View for displaying its scroll bars. It should probably
+ * be moved in to the view package since it is used in that lower-level layer.
+ * For now, we'll hide it so it can be cleaned up later.
+ *
+ * {@hide}
+ */
+public class ScrollBarDrawable extends Drawable implements Drawable.Callback {
+    private Drawable mVerticalTrack;
+    private Drawable mHorizontalTrack;
+    private Drawable mVerticalThumb;
+    private Drawable mHorizontalThumb;
+
+    private int mRange;
+    private int mOffset;
+    private int mExtent;
+
+    private boolean mVertical;
+    private boolean mBoundsChanged;
+    private boolean mRangeChanged;
+    private boolean mAlwaysDrawHorizontalTrack;
+    private boolean mAlwaysDrawVerticalTrack;
+    private boolean mMutated;
+
+    private int mAlpha = 255;
+    private boolean mHasSetAlpha;
+
+    private ColorFilter mColorFilter;
+    private boolean mHasSetColorFilter;
+
+    /**
+     * Indicate whether the horizontal scrollbar track should always be drawn
+     * regardless of the extent. Defaults to false.
+     *
+     * @param alwaysDrawTrack Whether the track should always be drawn
+     *
+     * @see #getAlwaysDrawHorizontalTrack()
+     */
+    public void setAlwaysDrawHorizontalTrack(boolean alwaysDrawTrack) {
+        mAlwaysDrawHorizontalTrack = alwaysDrawTrack;
+    }
+
+    /**
+     * Indicate whether the vertical scrollbar track should always be drawn
+     * regardless of the extent. Defaults to false.
+     *
+     * @param alwaysDrawTrack Whether the track should always be drawn
+     *
+     * @see #getAlwaysDrawVerticalTrack()
+     */
+    public void setAlwaysDrawVerticalTrack(boolean alwaysDrawTrack) {
+        mAlwaysDrawVerticalTrack = alwaysDrawTrack;
+    }
+
+    /**
+     * @return whether the vertical scrollbar track should always be drawn
+     *         regardless of the extent.
+     *
+     * @see #setAlwaysDrawVerticalTrack(boolean)
+     */
+    public boolean getAlwaysDrawVerticalTrack() {
+        return mAlwaysDrawVerticalTrack;
+    }
+
+    /**
+     * @return whether the horizontal scrollbar track should always be drawn
+     *         regardless of the extent.
+     *
+     * @see #setAlwaysDrawHorizontalTrack(boolean)
+     */
+    public boolean getAlwaysDrawHorizontalTrack() {
+        return mAlwaysDrawHorizontalTrack;
+    }
+
+    public void setParameters(int range, int offset, int extent, boolean vertical) {
+        if (mVertical != vertical) {
+            mVertical = vertical;
+
+            mBoundsChanged = true;
+        }
+
+        if (mRange != range || mOffset != offset || mExtent != extent) {
+            mRange = range;
+            mOffset = offset;
+            mExtent = extent;
+
+            mRangeChanged = true;
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        final boolean vertical = mVertical;
+        final int extent = mExtent;
+        final int range = mRange;
+
+        boolean drawTrack = true;
+        boolean drawThumb = true;
+        if (extent <= 0 || range <= extent) {
+            drawTrack = vertical ? mAlwaysDrawVerticalTrack : mAlwaysDrawHorizontalTrack;
+            drawThumb = false;
+        }
+
+        final Rect r = getBounds();
+        if (canvas.quickReject(r.left, r.top, r.right, r.bottom, Canvas.EdgeType.AA)) {
+            return;
+        }
+
+        if (drawTrack) {
+            drawTrack(canvas, r, vertical);
+        }
+
+        if (drawThumb) {
+            final int scrollBarLength = vertical ? r.height() : r.width();
+            final int thickness = vertical ? r.width() : r.height();
+            final int thumbLength =
+                    ScrollBarUtils.getThumbLength(scrollBarLength, thickness, extent, range);
+            final int thumbOffset =
+                    ScrollBarUtils.getThumbOffset(scrollBarLength, thumbLength, extent, range,
+                            mOffset);
+
+            drawThumb(canvas, r, thumbOffset, thumbLength, vertical);
+        }
+    }
+
+    @Override
+    protected void onBoundsChange(Rect bounds) {
+        super.onBoundsChange(bounds);
+        mBoundsChanged = true;
+    }
+
+    @Override
+    public boolean isStateful() {
+        return (mVerticalTrack != null && mVerticalTrack.isStateful())
+                || (mVerticalThumb != null && mVerticalThumb.isStateful())
+                || (mHorizontalTrack != null && mHorizontalTrack.isStateful())
+                || (mHorizontalThumb != null && mHorizontalThumb.isStateful())
+                || super.isStateful();
+    }
+
+    @Override
+    protected boolean onStateChange(int[] state) {
+        boolean changed = super.onStateChange(state);
+        if (mVerticalTrack != null) {
+            changed |= mVerticalTrack.setState(state);
+        }
+        if (mVerticalThumb != null) {
+            changed |= mVerticalThumb.setState(state);
+        }
+        if (mHorizontalTrack != null) {
+            changed |= mHorizontalTrack.setState(state);
+        }
+        if (mHorizontalThumb != null) {
+            changed |= mHorizontalThumb.setState(state);
+        }
+        return changed;
+    }
+
+    private void drawTrack(Canvas canvas, Rect bounds, boolean vertical) {
+        final Drawable track;
+        if (vertical) {
+            track = mVerticalTrack;
+        } else {
+            track = mHorizontalTrack;
+        }
+
+        if (track != null) {
+            if (mBoundsChanged) {
+                track.setBounds(bounds);
+            }
+            track.draw(canvas);
+        }
+    }
+
+    private void drawThumb(Canvas canvas, Rect bounds, int offset, int length, boolean vertical) {
+        final boolean changed = mRangeChanged || mBoundsChanged;
+        if (vertical) {
+            if (mVerticalThumb != null) {
+                final Drawable thumb = mVerticalThumb;
+                if (changed) {
+                    thumb.setBounds(bounds.left, bounds.top + offset,
+                            bounds.right, bounds.top + offset + length);
+                }
+
+                thumb.draw(canvas);
+            }
+        } else {
+            if (mHorizontalThumb != null) {
+                final Drawable thumb = mHorizontalThumb;
+                if (changed) {
+                    thumb.setBounds(bounds.left + offset, bounds.top,
+                            bounds.left + offset + length, bounds.bottom);
+                }
+
+                thumb.draw(canvas);
+            }
+        }
+    }
+
+    public void setVerticalThumbDrawable(Drawable thumb) {
+        if (mVerticalThumb != null) {
+            mVerticalThumb.setCallback(null);
+        }
+
+        propagateCurrentState(thumb);
+        mVerticalThumb = thumb;
+    }
+
+    public void setVerticalTrackDrawable(Drawable track) {
+        if (mVerticalTrack != null) {
+            mVerticalTrack.setCallback(null);
+        }
+
+        propagateCurrentState(track);
+        mVerticalTrack = track;
+    }
+
+    public void setHorizontalThumbDrawable(Drawable thumb) {
+        if (mHorizontalThumb != null) {
+            mHorizontalThumb.setCallback(null);
+        }
+
+        propagateCurrentState(thumb);
+        mHorizontalThumb = thumb;
+    }
+
+    public void setHorizontalTrackDrawable(Drawable track) {
+        if (mHorizontalTrack != null) {
+            mHorizontalTrack.setCallback(null);
+        }
+
+        propagateCurrentState(track);
+        mHorizontalTrack = track;
+    }
+
+    private void propagateCurrentState(Drawable d) {
+        if (d != null) {
+            if (mMutated) {
+                d.mutate();
+            }
+
+            d.setState(getState());
+            d.setCallback(this);
+
+            if (mHasSetAlpha) {
+                d.setAlpha(mAlpha);
+            }
+
+            if (mHasSetColorFilter) {
+                d.setColorFilter(mColorFilter);
+            }
+        }
+    }
+
+    public int getSize(boolean vertical) {
+        if (vertical) {
+            return mVerticalTrack != null ? mVerticalTrack.getIntrinsicWidth() :
+                    mVerticalThumb != null ? mVerticalThumb.getIntrinsicWidth() : 0;
+        } else {
+            return mHorizontalTrack != null ? mHorizontalTrack.getIntrinsicHeight() :
+                    mHorizontalThumb != null ? mHorizontalThumb.getIntrinsicHeight() : 0;
+        }
+    }
+
+    @Override
+    public ScrollBarDrawable mutate() {
+        if (!mMutated && super.mutate() == this) {
+            if (mVerticalTrack != null) {
+                mVerticalTrack.mutate();
+            }
+            if (mVerticalThumb != null) {
+                mVerticalThumb.mutate();
+            }
+            if (mHorizontalTrack != null) {
+                mHorizontalTrack.mutate();
+            }
+            if (mHorizontalThumb != null) {
+                mHorizontalThumb.mutate();
+            }
+            mMutated = true;
+        }
+        return this;
+    }
+
+    @Override
+    public void setAlpha(int alpha) {
+        mAlpha = alpha;
+        mHasSetAlpha = true;
+
+        if (mVerticalTrack != null) {
+            mVerticalTrack.setAlpha(alpha);
+        }
+        if (mVerticalThumb != null) {
+            mVerticalThumb.setAlpha(alpha);
+        }
+        if (mHorizontalTrack != null) {
+            mHorizontalTrack.setAlpha(alpha);
+        }
+        if (mHorizontalThumb != null) {
+            mHorizontalThumb.setAlpha(alpha);
+        }
+    }
+
+    @Override
+    public int getAlpha() {
+        return mAlpha;
+    }
+
+    @Override
+    public void setColorFilter(ColorFilter colorFilter) {
+        mColorFilter = colorFilter;
+        mHasSetColorFilter = true;
+
+        if (mVerticalTrack != null) {
+            mVerticalTrack.setColorFilter(colorFilter);
+        }
+        if (mVerticalThumb != null) {
+            mVerticalThumb.setColorFilter(colorFilter);
+        }
+        if (mHorizontalTrack != null) {
+            mHorizontalTrack.setColorFilter(colorFilter);
+        }
+        if (mHorizontalThumb != null) {
+            mHorizontalThumb.setColorFilter(colorFilter);
+        }
+    }
+
+    @Override
+    public ColorFilter getColorFilter() {
+        return mColorFilter;
+    }
+
+    @Override
+    public int getOpacity() {
+        return PixelFormat.TRANSLUCENT;
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable who) {
+        invalidateSelf();
+    }
+
+    @Override
+    public void scheduleDrawable(@NonNull Drawable who, @NonNull Runnable what, long when) {
+        scheduleSelf(what, when);
+    }
+
+    @Override
+    public void unscheduleDrawable(@NonNull Drawable who, @NonNull Runnable what) {
+        unscheduleSelf(what);
+    }
+
+    @Override
+    public String toString() {
+        return "ScrollBarDrawable: range=" + mRange + " offset=" + mOffset +
+               " extent=" + mExtent + (mVertical ? " V" : " H");
+    }
+}
+
+
diff --git a/android/widget/ScrollView.java b/android/widget/ScrollView.java
new file mode 100644
index 0000000..97d32f1
--- /dev/null
+++ b/android/widget/ScrollView.java
@@ -0,0 +1,1897 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.Build;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.StrictMode;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.FocusFinder;
+import android.view.InputDevice;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+
+import com.android.internal.R;
+
+import java.util.List;
+
+/**
+ * A view group that allows the view hierarchy placed within it to be scrolled.
+ * Scroll view may have only one direct child placed within it.
+ * To add multiple views within the scroll view, make
+ * the direct child you add a view group, for example {@link LinearLayout}, and
+ * place additional views within that LinearLayout.
+ *
+ * <p>Scroll view supports vertical scrolling only. For horizontal scrolling,
+ * use {@link HorizontalScrollView} instead.</p>
+ *
+ * <p>Never add a {@link android.support.v7.widget.RecyclerView} or {@link ListView} to
+ * a scroll view. Doing so results in poor user interface performance and a poor user
+ * experience.</p>
+ *
+ * <p class="note">
+ * For vertical scrolling, consider {@link android.support.v4.widget.NestedScrollView}
+ * instead of scroll view which offers greater user interface flexibility and
+ * support for the material design scrolling patterns.</p>
+ *
+ * <p>To learn more about material design patterns for handling scrolling, see
+ * <a href="https://material.io/guidelines/patterns/scrolling-techniques.html#">
+ * Scrolling techniques</a>.</p>
+ *
+ * @attr ref android.R.styleable#ScrollView_fillViewport
+ */
+public class ScrollView extends FrameLayout {
+    static final int ANIMATED_SCROLL_GAP = 250;
+
+    static final float MAX_SCROLL_FACTOR = 0.5f;
+
+    private static final String TAG = "ScrollView";
+
+    private long mLastScroll;
+
+    private final Rect mTempRect = new Rect();
+    private OverScroller mScroller;
+    private EdgeEffect mEdgeGlowTop;
+    private EdgeEffect mEdgeGlowBottom;
+
+    /**
+     * Position of the last motion event.
+     */
+    private int mLastMotionY;
+
+    /**
+     * True when the layout has changed but the traversal has not come through yet.
+     * Ideally the view hierarchy would keep track of this for us.
+     */
+    private boolean mIsLayoutDirty = true;
+
+    /**
+     * The child to give focus to in the event that a child has requested focus while the
+     * layout is dirty. This prevents the scroll from being wrong if the child has not been
+     * laid out before requesting focus.
+     */
+    private View mChildToScrollTo = null;
+
+    /**
+     * True if the user is currently dragging this ScrollView around. This is
+     * not the same as 'is being flinged', which can be checked by
+     * mScroller.isFinished() (flinging begins when the user lifts his finger).
+     */
+    private boolean mIsBeingDragged = false;
+
+    /**
+     * Determines speed during touch scrolling
+     */
+    private VelocityTracker mVelocityTracker;
+
+    /**
+     * When set to true, the scroll view measure its child to make it fill the currently
+     * visible area.
+     */
+    @ViewDebug.ExportedProperty(category = "layout")
+    private boolean mFillViewport;
+
+    /**
+     * Whether arrow scrolling is animated.
+     */
+    private boolean mSmoothScrollingEnabled = true;
+
+    private int mTouchSlop;
+    private int mMinimumVelocity;
+    private int mMaximumVelocity;
+
+    private int mOverscrollDistance;
+    private int mOverflingDistance;
+
+    private float mVerticalScrollFactor;
+
+    /**
+     * ID of the active pointer. This is used to retain consistency during
+     * drags/flings if multiple pointers are used.
+     */
+    private int mActivePointerId = INVALID_POINTER;
+
+    /**
+     * Used during scrolling to retrieve the new offset within the window.
+     */
+    private final int[] mScrollOffset = new int[2];
+    private final int[] mScrollConsumed = new int[2];
+    private int mNestedYOffset;
+
+    /**
+     * The StrictMode "critical time span" objects to catch animation
+     * stutters.  Non-null when a time-sensitive animation is
+     * in-flight.  Must call finish() on them when done animating.
+     * These are no-ops on user builds.
+     */
+    private StrictMode.Span mScrollStrictSpan = null;  // aka "drag"
+    private StrictMode.Span mFlingStrictSpan = null;
+
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    private SavedState mSavedState;
+
+    public ScrollView(Context context) {
+        this(context, null);
+    }
+
+    public ScrollView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.scrollViewStyle);
+    }
+
+    public ScrollView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ScrollView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        initScrollView();
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.ScrollView, defStyleAttr, defStyleRes);
+
+        setFillViewport(a.getBoolean(R.styleable.ScrollView_fillViewport, false));
+
+        a.recycle();
+
+        if (context.getResources().getConfiguration().uiMode == Configuration.UI_MODE_TYPE_WATCH) {
+            setRevealOnFocusHint(false);
+        }
+    }
+
+    @Override
+    public boolean shouldDelayChildPressedState() {
+        return true;
+    }
+
+    @Override
+    protected float getTopFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+
+        final int length = getVerticalFadingEdgeLength();
+        if (mScrollY < length) {
+            return mScrollY / (float) length;
+        }
+
+        return 1.0f;
+    }
+
+    @Override
+    protected float getBottomFadingEdgeStrength() {
+        if (getChildCount() == 0) {
+            return 0.0f;
+        }
+
+        final int length = getVerticalFadingEdgeLength();
+        final int bottomEdge = getHeight() - mPaddingBottom;
+        final int span = getChildAt(0).getBottom() - mScrollY - bottomEdge;
+        if (span < length) {
+            return span / (float) length;
+        }
+
+        return 1.0f;
+    }
+
+    /**
+     * @return The maximum amount this scroll view will scroll in response to
+     *   an arrow event.
+     */
+    public int getMaxScrollAmount() {
+        return (int) (MAX_SCROLL_FACTOR * (mBottom - mTop));
+    }
+
+
+    private void initScrollView() {
+        mScroller = new OverScroller(getContext());
+        setFocusable(true);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+        setWillNotDraw(false);
+        final ViewConfiguration configuration = ViewConfiguration.get(mContext);
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMinimumVelocity = configuration.getScaledMinimumFlingVelocity();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mOverscrollDistance = configuration.getScaledOverscrollDistance();
+        mOverflingDistance = configuration.getScaledOverflingDistance();
+        mVerticalScrollFactor = configuration.getScaledVerticalScrollFactor();
+    }
+
+    @Override
+    public void addView(View child) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child);
+    }
+
+    @Override
+    public void addView(View child, int index) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, index);
+    }
+
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, params);
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (getChildCount() > 0) {
+            throw new IllegalStateException("ScrollView can host only one direct child");
+        }
+
+        super.addView(child, index, params);
+    }
+
+    /**
+     * @return Returns true this ScrollView can be scrolled
+     */
+    private boolean canScroll() {
+        View child = getChildAt(0);
+        if (child != null) {
+            int childHeight = child.getHeight();
+            return getHeight() < childHeight + mPaddingTop + mPaddingBottom;
+        }
+        return false;
+    }
+
+    /**
+     * Indicates whether this ScrollView's content is stretched to fill the viewport.
+     *
+     * @return True if the content fills the viewport, false otherwise.
+     *
+     * @attr ref android.R.styleable#ScrollView_fillViewport
+     */
+    public boolean isFillViewport() {
+        return mFillViewport;
+    }
+
+    /**
+     * Indicates this ScrollView whether it should stretch its content height to fill
+     * the viewport or not.
+     *
+     * @param fillViewport True to stretch the content's height to the viewport's
+     *        boundaries, false otherwise.
+     *
+     * @attr ref android.R.styleable#ScrollView_fillViewport
+     */
+    public void setFillViewport(boolean fillViewport) {
+        if (fillViewport != mFillViewport) {
+            mFillViewport = fillViewport;
+            requestLayout();
+        }
+    }
+
+    /**
+     * @return Whether arrow scrolling will animate its transition.
+     */
+    public boolean isSmoothScrollingEnabled() {
+        return mSmoothScrollingEnabled;
+    }
+
+    /**
+     * Set whether arrow scrolling will animate its transition.
+     * @param smoothScrollingEnabled whether arrow scrolling will animate its transition
+     */
+    public void setSmoothScrollingEnabled(boolean smoothScrollingEnabled) {
+        mSmoothScrollingEnabled = smoothScrollingEnabled;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        if (!mFillViewport) {
+            return;
+        }
+
+        final int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        if (heightMode == MeasureSpec.UNSPECIFIED) {
+            return;
+        }
+
+        if (getChildCount() > 0) {
+            final View child = getChildAt(0);
+            final int widthPadding;
+            final int heightPadding;
+            final int targetSdkVersion = getContext().getApplicationInfo().targetSdkVersion;
+            final FrameLayout.LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (targetSdkVersion >= VERSION_CODES.M) {
+                widthPadding = mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin;
+                heightPadding = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin;
+            } else {
+                widthPadding = mPaddingLeft + mPaddingRight;
+                heightPadding = mPaddingTop + mPaddingBottom;
+            }
+
+            final int desiredHeight = getMeasuredHeight() - heightPadding;
+            if (child.getMeasuredHeight() < desiredHeight) {
+                final int childWidthMeasureSpec = getChildMeasureSpec(
+                        widthMeasureSpec, widthPadding, lp.width);
+                final int childHeightMeasureSpec = MeasureSpec.makeMeasureSpec(
+                        desiredHeight, MeasureSpec.EXACTLY);
+                child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+            }
+        }
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        // Let the focused view and/or our descendants get the key first
+        return super.dispatchKeyEvent(event) || executeKeyEvent(event);
+    }
+
+    /**
+     * You can call this function yourself to have the scroll view perform
+     * scrolling from a key event, just as if the event had been dispatched to
+     * it by the view hierarchy.
+     *
+     * @param event The key event to execute.
+     * @return Return true if the event was handled, else false.
+     */
+    public boolean executeKeyEvent(KeyEvent event) {
+        mTempRect.setEmpty();
+
+        if (!canScroll()) {
+            if (isFocused() && event.getKeyCode() != KeyEvent.KEYCODE_BACK) {
+                View currentFocused = findFocus();
+                if (currentFocused == this) currentFocused = null;
+                View nextFocused = FocusFinder.getInstance().findNextFocus(this,
+                        currentFocused, View.FOCUS_DOWN);
+                return nextFocused != null
+                        && nextFocused != this
+                        && nextFocused.requestFocus(View.FOCUS_DOWN);
+            }
+            return false;
+        }
+
+        boolean handled = false;
+        if (event.getAction() == KeyEvent.ACTION_DOWN) {
+            switch (event.getKeyCode()) {
+                case KeyEvent.KEYCODE_DPAD_UP:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_UP);
+                    } else {
+                        handled = fullScroll(View.FOCUS_UP);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_DPAD_DOWN:
+                    if (!event.isAltPressed()) {
+                        handled = arrowScroll(View.FOCUS_DOWN);
+                    } else {
+                        handled = fullScroll(View.FOCUS_DOWN);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_SPACE:
+                    pageScroll(event.isShiftPressed() ? View.FOCUS_UP : View.FOCUS_DOWN);
+                    break;
+            }
+        }
+
+        return handled;
+    }
+
+    private boolean inChild(int x, int y) {
+        if (getChildCount() > 0) {
+            final int scrollY = mScrollY;
+            final View child = getChildAt(0);
+            return !(y < child.getTop() - scrollY
+                    || y >= child.getBottom() - scrollY
+                    || x < child.getLeft()
+                    || x >= child.getRight());
+        }
+        return false;
+    }
+
+    private void initOrResetVelocityTracker() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        } else {
+            mVelocityTracker.clear();
+        }
+    }
+
+    private void initVelocityTrackerIfNotExists() {
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+    }
+
+    private void recycleVelocityTracker() {
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    @Override
+    public void requestDisallowInterceptTouchEvent(boolean disallowIntercept) {
+        if (disallowIntercept) {
+            recycleVelocityTracker();
+        }
+        super.requestDisallowInterceptTouchEvent(disallowIntercept);
+    }
+
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        /*
+         * This method JUST determines whether we want to intercept the motion.
+         * If we return true, onMotionEvent will be called and we do the actual
+         * scrolling there.
+         */
+
+        /*
+        * Shortcut the most recurring case: the user is in the dragging
+        * state and he is moving his finger.  We want to intercept this
+        * motion.
+        */
+        final int action = ev.getAction();
+        if ((action == MotionEvent.ACTION_MOVE) && (mIsBeingDragged)) {
+            return true;
+        }
+
+        if (super.onInterceptTouchEvent(ev)) {
+            return true;
+        }
+
+        /*
+         * Don't try to intercept touch if we can't scroll anyway.
+         */
+        if (getScrollY() == 0 && !canScrollVertically(1)) {
+            return false;
+        }
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                /*
+                 * mIsBeingDragged == false, otherwise the shortcut would have caught it. Check
+                 * whether the user has moved far enough from his original down touch.
+                 */
+
+                /*
+                * Locally do absolute value. mLastMotionY is set to the y value
+                * of the down event.
+                */
+                final int activePointerId = mActivePointerId;
+                if (activePointerId == INVALID_POINTER) {
+                    // If we don't have a valid id, the touch down wasn't on content.
+                    break;
+                }
+
+                final int pointerIndex = ev.findPointerIndex(activePointerId);
+                if (pointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + activePointerId
+                            + " in onInterceptTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(pointerIndex);
+                final int yDiff = Math.abs(y - mLastMotionY);
+                if (yDiff > mTouchSlop && (getNestedScrollAxes() & SCROLL_AXIS_VERTICAL) == 0) {
+                    mIsBeingDragged = true;
+                    mLastMotionY = y;
+                    initVelocityTrackerIfNotExists();
+                    mVelocityTracker.addMovement(ev);
+                    mNestedYOffset = 0;
+                    if (mScrollStrictSpan == null) {
+                        mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
+                    }
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_DOWN: {
+                final int y = (int) ev.getY();
+                if (!inChild((int) ev.getX(), (int) y)) {
+                    mIsBeingDragged = false;
+                    recycleVelocityTracker();
+                    break;
+                }
+
+                /*
+                 * Remember location of down touch.
+                 * ACTION_DOWN always refers to pointer index 0.
+                 */
+                mLastMotionY = y;
+                mActivePointerId = ev.getPointerId(0);
+
+                initOrResetVelocityTracker();
+                mVelocityTracker.addMovement(ev);
+                /*
+                 * If being flinged and user touches the screen, initiate drag;
+                 * otherwise don't. mScroller.isFinished should be false when
+                 * being flinged. We need to call computeScrollOffset() first so that
+                 * isFinished() is correct.
+                */
+                mScroller.computeScrollOffset();
+                mIsBeingDragged = !mScroller.isFinished();
+                if (mIsBeingDragged && mScrollStrictSpan == null) {
+                    mScrollStrictSpan = StrictMode.enterCriticalSpan("ScrollView-scroll");
+                }
+                startNestedScroll(SCROLL_AXIS_VERTICAL);
+                break;
+            }
+
+            case MotionEvent.ACTION_CANCEL:
+            case MotionEvent.ACTION_UP:
+                /* Release the drag */
+                mIsBeingDragged = false;
+                mActivePointerId = INVALID_POINTER;
+                recycleVelocityTracker();
+                if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
+                    postInvalidateOnAnimation();
+                }
+                stopNestedScroll();
+                break;
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                break;
+        }
+
+        /*
+        * The only time we want to intercept motion events is if we are in the
+        * drag mode.
+        */
+        return mIsBeingDragged;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        initVelocityTrackerIfNotExists();
+
+        MotionEvent vtev = MotionEvent.obtain(ev);
+
+        final int actionMasked = ev.getActionMasked();
+
+        if (actionMasked == MotionEvent.ACTION_DOWN) {
+            mNestedYOffset = 0;
+        }
+        vtev.offsetLocation(0, mNestedYOffset);
+
+        switch (actionMasked) {
+            case MotionEvent.ACTION_DOWN: {
+                if (getChildCount() == 0) {
+                    return false;
+                }
+                if ((mIsBeingDragged = !mScroller.isFinished())) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                }
+
+                /*
+                 * If being flinged and user touches, stop the fling. isFinished
+                 * will be false if being flinged.
+                 */
+                if (!mScroller.isFinished()) {
+                    mScroller.abortAnimation();
+                    if (mFlingStrictSpan != null) {
+                        mFlingStrictSpan.finish();
+                        mFlingStrictSpan = null;
+                    }
+                }
+
+                // Remember where the motion event started
+                mLastMotionY = (int) ev.getY();
+                mActivePointerId = ev.getPointerId(0);
+                startNestedScroll(SCROLL_AXIS_VERTICAL);
+                break;
+            }
+            case MotionEvent.ACTION_MOVE:
+                final int activePointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (activePointerIndex == -1) {
+                    Log.e(TAG, "Invalid pointerId=" + mActivePointerId + " in onTouchEvent");
+                    break;
+                }
+
+                final int y = (int) ev.getY(activePointerIndex);
+                int deltaY = mLastMotionY - y;
+                if (dispatchNestedPreScroll(0, deltaY, mScrollConsumed, mScrollOffset)) {
+                    deltaY -= mScrollConsumed[1];
+                    vtev.offsetLocation(0, mScrollOffset[1]);
+                    mNestedYOffset += mScrollOffset[1];
+                }
+                if (!mIsBeingDragged && Math.abs(deltaY) > mTouchSlop) {
+                    final ViewParent parent = getParent();
+                    if (parent != null) {
+                        parent.requestDisallowInterceptTouchEvent(true);
+                    }
+                    mIsBeingDragged = true;
+                    if (deltaY > 0) {
+                        deltaY -= mTouchSlop;
+                    } else {
+                        deltaY += mTouchSlop;
+                    }
+                }
+                if (mIsBeingDragged) {
+                    // Scroll to follow the motion event
+                    mLastMotionY = y - mScrollOffset[1];
+
+                    final int oldY = mScrollY;
+                    final int range = getScrollRange();
+                    final int overscrollMode = getOverScrollMode();
+                    boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                            (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                    // Calling overScrollBy will call onOverScrolled, which
+                    // calls onScrollChanged if applicable.
+                    if (overScrollBy(0, deltaY, 0, mScrollY, 0, range, 0, mOverscrollDistance, true)
+                            && !hasNestedScrollingParent()) {
+                        // Break our velocity if we hit a scroll barrier.
+                        mVelocityTracker.clear();
+                    }
+
+                    final int scrolledDeltaY = mScrollY - oldY;
+                    final int unconsumedY = deltaY - scrolledDeltaY;
+                    if (dispatchNestedScroll(0, scrolledDeltaY, 0, unconsumedY, mScrollOffset)) {
+                        mLastMotionY -= mScrollOffset[1];
+                        vtev.offsetLocation(0, mScrollOffset[1]);
+                        mNestedYOffset += mScrollOffset[1];
+                    } else if (canOverscroll) {
+                        final int pulledToY = oldY + deltaY;
+                        if (pulledToY < 0) {
+                            mEdgeGlowTop.onPull((float) deltaY / getHeight(),
+                                    ev.getX(activePointerIndex) / getWidth());
+                            if (!mEdgeGlowBottom.isFinished()) {
+                                mEdgeGlowBottom.onRelease();
+                            }
+                        } else if (pulledToY > range) {
+                            mEdgeGlowBottom.onPull((float) deltaY / getHeight(),
+                                    1.f - ev.getX(activePointerIndex) / getWidth());
+                            if (!mEdgeGlowTop.isFinished()) {
+                                mEdgeGlowTop.onRelease();
+                            }
+                        }
+                        if (mEdgeGlowTop != null
+                                && (!mEdgeGlowTop.isFinished() || !mEdgeGlowBottom.isFinished())) {
+                            postInvalidateOnAnimation();
+                        }
+                    }
+                }
+                break;
+            case MotionEvent.ACTION_UP:
+                if (mIsBeingDragged) {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+                    int initialVelocity = (int) velocityTracker.getYVelocity(mActivePointerId);
+
+                    if ((Math.abs(initialVelocity) > mMinimumVelocity)) {
+                        flingWithNestedDispatch(-initialVelocity);
+                    } else if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0,
+                            getScrollRange())) {
+                        postInvalidateOnAnimation();
+                    }
+
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_CANCEL:
+                if (mIsBeingDragged && getChildCount() > 0) {
+                    if (mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange())) {
+                        postInvalidateOnAnimation();
+                    }
+                    mActivePointerId = INVALID_POINTER;
+                    endDrag();
+                }
+                break;
+            case MotionEvent.ACTION_POINTER_DOWN: {
+                final int index = ev.getActionIndex();
+                mLastMotionY = (int) ev.getY(index);
+                mActivePointerId = ev.getPointerId(index);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP:
+                onSecondaryPointerUp(ev);
+                mLastMotionY = (int) ev.getY(ev.findPointerIndex(mActivePointerId));
+                break;
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.addMovement(vtev);
+        }
+        vtev.recycle();
+        return true;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int pointerIndex = (ev.getAction() & MotionEvent.ACTION_POINTER_INDEX_MASK) >>
+                MotionEvent.ACTION_POINTER_INDEX_SHIFT;
+        final int pointerId = ev.getPointerId(pointerIndex);
+        if (pointerId == mActivePointerId) {
+            // This was our active pointer going up. Choose a new
+            // active pointer and adjust accordingly.
+            // TODO: Make this decision more intelligent.
+            final int newPointerIndex = pointerIndex == 0 ? 1 : 0;
+            mLastMotionY = (int) ev.getY(newPointerIndex);
+            mActivePointerId = ev.getPointerId(newPointerIndex);
+            if (mVelocityTracker != null) {
+                mVelocityTracker.clear();
+            }
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        switch (event.getAction()) {
+            case MotionEvent.ACTION_SCROLL:
+                final float axisValue;
+                if (event.isFromSource(InputDevice.SOURCE_CLASS_POINTER)) {
+                    axisValue = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                } else if (event.isFromSource(InputDevice.SOURCE_ROTARY_ENCODER)) {
+                    axisValue = event.getAxisValue(MotionEvent.AXIS_SCROLL);
+                } else {
+                    axisValue = 0;
+                }
+
+                final int delta = Math.round(axisValue * mVerticalScrollFactor);
+                if (delta != 0) {
+                    final int range = getScrollRange();
+                    int oldScrollY = mScrollY;
+                    int newScrollY = oldScrollY - delta;
+                    if (newScrollY < 0) {
+                        newScrollY = 0;
+                    } else if (newScrollY > range) {
+                        newScrollY = range;
+                    }
+                    if (newScrollY != oldScrollY) {
+                        super.scrollTo(mScrollX, newScrollY);
+                        return true;
+                    }
+                }
+                break;
+        }
+
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
+    protected void onOverScrolled(int scrollX, int scrollY,
+            boolean clampedX, boolean clampedY) {
+        // Treat animating scrolls differently; see #computeScroll() for why.
+        if (!mScroller.isFinished()) {
+            final int oldX = mScrollX;
+            final int oldY = mScrollY;
+            mScrollX = scrollX;
+            mScrollY = scrollY;
+            invalidateParentIfNeeded();
+            onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+            if (clampedY) {
+                mScroller.springBack(mScrollX, mScrollY, 0, 0, 0, getScrollRange());
+            }
+        } else {
+            super.scrollTo(scrollX, scrollY);
+        }
+
+        awakenScrollBars();
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (!isEnabled()) {
+            return false;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD:
+            case R.id.accessibilityActionScrollDown: {
+                final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+                final int targetScrollY = Math.min(mScrollY + viewportHeight, getScrollRange());
+                if (targetScrollY != mScrollY) {
+                    smoothScrollTo(0, targetScrollY);
+                    return true;
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD:
+            case R.id.accessibilityActionScrollUp: {
+                final int viewportHeight = getHeight() - mPaddingBottom - mPaddingTop;
+                final int targetScrollY = Math.max(mScrollY - viewportHeight, 0);
+                if (targetScrollY != mScrollY) {
+                    smoothScrollTo(0, targetScrollY);
+                    return true;
+                }
+            } return false;
+        }
+        return false;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ScrollView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        if (isEnabled()) {
+            final int scrollRange = getScrollRange();
+            if (scrollRange > 0) {
+                info.setScrollable(true);
+                if (mScrollY > 0) {
+                    info.addAction(
+                            AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_BACKWARD);
+                    info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_UP);
+                }
+                if (mScrollY < scrollRange) {
+                    info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_FORWARD);
+                    info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SCROLL_DOWN);
+                }
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        final boolean scrollable = getScrollRange() > 0;
+        event.setScrollable(scrollable);
+        event.setScrollX(mScrollX);
+        event.setScrollY(mScrollY);
+        event.setMaxScrollX(mScrollX);
+        event.setMaxScrollY(getScrollRange());
+    }
+
+    private int getScrollRange() {
+        int scrollRange = 0;
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            scrollRange = Math.max(0,
+                    child.getHeight() - (getHeight() - mPaddingBottom - mPaddingTop));
+        }
+        return scrollRange;
+    }
+
+    /**
+     * <p>
+     * Finds the next focusable component that fits in the specified bounds.
+     * </p>
+     *
+     * @param topFocus look for a candidate is the one at the top of the bounds
+     *                 if topFocus is true, or at the bottom of the bounds if topFocus is
+     *                 false
+     * @param top      the top offset of the bounds in which a focusable must be
+     *                 found
+     * @param bottom   the bottom offset of the bounds in which a focusable must
+     *                 be found
+     * @return the next focusable component in the bounds or null if none can
+     *         be found
+     */
+    private View findFocusableViewInBounds(boolean topFocus, int top, int bottom) {
+
+        List<View> focusables = getFocusables(View.FOCUS_FORWARD);
+        View focusCandidate = null;
+
+        /*
+         * A fully contained focusable is one where its top is below the bound's
+         * top, and its bottom is above the bound's bottom. A partially
+         * contained focusable is one where some part of it is within the
+         * bounds, but it also has some part that is not within bounds.  A fully contained
+         * focusable is preferred to a partially contained focusable.
+         */
+        boolean foundFullyContainedFocusable = false;
+
+        int count = focusables.size();
+        for (int i = 0; i < count; i++) {
+            View view = focusables.get(i);
+            int viewTop = view.getTop();
+            int viewBottom = view.getBottom();
+
+            if (top < viewBottom && viewTop < bottom) {
+                /*
+                 * the focusable is in the target area, it is a candidate for
+                 * focusing
+                 */
+
+                final boolean viewIsFullyContained = (top < viewTop) &&
+                        (viewBottom < bottom);
+
+                if (focusCandidate == null) {
+                    /* No candidate, take this one */
+                    focusCandidate = view;
+                    foundFullyContainedFocusable = viewIsFullyContained;
+                } else {
+                    final boolean viewIsCloserToBoundary =
+                            (topFocus && viewTop < focusCandidate.getTop()) ||
+                                    (!topFocus && viewBottom > focusCandidate
+                                            .getBottom());
+
+                    if (foundFullyContainedFocusable) {
+                        if (viewIsFullyContained && viewIsCloserToBoundary) {
+                            /*
+                             * We're dealing with only fully contained views, so
+                             * it has to be closer to the boundary to beat our
+                             * candidate
+                             */
+                            focusCandidate = view;
+                        }
+                    } else {
+                        if (viewIsFullyContained) {
+                            /* Any fully contained view beats a partially contained view */
+                            focusCandidate = view;
+                            foundFullyContainedFocusable = true;
+                        } else if (viewIsCloserToBoundary) {
+                            /*
+                             * Partially contained view beats another partially
+                             * contained view if it's closer
+                             */
+                            focusCandidate = view;
+                        }
+                    }
+                }
+            }
+        }
+
+        return focusCandidate;
+    }
+
+    /**
+     * <p>Handles scrolling in response to a "page up/down" shortcut press. This
+     * method will scroll the view by one page up or down and give the focus
+     * to the topmost/bottommost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go one page up or
+     *                  {@link android.view.View#FOCUS_DOWN} to go one page down
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean pageScroll(int direction) {
+        boolean down = direction == View.FOCUS_DOWN;
+        int height = getHeight();
+
+        if (down) {
+            mTempRect.top = getScrollY() + height;
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(count - 1);
+                if (mTempRect.top + height > view.getBottom()) {
+                    mTempRect.top = view.getBottom() - height;
+                }
+            }
+        } else {
+            mTempRect.top = getScrollY() - height;
+            if (mTempRect.top < 0) {
+                mTempRect.top = 0;
+            }
+        }
+        mTempRect.bottom = mTempRect.top + height;
+
+        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+    }
+
+    /**
+     * <p>Handles scrolling in response to a "home/end" shortcut press. This
+     * method will scroll the view to the top or bottom and give the focus
+     * to the topmost/bottommost component in the new visible area. If no
+     * component is a good candidate for focus, this scrollview reclaims the
+     * focus.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go the top of the view or
+     *                  {@link android.view.View#FOCUS_DOWN} to go the bottom
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    public boolean fullScroll(int direction) {
+        boolean down = direction == View.FOCUS_DOWN;
+        int height = getHeight();
+
+        mTempRect.top = 0;
+        mTempRect.bottom = height;
+
+        if (down) {
+            int count = getChildCount();
+            if (count > 0) {
+                View view = getChildAt(count - 1);
+                mTempRect.bottom = view.getBottom() + mPaddingBottom;
+                mTempRect.top = mTempRect.bottom - height;
+            }
+        }
+
+        return scrollAndFocus(direction, mTempRect.top, mTempRect.bottom);
+    }
+
+    /**
+     * <p>Scrolls the view to make the area defined by <code>top</code> and
+     * <code>bottom</code> visible. This method attempts to give the focus
+     * to a component visible in this area. If no component can be focused in
+     * the new visible area, the focus is reclaimed by this ScrollView.</p>
+     *
+     * @param direction the scroll direction: {@link android.view.View#FOCUS_UP}
+     *                  to go upward, {@link android.view.View#FOCUS_DOWN} to downward
+     * @param top       the top offset of the new area to be made visible
+     * @param bottom    the bottom offset of the new area to be made visible
+     * @return true if the key event is consumed by this method, false otherwise
+     */
+    private boolean scrollAndFocus(int direction, int top, int bottom) {
+        boolean handled = true;
+
+        int height = getHeight();
+        int containerTop = getScrollY();
+        int containerBottom = containerTop + height;
+        boolean up = direction == View.FOCUS_UP;
+
+        View newFocused = findFocusableViewInBounds(up, top, bottom);
+        if (newFocused == null) {
+            newFocused = this;
+        }
+
+        if (top >= containerTop && bottom <= containerBottom) {
+            handled = false;
+        } else {
+            int delta = up ? (top - containerTop) : (bottom - containerBottom);
+            doScrollY(delta);
+        }
+
+        if (newFocused != findFocus()) newFocused.requestFocus(direction);
+
+        return handled;
+    }
+
+    /**
+     * Handle scrolling in response to an up or down arrow click.
+     *
+     * @param direction The direction corresponding to the arrow key that was
+     *                  pressed
+     * @return True if we consumed the event, false otherwise
+     */
+    public boolean arrowScroll(int direction) {
+
+        View currentFocused = findFocus();
+        if (currentFocused == this) currentFocused = null;
+
+        View nextFocused = FocusFinder.getInstance().findNextFocus(this, currentFocused, direction);
+
+        final int maxJump = getMaxScrollAmount();
+
+        if (nextFocused != null && isWithinDeltaOfScreen(nextFocused, maxJump, getHeight())) {
+            nextFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(nextFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollY(scrollDelta);
+            nextFocused.requestFocus(direction);
+        } else {
+            // no new focus
+            int scrollDelta = maxJump;
+
+            if (direction == View.FOCUS_UP && getScrollY() < scrollDelta) {
+                scrollDelta = getScrollY();
+            } else if (direction == View.FOCUS_DOWN) {
+                if (getChildCount() > 0) {
+                    int daBottom = getChildAt(0).getBottom();
+                    int screenBottom = getScrollY() + getHeight() - mPaddingBottom;
+                    if (daBottom - screenBottom < maxJump) {
+                        scrollDelta = daBottom - screenBottom;
+                    }
+                }
+            }
+            if (scrollDelta == 0) {
+                return false;
+            }
+            doScrollY(direction == View.FOCUS_DOWN ? scrollDelta : -scrollDelta);
+        }
+
+        if (currentFocused != null && currentFocused.isFocused()
+                && isOffScreen(currentFocused)) {
+            // previously focused item still has focus and is off screen, give
+            // it up (take it back to ourselves)
+            // (also, need to temporarily force FOCUS_BEFORE_DESCENDANTS so we are
+            // sure to
+            // get it)
+            final int descendantFocusability = getDescendantFocusability();  // save
+            setDescendantFocusability(ViewGroup.FOCUS_BEFORE_DESCENDANTS);
+            requestFocus();
+            setDescendantFocusability(descendantFocusability);  // restore
+        }
+        return true;
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is scrolled off
+     *  screen.
+     */
+    private boolean isOffScreen(View descendant) {
+        return !isWithinDeltaOfScreen(descendant, 0, getHeight());
+    }
+
+    /**
+     * @return whether the descendant of this scroll view is within delta
+     *  pixels of being on the screen.
+     */
+    private boolean isWithinDeltaOfScreen(View descendant, int delta, int height) {
+        descendant.getDrawingRect(mTempRect);
+        offsetDescendantRectToMyCoords(descendant, mTempRect);
+
+        return (mTempRect.bottom + delta) >= getScrollY()
+                && (mTempRect.top - delta) <= (getScrollY() + height);
+    }
+
+    /**
+     * Smooth scroll by a Y delta
+     *
+     * @param delta the number of pixels to scroll by on the Y axis
+     */
+    private void doScrollY(int delta) {
+        if (delta != 0) {
+            if (mSmoothScrollingEnabled) {
+                smoothScrollBy(0, delta);
+            } else {
+                scrollBy(0, delta);
+            }
+        }
+    }
+
+    /**
+     * Like {@link View#scrollBy}, but scroll smoothly instead of immediately.
+     *
+     * @param dx the number of pixels to scroll by on the X axis
+     * @param dy the number of pixels to scroll by on the Y axis
+     */
+    public final void smoothScrollBy(int dx, int dy) {
+        if (getChildCount() == 0) {
+            // Nothing to do.
+            return;
+        }
+        long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+        if (duration > ANIMATED_SCROLL_GAP) {
+            final int height = getHeight() - mPaddingBottom - mPaddingTop;
+            final int bottom = getChildAt(0).getHeight();
+            final int maxY = Math.max(0, bottom - height);
+            final int scrollY = mScrollY;
+            dy = Math.max(0, Math.min(scrollY + dy, maxY)) - scrollY;
+
+            mScroller.startScroll(mScrollX, scrollY, 0, dy);
+            postInvalidateOnAnimation();
+        } else {
+            if (!mScroller.isFinished()) {
+                mScroller.abortAnimation();
+                if (mFlingStrictSpan != null) {
+                    mFlingStrictSpan.finish();
+                    mFlingStrictSpan = null;
+                }
+            }
+            scrollBy(dx, dy);
+        }
+        mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+    }
+
+    /**
+     * Like {@link #scrollTo}, but scroll smoothly instead of immediately.
+     *
+     * @param x the position where to scroll on the X axis
+     * @param y the position where to scroll on the Y axis
+     */
+    public final void smoothScrollTo(int x, int y) {
+        smoothScrollBy(x - mScrollX, y - mScrollY);
+    }
+
+    /**
+     * <p>The scroll range of a scroll view is the overall height of all of its
+     * children.</p>
+     */
+    @Override
+    protected int computeVerticalScrollRange() {
+        final int count = getChildCount();
+        final int contentHeight = getHeight() - mPaddingBottom - mPaddingTop;
+        if (count == 0) {
+            return contentHeight;
+        }
+
+        int scrollRange = getChildAt(0).getBottom();
+        final int scrollY = mScrollY;
+        final int overscrollBottom = Math.max(0, scrollRange - contentHeight);
+        if (scrollY < 0) {
+            scrollRange -= scrollY;
+        } else if (scrollY > overscrollBottom) {
+            scrollRange += scrollY - overscrollBottom;
+        }
+
+        return scrollRange;
+    }
+
+    @Override
+    protected int computeVerticalScrollOffset() {
+        return Math.max(0, super.computeVerticalScrollOffset());
+    }
+
+    @Override
+    protected void measureChild(View child, int parentWidthMeasureSpec,
+            int parentHeightMeasureSpec) {
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+
+        int childWidthMeasureSpec;
+        int childHeightMeasureSpec;
+
+        childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec, mPaddingLeft
+                + mPaddingRight, lp.width);
+        final int verticalPadding = mPaddingTop + mPaddingBottom;
+        childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - verticalPadding),
+                MeasureSpec.UNSPECIFIED);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    protected void measureChildWithMargins(View child, int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+                        + widthUsed, lp.width);
+        final int usedTotal = mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin +
+                heightUsed;
+        final int childHeightMeasureSpec = MeasureSpec.makeSafeMeasureSpec(
+                Math.max(0, MeasureSpec.getSize(parentHeightMeasureSpec) - usedTotal),
+                MeasureSpec.UNSPECIFIED);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller.computeScrollOffset()) {
+            // This is called at drawing time by ViewGroup.  We don't want to
+            // re-show the scrollbars at this point, which scrollTo will do,
+            // so we replicate most of scrollTo here.
+            //
+            //         It's a little odd to call onScrollChanged from inside the drawing.
+            //
+            //         It is, except when you remember that computeScroll() is used to
+            //         animate scrolling. So unless we want to defer the onScrollChanged()
+            //         until the end of the animated scrolling, we don't really have a
+            //         choice here.
+            //
+            //         I agree.  The alternative, which I think would be worse, is to post
+            //         something and tell the subclasses later.  This is bad because there
+            //         will be a window where mScrollX/Y is different from what the app
+            //         thinks it is.
+            //
+            int oldX = mScrollX;
+            int oldY = mScrollY;
+            int x = mScroller.getCurrX();
+            int y = mScroller.getCurrY();
+
+            if (oldX != x || oldY != y) {
+                final int range = getScrollRange();
+                final int overscrollMode = getOverScrollMode();
+                final boolean canOverscroll = overscrollMode == OVER_SCROLL_ALWAYS ||
+                        (overscrollMode == OVER_SCROLL_IF_CONTENT_SCROLLS && range > 0);
+
+                overScrollBy(x - oldX, y - oldY, oldX, oldY, 0, range,
+                        0, mOverflingDistance, false);
+                onScrollChanged(mScrollX, mScrollY, oldX, oldY);
+
+                if (canOverscroll) {
+                    if (y < 0 && oldY >= 0) {
+                        mEdgeGlowTop.onAbsorb((int) mScroller.getCurrVelocity());
+                    } else if (y > range && oldY <= range) {
+                        mEdgeGlowBottom.onAbsorb((int) mScroller.getCurrVelocity());
+                    }
+                }
+            }
+
+            if (!awakenScrollBars()) {
+                // Keep on drawing until the animation has finished.
+                postInvalidateOnAnimation();
+            }
+        } else {
+            if (mFlingStrictSpan != null) {
+                mFlingStrictSpan.finish();
+                mFlingStrictSpan = null;
+            }
+        }
+    }
+
+    /**
+     * Scrolls the view to the given child.
+     *
+     * @param child the View to scroll to
+     */
+    private void scrollToChild(View child) {
+        child.getDrawingRect(mTempRect);
+
+        /* Offset from child's local coordinates to ScrollView coordinates */
+        offsetDescendantRectToMyCoords(child, mTempRect);
+
+        int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+
+        if (scrollDelta != 0) {
+            scrollBy(0, scrollDelta);
+        }
+    }
+
+    /**
+     * If rect is off screen, scroll just enough to get it (or at least the
+     * first screen size chunk of it) on screen.
+     *
+     * @param rect      The rectangle.
+     * @param immediate True to scroll immediately without animation
+     * @return true if scrolling was performed
+     */
+    private boolean scrollToChildRect(Rect rect, boolean immediate) {
+        final int delta = computeScrollDeltaToGetChildRectOnScreen(rect);
+        final boolean scroll = delta != 0;
+        if (scroll) {
+            if (immediate) {
+                scrollBy(0, delta);
+            } else {
+                smoothScrollBy(0, delta);
+            }
+        }
+        return scroll;
+    }
+
+    /**
+     * Compute the amount to scroll in the Y direction in order to get
+     * a rectangle completely on the screen (or, if taller than the screen,
+     * at least the first screen size chunk of it).
+     *
+     * @param rect The rect.
+     * @return The scroll delta.
+     */
+    protected int computeScrollDeltaToGetChildRectOnScreen(Rect rect) {
+        if (getChildCount() == 0) return 0;
+
+        int height = getHeight();
+        int screenTop = getScrollY();
+        int screenBottom = screenTop + height;
+
+        int fadingEdge = getVerticalFadingEdgeLength();
+
+        // leave room for top fading edge as long as rect isn't at very top
+        if (rect.top > 0) {
+            screenTop += fadingEdge;
+        }
+
+        // leave room for bottom fading edge as long as rect isn't at very bottom
+        if (rect.bottom < getChildAt(0).getHeight()) {
+            screenBottom -= fadingEdge;
+        }
+
+        int scrollYDelta = 0;
+
+        if (rect.bottom > screenBottom && rect.top > screenTop) {
+            // need to move down to get it in view: move down just enough so
+            // that the entire rectangle is in view (or at least the first
+            // screen size chunk).
+
+            if (rect.height() > height) {
+                // just enough to get screen size chunk on
+                scrollYDelta += (rect.top - screenTop);
+            } else {
+                // get entire rect at bottom of screen
+                scrollYDelta += (rect.bottom - screenBottom);
+            }
+
+            // make sure we aren't scrolling beyond the end of our content
+            int bottom = getChildAt(0).getBottom();
+            int distanceToBottom = bottom - screenBottom;
+            scrollYDelta = Math.min(scrollYDelta, distanceToBottom);
+
+        } else if (rect.top < screenTop && rect.bottom < screenBottom) {
+            // need to move up to get it in view: move up just enough so that
+            // entire rectangle is in view (or at least the first screen
+            // size chunk of it).
+
+            if (rect.height() > height) {
+                // screen size chunk
+                scrollYDelta -= (screenBottom - rect.bottom);
+            } else {
+                // entire rect at top
+                scrollYDelta -= (screenTop - rect.top);
+            }
+
+            // make sure we aren't scrolling any further than the top our content
+            scrollYDelta = Math.max(scrollYDelta, -getScrollY());
+        }
+        return scrollYDelta;
+    }
+
+    @Override
+    public void requestChildFocus(View child, View focused) {
+        if (focused != null && focused.getRevealOnFocusHint()) {
+            if (!mIsLayoutDirty) {
+                scrollToChild(focused);
+            } else {
+                // The child may not be laid out yet, we can't compute the scroll yet
+                mChildToScrollTo = focused;
+            }
+        }
+        super.requestChildFocus(child, focused);
+    }
+
+
+    /**
+     * When looking for focus in children of a scroll view, need to be a little
+     * more careful not to give focus to something that is scrolled off screen.
+     *
+     * This is more expensive than the default {@link android.view.ViewGroup}
+     * implementation, otherwise this behavior might have been made the default.
+     */
+    @Override
+    protected boolean onRequestFocusInDescendants(int direction,
+            Rect previouslyFocusedRect) {
+
+        // convert from forward / backward notation to up / down / left / right
+        // (ugh).
+        if (direction == View.FOCUS_FORWARD) {
+            direction = View.FOCUS_DOWN;
+        } else if (direction == View.FOCUS_BACKWARD) {
+            direction = View.FOCUS_UP;
+        }
+
+        final View nextFocus = previouslyFocusedRect == null ?
+                FocusFinder.getInstance().findNextFocus(this, null, direction) :
+                FocusFinder.getInstance().findNextFocusFromRect(this,
+                        previouslyFocusedRect, direction);
+
+        if (nextFocus == null) {
+            return false;
+        }
+
+        if (isOffScreen(nextFocus)) {
+            return false;
+        }
+
+        return nextFocus.requestFocus(direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public boolean requestChildRectangleOnScreen(View child, Rect rectangle,
+            boolean immediate) {
+        // offset into coordinate space of this scroll view
+        rectangle.offset(child.getLeft() - child.getScrollX(),
+                child.getTop() - child.getScrollY());
+
+        return scrollToChildRect(rectangle, immediate);
+    }
+
+    @Override
+    public void requestLayout() {
+        mIsLayoutDirty = true;
+        super.requestLayout();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+        if (mFlingStrictSpan != null) {
+            mFlingStrictSpan.finish();
+            mFlingStrictSpan = null;
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mIsLayoutDirty = false;
+        // Give a child focus if it needs it
+        if (mChildToScrollTo != null && isViewDescendantOf(mChildToScrollTo, this)) {
+            scrollToChild(mChildToScrollTo);
+        }
+        mChildToScrollTo = null;
+
+        if (!isLaidOut()) {
+            if (mSavedState != null) {
+                mScrollY = mSavedState.scrollPosition;
+                mSavedState = null;
+            } // mScrollY default value is "0"
+
+            final int childHeight = (getChildCount() > 0) ? getChildAt(0).getMeasuredHeight() : 0;
+            final int scrollRange = Math.max(0,
+                    childHeight - (b - t - mPaddingBottom - mPaddingTop));
+
+            // Don't forget to clamp
+            if (mScrollY > scrollRange) {
+                mScrollY = scrollRange;
+            } else if (mScrollY < 0) {
+                mScrollY = 0;
+            }
+        }
+
+        // Calling this with the present values causes it to re-claim them
+        scrollTo(mScrollX, mScrollY);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        super.onSizeChanged(w, h, oldw, oldh);
+
+        View currentFocused = findFocus();
+        if (null == currentFocused || this == currentFocused)
+            return;
+
+        // If the currently-focused view was visible on the screen when the
+        // screen was at the old height, then scroll the screen to make that
+        // view visible with the new screen height.
+        if (isWithinDeltaOfScreen(currentFocused, 0, oldh)) {
+            currentFocused.getDrawingRect(mTempRect);
+            offsetDescendantRectToMyCoords(currentFocused, mTempRect);
+            int scrollDelta = computeScrollDeltaToGetChildRectOnScreen(mTempRect);
+            doScrollY(scrollDelta);
+        }
+    }
+
+    /**
+     * Return true if child is a descendant of parent, (or equal to the parent).
+     */
+    private static boolean isViewDescendantOf(View child, View parent) {
+        if (child == parent) {
+            return true;
+        }
+
+        final ViewParent theParent = child.getParent();
+        return (theParent instanceof ViewGroup) && isViewDescendantOf((View) theParent, parent);
+    }
+
+    /**
+     * Fling the scroll view
+     *
+     * @param velocityY The initial velocity in the Y direction. Positive
+     *                  numbers mean that the finger/cursor is moving down the screen,
+     *                  which means we want to scroll towards the top.
+     */
+    public void fling(int velocityY) {
+        if (getChildCount() > 0) {
+            int height = getHeight() - mPaddingBottom - mPaddingTop;
+            int bottom = getChildAt(0).getHeight();
+
+            mScroller.fling(mScrollX, mScrollY, 0, velocityY, 0, 0, 0,
+                    Math.max(0, bottom - height), 0, height/2);
+
+            if (mFlingStrictSpan == null) {
+                mFlingStrictSpan = StrictMode.enterCriticalSpan("ScrollView-fling");
+            }
+
+            postInvalidateOnAnimation();
+        }
+    }
+
+    private void flingWithNestedDispatch(int velocityY) {
+        final boolean canFling = (mScrollY > 0 || velocityY > 0) &&
+                (mScrollY < getScrollRange() || velocityY < 0);
+        if (!dispatchNestedPreFling(0, velocityY)) {
+            dispatchNestedFling(0, velocityY, canFling);
+            if (canFling) {
+                fling(velocityY);
+            }
+        }
+    }
+
+    private void endDrag() {
+        mIsBeingDragged = false;
+
+        recycleVelocityTracker();
+
+        if (mEdgeGlowTop != null) {
+            mEdgeGlowTop.onRelease();
+            mEdgeGlowBottom.onRelease();
+        }
+
+        if (mScrollStrictSpan != null) {
+            mScrollStrictSpan.finish();
+            mScrollStrictSpan = null;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * <p>This version also clamps the scrolling to the bounds of our child.
+     */
+    @Override
+    public void scrollTo(int x, int y) {
+        // we rely on the fact the View.scrollBy calls scrollTo.
+        if (getChildCount() > 0) {
+            View child = getChildAt(0);
+            x = clamp(x, getWidth() - mPaddingRight - mPaddingLeft, child.getWidth());
+            y = clamp(y, getHeight() - mPaddingBottom - mPaddingTop, child.getHeight());
+            if (x != mScrollX || y != mScrollY) {
+                super.scrollTo(x, y);
+            }
+        }
+    }
+
+    @Override
+    public void setOverScrollMode(int mode) {
+        if (mode != OVER_SCROLL_NEVER) {
+            if (mEdgeGlowTop == null) {
+                Context context = getContext();
+                mEdgeGlowTop = new EdgeEffect(context);
+                mEdgeGlowBottom = new EdgeEffect(context);
+            }
+        } else {
+            mEdgeGlowTop = null;
+            mEdgeGlowBottom = null;
+        }
+        super.setOverScrollMode(mode);
+    }
+
+    @Override
+    public boolean onStartNestedScroll(View child, View target, int nestedScrollAxes) {
+        return (nestedScrollAxes & SCROLL_AXIS_VERTICAL) != 0;
+    }
+
+    @Override
+    public void onNestedScrollAccepted(View child, View target, int axes) {
+        super.onNestedScrollAccepted(child, target, axes);
+        startNestedScroll(SCROLL_AXIS_VERTICAL);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    @Override
+    public void onStopNestedScroll(View target) {
+        super.onStopNestedScroll(target);
+    }
+
+    @Override
+    public void onNestedScroll(View target, int dxConsumed, int dyConsumed,
+            int dxUnconsumed, int dyUnconsumed) {
+        final int oldScrollY = mScrollY;
+        scrollBy(0, dyUnconsumed);
+        final int myConsumed = mScrollY - oldScrollY;
+        final int myUnconsumed = dyUnconsumed - myConsumed;
+        dispatchNestedScroll(0, myConsumed, 0, myUnconsumed, null);
+    }
+
+    /**
+     * @inheritDoc
+     */
+    @Override
+    public boolean onNestedFling(View target, float velocityX, float velocityY, boolean consumed) {
+        if (!consumed) {
+            flingWithNestedDispatch((int) velocityY);
+            return true;
+        }
+        return false;
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+        if (mEdgeGlowTop != null) {
+            final int scrollY = mScrollY;
+            final boolean clipToPadding = getClipToPadding();
+            if (!mEdgeGlowTop.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int width;
+                final int height;
+                final float translateX;
+                final float translateY;
+                if (clipToPadding) {
+                    width = getWidth() - mPaddingLeft - mPaddingRight;
+                    height = getHeight() - mPaddingTop - mPaddingBottom;
+                    translateX = mPaddingLeft;
+                    translateY = mPaddingTop;
+                } else {
+                    width = getWidth();
+                    height = getHeight();
+                    translateX = 0;
+                    translateY = 0;
+                }
+                canvas.translate(translateX, Math.min(0, scrollY) + translateY);
+                mEdgeGlowTop.setSize(width, height);
+                if (mEdgeGlowTop.draw(canvas)) {
+                    postInvalidateOnAnimation();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+            if (!mEdgeGlowBottom.isFinished()) {
+                final int restoreCount = canvas.save();
+                final int width;
+                final int height;
+                final float translateX;
+                final float translateY;
+                if (clipToPadding) {
+                    width = getWidth() - mPaddingLeft - mPaddingRight;
+                    height = getHeight() - mPaddingTop - mPaddingBottom;
+                    translateX = mPaddingLeft;
+                    translateY = mPaddingTop;
+                } else {
+                    width = getWidth();
+                    height = getHeight();
+                    translateX = 0;
+                    translateY = 0;
+                }
+                canvas.translate(-width + translateX,
+                            Math.max(getScrollRange(), scrollY) + height + translateY);
+                canvas.rotate(180, width, 0);
+                mEdgeGlowBottom.setSize(width, height);
+                if (mEdgeGlowBottom.draw(canvas)) {
+                    postInvalidateOnAnimation();
+                }
+                canvas.restoreToCount(restoreCount);
+            }
+        }
+    }
+
+    private static int clamp(int n, int my, int child) {
+        if (my >= child || n < 0) {
+            /* my >= child is this case:
+             *                    |--------------- me ---------------|
+             *     |------ child ------|
+             * or
+             *     |--------------- me ---------------|
+             *            |------ child ------|
+             * or
+             *     |--------------- me ---------------|
+             *                                  |------ child ------|
+             *
+             * n < 0 is this case:
+             *     |------ me ------|
+             *                    |-------- child --------|
+             *     |-- mScrollX --|
+             */
+            return 0;
+        }
+        if ((my+n) > child) {
+            /* this case:
+             *                    |------ me ------|
+             *     |------ child ------|
+             *     |-- mScrollX --|
+             */
+            return child-my;
+        }
+        return n;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            // Some old apps reused IDs in ways they shouldn't have.
+            // Don't break them, but they don't get scroll state restoration.
+            super.onRestoreInstanceState(state);
+            return;
+        }
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mSavedState = ss;
+        requestLayout();
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        if (mContext.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.JELLY_BEAN_MR2) {
+            // Some old apps reused IDs in ways they shouldn't have.
+            // Don't break them, but they don't get scroll state restoration.
+            return super.onSaveInstanceState();
+        }
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.scrollPosition = mScrollY;
+        return ss;
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+        super.encodeProperties(encoder);
+        encoder.addProperty("fillViewport", mFillViewport);
+    }
+
+    static class SavedState extends BaseSavedState {
+        public int scrollPosition;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public SavedState(Parcel source) {
+            super(source);
+            scrollPosition = source.readInt();
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeInt(scrollPosition);
+        }
+
+        @Override
+        public String toString() {
+            return "ScrollView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " scrollPosition=" + scrollPosition + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR
+                = new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+}
diff --git a/android/widget/Scroller.java b/android/widget/Scroller.java
new file mode 100644
index 0000000..357c9c3
--- /dev/null
+++ b/android/widget/Scroller.java
@@ -0,0 +1,597 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.hardware.SensorManager;
+import android.os.Build;
+import android.view.ViewConfiguration;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+
+/**
+ * <p>This class encapsulates scrolling. You can use scrollers ({@link Scroller}
+ * or {@link OverScroller}) to collect the data you need to produce a scrolling
+ * animation&mdash;for example, in response to a fling gesture. Scrollers track
+ * scroll offsets for you over time, but they don't automatically apply those
+ * positions to your view. It's your responsibility to get and apply new
+ * coordinates at a rate that will make the scrolling animation look smooth.</p>
+ *
+ * <p>Here is a simple example:</p>
+ *
+ * <pre> private Scroller mScroller = new Scroller(context);
+ * ...
+ * public void zoomIn() {
+ *     // Revert any animation currently in progress
+ *     mScroller.forceFinished(true);
+ *     // Start scrolling by providing a starting point and
+ *     // the distance to travel
+ *     mScroller.startScroll(0, 0, 100, 0);
+ *     // Invalidate to request a redraw
+ *     invalidate();
+ * }</pre>
+ *
+ * <p>To track the changing positions of the x/y coordinates, use
+ * {@link #computeScrollOffset}. The method returns a boolean to indicate
+ * whether the scroller is finished. If it isn't, it means that a fling or
+ * programmatic pan operation is still in progress. You can use this method to
+ * find the current offsets of the x and y coordinates, for example:</p>
+ *
+ * <pre>if (mScroller.computeScrollOffset()) {
+ *     // Get current x and y positions
+ *     int currX = mScroller.getCurrX();
+ *     int currY = mScroller.getCurrY();
+ *    ...
+ * }</pre>
+ */
+public class Scroller  {
+    private final Interpolator mInterpolator;
+
+    private int mMode;
+
+    private int mStartX;
+    private int mStartY;
+    private int mFinalX;
+    private int mFinalY;
+
+    private int mMinX;
+    private int mMaxX;
+    private int mMinY;
+    private int mMaxY;
+
+    private int mCurrX;
+    private int mCurrY;
+    private long mStartTime;
+    private int mDuration;
+    private float mDurationReciprocal;
+    private float mDeltaX;
+    private float mDeltaY;
+    private boolean mFinished;
+    private boolean mFlywheel;
+
+    private float mVelocity;
+    private float mCurrVelocity;
+    private int mDistance;
+
+    private float mFlingFriction = ViewConfiguration.getScrollFriction();
+
+    private static final int DEFAULT_DURATION = 250;
+    private static final int SCROLL_MODE = 0;
+    private static final int FLING_MODE = 1;
+
+    private static float DECELERATION_RATE = (float) (Math.log(0.78) / Math.log(0.9));
+    private static final float INFLEXION = 0.35f; // Tension lines cross at (INFLEXION, 1)
+    private static final float START_TENSION = 0.5f;
+    private static final float END_TENSION = 1.0f;
+    private static final float P1 = START_TENSION * INFLEXION;
+    private static final float P2 = 1.0f - END_TENSION * (1.0f - INFLEXION);
+
+    private static final int NB_SAMPLES = 100;
+    private static final float[] SPLINE_POSITION = new float[NB_SAMPLES + 1];
+    private static final float[] SPLINE_TIME = new float[NB_SAMPLES + 1];
+
+    private float mDeceleration;
+    private final float mPpi;
+
+    // A context-specific coefficient adjusted to physical values.
+    private float mPhysicalCoeff;
+
+    static {
+        float x_min = 0.0f;
+        float y_min = 0.0f;
+        for (int i = 0; i < NB_SAMPLES; i++) {
+            final float alpha = (float) i / NB_SAMPLES;
+
+            float x_max = 1.0f;
+            float x, tx, coef;
+            while (true) {
+                x = x_min + (x_max - x_min) / 2.0f;
+                coef = 3.0f * x * (1.0f - x);
+                tx = coef * ((1.0f - x) * P1 + x * P2) + x * x * x;
+                if (Math.abs(tx - alpha) < 1E-5) break;
+                if (tx > alpha) x_max = x;
+                else x_min = x;
+            }
+            SPLINE_POSITION[i] = coef * ((1.0f - x) * START_TENSION + x) + x * x * x;
+
+            float y_max = 1.0f;
+            float y, dy;
+            while (true) {
+                y = y_min + (y_max - y_min) / 2.0f;
+                coef = 3.0f * y * (1.0f - y);
+                dy = coef * ((1.0f - y) * START_TENSION + y) + y * y * y;
+                if (Math.abs(dy - alpha) < 1E-5) break;
+                if (dy > alpha) y_max = y;
+                else y_min = y;
+            }
+            SPLINE_TIME[i] = coef * ((1.0f - y) * P1 + y * P2) + y * y * y;
+        }
+        SPLINE_POSITION[NB_SAMPLES] = SPLINE_TIME[NB_SAMPLES] = 1.0f;
+    }
+
+    /**
+     * Create a Scroller with the default duration and interpolator.
+     */
+    public Scroller(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. "Flywheel" behavior will
+     * be in effect for apps targeting Honeycomb or newer.
+     */
+    public Scroller(Context context, Interpolator interpolator) {
+        this(context, interpolator,
+                context.getApplicationInfo().targetSdkVersion >= Build.VERSION_CODES.HONEYCOMB);
+    }
+
+    /**
+     * Create a Scroller with the specified interpolator. If the interpolator is
+     * null, the default (viscous) interpolator will be used. Specify whether or
+     * not to support progressive "flywheel" behavior in flinging.
+     */
+    public Scroller(Context context, Interpolator interpolator, boolean flywheel) {
+        mFinished = true;
+        if (interpolator == null) {
+            mInterpolator = new ViscousFluidInterpolator();
+        } else {
+            mInterpolator = interpolator;
+        }
+        mPpi = context.getResources().getDisplayMetrics().density * 160.0f;
+        mDeceleration = computeDeceleration(ViewConfiguration.getScrollFriction());
+        mFlywheel = flywheel;
+
+        mPhysicalCoeff = computeDeceleration(0.84f); // look and feel tuning
+    }
+
+    /**
+     * The amount of friction applied to flings. The default value
+     * is {@link ViewConfiguration#getScrollFriction}.
+     * 
+     * @param friction A scalar dimension-less value representing the coefficient of
+     *         friction.
+     */
+    public final void setFriction(float friction) {
+        mDeceleration = computeDeceleration(friction);
+        mFlingFriction = friction;
+    }
+    
+    private float computeDeceleration(float friction) {
+        return SensorManager.GRAVITY_EARTH   // g (m/s^2)
+                      * 39.37f               // inch/meter
+                      * mPpi                 // pixels per inch
+                      * friction;
+    }
+
+    /**
+     * 
+     * Returns whether the scroller has finished scrolling.
+     * 
+     * @return True if the scroller has finished scrolling, false otherwise.
+     */
+    public final boolean isFinished() {
+        return mFinished;
+    }
+    
+    /**
+     * Force the finished field to a particular value.
+     *  
+     * @param finished The new finished value.
+     */
+    public final void forceFinished(boolean finished) {
+        mFinished = finished;
+    }
+    
+    /**
+     * Returns how long the scroll event will take, in milliseconds.
+     * 
+     * @return The duration of the scroll in milliseconds.
+     */
+    public final int getDuration() {
+        return mDuration;
+    }
+    
+    /**
+     * Returns the current X offset in the scroll. 
+     * 
+     * @return The new X offset as an absolute distance from the origin.
+     */
+    public final int getCurrX() {
+        return mCurrX;
+    }
+    
+    /**
+     * Returns the current Y offset in the scroll. 
+     * 
+     * @return The new Y offset as an absolute distance from the origin.
+     */
+    public final int getCurrY() {
+        return mCurrY;
+    }
+    
+    /**
+     * Returns the current velocity.
+     *
+     * @return The original velocity less the deceleration. Result may be
+     * negative.
+     */
+    public float getCurrVelocity() {
+        return mMode == FLING_MODE ?
+                mCurrVelocity : mVelocity - mDeceleration * timePassed() / 2000.0f;
+    }
+
+    /**
+     * Returns the start X offset in the scroll. 
+     * 
+     * @return The start X offset as an absolute distance from the origin.
+     */
+    public final int getStartX() {
+        return mStartX;
+    }
+    
+    /**
+     * Returns the start Y offset in the scroll. 
+     * 
+     * @return The start Y offset as an absolute distance from the origin.
+     */
+    public final int getStartY() {
+        return mStartY;
+    }
+    
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     * 
+     * @return The final X offset as an absolute distance from the origin.
+     */
+    public final int getFinalX() {
+        return mFinalX;
+    }
+    
+    /**
+     * Returns where the scroll will end. Valid only for "fling" scrolls.
+     * 
+     * @return The final Y offset as an absolute distance from the origin.
+     */
+    public final int getFinalY() {
+        return mFinalY;
+    }
+
+    /**
+     * Call this when you want to know the new location.  If it returns true,
+     * the animation is not yet finished.
+     */ 
+    public boolean computeScrollOffset() {
+        if (mFinished) {
+            return false;
+        }
+
+        int timePassed = (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    
+        if (timePassed < mDuration) {
+            switch (mMode) {
+            case SCROLL_MODE:
+                final float x = mInterpolator.getInterpolation(timePassed * mDurationReciprocal);
+                mCurrX = mStartX + Math.round(x * mDeltaX);
+                mCurrY = mStartY + Math.round(x * mDeltaY);
+                break;
+            case FLING_MODE:
+                final float t = (float) timePassed / mDuration;
+                final int index = (int) (NB_SAMPLES * t);
+                float distanceCoef = 1.f;
+                float velocityCoef = 0.f;
+                if (index < NB_SAMPLES) {
+                    final float t_inf = (float) index / NB_SAMPLES;
+                    final float t_sup = (float) (index + 1) / NB_SAMPLES;
+                    final float d_inf = SPLINE_POSITION[index];
+                    final float d_sup = SPLINE_POSITION[index + 1];
+                    velocityCoef = (d_sup - d_inf) / (t_sup - t_inf);
+                    distanceCoef = d_inf + (t - t_inf) * velocityCoef;
+                }
+
+                mCurrVelocity = velocityCoef * mDistance / mDuration * 1000.0f;
+                
+                mCurrX = mStartX + Math.round(distanceCoef * (mFinalX - mStartX));
+                // Pin to mMinX <= mCurrX <= mMaxX
+                mCurrX = Math.min(mCurrX, mMaxX);
+                mCurrX = Math.max(mCurrX, mMinX);
+                
+                mCurrY = mStartY + Math.round(distanceCoef * (mFinalY - mStartY));
+                // Pin to mMinY <= mCurrY <= mMaxY
+                mCurrY = Math.min(mCurrY, mMaxY);
+                mCurrY = Math.max(mCurrY, mMinY);
+
+                if (mCurrX == mFinalX && mCurrY == mFinalY) {
+                    mFinished = true;
+                }
+
+                break;
+            }
+        }
+        else {
+            mCurrX = mFinalX;
+            mCurrY = mFinalY;
+            mFinished = true;
+        }
+        return true;
+    }
+    
+    /**
+     * Start scrolling by providing a starting point and the distance to travel.
+     * The scroll will use the default value of 250 milliseconds for the
+     * duration.
+     * 
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy) {
+        startScroll(startX, startY, dx, dy, DEFAULT_DURATION);
+    }
+
+    /**
+     * Start scrolling by providing a starting point, the distance to travel,
+     * and the duration of the scroll.
+     * 
+     * @param startX Starting horizontal scroll offset in pixels. Positive
+     *        numbers will scroll the content to the left.
+     * @param startY Starting vertical scroll offset in pixels. Positive numbers
+     *        will scroll the content up.
+     * @param dx Horizontal distance to travel. Positive numbers will scroll the
+     *        content to the left.
+     * @param dy Vertical distance to travel. Positive numbers will scroll the
+     *        content up.
+     * @param duration Duration of the scroll in milliseconds.
+     */
+    public void startScroll(int startX, int startY, int dx, int dy, int duration) {
+        mMode = SCROLL_MODE;
+        mFinished = false;
+        mDuration = duration;
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+        mFinalX = startX + dx;
+        mFinalY = startY + dy;
+        mDeltaX = dx;
+        mDeltaY = dy;
+        mDurationReciprocal = 1.0f / (float) mDuration;
+    }
+
+    /**
+     * Start scrolling based on a fling gesture. The distance travelled will
+     * depend on the initial velocity of the fling.
+     * 
+     * @param startX Starting point of the scroll (X)
+     * @param startY Starting point of the scroll (Y)
+     * @param velocityX Initial velocity of the fling (X) measured in pixels per
+     *        second.
+     * @param velocityY Initial velocity of the fling (Y) measured in pixels per
+     *        second
+     * @param minX Minimum X value. The scroller will not scroll past this
+     *        point.
+     * @param maxX Maximum X value. The scroller will not scroll past this
+     *        point.
+     * @param minY Minimum Y value. The scroller will not scroll past this
+     *        point.
+     * @param maxY Maximum Y value. The scroller will not scroll past this
+     *        point.
+     */
+    public void fling(int startX, int startY, int velocityX, int velocityY,
+            int minX, int maxX, int minY, int maxY) {
+        // Continue a scroll or fling in progress
+        if (mFlywheel && !mFinished) {
+            float oldVel = getCurrVelocity();
+
+            float dx = (float) (mFinalX - mStartX);
+            float dy = (float) (mFinalY - mStartY);
+            float hyp = (float) Math.hypot(dx, dy);
+
+            float ndx = dx / hyp;
+            float ndy = dy / hyp;
+
+            float oldVelocityX = ndx * oldVel;
+            float oldVelocityY = ndy * oldVel;
+            if (Math.signum(velocityX) == Math.signum(oldVelocityX) &&
+                    Math.signum(velocityY) == Math.signum(oldVelocityY)) {
+                velocityX += oldVelocityX;
+                velocityY += oldVelocityY;
+            }
+        }
+
+        mMode = FLING_MODE;
+        mFinished = false;
+
+        float velocity = (float) Math.hypot(velocityX, velocityY);
+     
+        mVelocity = velocity;
+        mDuration = getSplineFlingDuration(velocity);
+        mStartTime = AnimationUtils.currentAnimationTimeMillis();
+        mStartX = startX;
+        mStartY = startY;
+
+        float coeffX = velocity == 0 ? 1.0f : velocityX / velocity;
+        float coeffY = velocity == 0 ? 1.0f : velocityY / velocity;
+
+        double totalDistance = getSplineFlingDistance(velocity);
+        mDistance = (int) (totalDistance * Math.signum(velocity));
+        
+        mMinX = minX;
+        mMaxX = maxX;
+        mMinY = minY;
+        mMaxY = maxY;
+
+        mFinalX = startX + (int) Math.round(totalDistance * coeffX);
+        // Pin to mMinX <= mFinalX <= mMaxX
+        mFinalX = Math.min(mFinalX, mMaxX);
+        mFinalX = Math.max(mFinalX, mMinX);
+        
+        mFinalY = startY + (int) Math.round(totalDistance * coeffY);
+        // Pin to mMinY <= mFinalY <= mMaxY
+        mFinalY = Math.min(mFinalY, mMaxY);
+        mFinalY = Math.max(mFinalY, mMinY);
+    }
+    
+    private double getSplineDeceleration(float velocity) {
+        return Math.log(INFLEXION * Math.abs(velocity) / (mFlingFriction * mPhysicalCoeff));
+    }
+
+    private int getSplineFlingDuration(float velocity) {
+        final double l = getSplineDeceleration(velocity);
+        final double decelMinusOne = DECELERATION_RATE - 1.0;
+        return (int) (1000.0 * Math.exp(l / decelMinusOne));
+    }
+
+    private double getSplineFlingDistance(float velocity) {
+        final double l = getSplineDeceleration(velocity);
+        final double decelMinusOne = DECELERATION_RATE - 1.0;
+        return mFlingFriction * mPhysicalCoeff * Math.exp(DECELERATION_RATE / decelMinusOne * l);
+    }
+
+    /**
+     * Stops the animation. Contrary to {@link #forceFinished(boolean)},
+     * aborting the animating cause the scroller to move to the final x and y
+     * position
+     *
+     * @see #forceFinished(boolean)
+     */
+    public void abortAnimation() {
+        mCurrX = mFinalX;
+        mCurrY = mFinalY;
+        mFinished = true;
+    }
+    
+    /**
+     * Extend the scroll animation. This allows a running animation to scroll
+     * further and longer, when used with {@link #setFinalX(int)} or {@link #setFinalY(int)}.
+     *
+     * @param extend Additional time to scroll in milliseconds.
+     * @see #setFinalX(int)
+     * @see #setFinalY(int)
+     */
+    public void extendDuration(int extend) {
+        int passed = timePassed();
+        mDuration = passed + extend;
+        mDurationReciprocal = 1.0f / mDuration;
+        mFinished = false;
+    }
+
+    /**
+     * Returns the time elapsed since the beginning of the scrolling.
+     *
+     * @return The elapsed time in milliseconds.
+     */
+    public int timePassed() {
+        return (int)(AnimationUtils.currentAnimationTimeMillis() - mStartTime);
+    }
+
+    /**
+     * Sets the final position (X) for this scroller.
+     *
+     * @param newX The new X offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalY(int)
+     */
+    public void setFinalX(int newX) {
+        mFinalX = newX;
+        mDeltaX = mFinalX - mStartX;
+        mFinished = false;
+    }
+
+    /**
+     * Sets the final position (Y) for this scroller.
+     *
+     * @param newY The new Y offset as an absolute distance from the origin.
+     * @see #extendDuration(int)
+     * @see #setFinalX(int)
+     */
+    public void setFinalY(int newY) {
+        mFinalY = newY;
+        mDeltaY = mFinalY - mStartY;
+        mFinished = false;
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isScrollingInDirection(float xvel, float yvel) {
+        return !mFinished && Math.signum(xvel) == Math.signum(mFinalX - mStartX) &&
+                Math.signum(yvel) == Math.signum(mFinalY - mStartY);
+    }
+
+    static class ViscousFluidInterpolator implements Interpolator {
+        /** Controls the viscous fluid effect (how much of it). */
+        private static final float VISCOUS_FLUID_SCALE = 8.0f;
+
+        private static final float VISCOUS_FLUID_NORMALIZE;
+        private static final float VISCOUS_FLUID_OFFSET;
+
+        static {
+
+            // must be set to 1.0 (used in viscousFluid())
+            VISCOUS_FLUID_NORMALIZE = 1.0f / viscousFluid(1.0f);
+            // account for very small floating-point error
+            VISCOUS_FLUID_OFFSET = 1.0f - VISCOUS_FLUID_NORMALIZE * viscousFluid(1.0f);
+        }
+
+        private static float viscousFluid(float x) {
+            x *= VISCOUS_FLUID_SCALE;
+            if (x < 1.0f) {
+                x -= (1.0f - (float)Math.exp(-x));
+            } else {
+                float start = 0.36787944117f;   // 1/e == exp(-1)
+                x = 1.0f - (float)Math.exp(1.0f - x);
+                x = start + x * (1.0f - start);
+            }
+            return x;
+        }
+
+        @Override
+        public float getInterpolation(float input) {
+            final float interpolated = VISCOUS_FLUID_NORMALIZE * viscousFluid(input);
+            if (interpolated > 0) {
+                return interpolated + VISCOUS_FLUID_OFFSET;
+            }
+            return interpolated;
+        }
+    }
+}
diff --git a/android/widget/SearchView.java b/android/widget/SearchView.java
new file mode 100644
index 0000000..519a7dd
--- /dev/null
+++ b/android/widget/SearchView.java
@@ -0,0 +1,2080 @@
+/*
+ * 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 android.widget;
+
+import static android.widget.SuggestionsAdapter.getColumnString;
+
+import android.annotation.Nullable;
+import android.app.PendingIntent;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ActivityNotFoundException;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.database.Cursor;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.speech.RecognizerIntent;
+import android.text.Editable;
+import android.text.InputType;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.TextWatcher;
+import android.text.style.ImageSpan;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.CollapsibleActionView;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.TouchDelegate;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.AdapterView.OnItemSelectedListener;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.R;
+
+import java.util.WeakHashMap;
+
+/**
+ * A widget that provides a user interface for the user to enter a search query and submit a request
+ * to a search provider. Shows a list of query suggestions or results, if available, and allows the
+ * user to pick a suggestion or result to launch into.
+ *
+ * <p>
+ * When the SearchView is used in an ActionBar as an action view for a collapsible menu item, it
+ * needs to be set to iconified by default using {@link #setIconifiedByDefault(boolean)
+ * setIconifiedByDefault(true)}. This is the default, so nothing needs to be done.
+ * </p>
+ * <p>
+ * If you want the search field to always be visible, then call setIconifiedByDefault(false).
+ * </p>
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about using {@code SearchView}, read the
+ * <a href="{@docRoot}guide/topics/search/index.html">Search</a> developer guide.</p>
+ * </div>
+ *
+ * @see android.view.MenuItem#SHOW_AS_ACTION_COLLAPSE_ACTION_VIEW
+ * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+ * @attr ref android.R.styleable#SearchView_imeOptions
+ * @attr ref android.R.styleable#SearchView_inputType
+ * @attr ref android.R.styleable#SearchView_maxWidth
+ * @attr ref android.R.styleable#SearchView_queryHint
+ */
+public class SearchView extends LinearLayout implements CollapsibleActionView {
+
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "SearchView";
+
+    /**
+     * Private constant for removing the microphone in the keyboard.
+     */
+    private static final String IME_OPTION_NO_MICROPHONE = "nm";
+
+    private final SearchAutoComplete mSearchSrcTextView;
+    private final View mSearchEditFrame;
+    private final View mSearchPlate;
+    private final View mSubmitArea;
+    private final ImageView mSearchButton;
+    private final ImageView mGoButton;
+    private final ImageView mCloseButton;
+    private final ImageView mVoiceButton;
+    private final View mDropDownAnchor;
+
+    private UpdatableTouchDelegate mTouchDelegate;
+    private Rect mSearchSrcTextViewBounds = new Rect();
+    private Rect mSearchSrtTextViewBoundsExpanded = new Rect();
+    private int[] mTemp = new int[2];
+    private int[] mTemp2 = new int[2];
+
+    /** Icon optionally displayed when the SearchView is collapsed. */
+    private final ImageView mCollapsedIcon;
+
+    /** Drawable used as an EditText hint. */
+    private final Drawable mSearchHintIcon;
+
+    // Resources used by SuggestionsAdapter to display suggestions.
+    private final int mSuggestionRowLayout;
+    private final int mSuggestionCommitIconResId;
+
+    // Intents used for voice searching.
+    private final Intent mVoiceWebSearchIntent;
+    private final Intent mVoiceAppSearchIntent;
+
+    private final CharSequence mDefaultQueryHint;
+
+    private OnQueryTextListener mOnQueryChangeListener;
+    private OnCloseListener mOnCloseListener;
+    private OnFocusChangeListener mOnQueryTextFocusChangeListener;
+    private OnSuggestionListener mOnSuggestionListener;
+    private OnClickListener mOnSearchClickListener;
+
+    private boolean mIconifiedByDefault;
+    private boolean mIconified;
+    private CursorAdapter mSuggestionsAdapter;
+    private boolean mSubmitButtonEnabled;
+    private CharSequence mQueryHint;
+    private boolean mQueryRefinement;
+    private boolean mClearingFocus;
+    private int mMaxWidth;
+    private boolean mVoiceButtonEnabled;
+    private CharSequence mOldQueryText;
+    private CharSequence mUserQuery;
+    private boolean mExpandedInActionView;
+    private int mCollapsedImeOptions;
+
+    private SearchableInfo mSearchable;
+    private Bundle mAppSearchData;
+
+    private Runnable mUpdateDrawableStateRunnable = new Runnable() {
+        public void run() {
+            updateFocusedState();
+        }
+    };
+
+    private Runnable mReleaseCursorRunnable = new Runnable() {
+        public void run() {
+            if (mSuggestionsAdapter != null && mSuggestionsAdapter instanceof SuggestionsAdapter) {
+                mSuggestionsAdapter.changeCursor(null);
+            }
+        }
+    };
+
+    // A weak map of drawables we've gotten from other packages, so we don't load them
+    // more than once.
+    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache =
+            new WeakHashMap<String, Drawable.ConstantState>();
+
+    /**
+     * Callbacks for changes to the query text.
+     */
+    public interface OnQueryTextListener {
+
+        /**
+         * Called when the user submits the query. This could be due to a key press on the
+         * keyboard or due to pressing a submit button.
+         * The listener can override the standard behavior by returning true
+         * to indicate that it has handled the submit request. Otherwise return false to
+         * let the SearchView handle the submission by launching any associated intent.
+         *
+         * @param query the query text that is to be submitted
+         *
+         * @return true if the query has been handled by the listener, false to let the
+         * SearchView perform the default action.
+         */
+        boolean onQueryTextSubmit(String query);
+
+        /**
+         * Called when the query text is changed by the user.
+         *
+         * @param newText the new content of the query text field.
+         *
+         * @return false if the SearchView should perform the default action of showing any
+         * suggestions if available, true if the action was handled by the listener.
+         */
+        boolean onQueryTextChange(String newText);
+    }
+
+    public interface OnCloseListener {
+
+        /**
+         * The user is attempting to close the SearchView.
+         *
+         * @return true if the listener wants to override the default behavior of clearing the
+         * text field and dismissing it, false otherwise.
+         */
+        boolean onClose();
+    }
+
+    /**
+     * Callback interface for selection events on suggestions. These callbacks
+     * are only relevant when a SearchableInfo has been specified by {@link #setSearchableInfo}.
+     */
+    public interface OnSuggestionListener {
+
+        /**
+         * Called when a suggestion was selected by navigating to it.
+         * @param position the absolute position in the list of suggestions.
+         *
+         * @return true if the listener handles the event and wants to override the default
+         * behavior of possibly rewriting the query based on the selected item, false otherwise.
+         */
+        boolean onSuggestionSelect(int position);
+
+        /**
+         * Called when a suggestion was clicked.
+         * @param position the absolute position of the clicked item in the list of suggestions.
+         *
+         * @return true if the listener handles the event and wants to override the default
+         * behavior of launching any intent or submitting a search query specified on that item.
+         * Return false otherwise.
+         */
+        boolean onSuggestionClick(int position);
+    }
+
+    public SearchView(Context context) {
+        this(context, null);
+    }
+
+    public SearchView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.searchViewStyle);
+    }
+
+    public SearchView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SearchView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.SearchView, defStyleAttr, defStyleRes);
+        final LayoutInflater inflater = (LayoutInflater) context.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        final int layoutResId = a.getResourceId(
+                R.styleable.SearchView_layout, R.layout.search_view);
+        inflater.inflate(layoutResId, this, true);
+
+        mSearchSrcTextView = (SearchAutoComplete) findViewById(R.id.search_src_text);
+        mSearchSrcTextView.setSearchView(this);
+
+        mSearchEditFrame = findViewById(R.id.search_edit_frame);
+        mSearchPlate = findViewById(R.id.search_plate);
+        mSubmitArea = findViewById(R.id.submit_area);
+        mSearchButton = (ImageView) findViewById(R.id.search_button);
+        mGoButton = (ImageView) findViewById(R.id.search_go_btn);
+        mCloseButton = (ImageView) findViewById(R.id.search_close_btn);
+        mVoiceButton = (ImageView) findViewById(R.id.search_voice_btn);
+        mCollapsedIcon = (ImageView) findViewById(R.id.search_mag_icon);
+
+        // Set up icons and backgrounds.
+        mSearchPlate.setBackground(a.getDrawable(R.styleable.SearchView_queryBackground));
+        mSubmitArea.setBackground(a.getDrawable(R.styleable.SearchView_submitBackground));
+        mSearchButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
+        mGoButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_goIcon));
+        mCloseButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_closeIcon));
+        mVoiceButton.setImageDrawable(a.getDrawable(R.styleable.SearchView_voiceIcon));
+        mCollapsedIcon.setImageDrawable(a.getDrawable(R.styleable.SearchView_searchIcon));
+
+        // Prior to L MR1, the search hint icon defaulted to searchIcon. If the
+        // style does not have an explicit value set, fall back to that.
+        if (a.hasValueOrEmpty(R.styleable.SearchView_searchHintIcon)) {
+            mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchHintIcon);
+        } else {
+            mSearchHintIcon = a.getDrawable(R.styleable.SearchView_searchIcon);
+        }
+
+        // Extract dropdown layout resource IDs for later use.
+        mSuggestionRowLayout = a.getResourceId(R.styleable.SearchView_suggestionRowLayout,
+                R.layout.search_dropdown_item_icons_2line);
+        mSuggestionCommitIconResId = a.getResourceId(R.styleable.SearchView_commitIcon, 0);
+
+        mSearchButton.setOnClickListener(mOnClickListener);
+        mCloseButton.setOnClickListener(mOnClickListener);
+        mGoButton.setOnClickListener(mOnClickListener);
+        mVoiceButton.setOnClickListener(mOnClickListener);
+        mSearchSrcTextView.setOnClickListener(mOnClickListener);
+
+        mSearchSrcTextView.addTextChangedListener(mTextWatcher);
+        mSearchSrcTextView.setOnEditorActionListener(mOnEditorActionListener);
+        mSearchSrcTextView.setOnItemClickListener(mOnItemClickListener);
+        mSearchSrcTextView.setOnItemSelectedListener(mOnItemSelectedListener);
+        mSearchSrcTextView.setOnKeyListener(mTextKeyListener);
+
+        // Inform any listener of focus changes
+        mSearchSrcTextView.setOnFocusChangeListener(new OnFocusChangeListener() {
+
+            public void onFocusChange(View v, boolean hasFocus) {
+                if (mOnQueryTextFocusChangeListener != null) {
+                    mOnQueryTextFocusChangeListener.onFocusChange(SearchView.this, hasFocus);
+                }
+            }
+        });
+        setIconifiedByDefault(a.getBoolean(R.styleable.SearchView_iconifiedByDefault, true));
+
+        final int maxWidth = a.getDimensionPixelSize(R.styleable.SearchView_maxWidth, -1);
+        if (maxWidth != -1) {
+            setMaxWidth(maxWidth);
+        }
+
+        mDefaultQueryHint = a.getText(R.styleable.SearchView_defaultQueryHint);
+        mQueryHint = a.getText(R.styleable.SearchView_queryHint);
+
+        final int imeOptions = a.getInt(R.styleable.SearchView_imeOptions, -1);
+        if (imeOptions != -1) {
+            setImeOptions(imeOptions);
+        }
+
+        final int inputType = a.getInt(R.styleable.SearchView_inputType, -1);
+        if (inputType != -1) {
+            setInputType(inputType);
+        }
+
+        if (getFocusable() == FOCUSABLE_AUTO) {
+            setFocusable(FOCUSABLE);
+        }
+
+        a.recycle();
+
+        // Save voice intent for later queries/launching
+        mVoiceWebSearchIntent = new Intent(RecognizerIntent.ACTION_WEB_SEARCH);
+        mVoiceWebSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        mVoiceWebSearchIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL,
+                RecognizerIntent.LANGUAGE_MODEL_WEB_SEARCH);
+
+        mVoiceAppSearchIntent = new Intent(RecognizerIntent.ACTION_RECOGNIZE_SPEECH);
+        mVoiceAppSearchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+
+        mDropDownAnchor = findViewById(mSearchSrcTextView.getDropDownAnchor());
+        if (mDropDownAnchor != null) {
+            mDropDownAnchor.addOnLayoutChangeListener(new OnLayoutChangeListener() {
+                @Override
+                public void onLayoutChange(View v, int left, int top, int right, int bottom,
+                        int oldLeft, int oldTop, int oldRight, int oldBottom) {
+                    adjustDropDownSizeAndPosition();
+                }
+            });
+        }
+
+        updateViewsVisibility(mIconifiedByDefault);
+        updateQueryHint();
+    }
+
+    int getSuggestionRowLayout() {
+        return mSuggestionRowLayout;
+    }
+
+    int getSuggestionCommitIconResId() {
+        return mSuggestionCommitIconResId;
+    }
+
+    /**
+     * Sets the SearchableInfo for this SearchView. Properties in the SearchableInfo are used
+     * to display labels, hints, suggestions, create intents for launching search results screens
+     * and controlling other affordances such as a voice button.
+     *
+     * @param searchable a SearchableInfo can be retrieved from the SearchManager, for a specific
+     * activity or a global search provider.
+     */
+    public void setSearchableInfo(SearchableInfo searchable) {
+        mSearchable = searchable;
+        if (mSearchable != null) {
+            updateSearchAutoComplete();
+            updateQueryHint();
+        }
+        // Cache the voice search capability
+        mVoiceButtonEnabled = hasVoiceSearch();
+
+        if (mVoiceButtonEnabled) {
+            // Disable the microphone on the keyboard, as a mic is displayed near the text box
+            // TODO: use imeOptions to disable voice input when the new API will be available
+            mSearchSrcTextView.setPrivateImeOptions(IME_OPTION_NO_MICROPHONE);
+        }
+        updateViewsVisibility(isIconified());
+    }
+
+    /**
+     * Sets the APP_DATA for legacy SearchDialog use.
+     * @param appSearchData bundle provided by the app when launching the search dialog
+     * @hide
+     */
+    public void setAppSearchData(Bundle appSearchData) {
+        mAppSearchData = appSearchData;
+    }
+
+    /**
+     * Sets the IME options on the query text field.
+     *
+     * @see TextView#setImeOptions(int)
+     * @param imeOptions the options to set on the query text field
+     *
+     * @attr ref android.R.styleable#SearchView_imeOptions
+     */
+    public void setImeOptions(int imeOptions) {
+        mSearchSrcTextView.setImeOptions(imeOptions);
+    }
+
+    /**
+     * Returns the IME options set on the query text field.
+     * @return the ime options
+     * @see TextView#setImeOptions(int)
+     *
+     * @attr ref android.R.styleable#SearchView_imeOptions
+     */
+    public int getImeOptions() {
+        return mSearchSrcTextView.getImeOptions();
+    }
+
+    /**
+     * Sets the input type on the query text field.
+     *
+     * @see TextView#setInputType(int)
+     * @param inputType the input type to set on the query text field
+     *
+     * @attr ref android.R.styleable#SearchView_inputType
+     */
+    public void setInputType(int inputType) {
+        mSearchSrcTextView.setInputType(inputType);
+    }
+
+    /**
+     * Returns the input type set on the query text field.
+     * @return the input type
+     *
+     * @attr ref android.R.styleable#SearchView_inputType
+     */
+    public int getInputType() {
+        return mSearchSrcTextView.getInputType();
+    }
+
+    /** @hide */
+    @Override
+    public boolean requestFocus(int direction, Rect previouslyFocusedRect) {
+        // Don't accept focus if in the middle of clearing focus
+        if (mClearingFocus) return false;
+        // Check if SearchView is focusable.
+        if (!isFocusable()) return false;
+        // If it is not iconified, then give the focus to the text field
+        if (!isIconified()) {
+            boolean result = mSearchSrcTextView.requestFocus(direction, previouslyFocusedRect);
+            if (result) {
+                updateViewsVisibility(false);
+            }
+            return result;
+        } else {
+            return super.requestFocus(direction, previouslyFocusedRect);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void clearFocus() {
+        mClearingFocus = true;
+        super.clearFocus();
+        mSearchSrcTextView.clearFocus();
+        mSearchSrcTextView.setImeVisibility(false);
+        mClearingFocus = false;
+    }
+
+    /**
+     * Sets a listener for user actions within the SearchView.
+     *
+     * @param listener the listener object that receives callbacks when the user performs
+     * actions in the SearchView such as clicking on buttons or typing a query.
+     */
+    public void setOnQueryTextListener(OnQueryTextListener listener) {
+        mOnQueryChangeListener = listener;
+    }
+
+    /**
+     * Sets a listener to inform when the user closes the SearchView.
+     *
+     * @param listener the listener to call when the user closes the SearchView.
+     */
+    public void setOnCloseListener(OnCloseListener listener) {
+        mOnCloseListener = listener;
+    }
+
+    /**
+     * Sets a listener to inform when the focus of the query text field changes.
+     *
+     * @param listener the listener to inform of focus changes.
+     */
+    public void setOnQueryTextFocusChangeListener(OnFocusChangeListener listener) {
+        mOnQueryTextFocusChangeListener = listener;
+    }
+
+    /**
+     * Sets a listener to inform when a suggestion is focused or clicked.
+     *
+     * @param listener the listener to inform of suggestion selection events.
+     */
+    public void setOnSuggestionListener(OnSuggestionListener listener) {
+        mOnSuggestionListener = listener;
+    }
+
+    /**
+     * Sets a listener to inform when the search button is pressed. This is only
+     * relevant when the text field is not visible by default. Calling {@link #setIconified
+     * setIconified(false)} can also cause this listener to be informed.
+     *
+     * @param listener the listener to inform when the search button is clicked or
+     * the text field is programmatically de-iconified.
+     */
+    public void setOnSearchClickListener(OnClickListener listener) {
+        mOnSearchClickListener = listener;
+    }
+
+    /**
+     * Returns the query string currently in the text field.
+     *
+     * @return the query string
+     */
+    public CharSequence getQuery() {
+        return mSearchSrcTextView.getText();
+    }
+
+    /**
+     * Sets a query string in the text field and optionally submits the query as well.
+     *
+     * @param query the query string. This replaces any query text already present in the
+     * text field.
+     * @param submit whether to submit the query right now or only update the contents of
+     * text field.
+     */
+    public void setQuery(CharSequence query, boolean submit) {
+        mSearchSrcTextView.setText(query);
+        if (query != null) {
+            mSearchSrcTextView.setSelection(mSearchSrcTextView.length());
+            mUserQuery = query;
+        }
+
+        // If the query is not empty and submit is requested, submit the query
+        if (submit && !TextUtils.isEmpty(query)) {
+            onSubmitQuery();
+        }
+    }
+
+    /**
+     * Sets the hint text to display in the query text field. This overrides
+     * any hint specified in the {@link SearchableInfo}.
+     * <p>
+     * This value may be specified as an empty string to prevent any query hint
+     * from being displayed.
+     *
+     * @param hint the hint text to display or {@code null} to clear
+     * @attr ref android.R.styleable#SearchView_queryHint
+     */
+    public void setQueryHint(@Nullable CharSequence hint) {
+        mQueryHint = hint;
+        updateQueryHint();
+    }
+
+    /**
+     * Returns the hint text that will be displayed in the query text field.
+     * <p>
+     * The displayed query hint is chosen in the following order:
+     * <ol>
+     * <li>Non-null value set with {@link #setQueryHint(CharSequence)}
+     * <li>Value specified in XML using
+     *     {@link android.R.styleable#SearchView_queryHint android:queryHint}
+     * <li>Valid string resource ID exposed by the {@link SearchableInfo} via
+     *     {@link SearchableInfo#getHintId()}
+     * <li>Default hint provided by the theme against which the view was
+     *     inflated
+     * </ol>
+     *
+     * @return the displayed query hint text, or {@code null} if none set
+     * @attr ref android.R.styleable#SearchView_queryHint
+     */
+    @Nullable
+    public CharSequence getQueryHint() {
+        final CharSequence hint;
+        if (mQueryHint != null) {
+            hint = mQueryHint;
+        } else if (mSearchable != null && mSearchable.getHintId() != 0) {
+            hint = getContext().getText(mSearchable.getHintId());
+        } else {
+            hint = mDefaultQueryHint;
+        }
+        return hint;
+    }
+
+    /**
+     * Sets the default or resting state of the search field. If true, a single search icon is
+     * shown by default and expands to show the text field and other buttons when pressed. Also,
+     * if the default state is iconified, then it collapses to that state when the close button
+     * is pressed. Changes to this property will take effect immediately.
+     *
+     * <p>The default value is true.</p>
+     *
+     * @param iconified whether the search field should be iconified by default
+     *
+     * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+     */
+    public void setIconifiedByDefault(boolean iconified) {
+        if (mIconifiedByDefault == iconified) return;
+        mIconifiedByDefault = iconified;
+        updateViewsVisibility(iconified);
+        updateQueryHint();
+    }
+
+    /**
+     * Returns the default iconified state of the search field.
+     * @return
+     *
+     * @attr ref android.R.styleable#SearchView_iconifiedByDefault
+     */
+    public boolean isIconfiedByDefault() {
+        return mIconifiedByDefault;
+    }
+
+    /**
+     * Iconifies or expands the SearchView. Any query text is cleared when iconified. This is
+     * a temporary state and does not override the default iconified state set by
+     * {@link #setIconifiedByDefault(boolean)}. If the default state is iconified, then
+     * a false here will only be valid until the user closes the field. And if the default
+     * state is expanded, then a true here will only clear the text field and not close it.
+     *
+     * @param iconify a true value will collapse the SearchView to an icon, while a false will
+     * expand it.
+     */
+    public void setIconified(boolean iconify) {
+        if (iconify) {
+            onCloseClicked();
+        } else {
+            onSearchClicked();
+        }
+    }
+
+    /**
+     * Returns the current iconified state of the SearchView.
+     *
+     * @return true if the SearchView is currently iconified, false if the search field is
+     * fully visible.
+     */
+    public boolean isIconified() {
+        return mIconified;
+    }
+
+    /**
+     * Enables showing a submit button when the query is non-empty. In cases where the SearchView
+     * is being used to filter the contents of the current activity and doesn't launch a separate
+     * results activity, then the submit button should be disabled.
+     *
+     * @param enabled true to show a submit button for submitting queries, false if a submit
+     * button is not required.
+     */
+    public void setSubmitButtonEnabled(boolean enabled) {
+        mSubmitButtonEnabled = enabled;
+        updateViewsVisibility(isIconified());
+    }
+
+    /**
+     * Returns whether the submit button is enabled when necessary or never displayed.
+     *
+     * @return whether the submit button is enabled automatically when necessary
+     */
+    public boolean isSubmitButtonEnabled() {
+        return mSubmitButtonEnabled;
+    }
+
+    /**
+     * Specifies if a query refinement button should be displayed alongside each suggestion
+     * or if it should depend on the flags set in the individual items retrieved from the
+     * suggestions provider. Clicking on the query refinement button will replace the text
+     * in the query text field with the text from the suggestion. This flag only takes effect
+     * if a SearchableInfo has been specified with {@link #setSearchableInfo(SearchableInfo)}
+     * and not when using a custom adapter.
+     *
+     * @param enable true if all items should have a query refinement button, false if only
+     * those items that have a query refinement flag set should have the button.
+     *
+     * @see SearchManager#SUGGEST_COLUMN_FLAGS
+     * @see SearchManager#FLAG_QUERY_REFINEMENT
+     */
+    public void setQueryRefinementEnabled(boolean enable) {
+        mQueryRefinement = enable;
+        if (mSuggestionsAdapter instanceof SuggestionsAdapter) {
+            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
+                    enable ? SuggestionsAdapter.REFINE_ALL : SuggestionsAdapter.REFINE_BY_ENTRY);
+        }
+    }
+
+    /**
+     * Returns whether query refinement is enabled for all items or only specific ones.
+     * @return true if enabled for all items, false otherwise.
+     */
+    public boolean isQueryRefinementEnabled() {
+        return mQueryRefinement;
+    }
+
+    /**
+     * You can set a custom adapter if you wish. Otherwise the default adapter is used to
+     * display the suggestions from the suggestions provider associated with the SearchableInfo.
+     *
+     * @see #setSearchableInfo(SearchableInfo)
+     */
+    public void setSuggestionsAdapter(CursorAdapter adapter) {
+        mSuggestionsAdapter = adapter;
+
+        mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
+    }
+
+    /**
+     * Returns the adapter used for suggestions, if any.
+     * @return the suggestions adapter
+     */
+    public CursorAdapter getSuggestionsAdapter() {
+        return mSuggestionsAdapter;
+    }
+
+    /**
+     * Makes the view at most this many pixels wide
+     *
+     * @attr ref android.R.styleable#SearchView_maxWidth
+     */
+    public void setMaxWidth(int maxpixels) {
+        mMaxWidth = maxpixels;
+
+        requestLayout();
+    }
+
+    /**
+     * Gets the specified maximum width in pixels, if set. Returns zero if
+     * no maximum width was specified.
+     * @return the maximum width of the view
+     *
+     * @attr ref android.R.styleable#SearchView_maxWidth
+     */
+    public int getMaxWidth() {
+        return mMaxWidth;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // Let the standard measurements take effect in iconified state.
+        if (isIconified()) {
+            super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int width = MeasureSpec.getSize(widthMeasureSpec);
+
+        switch (widthMode) {
+        case MeasureSpec.AT_MOST:
+            // If there is an upper limit, don't exceed maximum width (explicit or implicit)
+            if (mMaxWidth > 0) {
+                width = Math.min(mMaxWidth, width);
+            } else {
+                width = Math.min(getPreferredWidth(), width);
+            }
+            break;
+        case MeasureSpec.EXACTLY:
+            // If an exact width is specified, still don't exceed any specified maximum width
+            if (mMaxWidth > 0) {
+                width = Math.min(mMaxWidth, width);
+            }
+            break;
+        case MeasureSpec.UNSPECIFIED:
+            // Use maximum width, if specified, else preferred width
+            width = mMaxWidth > 0 ? mMaxWidth : getPreferredWidth();
+            break;
+        }
+        widthMode = MeasureSpec.EXACTLY;
+
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int height = MeasureSpec.getSize(heightMeasureSpec);
+
+        switch (heightMode) {
+            case MeasureSpec.AT_MOST:
+                height = Math.min(getPreferredHeight(), height);
+                break;
+            case MeasureSpec.UNSPECIFIED:
+                height = getPreferredHeight();
+                break;
+        }
+        heightMode = MeasureSpec.EXACTLY;
+
+        super.onMeasure(MeasureSpec.makeMeasureSpec(width, widthMode),
+                MeasureSpec.makeMeasureSpec(height, heightMode));
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (changed) {
+            // Expand mSearchSrcTextView touch target to be the height of the parent in order to
+            // allow it to be up to 48dp.
+            getChildBoundsWithinSearchView(mSearchSrcTextView, mSearchSrcTextViewBounds);
+            mSearchSrtTextViewBoundsExpanded.set(
+                    mSearchSrcTextViewBounds.left, 0, mSearchSrcTextViewBounds.right, bottom - top);
+            if (mTouchDelegate == null) {
+                mTouchDelegate = new UpdatableTouchDelegate(mSearchSrtTextViewBoundsExpanded,
+                        mSearchSrcTextViewBounds, mSearchSrcTextView);
+                setTouchDelegate(mTouchDelegate);
+            } else {
+                mTouchDelegate.setBounds(mSearchSrtTextViewBoundsExpanded, mSearchSrcTextViewBounds);
+            }
+        }
+    }
+
+    private void getChildBoundsWithinSearchView(View view, Rect rect) {
+        view.getLocationInWindow(mTemp);
+        getLocationInWindow(mTemp2);
+        final int top = mTemp[1] - mTemp2[1];
+        final int left = mTemp[0] - mTemp2[0];
+        rect.set(left , top, left + view.getWidth(), top + view.getHeight());
+    }
+
+    private int getPreferredWidth() {
+        return getContext().getResources()
+                .getDimensionPixelSize(R.dimen.search_view_preferred_width);
+    }
+
+    private int getPreferredHeight() {
+        return getContext().getResources()
+                .getDimensionPixelSize(R.dimen.search_view_preferred_height);
+    }
+
+    private void updateViewsVisibility(final boolean collapsed) {
+        mIconified = collapsed;
+        // Visibility of views that are visible when collapsed
+        final int visCollapsed = collapsed ? VISIBLE : GONE;
+        // Is there text in the query
+        final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
+
+        mSearchButton.setVisibility(visCollapsed);
+        updateSubmitButton(hasText);
+        mSearchEditFrame.setVisibility(collapsed ? GONE : VISIBLE);
+
+        final int iconVisibility;
+        if (mCollapsedIcon.getDrawable() == null || mIconifiedByDefault) {
+            iconVisibility = GONE;
+        } else {
+            iconVisibility = VISIBLE;
+        }
+        mCollapsedIcon.setVisibility(iconVisibility);
+
+        updateCloseButton();
+        updateVoiceButton(!hasText);
+        updateSubmitArea();
+    }
+
+    private boolean hasVoiceSearch() {
+        if (mSearchable != null && mSearchable.getVoiceSearchEnabled()) {
+            Intent testIntent = null;
+            if (mSearchable.getVoiceSearchLaunchWebSearch()) {
+                testIntent = mVoiceWebSearchIntent;
+            } else if (mSearchable.getVoiceSearchLaunchRecognizer()) {
+                testIntent = mVoiceAppSearchIntent;
+            }
+            if (testIntent != null) {
+                ResolveInfo ri = getContext().getPackageManager().resolveActivity(testIntent,
+                        PackageManager.MATCH_DEFAULT_ONLY);
+                return ri != null;
+            }
+        }
+        return false;
+    }
+
+    private boolean isSubmitAreaEnabled() {
+        return (mSubmitButtonEnabled || mVoiceButtonEnabled) && !isIconified();
+    }
+
+    private void updateSubmitButton(boolean hasText) {
+        int visibility = GONE;
+        if (mSubmitButtonEnabled && isSubmitAreaEnabled() && hasFocus()
+                && (hasText || !mVoiceButtonEnabled)) {
+            visibility = VISIBLE;
+        }
+        mGoButton.setVisibility(visibility);
+    }
+
+    private void updateSubmitArea() {
+        int visibility = GONE;
+        if (isSubmitAreaEnabled()
+                && (mGoButton.getVisibility() == VISIBLE
+                        || mVoiceButton.getVisibility() == VISIBLE)) {
+            visibility = VISIBLE;
+        }
+        mSubmitArea.setVisibility(visibility);
+    }
+
+    private void updateCloseButton() {
+        final boolean hasText = !TextUtils.isEmpty(mSearchSrcTextView.getText());
+        // Should we show the close button? It is not shown if there's no focus,
+        // field is not iconified by default and there is no text in it.
+        final boolean showClose = hasText || (mIconifiedByDefault && !mExpandedInActionView);
+        mCloseButton.setVisibility(showClose ? VISIBLE : GONE);
+        final Drawable closeButtonImg = mCloseButton.getDrawable();
+        if (closeButtonImg != null){
+            closeButtonImg.setState(hasText ? ENABLED_STATE_SET : EMPTY_STATE_SET);
+        }
+    }
+
+    private void postUpdateFocusedState() {
+        post(mUpdateDrawableStateRunnable);
+    }
+
+    private void updateFocusedState() {
+        final boolean focused = mSearchSrcTextView.hasFocus();
+        final int[] stateSet = focused ? FOCUSED_STATE_SET : EMPTY_STATE_SET;
+        final Drawable searchPlateBg = mSearchPlate.getBackground();
+        if (searchPlateBg != null) {
+            searchPlateBg.setState(stateSet);
+        }
+        final Drawable submitAreaBg = mSubmitArea.getBackground();
+        if (submitAreaBg != null) {
+            submitAreaBg.setState(stateSet);
+        }
+        invalidate();
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        removeCallbacks(mUpdateDrawableStateRunnable);
+        post(mReleaseCursorRunnable);
+        super.onDetachedFromWindow();
+    }
+
+    /**
+     * Called by the SuggestionsAdapter
+     * @hide
+     */
+    /* package */void onQueryRefine(CharSequence queryText) {
+        setQuery(queryText);
+    }
+
+    private final OnClickListener mOnClickListener = new OnClickListener() {
+
+        public void onClick(View v) {
+            if (v == mSearchButton) {
+                onSearchClicked();
+            } else if (v == mCloseButton) {
+                onCloseClicked();
+            } else if (v == mGoButton) {
+                onSubmitQuery();
+            } else if (v == mVoiceButton) {
+                onVoiceClicked();
+            } else if (v == mSearchSrcTextView) {
+                forceSuggestionQuery();
+            }
+        }
+    };
+
+    /**
+     * Handles the key down event for dealing with action keys.
+     *
+     * @param keyCode This is the keycode of the typed key, and is the same value as
+     *        found in the KeyEvent parameter.
+     * @param event The complete event record for the typed key
+     *
+     * @return true if the event was handled here, or false if not.
+     */
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        if (mSearchable == null) {
+            return false;
+        }
+
+        // if it's an action specified by the searchable activity, launch the
+        // entered query with the action key
+        SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+        if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+            launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView.getText()
+                    .toString());
+            return true;
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    /**
+     * React to the user typing "enter" or other hardwired keys while typing in
+     * the search box. This handles these special keys while the edit box has
+     * focus.
+     */
+    View.OnKeyListener mTextKeyListener = new View.OnKeyListener() {
+        public boolean onKey(View v, int keyCode, KeyEvent event) {
+            // guard against possible race conditions
+            if (mSearchable == null) {
+                return false;
+            }
+
+            if (DBG) {
+                Log.d(LOG_TAG, "mTextListener.onKey(" + keyCode + "," + event + "), selection: "
+                        + mSearchSrcTextView.getListSelection());
+            }
+
+            // If a suggestion is selected, handle enter, search key, and action keys
+            // as presses on the selected suggestion
+            if (mSearchSrcTextView.isPopupShowing()
+                    && mSearchSrcTextView.getListSelection() != ListView.INVALID_POSITION) {
+                return onSuggestionsKey(v, keyCode, event);
+            }
+
+            // If there is text in the query box, handle enter, and action keys
+            // The search key is handled by the dialog's onKeyDown().
+            if (!mSearchSrcTextView.isEmpty() && event.hasNoModifiers()) {
+                if (event.getAction() == KeyEvent.ACTION_UP) {
+                    if (keyCode == KeyEvent.KEYCODE_ENTER) {
+                        v.cancelLongPress();
+
+                        // Launch as a regular search.
+                        launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, mSearchSrcTextView.getText()
+                                .toString());
+                        return true;
+                    }
+                }
+                if (event.getAction() == KeyEvent.ACTION_DOWN) {
+                    SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+                    if ((actionKey != null) && (actionKey.getQueryActionMsg() != null)) {
+                        launchQuerySearch(keyCode, actionKey.getQueryActionMsg(), mSearchSrcTextView
+                                .getText().toString());
+                        return true;
+                    }
+                }
+            }
+            return false;
+        }
+    };
+
+    /**
+     * React to the user typing while in the suggestions list. First, check for
+     * action keys. If not handled, try refocusing regular characters into the
+     * EditText.
+     */
+    private boolean onSuggestionsKey(View v, int keyCode, KeyEvent event) {
+        // guard against possible race conditions (late arrival after dismiss)
+        if (mSearchable == null) {
+            return false;
+        }
+        if (mSuggestionsAdapter == null) {
+            return false;
+        }
+        if (event.getAction() == KeyEvent.ACTION_DOWN && event.hasNoModifiers()) {
+            // First, check for enter or search (both of which we'll treat as a
+            // "click")
+            if (keyCode == KeyEvent.KEYCODE_ENTER || keyCode == KeyEvent.KEYCODE_SEARCH
+                    || keyCode == KeyEvent.KEYCODE_TAB) {
+                int position = mSearchSrcTextView.getListSelection();
+                return onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
+            }
+
+            // Next, check for left/right moves, which we use to "return" the
+            // user to the edit view
+            if (keyCode == KeyEvent.KEYCODE_DPAD_LEFT || keyCode == KeyEvent.KEYCODE_DPAD_RIGHT) {
+                // give "focus" to text editor, with cursor at the beginning if
+                // left key, at end if right key
+                // TODO: Reverse left/right for right-to-left languages, e.g.
+                // Arabic
+                int selPoint = (keyCode == KeyEvent.KEYCODE_DPAD_LEFT) ? 0 : mSearchSrcTextView
+                        .length();
+                mSearchSrcTextView.setSelection(selPoint);
+                mSearchSrcTextView.setListSelection(0);
+                mSearchSrcTextView.clearListSelection();
+                mSearchSrcTextView.ensureImeVisible(true);
+
+                return true;
+            }
+
+            // Next, check for an "up and out" move
+            if (keyCode == KeyEvent.KEYCODE_DPAD_UP && 0 == mSearchSrcTextView.getListSelection()) {
+                // TODO: restoreUserQuery();
+                // let ACTV complete the move
+                return false;
+            }
+
+            // Next, check for an "action key"
+            SearchableInfo.ActionKeyInfo actionKey = mSearchable.findActionKey(keyCode);
+            if ((actionKey != null)
+                    && ((actionKey.getSuggestActionMsg() != null) || (actionKey
+                            .getSuggestActionMsgColumn() != null))) {
+                // launch suggestion using action key column
+                int position = mSearchSrcTextView.getListSelection();
+                if (position != ListView.INVALID_POSITION) {
+                    Cursor c = mSuggestionsAdapter.getCursor();
+                    if (c.moveToPosition(position)) {
+                        final String actionMsg = getActionKeyMessage(c, actionKey);
+                        if (actionMsg != null && (actionMsg.length() > 0)) {
+                            return onItemClicked(position, keyCode, actionMsg);
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    /**
+     * For a given suggestion and a given cursor row, get the action message. If
+     * not provided by the specific row/column, also check for a single
+     * definition (for the action key).
+     *
+     * @param c The cursor providing suggestions
+     * @param actionKey The actionkey record being examined
+     *
+     * @return Returns a string, or null if no action key message for this
+     *         suggestion
+     */
+    private static String getActionKeyMessage(Cursor c, SearchableInfo.ActionKeyInfo actionKey) {
+        String result = null;
+        // check first in the cursor data, for a suggestion-specific message
+        final String column = actionKey.getSuggestActionMsgColumn();
+        if (column != null) {
+            result = SuggestionsAdapter.getColumnString(c, column);
+        }
+        // If the cursor didn't give us a message, see if there's a single
+        // message defined
+        // for the actionkey (for all suggestions)
+        if (result == null) {
+            result = actionKey.getSuggestActionMsg();
+        }
+        return result;
+    }
+
+    private CharSequence getDecoratedHint(CharSequence hintText) {
+        // If the field is always expanded or we don't have a search hint icon,
+        // then don't add the search icon to the hint.
+        if (!mIconifiedByDefault || mSearchHintIcon == null) {
+            return hintText;
+        }
+
+        final int textSize = (int) (mSearchSrcTextView.getTextSize() * 1.25);
+        mSearchHintIcon.setBounds(0, 0, textSize, textSize);
+
+        final SpannableStringBuilder ssb = new SpannableStringBuilder("   ");
+        ssb.setSpan(new ImageSpan(mSearchHintIcon), 1, 2, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        ssb.append(hintText);
+        return ssb;
+    }
+
+    private void updateQueryHint() {
+        final CharSequence hint = getQueryHint();
+        mSearchSrcTextView.setHint(getDecoratedHint(hint == null ? "" : hint));
+    }
+
+    /**
+     * Updates the auto-complete text view.
+     */
+    private void updateSearchAutoComplete() {
+        mSearchSrcTextView.setDropDownAnimationStyle(0); // no animation
+        mSearchSrcTextView.setThreshold(mSearchable.getSuggestThreshold());
+        mSearchSrcTextView.setImeOptions(mSearchable.getImeOptions());
+        int inputType = mSearchable.getInputType();
+        // We only touch this if the input type is set up for text (which it almost certainly
+        // should be, in the case of search!)
+        if ((inputType & InputType.TYPE_MASK_CLASS) == InputType.TYPE_CLASS_TEXT) {
+            // The existence of a suggestions authority is the proxy for "suggestions
+            // are available here"
+            inputType &= ~InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+            if (mSearchable.getSuggestAuthority() != null) {
+                inputType |= InputType.TYPE_TEXT_FLAG_AUTO_COMPLETE;
+                // TYPE_TEXT_FLAG_AUTO_COMPLETE means that the text editor is performing
+                // auto-completion based on its own semantics, which it will present to the user
+                // as they type. This generally means that the input method should not show its
+                // own candidates, and the spell checker should not be in action. The text editor
+                // supplies its candidates by calling InputMethodManager.displayCompletions(),
+                // which in turn will call InputMethodSession.displayCompletions().
+                inputType |= InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS;
+            }
+        }
+        mSearchSrcTextView.setInputType(inputType);
+        if (mSuggestionsAdapter != null) {
+            mSuggestionsAdapter.changeCursor(null);
+        }
+        // attach the suggestions adapter, if suggestions are available
+        // The existence of a suggestions authority is the proxy for "suggestions available here"
+        if (mSearchable.getSuggestAuthority() != null) {
+            mSuggestionsAdapter = new SuggestionsAdapter(getContext(),
+                    this, mSearchable, mOutsideDrawablesCache);
+            mSearchSrcTextView.setAdapter(mSuggestionsAdapter);
+            ((SuggestionsAdapter) mSuggestionsAdapter).setQueryRefinement(
+                    mQueryRefinement ? SuggestionsAdapter.REFINE_ALL
+                    : SuggestionsAdapter.REFINE_BY_ENTRY);
+        }
+    }
+
+    /**
+     * Update the visibility of the voice button.  There are actually two voice search modes,
+     * either of which will activate the button.
+     * @param empty whether the search query text field is empty. If it is, then the other
+     * criteria apply to make the voice button visible.
+     */
+    private void updateVoiceButton(boolean empty) {
+        int visibility = GONE;
+        if (mVoiceButtonEnabled && !isIconified() && empty) {
+            visibility = VISIBLE;
+            mGoButton.setVisibility(GONE);
+        }
+        mVoiceButton.setVisibility(visibility);
+    }
+
+    private final OnEditorActionListener mOnEditorActionListener = new OnEditorActionListener() {
+
+        /**
+         * Called when the input method default action key is pressed.
+         */
+        public boolean onEditorAction(TextView v, int actionId, KeyEvent event) {
+            onSubmitQuery();
+            return true;
+        }
+    };
+
+    private void onTextChanged(CharSequence newText) {
+        CharSequence text = mSearchSrcTextView.getText();
+        mUserQuery = text;
+        boolean hasText = !TextUtils.isEmpty(text);
+        updateSubmitButton(hasText);
+        updateVoiceButton(!hasText);
+        updateCloseButton();
+        updateSubmitArea();
+        if (mOnQueryChangeListener != null && !TextUtils.equals(newText, mOldQueryText)) {
+            mOnQueryChangeListener.onQueryTextChange(newText.toString());
+        }
+        mOldQueryText = newText.toString();
+    }
+
+    private void onSubmitQuery() {
+        CharSequence query = mSearchSrcTextView.getText();
+        if (query != null && TextUtils.getTrimmedLength(query) > 0) {
+            if (mOnQueryChangeListener == null
+                    || !mOnQueryChangeListener.onQueryTextSubmit(query.toString())) {
+                if (mSearchable != null) {
+                    launchQuerySearch(KeyEvent.KEYCODE_UNKNOWN, null, query.toString());
+                }
+                mSearchSrcTextView.setImeVisibility(false);
+                dismissSuggestions();
+            }
+        }
+    }
+
+    private void dismissSuggestions() {
+        mSearchSrcTextView.dismissDropDown();
+    }
+
+    private void onCloseClicked() {
+        CharSequence text = mSearchSrcTextView.getText();
+        if (TextUtils.isEmpty(text)) {
+            if (mIconifiedByDefault) {
+                // If the app doesn't override the close behavior
+                if (mOnCloseListener == null || !mOnCloseListener.onClose()) {
+                    // hide the keyboard and remove focus
+                    clearFocus();
+                    // collapse the search field
+                    updateViewsVisibility(true);
+                }
+            }
+        } else {
+            mSearchSrcTextView.setText("");
+            mSearchSrcTextView.requestFocus();
+            mSearchSrcTextView.setImeVisibility(true);
+        }
+
+    }
+
+    private void onSearchClicked() {
+        updateViewsVisibility(false);
+        mSearchSrcTextView.requestFocus();
+        mSearchSrcTextView.setImeVisibility(true);
+        if (mOnSearchClickListener != null) {
+            mOnSearchClickListener.onClick(this);
+        }
+    }
+
+    private void onVoiceClicked() {
+        // guard against possible race conditions
+        if (mSearchable == null) {
+            return;
+        }
+        SearchableInfo searchable = mSearchable;
+        try {
+            if (searchable.getVoiceSearchLaunchWebSearch()) {
+                Intent webSearchIntent = createVoiceWebSearchIntent(mVoiceWebSearchIntent,
+                        searchable);
+                getContext().startActivity(webSearchIntent);
+            } else if (searchable.getVoiceSearchLaunchRecognizer()) {
+                Intent appSearchIntent = createVoiceAppSearchIntent(mVoiceAppSearchIntent,
+                        searchable);
+                getContext().startActivity(appSearchIntent);
+            }
+        } catch (ActivityNotFoundException e) {
+            // Should not happen, since we check the availability of
+            // voice search before showing the button. But just in case...
+            Log.w(LOG_TAG, "Could not find voice search activity");
+        }
+    }
+
+    void onTextFocusChanged() {
+        updateViewsVisibility(isIconified());
+        // Delayed update to make sure that the focus has settled down and window focus changes
+        // don't affect it. A synchronous update was not working.
+        postUpdateFocusedState();
+        if (mSearchSrcTextView.hasFocus()) {
+            forceSuggestionQuery();
+        }
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+
+        postUpdateFocusedState();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onActionViewCollapsed() {
+        setQuery("", false);
+        clearFocus();
+        updateViewsVisibility(true);
+        mSearchSrcTextView.setImeOptions(mCollapsedImeOptions);
+        mExpandedInActionView = false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onActionViewExpanded() {
+        if (mExpandedInActionView) return;
+
+        mExpandedInActionView = true;
+        mCollapsedImeOptions = mSearchSrcTextView.getImeOptions();
+        mSearchSrcTextView.setImeOptions(mCollapsedImeOptions | EditorInfo.IME_FLAG_NO_FULLSCREEN);
+        mSearchSrcTextView.setText("");
+        setIconified(false);
+    }
+
+    static class SavedState extends BaseSavedState {
+        boolean isIconified;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        public SavedState(Parcel source) {
+            super(source);
+            isIconified = (Boolean) source.readValue(null);
+        }
+
+        @Override
+        public void writeToParcel(Parcel dest, int flags) {
+            super.writeToParcel(dest, flags);
+            dest.writeValue(isIconified);
+        }
+
+        @Override
+        public String toString() {
+            return "SearchView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " isIconified=" + isIconified + "}";
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        SavedState ss = new SavedState(superState);
+        ss.isIconified = isIconified();
+        return ss;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        updateViewsVisibility(ss.isIconified);
+        requestLayout();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return SearchView.class.getName();
+    }
+
+    private void adjustDropDownSizeAndPosition() {
+        if (mDropDownAnchor.getWidth() > 1) {
+            Resources res = getContext().getResources();
+            int anchorPadding = mSearchPlate.getPaddingLeft();
+            Rect dropDownPadding = new Rect();
+            final boolean isLayoutRtl = isLayoutRtl();
+            int iconOffset = mIconifiedByDefault
+                    ? res.getDimensionPixelSize(R.dimen.dropdownitem_icon_width)
+                    + res.getDimensionPixelSize(R.dimen.dropdownitem_text_padding_left)
+                    : 0;
+            mSearchSrcTextView.getDropDownBackground().getPadding(dropDownPadding);
+            int offset;
+            if (isLayoutRtl) {
+                offset = - dropDownPadding.left;
+            } else {
+                offset = anchorPadding - (dropDownPadding.left + iconOffset);
+            }
+            mSearchSrcTextView.setDropDownHorizontalOffset(offset);
+            final int width = mDropDownAnchor.getWidth() + dropDownPadding.left
+                    + dropDownPadding.right + iconOffset - anchorPadding;
+            mSearchSrcTextView.setDropDownWidth(width);
+        }
+    }
+
+    private boolean onItemClicked(int position, int actionKey, String actionMsg) {
+        if (mOnSuggestionListener == null
+                || !mOnSuggestionListener.onSuggestionClick(position)) {
+            launchSuggestion(position, KeyEvent.KEYCODE_UNKNOWN, null);
+            mSearchSrcTextView.setImeVisibility(false);
+            dismissSuggestions();
+            return true;
+        }
+        return false;
+    }
+
+    private boolean onItemSelected(int position) {
+        if (mOnSuggestionListener == null
+                || !mOnSuggestionListener.onSuggestionSelect(position)) {
+            rewriteQueryFromSuggestion(position);
+            return true;
+        }
+        return false;
+    }
+
+    private final OnItemClickListener mOnItemClickListener = new OnItemClickListener() {
+
+        /**
+         * Implements OnItemClickListener
+         */
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            if (DBG) Log.d(LOG_TAG, "onItemClick() position " + position);
+            onItemClicked(position, KeyEvent.KEYCODE_UNKNOWN, null);
+        }
+    };
+
+    private final OnItemSelectedListener mOnItemSelectedListener = new OnItemSelectedListener() {
+
+        /**
+         * Implements OnItemSelectedListener
+         */
+        public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
+            if (DBG) Log.d(LOG_TAG, "onItemSelected() position " + position);
+            SearchView.this.onItemSelected(position);
+        }
+
+        /**
+         * Implements OnItemSelectedListener
+         */
+        public void onNothingSelected(AdapterView<?> parent) {
+            if (DBG)
+                Log.d(LOG_TAG, "onNothingSelected()");
+        }
+    };
+
+    /**
+     * Query rewriting.
+     */
+    private void rewriteQueryFromSuggestion(int position) {
+        CharSequence oldQuery = mSearchSrcTextView.getText();
+        Cursor c = mSuggestionsAdapter.getCursor();
+        if (c == null) {
+            return;
+        }
+        if (c.moveToPosition(position)) {
+            // Get the new query from the suggestion.
+            CharSequence newQuery = mSuggestionsAdapter.convertToString(c);
+            if (newQuery != null) {
+                // The suggestion rewrites the query.
+                // Update the text field, without getting new suggestions.
+                setQuery(newQuery);
+            } else {
+                // The suggestion does not rewrite the query, restore the user's query.
+                setQuery(oldQuery);
+            }
+        } else {
+            // We got a bad position, restore the user's query.
+            setQuery(oldQuery);
+        }
+    }
+
+    /**
+     * Launches an intent based on a suggestion.
+     *
+     * @param position The index of the suggestion to create the intent from.
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @return true if a successful launch, false if could not (e.g. bad position).
+     */
+    private boolean launchSuggestion(int position, int actionKey, String actionMsg) {
+        Cursor c = mSuggestionsAdapter.getCursor();
+        if ((c != null) && c.moveToPosition(position)) {
+
+            Intent intent = createIntentFromSuggestion(c, actionKey, actionMsg);
+
+            // launch the intent
+            launchIntent(intent);
+
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Launches an intent, including any special intent handling.
+     */
+    private void launchIntent(Intent intent) {
+        if (intent == null) {
+            return;
+        }
+        try {
+            // If the intent was created from a suggestion, it will always have an explicit
+            // component here.
+            getContext().startActivity(intent);
+        } catch (RuntimeException ex) {
+            Log.e(LOG_TAG, "Failed launch activity: " + intent, ex);
+        }
+    }
+
+    /**
+     * Sets the text in the query box, without updating the suggestions.
+     */
+    private void setQuery(CharSequence query) {
+        mSearchSrcTextView.setText(query, true);
+        // Move the cursor to the end
+        mSearchSrcTextView.setSelection(TextUtils.isEmpty(query) ? 0 : query.length());
+    }
+
+    private void launchQuerySearch(int actionKey, String actionMsg, String query) {
+        String action = Intent.ACTION_SEARCH;
+        Intent intent = createIntent(action, null, null, query, actionKey, actionMsg);
+        getContext().startActivity(intent);
+    }
+
+    /**
+     * Constructs an intent from the given information and the search dialog state.
+     *
+     * @param action Intent action.
+     * @param data Intent data, or <code>null</code>.
+     * @param extraData Data for {@link SearchManager#EXTRA_DATA_KEY} or <code>null</code>.
+     * @param query Intent query, or <code>null</code>.
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @param mode The search mode, one of the acceptable values for
+     *             {@link SearchManager#SEARCH_MODE}, or {@code null}.
+     * @return The intent.
+     */
+    private Intent createIntent(String action, Uri data, String extraData, String query,
+            int actionKey, String actionMsg) {
+        // Now build the Intent
+        Intent intent = new Intent(action);
+        intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        // We need CLEAR_TOP to avoid reusing an old task that has other activities
+        // on top of the one we want. We don't want to do this in in-app search though,
+        // as it can be destructive to the activity stack.
+        if (data != null) {
+            intent.setData(data);
+        }
+        intent.putExtra(SearchManager.USER_QUERY, mUserQuery);
+        if (query != null) {
+            intent.putExtra(SearchManager.QUERY, query);
+        }
+        if (extraData != null) {
+            intent.putExtra(SearchManager.EXTRA_DATA_KEY, extraData);
+        }
+        if (mAppSearchData != null) {
+            intent.putExtra(SearchManager.APP_DATA, mAppSearchData);
+        }
+        if (actionKey != KeyEvent.KEYCODE_UNKNOWN) {
+            intent.putExtra(SearchManager.ACTION_KEY, actionKey);
+            intent.putExtra(SearchManager.ACTION_MSG, actionMsg);
+        }
+        intent.setComponent(mSearchable.getSearchActivity());
+        return intent;
+    }
+
+    /**
+     * Create and return an Intent that can launch the voice search activity for web search.
+     */
+    private Intent createVoiceWebSearchIntent(Intent baseIntent, SearchableInfo searchable) {
+        Intent voiceIntent = new Intent(baseIntent);
+        ComponentName searchActivity = searchable.getSearchActivity();
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
+                : searchActivity.flattenToShortString());
+        return voiceIntent;
+    }
+
+    /**
+     * Create and return an Intent that can launch the voice search activity, perform a specific
+     * voice transcription, and forward the results to the searchable activity.
+     *
+     * @param baseIntent The voice app search intent to start from
+     * @return A completely-configured intent ready to send to the voice search activity
+     */
+    private Intent createVoiceAppSearchIntent(Intent baseIntent, SearchableInfo searchable) {
+        ComponentName searchActivity = searchable.getSearchActivity();
+
+        // create the necessary intent to set up a search-and-forward operation
+        // in the voice search system.   We have to keep the bundle separate,
+        // because it becomes immutable once it enters the PendingIntent
+        Intent queryIntent = new Intent(Intent.ACTION_SEARCH);
+        queryIntent.setComponent(searchActivity);
+        PendingIntent pending = PendingIntent.getActivity(getContext(), 0, queryIntent,
+                PendingIntent.FLAG_ONE_SHOT);
+
+        // Now set up the bundle that will be inserted into the pending intent
+        // when it's time to do the search.  We always build it here (even if empty)
+        // because the voice search activity will always need to insert "QUERY" into
+        // it anyway.
+        Bundle queryExtras = new Bundle();
+        if (mAppSearchData != null) {
+            queryExtras.putParcelable(SearchManager.APP_DATA, mAppSearchData);
+        }
+
+        // Now build the intent to launch the voice search.  Add all necessary
+        // extras to launch the voice recognizer, and then all the necessary extras
+        // to forward the results to the searchable activity
+        Intent voiceIntent = new Intent(baseIntent);
+
+        // Add all of the configuration options supplied by the searchable's metadata
+        String languageModel = RecognizerIntent.LANGUAGE_MODEL_FREE_FORM;
+        String prompt = null;
+        String language = null;
+        int maxResults = 1;
+
+        Resources resources = getResources();
+        if (searchable.getVoiceLanguageModeId() != 0) {
+            languageModel = resources.getString(searchable.getVoiceLanguageModeId());
+        }
+        if (searchable.getVoicePromptTextId() != 0) {
+            prompt = resources.getString(searchable.getVoicePromptTextId());
+        }
+        if (searchable.getVoiceLanguageId() != 0) {
+            language = resources.getString(searchable.getVoiceLanguageId());
+        }
+        if (searchable.getVoiceMaxResults() != 0) {
+            maxResults = searchable.getVoiceMaxResults();
+        }
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE_MODEL, languageModel);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_PROMPT, prompt);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_LANGUAGE, language);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_MAX_RESULTS, maxResults);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_CALLING_PACKAGE, searchActivity == null ? null
+                : searchActivity.flattenToShortString());
+
+        // Add the values that configure forwarding the results
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT, pending);
+        voiceIntent.putExtra(RecognizerIntent.EXTRA_RESULTS_PENDINGINTENT_BUNDLE, queryExtras);
+
+        return voiceIntent;
+    }
+
+    /**
+     * When a particular suggestion has been selected, perform the various lookups required
+     * to use the suggestion.  This includes checking the cursor for suggestion-specific data,
+     * and/or falling back to the XML for defaults;  It also creates REST style Uri data when
+     * the suggestion includes a data id.
+     *
+     * @param c The suggestions cursor, moved to the row of the user's selection
+     * @param actionKey The key code of the action key that was pressed,
+     *        or {@link KeyEvent#KEYCODE_UNKNOWN} if none.
+     * @param actionMsg The message for the action key that was pressed,
+     *        or <code>null</code> if none.
+     * @return An intent for the suggestion at the cursor's position.
+     */
+    private Intent createIntentFromSuggestion(Cursor c, int actionKey, String actionMsg) {
+        try {
+            // use specific action if supplied, or default action if supplied, or fixed default
+            String action = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_ACTION);
+
+            if (action == null) {
+                action = mSearchable.getSuggestIntentAction();
+            }
+            if (action == null) {
+                action = Intent.ACTION_SEARCH;
+            }
+
+            // use specific data if supplied, or default data if supplied
+            String data = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+            if (data == null) {
+                data = mSearchable.getSuggestIntentData();
+            }
+            // then, if an ID was provided, append it.
+            if (data != null) {
+                String id = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_DATA_ID);
+                if (id != null) {
+                    data = data + "/" + Uri.encode(id);
+                }
+            }
+            Uri dataUri = (data == null) ? null : Uri.parse(data);
+
+            String query = getColumnString(c, SearchManager.SUGGEST_COLUMN_QUERY);
+            String extraData = getColumnString(c, SearchManager.SUGGEST_COLUMN_INTENT_EXTRA_DATA);
+
+            return createIntent(action, dataUri, extraData, query, actionKey, actionMsg);
+        } catch (RuntimeException e ) {
+            int rowNum;
+            try {                       // be really paranoid now
+                rowNum = c.getPosition();
+            } catch (RuntimeException e2 ) {
+                rowNum = -1;
+            }
+            Log.w(LOG_TAG, "Search suggestions cursor at row " + rowNum +
+                            " returned exception.", e);
+            return null;
+        }
+    }
+
+    private void forceSuggestionQuery() {
+        mSearchSrcTextView.doBeforeTextChanged();
+        mSearchSrcTextView.doAfterTextChanged();
+    }
+
+    static boolean isLandscapeMode(Context context) {
+        return context.getResources().getConfiguration().orientation
+                == Configuration.ORIENTATION_LANDSCAPE;
+    }
+
+    /**
+     * Callback to watch the text field for empty/non-empty
+     */
+    private TextWatcher mTextWatcher = new TextWatcher() {
+
+        public void beforeTextChanged(CharSequence s, int start, int before, int after) { }
+
+        public void onTextChanged(CharSequence s, int start,
+                int before, int after) {
+            SearchView.this.onTextChanged(s);
+        }
+
+        public void afterTextChanged(Editable s) {
+        }
+    };
+
+    private static class UpdatableTouchDelegate extends TouchDelegate {
+        /**
+         * View that should receive forwarded touch events
+         */
+        private final View mDelegateView;
+
+        /**
+         * Bounds in local coordinates of the containing view that should be mapped to the delegate
+         * view. This rect is used for initial hit testing.
+         */
+        private final Rect mTargetBounds;
+
+        /**
+         * Bounds in local coordinates of the containing view that are actual bounds of the delegate
+         * view. This rect is used for event coordinate mapping.
+         */
+        private final Rect mActualBounds;
+
+        /**
+         * mTargetBounds inflated to include some slop. This rect is to track whether the motion events
+         * should be considered to be be within the delegate view.
+         */
+        private final Rect mSlopBounds;
+
+        private final int mSlop;
+
+        /**
+         * True if the delegate had been targeted on a down event (intersected mTargetBounds).
+         */
+        private boolean mDelegateTargeted;
+
+        public UpdatableTouchDelegate(Rect targetBounds, Rect actualBounds, View delegateView) {
+            super(targetBounds, delegateView);
+            mSlop = ViewConfiguration.get(delegateView.getContext()).getScaledTouchSlop();
+            mTargetBounds = new Rect();
+            mSlopBounds = new Rect();
+            mActualBounds = new Rect();
+            setBounds(targetBounds, actualBounds);
+            mDelegateView = delegateView;
+        }
+
+        public void setBounds(Rect desiredBounds, Rect actualBounds) {
+            mTargetBounds.set(desiredBounds);
+            mSlopBounds.set(desiredBounds);
+            mSlopBounds.inset(-mSlop, -mSlop);
+            mActualBounds.set(actualBounds);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent event) {
+            final int x = (int) event.getX();
+            final int y = (int) event.getY();
+            boolean sendToDelegate = false;
+            boolean hit = true;
+            boolean handled = false;
+
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_DOWN:
+                    if (mTargetBounds.contains(x, y)) {
+                        mDelegateTargeted = true;
+                        sendToDelegate = true;
+                    }
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_MOVE:
+                    sendToDelegate = mDelegateTargeted;
+                    if (sendToDelegate) {
+                        if (!mSlopBounds.contains(x, y)) {
+                            hit = false;
+                        }
+                    }
+                    break;
+                case MotionEvent.ACTION_CANCEL:
+                    sendToDelegate = mDelegateTargeted;
+                    mDelegateTargeted = false;
+                    break;
+            }
+            if (sendToDelegate) {
+                if (hit && !mActualBounds.contains(x, y)) {
+                    // Offset event coordinates to be in the center of the target view since we
+                    // are within the targetBounds, but not inside the actual bounds of
+                    // mDelegateView
+                    event.setLocation(mDelegateView.getWidth() / 2,
+                            mDelegateView.getHeight() / 2);
+                } else {
+                    // Offset event coordinates to the target view coordinates.
+                    event.setLocation(x - mActualBounds.left, y - mActualBounds.top);
+                }
+
+                handled = mDelegateView.dispatchTouchEvent(event);
+            }
+            return handled;
+        }
+    }
+
+    /**
+     * Local subclass for AutoCompleteTextView.
+     * @hide
+     */
+    public static class SearchAutoComplete extends AutoCompleteTextView {
+
+        private int mThreshold;
+        private SearchView mSearchView;
+
+        private boolean mHasPendingShowSoftInputRequest;
+        final Runnable mRunShowSoftInputIfNecessary = () -> showSoftInputIfNecessary();
+
+        public SearchAutoComplete(Context context) {
+            super(context);
+            mThreshold = getThreshold();
+        }
+
+        public SearchAutoComplete(Context context, AttributeSet attrs) {
+            super(context, attrs);
+            mThreshold = getThreshold();
+        }
+
+        public SearchAutoComplete(Context context, AttributeSet attrs, int defStyleAttrs) {
+            super(context, attrs, defStyleAttrs);
+            mThreshold = getThreshold();
+        }
+
+        public SearchAutoComplete(
+                Context context, AttributeSet attrs, int defStyleAttrs, int defStyleRes) {
+            super(context, attrs, defStyleAttrs, defStyleRes);
+            mThreshold = getThreshold();
+        }
+
+        @Override
+        protected void onFinishInflate() {
+            super.onFinishInflate();
+            DisplayMetrics metrics = getResources().getDisplayMetrics();
+            setMinWidth((int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP,
+                    getSearchViewTextMinWidthDp(), metrics));
+        }
+
+        void setSearchView(SearchView searchView) {
+            mSearchView = searchView;
+        }
+
+        @Override
+        public void setThreshold(int threshold) {
+            super.setThreshold(threshold);
+            mThreshold = threshold;
+        }
+
+        /**
+         * Returns true if the text field is empty, or contains only whitespace.
+         */
+        private boolean isEmpty() {
+            return TextUtils.getTrimmedLength(getText()) == 0;
+        }
+
+        /**
+         * We override this method to avoid replacing the query box text when a
+         * suggestion is clicked.
+         */
+        @Override
+        protected void replaceText(CharSequence text) {
+        }
+
+        /**
+         * We override this method to avoid an extra onItemClick being called on
+         * the drop-down's OnItemClickListener by
+         * {@link AutoCompleteTextView#onKeyUp(int, KeyEvent)} when an item is
+         * clicked with the trackball.
+         */
+        @Override
+        public void performCompletion() {
+        }
+
+        /**
+         * We override this method to be sure and show the soft keyboard if
+         * appropriate when the TextView has focus.
+         */
+        @Override
+        public void onWindowFocusChanged(boolean hasWindowFocus) {
+            super.onWindowFocusChanged(hasWindowFocus);
+
+            if (hasWindowFocus && mSearchView.hasFocus() && getVisibility() == VISIBLE) {
+                // Since InputMethodManager#onPostWindowFocus() will be called after this callback,
+                // it is a bit too early to call InputMethodManager#showSoftInput() here. We still
+                // need to wait until the system calls back onCreateInputConnection() to call
+                // InputMethodManager#showSoftInput().
+                mHasPendingShowSoftInputRequest = true;
+
+                // If in landscape mode, then make sure that the ime is in front of the dropdown.
+                if (isLandscapeMode(getContext())) {
+                    ensureImeVisible(true);
+                }
+            }
+        }
+
+        @Override
+        protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+            super.onFocusChanged(focused, direction, previouslyFocusedRect);
+            mSearchView.onTextFocusChanged();
+        }
+
+        /**
+         * We override this method so that we can allow a threshold of zero,
+         * which ACTV does not.
+         */
+        @Override
+        public boolean enoughToFilter() {
+            return mThreshold <= 0 || super.enoughToFilter();
+        }
+
+        @Override
+        public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                // special case for the back key, we do not even try to send it
+                // to the drop down list but instead, consume it immediately
+                if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+                    KeyEvent.DispatcherState state = getKeyDispatcherState();
+                    if (state != null) {
+                        state.startTracking(event, this);
+                    }
+                    return true;
+                } else if (event.getAction() == KeyEvent.ACTION_UP) {
+                    KeyEvent.DispatcherState state = getKeyDispatcherState();
+                    if (state != null) {
+                        state.handleUpEvent(event);
+                    }
+                    if (event.isTracking() && !event.isCanceled()) {
+                        mSearchView.clearFocus();
+                        setImeVisibility(false);
+                        return true;
+                    }
+                }
+            }
+            return super.onKeyPreIme(keyCode, event);
+        }
+
+        /**
+         * Get minimum width of the search view text entry area.
+         */
+        private int getSearchViewTextMinWidthDp() {
+            final Configuration configuration = getResources().getConfiguration();
+            final int width = configuration.screenWidthDp;
+            final int height = configuration.screenHeightDp;
+            final int orientation = configuration.orientation;
+            if (width >= 960 && height >= 720
+                    && orientation == Configuration.ORIENTATION_LANDSCAPE) {
+                return 256;
+            } else if (width >= 600 || (width >= 640 && height >= 480)) {
+                return 192;
+            };
+            return 160;
+        }
+
+        /**
+         * We override {@link View#onCreateInputConnection(EditorInfo)} as a signal to schedule a
+         * pending {@link InputMethodManager#showSoftInput(View, int)} request (if any).
+         */
+        @Override
+        public InputConnection onCreateInputConnection(EditorInfo editorInfo) {
+            final InputConnection ic = super.onCreateInputConnection(editorInfo);
+            if (mHasPendingShowSoftInputRequest) {
+                removeCallbacks(mRunShowSoftInputIfNecessary);
+                post(mRunShowSoftInputIfNecessary);
+            }
+            return ic;
+        }
+
+        private void showSoftInputIfNecessary() {
+            if (mHasPendingShowSoftInputRequest) {
+                final InputMethodManager imm =
+                        getContext().getSystemService(InputMethodManager.class);
+                imm.showSoftInput(this, 0);
+                mHasPendingShowSoftInputRequest = false;
+            }
+        }
+
+        private void setImeVisibility(final boolean visible) {
+            final InputMethodManager imm = getContext().getSystemService(InputMethodManager.class);
+            if (!visible) {
+                mHasPendingShowSoftInputRequest = false;
+                removeCallbacks(mRunShowSoftInputIfNecessary);
+                imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                return;
+            }
+
+            if (imm.isActive(this)) {
+                // This means that SearchAutoComplete is already connected to the IME.
+                // InputMethodManager#showSoftInput() is guaranteed to pass client-side focus check.
+                mHasPendingShowSoftInputRequest = false;
+                removeCallbacks(mRunShowSoftInputIfNecessary);
+                imm.showSoftInput(this, 0);
+                return;
+            }
+
+            // Otherwise, InputMethodManager#showSoftInput() should be deferred after
+            // onCreateInputConnection().
+            mHasPendingShowSoftInputRequest = true;
+        }
+    }
+}
diff --git a/android/widget/SectionIndexer.java b/android/widget/SectionIndexer.java
new file mode 100644
index 0000000..f6333d1
--- /dev/null
+++ b/android/widget/SectionIndexer.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+/**
+ * Interface that may implemented on {@link Adapter}s to enable fast scrolling
+ * between sections of an {@link AbsListView}.
+ * <p>
+ * A section is a group of list items that have something in common. For
+ * example, they may begin with the same letter or they may be songs from the
+ * same artist.
+ * <p>
+ * {@link ExpandableListAdapter}s that consider groups and sections as
+ * synonymous should account for collapsed groups and return an appropriate
+ * section/position.
+ *
+ * @see AbsListView#setFastScrollEnabled(boolean)
+ */
+public interface SectionIndexer {
+    /**
+     * Returns an array of objects representing sections of the list. The
+     * returned array and its contents should be non-null.
+     * <p>
+     * The list view will call toString() on the objects to get the preview text
+     * to display while scrolling. For example, an adapter may return an array
+     * of Strings representing letters of the alphabet. Or, it may return an
+     * array of objects whose toString() methods return their section titles.
+     *
+     * @return the array of section objects
+     */
+    Object[] getSections();
+
+    /**
+     * Given the index of a section within the array of section objects, returns
+     * the starting position of that section within the adapter.
+     * <p>
+     * If the section's starting position is outside of the adapter bounds, the
+     * position must be clipped to fall within the size of the adapter.
+     *
+     * @param sectionIndex the index of the section within the array of section
+     *            objects
+     * @return the starting position of that section within the adapter,
+     *         constrained to fall within the adapter bounds
+     */
+    int getPositionForSection(int sectionIndex);
+
+    /**
+     * Given a position within the adapter, returns the index of the
+     * corresponding section within the array of section objects.
+     * <p>
+     * If the section index is outside of the section array bounds, the index
+     * must be clipped to fall within the size of the section array.
+     * <p>
+     * For example, consider an indexer where the section at array index 0
+     * starts at adapter position 100. Calling this method with position 10,
+     * which is before the first section, must return index 0.
+     *
+     * @param position the position within the adapter for which to return the
+     *            corresponding section index
+     * @return the index of the corresponding section within the array of
+     *         section objects, constrained to fall within the array bounds
+     */
+    int getSectionForPosition(int position);
+}
diff --git a/android/widget/SeekBar.java b/android/widget/SeekBar.java
new file mode 100644
index 0000000..f9aced0
--- /dev/null
+++ b/android/widget/SeekBar.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+
+/**
+ * A SeekBar is an extension of ProgressBar that adds a draggable thumb. The user can touch
+ * the thumb and drag left or right to set the current progress level or use the arrow keys.
+ * Placing focusable widgets to the left or right of a SeekBar is discouraged.
+ * <p>
+ * Clients of the SeekBar can attach a {@link SeekBar.OnSeekBarChangeListener} to
+ * be notified of the user's actions.
+ *
+ * @attr ref android.R.styleable#SeekBar_thumb
+ */
+public class SeekBar extends AbsSeekBar {
+
+    /**
+     * A callback that notifies clients when the progress level has been
+     * changed. This includes changes that were initiated by the user through a
+     * touch gesture or arrow key/trackball as well as changes that were initiated
+     * programmatically.
+     */
+    public interface OnSeekBarChangeListener {
+
+        /**
+         * Notification that the progress level has changed. Clients can use the fromUser parameter
+         * to distinguish user-initiated changes from those that occurred programmatically.
+         *
+         * @param seekBar The SeekBar whose progress has changed
+         * @param progress The current progress level. This will be in the range min..max where min
+         *                 and max were set by {@link ProgressBar#setMin(int)} and
+         *                 {@link ProgressBar#setMax(int)}, respectively. (The default values for
+         *                 min is 0 and max is 100.)
+         * @param fromUser True if the progress change was initiated by the user.
+         */
+        void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser);
+
+        /**
+         * Notification that the user has started a touch gesture. Clients may want to use this
+         * to disable advancing the seekbar.
+         * @param seekBar The SeekBar in which the touch gesture began
+         */
+        void onStartTrackingTouch(SeekBar seekBar);
+
+        /**
+         * Notification that the user has finished a touch gesture. Clients may want to use this
+         * to re-enable advancing the seekbar.
+         * @param seekBar The SeekBar in which the touch gesture began
+         */
+        void onStopTrackingTouch(SeekBar seekBar);
+    }
+
+    private OnSeekBarChangeListener mOnSeekBarChangeListener;
+
+    public SeekBar(Context context) {
+        this(context, null);
+    }
+
+    public SeekBar(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.seekBarStyle);
+    }
+
+    public SeekBar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SeekBar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+    }
+
+    @Override
+    void onProgressRefresh(float scale, boolean fromUser, int progress) {
+        super.onProgressRefresh(scale, fromUser, progress);
+
+        if (mOnSeekBarChangeListener != null) {
+            mOnSeekBarChangeListener.onProgressChanged(this, progress, fromUser);
+        }
+    }
+
+    /**
+     * Sets a listener to receive notifications of changes to the SeekBar's progress level. Also
+     * provides notifications of when the user starts and stops a touch gesture within the SeekBar.
+     *
+     * @param l The seek bar notification listener
+     *
+     * @see SeekBar.OnSeekBarChangeListener
+     */
+    public void setOnSeekBarChangeListener(OnSeekBarChangeListener l) {
+        mOnSeekBarChangeListener = l;
+    }
+
+    @Override
+    void onStartTrackingTouch() {
+        super.onStartTrackingTouch();
+        if (mOnSeekBarChangeListener != null) {
+            mOnSeekBarChangeListener.onStartTrackingTouch(this);
+        }
+    }
+
+    @Override
+    void onStopTrackingTouch() {
+        super.onStopTrackingTouch();
+        if (mOnSeekBarChangeListener != null) {
+            mOnSeekBarChangeListener.onStopTrackingTouch(this);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return SeekBar.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (canUserSetProgress()) {
+            info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_PROGRESS);
+        }
+    }
+}
diff --git a/android/widget/SelectionActionModeHelper.java b/android/widget/SelectionActionModeHelper.java
new file mode 100644
index 0000000..36dc330
--- /dev/null
+++ b/android/widget/SelectionActionModeHelper.java
@@ -0,0 +1,857 @@
+/*
+ * Copyright (C) 2017 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.UiThread;
+import android.annotation.WorkerThread;
+import android.graphics.Canvas;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.os.AsyncTask;
+import android.os.LocaleList;
+import android.text.Layout;
+import android.text.Selection;
+import android.text.Spannable;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.textclassifier.TextClassification;
+import android.view.textclassifier.TextClassifier;
+import android.view.textclassifier.TextSelection;
+import android.view.textclassifier.logging.SmartSelectionEventTracker;
+import android.view.textclassifier.logging.SmartSelectionEventTracker.SelectionEvent;
+import android.widget.Editor.SelectionModifierCursorController;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.util.Preconditions;
+
+import java.text.BreakIterator;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Objects;
+import java.util.function.Consumer;
+import java.util.function.Supplier;
+import java.util.regex.Pattern;
+
+/**
+ * Helper class for starting selection action mode
+ * (synchronously without the TextClassifier, asynchronously with the TextClassifier).
+ * @hide
+ */
+@UiThread
+@VisibleForTesting(visibility = VisibleForTesting.Visibility.PACKAGE)
+public final class SelectionActionModeHelper {
+
+    /**
+     * Maximum time (in milliseconds) to wait for a result before timing out.
+     */
+    // TODO: Consider making this a ViewConfiguration.
+    private static final int TIMEOUT_DURATION = 200;
+
+    private static final boolean SMART_SELECT_ANIMATION_ENABLED = true;
+
+    private final Editor mEditor;
+    private final TextView mTextView;
+    private final TextClassificationHelper mTextClassificationHelper;
+
+    private TextClassification mTextClassification;
+    private AsyncTask mTextClassificationAsyncTask;
+
+    private final SelectionTracker mSelectionTracker;
+
+    // TODO remove nullable marker once the switch gating the feature gets removed
+    @Nullable
+    private final SmartSelectSprite mSmartSelectSprite;
+
+    SelectionActionModeHelper(@NonNull Editor editor) {
+        mEditor = Preconditions.checkNotNull(editor);
+        mTextView = mEditor.getTextView();
+        mTextClassificationHelper = new TextClassificationHelper(
+                mTextView.getTextClassifier(), mTextView.getText(),
+                0, 1, mTextView.getTextLocales());
+        mSelectionTracker = new SelectionTracker(mTextView);
+
+        if (SMART_SELECT_ANIMATION_ENABLED) {
+            mSmartSelectSprite = new SmartSelectSprite(mTextView.getContext(),
+                    mTextView::invalidate);
+        } else {
+            mSmartSelectSprite = null;
+        }
+    }
+
+    public void startActionModeAsync(boolean adjustSelection) {
+        mSelectionTracker.onOriginalSelection(
+                mTextView.getText(),
+                mTextView.getSelectionStart(),
+                mTextView.getSelectionEnd(),
+                mTextView.isTextEditable());
+        cancelAsyncTask();
+        if (skipTextClassification()) {
+            startActionMode(null);
+        } else {
+            resetTextClassificationHelper();
+            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+                    mTextView,
+                    TIMEOUT_DURATION,
+                    adjustSelection
+                            ? mTextClassificationHelper::suggestSelection
+                            : mTextClassificationHelper::classifyText,
+                    mSmartSelectSprite != null
+                            ? this::startActionModeWithSmartSelectAnimation
+                            : this::startActionMode)
+                    .execute();
+        }
+    }
+
+    public void invalidateActionModeAsync() {
+        cancelAsyncTask();
+        if (skipTextClassification()) {
+            invalidateActionMode(null);
+        } else {
+            resetTextClassificationHelper();
+            mTextClassificationAsyncTask = new TextClassificationAsyncTask(
+                    mTextView,
+                    TIMEOUT_DURATION,
+                    mTextClassificationHelper::classifyText,
+                    this::invalidateActionMode)
+                    .execute();
+        }
+    }
+
+    public void onSelectionAction(int menuItemId) {
+        mSelectionTracker.onSelectionAction(
+                mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+                getActionType(menuItemId), mTextClassification);
+    }
+
+    public void onSelectionDrag() {
+        mSelectionTracker.onSelectionAction(
+                mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+                SelectionEvent.ActionType.DRAG, mTextClassification);
+    }
+
+    public void onTextChanged(int start, int end) {
+        mSelectionTracker.onTextChanged(start, end, mTextClassification);
+    }
+
+    public boolean resetSelection(int textIndex) {
+        if (mSelectionTracker.resetSelection(textIndex, mEditor)) {
+            invalidateActionModeAsync();
+            return true;
+        }
+        return false;
+    }
+
+    @Nullable
+    public TextClassification getTextClassification() {
+        return mTextClassification;
+    }
+
+    public void onDestroyActionMode() {
+        cancelSmartSelectAnimation();
+        mSelectionTracker.onSelectionDestroyed();
+        cancelAsyncTask();
+    }
+
+    public void onDraw(final Canvas canvas) {
+        if (mSmartSelectSprite != null) {
+            mSmartSelectSprite.draw(canvas);
+        }
+    }
+
+    private void cancelAsyncTask() {
+        if (mTextClassificationAsyncTask != null) {
+            mTextClassificationAsyncTask.cancel(true);
+            mTextClassificationAsyncTask = null;
+        }
+        mTextClassification = null;
+    }
+
+    private boolean skipTextClassification() {
+        // No need to make an async call for a no-op TextClassifier.
+        final boolean noOpTextClassifier = mTextView.getTextClassifier() == TextClassifier.NO_OP;
+        // Do not call the TextClassifier if there is no selection.
+        final boolean noSelection = mTextView.getSelectionEnd() == mTextView.getSelectionStart();
+        // Do not call the TextClassifier if this is a password field.
+        final boolean password = mTextView.hasPasswordTransformationMethod()
+                || TextView.isPasswordInputType(mTextView.getInputType());
+        return noOpTextClassifier || noSelection || password;
+    }
+
+    private void startActionMode(@Nullable SelectionResult result) {
+        final CharSequence text = mTextView.getText();
+        if (result != null && text instanceof Spannable) {
+            Selection.setSelection((Spannable) text, result.mStart, result.mEnd);
+            mTextClassification = result.mClassification;
+        } else {
+            mTextClassification = null;
+        }
+        if (mEditor.startSelectionActionModeInternal()) {
+            final SelectionModifierCursorController controller = mEditor.getSelectionController();
+            if (controller != null) {
+                controller.show();
+            }
+            if (result != null) {
+                mSelectionTracker.onSmartSelection(result);
+            }
+        }
+        mEditor.setRestartActionModeOnNextRefresh(false);
+        mTextClassificationAsyncTask = null;
+    }
+
+    private void startActionModeWithSmartSelectAnimation(@Nullable SelectionResult result) {
+        final Layout layout = mTextView.getLayout();
+
+        final Runnable onAnimationEndCallback = () -> startActionMode(result);
+        // TODO do not trigger the animation if the change included only non-printable characters
+        final boolean didSelectionChange =
+                result != null && (mTextView.getSelectionStart() != result.mStart
+                        || mTextView.getSelectionEnd() != result.mEnd);
+
+        if (!didSelectionChange) {
+            onAnimationEndCallback.run();
+            return;
+        }
+
+        final List<RectF> selectionRectangles =
+                convertSelectionToRectangles(layout, result.mStart, result.mEnd);
+
+        final PointF touchPoint = new PointF(
+                mEditor.getLastUpPositionX(),
+                mEditor.getLastUpPositionY());
+
+        final PointF animationStartPoint =
+                movePointInsideNearestRectangle(touchPoint, selectionRectangles);
+
+        mSmartSelectSprite.startAnimation(
+                animationStartPoint,
+                selectionRectangles,
+                onAnimationEndCallback);
+    }
+
+    private List<RectF> convertSelectionToRectangles(final Layout layout, final int start,
+            final int end) {
+        final List<RectF> result = new ArrayList<>();
+        layout.getSelection(start, end, (left, top, right, bottom, textSelectionLayout) ->
+                mergeRectangleIntoList(result, new RectF(left, top, right, bottom)));
+
+        result.sort(SmartSelectSprite.RECTANGLE_COMPARATOR);
+        return result;
+    }
+
+    /**
+     * Merges a {@link RectF} into an existing list of rectangles. While merging, this method
+     * makes sure that:
+     *
+     * <ol>
+     * <li>No rectangle is redundant (contained within a bigger rectangle)</li>
+     * <li>Rectangles of the same height and vertical position that intersect get merged</li>
+     * </ol>
+     *
+     * @param list      the list of rectangles to merge the new rectangle in
+     * @param candidate the {@link RectF} to merge into the list
+     * @hide
+     */
+    @VisibleForTesting
+    public static void mergeRectangleIntoList(List<RectF> list, RectF candidate) {
+        if (candidate.isEmpty()) {
+            return;
+        }
+
+        final int elementCount = list.size();
+        for (int index = 0; index < elementCount; ++index) {
+            final RectF existingRectangle = list.get(index);
+            if (existingRectangle.contains(candidate)) {
+                return;
+            }
+            if (candidate.contains(existingRectangle)) {
+                existingRectangle.setEmpty();
+                continue;
+            }
+
+            final boolean rectanglesContinueEachOther = candidate.left == existingRectangle.right
+                    || candidate.right == existingRectangle.left;
+            final boolean canMerge = candidate.top == existingRectangle.top
+                    && candidate.bottom == existingRectangle.bottom
+                    && (RectF.intersects(candidate, existingRectangle)
+                    || rectanglesContinueEachOther);
+
+            if (canMerge) {
+                candidate.union(existingRectangle);
+                existingRectangle.setEmpty();
+            }
+        }
+
+        for (int index = elementCount - 1; index >= 0; --index) {
+            if (list.get(index).isEmpty()) {
+                list.remove(index);
+            }
+        }
+
+        list.add(candidate);
+    }
+
+
+    /** @hide */
+    @VisibleForTesting
+    public static PointF movePointInsideNearestRectangle(final PointF point,
+            final List<RectF> rectangles) {
+        float bestX = -1;
+        float bestY = -1;
+        double bestDistance = Double.MAX_VALUE;
+
+        final int elementCount = rectangles.size();
+        for (int index = 0; index < elementCount; ++index) {
+            final RectF rectangle = rectangles.get(index);
+            final float candidateY = rectangle.centerY();
+            final float candidateX;
+
+            if (point.x > rectangle.right) {
+                candidateX = rectangle.right;
+            } else if (point.x < rectangle.left) {
+                candidateX = rectangle.left;
+            } else {
+                candidateX = point.x;
+            }
+
+            final double candidateDistance = Math.pow(point.x - candidateX, 2)
+                    + Math.pow(point.y - candidateY, 2);
+
+            if (candidateDistance < bestDistance) {
+                bestX = candidateX;
+                bestY = candidateY;
+                bestDistance = candidateDistance;
+            }
+        }
+
+        return new PointF(bestX, bestY);
+    }
+
+    private void invalidateActionMode(@Nullable SelectionResult result) {
+        cancelSmartSelectAnimation();
+        mTextClassification = result != null ? result.mClassification : null;
+        final ActionMode actionMode = mEditor.getTextActionMode();
+        if (actionMode != null) {
+            actionMode.invalidate();
+        }
+        mSelectionTracker.onSelectionUpdated(
+                mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mTextClassification);
+        mTextClassificationAsyncTask = null;
+    }
+
+    private void resetTextClassificationHelper() {
+        mTextClassificationHelper.reset(mTextView.getTextClassifier(), mTextView.getText(),
+                mTextView.getSelectionStart(), mTextView.getSelectionEnd(),
+                mTextView.getTextLocales());
+    }
+
+    private void cancelSmartSelectAnimation() {
+        if (mSmartSelectSprite != null) {
+            mSmartSelectSprite.cancelAnimation();
+        }
+    }
+
+    /**
+     * Tracks and logs smart selection changes.
+     * It is important to trigger this object's methods at the appropriate event so that it tracks
+     * smart selection events appropriately.
+     */
+    private static final class SelectionTracker {
+
+        private final TextView mTextView;
+        private SelectionMetricsLogger mLogger;
+
+        private int mOriginalStart;
+        private int mOriginalEnd;
+        private int mSelectionStart;
+        private int mSelectionEnd;
+        private boolean mAllowReset;
+
+        SelectionTracker(TextView textView) {
+            mTextView = Preconditions.checkNotNull(textView);
+            mLogger = new SelectionMetricsLogger(textView);
+        }
+
+        /**
+         * Called when the original selection happens, before smart selection is triggered.
+         */
+        public void onOriginalSelection(
+                CharSequence text, int selectionStart, int selectionEnd, boolean editableText) {
+            mOriginalStart = mSelectionStart = selectionStart;
+            mOriginalEnd = mSelectionEnd = selectionEnd;
+            mAllowReset = false;
+            maybeInvalidateLogger();
+            mLogger.logSelectionStarted(text, selectionStart);
+        }
+
+        /**
+         * Called when selection action mode is started and the results come from a classifier.
+         */
+        public void onSmartSelection(SelectionResult result) {
+            if (isSelectionStarted()) {
+                mSelectionStart = result.mStart;
+                mSelectionEnd = result.mEnd;
+                mAllowReset = mSelectionStart != mOriginalStart || mSelectionEnd != mOriginalEnd;
+                mLogger.logSelectionModified(
+                        result.mStart, result.mEnd, result.mClassification, result.mSelection);
+            }
+        }
+
+        /**
+         * Called when selection bounds change.
+         */
+        public void onSelectionUpdated(
+                int selectionStart, int selectionEnd,
+                @Nullable TextClassification classification) {
+            if (isSelectionStarted()) {
+                mSelectionStart = selectionStart;
+                mSelectionEnd = selectionEnd;
+                mAllowReset = false;
+                mLogger.logSelectionModified(selectionStart, selectionEnd, classification, null);
+            }
+        }
+
+        /**
+         * Called when the selection action mode is destroyed.
+         */
+        public void onSelectionDestroyed() {
+            mAllowReset = false;
+            // Wait a few ms to see if the selection was destroyed because of a text change event.
+            mTextView.postDelayed(() -> {
+                mLogger.logSelectionAction(
+                        mSelectionStart, mSelectionEnd,
+                        SelectionEvent.ActionType.ABANDON, null /* classification */);
+                mSelectionStart = mSelectionEnd = -1;
+            }, 100 /* ms */);
+        }
+
+        /**
+         * Called when an action is taken on a smart selection.
+         */
+        public void onSelectionAction(
+                int selectionStart, int selectionEnd,
+                @SelectionEvent.ActionType int action,
+                @Nullable TextClassification classification) {
+            if (isSelectionStarted()) {
+                mAllowReset = false;
+                mLogger.logSelectionAction(selectionStart, selectionEnd, action, classification);
+            }
+        }
+
+        /**
+         * Returns true if the current smart selection should be reset to normal selection based on
+         * information that has been recorded about the original selection and the smart selection.
+         * The expected UX here is to allow the user to select a word inside of the smart selection
+         * on a single tap.
+         */
+        public boolean resetSelection(int textIndex, Editor editor) {
+            final TextView textView = editor.getTextView();
+            if (isSelectionStarted()
+                    && mAllowReset
+                    && textIndex >= mSelectionStart && textIndex <= mSelectionEnd
+                    && textView.getText() instanceof Spannable) {
+                mAllowReset = false;
+                boolean selected = editor.selectCurrentWord();
+                if (selected) {
+                    mSelectionStart = editor.getTextView().getSelectionStart();
+                    mSelectionEnd = editor.getTextView().getSelectionEnd();
+                    mLogger.logSelectionAction(
+                            textView.getSelectionStart(), textView.getSelectionEnd(),
+                            SelectionEvent.ActionType.RESET, null /* classification */);
+                }
+                return selected;
+            }
+            return false;
+        }
+
+        public void onTextChanged(int start, int end, TextClassification classification) {
+            if (isSelectionStarted() && start == mSelectionStart && end == mSelectionEnd) {
+                onSelectionAction(start, end, SelectionEvent.ActionType.OVERTYPE, classification);
+            }
+        }
+
+        private void maybeInvalidateLogger() {
+            if (mLogger.isEditTextLogger() != mTextView.isTextEditable()) {
+                mLogger = new SelectionMetricsLogger(mTextView);
+            }
+        }
+
+        private boolean isSelectionStarted() {
+            return mSelectionStart >= 0 && mSelectionEnd >= 0 && mSelectionStart != mSelectionEnd;
+        }
+    }
+
+    // TODO: Write tests
+    /**
+     * Metrics logging helper.
+     *
+     * This logger logs selection by word indices. The initial (start) single word selection is
+     * logged at [0, 1) -- end index is exclusive. Other word indices are logged relative to the
+     * initial single word selection.
+     * e.g. New York city, NY. Suppose the initial selection is "York" in
+     * "New York city, NY", then "York" is at [0, 1), "New" is at [-1, 0], and "city" is at [1, 2).
+     * "New York" is at [-1, 1).
+     * Part selection of a word e.g. "or" is counted as selecting the
+     * entire word i.e. equivalent to "York", and each special character is counted as a word, e.g.
+     * "," is at [2, 3). Whitespaces are ignored.
+     */
+    private static final class SelectionMetricsLogger {
+
+        private static final String LOG_TAG = "SelectionMetricsLogger";
+        private static final Pattern PATTERN_WHITESPACE = Pattern.compile("\\s+");
+
+        private final SmartSelectionEventTracker mDelegate;
+        private final boolean mEditTextLogger;
+        private final BreakIterator mWordIterator;
+        private int mStartIndex;
+        private String mText;
+
+        SelectionMetricsLogger(TextView textView) {
+            Preconditions.checkNotNull(textView);
+            final @SmartSelectionEventTracker.WidgetType int widgetType = textView.isTextEditable()
+                    ? SmartSelectionEventTracker.WidgetType.EDITTEXT
+                    : SmartSelectionEventTracker.WidgetType.TEXTVIEW;
+            mDelegate = new SmartSelectionEventTracker(textView.getContext(), widgetType);
+            mEditTextLogger = textView.isTextEditable();
+            mWordIterator = BreakIterator.getWordInstance(textView.getTextLocale());
+        }
+
+        public void logSelectionStarted(CharSequence text, int index) {
+            try {
+                Preconditions.checkNotNull(text);
+                Preconditions.checkArgumentInRange(index, 0, text.length(), "index");
+                if (mText == null || !mText.contentEquals(text)) {
+                    mText = text.toString();
+                }
+                mWordIterator.setText(mText);
+                mStartIndex = index;
+                mDelegate.logEvent(SelectionEvent.selectionStarted(0));
+            } catch (Exception e) {
+                // Avoid crashes due to logging.
+                Log.d(LOG_TAG, e.getMessage());
+            }
+        }
+
+        public void logSelectionModified(int start, int end,
+                @Nullable TextClassification classification, @Nullable TextSelection selection) {
+            try {
+                Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+                Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
+                int[] wordIndices = getWordDelta(start, end);
+                if (selection != null) {
+                    mDelegate.logEvent(SelectionEvent.selectionModified(
+                            wordIndices[0], wordIndices[1], selection));
+                } else if (classification != null) {
+                    mDelegate.logEvent(SelectionEvent.selectionModified(
+                            wordIndices[0], wordIndices[1], classification));
+                } else {
+                    mDelegate.logEvent(SelectionEvent.selectionModified(
+                            wordIndices[0], wordIndices[1]));
+                }
+            } catch (Exception e) {
+                // Avoid crashes due to logging.
+                Log.d(LOG_TAG, e.getMessage());
+            }
+        }
+
+        public void logSelectionAction(
+                int start, int end,
+                @SelectionEvent.ActionType int action,
+                @Nullable TextClassification classification) {
+            try {
+                Preconditions.checkArgumentInRange(start, 0, mText.length(), "start");
+                Preconditions.checkArgumentInRange(end, start, mText.length(), "end");
+                int[] wordIndices = getWordDelta(start, end);
+                if (classification != null) {
+                    mDelegate.logEvent(SelectionEvent.selectionAction(
+                            wordIndices[0], wordIndices[1], action, classification));
+                } else {
+                    mDelegate.logEvent(SelectionEvent.selectionAction(
+                            wordIndices[0], wordIndices[1], action));
+                }
+            } catch (Exception e) {
+                // Avoid crashes due to logging.
+                Log.d(LOG_TAG, e.getMessage());
+            }
+        }
+
+        public boolean isEditTextLogger() {
+            return mEditTextLogger;
+        }
+
+        private int[] getWordDelta(int start, int end) {
+            int[] wordIndices = new int[2];
+
+            if (start == mStartIndex) {
+                wordIndices[0] = 0;
+            } else if (start < mStartIndex) {
+                wordIndices[0] = -countWordsForward(start);
+            } else {  // start > mStartIndex
+                wordIndices[0] = countWordsBackward(start);
+
+                // For the selection start index, avoid counting a partial word backwards.
+                if (!mWordIterator.isBoundary(start)
+                        && !isWhitespace(
+                        mWordIterator.preceding(start),
+                        mWordIterator.following(start))) {
+                    // We counted a partial word. Remove it.
+                    wordIndices[0]--;
+                }
+            }
+
+            if (end == mStartIndex) {
+                wordIndices[1] = 0;
+            } else if (end < mStartIndex) {
+                wordIndices[1] = -countWordsForward(end);
+            } else {  // end > mStartIndex
+                wordIndices[1] = countWordsBackward(end);
+            }
+
+            return wordIndices;
+        }
+
+        private int countWordsBackward(int from) {
+            Preconditions.checkArgument(from >= mStartIndex);
+            int wordCount = 0;
+            int offset = from;
+            while (offset > mStartIndex) {
+                int start = mWordIterator.preceding(offset);
+                if (!isWhitespace(start, offset)) {
+                    wordCount++;
+                }
+                offset = start;
+            }
+            return wordCount;
+        }
+
+        private int countWordsForward(int from) {
+            Preconditions.checkArgument(from <= mStartIndex);
+            int wordCount = 0;
+            int offset = from;
+            while (offset < mStartIndex) {
+                int end = mWordIterator.following(offset);
+                if (!isWhitespace(offset, end)) {
+                    wordCount++;
+                }
+                offset = end;
+            }
+            return wordCount;
+        }
+
+        private boolean isWhitespace(int start, int end) {
+            return PATTERN_WHITESPACE.matcher(mText.substring(start, end)).matches();
+        }
+    }
+
+    /**
+     * AsyncTask for running a query on a background thread and returning the result on the
+     * UiThread. The AsyncTask times out after a specified time, returning a null result if the
+     * query has not yet returned.
+     */
+    private static final class TextClassificationAsyncTask
+            extends AsyncTask<Void, Void, SelectionResult> {
+
+        private final int mTimeOutDuration;
+        private final Supplier<SelectionResult> mSelectionResultSupplier;
+        private final Consumer<SelectionResult> mSelectionResultCallback;
+        private final TextView mTextView;
+        private final String mOriginalText;
+
+        /**
+         * @param textView the TextView
+         * @param timeOut time in milliseconds to timeout the query if it has not completed
+         * @param selectionResultSupplier fetches the selection results. Runs on a background thread
+         * @param selectionResultCallback receives the selection results. Runs on the UiThread
+         */
+        TextClassificationAsyncTask(
+                @NonNull TextView textView, int timeOut,
+                @NonNull Supplier<SelectionResult> selectionResultSupplier,
+                @NonNull Consumer<SelectionResult> selectionResultCallback) {
+            super(textView != null ? textView.getHandler() : null);
+            mTextView = Preconditions.checkNotNull(textView);
+            mTimeOutDuration = timeOut;
+            mSelectionResultSupplier = Preconditions.checkNotNull(selectionResultSupplier);
+            mSelectionResultCallback = Preconditions.checkNotNull(selectionResultCallback);
+            // Make a copy of the original text.
+            mOriginalText = mTextView.getText().toString();
+        }
+
+        @Override
+        @WorkerThread
+        protected SelectionResult doInBackground(Void... params) {
+            final Runnable onTimeOut = this::onTimeOut;
+            mTextView.postDelayed(onTimeOut, mTimeOutDuration);
+            final SelectionResult result = mSelectionResultSupplier.get();
+            mTextView.removeCallbacks(onTimeOut);
+            return result;
+        }
+
+        @Override
+        @UiThread
+        protected void onPostExecute(SelectionResult result) {
+            result = TextUtils.equals(mOriginalText, mTextView.getText()) ? result : null;
+            mSelectionResultCallback.accept(result);
+        }
+
+        private void onTimeOut() {
+            if (getStatus() == Status.RUNNING) {
+                onPostExecute(null);
+            }
+            cancel(true);
+        }
+    }
+
+    /**
+     * Helper class for querying the TextClassifier.
+     * It trims text so that only text necessary to provide context of the selected text is
+     * sent to the TextClassifier.
+     */
+    private static final class TextClassificationHelper {
+
+        private static final int TRIM_DELTA = 120;  // characters
+
+        private TextClassifier mTextClassifier;
+
+        /** The original TextView text. **/
+        private String mText;
+        /** Start index relative to mText. */
+        private int mSelectionStart;
+        /** End index relative to mText. */
+        private int mSelectionEnd;
+        private LocaleList mLocales;
+
+        /** Trimmed text starting from mTrimStart in mText. */
+        private CharSequence mTrimmedText;
+        /** Index indicating the start of mTrimmedText in mText. */
+        private int mTrimStart;
+        /** Start index relative to mTrimmedText */
+        private int mRelativeStart;
+        /** End index relative to mTrimmedText */
+        private int mRelativeEnd;
+
+        /** Information about the last classified text to avoid re-running a query. */
+        private CharSequence mLastClassificationText;
+        private int mLastClassificationSelectionStart;
+        private int mLastClassificationSelectionEnd;
+        private LocaleList mLastClassificationLocales;
+        private SelectionResult mLastClassificationResult;
+
+        TextClassificationHelper(TextClassifier textClassifier,
+                CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
+            reset(textClassifier, text, selectionStart, selectionEnd, locales);
+        }
+
+        @UiThread
+        public void reset(TextClassifier textClassifier,
+                CharSequence text, int selectionStart, int selectionEnd, LocaleList locales) {
+            mTextClassifier = Preconditions.checkNotNull(textClassifier);
+            mText = Preconditions.checkNotNull(text).toString();
+            mLastClassificationText = null; // invalidate.
+            Preconditions.checkArgument(selectionEnd > selectionStart);
+            mSelectionStart = selectionStart;
+            mSelectionEnd = selectionEnd;
+            mLocales = locales;
+        }
+
+        @WorkerThread
+        public SelectionResult classifyText() {
+            return performClassification(null /* selection */);
+        }
+
+        @WorkerThread
+        public SelectionResult suggestSelection() {
+            trimText();
+            final TextSelection selection = mTextClassifier.suggestSelection(
+                    mTrimmedText, mRelativeStart, mRelativeEnd, mLocales);
+            mSelectionStart = Math.max(0, selection.getSelectionStartIndex() + mTrimStart);
+            mSelectionEnd = Math.min(mText.length(), selection.getSelectionEndIndex() + mTrimStart);
+            return performClassification(selection);
+        }
+
+        private SelectionResult performClassification(@Nullable TextSelection selection) {
+            if (!Objects.equals(mText, mLastClassificationText)
+                    || mSelectionStart != mLastClassificationSelectionStart
+                    || mSelectionEnd != mLastClassificationSelectionEnd
+                    || !Objects.equals(mLocales, mLastClassificationLocales)) {
+
+                mLastClassificationText = mText;
+                mLastClassificationSelectionStart = mSelectionStart;
+                mLastClassificationSelectionEnd = mSelectionEnd;
+                mLastClassificationLocales = mLocales;
+
+                trimText();
+                mLastClassificationResult = new SelectionResult(
+                        mSelectionStart,
+                        mSelectionEnd,
+                        mTextClassifier.classifyText(
+                                mTrimmedText, mRelativeStart, mRelativeEnd, mLocales),
+                        selection);
+
+            }
+            return mLastClassificationResult;
+        }
+
+        private void trimText() {
+            mTrimStart = Math.max(0, mSelectionStart - TRIM_DELTA);
+            final int referenceEnd = Math.min(mText.length(), mSelectionEnd + TRIM_DELTA);
+            mTrimmedText = mText.subSequence(mTrimStart, referenceEnd);
+            mRelativeStart = mSelectionStart - mTrimStart;
+            mRelativeEnd = mSelectionEnd - mTrimStart;
+        }
+    }
+
+    /**
+     * Selection result.
+     */
+    private static final class SelectionResult {
+        private final int mStart;
+        private final int mEnd;
+        private final TextClassification mClassification;
+        @Nullable private final TextSelection mSelection;
+
+        SelectionResult(int start, int end,
+                TextClassification classification, @Nullable TextSelection selection) {
+            mStart = start;
+            mEnd = end;
+            mClassification = Preconditions.checkNotNull(classification);
+            mSelection = selection;
+        }
+    }
+
+    @SelectionEvent.ActionType
+    private static int getActionType(int menuItemId) {
+        switch (menuItemId) {
+            case TextView.ID_SELECT_ALL:
+                return SelectionEvent.ActionType.SELECT_ALL;
+            case TextView.ID_CUT:
+                return SelectionEvent.ActionType.CUT;
+            case TextView.ID_COPY:
+                return SelectionEvent.ActionType.COPY;
+            case TextView.ID_PASTE:  // fall through
+            case TextView.ID_PASTE_AS_PLAIN_TEXT:
+                return SelectionEvent.ActionType.PASTE;
+            case TextView.ID_SHARE:
+                return SelectionEvent.ActionType.SHARE;
+            case TextView.ID_ASSIST:
+                return SelectionEvent.ActionType.SMART_SHARE;
+            default:
+                return SelectionEvent.ActionType.OTHER;
+        }
+    }
+}
diff --git a/android/widget/ShareActionProvider.java b/android/widget/ShareActionProvider.java
new file mode 100644
index 0000000..9a24061
--- /dev/null
+++ b/android/widget/ShareActionProvider.java
@@ -0,0 +1,342 @@
+/*
+ * 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 android.widget;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.graphics.drawable.Drawable;
+import android.util.TypedValue;
+import android.view.ActionProvider;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MenuItem.OnMenuItemClickListener;
+import android.view.SubMenu;
+import android.view.View;
+import android.widget.ActivityChooserModel.OnChooseActivityListener;
+
+import com.android.internal.R;
+
+/**
+ * This is a provider for a share action. It is responsible for creating views
+ * that enable data sharing and also to show a sub menu with sharing activities
+ * if the hosting item is placed on the overflow menu.
+ * <p>
+ * Here is how to use the action provider with custom backing file in a {@link MenuItem}:
+ * </p>
+ * <pre>
+ * // In Activity#onCreateOptionsMenu
+ * public boolean onCreateOptionsMenu(Menu menu) {
+ *     // Get the menu item.
+ *     MenuItem menuItem = menu.findItem(R.id.my_menu_item);
+ *     // Get the provider and hold onto it to set/change the share intent.
+ *     mShareActionProvider = (ShareActionProvider) menuItem.getActionProvider();
+ *     // Set history different from the default before getting the action
+ *     // view since a call to {@link MenuItem#getActionView() MenuItem.getActionView()} calls
+ *     // {@link ActionProvider#onCreateActionView()} which uses the backing file name. Omit this
+ *     // line if using the default share history file is desired.
+ *     mShareActionProvider.setShareHistoryFileName("custom_share_history.xml");
+ *     . . .
+ * }
+ *
+ * // Somewhere in the application.
+ * public void doShare(Intent shareIntent) {
+ *     // When you want to share set the share intent.
+ *     mShareActionProvider.setShareIntent(shareIntent);
+ * }</pre>
+ * <p>
+ * <strong>Note:</strong> While the sample snippet demonstrates how to use this provider
+ * in the context of a menu item, the use of the provider is not limited to menu items.
+ * </p>
+ *
+ * @see ActionProvider
+ */
+public class ShareActionProvider extends ActionProvider {
+
+    /**
+     * Listener for the event of selecting a share target.
+     */
+    public interface OnShareTargetSelectedListener {
+
+        /**
+         * Called when a share target has been selected. The client can
+         * decide whether to perform some action before the sharing is
+         * actually performed.
+         * <p>
+         * <strong>Note:</strong> Modifying the intent is not permitted and
+         *     any changes to the latter will be ignored.
+         * </p>
+         * <p>
+         * <strong>Note:</strong> You should <strong>not</strong> handle the
+         *     intent here. This callback aims to notify the client that a
+         *     sharing is being performed, so the client can update the UI
+         *     if necessary.
+         * </p>
+         *
+         * @param source The source of the notification.
+         * @param intent The intent for launching the chosen share target.
+         * @return The return result is ignored. Always return false for consistency.
+         */
+        public boolean onShareTargetSelected(ShareActionProvider source, Intent intent);
+    }
+
+    /**
+     * The default for the maximal number of activities shown in the sub-menu.
+     */
+    private static final int DEFAULT_INITIAL_ACTIVITY_COUNT = 4;
+
+    /**
+     * The the maximum number activities shown in the sub-menu.
+     */
+    private int mMaxShownActivityCount = DEFAULT_INITIAL_ACTIVITY_COUNT;
+
+    /**
+     * Listener for handling menu item clicks.
+     */
+    private final ShareMenuItemOnMenuItemClickListener mOnMenuItemClickListener =
+        new ShareMenuItemOnMenuItemClickListener();
+
+    /**
+     * The default name for storing share history.
+     */
+    public static final String DEFAULT_SHARE_HISTORY_FILE_NAME = "share_history.xml";
+
+    /**
+     * Context for accessing resources.
+     */
+    private final Context mContext;
+
+    /**
+     * The name of the file with share history data.
+     */
+    private String mShareHistoryFileName = DEFAULT_SHARE_HISTORY_FILE_NAME;
+
+    private OnShareTargetSelectedListener mOnShareTargetSelectedListener;
+
+    private OnChooseActivityListener mOnChooseActivityListener;
+
+    /**
+     * Creates a new instance.
+     *
+     * @param context Context for accessing resources.
+     */
+    public ShareActionProvider(Context context) {
+        super(context);
+        mContext = context;
+    }
+
+    /**
+     * Sets a listener to be notified when a share target has been selected.
+     * The listener can optionally decide to handle the selection and
+     * not rely on the default behavior which is to launch the activity.
+     * <p>
+     * <strong>Note:</strong> If you choose the backing share history file
+     *     you will still be notified in this callback.
+     * </p>
+     * @param listener The listener.
+     */
+    public void setOnShareTargetSelectedListener(OnShareTargetSelectedListener listener) {
+        mOnShareTargetSelectedListener = listener;
+        setActivityChooserPolicyIfNeeded();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View onCreateActionView() {
+        // Create the view and set its data model.
+        ActivityChooserView activityChooserView = new ActivityChooserView(mContext);
+        if (!activityChooserView.isInEditMode()) {
+            ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+            activityChooserView.setActivityChooserModel(dataModel);
+        }
+
+        // Lookup and set the expand action icon.
+        TypedValue outTypedValue = new TypedValue();
+        mContext.getTheme().resolveAttribute(R.attr.actionModeShareDrawable, outTypedValue, true);
+        Drawable drawable = mContext.getDrawable(outTypedValue.resourceId);
+        activityChooserView.setExpandActivityOverflowButtonDrawable(drawable);
+        activityChooserView.setProvider(this);
+
+        // Set content description.
+        activityChooserView.setDefaultActionButtonContentDescription(
+                R.string.shareactionprovider_share_with_application);
+        activityChooserView.setExpandActivityOverflowButtonContentDescription(
+                R.string.shareactionprovider_share_with);
+
+        return activityChooserView;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean hasSubMenu() {
+        return true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void onPrepareSubMenu(SubMenu subMenu) {
+        // Clear since the order of items may change.
+        subMenu.clear();
+
+        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+        PackageManager packageManager = mContext.getPackageManager();
+
+        final int expandedActivityCount = dataModel.getActivityCount();
+        final int collapsedActivityCount = Math.min(expandedActivityCount, mMaxShownActivityCount);
+
+        // Populate the sub-menu with a sub set of the activities.
+        for (int i = 0; i < collapsedActivityCount; i++) {
+            ResolveInfo activity = dataModel.getActivity(i);
+            subMenu.add(0, i, i, activity.loadLabel(packageManager))
+                .setIcon(activity.loadIcon(packageManager))
+                .setOnMenuItemClickListener(mOnMenuItemClickListener);
+        }
+
+        if (collapsedActivityCount < expandedActivityCount) {
+            // Add a sub-menu for showing all activities as a list item.
+            SubMenu expandedSubMenu = subMenu.addSubMenu(Menu.NONE, collapsedActivityCount,
+                    collapsedActivityCount,
+                    mContext.getString(R.string.activity_chooser_view_see_all));
+            for (int i = 0; i < expandedActivityCount; i++) {
+                ResolveInfo activity = dataModel.getActivity(i);
+                expandedSubMenu.add(0, i, i, activity.loadLabel(packageManager))
+                    .setIcon(activity.loadIcon(packageManager))
+                    .setOnMenuItemClickListener(mOnMenuItemClickListener);
+            }
+        }
+    }
+
+    /**
+     * Sets the file name of a file for persisting the share history which
+     * history will be used for ordering share targets. This file will be used
+     * for all view created by {@link #onCreateActionView()}. Defaults to
+     * {@link #DEFAULT_SHARE_HISTORY_FILE_NAME}. Set to <code>null</code>
+     * if share history should not be persisted between sessions.
+     * <p>
+     * <strong>Note:</strong> The history file name can be set any time, however
+     * only the action views created by {@link #onCreateActionView()} after setting
+     * the file name will be backed by the provided file. Therefore, if you want to
+     * use different history files for sharing specific types of content, every time
+     * you change the history file {@link #setShareHistoryFileName(String)} you must
+     * call {@link android.app.Activity#invalidateOptionsMenu()} to recreate the
+     * action view. You should <strong>not</strong> call
+     * {@link android.app.Activity#invalidateOptionsMenu()} from
+     * {@link android.app.Activity#onCreateOptionsMenu(Menu)}.
+     * </p>
+     * <pre>
+     * private void doShare(Intent intent) {
+     *     if (IMAGE.equals(intent.getMimeType())) {
+     *         mShareActionProvider.setHistoryFileName(SHARE_IMAGE_HISTORY_FILE_NAME);
+     *     } else if (TEXT.equals(intent.getMimeType())) {
+     *         mShareActionProvider.setHistoryFileName(SHARE_TEXT_HISTORY_FILE_NAME);
+     *     }
+     *     mShareActionProvider.setIntent(intent);
+     *     invalidateOptionsMenu();
+     * }</pre>
+     * @param shareHistoryFile The share history file name.
+     */
+    public void setShareHistoryFileName(String shareHistoryFile) {
+        mShareHistoryFileName = shareHistoryFile;
+        setActivityChooserPolicyIfNeeded();
+    }
+
+    /**
+     * Sets an intent with information about the share action. Here is a
+     * sample for constructing a share intent:
+     * <pre>
+     * Intent shareIntent = new Intent(Intent.ACTION_SEND);
+     * shareIntent.setType("image/*");
+     * Uri uri = Uri.fromFile(new File(getFilesDir(), "foo.jpg"));
+     * shareIntent.putExtra(Intent.EXTRA_STREAM, uri));</pre>
+     *
+     * @param shareIntent The share intent.
+     *
+     * @see Intent#ACTION_SEND
+     * @see Intent#ACTION_SEND_MULTIPLE
+     */
+    public void setShareIntent(Intent shareIntent) {
+        if (shareIntent != null) {
+            final String action = shareIntent.getAction();
+            if (Intent.ACTION_SEND.equals(action) || Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+                shareIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+                        Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+            }
+        }
+        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
+            mShareHistoryFileName);
+        dataModel.setIntent(shareIntent);
+    }
+
+    /**
+     * Reusable listener for handling share item clicks.
+     */
+    private class ShareMenuItemOnMenuItemClickListener implements OnMenuItemClickListener {
+        @Override
+        public boolean onMenuItemClick(MenuItem item) {
+            ActivityChooserModel dataModel = ActivityChooserModel.get(mContext,
+                    mShareHistoryFileName);
+            final int itemId = item.getItemId();
+            Intent launchIntent = dataModel.chooseActivity(itemId);
+            if (launchIntent != null) {
+                final String action = launchIntent.getAction();
+                if (Intent.ACTION_SEND.equals(action) ||
+                        Intent.ACTION_SEND_MULTIPLE.equals(action)) {
+                    launchIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT |
+                            Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
+                }
+                mContext.startActivity(launchIntent);
+            }
+            return true;
+        }
+    }
+
+    /**
+     * Set the activity chooser policy of the model backed by the current
+     * share history file if needed which is if there is a registered callback.
+     */
+    private void setActivityChooserPolicyIfNeeded() {
+        if (mOnShareTargetSelectedListener == null) {
+            return;
+        }
+        if (mOnChooseActivityListener == null) {
+            mOnChooseActivityListener = new ShareActivityChooserModelPolicy();
+        }
+        ActivityChooserModel dataModel = ActivityChooserModel.get(mContext, mShareHistoryFileName);
+        dataModel.setOnChooseActivityListener(mOnChooseActivityListener);
+    }
+
+    /**
+     * Policy that delegates to the {@link OnShareTargetSelectedListener}, if such.
+     */
+    private class ShareActivityChooserModelPolicy implements OnChooseActivityListener {
+        @Override
+        public boolean onChooseActivity(ActivityChooserModel host, Intent intent) {
+            if (mOnShareTargetSelectedListener != null) {
+                mOnShareTargetSelectedListener.onShareTargetSelected(
+                        ShareActionProvider.this, intent);
+            }
+            return false;
+        }
+    }
+}
diff --git a/android/widget/SimpleAdapter.java b/android/widget/SimpleAdapter.java
new file mode 100644
index 0000000..9190117
--- /dev/null
+++ b/android/widget/SimpleAdapter.java
@@ -0,0 +1,423 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.IdRes;
+import android.annotation.LayoutRes;
+import android.content.Context;
+import android.content.res.Resources;
+import android.net.Uri;
+import android.view.ContextThemeWrapper;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An easy adapter to map static data to views defined in an XML file. You can specify the data
+ * backing the list as an ArrayList of Maps. Each entry in the ArrayList corresponds to one row
+ * in the list. The Maps contain the data for each row. You also specify an XML file that
+ * defines the views used to display the row, and a mapping from keys in the Map to specific
+ * views.
+ *
+ * Binding data to views occurs in two phases. First, if a
+ * {@link android.widget.SimpleAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, Object, String)}
+ * is invoked. If the returned value is true, binding has occurred.
+ * If the returned value is false, the following views are then tried in order:
+ * <ul>
+ * <li> A view that implements Checkable (e.g. CheckBox).  The expected bind value is a boolean.
+ * <li> TextView.  The expected bind value is a string and {@link #setViewText(TextView, String)}
+ * is invoked.
+ * <li> ImageView. The expected bind value is a resource id or a string and
+ * {@link #setViewImage(ImageView, int)} or {@link #setViewImage(ImageView, String)} is invoked.
+ * </ul>
+ * If no appropriate binding can be found, an {@link IllegalStateException} is thrown.
+ */
+public class SimpleAdapter extends BaseAdapter implements Filterable, ThemedSpinnerAdapter {
+    private final LayoutInflater mInflater;
+
+    private int[] mTo;
+    private String[] mFrom;
+    private ViewBinder mViewBinder;
+
+    private List<? extends Map<String, ?>> mData;
+
+    private int mResource;
+    private int mDropDownResource;
+
+    /** Layout inflater used for {@link #getDropDownView(int, View, ViewGroup)}. */
+    private LayoutInflater mDropDownInflater;
+
+    private SimpleFilter mFilter;
+    private ArrayList<Map<String, ?>> mUnfilteredData;
+
+    /**
+     * Constructor
+     *
+     * @param context The context where the View associated with this SimpleAdapter is running
+     * @param data A List of Maps. Each entry in the List corresponds to one row in the list. The
+     *        Maps contain the data for each row, and should include all the entries specified in
+     *        "from"
+     * @param resource Resource identifier of a view layout that defines the views for this list
+     *        item. The layout file should include at least those named views defined in "to"
+     * @param from A list of column names that will be added to the Map associated with each
+     *        item.
+     * @param to The views that should display column in the "from" parameter. These should all be
+     *        TextViews. The first N views in this list are given the values of the first N columns
+     *        in the from parameter.
+     */
+    public SimpleAdapter(Context context, List<? extends Map<String, ?>> data,
+            @LayoutRes int resource, String[] from, @IdRes int[] to) {
+        mData = data;
+        mResource = mDropDownResource = resource;
+        mFrom = from;
+        mTo = to;
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    /**
+     * @see android.widget.Adapter#getCount()
+     */
+    public int getCount() {
+        return mData.size();
+    }
+
+    /**
+     * @see android.widget.Adapter#getItem(int)
+     */
+    public Object getItem(int position) {
+        return mData.get(position);
+    }
+
+    /**
+     * @see android.widget.Adapter#getItemId(int)
+     */
+    public long getItemId(int position) {
+        return position;
+    }
+
+    /**
+     * @see android.widget.Adapter#getView(int, View, ViewGroup)
+     */
+    public View getView(int position, View convertView, ViewGroup parent) {
+        return createViewFromResource(mInflater, position, convertView, parent, mResource);
+    }
+
+    private View createViewFromResource(LayoutInflater inflater, int position, View convertView,
+            ViewGroup parent, int resource) {
+        View v;
+        if (convertView == null) {
+            v = inflater.inflate(resource, parent, false);
+        } else {
+            v = convertView;
+        }
+
+        bindView(position, v);
+
+        return v;
+    }
+
+    /**
+     * <p>Sets the layout resource to create the drop down views.</p>
+     *
+     * @param resource the layout resource defining the drop down views
+     * @see #getDropDownView(int, android.view.View, android.view.ViewGroup)
+     */
+    public void setDropDownViewResource(int resource) {
+        mDropDownResource = resource;
+    }
+
+    /**
+     * Sets the {@link android.content.res.Resources.Theme} against which drop-down views are
+     * inflated.
+     * <p>
+     * By default, drop-down views are inflated against the theme of the
+     * {@link Context} passed to the adapter's constructor.
+     *
+     * @param theme the theme against which to inflate drop-down views or
+     *              {@code null} to use the theme from the adapter's context
+     * @see #getDropDownView(int, View, ViewGroup)
+     */
+    @Override
+    public void setDropDownViewTheme(Resources.Theme theme) {
+        if (theme == null) {
+            mDropDownInflater = null;
+        } else if (theme == mInflater.getContext().getTheme()) {
+            mDropDownInflater = mInflater;
+        } else {
+            final Context context = new ContextThemeWrapper(mInflater.getContext(), theme);
+            mDropDownInflater = LayoutInflater.from(context);
+        }
+    }
+
+    @Override
+    public Resources.Theme getDropDownViewTheme() {
+        return mDropDownInflater == null ? null : mDropDownInflater.getContext().getTheme();
+    }
+
+    @Override
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        final LayoutInflater inflater = mDropDownInflater == null ? mInflater : mDropDownInflater;
+        return createViewFromResource(inflater, position, convertView, parent, mDropDownResource);
+    }
+
+    private void bindView(int position, View view) {
+        final Map dataSet = mData.get(position);
+        if (dataSet == null) {
+            return;
+        }
+
+        final ViewBinder binder = mViewBinder;
+        final String[] from = mFrom;
+        final int[] to = mTo;
+        final int count = to.length;
+
+        for (int i = 0; i < count; i++) {
+            final View v = view.findViewById(to[i]);
+            if (v != null) {
+                final Object data = dataSet.get(from[i]);
+                String text = data == null ? "" : data.toString();
+                if (text == null) {
+                    text = "";
+                }
+
+                boolean bound = false;
+                if (binder != null) {
+                    bound = binder.setViewValue(v, data, text);
+                }
+
+                if (!bound) {
+                    if (v instanceof Checkable) {
+                        if (data instanceof Boolean) {
+                            ((Checkable) v).setChecked((Boolean) data);
+                        } else if (v instanceof TextView) {
+                            // Note: keep the instanceof TextView check at the bottom of these
+                            // ifs since a lot of views are TextViews (e.g. CheckBoxes).
+                            setViewText((TextView) v, text);
+                        } else {
+                            throw new IllegalStateException(v.getClass().getName() +
+                                    " should be bound to a Boolean, not a " +
+                                    (data == null ? "<unknown type>" : data.getClass()));
+                        }
+                    } else if (v instanceof TextView) {
+                        // Note: keep the instanceof TextView check at the bottom of these
+                        // ifs since a lot of views are TextViews (e.g. CheckBoxes).
+                        setViewText((TextView) v, text);
+                    } else if (v instanceof ImageView) {
+                        if (data instanceof Integer) {
+                            setViewImage((ImageView) v, (Integer) data);
+                        } else {
+                            setViewImage((ImageView) v, text);
+                        }
+                    } else {
+                        throw new IllegalStateException(v.getClass().getName() + " is not a " +
+                                " view that can be bounds by this SimpleAdapter");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link ViewBinder} used to bind data to views.
+     *
+     * @return a ViewBinder or null if the binder does not exist
+     *
+     * @see #setViewBinder(android.widget.SimpleAdapter.ViewBinder)
+     */
+    public ViewBinder getViewBinder() {
+        return mViewBinder;
+    }
+
+    /**
+     * Sets the binder used to bind data to views.
+     *
+     * @param viewBinder the binder used to bind data to views, can be null to
+     *        remove the existing binder
+     *
+     * @see #getViewBinder()
+     */
+    public void setViewBinder(ViewBinder viewBinder) {
+        mViewBinder = viewBinder;
+    }
+
+    /**
+     * Called by bindView() to set the image for an ImageView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to an ImageView.
+     *
+     * This method is called instead of {@link #setViewImage(ImageView, String)}
+     * if the supplied data is an int or Integer.
+     *
+     * @param v ImageView to receive an image
+     * @param value the value retrieved from the data set
+     *
+     * @see #setViewImage(ImageView, String)
+     */
+    public void setViewImage(ImageView v, int value) {
+        v.setImageResource(value);
+    }
+
+    /**
+     * Called by bindView() to set the image for an ImageView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to an ImageView.
+     *
+     * By default, the value will be treated as an image resource. If the
+     * value cannot be used as an image resource, the value is used as an
+     * image Uri.
+     *
+     * This method is called instead of {@link #setViewImage(ImageView, int)}
+     * if the supplied data is not an int or Integer.
+     *
+     * @param v ImageView to receive an image
+     * @param value the value retrieved from the data set
+     *
+     * @see #setViewImage(ImageView, int)
+     */
+    public void setViewImage(ImageView v, String value) {
+        try {
+            v.setImageResource(Integer.parseInt(value));
+        } catch (NumberFormatException nfe) {
+            v.setImageURI(Uri.parse(value));
+        }
+    }
+
+    /**
+     * Called by bindView() to set the text for a TextView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to a TextView.
+     *
+     * @param v TextView to receive text
+     * @param text the text to be set for the TextView
+     */
+    public void setViewText(TextView v, String text) {
+        v.setText(text);
+    }
+
+    public Filter getFilter() {
+        if (mFilter == null) {
+            mFilter = new SimpleFilter();
+        }
+        return mFilter;
+    }
+
+    /**
+     * This class can be used by external clients of SimpleAdapter to bind
+     * values to views.
+     *
+     * You should use this class to bind values to views that are not
+     * directly supported by SimpleAdapter or to change the way binding
+     * occurs for views supported by SimpleAdapter.
+     *
+     * @see SimpleAdapter#setViewImage(ImageView, int)
+     * @see SimpleAdapter#setViewImage(ImageView, String)
+     * @see SimpleAdapter#setViewText(TextView, String)
+     */
+    public static interface ViewBinder {
+        /**
+         * Binds the specified data to the specified view.
+         *
+         * When binding is handled by this ViewBinder, this method must return true.
+         * If this method returns false, SimpleAdapter will attempts to handle
+         * the binding on its own.
+         *
+         * @param view the view to bind the data to
+         * @param data the data to bind to the view
+         * @param textRepresentation a safe String representation of the supplied data:
+         *        it is either the result of data.toString() or an empty String but it
+         *        is never null
+         *
+         * @return true if the data was bound to the view, false otherwise
+         */
+        boolean setViewValue(View view, Object data, String textRepresentation);
+    }
+
+    /**
+     * <p>An array filters constrains the content of the array adapter with
+     * a prefix. Each item that does not start with the supplied prefix
+     * is removed from the list.</p>
+     */
+    private class SimpleFilter extends Filter {
+
+        @Override
+        protected FilterResults performFiltering(CharSequence prefix) {
+            FilterResults results = new FilterResults();
+
+            if (mUnfilteredData == null) {
+                mUnfilteredData = new ArrayList<Map<String, ?>>(mData);
+            }
+
+            if (prefix == null || prefix.length() == 0) {
+                ArrayList<Map<String, ?>> list = mUnfilteredData;
+                results.values = list;
+                results.count = list.size();
+            } else {
+                String prefixString = prefix.toString().toLowerCase();
+
+                ArrayList<Map<String, ?>> unfilteredValues = mUnfilteredData;
+                int count = unfilteredValues.size();
+
+                ArrayList<Map<String, ?>> newValues = new ArrayList<Map<String, ?>>(count);
+
+                for (int i = 0; i < count; i++) {
+                    Map<String, ?> h = unfilteredValues.get(i);
+                    if (h != null) {
+
+                        int len = mTo.length;
+
+                        for (int j=0; j<len; j++) {
+                            String str =  (String)h.get(mFrom[j]);
+
+                            String[] words = str.split(" ");
+                            int wordCount = words.length;
+
+                            for (int k = 0; k < wordCount; k++) {
+                                String word = words[k];
+
+                                if (word.toLowerCase().startsWith(prefixString)) {
+                                    newValues.add(h);
+                                    break;
+                                }
+                            }
+                        }
+                    }
+                }
+
+                results.values = newValues;
+                results.count = newValues.size();
+            }
+
+            return results;
+        }
+
+        @Override
+        protected void publishResults(CharSequence constraint, FilterResults results) {
+            //noinspection unchecked
+            mData = (List<Map<String, ?>>) results.values;
+            if (results.count > 0) {
+                notifyDataSetChanged();
+            } else {
+                notifyDataSetInvalidated();
+            }
+        }
+    }
+}
diff --git a/android/widget/SimpleCursorAdapter.java b/android/widget/SimpleCursorAdapter.java
new file mode 100644
index 0000000..3dd0a95
--- /dev/null
+++ b/android/widget/SimpleCursorAdapter.java
@@ -0,0 +1,418 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+
+/**
+ * An easy adapter to map columns from a cursor to TextViews or ImageViews
+ * defined in an XML file. You can specify which columns you want, which
+ * views you want to display the columns, and the XML file that defines
+ * the appearance of these views.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occured. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value
+ * is false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ *
+ * If this adapter is used with filtering, for instance in an
+ * {@link android.widget.AutoCompleteTextView}, you can use the
+ * {@link android.widget.SimpleCursorAdapter.CursorToStringConverter} and the
+ * {@link android.widget.FilterQueryProvider} interfaces
+ * to get control over the filtering process. You can refer to
+ * {@link #convertToString(android.database.Cursor)} and
+ * {@link #runQueryOnBackgroundThread(CharSequence)} for more information.
+ */
+public class SimpleCursorAdapter extends ResourceCursorAdapter {
+    /**
+     * A list of columns containing the data to bind to the UI.
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected int[] mFrom;
+    /**
+     * A list of View ids representing the views to which the data must be bound.
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected int[] mTo;
+
+    private int mStringConversionColumn = -1;
+    private CursorToStringConverter mCursorToStringConverter;
+    private ViewBinder mViewBinder;
+
+    String[] mOriginalFrom;
+
+    /**
+     * Constructor the enables auto-requery.
+     *
+     * @deprecated This option is discouraged, as it results in Cursor queries
+     * being performed on the application's UI thread and thus can cause poor
+     * responsiveness or even Application Not Responding errors.  As an alternative,
+     * use {@link android.app.LoaderManager} with a {@link android.content.CursorLoader}.
+     */
+    @Deprecated
+    public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from, int[] to) {
+        super(context, layout, c);
+        mTo = to;
+        mOriginalFrom = from;
+        findColumns(c, from);
+    }
+
+    /**
+     * Standard constructor.
+     * 
+     * @param context The context where the ListView associated with this
+     *            SimpleListItemFactory is running
+     * @param layout resource identifier of a layout file that defines the views
+     *            for this list item. The layout file should include at least
+     *            those named views defined in "to"
+     * @param c The database cursor.  Can be null if the cursor is not available yet.
+     * @param from A list of column names representing the data to bind to the UI.  Can be null 
+     *            if the cursor is not available yet.
+     * @param to The views that should display column in the "from" parameter.
+     *            These should all be TextViews. The first N views in this list
+     *            are given the values of the first N columns in the from
+     *            parameter.  Can be null if the cursor is not available yet.
+     * @param flags Flags used to determine the behavior of the adapter,
+     * as per {@link CursorAdapter#CursorAdapter(Context, Cursor, int)}.
+     */
+    public SimpleCursorAdapter(Context context, int layout, Cursor c, String[] from,
+            int[] to, int flags) {
+        super(context, layout, c, flags);
+        mTo = to;
+        mOriginalFrom = from;
+        findColumns(c, from);
+    }
+
+    /**
+     * Binds all of the field names passed into the "to" parameter of the
+     * constructor with their corresponding cursor columns as specified in the
+     * "from" parameter.
+     *
+     * Binding occurs in two phases. First, if a
+     * {@link android.widget.SimpleCursorAdapter.ViewBinder} is available,
+     * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+     * is invoked. If the returned value is true, binding has occured. If the
+     * returned value is false and the view to bind is a TextView,
+     * {@link #setViewText(TextView, String)} is invoked. If the returned value is
+     * false and the view to bind is an ImageView,
+     * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+     * binding can be found, an {@link IllegalStateException} is thrown.
+     *
+     * @throws IllegalStateException if binding cannot occur
+     * 
+     * @see android.widget.CursorAdapter#bindView(android.view.View,
+     *      android.content.Context, android.database.Cursor)
+     * @see #getViewBinder()
+     * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+     * @see #setViewImage(ImageView, String)
+     * @see #setViewText(TextView, String)
+     */
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        final ViewBinder binder = mViewBinder;
+        final int count = mTo.length;
+        final int[] from = mFrom;
+        final int[] to = mTo;
+
+        for (int i = 0; i < count; i++) {
+            final View v = view.findViewById(to[i]);
+            if (v != null) {
+                boolean bound = false;
+                if (binder != null) {
+                    bound = binder.setViewValue(v, cursor, from[i]);
+                }
+
+                if (!bound) {
+                    String text = cursor.getString(from[i]);
+                    if (text == null) {
+                        text = "";
+                    }
+
+                    if (v instanceof TextView) {
+                        setViewText((TextView) v, text);
+                    } else if (v instanceof ImageView) {
+                        setViewImage((ImageView) v, text);
+                    } else {
+                        throw new IllegalStateException(v.getClass().getName() + " is not a " +
+                                " view that can be bounds by this SimpleCursorAdapter");
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * Returns the {@link ViewBinder} used to bind data to views.
+     *
+     * @return a ViewBinder or null if the binder does not exist
+     *
+     * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+     * @see #setViewBinder(android.widget.SimpleCursorAdapter.ViewBinder)
+     */
+    public ViewBinder getViewBinder() {
+        return mViewBinder;
+    }
+
+    /**
+     * Sets the binder used to bind data to views.
+     *
+     * @param viewBinder the binder used to bind data to views, can be null to
+     *        remove the existing binder
+     *
+     * @see #bindView(android.view.View, android.content.Context, android.database.Cursor)
+     * @see #getViewBinder()
+     */
+    public void setViewBinder(ViewBinder viewBinder) {
+        mViewBinder = viewBinder;
+    }
+
+    /**
+     * Called by bindView() to set the image for an ImageView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to an ImageView.
+     *
+     * By default, the value will be treated as an image resource. If the
+     * value cannot be used as an image resource, the value is used as an
+     * image Uri.
+     *
+     * Intended to be overridden by Adapters that need to filter strings
+     * retrieved from the database.
+     *
+     * @param v ImageView to receive an image
+     * @param value the value retrieved from the cursor
+     */
+    public void setViewImage(ImageView v, String value) {
+        try {
+            v.setImageResource(Integer.parseInt(value));
+        } catch (NumberFormatException nfe) {
+            v.setImageURI(Uri.parse(value));
+        }
+    }
+
+    /**
+     * Called by bindView() to set the text for a TextView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to a TextView.
+     *
+     * Intended to be overridden by Adapters that need to filter strings
+     * retrieved from the database.
+     * 
+     * @param v TextView to receive text
+     * @param text the text to be set for the TextView
+     */    
+    public void setViewText(TextView v, String text) {
+        v.setText(text);
+    }
+
+    /**
+     * Return the index of the column used to get a String representation
+     * of the Cursor.
+     *
+     * @return a valid index in the current Cursor or -1
+     *
+     * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+     * @see #setStringConversionColumn(int) 
+     * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+     * @see #getCursorToStringConverter()
+     */
+    public int getStringConversionColumn() {
+        return mStringConversionColumn;
+    }
+
+    /**
+     * Defines the index of the column in the Cursor used to get a String
+     * representation of that Cursor. The column is used to convert the
+     * Cursor to a String only when the current CursorToStringConverter
+     * is null.
+     *
+     * @param stringConversionColumn a valid index in the current Cursor or -1 to use the default
+     *        conversion mechanism
+     *
+     * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+     * @see #getStringConversionColumn()
+     * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+     * @see #getCursorToStringConverter()
+     */
+    public void setStringConversionColumn(int stringConversionColumn) {
+        mStringConversionColumn = stringConversionColumn;
+    }
+
+    /**
+     * Returns the converter used to convert the filtering Cursor
+     * into a String.
+     *
+     * @return null if the converter does not exist or an instance of
+     *         {@link android.widget.SimpleCursorAdapter.CursorToStringConverter}
+     *
+     * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter)
+     * @see #getStringConversionColumn()
+     * @see #setStringConversionColumn(int)
+     * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+     */
+    public CursorToStringConverter getCursorToStringConverter() {
+        return mCursorToStringConverter;
+    }
+
+    /**
+     * Sets the converter  used to convert the filtering Cursor
+     * into a String.
+     *
+     * @param cursorToStringConverter the Cursor to String converter, or
+     *        null to remove the converter
+     *
+     * @see #setCursorToStringConverter(android.widget.SimpleCursorAdapter.CursorToStringConverter) 
+     * @see #getStringConversionColumn()
+     * @see #setStringConversionColumn(int)
+     * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+     */
+    public void setCursorToStringConverter(CursorToStringConverter cursorToStringConverter) {
+        mCursorToStringConverter = cursorToStringConverter;
+    }
+
+    /**
+     * Returns a CharSequence representation of the specified Cursor as defined
+     * by the current CursorToStringConverter. If no CursorToStringConverter
+     * has been set, the String conversion column is used instead. If the
+     * conversion column is -1, the returned String is empty if the cursor
+     * is null or Cursor.toString().
+     *
+     * @param cursor the Cursor to convert to a CharSequence
+     *
+     * @return a non-null CharSequence representing the cursor
+     */
+    @Override
+    public CharSequence convertToString(Cursor cursor) {
+        if (mCursorToStringConverter != null) {
+            return mCursorToStringConverter.convertToString(cursor);
+        } else if (mStringConversionColumn > -1) {
+            return cursor.getString(mStringConversionColumn);
+        }
+
+        return super.convertToString(cursor);
+    }
+
+    /**
+     * Create a map from an array of strings to an array of column-id integers in cursor c.
+     * If c is null, the array will be discarded.
+     *
+     * @param c the cursor to find the columns from
+     * @param from the Strings naming the columns of interest
+     */
+    private void findColumns(Cursor c, String[] from) {
+        if (c != null) {
+            int i;
+            int count = from.length;
+            if (mFrom == null || mFrom.length != count) {
+                mFrom = new int[count];
+            }
+            for (i = 0; i < count; i++) {
+                mFrom[i] = c.getColumnIndexOrThrow(from[i]);
+            }
+        } else {
+            mFrom = null;
+        }
+    }
+
+    @Override
+    public Cursor swapCursor(Cursor c) {
+        // super.swapCursor() will notify observers before we have
+        // a valid mapping, make sure we have a mapping before this
+        // happens
+        findColumns(c, mOriginalFrom);
+        return super.swapCursor(c);
+    }
+    
+    /**
+     * Change the cursor and change the column-to-view mappings at the same time.
+     *  
+     * @param c The database cursor.  Can be null if the cursor is not available yet.
+     * @param from A list of column names representing the data to bind to the UI.  Can be null 
+     *            if the cursor is not available yet.
+     * @param to The views that should display column in the "from" parameter.
+     *            These should all be TextViews. The first N views in this list
+     *            are given the values of the first N columns in the from
+     *            parameter.  Can be null if the cursor is not available yet.
+     */
+    public void changeCursorAndColumns(Cursor c, String[] from, int[] to) {
+        mOriginalFrom = from;
+        mTo = to;
+        // super.changeCursor() will notify observers before we have
+        // a valid mapping, make sure we have a mapping before this
+        // happens
+        findColumns(c, mOriginalFrom);
+        super.changeCursor(c);
+    }
+
+    /**
+     * This class can be used by external clients of SimpleCursorAdapter
+     * to bind values fom the Cursor to views.
+     *
+     * You should use this class to bind values from the Cursor to views
+     * that are not directly supported by SimpleCursorAdapter or to
+     * change the way binding occurs for views supported by
+     * SimpleCursorAdapter.
+     *
+     * @see SimpleCursorAdapter#bindView(android.view.View, android.content.Context, android.database.Cursor)
+     * @see SimpleCursorAdapter#setViewImage(ImageView, String) 
+     * @see SimpleCursorAdapter#setViewText(TextView, String)
+     */
+    public static interface ViewBinder {
+        /**
+         * Binds the Cursor column defined by the specified index to the specified view.
+         *
+         * When binding is handled by this ViewBinder, this method must return true.
+         * If this method returns false, SimpleCursorAdapter will attempts to handle
+         * the binding on its own.
+         *
+         * @param view the view to bind the data to
+         * @param cursor the cursor to get the data from
+         * @param columnIndex the column at which the data can be found in the cursor
+         *
+         * @return true if the data was bound to the view, false otherwise
+         */
+        boolean setViewValue(View view, Cursor cursor, int columnIndex);
+    }
+
+    /**
+     * This class can be used by external clients of SimpleCursorAdapter
+     * to define how the Cursor should be converted to a String.
+     *
+     * @see android.widget.CursorAdapter#convertToString(android.database.Cursor)
+     */
+    public static interface CursorToStringConverter {
+        /**
+         * Returns a CharSequence representing the specified Cursor.
+         *
+         * @param cursor the cursor for which a CharSequence representation
+         *        is requested
+         *
+         * @return a non-null CharSequence representing the cursor
+         */
+        CharSequence convertToString(Cursor cursor);
+    }
+
+}
diff --git a/android/widget/SimpleCursorTreeAdapter.java b/android/widget/SimpleCursorTreeAdapter.java
new file mode 100644
index 0000000..6babf3e
--- /dev/null
+++ b/android/widget/SimpleCursorTreeAdapter.java
@@ -0,0 +1,326 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.database.Cursor;
+import android.net.Uri;
+import android.view.View;
+
+/**
+ * An easy adapter to map columns from a cursor to TextViews or ImageViews
+ * defined in an XML file. You can specify which columns you want, which views
+ * you want to display the columns, and the XML file that defines the appearance
+ * of these views. Separate XML files for child and groups are possible.
+ *
+ * Binding occurs in two phases. First, if a
+ * {@link android.widget.SimpleCursorTreeAdapter.ViewBinder} is available,
+ * {@link ViewBinder#setViewValue(android.view.View, android.database.Cursor, int)}
+ * is invoked. If the returned value is true, binding has occurred. If the
+ * returned value is false and the view to bind is a TextView,
+ * {@link #setViewText(TextView, String)} is invoked. If the returned value
+ * is false and the view to bind is an ImageView,
+ * {@link #setViewImage(ImageView, String)} is invoked. If no appropriate
+ * binding can be found, an {@link IllegalStateException} is thrown.
+ */
+public abstract class SimpleCursorTreeAdapter extends ResourceCursorTreeAdapter {
+    
+    /** The name of the columns that contain the data to display for a group. */
+    private String[] mGroupFromNames;
+    
+    /** The indices of columns that contain data to display for a group. */
+    private int[] mGroupFrom;
+    /**
+     * The View IDs that will display a group's data fetched from the
+     * corresponding column.
+     */
+    private int[] mGroupTo;
+
+    /** The name of the columns that contain the data to display for a child. */
+    private String[] mChildFromNames;
+    
+    /** The indices of columns that contain data to display for a child. */
+    private int[] mChildFrom;
+    /**
+     * The View IDs that will display a child's data fetched from the
+     * corresponding column.
+     */
+    private int[] mChildTo;
+    
+    /**
+     * View binder, if supplied
+     */
+    private ViewBinder mViewBinder;
+
+    /**
+     * Constructor.
+     * 
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleCursorTreeAdapter} is
+     *            running
+     * @param cursor The database cursor
+     * @param collapsedGroupLayout The resource identifier of a layout file that
+     *            defines the views for a collapsed group. The layout file
+     *            should include at least those named views defined in groupTo.
+     * @param expandedGroupLayout The resource identifier of a layout file that
+     *            defines the views for an expanded group. The layout file
+     *            should include at least those named views defined in groupTo.
+     * @param groupFrom A list of column names that will be used to display the
+     *            data for a group.
+     * @param groupTo The group views (from the group layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     * @param childLayout The resource identifier of a layout file that defines
+     *            the views for a child (except the last). The layout file
+     *            should include at least those named views defined in childTo.
+     * @param lastChildLayout The resource identifier of a layout file that
+     *            defines the views for the last child within a group. The
+     *            layout file should include at least those named views defined
+     *            in childTo.
+     * @param childFrom A list of column names that will be used to display the
+     *            data for a child.
+     * @param childTo The child views (from the child layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     */
+    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+            int expandedGroupLayout, String[] groupFrom, int[] groupTo, int childLayout,
+            int lastChildLayout, String[] childFrom, int[] childTo) {
+        super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout,
+                lastChildLayout);
+        init(groupFrom, groupTo, childFrom, childTo);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleCursorTreeAdapter} is
+     *            running
+     * @param cursor The database cursor
+     * @param collapsedGroupLayout The resource identifier of a layout file that
+     *            defines the views for a collapsed group. The layout file
+     *            should include at least those named views defined in groupTo.
+     * @param expandedGroupLayout The resource identifier of a layout file that
+     *            defines the views for an expanded group. The layout file
+     *            should include at least those named views defined in groupTo.
+     * @param groupFrom A list of column names that will be used to display the
+     *            data for a group.
+     * @param groupTo The group views (from the group layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     * @param childLayout The resource identifier of a layout file that defines
+     *            the views for a child. The layout file
+     *            should include at least those named views defined in childTo.
+     * @param childFrom A list of column names that will be used to display the
+     *            data for a child.
+     * @param childTo The child views (from the child layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     */
+    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int collapsedGroupLayout,
+            int expandedGroupLayout, String[] groupFrom, int[] groupTo,
+            int childLayout, String[] childFrom, int[] childTo) {
+        super(context, cursor, collapsedGroupLayout, expandedGroupLayout, childLayout);
+        init(groupFrom, groupTo, childFrom, childTo);
+    }
+
+    /**
+     * Constructor.
+     * 
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleCursorTreeAdapter} is
+     *            running
+     * @param cursor The database cursor
+     * @param groupLayout The resource identifier of a layout file that defines
+     *            the views for a group. The layout file should include at least
+     *            those named views defined in groupTo.
+     * @param groupFrom A list of column names that will be used to display the
+     *            data for a group.
+     * @param groupTo The group views (from the group layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     * @param childLayout The resource identifier of a layout file that defines
+     *            the views for a child. The layout file should include at least
+     *            those named views defined in childTo.
+     * @param childFrom A list of column names that will be used to display the
+     *            data for a child.
+     * @param childTo The child views (from the child layouts) that should
+     *            display column in the "from" parameter. These should all be
+     *            TextViews or ImageViews. The first N views in this list are
+     *            given the values of the first N columns in the from parameter.
+     */
+    public SimpleCursorTreeAdapter(Context context, Cursor cursor, int groupLayout,
+            String[] groupFrom, int[] groupTo, int childLayout, String[] childFrom,
+            int[] childTo) {
+        super(context, cursor, groupLayout, childLayout);
+        init(groupFrom, groupTo, childFrom, childTo);
+    }
+
+    private void init(String[] groupFromNames, int[] groupTo, String[] childFromNames,
+            int[] childTo) {
+        
+        mGroupFromNames = groupFromNames;
+        mGroupTo = groupTo;
+        
+        mChildFromNames = childFromNames;
+        mChildTo = childTo;
+    }
+    
+    /**
+     * Returns the {@link ViewBinder} used to bind data to views.
+     *
+     * @return a ViewBinder or null if the binder does not exist
+     *
+     * @see #setViewBinder(android.widget.SimpleCursorTreeAdapter.ViewBinder)
+     */
+    public ViewBinder getViewBinder() {
+        return mViewBinder;
+    }
+
+    /**
+     * Sets the binder used to bind data to views.
+     *
+     * @param viewBinder the binder used to bind data to views, can be null to
+     *        remove the existing binder
+     *
+     * @see #getViewBinder()
+     */
+    public void setViewBinder(ViewBinder viewBinder) {
+        mViewBinder = viewBinder;
+    }
+
+    private void bindView(View view, Context context, Cursor cursor, int[] from, int[] to) {
+        final ViewBinder binder = mViewBinder;
+        
+        for (int i = 0; i < to.length; i++) {
+            View v = view.findViewById(to[i]);
+            if (v != null) {
+                boolean bound = false;
+                if (binder != null) {
+                    bound = binder.setViewValue(v, cursor, from[i]);
+                }
+                
+                if (!bound) {
+                    String text = cursor.getString(from[i]);
+                    if (text == null) {
+                        text = "";
+                    }
+                    if (v instanceof TextView) {
+                        setViewText((TextView) v, text);
+                    } else if (v instanceof ImageView) {
+                        setViewImage((ImageView) v, text);
+                    } else {
+                        throw new IllegalStateException("SimpleCursorTreeAdapter can bind values" +
+                                " only to TextView and ImageView!");
+                    }
+                }
+            }
+        }
+    }
+    
+    private void initFromColumns(Cursor cursor, String[] fromColumnNames, int[] fromColumns) {
+        for (int i = fromColumnNames.length - 1; i >= 0; i--) {
+            fromColumns[i] = cursor.getColumnIndexOrThrow(fromColumnNames[i]);
+        }
+    }
+    
+    @Override
+    protected void bindChildView(View view, Context context, Cursor cursor, boolean isLastChild) {
+        if (mChildFrom == null) {
+            mChildFrom = new int[mChildFromNames.length];
+            initFromColumns(cursor, mChildFromNames, mChildFrom);
+        }
+        
+        bindView(view, context, cursor, mChildFrom, mChildTo);
+    }
+
+    @Override
+    protected void bindGroupView(View view, Context context, Cursor cursor, boolean isExpanded) {
+        if (mGroupFrom == null) {
+            mGroupFrom = new int[mGroupFromNames.length];
+            initFromColumns(cursor, mGroupFromNames, mGroupFrom);
+        }
+        
+        bindView(view, context, cursor, mGroupFrom, mGroupTo);
+    }
+
+    /**
+     * Called by bindView() to set the image for an ImageView. By default, the
+     * value will be treated as a Uri. Intended to be overridden by Adapters
+     * that need to filter strings retrieved from the database.
+     * 
+     * @param v ImageView to receive an image
+     * @param value the value retrieved from the cursor
+     */
+    protected void setViewImage(ImageView v, String value) {
+        try {
+            v.setImageResource(Integer.parseInt(value));
+        } catch (NumberFormatException nfe) {
+            v.setImageURI(Uri.parse(value));
+        }
+    }
+
+    /**
+     * Called by bindView() to set the text for a TextView but only if
+     * there is no existing ViewBinder or if the existing ViewBinder cannot
+     * handle binding to a TextView.
+     *
+     * Intended to be overridden by Adapters that need to filter strings
+     * retrieved from the database.
+     * 
+     * @param v TextView to receive text
+     * @param text the text to be set for the TextView
+     */
+    public void setViewText(TextView v, String text) {
+        v.setText(text);
+    }
+
+    /**
+     * This class can be used by external clients of SimpleCursorTreeAdapter
+     * to bind values from the Cursor to views.
+     *
+     * You should use this class to bind values from the Cursor to views
+     * that are not directly supported by SimpleCursorTreeAdapter or to
+     * change the way binding occurs for views supported by
+     * SimpleCursorTreeAdapter.
+     *
+     * @see SimpleCursorTreeAdapter#setViewImage(ImageView, String) 
+     * @see SimpleCursorTreeAdapter#setViewText(TextView, String)
+     */
+    public static interface ViewBinder {
+        /**
+         * Binds the Cursor column defined by the specified index to the specified view.
+         *
+         * When binding is handled by this ViewBinder, this method must return true.
+         * If this method returns false, SimpleCursorTreeAdapter will attempts to handle
+         * the binding on its own.
+         *
+         * @param view the view to bind the data to
+         * @param cursor the cursor to get the data from
+         * @param columnIndex the column at which the data can be found in the cursor
+         *
+         * @return true if the data was bound to the view, false otherwise
+         */
+        boolean setViewValue(View view, Cursor cursor, int columnIndex);
+    }
+}
diff --git a/android/widget/SimpleExpandableListAdapter.java b/android/widget/SimpleExpandableListAdapter.java
new file mode 100644
index 0000000..597502b
--- /dev/null
+++ b/android/widget/SimpleExpandableListAdapter.java
@@ -0,0 +1,301 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * An easy adapter to map static data to group and child views defined in an XML
+ * file. You can separately specify the data backing the group as a List of
+ * Maps. Each entry in the ArrayList corresponds to one group in the expandable
+ * list. The Maps contain the data for each row. You also specify an XML file
+ * that defines the views used to display a group, and a mapping from keys in
+ * the Map to specific views. This process is similar for a child, except it is
+ * one-level deeper so the data backing is specified as a List<List<Map>>,
+ * where the first List corresponds to the group of the child, the second List
+ * corresponds to the position of the child within the group, and finally the
+ * Map holds the data for that particular child.
+ */
+public class SimpleExpandableListAdapter extends BaseExpandableListAdapter {
+    private List<? extends Map<String, ?>> mGroupData;
+    private int mExpandedGroupLayout;
+    private int mCollapsedGroupLayout;
+    private String[] mGroupFrom;
+    private int[] mGroupTo;
+
+    private List<? extends List<? extends Map<String, ?>>> mChildData;
+    private int mChildLayout;
+    private int mLastChildLayout;
+    private String[] mChildFrom;
+    private int[] mChildTo;
+
+    private LayoutInflater mInflater;
+
+    /**
+     * Constructor
+     *
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleExpandableListAdapter} is
+     *            running
+     * @param groupData A List of Maps. Each entry in the List corresponds to
+     *            one group in the list. The Maps contain the data for each
+     *            group, and should include all the entries specified in
+     *            "groupFrom"
+     * @param groupFrom A list of keys that will be fetched from the Map
+     *            associated with each group.
+     * @param groupTo The group views that should display column in the
+     *            "groupFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the groupFrom parameter.
+     * @param groupLayout resource identifier of a view layout that defines the
+     *            views for a group. The layout file should include at least
+     *            those named views defined in "groupTo"
+     * @param childData A List of List of Maps. Each entry in the outer List
+     *            corresponds to a group (index by group position), each entry
+     *            in the inner List corresponds to a child within the group
+     *            (index by child position), and the Map corresponds to the data
+     *            for a child (index by values in the childFrom array). The Map
+     *            contains the data for each child, and should include all the
+     *            entries specified in "childFrom"
+     * @param childFrom A list of keys that will be fetched from the Map
+     *            associated with each child.
+     * @param childTo The child views that should display column in the
+     *            "childFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the childFrom parameter.
+     * @param childLayout resource identifier of a view layout that defines the
+     *            views for a child. The layout file should include at least
+     *            those named views defined in "childTo"
+     */
+    public SimpleExpandableListAdapter(Context context,
+            List<? extends Map<String, ?>> groupData, int groupLayout,
+            String[] groupFrom, int[] groupTo,
+            List<? extends List<? extends Map<String, ?>>> childData,
+            int childLayout, String[] childFrom, int[] childTo) {
+        this(context, groupData, groupLayout, groupLayout, groupFrom, groupTo, childData,
+                childLayout, childLayout, childFrom, childTo);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleExpandableListAdapter} is
+     *            running
+     * @param groupData A List of Maps. Each entry in the List corresponds to
+     *            one group in the list. The Maps contain the data for each
+     *            group, and should include all the entries specified in
+     *            "groupFrom"
+     * @param groupFrom A list of keys that will be fetched from the Map
+     *            associated with each group.
+     * @param groupTo The group views that should display column in the
+     *            "groupFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the groupFrom parameter.
+     * @param expandedGroupLayout resource identifier of a view layout that
+     *            defines the views for an expanded group. The layout file
+     *            should include at least those named views defined in "groupTo"
+     * @param collapsedGroupLayout resource identifier of a view layout that
+     *            defines the views for a collapsed group. The layout file
+     *            should include at least those named views defined in "groupTo"
+     * @param childData A List of List of Maps. Each entry in the outer List
+     *            corresponds to a group (index by group position), each entry
+     *            in the inner List corresponds to a child within the group
+     *            (index by child position), and the Map corresponds to the data
+     *            for a child (index by values in the childFrom array). The Map
+     *            contains the data for each child, and should include all the
+     *            entries specified in "childFrom"
+     * @param childFrom A list of keys that will be fetched from the Map
+     *            associated with each child.
+     * @param childTo The child views that should display column in the
+     *            "childFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the childFrom parameter.
+     * @param childLayout resource identifier of a view layout that defines the
+     *            views for a child. The layout file should include at least
+     *            those named views defined in "childTo"
+     */
+    public SimpleExpandableListAdapter(Context context,
+            List<? extends Map<String, ?>> groupData, int expandedGroupLayout,
+            int collapsedGroupLayout, String[] groupFrom, int[] groupTo,
+            List<? extends List<? extends Map<String, ?>>> childData,
+            int childLayout, String[] childFrom, int[] childTo) {
+        this(context, groupData, expandedGroupLayout, collapsedGroupLayout,
+                groupFrom, groupTo, childData, childLayout, childLayout,
+                childFrom, childTo);
+    }
+
+    /**
+     * Constructor
+     *
+     * @param context The context where the {@link ExpandableListView}
+     *            associated with this {@link SimpleExpandableListAdapter} is
+     *            running
+     * @param groupData A List of Maps. Each entry in the List corresponds to
+     *            one group in the list. The Maps contain the data for each
+     *            group, and should include all the entries specified in
+     *            "groupFrom"
+     * @param groupFrom A list of keys that will be fetched from the Map
+     *            associated with each group.
+     * @param groupTo The group views that should display column in the
+     *            "groupFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the groupFrom parameter.
+     * @param expandedGroupLayout resource identifier of a view layout that
+     *            defines the views for an expanded group. The layout file
+     *            should include at least those named views defined in "groupTo"
+     * @param collapsedGroupLayout resource identifier of a view layout that
+     *            defines the views for a collapsed group. The layout file
+     *            should include at least those named views defined in "groupTo"
+     * @param childData A List of List of Maps. Each entry in the outer List
+     *            corresponds to a group (index by group position), each entry
+     *            in the inner List corresponds to a child within the group
+     *            (index by child position), and the Map corresponds to the data
+     *            for a child (index by values in the childFrom array). The Map
+     *            contains the data for each child, and should include all the
+     *            entries specified in "childFrom"
+     * @param childFrom A list of keys that will be fetched from the Map
+     *            associated with each child.
+     * @param childTo The child views that should display column in the
+     *            "childFrom" parameter. These should all be TextViews. The
+     *            first N views in this list are given the values of the first N
+     *            columns in the childFrom parameter.
+     * @param childLayout resource identifier of a view layout that defines the
+     *            views for a child (unless it is the last child within a group,
+     *            in which case the lastChildLayout is used). The layout file
+     *            should include at least those named views defined in "childTo"
+     * @param lastChildLayout resource identifier of a view layout that defines
+     *            the views for the last child within each group. The layout
+     *            file should include at least those named views defined in
+     *            "childTo"
+     */
+    public SimpleExpandableListAdapter(Context context,
+            List<? extends Map<String, ?>> groupData, int expandedGroupLayout,
+            int collapsedGroupLayout, String[] groupFrom, int[] groupTo,
+            List<? extends List<? extends Map<String, ?>>> childData,
+            int childLayout, int lastChildLayout, String[] childFrom,
+            int[] childTo) {
+        mGroupData = groupData;
+        mExpandedGroupLayout = expandedGroupLayout;
+        mCollapsedGroupLayout = collapsedGroupLayout;
+        mGroupFrom = groupFrom;
+        mGroupTo = groupTo;
+
+        mChildData = childData;
+        mChildLayout = childLayout;
+        mLastChildLayout = lastChildLayout;
+        mChildFrom = childFrom;
+        mChildTo = childTo;
+
+        mInflater = (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+    }
+
+    public Object getChild(int groupPosition, int childPosition) {
+        return mChildData.get(groupPosition).get(childPosition);
+    }
+
+    public long getChildId(int groupPosition, int childPosition) {
+        return childPosition;
+    }
+
+    public View getChildView(int groupPosition, int childPosition, boolean isLastChild,
+            View convertView, ViewGroup parent) {
+        View v;
+        if (convertView == null) {
+            v = newChildView(isLastChild, parent);
+        } else {
+            v = convertView;
+        }
+        bindView(v, mChildData.get(groupPosition).get(childPosition), mChildFrom, mChildTo);
+        return v;
+    }
+
+    /**
+     * Instantiates a new View for a child.
+     * @param isLastChild Whether the child is the last child within its group.
+     * @param parent The eventual parent of this new View.
+     * @return A new child View
+     */
+    public View newChildView(boolean isLastChild, ViewGroup parent) {
+        return mInflater.inflate((isLastChild) ? mLastChildLayout : mChildLayout, parent, false);
+    }
+
+    private void bindView(View view, Map<String, ?> data, String[] from, int[] to) {
+        int len = to.length;
+
+        for (int i = 0; i < len; i++) {
+            TextView v = (TextView)view.findViewById(to[i]);
+            if (v != null) {
+                v.setText((String)data.get(from[i]));
+            }
+        }
+    }
+
+    public int getChildrenCount(int groupPosition) {
+        return mChildData.get(groupPosition).size();
+    }
+
+    public Object getGroup(int groupPosition) {
+        return mGroupData.get(groupPosition);
+    }
+
+    public int getGroupCount() {
+        return mGroupData.size();
+    }
+
+    public long getGroupId(int groupPosition) {
+        return groupPosition;
+    }
+
+    public View getGroupView(int groupPosition, boolean isExpanded, View convertView,
+            ViewGroup parent) {
+        View v;
+        if (convertView == null) {
+            v = newGroupView(isExpanded, parent);
+        } else {
+            v = convertView;
+        }
+        bindView(v, mGroupData.get(groupPosition), mGroupFrom, mGroupTo);
+        return v;
+    }
+
+    /**
+     * Instantiates a new View for a group.
+     * @param isExpanded Whether the group is currently expanded.
+     * @param parent The eventual parent of this new View.
+     * @return A new group View
+     */
+    public View newGroupView(boolean isExpanded, ViewGroup parent) {
+        return mInflater.inflate((isExpanded) ? mExpandedGroupLayout : mCollapsedGroupLayout,
+                parent, false);
+    }
+
+    public boolean isChildSelectable(int groupPosition, int childPosition) {
+        return true;
+    }
+
+    public boolean hasStableIds() {
+        return true;
+    }
+
+}
diff --git a/android/widget/SimpleMonthView.java b/android/widget/SimpleMonthView.java
new file mode 100644
index 0000000..9982732
--- /dev/null
+++ b/android/widget/SimpleMonthView.java
@@ -0,0 +1,1161 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Paint.Align;
+import android.graphics.Paint.Style;
+import android.graphics.Rect;
+import android.graphics.Typeface;
+import android.icu.text.DisplayContext;
+import android.icu.text.SimpleDateFormat;
+import android.icu.util.Calendar;
+import android.os.Bundle;
+import android.text.TextPaint;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.util.IntArray;
+import android.util.MathUtils;
+import android.util.StateSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewParent;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+
+import com.android.internal.R;
+import com.android.internal.widget.ExploreByTouchHelper;
+
+import libcore.icu.LocaleData;
+
+import java.text.NumberFormat;
+import java.util.Locale;
+
+/**
+ * A calendar-like view displaying a specified month and the appropriate selectable day numbers
+ * within the specified month.
+ */
+class SimpleMonthView extends View {
+    private static final int DAYS_IN_WEEK = 7;
+    private static final int MAX_WEEKS_IN_MONTH = 6;
+
+    private static final int DEFAULT_SELECTED_DAY = -1;
+    private static final int DEFAULT_WEEK_START = Calendar.SUNDAY;
+
+    private static final String MONTH_YEAR_FORMAT = "MMMMy";
+
+    private static final int SELECTED_HIGHLIGHT_ALPHA = 0xB0;
+
+    private final TextPaint mMonthPaint = new TextPaint();
+    private final TextPaint mDayOfWeekPaint = new TextPaint();
+    private final TextPaint mDayPaint = new TextPaint();
+    private final Paint mDaySelectorPaint = new Paint();
+    private final Paint mDayHighlightPaint = new Paint();
+    private final Paint mDayHighlightSelectorPaint = new Paint();
+
+    /** Array of single-character weekday labels ordered by column index. */
+    private final String[] mDayOfWeekLabels = new String[7];
+
+    private final Calendar mCalendar;
+    private final Locale mLocale;
+
+    private final MonthViewTouchHelper mTouchHelper;
+
+    private final NumberFormat mDayFormatter;
+
+    // Desired dimensions.
+    private final int mDesiredMonthHeight;
+    private final int mDesiredDayOfWeekHeight;
+    private final int mDesiredDayHeight;
+    private final int mDesiredCellWidth;
+    private final int mDesiredDaySelectorRadius;
+
+    private String mMonthYearLabel;
+
+    private int mMonth;
+    private int mYear;
+
+    // Dimensions as laid out.
+    private int mMonthHeight;
+    private int mDayOfWeekHeight;
+    private int mDayHeight;
+    private int mCellWidth;
+    private int mDaySelectorRadius;
+
+    private int mPaddedWidth;
+    private int mPaddedHeight;
+
+    /** The day of month for the selected day, or -1 if no day is selected. */
+    private int mActivatedDay = -1;
+
+    /**
+     * The day of month for today, or -1 if the today is not in the current
+     * month.
+     */
+    private int mToday = DEFAULT_SELECTED_DAY;
+
+    /** The first day of the week (ex. Calendar.SUNDAY) indexed from one. */
+    private int mWeekStart = DEFAULT_WEEK_START;
+
+    /** The number of days (ex. 28) in the current month. */
+    private int mDaysInMonth;
+
+    /**
+     * The day of week (ex. Calendar.SUNDAY) for the first day of the current
+     * month.
+     */
+    private int mDayOfWeekStart;
+
+    /** The day of month for the first (inclusive) enabled day. */
+    private int mEnabledDayStart = 1;
+
+    /** The day of month for the last (inclusive) enabled day. */
+    private int mEnabledDayEnd = 31;
+
+    /** Optional listener for handling day click actions. */
+    private OnDayClickListener mOnDayClickListener;
+
+    private ColorStateList mDayTextColor;
+
+    private int mHighlightedDay = -1;
+    private int mPreviouslyHighlightedDay = -1;
+    private boolean mIsTouchHighlighted = false;
+
+    public SimpleMonthView(Context context) {
+        this(context, null);
+    }
+
+    public SimpleMonthView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.datePickerStyle);
+    }
+
+    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public SimpleMonthView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final Resources res = context.getResources();
+        mDesiredMonthHeight = res.getDimensionPixelSize(R.dimen.date_picker_month_height);
+        mDesiredDayOfWeekHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_of_week_height);
+        mDesiredDayHeight = res.getDimensionPixelSize(R.dimen.date_picker_day_height);
+        mDesiredCellWidth = res.getDimensionPixelSize(R.dimen.date_picker_day_width);
+        mDesiredDaySelectorRadius = res.getDimensionPixelSize(
+                R.dimen.date_picker_day_selector_radius);
+
+        // Set up accessibility components.
+        mTouchHelper = new MonthViewTouchHelper(this);
+        setAccessibilityDelegate(mTouchHelper);
+        setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+
+        mLocale = res.getConfiguration().locale;
+        mCalendar = Calendar.getInstance(mLocale);
+
+        mDayFormatter = NumberFormat.getIntegerInstance(mLocale);
+
+        updateMonthYearLabel();
+        updateDayOfWeekLabels();
+
+        initPaints(res);
+    }
+
+    private void updateMonthYearLabel() {
+        final String format = DateFormat.getBestDateTimePattern(mLocale, MONTH_YEAR_FORMAT);
+        final SimpleDateFormat formatter = new SimpleDateFormat(format, mLocale);
+        formatter.setContext(DisplayContext.CAPITALIZATION_FOR_STANDALONE);
+        mMonthYearLabel = formatter.format(mCalendar.getTime());
+    }
+
+    private void updateDayOfWeekLabels() {
+        // Use tiny (e.g. single-character) weekday names from ICU. The indices
+        // for this list correspond to Calendar days, e.g. SUNDAY is index 1.
+        final String[] tinyWeekdayNames = LocaleData.get(mLocale).tinyWeekdayNames;
+        for (int i = 0; i < DAYS_IN_WEEK; i++) {
+            mDayOfWeekLabels[i] = tinyWeekdayNames[(mWeekStart + i - 1) % DAYS_IN_WEEK + 1];
+        }
+    }
+
+    /**
+     * Applies the specified text appearance resource to a paint, returning the
+     * text color if one is set in the text appearance.
+     *
+     * @param p the paint to modify
+     * @param resId the resource ID of the text appearance
+     * @return the text color, if available
+     */
+    private ColorStateList applyTextAppearance(Paint p, int resId) {
+        final TypedArray ta = mContext.obtainStyledAttributes(null,
+                R.styleable.TextAppearance, 0, resId);
+
+        final String fontFamily = ta.getString(R.styleable.TextAppearance_fontFamily);
+        if (fontFamily != null) {
+            p.setTypeface(Typeface.create(fontFamily, 0));
+        }
+
+        p.setTextSize(ta.getDimensionPixelSize(
+                R.styleable.TextAppearance_textSize, (int) p.getTextSize()));
+
+        final ColorStateList textColor = ta.getColorStateList(R.styleable.TextAppearance_textColor);
+        if (textColor != null) {
+            final int enabledColor = textColor.getColorForState(ENABLED_STATE_SET, 0);
+            p.setColor(enabledColor);
+        }
+
+        ta.recycle();
+
+        return textColor;
+    }
+
+    public int getMonthHeight() {
+        return mMonthHeight;
+    }
+
+    public int getCellWidth() {
+        return mCellWidth;
+    }
+
+    public void setMonthTextAppearance(int resId) {
+        applyTextAppearance(mMonthPaint, resId);
+
+        invalidate();
+    }
+
+    public void setDayOfWeekTextAppearance(int resId) {
+        applyTextAppearance(mDayOfWeekPaint, resId);
+        invalidate();
+    }
+
+    public void setDayTextAppearance(int resId) {
+        final ColorStateList textColor = applyTextAppearance(mDayPaint, resId);
+        if (textColor != null) {
+            mDayTextColor = textColor;
+        }
+
+        invalidate();
+    }
+
+    /**
+     * Sets up the text and style properties for painting.
+     */
+    private void initPaints(Resources res) {
+        final String monthTypeface = res.getString(R.string.date_picker_month_typeface);
+        final String dayOfWeekTypeface = res.getString(R.string.date_picker_day_of_week_typeface);
+        final String dayTypeface = res.getString(R.string.date_picker_day_typeface);
+
+        final int monthTextSize = res.getDimensionPixelSize(
+                R.dimen.date_picker_month_text_size);
+        final int dayOfWeekTextSize = res.getDimensionPixelSize(
+                R.dimen.date_picker_day_of_week_text_size);
+        final int dayTextSize = res.getDimensionPixelSize(
+                R.dimen.date_picker_day_text_size);
+
+        mMonthPaint.setAntiAlias(true);
+        mMonthPaint.setTextSize(monthTextSize);
+        mMonthPaint.setTypeface(Typeface.create(monthTypeface, 0));
+        mMonthPaint.setTextAlign(Align.CENTER);
+        mMonthPaint.setStyle(Style.FILL);
+
+        mDayOfWeekPaint.setAntiAlias(true);
+        mDayOfWeekPaint.setTextSize(dayOfWeekTextSize);
+        mDayOfWeekPaint.setTypeface(Typeface.create(dayOfWeekTypeface, 0));
+        mDayOfWeekPaint.setTextAlign(Align.CENTER);
+        mDayOfWeekPaint.setStyle(Style.FILL);
+
+        mDaySelectorPaint.setAntiAlias(true);
+        mDaySelectorPaint.setStyle(Style.FILL);
+
+        mDayHighlightPaint.setAntiAlias(true);
+        mDayHighlightPaint.setStyle(Style.FILL);
+
+        mDayHighlightSelectorPaint.setAntiAlias(true);
+        mDayHighlightSelectorPaint.setStyle(Style.FILL);
+
+        mDayPaint.setAntiAlias(true);
+        mDayPaint.setTextSize(dayTextSize);
+        mDayPaint.setTypeface(Typeface.create(dayTypeface, 0));
+        mDayPaint.setTextAlign(Align.CENTER);
+        mDayPaint.setStyle(Style.FILL);
+    }
+
+    void setMonthTextColor(ColorStateList monthTextColor) {
+        final int enabledColor = monthTextColor.getColorForState(ENABLED_STATE_SET, 0);
+        mMonthPaint.setColor(enabledColor);
+        invalidate();
+    }
+
+    void setDayOfWeekTextColor(ColorStateList dayOfWeekTextColor) {
+        final int enabledColor = dayOfWeekTextColor.getColorForState(ENABLED_STATE_SET, 0);
+        mDayOfWeekPaint.setColor(enabledColor);
+        invalidate();
+    }
+
+    void setDayTextColor(ColorStateList dayTextColor) {
+        mDayTextColor = dayTextColor;
+        invalidate();
+    }
+
+    void setDaySelectorColor(ColorStateList dayBackgroundColor) {
+        final int activatedColor = dayBackgroundColor.getColorForState(
+                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_ACTIVATED), 0);
+        mDaySelectorPaint.setColor(activatedColor);
+        mDayHighlightSelectorPaint.setColor(activatedColor);
+        mDayHighlightSelectorPaint.setAlpha(SELECTED_HIGHLIGHT_ALPHA);
+        invalidate();
+    }
+
+    void setDayHighlightColor(ColorStateList dayHighlightColor) {
+        final int pressedColor = dayHighlightColor.getColorForState(
+                StateSet.get(StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_PRESSED), 0);
+        mDayHighlightPaint.setColor(pressedColor);
+        invalidate();
+    }
+
+    public void setOnDayClickListener(OnDayClickListener listener) {
+        mOnDayClickListener = listener;
+    }
+
+    @Override
+    public boolean dispatchHoverEvent(MotionEvent event) {
+        // First right-of-refusal goes the touch exploration helper.
+        return mTouchHelper.dispatchHoverEvent(event) || super.dispatchHoverEvent(event);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int x = (int) (event.getX() + 0.5f);
+        final int y = (int) (event.getY() + 0.5f);
+
+        final int action = event.getAction();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+            case MotionEvent.ACTION_MOVE:
+                final int touchedItem = getDayAtLocation(x, y);
+                mIsTouchHighlighted = true;
+                if (mHighlightedDay != touchedItem) {
+                    mHighlightedDay = touchedItem;
+                    mPreviouslyHighlightedDay = touchedItem;
+                    invalidate();
+                }
+                if (action == MotionEvent.ACTION_DOWN && touchedItem < 0) {
+                    // Touch something that's not an item, reject event.
+                    return false;
+                }
+                break;
+
+            case MotionEvent.ACTION_UP:
+                final int clickedDay = getDayAtLocation(x, y);
+                onDayClicked(clickedDay);
+                // Fall through.
+            case MotionEvent.ACTION_CANCEL:
+                // Reset touched day on stream end.
+                mHighlightedDay = -1;
+                mIsTouchHighlighted = false;
+                invalidate();
+                break;
+        }
+        return true;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        // We need to handle focus change within the SimpleMonthView because we are simulating
+        // multiple Views. The arrow keys will move between days until there is no space (no
+        // day to the left, top, right, or bottom). Focus forward and back jumps out of the
+        // SimpleMonthView, skipping over other SimpleMonthViews in the parent ViewPager
+        // to the next focusable View in the hierarchy.
+        boolean focusChanged = false;
+        switch (event.getKeyCode()) {
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+                if (event.hasNoModifiers()) {
+                    focusChanged = moveOneDay(isLayoutRtl());
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                if (event.hasNoModifiers()) {
+                    focusChanged = moveOneDay(!isLayoutRtl());
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_UP:
+                if (event.hasNoModifiers()) {
+                    ensureFocusedDay();
+                    if (mHighlightedDay > 7) {
+                        mHighlightedDay -= 7;
+                        focusChanged = true;
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+                if (event.hasNoModifiers()) {
+                    ensureFocusedDay();
+                    if (mHighlightedDay <= mDaysInMonth - 7) {
+                        mHighlightedDay += 7;
+                        focusChanged = true;
+                    }
+                }
+                break;
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_ENTER:
+                if (mHighlightedDay != -1) {
+                    onDayClicked(mHighlightedDay);
+                    return true;
+                }
+                break;
+            case KeyEvent.KEYCODE_TAB: {
+                int focusChangeDirection = 0;
+                if (event.hasNoModifiers()) {
+                    focusChangeDirection = View.FOCUS_FORWARD;
+                } else if (event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                    focusChangeDirection = View.FOCUS_BACKWARD;
+                }
+                if (focusChangeDirection != 0) {
+                    final ViewParent parent = getParent();
+                    // move out of the ViewPager next/previous
+                    View nextFocus = this;
+                    do {
+                        nextFocus = nextFocus.focusSearch(focusChangeDirection);
+                    } while (nextFocus != null && nextFocus != this &&
+                            nextFocus.getParent() == parent);
+                    if (nextFocus != null) {
+                        nextFocus.requestFocus();
+                        return true;
+                    }
+                }
+                break;
+            }
+        }
+        if (focusChanged) {
+            invalidate();
+            return true;
+        } else {
+            return super.onKeyDown(keyCode, event);
+        }
+    }
+
+    private boolean moveOneDay(boolean positive) {
+        ensureFocusedDay();
+        boolean focusChanged = false;
+        if (positive) {
+            if (!isLastDayOfWeek(mHighlightedDay) && mHighlightedDay < mDaysInMonth) {
+                mHighlightedDay++;
+                focusChanged = true;
+            }
+        } else {
+            if (!isFirstDayOfWeek(mHighlightedDay) && mHighlightedDay > 1) {
+                mHighlightedDay--;
+                focusChanged = true;
+            }
+        }
+        return focusChanged;
+    }
+
+    @Override
+    protected void onFocusChanged(boolean gainFocus, @FocusDirection int direction,
+            @Nullable Rect previouslyFocusedRect) {
+        if (gainFocus) {
+            // If we've gained focus through arrow keys, we should find the day closest
+            // to the focus rect. If we've gained focus through forward/back, we should
+            // focus on the selected day if there is one.
+            final int offset = findDayOffset();
+            switch(direction) {
+                case View.FOCUS_RIGHT: {
+                    int row = findClosestRow(previouslyFocusedRect);
+                    mHighlightedDay = row == 0 ? 1 : (row * DAYS_IN_WEEK) - offset + 1;
+                    break;
+                }
+                case View.FOCUS_LEFT: {
+                    int row = findClosestRow(previouslyFocusedRect) + 1;
+                    mHighlightedDay = Math.min(mDaysInMonth, (row * DAYS_IN_WEEK) - offset);
+                    break;
+                }
+                case View.FOCUS_DOWN: {
+                    final int col = findClosestColumn(previouslyFocusedRect);
+                    final int day = col - offset + 1;
+                    mHighlightedDay = day < 1 ? day + DAYS_IN_WEEK : day;
+                    break;
+                }
+                case View.FOCUS_UP: {
+                    final int col = findClosestColumn(previouslyFocusedRect);
+                    final int maxWeeks = (offset + mDaysInMonth) / DAYS_IN_WEEK;
+                    final int day = col - offset + (DAYS_IN_WEEK * maxWeeks) + 1;
+                    mHighlightedDay = day > mDaysInMonth ? day - DAYS_IN_WEEK : day;
+                    break;
+                }
+            }
+            ensureFocusedDay();
+            invalidate();
+        }
+        super.onFocusChanged(gainFocus, direction, previouslyFocusedRect);
+    }
+
+    /**
+     * Returns the row (0 indexed) closest to previouslyFocusedRect or center if null.
+     */
+    private int findClosestRow(@Nullable Rect previouslyFocusedRect) {
+        if (previouslyFocusedRect == null) {
+            return 3;
+        } else if (mDayHeight == 0) {
+            return 0; // There hasn't been a layout, so just choose the first row
+        } else {
+            int centerY = previouslyFocusedRect.centerY();
+
+            final TextPaint p = mDayPaint;
+            final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+            final int rowHeight = mDayHeight;
+
+            // Text is vertically centered within the row height.
+            final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+            final int rowCenter = headerHeight + rowHeight / 2;
+
+            centerY -= rowCenter - halfLineHeight;
+            int row = Math.round(centerY / (float) rowHeight);
+            final int maxDay = findDayOffset() + mDaysInMonth;
+            final int maxRows = (maxDay / DAYS_IN_WEEK) - ((maxDay % DAYS_IN_WEEK == 0) ? 1 : 0);
+
+            row = MathUtils.constrain(row, 0, maxRows);
+            return row;
+        }
+    }
+
+    /**
+     * Returns the column (0 indexed) closest to the previouslyFocusedRect or center if null.
+     * The 0 index is related to the first day of the week.
+     */
+    private int findClosestColumn(@Nullable Rect previouslyFocusedRect) {
+        if (previouslyFocusedRect == null) {
+            return DAYS_IN_WEEK / 2;
+        } else if (mCellWidth == 0) {
+            return 0; // There hasn't been a layout, so we can just choose the first column
+        } else {
+            int centerX = previouslyFocusedRect.centerX() - mPaddingLeft;
+            final int columnFromLeft =
+                    MathUtils.constrain(centerX / mCellWidth, 0, DAYS_IN_WEEK - 1);
+            return isLayoutRtl() ? DAYS_IN_WEEK - columnFromLeft - 1: columnFromLeft;
+        }
+    }
+
+    @Override
+    public void getFocusedRect(Rect r) {
+        if (mHighlightedDay > 0) {
+            getBoundsForDay(mHighlightedDay, r);
+        } else {
+            super.getFocusedRect(r);
+        }
+    }
+
+    @Override
+    protected void onFocusLost() {
+        if (!mIsTouchHighlighted) {
+            // Unhighlight a day.
+            mPreviouslyHighlightedDay = mHighlightedDay;
+            mHighlightedDay = -1;
+            invalidate();
+        }
+        super.onFocusLost();
+    }
+
+    /**
+     * Ensure some day is highlighted. If a day isn't highlighted, it chooses the selected day,
+     * if possible, or the first day of the month if not.
+     */
+    private void ensureFocusedDay() {
+        if (mHighlightedDay != -1) {
+            return;
+        }
+        if (mPreviouslyHighlightedDay != -1) {
+            mHighlightedDay = mPreviouslyHighlightedDay;
+            return;
+        }
+        if (mActivatedDay != -1) {
+            mHighlightedDay = mActivatedDay;
+            return;
+        }
+        mHighlightedDay = 1;
+    }
+
+    private boolean isFirstDayOfWeek(int day) {
+        final int offset = findDayOffset();
+        return (offset + day - 1) % DAYS_IN_WEEK == 0;
+    }
+
+    private boolean isLastDayOfWeek(int day) {
+        final int offset = findDayOffset();
+        return (offset + day) % DAYS_IN_WEEK == 0;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        final int paddingLeft = getPaddingLeft();
+        final int paddingTop = getPaddingTop();
+        canvas.translate(paddingLeft, paddingTop);
+
+        drawMonth(canvas);
+        drawDaysOfWeek(canvas);
+        drawDays(canvas);
+
+        canvas.translate(-paddingLeft, -paddingTop);
+    }
+
+    private void drawMonth(Canvas canvas) {
+        final float x = mPaddedWidth / 2f;
+
+        // Vertically centered within the month header height.
+        final float lineHeight = mMonthPaint.ascent() + mMonthPaint.descent();
+        final float y = (mMonthHeight - lineHeight) / 2f;
+
+        canvas.drawText(mMonthYearLabel, x, y, mMonthPaint);
+    }
+
+    public String getMonthYearLabel() {
+        return mMonthYearLabel;
+    }
+
+    private void drawDaysOfWeek(Canvas canvas) {
+        final TextPaint p = mDayOfWeekPaint;
+        final int headerHeight = mMonthHeight;
+        final int rowHeight = mDayOfWeekHeight;
+        final int colWidth = mCellWidth;
+
+        // Text is vertically centered within the day of week height.
+        final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+        final int rowCenter = headerHeight + rowHeight / 2;
+
+        for (int col = 0; col < DAYS_IN_WEEK; col++) {
+            final int colCenter = colWidth * col + colWidth / 2;
+            final int colCenterRtl;
+            if (isLayoutRtl()) {
+                colCenterRtl = mPaddedWidth - colCenter;
+            } else {
+                colCenterRtl = colCenter;
+            }
+
+            final String label = mDayOfWeekLabels[col];
+            canvas.drawText(label, colCenterRtl, rowCenter - halfLineHeight, p);
+        }
+    }
+
+    /**
+     * Draws the month days.
+     */
+    private void drawDays(Canvas canvas) {
+        final TextPaint p = mDayPaint;
+        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+        final int rowHeight = mDayHeight;
+        final int colWidth = mCellWidth;
+
+        // Text is vertically centered within the row height.
+        final float halfLineHeight = (p.ascent() + p.descent()) / 2f;
+        int rowCenter = headerHeight + rowHeight / 2;
+
+        for (int day = 1, col = findDayOffset(); day <= mDaysInMonth; day++) {
+            final int colCenter = colWidth * col + colWidth / 2;
+            final int colCenterRtl;
+            if (isLayoutRtl()) {
+                colCenterRtl = mPaddedWidth - colCenter;
+            } else {
+                colCenterRtl = colCenter;
+            }
+
+            int stateMask = 0;
+
+            final boolean isDayEnabled = isDayEnabled(day);
+            if (isDayEnabled) {
+                stateMask |= StateSet.VIEW_STATE_ENABLED;
+            }
+
+            final boolean isDayActivated = mActivatedDay == day;
+            final boolean isDayHighlighted = mHighlightedDay == day;
+            if (isDayActivated) {
+                stateMask |= StateSet.VIEW_STATE_ACTIVATED;
+
+                // Adjust the circle to be centered on the row.
+                final Paint paint = isDayHighlighted ? mDayHighlightSelectorPaint :
+                        mDaySelectorPaint;
+                canvas.drawCircle(colCenterRtl, rowCenter, mDaySelectorRadius, paint);
+            } else if (isDayHighlighted) {
+                stateMask |= StateSet.VIEW_STATE_PRESSED;
+
+                if (isDayEnabled) {
+                    // Adjust the circle to be centered on the row.
+                    canvas.drawCircle(colCenterRtl, rowCenter,
+                            mDaySelectorRadius, mDayHighlightPaint);
+                }
+            }
+
+            final boolean isDayToday = mToday == day;
+            final int dayTextColor;
+            if (isDayToday && !isDayActivated) {
+                dayTextColor = mDaySelectorPaint.getColor();
+            } else {
+                final int[] stateSet = StateSet.get(stateMask);
+                dayTextColor = mDayTextColor.getColorForState(stateSet, 0);
+            }
+            p.setColor(dayTextColor);
+
+            canvas.drawText(mDayFormatter.format(day), colCenterRtl, rowCenter - halfLineHeight, p);
+
+            col++;
+
+            if (col == DAYS_IN_WEEK) {
+                col = 0;
+                rowCenter += rowHeight;
+            }
+        }
+    }
+
+    private boolean isDayEnabled(int day) {
+        return day >= mEnabledDayStart && day <= mEnabledDayEnd;
+    }
+
+    private boolean isValidDayOfMonth(int day) {
+        return day >= 1 && day <= mDaysInMonth;
+    }
+
+    private static boolean isValidDayOfWeek(int day) {
+        return day >= Calendar.SUNDAY && day <= Calendar.SATURDAY;
+    }
+
+    private static boolean isValidMonth(int month) {
+        return month >= Calendar.JANUARY && month <= Calendar.DECEMBER;
+    }
+
+    /**
+     * Sets the selected day.
+     *
+     * @param dayOfMonth the selected day of the month, or {@code -1} to clear
+     *                   the selection
+     */
+    public void setSelectedDay(int dayOfMonth) {
+        mActivatedDay = dayOfMonth;
+
+        // Invalidate cached accessibility information.
+        mTouchHelper.invalidateRoot();
+        invalidate();
+    }
+
+    /**
+     * Sets the first day of the week.
+     *
+     * @param weekStart which day the week should start on, valid values are
+     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+     */
+    public void setFirstDayOfWeek(int weekStart) {
+        if (isValidDayOfWeek(weekStart)) {
+            mWeekStart = weekStart;
+        } else {
+            mWeekStart = mCalendar.getFirstDayOfWeek();
+        }
+
+        updateDayOfWeekLabels();
+
+        // Invalidate cached accessibility information.
+        mTouchHelper.invalidateRoot();
+        invalidate();
+    }
+
+    /**
+     * Sets all the parameters for displaying this week.
+     * <p>
+     * Parameters have a default value and will only update if a new value is
+     * included, except for focus month, which will always default to no focus
+     * month if no value is passed in. The only required parameter is the week
+     * start.
+     *
+     * @param selectedDay the selected day of the month, or -1 for no selection
+     * @param month the month
+     * @param year the year
+     * @param weekStart which day the week should start on, valid values are
+     *                  {@link Calendar#SUNDAY} through {@link Calendar#SATURDAY}
+     * @param enabledDayStart the first enabled day
+     * @param enabledDayEnd the last enabled day
+     */
+    void setMonthParams(int selectedDay, int month, int year, int weekStart, int enabledDayStart,
+            int enabledDayEnd) {
+        mActivatedDay = selectedDay;
+
+        if (isValidMonth(month)) {
+            mMonth = month;
+        }
+        mYear = year;
+
+        mCalendar.set(Calendar.MONTH, mMonth);
+        mCalendar.set(Calendar.YEAR, mYear);
+        mCalendar.set(Calendar.DAY_OF_MONTH, 1);
+        mDayOfWeekStart = mCalendar.get(Calendar.DAY_OF_WEEK);
+
+        if (isValidDayOfWeek(weekStart)) {
+            mWeekStart = weekStart;
+        } else {
+            mWeekStart = mCalendar.getFirstDayOfWeek();
+        }
+
+        // Figure out what day today is.
+        final Calendar today = Calendar.getInstance();
+        mToday = -1;
+        mDaysInMonth = getDaysInMonth(mMonth, mYear);
+        for (int i = 0; i < mDaysInMonth; i++) {
+            final int day = i + 1;
+            if (sameDay(day, today)) {
+                mToday = day;
+            }
+        }
+
+        mEnabledDayStart = MathUtils.constrain(enabledDayStart, 1, mDaysInMonth);
+        mEnabledDayEnd = MathUtils.constrain(enabledDayEnd, mEnabledDayStart, mDaysInMonth);
+
+        updateMonthYearLabel();
+        updateDayOfWeekLabels();
+
+        // Invalidate cached accessibility information.
+        mTouchHelper.invalidateRoot();
+        invalidate();
+    }
+
+    private static int getDaysInMonth(int month, int year) {
+        switch (month) {
+            case Calendar.JANUARY:
+            case Calendar.MARCH:
+            case Calendar.MAY:
+            case Calendar.JULY:
+            case Calendar.AUGUST:
+            case Calendar.OCTOBER:
+            case Calendar.DECEMBER:
+                return 31;
+            case Calendar.APRIL:
+            case Calendar.JUNE:
+            case Calendar.SEPTEMBER:
+            case Calendar.NOVEMBER:
+                return 30;
+            case Calendar.FEBRUARY:
+                return (year % 4 == 0) ? 29 : 28;
+            default:
+                throw new IllegalArgumentException("Invalid Month");
+        }
+    }
+
+    private boolean sameDay(int day, Calendar today) {
+        return mYear == today.get(Calendar.YEAR) && mMonth == today.get(Calendar.MONTH)
+                && day == today.get(Calendar.DAY_OF_MONTH);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        final int preferredHeight = mDesiredDayHeight * MAX_WEEKS_IN_MONTH
+                + mDesiredDayOfWeekHeight + mDesiredMonthHeight
+                + getPaddingTop() + getPaddingBottom();
+        final int preferredWidth = mDesiredCellWidth * DAYS_IN_WEEK
+                + getPaddingStart() + getPaddingEnd();
+        final int resolvedWidth = resolveSize(preferredWidth, widthMeasureSpec);
+        final int resolvedHeight = resolveSize(preferredHeight, heightMeasureSpec);
+        setMeasuredDimension(resolvedWidth, resolvedHeight);
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        requestLayout();
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        if (!changed) {
+            return;
+        }
+
+        // Let's initialize a completely reasonable number of variables.
+        final int w = right - left;
+        final int h = bottom - top;
+        final int paddingLeft = getPaddingLeft();
+        final int paddingTop = getPaddingTop();
+        final int paddingRight = getPaddingRight();
+        final int paddingBottom = getPaddingBottom();
+        final int paddedRight = w - paddingRight;
+        final int paddedBottom = h - paddingBottom;
+        final int paddedWidth = paddedRight - paddingLeft;
+        final int paddedHeight = paddedBottom - paddingTop;
+        if (paddedWidth == mPaddedWidth || paddedHeight == mPaddedHeight) {
+            return;
+        }
+
+        mPaddedWidth = paddedWidth;
+        mPaddedHeight = paddedHeight;
+
+        // We may have been laid out smaller than our preferred size. If so,
+        // scale all dimensions to fit.
+        final int measuredPaddedHeight = getMeasuredHeight() - paddingTop - paddingBottom;
+        final float scaleH = paddedHeight / (float) measuredPaddedHeight;
+        final int monthHeight = (int) (mDesiredMonthHeight * scaleH);
+        final int cellWidth = mPaddedWidth / DAYS_IN_WEEK;
+        mMonthHeight = monthHeight;
+        mDayOfWeekHeight = (int) (mDesiredDayOfWeekHeight * scaleH);
+        mDayHeight = (int) (mDesiredDayHeight * scaleH);
+        mCellWidth = cellWidth;
+
+        // Compute the largest day selector radius that's still within the clip
+        // bounds and desired selector radius.
+        final int maxSelectorWidth = cellWidth / 2 + Math.min(paddingLeft, paddingRight);
+        final int maxSelectorHeight = mDayHeight / 2 + paddingBottom;
+        mDaySelectorRadius = Math.min(mDesiredDaySelectorRadius,
+                Math.min(maxSelectorWidth, maxSelectorHeight));
+
+        // Invalidate cached accessibility information.
+        mTouchHelper.invalidateRoot();
+    }
+
+    private int findDayOffset() {
+        final int offset = mDayOfWeekStart - mWeekStart;
+        if (mDayOfWeekStart < mWeekStart) {
+            return offset + DAYS_IN_WEEK;
+        }
+        return offset;
+    }
+
+    /**
+     * Calculates the day of the month at the specified touch position. Returns
+     * the day of the month or -1 if the position wasn't in a valid day.
+     *
+     * @param x the x position of the touch event
+     * @param y the y position of the touch event
+     * @return the day of the month at (x, y), or -1 if the position wasn't in
+     *         a valid day
+     */
+    private int getDayAtLocation(int x, int y) {
+        final int paddedX = x - getPaddingLeft();
+        if (paddedX < 0 || paddedX >= mPaddedWidth) {
+            return -1;
+        }
+
+        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+        final int paddedY = y - getPaddingTop();
+        if (paddedY < headerHeight || paddedY >= mPaddedHeight) {
+            return -1;
+        }
+
+        // Adjust for RTL after applying padding.
+        final int paddedXRtl;
+        if (isLayoutRtl()) {
+            paddedXRtl = mPaddedWidth - paddedX;
+        } else {
+            paddedXRtl = paddedX;
+        }
+
+        final int row = (paddedY - headerHeight) / mDayHeight;
+        final int col = (paddedXRtl * DAYS_IN_WEEK) / mPaddedWidth;
+        final int index = col + row * DAYS_IN_WEEK;
+        final int day = index + 1 - findDayOffset();
+        if (!isValidDayOfMonth(day)) {
+            return -1;
+        }
+
+        return day;
+    }
+
+    /**
+     * Calculates the bounds of the specified day.
+     *
+     * @param id the day of the month
+     * @param outBounds the rect to populate with bounds
+     */
+    public boolean getBoundsForDay(int id, Rect outBounds) {
+        if (!isValidDayOfMonth(id)) {
+            return false;
+        }
+
+        final int index = id - 1 + findDayOffset();
+
+        // Compute left edge, taking into account RTL.
+        final int col = index % DAYS_IN_WEEK;
+        final int colWidth = mCellWidth;
+        final int left;
+        if (isLayoutRtl()) {
+            left = getWidth() - getPaddingRight() - (col + 1) * colWidth;
+        } else {
+            left = getPaddingLeft() + col * colWidth;
+        }
+
+        // Compute top edge.
+        final int row = index / DAYS_IN_WEEK;
+        final int rowHeight = mDayHeight;
+        final int headerHeight = mMonthHeight + mDayOfWeekHeight;
+        final int top = getPaddingTop() + headerHeight + row * rowHeight;
+
+        outBounds.set(left, top, left + colWidth, top + rowHeight);
+
+        return true;
+    }
+
+    /**
+     * Called when the user clicks on a day. Handles callbacks to the
+     * {@link OnDayClickListener} if one is set.
+     *
+     * @param day the day that was clicked
+     */
+    private boolean onDayClicked(int day) {
+        if (!isValidDayOfMonth(day) || !isDayEnabled(day)) {
+            return false;
+        }
+
+        if (mOnDayClickListener != null) {
+            final Calendar date = Calendar.getInstance();
+            date.set(mYear, mMonth, day);
+            mOnDayClickListener.onDayClick(this, date);
+        }
+
+        // This is a no-op if accessibility is turned off.
+        mTouchHelper.sendEventForVirtualView(day, AccessibilityEvent.TYPE_VIEW_CLICKED);
+        return true;
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (!isEnabled()) {
+            return null;
+        }
+        // Add 0.5f to event coordinates to match the logic in onTouchEvent.
+        final int x = (int) (event.getX() + 0.5f);
+        final int y = (int) (event.getY() + 0.5f);
+        final int dayUnderPointer = getDayAtLocation(x, y);
+        if (dayUnderPointer >= 0) {
+            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    /**
+     * Provides a virtual view hierarchy for interfacing with an accessibility
+     * service.
+     */
+    private class MonthViewTouchHelper extends ExploreByTouchHelper {
+        private static final String DATE_FORMAT = "dd MMMM yyyy";
+
+        private final Rect mTempRect = new Rect();
+        private final Calendar mTempCalendar = Calendar.getInstance();
+
+        public MonthViewTouchHelper(View host) {
+            super(host);
+        }
+
+        @Override
+        protected int getVirtualViewAt(float x, float y) {
+            final int day = getDayAtLocation((int) (x + 0.5f), (int) (y + 0.5f));
+            if (day != -1) {
+                return day;
+            }
+            return ExploreByTouchHelper.INVALID_ID;
+        }
+
+        @Override
+        protected void getVisibleVirtualViews(IntArray virtualViewIds) {
+            for (int day = 1; day <= mDaysInMonth; day++) {
+                virtualViewIds.add(day);
+            }
+        }
+
+        @Override
+        protected void onPopulateEventForVirtualView(int virtualViewId, AccessibilityEvent event) {
+            event.setContentDescription(getDayDescription(virtualViewId));
+        }
+
+        @Override
+        protected void onPopulateNodeForVirtualView(int virtualViewId, AccessibilityNodeInfo node) {
+            final boolean hasBounds = getBoundsForDay(virtualViewId, mTempRect);
+
+            if (!hasBounds) {
+                // The day is invalid, kill the node.
+                mTempRect.setEmpty();
+                node.setContentDescription("");
+                node.setBoundsInParent(mTempRect);
+                node.setVisibleToUser(false);
+                return;
+            }
+
+            node.setText(getDayText(virtualViewId));
+            node.setContentDescription(getDayDescription(virtualViewId));
+            node.setBoundsInParent(mTempRect);
+
+            final boolean isDayEnabled = isDayEnabled(virtualViewId);
+            if (isDayEnabled) {
+                node.addAction(AccessibilityAction.ACTION_CLICK);
+            }
+
+            node.setEnabled(isDayEnabled);
+
+            if (virtualViewId == mActivatedDay) {
+                // TODO: This should use activated once that's supported.
+                node.setChecked(true);
+            }
+
+        }
+
+        @Override
+        protected boolean onPerformActionForVirtualView(int virtualViewId, int action,
+                Bundle arguments) {
+            switch (action) {
+                case AccessibilityNodeInfo.ACTION_CLICK:
+                    return onDayClicked(virtualViewId);
+            }
+
+            return false;
+        }
+
+        /**
+         * Generates a description for a given virtual view.
+         *
+         * @param id the day to generate a description for
+         * @return a description of the virtual view
+         */
+        private CharSequence getDayDescription(int id) {
+            if (isValidDayOfMonth(id)) {
+                mTempCalendar.set(mYear, mMonth, id);
+                return DateFormat.format(DATE_FORMAT, mTempCalendar.getTimeInMillis());
+            }
+
+            return "";
+        }
+
+        /**
+         * Generates displayed text for a given virtual view.
+         *
+         * @param id the day to generate text for
+         * @return the visible text of the virtual view
+         */
+        private CharSequence getDayText(int id) {
+            if (isValidDayOfMonth(id)) {
+                return mDayFormatter.format(id);
+            }
+
+            return null;
+        }
+    }
+
+    /**
+     * Handles callbacks when the user clicks on a time object.
+     */
+    public interface OnDayClickListener {
+        void onDayClick(SimpleMonthView view, Calendar day);
+    }
+}
diff --git a/android/widget/SlidingDrawer.java b/android/widget/SlidingDrawer.java
new file mode 100644
index 0000000..9f48397
--- /dev/null
+++ b/android/widget/SlidingDrawer.java
@@ -0,0 +1,979 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.R;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.os.SystemClock;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+/**
+ * SlidingDrawer hides content out of the screen and allows the user to drag a handle
+ * to bring the content on screen. SlidingDrawer can be used vertically or horizontally.
+ *
+ * A special widget composed of two children views: the handle, that the users drags,
+ * and the content, attached to the handle and dragged with it.
+ *
+ * SlidingDrawer should be used as an overlay inside layouts. This means SlidingDrawer
+ * should only be used inside of a FrameLayout or a RelativeLayout for instance. The
+ * size of the SlidingDrawer defines how much space the content will occupy once slid
+ * out so SlidingDrawer should usually use match_parent for both its dimensions.
+ *
+ * Inside an XML layout, SlidingDrawer must define the id of the handle and of the
+ * content:
+ *
+ * <pre class="prettyprint">
+ * &lt;SlidingDrawer
+ *     android:id="@+id/drawer"
+ *     android:layout_width="match_parent"
+ *     android:layout_height="match_parent"
+ *
+ *     android:handle="@+id/handle"
+ *     android:content="@+id/content"&gt;
+ *
+ *     &lt;ImageView
+ *         android:id="@id/handle"
+ *         android:layout_width="88dip"
+ *         android:layout_height="44dip" /&gt;
+ *
+ *     &lt;GridView
+ *         android:id="@id/content"
+ *         android:layout_width="match_parent"
+ *         android:layout_height="match_parent" /&gt;
+ *
+ * &lt;/SlidingDrawer&gt;
+ * </pre>
+ *
+ * @attr ref android.R.styleable#SlidingDrawer_content
+ * @attr ref android.R.styleable#SlidingDrawer_handle
+ * @attr ref android.R.styleable#SlidingDrawer_topOffset
+ * @attr ref android.R.styleable#SlidingDrawer_bottomOffset
+ * @attr ref android.R.styleable#SlidingDrawer_orientation
+ * @attr ref android.R.styleable#SlidingDrawer_allowSingleTap
+ * @attr ref android.R.styleable#SlidingDrawer_animateOnClick
+ * 
+ * @deprecated This class is not supported anymore. It is recommended you
+ * base your own implementation on the source code for the Android Open
+ * Source Project if you must use it in your application.
+ */
+@Deprecated
+public class SlidingDrawer extends ViewGroup {
+    public static final int ORIENTATION_HORIZONTAL = 0;
+    public static final int ORIENTATION_VERTICAL = 1;
+
+    private static final int TAP_THRESHOLD = 6;
+    private static final float MAXIMUM_TAP_VELOCITY = 100.0f;
+    private static final float MAXIMUM_MINOR_VELOCITY = 150.0f;
+    private static final float MAXIMUM_MAJOR_VELOCITY = 200.0f;
+    private static final float MAXIMUM_ACCELERATION = 2000.0f;
+    private static final int VELOCITY_UNITS = 1000;
+    private static final int ANIMATION_FRAME_DURATION = 1000 / 60;
+
+    private static final int EXPANDED_FULL_OPEN = -10001;
+    private static final int COLLAPSED_FULL_CLOSED = -10002;
+
+    private final int mHandleId;
+    private final int mContentId;
+
+    private View mHandle;
+    private View mContent;
+
+    private final Rect mFrame = new Rect();
+    private final Rect mInvalidate = new Rect();
+    private boolean mTracking;
+    private boolean mLocked;
+
+    private VelocityTracker mVelocityTracker;
+
+    private boolean mVertical;
+    private boolean mExpanded;
+    private int mBottomOffset;
+    private int mTopOffset;
+    private int mHandleHeight;
+    private int mHandleWidth;
+
+    private OnDrawerOpenListener mOnDrawerOpenListener;
+    private OnDrawerCloseListener mOnDrawerCloseListener;
+    private OnDrawerScrollListener mOnDrawerScrollListener;
+
+    private float mAnimatedAcceleration;
+    private float mAnimatedVelocity;
+    private float mAnimationPosition;
+    private long mAnimationLastTime;
+    private long mCurrentAnimationTime;
+    private int mTouchDelta;
+    private boolean mAnimating;
+    private boolean mAllowSingleTap;
+    private boolean mAnimateOnClick;
+
+    private final int mTapThreshold;
+    private final int mMaximumTapVelocity;
+    private final int mMaximumMinorVelocity;
+    private final int mMaximumMajorVelocity;
+    private final int mMaximumAcceleration;
+    private final int mVelocityUnits;
+
+    /**
+     * Callback invoked when the drawer is opened.
+     */
+    public static interface OnDrawerOpenListener {
+        /**
+         * Invoked when the drawer becomes fully open.
+         */
+        public void onDrawerOpened();
+    }
+
+    /**
+     * Callback invoked when the drawer is closed.
+     */
+    public static interface OnDrawerCloseListener {
+        /**
+         * Invoked when the drawer becomes fully closed.
+         */
+        public void onDrawerClosed();
+    }
+
+    /**
+     * Callback invoked when the drawer is scrolled.
+     */
+    public static interface OnDrawerScrollListener {
+        /**
+         * Invoked when the user starts dragging/flinging the drawer's handle.
+         */
+        public void onScrollStarted();
+
+        /**
+         * Invoked when the user stops dragging/flinging the drawer's handle.
+         */
+        public void onScrollEnded();
+    }
+
+    /**
+     * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+     *
+     * @param context The application's environment.
+     * @param attrs The attributes defined in XML.
+     */
+    public SlidingDrawer(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+     *
+     * @param context The application's environment.
+     * @param attrs The attributes defined in XML.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     */
+    public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * Creates a new SlidingDrawer from a specified set of attributes defined in XML.
+     *
+     * @param context The application's environment.
+     * @param attrs The attributes defined in XML.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *        supplies default values for the view, used only if
+     *        defStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public SlidingDrawer(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.SlidingDrawer, defStyleAttr, defStyleRes);
+
+        int orientation = a.getInt(R.styleable.SlidingDrawer_orientation, ORIENTATION_VERTICAL);
+        mVertical = orientation == ORIENTATION_VERTICAL;
+        mBottomOffset = (int) a.getDimension(R.styleable.SlidingDrawer_bottomOffset, 0.0f);
+        mTopOffset = (int) a.getDimension(R.styleable.SlidingDrawer_topOffset, 0.0f);
+        mAllowSingleTap = a.getBoolean(R.styleable.SlidingDrawer_allowSingleTap, true);
+        mAnimateOnClick = a.getBoolean(R.styleable.SlidingDrawer_animateOnClick, true);
+
+        int handleId = a.getResourceId(R.styleable.SlidingDrawer_handle, 0);
+        if (handleId == 0) {
+            throw new IllegalArgumentException("The handle attribute is required and must refer "
+                    + "to a valid child.");
+        }
+
+        int contentId = a.getResourceId(R.styleable.SlidingDrawer_content, 0);
+        if (contentId == 0) {
+            throw new IllegalArgumentException("The content attribute is required and must refer "
+                    + "to a valid child.");
+        }
+
+        if (handleId == contentId) {
+            throw new IllegalArgumentException("The content and handle attributes must refer "
+                    + "to different children.");
+        }
+
+        mHandleId = handleId;
+        mContentId = contentId;
+
+        final float density = getResources().getDisplayMetrics().density;
+        mTapThreshold = (int) (TAP_THRESHOLD * density + 0.5f);
+        mMaximumTapVelocity = (int) (MAXIMUM_TAP_VELOCITY * density + 0.5f);
+        mMaximumMinorVelocity = (int) (MAXIMUM_MINOR_VELOCITY * density + 0.5f);
+        mMaximumMajorVelocity = (int) (MAXIMUM_MAJOR_VELOCITY * density + 0.5f);
+        mMaximumAcceleration = (int) (MAXIMUM_ACCELERATION * density + 0.5f);
+        mVelocityUnits = (int) (VELOCITY_UNITS * density + 0.5f);
+
+        a.recycle();
+
+        setAlwaysDrawnWithCacheEnabled(false);
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        mHandle = findViewById(mHandleId);
+        if (mHandle == null) {
+            throw new IllegalArgumentException("The handle attribute is must refer to an"
+                    + " existing child.");
+        }
+        mHandle.setOnClickListener(new DrawerToggler());
+
+        mContent = findViewById(mContentId);
+        if (mContent == null) {
+            throw new IllegalArgumentException("The content attribute is must refer to an" 
+                    + " existing child.");
+        }
+        mContent.setVisibility(View.GONE);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        int widthSpecSize =  MeasureSpec.getSize(widthMeasureSpec);
+
+        int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+        int heightSpecSize =  MeasureSpec.getSize(heightMeasureSpec);
+
+        if (widthSpecMode == MeasureSpec.UNSPECIFIED || heightSpecMode == MeasureSpec.UNSPECIFIED) {
+            throw new RuntimeException("SlidingDrawer cannot have UNSPECIFIED dimensions");
+        }
+
+        final View handle = mHandle;
+        measureChild(handle, widthMeasureSpec, heightMeasureSpec);
+
+        if (mVertical) {
+            int height = heightSpecSize - handle.getMeasuredHeight() - mTopOffset;
+            mContent.measure(MeasureSpec.makeMeasureSpec(widthSpecSize, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+        } else {
+            int width = widthSpecSize - handle.getMeasuredWidth() - mTopOffset;
+            mContent.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                    MeasureSpec.makeMeasureSpec(heightSpecSize, MeasureSpec.EXACTLY));
+        }
+
+        setMeasuredDimension(widthSpecSize, heightSpecSize);
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        final long drawingTime = getDrawingTime();
+        final View handle = mHandle;
+        final boolean isVertical = mVertical;
+
+        drawChild(canvas, handle, drawingTime);
+
+        if (mTracking || mAnimating) {
+            final Bitmap cache = mContent.getDrawingCache();
+            if (cache != null) {
+                if (isVertical) {
+                    canvas.drawBitmap(cache, 0, handle.getBottom(), null);
+                } else {
+                    canvas.drawBitmap(cache, handle.getRight(), 0, null);                    
+                }
+            } else {
+                canvas.save();
+                canvas.translate(isVertical ? 0 : handle.getLeft() - mTopOffset,
+                        isVertical ? handle.getTop() - mTopOffset : 0);
+                drawChild(canvas, mContent, drawingTime);
+                canvas.restore();
+            }
+        } else if (mExpanded) {
+            drawChild(canvas, mContent, drawingTime);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        if (mTracking) {
+            return;
+        }
+
+        final int width = r - l;
+        final int height = b - t;
+
+        final View handle = mHandle;
+
+        int childWidth = handle.getMeasuredWidth();
+        int childHeight = handle.getMeasuredHeight();
+
+        int childLeft;
+        int childTop;
+
+        final View content = mContent;
+
+        if (mVertical) {
+            childLeft = (width - childWidth) / 2;
+            childTop = mExpanded ? mTopOffset : height - childHeight + mBottomOffset;
+
+            content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
+                    mTopOffset + childHeight + content.getMeasuredHeight());
+        } else {
+            childLeft = mExpanded ? mTopOffset : width - childWidth + mBottomOffset;
+            childTop = (height - childHeight) / 2;
+
+            content.layout(mTopOffset + childWidth, 0,
+                    mTopOffset + childWidth + content.getMeasuredWidth(),
+                    content.getMeasuredHeight());            
+        }
+
+        handle.layout(childLeft, childTop, childLeft + childWidth, childTop + childHeight);
+        mHandleHeight = handle.getHeight();
+        mHandleWidth = handle.getWidth();
+    }
+
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent event) {
+        if (mLocked) {
+            return false;
+        }
+
+        final int action = event.getAction();
+
+        float x = event.getX();
+        float y = event.getY();
+
+        final Rect frame = mFrame;
+        final View handle = mHandle;
+
+        handle.getHitRect(frame);
+        if (!mTracking && !frame.contains((int) x, (int) y)) {
+            return false;
+        }
+
+        if (action == MotionEvent.ACTION_DOWN) {
+            mTracking = true;
+
+            handle.setPressed(true);
+            // Must be called before prepareTracking()
+            prepareContent();
+
+            // Must be called after prepareContent()
+            if (mOnDrawerScrollListener != null) {
+                mOnDrawerScrollListener.onScrollStarted();
+            }
+
+            if (mVertical) {
+                final int top = mHandle.getTop();
+                mTouchDelta = (int) y - top;
+                prepareTracking(top);
+            } else {
+                final int left = mHandle.getLeft();
+                mTouchDelta = (int) x - left;
+                prepareTracking(left);
+            }
+            mVelocityTracker.addMovement(event);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mLocked) {
+            return true;
+        }
+
+        if (mTracking) {
+            mVelocityTracker.addMovement(event);
+            final int action = event.getAction();
+            switch (action) {
+                case MotionEvent.ACTION_MOVE:
+                    moveHandle((int) (mVertical ? event.getY() : event.getX()) - mTouchDelta);
+                    break;
+                case MotionEvent.ACTION_UP:
+                case MotionEvent.ACTION_CANCEL: {
+                    final VelocityTracker velocityTracker = mVelocityTracker;
+                    velocityTracker.computeCurrentVelocity(mVelocityUnits);
+
+                    float yVelocity = velocityTracker.getYVelocity();
+                    float xVelocity = velocityTracker.getXVelocity();
+                    boolean negative;
+
+                    final boolean vertical = mVertical;
+                    if (vertical) {
+                        negative = yVelocity < 0;
+                        if (xVelocity < 0) {
+                            xVelocity = -xVelocity;
+                        }
+                        if (xVelocity > mMaximumMinorVelocity) {
+                            xVelocity = mMaximumMinorVelocity;
+                        }
+                    } else {
+                        negative = xVelocity < 0;
+                        if (yVelocity < 0) {
+                            yVelocity = -yVelocity;
+                        }
+                        if (yVelocity > mMaximumMinorVelocity) {
+                            yVelocity = mMaximumMinorVelocity;
+                        }
+                    }
+
+                    float velocity = (float) Math.hypot(xVelocity, yVelocity);
+                    if (negative) {
+                        velocity = -velocity;
+                    }
+
+                    final int top = mHandle.getTop();
+                    final int left = mHandle.getLeft();
+
+                    if (Math.abs(velocity) < mMaximumTapVelocity) {
+                        if (vertical ? (mExpanded && top < mTapThreshold + mTopOffset) ||
+                                (!mExpanded && top > mBottomOffset + mBottom - mTop -
+                                        mHandleHeight - mTapThreshold) :
+                                (mExpanded && left < mTapThreshold + mTopOffset) ||
+                                (!mExpanded && left > mBottomOffset + mRight - mLeft -
+                                        mHandleWidth - mTapThreshold)) {
+
+                            if (mAllowSingleTap) {
+                                playSoundEffect(SoundEffectConstants.CLICK);
+
+                                if (mExpanded) {
+                                    animateClose(vertical ? top : left, true);
+                                } else {
+                                    animateOpen(vertical ? top : left, true);
+                                }
+                            } else {
+                                performFling(vertical ? top : left, velocity, false, true);
+                            }
+
+                        } else {
+                            performFling(vertical ? top : left, velocity, false, true);
+                        }
+                    } else {
+                        performFling(vertical ? top : left, velocity, false, true);
+                    }
+                }
+                break;
+            }
+        }
+
+        return mTracking || mAnimating || super.onTouchEvent(event);
+    }
+
+    private void animateClose(int position, boolean notifyScrollListener) {
+        prepareTracking(position);
+        performFling(position, mMaximumAcceleration, true, notifyScrollListener);
+    }
+
+    private void animateOpen(int position, boolean notifyScrollListener) {
+        prepareTracking(position);
+        performFling(position, -mMaximumAcceleration, true, notifyScrollListener);
+    }
+
+    private void performFling(int position, float velocity, boolean always,
+            boolean notifyScrollListener) {
+        mAnimationPosition = position;
+        mAnimatedVelocity = velocity;
+
+        if (mExpanded) {
+            if (always || (velocity > mMaximumMajorVelocity ||
+                    (position > mTopOffset + (mVertical ? mHandleHeight : mHandleWidth) &&
+                            velocity > -mMaximumMajorVelocity))) {
+                // We are expanded, but they didn't move sufficiently to cause
+                // us to retract.  Animate back to the expanded position.
+                mAnimatedAcceleration = mMaximumAcceleration;
+                if (velocity < 0) {
+                    mAnimatedVelocity = 0;
+                }
+            } else {
+                // We are expanded and are now going to animate away.
+                mAnimatedAcceleration = -mMaximumAcceleration;
+                if (velocity > 0) {
+                    mAnimatedVelocity = 0;
+                }
+            }
+        } else {
+            if (!always && (velocity > mMaximumMajorVelocity ||
+                    (position > (mVertical ? getHeight() : getWidth()) / 2 &&
+                            velocity > -mMaximumMajorVelocity))) {
+                // We are collapsed, and they moved enough to allow us to expand.
+                mAnimatedAcceleration = mMaximumAcceleration;
+                if (velocity < 0) {
+                    mAnimatedVelocity = 0;
+                }
+            } else {
+                // We are collapsed, but they didn't move sufficiently to cause
+                // us to retract.  Animate back to the collapsed position.
+                mAnimatedAcceleration = -mMaximumAcceleration;
+                if (velocity > 0) {
+                    mAnimatedVelocity = 0;
+                }
+            }
+        }
+
+        long now = SystemClock.uptimeMillis();
+        mAnimationLastTime = now;
+        mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
+        mAnimating = true;
+        removeCallbacks(mSlidingRunnable);
+        postDelayed(mSlidingRunnable, ANIMATION_FRAME_DURATION);
+        stopTracking(notifyScrollListener);
+    }
+
+    private void prepareTracking(int position) {
+        mTracking = true;
+        mVelocityTracker = VelocityTracker.obtain();
+        boolean opening = !mExpanded;
+        if (opening) {
+            mAnimatedAcceleration = mMaximumAcceleration;
+            mAnimatedVelocity = mMaximumMajorVelocity;
+            mAnimationPosition = mBottomOffset +
+                    (mVertical ? getHeight() - mHandleHeight : getWidth() - mHandleWidth);
+            moveHandle((int) mAnimationPosition);
+            mAnimating = true;
+            removeCallbacks(mSlidingRunnable);
+            long now = SystemClock.uptimeMillis();
+            mAnimationLastTime = now;
+            mCurrentAnimationTime = now + ANIMATION_FRAME_DURATION;
+            mAnimating = true;
+        } else {
+            if (mAnimating) {
+                mAnimating = false;
+                removeCallbacks(mSlidingRunnable);
+            }
+            moveHandle(position);
+        }
+    }
+
+    private void moveHandle(int position) {
+        final View handle = mHandle;
+
+        if (mVertical) {
+            if (position == EXPANDED_FULL_OPEN) {
+                handle.offsetTopAndBottom(mTopOffset - handle.getTop());
+                invalidate();
+            } else if (position == COLLAPSED_FULL_CLOSED) {
+                handle.offsetTopAndBottom(mBottomOffset + mBottom - mTop -
+                        mHandleHeight - handle.getTop());
+                invalidate();
+            } else {
+                final int top = handle.getTop();
+                int deltaY = position - top;
+                if (position < mTopOffset) {
+                    deltaY = mTopOffset - top;
+                } else if (deltaY > mBottomOffset + mBottom - mTop - mHandleHeight - top) {
+                    deltaY = mBottomOffset + mBottom - mTop - mHandleHeight - top;
+                }
+                handle.offsetTopAndBottom(deltaY);
+
+                final Rect frame = mFrame;
+                final Rect region = mInvalidate;
+
+                handle.getHitRect(frame);
+                region.set(frame);
+
+                region.union(frame.left, frame.top - deltaY, frame.right, frame.bottom - deltaY);
+                region.union(0, frame.bottom - deltaY, getWidth(),
+                        frame.bottom - deltaY + mContent.getHeight());
+
+                invalidate(region);
+            }
+        } else {
+            if (position == EXPANDED_FULL_OPEN) {
+                handle.offsetLeftAndRight(mTopOffset - handle.getLeft());
+                invalidate();
+            } else if (position == COLLAPSED_FULL_CLOSED) {
+                handle.offsetLeftAndRight(mBottomOffset + mRight - mLeft -
+                        mHandleWidth - handle.getLeft());
+                invalidate();
+            } else {
+                final int left = handle.getLeft();
+                int deltaX = position - left;
+                if (position < mTopOffset) {
+                    deltaX = mTopOffset - left;
+                } else if (deltaX > mBottomOffset + mRight - mLeft - mHandleWidth - left) {
+                    deltaX = mBottomOffset + mRight - mLeft - mHandleWidth - left;
+                }
+                handle.offsetLeftAndRight(deltaX);
+
+                final Rect frame = mFrame;
+                final Rect region = mInvalidate;
+
+                handle.getHitRect(frame);
+                region.set(frame);
+
+                region.union(frame.left - deltaX, frame.top, frame.right - deltaX, frame.bottom);
+                region.union(frame.right - deltaX, 0,
+                        frame.right - deltaX + mContent.getWidth(), getHeight());
+
+                invalidate(region);
+            }
+        }
+    }
+
+    private void prepareContent() {
+        if (mAnimating) {
+            return;
+        }
+
+        // Something changed in the content, we need to honor the layout request
+        // before creating the cached bitmap
+        final View content = mContent;
+        if (content.isLayoutRequested()) {
+            if (mVertical) {
+                final int childHeight = mHandleHeight;
+                int height = mBottom - mTop - childHeight - mTopOffset;
+                content.measure(MeasureSpec.makeMeasureSpec(mRight - mLeft, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY));
+                content.layout(0, mTopOffset + childHeight, content.getMeasuredWidth(),
+                        mTopOffset + childHeight + content.getMeasuredHeight());
+            } else {
+                final int childWidth = mHandle.getWidth();
+                int width = mRight - mLeft - childWidth - mTopOffset;
+                content.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
+                        MeasureSpec.makeMeasureSpec(mBottom - mTop, MeasureSpec.EXACTLY));
+                content.layout(childWidth + mTopOffset, 0,
+                        mTopOffset + childWidth + content.getMeasuredWidth(),
+                        content.getMeasuredHeight());
+            }
+        }
+        // Try only once... we should really loop but it's not a big deal
+        // if the draw was cancelled, it will only be temporary anyway
+        content.getViewTreeObserver().dispatchOnPreDraw();
+        if (!content.isHardwareAccelerated()) content.buildDrawingCache();
+
+        content.setVisibility(View.GONE);        
+    }
+
+    private void stopTracking(boolean notifyScrollListener) {
+        mHandle.setPressed(false);
+        mTracking = false;
+
+        if (notifyScrollListener && mOnDrawerScrollListener != null) {
+            mOnDrawerScrollListener.onScrollEnded();
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+    }
+
+    private void doAnimation() {
+        if (mAnimating) {
+            incrementAnimation();
+            if (mAnimationPosition >= mBottomOffset + (mVertical ? getHeight() : getWidth()) - 1) {
+                mAnimating = false;
+                closeDrawer();
+            } else if (mAnimationPosition < mTopOffset) {
+                mAnimating = false;
+                openDrawer();
+            } else {
+                moveHandle((int) mAnimationPosition);
+                mCurrentAnimationTime += ANIMATION_FRAME_DURATION;
+                postDelayed(mSlidingRunnable, ANIMATION_FRAME_DURATION);
+            }
+        }
+    }
+
+    private void incrementAnimation() {
+        long now = SystemClock.uptimeMillis();
+        float t = (now - mAnimationLastTime) / 1000.0f;                   // ms -> s
+        final float position = mAnimationPosition;
+        final float v = mAnimatedVelocity;                                // px/s
+        final float a = mAnimatedAcceleration;                            // px/s/s
+        mAnimationPosition = position + (v * t) + (0.5f * a * t * t);     // px
+        mAnimatedVelocity = v + (a * t);                                  // px/s
+        mAnimationLastTime = now;                                         // ms
+    }
+
+    /**
+     * Toggles the drawer open and close. Takes effect immediately.
+     *
+     * @see #open()
+     * @see #close()
+     * @see #animateClose()
+     * @see #animateOpen()
+     * @see #animateToggle()
+     */
+    public void toggle() {
+        if (!mExpanded) {
+            openDrawer();
+        } else {
+            closeDrawer();
+        }
+        invalidate();
+        requestLayout();
+    }
+
+    /**
+     * Toggles the drawer open and close with an animation.
+     *
+     * @see #open()
+     * @see #close()
+     * @see #animateClose()
+     * @see #animateOpen()
+     * @see #toggle()
+     */
+    public void animateToggle() {
+        if (!mExpanded) {
+            animateOpen();
+        } else {
+            animateClose();
+        }
+    }
+
+    /**
+     * Opens the drawer immediately.
+     *
+     * @see #toggle()
+     * @see #close()
+     * @see #animateOpen()
+     */
+    public void open() {
+        openDrawer();
+        invalidate();
+        requestLayout();
+
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+    }
+
+    /**
+     * Closes the drawer immediately.
+     *
+     * @see #toggle()
+     * @see #open()
+     * @see #animateClose()
+     */
+    public void close() {
+        closeDrawer();
+        invalidate();
+        requestLayout();
+    }
+
+    /**
+     * Closes the drawer with an animation.
+     *
+     * @see #close()
+     * @see #open()
+     * @see #animateOpen()
+     * @see #animateToggle()
+     * @see #toggle()
+     */
+    public void animateClose() {
+        prepareContent();
+        final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
+        if (scrollListener != null) {
+            scrollListener.onScrollStarted();
+        }
+        animateClose(mVertical ? mHandle.getTop() : mHandle.getLeft(), false);
+
+        if (scrollListener != null) {
+            scrollListener.onScrollEnded();
+        }
+    }
+
+    /**
+     * Opens the drawer with an animation.
+     *
+     * @see #close()
+     * @see #open()
+     * @see #animateClose()
+     * @see #animateToggle()
+     * @see #toggle()
+     */
+    public void animateOpen() {
+        prepareContent();
+        final OnDrawerScrollListener scrollListener = mOnDrawerScrollListener;
+        if (scrollListener != null) {
+            scrollListener.onScrollStarted();
+        }
+        animateOpen(mVertical ? mHandle.getTop() : mHandle.getLeft(), false);
+
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED);
+
+        if (scrollListener != null) {
+            scrollListener.onScrollEnded();
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return SlidingDrawer.class.getName();
+    }
+
+    private void closeDrawer() {
+        moveHandle(COLLAPSED_FULL_CLOSED);
+        mContent.setVisibility(View.GONE);
+        mContent.destroyDrawingCache();
+
+        if (!mExpanded) {
+            return;
+        }
+
+        mExpanded = false;
+        if (mOnDrawerCloseListener != null) {
+            mOnDrawerCloseListener.onDrawerClosed();
+        }
+    }
+
+    private void openDrawer() {
+        moveHandle(EXPANDED_FULL_OPEN);
+        mContent.setVisibility(View.VISIBLE);
+
+        if (mExpanded) {
+            return;
+        }
+
+        mExpanded = true;
+
+        if (mOnDrawerOpenListener != null) {
+            mOnDrawerOpenListener.onDrawerOpened();
+        }
+    }
+
+    /**
+     * Sets the listener that receives a notification when the drawer becomes open.
+     *
+     * @param onDrawerOpenListener The listener to be notified when the drawer is opened.
+     */
+    public void setOnDrawerOpenListener(OnDrawerOpenListener onDrawerOpenListener) {
+        mOnDrawerOpenListener = onDrawerOpenListener;
+    }
+
+    /**
+     * Sets the listener that receives a notification when the drawer becomes close.
+     *
+     * @param onDrawerCloseListener The listener to be notified when the drawer is closed.
+     */
+    public void setOnDrawerCloseListener(OnDrawerCloseListener onDrawerCloseListener) {
+        mOnDrawerCloseListener = onDrawerCloseListener;
+    }
+
+    /**
+     * Sets the listener that receives a notification when the drawer starts or ends
+     * a scroll. A fling is considered as a scroll. A fling will also trigger a
+     * drawer opened or drawer closed event.
+     *
+     * @param onDrawerScrollListener The listener to be notified when scrolling
+     *        starts or stops.
+     */
+    public void setOnDrawerScrollListener(OnDrawerScrollListener onDrawerScrollListener) {
+        mOnDrawerScrollListener = onDrawerScrollListener;
+    }
+
+    /**
+     * Returns the handle of the drawer.
+     *
+     * @return The View reprenseting the handle of the drawer, identified by
+     *         the "handle" id in XML.
+     */
+    public View getHandle() {
+        return mHandle;
+    }
+
+    /**
+     * Returns the content of the drawer.
+     *
+     * @return The View reprenseting the content of the drawer, identified by
+     *         the "content" id in XML.
+     */
+    public View getContent() {
+        return mContent;
+    }
+
+    /**
+     * Unlocks the SlidingDrawer so that touch events are processed.
+     *
+     * @see #lock() 
+     */
+    public void unlock() {
+        mLocked = false;
+    }
+
+    /**
+     * Locks the SlidingDrawer so that touch events are ignores.
+     *
+     * @see #unlock()
+     */
+    public void lock() {
+        mLocked = true;
+    }
+
+    /**
+     * Indicates whether the drawer is currently fully opened.
+     *
+     * @return True if the drawer is opened, false otherwise.
+     */
+    public boolean isOpened() {
+        return mExpanded;
+    }
+
+    /**
+     * Indicates whether the drawer is scrolling or flinging.
+     *
+     * @return True if the drawer is scroller or flinging, false otherwise.
+     */
+    public boolean isMoving() {
+        return mTracking || mAnimating;
+    }
+
+    private class DrawerToggler implements OnClickListener {
+        public void onClick(View v) {
+            if (mLocked) {
+                return;
+            }
+            // mAllowSingleTap isn't relevant here; you're *always*
+            // allowed to open/close the drawer by clicking with the
+            // trackball.
+
+            if (mAnimateOnClick) {
+                animateToggle();
+            } else {
+                toggle();
+            }
+        }
+    }
+
+    private final Runnable mSlidingRunnable = new Runnable() {
+        @Override
+        public void run() {
+            doAnimation();
+        }
+    };
+}
diff --git a/android/widget/SmartSelectSprite.java b/android/widget/SmartSelectSprite.java
new file mode 100644
index 0000000..27b93bc
--- /dev/null
+++ b/android/widget/SmartSelectSprite.java
@@ -0,0 +1,608 @@
+/*
+ * Copyright (C) 2017 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 android.widget;
+
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import android.animation.Animator;
+import android.animation.AnimatorSet;
+import android.animation.ObjectAnimator;
+import android.animation.ValueAnimator;
+import android.annotation.ColorInt;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PointF;
+import android.graphics.RectF;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.ShapeDrawable;
+import android.graphics.drawable.shapes.Shape;
+import android.util.TypedValue;
+import android.view.animation.AnimationUtils;
+import android.view.animation.Interpolator;
+
+import com.android.internal.util.Preconditions;
+
+import java.lang.annotation.Retention;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+
+/**
+ * A utility class for creating and animating the Smart Select animation.
+ */
+final class SmartSelectSprite {
+
+    private static final int EXPAND_DURATION = 300;
+    private static final int CORNER_DURATION = 150;
+    private static final float STROKE_WIDTH_DP = 1.5F;
+
+    // GBLUE700
+    @ColorInt
+    private static final int DEFAULT_STROKE_COLOR = 0xFF3367D6;
+
+    private final Interpolator mExpandInterpolator;
+    private final Interpolator mCornerInterpolator;
+    private final float mStrokeWidth;
+
+    private Animator mActiveAnimator = null;
+    private final Runnable mInvalidator;
+    @ColorInt
+    private final int mStrokeColor;
+
+    static final Comparator<RectF> RECTANGLE_COMPARATOR = Comparator
+            .<RectF>comparingDouble(e -> e.bottom)
+            .thenComparingDouble(e -> e.left);
+
+    private Drawable mExistingDrawable = null;
+    private RectangleList mExistingRectangleList = null;
+
+    /**
+     * A rounded rectangle with a configurable corner radius and the ability to expand outside of
+     * its bounding rectangle and clip against it.
+     */
+    private static final class RoundedRectangleShape extends Shape {
+
+        private static final String PROPERTY_ROUND_RATIO = "roundRatio";
+
+        @Retention(SOURCE)
+        @IntDef({ExpansionDirection.LEFT, ExpansionDirection.CENTER, ExpansionDirection.RIGHT})
+        private @interface ExpansionDirection {
+        int LEFT = 0;
+        int CENTER = 1;
+        int RIGHT = 2;
+        }
+
+        @Retention(SOURCE)
+        @IntDef({RectangleBorderType.FIT, RectangleBorderType.OVERSHOOT})
+        private @interface RectangleBorderType {
+        /** A rectangle which, fully expanded, fits inside of its bounding rectangle. */
+        int FIT = 0;
+        /**
+         * A rectangle which, when fully expanded, clips outside of its bounding rectangle so that
+         * its edges no longer appear rounded.
+         */
+        int OVERSHOOT = 1;
+        }
+
+        private final float mStrokeWidth;
+        private final RectF mBoundingRectangle;
+        private float mRoundRatio = 1.0f;
+        private final @ExpansionDirection int mExpansionDirection;
+        private final @RectangleBorderType int mRectangleBorderType;
+
+        private final RectF mDrawRect = new RectF();
+        private final RectF mClipRect = new RectF();
+        private final Path mClipPath = new Path();
+
+        /** How far offset the left edge of the rectangle is from the bounding box. */
+        private float mLeftBoundary = 0;
+        /** How far offset the right edge of the rectangle is from the bounding box. */
+        private float mRightBoundary = 0;
+
+        private RoundedRectangleShape(
+                final RectF boundingRectangle,
+                final @ExpansionDirection int expansionDirection,
+                final @RectangleBorderType int rectangleBorderType,
+                final float strokeWidth) {
+            mBoundingRectangle = new RectF(boundingRectangle);
+            mExpansionDirection = expansionDirection;
+            mRectangleBorderType = rectangleBorderType;
+            mStrokeWidth = strokeWidth;
+
+            if (boundingRectangle.height() > boundingRectangle.width()) {
+                setRoundRatio(0.0f);
+            } else {
+                setRoundRatio(1.0f);
+            }
+        }
+
+        /*
+         * In order to achieve the "rounded rectangle hits the wall" effect, the drawing needs to be
+         * done in two passes. In this context, the wall is the bounding rectangle and in the first
+         * pass we need to draw the rounded rectangle (expanded and with a corner radius as per
+         * object properties) clipped by the bounding box. If the rounded rectangle expands outside
+         * of the bounding box, one more pass needs to be done, as there will now be a hole in the
+         * rounded rectangle where it "flattened" against the bounding box. In order to fill just
+         * this hole, we need to draw the bounding box, but clip it with the rounded rectangle and
+         * this will connect the missing pieces.
+         */
+        @Override
+        public void draw(Canvas canvas, Paint paint) {
+            final float cornerRadius = getCornerRadius();
+            final float adjustedCornerRadius = getAdjustedCornerRadius();
+
+            mDrawRect.set(mBoundingRectangle);
+            mDrawRect.left = mBoundingRectangle.left + mLeftBoundary;
+            mDrawRect.right = mBoundingRectangle.left + mRightBoundary;
+
+            if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
+                mDrawRect.left -= cornerRadius / 2;
+                mDrawRect.right -= cornerRadius / 2;
+            } else {
+                switch (mExpansionDirection) {
+                    case ExpansionDirection.CENTER:
+                        break;
+                    case ExpansionDirection.LEFT:
+                        mDrawRect.right += cornerRadius;
+                        break;
+                    case ExpansionDirection.RIGHT:
+                        mDrawRect.left -= cornerRadius;
+                        break;
+                }
+            }
+
+            canvas.save();
+            mClipRect.set(mBoundingRectangle);
+            mClipRect.inset(-mStrokeWidth, -mStrokeWidth);
+            canvas.clipRect(mClipRect);
+            canvas.drawRoundRect(mDrawRect, adjustedCornerRadius, adjustedCornerRadius, paint);
+            canvas.restore();
+
+            canvas.save();
+            mClipPath.reset();
+            mClipPath.addRoundRect(
+                    mDrawRect,
+                    adjustedCornerRadius,
+                    adjustedCornerRadius,
+                    Path.Direction.CW);
+            canvas.clipPath(mClipPath);
+            canvas.drawRect(mBoundingRectangle, paint);
+            canvas.restore();
+        }
+
+        public void setRoundRatio(@FloatRange(from = 0.0, to = 1.0) final float roundRatio) {
+            mRoundRatio = roundRatio;
+        }
+
+        public float getRoundRatio() {
+            return mRoundRatio;
+        }
+
+        private void setLeftBoundary(final float leftBoundary) {
+            mLeftBoundary = leftBoundary;
+        }
+
+        private void setRightBoundary(final float rightBoundary) {
+            mRightBoundary = rightBoundary;
+        }
+
+        private float getCornerRadius() {
+            return Math.min(mBoundingRectangle.width(), mBoundingRectangle.height());
+        }
+
+        private float getAdjustedCornerRadius() {
+            return (getCornerRadius() * mRoundRatio);
+        }
+
+        private float getBoundingWidth() {
+            if (mRectangleBorderType == RectangleBorderType.OVERSHOOT) {
+                return (int) (mBoundingRectangle.width() + getCornerRadius());
+            } else {
+                return mBoundingRectangle.width();
+            }
+        }
+
+    }
+
+    /**
+     * A collection of {@link RoundedRectangleShape}s that abstracts them to a single shape whose
+     * collective left and right boundary can be manipulated.
+     */
+    private static final class RectangleList extends Shape {
+
+        @Retention(SOURCE)
+        @IntDef({DisplayType.RECTANGLES, DisplayType.POLYGON})
+        private @interface DisplayType {
+            int RECTANGLES = 0;
+            int POLYGON = 1;
+        }
+
+        private static final String PROPERTY_RIGHT_BOUNDARY = "rightBoundary";
+        private static final String PROPERTY_LEFT_BOUNDARY = "leftBoundary";
+
+        private final List<RoundedRectangleShape> mRectangles;
+        private final List<RoundedRectangleShape> mReversedRectangles;
+
+        private final Path mOutlinePolygonPath;
+        private @DisplayType int mDisplayType = DisplayType.RECTANGLES;
+
+        private RectangleList(final List<RoundedRectangleShape> rectangles) {
+            mRectangles = new LinkedList<>(rectangles);
+            mReversedRectangles = new LinkedList<>(rectangles);
+            Collections.reverse(mReversedRectangles);
+            mOutlinePolygonPath = generateOutlinePolygonPath(rectangles);
+        }
+
+        private void setLeftBoundary(final float leftBoundary) {
+            float boundarySoFar = getTotalWidth();
+            for (RoundedRectangleShape rectangle : mReversedRectangles) {
+                final float rectangleLeftBoundary = boundarySoFar - rectangle.getBoundingWidth();
+                if (leftBoundary < rectangleLeftBoundary) {
+                    rectangle.setLeftBoundary(0);
+                } else if (leftBoundary > boundarySoFar) {
+                    rectangle.setLeftBoundary(rectangle.getBoundingWidth());
+                } else {
+                    rectangle.setLeftBoundary(
+                            rectangle.getBoundingWidth() - boundarySoFar + leftBoundary);
+                }
+
+                boundarySoFar = rectangleLeftBoundary;
+            }
+        }
+
+        private void setRightBoundary(final float rightBoundary) {
+            float boundarySoFar = 0;
+            for (RoundedRectangleShape rectangle : mRectangles) {
+                final float rectangleRightBoundary = rectangle.getBoundingWidth() + boundarySoFar;
+                if (rectangleRightBoundary < rightBoundary) {
+                    rectangle.setRightBoundary(rectangle.getBoundingWidth());
+                } else if (boundarySoFar > rightBoundary) {
+                    rectangle.setRightBoundary(0);
+                } else {
+                    rectangle.setRightBoundary(rightBoundary - boundarySoFar);
+                }
+
+                boundarySoFar = rectangleRightBoundary;
+            }
+        }
+
+        void setDisplayType(@DisplayType int displayType) {
+            mDisplayType = displayType;
+        }
+
+        private int getTotalWidth() {
+            int sum = 0;
+            for (RoundedRectangleShape rectangle : mRectangles) {
+                sum += rectangle.getBoundingWidth();
+            }
+            return sum;
+        }
+
+        @Override
+        public void draw(Canvas canvas, Paint paint) {
+            if (mDisplayType == DisplayType.POLYGON) {
+                drawPolygon(canvas, paint);
+            } else {
+                drawRectangles(canvas, paint);
+            }
+        }
+
+        private void drawRectangles(final Canvas canvas, final Paint paint) {
+            for (RoundedRectangleShape rectangle : mRectangles) {
+                rectangle.draw(canvas, paint);
+            }
+        }
+
+        private void drawPolygon(final Canvas canvas, final Paint paint) {
+            canvas.drawPath(mOutlinePolygonPath, paint);
+        }
+
+        private static Path generateOutlinePolygonPath(
+                final List<RoundedRectangleShape> rectangles) {
+            final Path path = new Path();
+            for (final RoundedRectangleShape shape : rectangles) {
+                final Path rectanglePath = new Path();
+                rectanglePath.addRect(shape.mBoundingRectangle, Path.Direction.CW);
+                path.op(rectanglePath, Path.Op.UNION);
+            }
+            return path;
+        }
+
+    }
+
+    /**
+     * @param context     The {@link Context} in which the animation will run
+     * @param invalidator A {@link Runnable} which will be called every time the animation updates,
+     *                    indicating that the view drawing the animation should invalidate itself
+     */
+    SmartSelectSprite(final Context context, final Runnable invalidator) {
+        mExpandInterpolator = AnimationUtils.loadInterpolator(
+                context,
+                android.R.interpolator.fast_out_slow_in);
+        mCornerInterpolator = AnimationUtils.loadInterpolator(
+                context,
+                android.R.interpolator.fast_out_linear_in);
+        mStrokeWidth = dpToPixel(context, STROKE_WIDTH_DP);
+        mStrokeColor = getStrokeColor(context);
+        mInvalidator = Preconditions.checkNotNull(invalidator);
+    }
+
+    /**
+     * Performs the Smart Select animation on the view bound to this SmartSelectSprite.
+     *
+     * @param start                 The point from which the animation will start. Must be inside
+     *                              destinationRectangles.
+     * @param destinationRectangles The rectangles which the animation will fill out by its
+     *                              "selection" and finally join them into a single polygon. In
+     *                              order to get the correct visual behavior, these rectangles
+     *                              should be sorted according to {@link #RECTANGLE_COMPARATOR}.
+     * @param onAnimationEnd        The callback which will be invoked once the whole animation
+     *                              completes.
+     * @throws IllegalArgumentException if the given start point is not in any of the
+     *                                  destinationRectangles.
+     * @see #cancelAnimation()
+     */
+    public void startAnimation(
+            final PointF start,
+            final List<RectF> destinationRectangles,
+            final Runnable onAnimationEnd) throws IllegalArgumentException {
+        cancelAnimation();
+
+        final ValueAnimator.AnimatorUpdateListener updateListener =
+                valueAnimator -> mInvalidator.run();
+
+        final List<RoundedRectangleShape> shapes = new LinkedList<>();
+        final List<Animator> cornerAnimators = new LinkedList<>();
+
+        final RectF centerRectangle = destinationRectangles
+                .stream()
+                .filter((r) -> contains(r, start))
+                .findFirst()
+                .orElseThrow(() -> new IllegalArgumentException(
+                        "Center point is not inside any of the rectangles!"));
+
+        int startingOffset = 0;
+        for (RectF rectangle : destinationRectangles) {
+            if (rectangle.equals(centerRectangle)) {
+                break;
+            }
+            startingOffset += rectangle.width();
+        }
+
+        startingOffset += start.x - centerRectangle.left;
+
+        final float centerRectangleHalfHeight = centerRectangle.height() / 2;
+        final float startingOffsetLeft = startingOffset - centerRectangleHalfHeight;
+        final float startingOffsetRight = startingOffset + centerRectangleHalfHeight;
+
+        final @RoundedRectangleShape.ExpansionDirection int[] expansionDirections =
+                generateDirections(centerRectangle, destinationRectangles);
+
+        final @RoundedRectangleShape.RectangleBorderType int[] rectangleBorderTypes =
+                generateBorderTypes(destinationRectangles);
+
+        int index = 0;
+
+        for (RectF rectangle : destinationRectangles) {
+            final RoundedRectangleShape shape = new RoundedRectangleShape(
+                    rectangle,
+                    expansionDirections[index],
+                    rectangleBorderTypes[index],
+                    mStrokeWidth);
+            cornerAnimators.add(createCornerAnimator(shape, updateListener));
+            shapes.add(shape);
+            index++;
+        }
+
+        final RectangleList rectangleList = new RectangleList(shapes);
+        final ShapeDrawable shapeDrawable = new ShapeDrawable(rectangleList);
+
+        final Paint paint = shapeDrawable.getPaint();
+        paint.setColor(mStrokeColor);
+        paint.setStyle(Paint.Style.STROKE);
+        paint.setStrokeWidth(mStrokeWidth);
+
+        mExistingRectangleList = rectangleList;
+        mExistingDrawable = shapeDrawable;
+
+        mActiveAnimator = createAnimator(rectangleList, startingOffsetLeft, startingOffsetRight,
+                cornerAnimators, updateListener,
+                onAnimationEnd);
+        mActiveAnimator.start();
+    }
+
+    private Animator createAnimator(
+            final RectangleList rectangleList,
+            final float startingOffsetLeft,
+            final float startingOffsetRight,
+            final List<Animator> cornerAnimators,
+            final ValueAnimator.AnimatorUpdateListener updateListener,
+            final Runnable onAnimationEnd) {
+        final ObjectAnimator rightBoundaryAnimator = ObjectAnimator.ofFloat(
+                rectangleList,
+                RectangleList.PROPERTY_RIGHT_BOUNDARY,
+                startingOffsetRight,
+                rectangleList.getTotalWidth());
+
+        final ObjectAnimator leftBoundaryAnimator = ObjectAnimator.ofFloat(
+                rectangleList,
+                RectangleList.PROPERTY_LEFT_BOUNDARY,
+                startingOffsetLeft,
+                0);
+
+        rightBoundaryAnimator.setDuration(EXPAND_DURATION);
+        leftBoundaryAnimator.setDuration(EXPAND_DURATION);
+
+        rightBoundaryAnimator.addUpdateListener(updateListener);
+        leftBoundaryAnimator.addUpdateListener(updateListener);
+
+        rightBoundaryAnimator.setInterpolator(mExpandInterpolator);
+        leftBoundaryAnimator.setInterpolator(mExpandInterpolator);
+
+        final AnimatorSet cornerAnimator = new AnimatorSet();
+        cornerAnimator.playTogether(cornerAnimators);
+
+        final AnimatorSet boundaryAnimator = new AnimatorSet();
+        boundaryAnimator.playTogether(leftBoundaryAnimator, rightBoundaryAnimator);
+
+        final AnimatorSet animatorSet = new AnimatorSet();
+        animatorSet.playSequentially(boundaryAnimator, cornerAnimator);
+
+        setUpAnimatorListener(animatorSet, onAnimationEnd);
+
+        return animatorSet;
+    }
+
+    private void setUpAnimatorListener(final Animator animator, final Runnable onAnimationEnd) {
+        animator.addListener(new Animator.AnimatorListener() {
+            @Override
+            public void onAnimationStart(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationEnd(Animator animator) {
+                mExistingRectangleList.setDisplayType(RectangleList.DisplayType.POLYGON);
+                mInvalidator.run();
+
+                onAnimationEnd.run();
+            }
+
+            @Override
+            public void onAnimationCancel(Animator animator) {
+            }
+
+            @Override
+            public void onAnimationRepeat(Animator animator) {
+            }
+        });
+    }
+
+    private ObjectAnimator createCornerAnimator(
+            final RoundedRectangleShape shape,
+            final ValueAnimator.AnimatorUpdateListener listener) {
+        final ObjectAnimator animator = ObjectAnimator.ofFloat(
+                shape,
+                RoundedRectangleShape.PROPERTY_ROUND_RATIO,
+                shape.getRoundRatio(), 0.0F);
+        animator.setDuration(CORNER_DURATION);
+        animator.addUpdateListener(listener);
+        animator.setInterpolator(mCornerInterpolator);
+        return animator;
+    }
+
+    private static @RoundedRectangleShape.ExpansionDirection int[] generateDirections(
+            final RectF centerRectangle, final List<RectF> rectangles) {
+        final @RoundedRectangleShape.ExpansionDirection int[] result = new int[rectangles.size()];
+
+        final int centerRectangleIndex = rectangles.indexOf(centerRectangle);
+
+        for (int i = 0; i < centerRectangleIndex - 1; ++i) {
+            result[i] = RoundedRectangleShape.ExpansionDirection.LEFT;
+        }
+
+        if (rectangles.size() == 1) {
+            result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER;
+        } else if (centerRectangleIndex == 0) {
+            result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.LEFT;
+        } else if (centerRectangleIndex == rectangles.size() - 1) {
+            result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.RIGHT;
+        } else {
+            result[centerRectangleIndex] = RoundedRectangleShape.ExpansionDirection.CENTER;
+        }
+
+        for (int i = centerRectangleIndex + 1; i < result.length; ++i) {
+            result[i] = RoundedRectangleShape.ExpansionDirection.RIGHT;
+        }
+
+        return result;
+    }
+
+    private static @RoundedRectangleShape.RectangleBorderType int[] generateBorderTypes(
+            final List<RectF> rectangles) {
+        final @RoundedRectangleShape.RectangleBorderType int[] result = new int[rectangles.size()];
+
+        for (int i = 1; i < result.length - 1; ++i) {
+            result[i] = RoundedRectangleShape.RectangleBorderType.OVERSHOOT;
+        }
+
+        result[0] = RoundedRectangleShape.RectangleBorderType.FIT;
+        result[result.length - 1] = RoundedRectangleShape.RectangleBorderType.FIT;
+        return result;
+    }
+
+    private static float dpToPixel(final Context context, final float dp) {
+        return TypedValue.applyDimension(
+                TypedValue.COMPLEX_UNIT_DIP,
+                dp,
+                context.getResources().getDisplayMetrics());
+    }
+
+    @ColorInt
+    private static int getStrokeColor(final Context context) {
+        final TypedValue typedValue = new TypedValue();
+        final TypedArray array = context.obtainStyledAttributes(typedValue.data, new int[]{
+                android.R.attr.colorControlActivated});
+        final int result = array.getColor(0, DEFAULT_STROKE_COLOR);
+        array.recycle();
+        return result;
+    }
+
+    /**
+     * A variant of {@link RectF#contains(float, float)} that also allows the point to reside on
+     * the right boundary of the rectangle.
+     *
+     * @param rectangle the rectangle inside which the point should be to be considered "contained"
+     * @param point     the point which will be tested
+     * @return whether the point is inside the rectangle (or on it's right boundary)
+     */
+    private static boolean contains(final RectF rectangle, final PointF point) {
+        final float x = point.x;
+        final float y = point.y;
+        return x >= rectangle.left && x <= rectangle.right && y >= rectangle.top
+                && y <= rectangle.bottom;
+    }
+
+    private void removeExistingDrawables() {
+        mExistingDrawable = null;
+        mExistingRectangleList = null;
+        mInvalidator.run();
+    }
+
+    /**
+     * Cancels any active Smart Select animation that might be in progress.
+     */
+    public void cancelAnimation() {
+        if (mActiveAnimator != null) {
+            mActiveAnimator.cancel();
+            mActiveAnimator = null;
+            removeExistingDrawables();
+        }
+    }
+
+    public void draw(Canvas canvas) {
+        if (mExistingDrawable != null) {
+            mExistingDrawable.draw(canvas);
+        }
+    }
+
+}
diff --git a/android/widget/Space.java b/android/widget/Space.java
new file mode 100644
index 0000000..c4eaeb7
--- /dev/null
+++ b/android/widget/Space.java
@@ -0,0 +1,100 @@
+/*
+ * 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 android.widget;
+
+import android.content.Context;
+import android.graphics.Canvas;
+import android.util.AttributeSet;
+import android.view.View;
+
+/**
+ * Space is a lightweight View subclass that may be used to create gaps between components
+ * in general purpose layouts.
+ */
+public final class Space extends View {
+    /**
+     * {@inheritDoc}
+     */
+    public Space(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        if (getVisibility() == VISIBLE) {
+            setVisibility(INVISIBLE);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Space(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Space(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public Space(Context context) {
+        //noinspection NullableProblems
+        this(context, null);
+    }
+
+    /**
+     * Draw nothing.
+     *
+     * @param canvas an unused parameter.
+     */
+    @Override
+    public void draw(Canvas canvas) {
+    }
+
+    /**
+     * Compare to: {@link View#getDefaultSize(int, int)}
+     * If mode is AT_MOST, return the child size instead of the parent size
+     * (unless it is too big).
+     */
+    private static int getDefaultSize2(int size, int measureSpec) {
+        int result = size;
+        int specMode = MeasureSpec.getMode(measureSpec);
+        int specSize = MeasureSpec.getSize(measureSpec);
+
+        switch (specMode) {
+            case MeasureSpec.UNSPECIFIED:
+                result = size;
+                break;
+            case MeasureSpec.AT_MOST:
+                result = Math.min(size, specSize);
+                break;
+            case MeasureSpec.EXACTLY:
+                result = specSize;
+                break;
+        }
+        return result;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        setMeasuredDimension(
+                getDefaultSize2(getSuggestedMinimumWidth(), widthMeasureSpec),
+                getDefaultSize2(getSuggestedMinimumHeight(), heightMeasureSpec));
+    }
+}
diff --git a/android/widget/SpellChecker.java b/android/widget/SpellChecker.java
new file mode 100644
index 0000000..3d4f546
--- /dev/null
+++ b/android/widget/SpellChecker.java
@@ -0,0 +1,788 @@
+/*
+ * 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 android.widget;
+
+import android.content.Context;
+import android.text.Editable;
+import android.text.Selection;
+import android.text.Spanned;
+import android.text.TextUtils;
+import android.text.method.WordIterator;
+import android.text.style.SpellCheckSpan;
+import android.text.style.SuggestionSpan;
+import android.util.Log;
+import android.util.LruCache;
+import android.view.textservice.SentenceSuggestionsInfo;
+import android.view.textservice.SpellCheckerSession;
+import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener;
+import android.view.textservice.SuggestionsInfo;
+import android.view.textservice.TextInfo;
+import android.view.textservice.TextServicesManager;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.util.GrowingArrayUtils;
+
+import java.text.BreakIterator;
+import java.util.Locale;
+
+
+/**
+ * Helper class for TextView. Bridge between the TextView and the Dictionary service.
+ *
+ * @hide
+ */
+public class SpellChecker implements SpellCheckerSessionListener {
+    private static final String TAG = SpellChecker.class.getSimpleName();
+    private static final boolean DBG = false;
+
+    // No more than this number of words will be parsed on each iteration to ensure a minimum
+    // lock of the UI thread
+    public static final int MAX_NUMBER_OF_WORDS = 50;
+
+    // Rough estimate, such that the word iterator interval usually does not need to be shifted
+    public static final int AVERAGE_WORD_LENGTH = 7;
+
+    // When parsing, use a character window of that size. Will be shifted if needed
+    public static final int WORD_ITERATOR_INTERVAL = AVERAGE_WORD_LENGTH * MAX_NUMBER_OF_WORDS;
+
+    // Pause between each spell check to keep the UI smooth
+    private final static int SPELL_PAUSE_DURATION = 400; // milliseconds
+
+    private static final int MIN_SENTENCE_LENGTH = 50;
+
+    private static final int USE_SPAN_RANGE = -1;
+
+    private final TextView mTextView;
+
+    SpellCheckerSession mSpellCheckerSession;
+    // We assume that the sentence level spell check will always provide better results than words.
+    // Although word SC has a sequential option.
+    private boolean mIsSentenceSpellCheckSupported;
+    final int mCookie;
+
+    // Paired arrays for the (id, spellCheckSpan) pair. A negative id means the associated
+    // SpellCheckSpan has been recycled and can be-reused.
+    // Contains null SpellCheckSpans after index mLength.
+    private int[] mIds;
+    private SpellCheckSpan[] mSpellCheckSpans;
+    // The mLength first elements of the above arrays have been initialized
+    private int mLength;
+
+    // Parsers on chunk of text, cutting text into words that will be checked
+    private SpellParser[] mSpellParsers = new SpellParser[0];
+
+    private int mSpanSequenceCounter = 0;
+
+    private Locale mCurrentLocale;
+
+    // Shared by all SpellParsers. Cannot be shared with TextView since it may be used
+    // concurrently due to the asynchronous nature of onGetSuggestions.
+    private WordIterator mWordIterator;
+
+    private TextServicesManager mTextServicesManager;
+
+    private Runnable mSpellRunnable;
+
+    private static final int SUGGESTION_SPAN_CACHE_SIZE = 10;
+    private final LruCache<Long, SuggestionSpan> mSuggestionSpanCache =
+            new LruCache<Long, SuggestionSpan>(SUGGESTION_SPAN_CACHE_SIZE);
+
+    public SpellChecker(TextView textView) {
+        mTextView = textView;
+
+        // Arbitrary: these arrays will automatically double their sizes on demand
+        final int size = 1;
+        mIds = ArrayUtils.newUnpaddedIntArray(size);
+        mSpellCheckSpans = new SpellCheckSpan[mIds.length];
+
+        setLocale(mTextView.getSpellCheckerLocale());
+
+        mCookie = hashCode();
+    }
+
+    private void resetSession() {
+        closeSession();
+
+        mTextServicesManager = (TextServicesManager) mTextView.getContext().
+                getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
+        if (!mTextServicesManager.isSpellCheckerEnabled()
+                || mCurrentLocale == null
+                || mTextServicesManager.getCurrentSpellCheckerSubtype(true) == null) {
+            mSpellCheckerSession = null;
+        } else {
+            mSpellCheckerSession = mTextServicesManager.newSpellCheckerSession(
+                    null /* Bundle not currently used by the textServicesManager */,
+                    mCurrentLocale, this,
+                    false /* means any available languages from current spell checker */);
+            mIsSentenceSpellCheckSupported = true;
+        }
+
+        // Restore SpellCheckSpans in pool
+        for (int i = 0; i < mLength; i++) {
+            mIds[i] = -1;
+        }
+        mLength = 0;
+
+        // Remove existing misspelled SuggestionSpans
+        mTextView.removeMisspelledSpans((Editable) mTextView.getText());
+        mSuggestionSpanCache.evictAll();
+    }
+
+    private void setLocale(Locale locale) {
+        mCurrentLocale = locale;
+
+        resetSession();
+
+        if (locale != null) {
+            // Change SpellParsers' wordIterator locale
+            mWordIterator = new WordIterator(locale);
+        }
+
+        // This class is the listener for locale change: warn other locale-aware objects
+        mTextView.onLocaleChanged();
+    }
+
+    /**
+     * @return true if a spell checker session has successfully been created. Returns false if not,
+     * for instance when spell checking has been disabled in settings.
+     */
+    private boolean isSessionActive() {
+        return mSpellCheckerSession != null;
+    }
+
+    public void closeSession() {
+        if (mSpellCheckerSession != null) {
+            mSpellCheckerSession.close();
+        }
+
+        final int length = mSpellParsers.length;
+        for (int i = 0; i < length; i++) {
+            mSpellParsers[i].stop();
+        }
+
+        if (mSpellRunnable != null) {
+            mTextView.removeCallbacks(mSpellRunnable);
+        }
+    }
+
+    private int nextSpellCheckSpanIndex() {
+        for (int i = 0; i < mLength; i++) {
+            if (mIds[i] < 0) return i;
+        }
+
+        mIds = GrowingArrayUtils.append(mIds, mLength, 0);
+        mSpellCheckSpans = GrowingArrayUtils.append(
+                mSpellCheckSpans, mLength, new SpellCheckSpan());
+        mLength++;
+        return mLength - 1;
+    }
+
+    private void addSpellCheckSpan(Editable editable, int start, int end) {
+        final int index = nextSpellCheckSpanIndex();
+        SpellCheckSpan spellCheckSpan = mSpellCheckSpans[index];
+        editable.setSpan(spellCheckSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        spellCheckSpan.setSpellCheckInProgress(false);
+        mIds[index] = mSpanSequenceCounter++;
+    }
+
+    public void onSpellCheckSpanRemoved(SpellCheckSpan spellCheckSpan) {
+        // Recycle any removed SpellCheckSpan (from this code or during text edition)
+        for (int i = 0; i < mLength; i++) {
+            if (mSpellCheckSpans[i] == spellCheckSpan) {
+                mIds[i] = -1;
+                return;
+            }
+        }
+    }
+
+    public void onSelectionChanged() {
+        spellCheck();
+    }
+
+    public void spellCheck(int start, int end) {
+        if (DBG) {
+            Log.d(TAG, "Start spell-checking: " + start + ", " + end);
+        }
+        final Locale locale = mTextView.getSpellCheckerLocale();
+        final boolean isSessionActive = isSessionActive();
+        if (locale == null || mCurrentLocale == null || (!(mCurrentLocale.equals(locale)))) {
+            setLocale(locale);
+            // Re-check the entire text
+            start = 0;
+            end = mTextView.getText().length();
+        } else {
+            final boolean spellCheckerActivated = mTextServicesManager.isSpellCheckerEnabled();
+            if (isSessionActive != spellCheckerActivated) {
+                // Spell checker has been turned of or off since last spellCheck
+                resetSession();
+            }
+        }
+
+        if (!isSessionActive) return;
+
+        // Find first available SpellParser from pool
+        final int length = mSpellParsers.length;
+        for (int i = 0; i < length; i++) {
+            final SpellParser spellParser = mSpellParsers[i];
+            if (spellParser.isFinished()) {
+                spellParser.parse(start, end);
+                return;
+            }
+        }
+
+        if (DBG) {
+            Log.d(TAG, "new spell parser.");
+        }
+        // No available parser found in pool, create a new one
+        SpellParser[] newSpellParsers = new SpellParser[length + 1];
+        System.arraycopy(mSpellParsers, 0, newSpellParsers, 0, length);
+        mSpellParsers = newSpellParsers;
+
+        SpellParser spellParser = new SpellParser();
+        mSpellParsers[length] = spellParser;
+        spellParser.parse(start, end);
+    }
+
+    private void spellCheck() {
+        if (mSpellCheckerSession == null) return;
+
+        Editable editable = (Editable) mTextView.getText();
+        final int selectionStart = Selection.getSelectionStart(editable);
+        final int selectionEnd = Selection.getSelectionEnd(editable);
+
+        TextInfo[] textInfos = new TextInfo[mLength];
+        int textInfosCount = 0;
+
+        for (int i = 0; i < mLength; i++) {
+            final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
+            if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) continue;
+
+            final int start = editable.getSpanStart(spellCheckSpan);
+            final int end = editable.getSpanEnd(spellCheckSpan);
+
+            // Do not check this word if the user is currently editing it
+            final boolean isEditing;
+
+            // Defer spell check when typing a word ending with a punctuation like an apostrophe
+            // which could end up being a mid-word punctuation.
+            if (selectionStart == end + 1
+                    && WordIterator.isMidWordPunctuation(
+                            mCurrentLocale, Character.codePointBefore(editable, end + 1))) {
+                isEditing = false;
+            } else if (mIsSentenceSpellCheckSupported) {
+                // Allow the overlap of the cursor and the first boundary of the spell check span
+                // no to skip the spell check of the following word because the
+                // following word will never be spell-checked even if the user finishes composing
+                isEditing = selectionEnd <= start || selectionStart > end;
+            } else {
+                isEditing = selectionEnd < start || selectionStart > end;
+            }
+            if (start >= 0 && end > start && isEditing) {
+                spellCheckSpan.setSpellCheckInProgress(true);
+                final TextInfo textInfo = new TextInfo(editable, start, end, mCookie, mIds[i]);
+                textInfos[textInfosCount++] = textInfo;
+                if (DBG) {
+                    Log.d(TAG, "create TextInfo: (" + i + "/" + mLength + ") text = "
+                            + textInfo.getSequence() + ", cookie = " + mCookie + ", seq = "
+                            + mIds[i] + ", sel start = " + selectionStart + ", sel end = "
+                            + selectionEnd + ", start = " + start + ", end = " + end);
+                }
+            }
+        }
+
+        if (textInfosCount > 0) {
+            if (textInfosCount < textInfos.length) {
+                TextInfo[] textInfosCopy = new TextInfo[textInfosCount];
+                System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount);
+                textInfos = textInfosCopy;
+            }
+
+            if (mIsSentenceSpellCheckSupported) {
+                mSpellCheckerSession.getSentenceSuggestions(
+                        textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE);
+            } else {
+                mSpellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE,
+                        false /* TODO Set sequentialWords to true for initial spell check */);
+            }
+        }
+    }
+
+    private SpellCheckSpan onGetSuggestionsInternal(
+            SuggestionsInfo suggestionsInfo, int offset, int length) {
+        if (suggestionsInfo == null || suggestionsInfo.getCookie() != mCookie) {
+            return null;
+        }
+        final Editable editable = (Editable) mTextView.getText();
+        final int sequenceNumber = suggestionsInfo.getSequence();
+        for (int k = 0; k < mLength; ++k) {
+            if (sequenceNumber == mIds[k]) {
+                final int attributes = suggestionsInfo.getSuggestionsAttributes();
+                final boolean isInDictionary =
+                        ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0);
+                final boolean looksLikeTypo =
+                        ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0);
+
+                final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[k];
+                //TODO: we need to change that rule for results from a sentence-level spell
+                // checker that will probably be in dictionary.
+                if (!isInDictionary && looksLikeTypo) {
+                    createMisspelledSuggestionSpan(
+                            editable, suggestionsInfo, spellCheckSpan, offset, length);
+                } else {
+                    // Valid word -- isInDictionary || !looksLikeTypo
+                    if (mIsSentenceSpellCheckSupported) {
+                        // Allow the spell checker to remove existing misspelled span by
+                        // overwriting the span over the same place
+                        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
+                        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
+                        final int start;
+                        final int end;
+                        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
+                            start = spellCheckSpanStart + offset;
+                            end = start + length;
+                        } else {
+                            start = spellCheckSpanStart;
+                            end = spellCheckSpanEnd;
+                        }
+                        if (spellCheckSpanStart >= 0 && spellCheckSpanEnd > spellCheckSpanStart
+                                && end > start) {
+                            final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
+                            final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
+                            if (tempSuggestionSpan != null) {
+                                if (DBG) {
+                                    Log.i(TAG, "Remove existing misspelled span. "
+                                            + editable.subSequence(start, end));
+                                }
+                                editable.removeSpan(tempSuggestionSpan);
+                                mSuggestionSpanCache.remove(key);
+                            }
+                        }
+                    }
+                }
+                return spellCheckSpan;
+            }
+        }
+        return null;
+    }
+
+    @Override
+    public void onGetSuggestions(SuggestionsInfo[] results) {
+        final Editable editable = (Editable) mTextView.getText();
+        for (int i = 0; i < results.length; ++i) {
+            final SpellCheckSpan spellCheckSpan =
+                    onGetSuggestionsInternal(results[i], USE_SPAN_RANGE, USE_SPAN_RANGE);
+            if (spellCheckSpan != null) {
+                // onSpellCheckSpanRemoved will recycle this span in the pool
+                editable.removeSpan(spellCheckSpan);
+            }
+        }
+        scheduleNewSpellCheck();
+    }
+
+    @Override
+    public void onGetSentenceSuggestions(SentenceSuggestionsInfo[] results) {
+        final Editable editable = (Editable) mTextView.getText();
+
+        for (int i = 0; i < results.length; ++i) {
+            final SentenceSuggestionsInfo ssi = results[i];
+            if (ssi == null) {
+                continue;
+            }
+            SpellCheckSpan spellCheckSpan = null;
+            for (int j = 0; j < ssi.getSuggestionsCount(); ++j) {
+                final SuggestionsInfo suggestionsInfo = ssi.getSuggestionsInfoAt(j);
+                if (suggestionsInfo == null) {
+                    continue;
+                }
+                final int offset = ssi.getOffsetAt(j);
+                final int length = ssi.getLengthAt(j);
+                final SpellCheckSpan scs = onGetSuggestionsInternal(
+                        suggestionsInfo, offset, length);
+                if (spellCheckSpan == null && scs != null) {
+                    // the spellCheckSpan is shared by all the "SuggestionsInfo"s in the same
+                    // SentenceSuggestionsInfo. Removal is deferred after this loop.
+                    spellCheckSpan = scs;
+                }
+            }
+            if (spellCheckSpan != null) {
+                // onSpellCheckSpanRemoved will recycle this span in the pool
+                editable.removeSpan(spellCheckSpan);
+            }
+        }
+        scheduleNewSpellCheck();
+    }
+
+    private void scheduleNewSpellCheck() {
+        if (DBG) {
+            Log.i(TAG, "schedule new spell check.");
+        }
+        if (mSpellRunnable == null) {
+            mSpellRunnable = new Runnable() {
+                @Override
+                public void run() {
+                    final int length = mSpellParsers.length;
+                    for (int i = 0; i < length; i++) {
+                        final SpellParser spellParser = mSpellParsers[i];
+                        if (!spellParser.isFinished()) {
+                            spellParser.parse();
+                            break; // run one spell parser at a time to bound running time
+                        }
+                    }
+                }
+            };
+        } else {
+            mTextView.removeCallbacks(mSpellRunnable);
+        }
+
+        mTextView.postDelayed(mSpellRunnable, SPELL_PAUSE_DURATION);
+    }
+
+    private void createMisspelledSuggestionSpan(Editable editable, SuggestionsInfo suggestionsInfo,
+            SpellCheckSpan spellCheckSpan, int offset, int length) {
+        final int spellCheckSpanStart = editable.getSpanStart(spellCheckSpan);
+        final int spellCheckSpanEnd = editable.getSpanEnd(spellCheckSpan);
+        if (spellCheckSpanStart < 0 || spellCheckSpanEnd <= spellCheckSpanStart)
+            return; // span was removed in the meantime
+
+        final int start;
+        final int end;
+        if (offset != USE_SPAN_RANGE && length != USE_SPAN_RANGE) {
+            start = spellCheckSpanStart + offset;
+            end = start + length;
+        } else {
+            start = spellCheckSpanStart;
+            end = spellCheckSpanEnd;
+        }
+
+        final int suggestionsCount = suggestionsInfo.getSuggestionsCount();
+        String[] suggestions;
+        if (suggestionsCount > 0) {
+            suggestions = new String[suggestionsCount];
+            for (int i = 0; i < suggestionsCount; i++) {
+                suggestions[i] = suggestionsInfo.getSuggestionAt(i);
+            }
+        } else {
+            suggestions = ArrayUtils.emptyArray(String.class);
+        }
+
+        SuggestionSpan suggestionSpan = new SuggestionSpan(mTextView.getContext(), suggestions,
+                SuggestionSpan.FLAG_EASY_CORRECT | SuggestionSpan.FLAG_MISSPELLED);
+        // TODO: Remove mIsSentenceSpellCheckSupported by extracting an interface
+        // to share the logic of word level spell checker and sentence level spell checker
+        if (mIsSentenceSpellCheckSupported) {
+            final Long key = Long.valueOf(TextUtils.packRangeInLong(start, end));
+            final SuggestionSpan tempSuggestionSpan = mSuggestionSpanCache.get(key);
+            if (tempSuggestionSpan != null) {
+                if (DBG) {
+                    Log.i(TAG, "Cached span on the same position is cleard. "
+                            + editable.subSequence(start, end));
+                }
+                editable.removeSpan(tempSuggestionSpan);
+            }
+            mSuggestionSpanCache.put(key, suggestionSpan);
+        }
+        editable.setSpan(suggestionSpan, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+        mTextView.invalidateRegion(start, end, false /* No cursor involved */);
+    }
+
+    private class SpellParser {
+        private Object mRange = new Object();
+
+        public void parse(int start, int end) {
+            final int max = mTextView.length();
+            final int parseEnd;
+            if (end > max) {
+                Log.w(TAG, "Parse invalid region, from " + start + " to " + end);
+                parseEnd = max;
+            } else {
+                parseEnd = end;
+            }
+            if (parseEnd > start) {
+                setRangeSpan((Editable) mTextView.getText(), start, parseEnd);
+                parse();
+            }
+        }
+
+        public boolean isFinished() {
+            return ((Editable) mTextView.getText()).getSpanStart(mRange) < 0;
+        }
+
+        public void stop() {
+            removeRangeSpan((Editable) mTextView.getText());
+        }
+
+        private void setRangeSpan(Editable editable, int start, int end) {
+            if (DBG) {
+                Log.d(TAG, "set next range span: " + start + ", " + end);
+            }
+            editable.setSpan(mRange, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+
+        private void removeRangeSpan(Editable editable) {
+            if (DBG) {
+                Log.d(TAG, "Remove range span." + editable.getSpanStart(editable)
+                        + editable.getSpanEnd(editable));
+            }
+            editable.removeSpan(mRange);
+        }
+
+        public void parse() {
+            Editable editable = (Editable) mTextView.getText();
+            // Iterate over the newly added text and schedule new SpellCheckSpans
+            final int start;
+            if (mIsSentenceSpellCheckSupported) {
+                // TODO: Find the start position of the sentence.
+                // Set span with the context
+                start =  Math.max(
+                        0, editable.getSpanStart(mRange) - MIN_SENTENCE_LENGTH);
+            } else {
+                start = editable.getSpanStart(mRange);
+            }
+
+            final int end = editable.getSpanEnd(mRange);
+
+            int wordIteratorWindowEnd = Math.min(end, start + WORD_ITERATOR_INTERVAL);
+            mWordIterator.setCharSequence(editable, start, wordIteratorWindowEnd);
+
+            // Move back to the beginning of the current word, if any
+            int wordStart = mWordIterator.preceding(start);
+            int wordEnd;
+            if (wordStart == BreakIterator.DONE) {
+                wordEnd = mWordIterator.following(start);
+                if (wordEnd != BreakIterator.DONE) {
+                    wordStart = mWordIterator.getBeginning(wordEnd);
+                }
+            } else {
+                wordEnd = mWordIterator.getEnd(wordStart);
+            }
+            if (wordEnd == BreakIterator.DONE) {
+                if (DBG) {
+                    Log.i(TAG, "No more spell check.");
+                }
+                removeRangeSpan(editable);
+                return;
+            }
+
+            // We need to expand by one character because we want to include the spans that
+            // end/start at position start/end respectively.
+            SpellCheckSpan[] spellCheckSpans = editable.getSpans(start - 1, end + 1,
+                    SpellCheckSpan.class);
+            SuggestionSpan[] suggestionSpans = editable.getSpans(start - 1, end + 1,
+                    SuggestionSpan.class);
+
+            int wordCount = 0;
+            boolean scheduleOtherSpellCheck = false;
+
+            if (mIsSentenceSpellCheckSupported) {
+                if (wordIteratorWindowEnd < end) {
+                    if (DBG) {
+                        Log.i(TAG, "schedule other spell check.");
+                    }
+                    // Several batches needed on that region. Cut after last previous word
+                    scheduleOtherSpellCheck = true;
+                }
+                int spellCheckEnd = mWordIterator.preceding(wordIteratorWindowEnd);
+                boolean correct = spellCheckEnd != BreakIterator.DONE;
+                if (correct) {
+                    spellCheckEnd = mWordIterator.getEnd(spellCheckEnd);
+                    correct = spellCheckEnd != BreakIterator.DONE;
+                }
+                if (!correct) {
+                    if (DBG) {
+                        Log.i(TAG, "Incorrect range span.");
+                    }
+                    removeRangeSpan(editable);
+                    return;
+                }
+                do {
+                    // TODO: Find the start position of the sentence.
+                    int spellCheckStart = wordStart;
+                    boolean createSpellCheckSpan = true;
+                    // Cancel or merge overlapped spell check spans
+                    for (int i = 0; i < mLength; ++i) {
+                        final SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i];
+                        if (mIds[i] < 0 || spellCheckSpan.isSpellCheckInProgress()) {
+                            continue;
+                        }
+                        final int spanStart = editable.getSpanStart(spellCheckSpan);
+                        final int spanEnd = editable.getSpanEnd(spellCheckSpan);
+                        if (spanEnd < spellCheckStart || spellCheckEnd < spanStart) {
+                            // No need to merge
+                            continue;
+                        }
+                        if (spanStart <= spellCheckStart && spellCheckEnd <= spanEnd) {
+                            // There is a completely overlapped spell check span
+                            // skip this span
+                            createSpellCheckSpan = false;
+                            if (DBG) {
+                                Log.i(TAG, "The range is overrapped. Skip spell check.");
+                            }
+                            break;
+                        }
+                        // This spellCheckSpan is replaced by the one we are creating
+                        editable.removeSpan(spellCheckSpan);
+                        spellCheckStart = Math.min(spanStart, spellCheckStart);
+                        spellCheckEnd = Math.max(spanEnd, spellCheckEnd);
+                    }
+
+                    if (DBG) {
+                        Log.d(TAG, "addSpellCheckSpan: "
+                                + ", End = " + spellCheckEnd + ", Start = " + spellCheckStart
+                                + ", next = " + scheduleOtherSpellCheck + "\n"
+                                + editable.subSequence(spellCheckStart, spellCheckEnd));
+                    }
+
+                    // Stop spell checking when there are no characters in the range.
+                    if (spellCheckEnd < start) {
+                        break;
+                    }
+                    if (spellCheckEnd <= spellCheckStart) {
+                        Log.w(TAG, "Trying to spellcheck invalid region, from "
+                                + start + " to " + end);
+                        break;
+                    }
+                    if (createSpellCheckSpan) {
+                        addSpellCheckSpan(editable, spellCheckStart, spellCheckEnd);
+                    }
+                } while (false);
+                wordStart = spellCheckEnd;
+            } else {
+                while (wordStart <= end) {
+                    if (wordEnd >= start && wordEnd > wordStart) {
+                        if (wordCount >= MAX_NUMBER_OF_WORDS) {
+                            scheduleOtherSpellCheck = true;
+                            break;
+                        }
+                        // A new word has been created across the interval boundaries with this
+                        // edit. The previous spans (that ended on start / started on end) are
+                        // not valid anymore and must be removed.
+                        if (wordStart < start && wordEnd > start) {
+                            removeSpansAt(editable, start, spellCheckSpans);
+                            removeSpansAt(editable, start, suggestionSpans);
+                        }
+
+                        if (wordStart < end && wordEnd > end) {
+                            removeSpansAt(editable, end, spellCheckSpans);
+                            removeSpansAt(editable, end, suggestionSpans);
+                        }
+
+                        // Do not create new boundary spans if they already exist
+                        boolean createSpellCheckSpan = true;
+                        if (wordEnd == start) {
+                            for (int i = 0; i < spellCheckSpans.length; i++) {
+                                final int spanEnd = editable.getSpanEnd(spellCheckSpans[i]);
+                                if (spanEnd == start) {
+                                    createSpellCheckSpan = false;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (wordStart == end) {
+                            for (int i = 0; i < spellCheckSpans.length; i++) {
+                                final int spanStart = editable.getSpanStart(spellCheckSpans[i]);
+                                if (spanStart == end) {
+                                    createSpellCheckSpan = false;
+                                    break;
+                                }
+                            }
+                        }
+
+                        if (createSpellCheckSpan) {
+                            addSpellCheckSpan(editable, wordStart, wordEnd);
+                        }
+                        wordCount++;
+                    }
+
+                    // iterate word by word
+                    int originalWordEnd = wordEnd;
+                    wordEnd = mWordIterator.following(wordEnd);
+                    if ((wordIteratorWindowEnd < end) &&
+                            (wordEnd == BreakIterator.DONE || wordEnd >= wordIteratorWindowEnd)) {
+                        wordIteratorWindowEnd =
+                                Math.min(end, originalWordEnd + WORD_ITERATOR_INTERVAL);
+                        mWordIterator.setCharSequence(
+                                editable, originalWordEnd, wordIteratorWindowEnd);
+                        wordEnd = mWordIterator.following(originalWordEnd);
+                    }
+                    if (wordEnd == BreakIterator.DONE) break;
+                    wordStart = mWordIterator.getBeginning(wordEnd);
+                    if (wordStart == BreakIterator.DONE) {
+                        break;
+                    }
+                }
+            }
+
+            if (scheduleOtherSpellCheck && wordStart != BreakIterator.DONE && wordStart <= end) {
+                // Update range span: start new spell check from last wordStart
+                setRangeSpan(editable, wordStart, end);
+            } else {
+                removeRangeSpan(editable);
+            }
+
+            spellCheck();
+        }
+
+        private <T> void removeSpansAt(Editable editable, int offset, T[] spans) {
+            final int length = spans.length;
+            for (int i = 0; i < length; i++) {
+                final T span = spans[i];
+                final int start = editable.getSpanStart(span);
+                if (start > offset) continue;
+                final int end = editable.getSpanEnd(span);
+                if (end < offset) continue;
+                editable.removeSpan(span);
+            }
+        }
+    }
+
+    public static boolean haveWordBoundariesChanged(final Editable editable, final int start,
+            final int end, final int spanStart, final int spanEnd) {
+        final boolean haveWordBoundariesChanged;
+        if (spanEnd != start && spanStart != end) {
+            haveWordBoundariesChanged = true;
+            if (DBG) {
+                Log.d(TAG, "(1) Text inside the span has been modified. Remove.");
+            }
+        } else if (spanEnd == start && start < editable.length()) {
+            final int codePoint = Character.codePointAt(editable, start);
+            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
+            if (DBG) {
+                Log.d(TAG, "(2) Characters have been appended to the spanned text. "
+                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep. <") + (char)(codePoint)
+                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
+                        + start);
+            }
+        } else if (spanStart == end && end > 0) {
+            final int codePoint = Character.codePointBefore(editable, end);
+            haveWordBoundariesChanged = Character.isLetterOrDigit(codePoint);
+            if (DBG) {
+                Log.d(TAG, "(3) Characters have been prepended to the spanned text. "
+                        + (haveWordBoundariesChanged ? "Remove.<" : "Keep.<") + (char)(codePoint)
+                        + ">, " + editable + ", " + editable.subSequence(spanStart, spanEnd) + ", "
+                        + end);
+            }
+        } else {
+            if (DBG) {
+                Log.d(TAG, "(4) Characters adjacent to the spanned text were deleted. Keep.");
+            }
+            haveWordBoundariesChanged = false;
+        }
+        return haveWordBoundariesChanged;
+    }
+}
diff --git a/android/widget/Spinner.java b/android/widget/Spinner.java
new file mode 100644
index 0000000..ddf0e74
--- /dev/null
+++ b/android/widget/Spinner.java
@@ -0,0 +1,1299 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.DialogInterface.OnClickListener;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.content.res.TypedArray;
+import android.database.DataSetObserver;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.ViewTreeObserver.OnGlobalLayoutListener;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.widget.PopupWindow.OnDismissListener;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.ShowableListMenu;
+
+/**
+ * A view that displays one child at a time and lets the user pick among them.
+ * The items in the Spinner come from the {@link Adapter} associated with
+ * this view.
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/spinner.html">Spinners</a> guide.</p>
+ *
+ * @attr ref android.R.styleable#Spinner_dropDownSelector
+ * @attr ref android.R.styleable#Spinner_dropDownWidth
+ * @attr ref android.R.styleable#Spinner_gravity
+ * @attr ref android.R.styleable#Spinner_popupBackground
+ * @attr ref android.R.styleable#Spinner_prompt
+ * @attr ref android.R.styleable#Spinner_spinnerMode
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+ * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+ */
+@Widget
+public class Spinner extends AbsSpinner implements OnClickListener {
+    private static final String TAG = "Spinner";
+
+    // Only measure this many items to get a decent max width.
+    private static final int MAX_ITEMS_MEASURED = 15;
+
+    /**
+     * Use a dialog window for selecting spinner options.
+     */
+    public static final int MODE_DIALOG = 0;
+
+    /**
+     * Use a dropdown anchored to the Spinner for selecting spinner options.
+     */
+    public static final int MODE_DROPDOWN = 1;
+
+    /**
+     * Use the theme-supplied value to select the dropdown mode.
+     */
+    private static final int MODE_THEME = -1;
+
+    private final Rect mTempRect = new Rect();
+
+    /** Context used to inflate the popup window or dialog. */
+    private final Context mPopupContext;
+
+    /** Forwarding listener used to implement drag-to-open. */
+    private ForwardingListener mForwardingListener;
+
+    /** Temporary holder for setAdapter() calls from the super constructor. */
+    private SpinnerAdapter mTempAdapter;
+
+    private SpinnerPopup mPopup;
+    int mDropDownWidth;
+
+    private int mGravity;
+    private boolean mDisableChildrenWhenDisabled;
+
+    /**
+     * Constructs a new spinner with the given context's theme.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     */
+    public Spinner(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Constructs a new spinner with the given context's theme and the supplied
+     * mode of displaying choices. <code>mode</code> may be one of
+     * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN}.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param mode Constant describing how the user will select choices from
+     *             the spinner.
+     *
+     * @see #MODE_DIALOG
+     * @see #MODE_DROPDOWN
+     */
+    public Spinner(Context context, int mode) {
+        this(context, null, com.android.internal.R.attr.spinnerStyle, mode);
+    }
+
+    /**
+     * Constructs a new spinner with the given context's theme and the supplied
+     * attribute set.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     */
+    public Spinner(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.spinnerStyle);
+    }
+
+    /**
+     * Constructs a new spinner with the given context's theme, the supplied
+     * attribute set, and default style attribute.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     */
+    public Spinner(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0, MODE_THEME);
+    }
+
+    /**
+     * Constructs a new spinner with the given context's theme, the supplied
+     * attribute set, and default style attribute. <code>mode</code> may be one
+     * of {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
+     * user will select choices from the spinner.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for defaults.
+     * @param mode Constant describing how the user will select choices from the
+     *             spinner.
+     *
+     * @see #MODE_DIALOG
+     * @see #MODE_DROPDOWN
+     */
+    public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int mode) {
+        this(context, attrs, defStyleAttr, 0, mode);
+    }
+
+    /**
+     * Constructs a new spinner with the given context's theme, the supplied
+     * attribute set, and default styles. <code>mode</code> may be one of
+     * {@link #MODE_DIALOG} or {@link #MODE_DROPDOWN} and determines how the
+     * user will select choices from the spinner.
+     *
+     * @param context The Context the view is running in, through which it can
+     *                access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *                    supplies default values for the view, used only if
+     *                    defStyleAttr is 0 or can not be found in the theme.
+     *                    Can be 0 to not look for defaults.
+     * @param mode Constant describing how the user will select choices from
+     *             the spinner.
+     *
+     * @see #MODE_DIALOG
+     * @see #MODE_DROPDOWN
+     */
+    public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes,
+            int mode) {
+        this(context, attrs, defStyleAttr, defStyleRes, mode, null);
+    }
+
+    /**
+     * Constructs a new spinner with the given context, the supplied attribute
+     * set, default styles, popup mode (one of {@link #MODE_DIALOG} or
+     * {@link #MODE_DROPDOWN}), and the theme against which the popup should be
+     * inflated.
+     *
+     * @param context The context against which the view is inflated, which
+     *                provides access to the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *                     reference to a style resource that supplies default
+     *                     values for the view. Can be 0 to not look for
+     *                     defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *                    supplies default values for the view, used only if
+     *                    defStyleAttr is 0 or can not be found in the theme.
+     *                    Can be 0 to not look for defaults.
+     * @param mode Constant describing how the user will select choices from
+     *             the spinner.
+     * @param popupTheme The theme against which the dialog or dropdown popup
+     *                   should be inflated. May be {@code null} to use the
+     *                   view theme. If set, this will override any value
+     *                   specified by
+     *                   {@link android.R.styleable#Spinner_popupTheme}.
+     *
+     * @see #MODE_DIALOG
+     * @see #MODE_DROPDOWN
+     */
+    public Spinner(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes, int mode,
+            Theme popupTheme) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
+
+        if (popupTheme != null) {
+            mPopupContext = new ContextThemeWrapper(context, popupTheme);
+        } else {
+            final int popupThemeResId = a.getResourceId(R.styleable.Spinner_popupTheme, 0);
+            if (popupThemeResId != 0) {
+                mPopupContext = new ContextThemeWrapper(context, popupThemeResId);
+            } else {
+                mPopupContext = context;
+            }
+        }
+
+        if (mode == MODE_THEME) {
+            mode = a.getInt(R.styleable.Spinner_spinnerMode, MODE_DIALOG);
+        }
+
+        switch (mode) {
+            case MODE_DIALOG: {
+                mPopup = new DialogPopup();
+                mPopup.setPromptText(a.getString(R.styleable.Spinner_prompt));
+                break;
+            }
+
+            case MODE_DROPDOWN: {
+                final DropdownPopup popup = new DropdownPopup(
+                        mPopupContext, attrs, defStyleAttr, defStyleRes);
+                final TypedArray pa = mPopupContext.obtainStyledAttributes(
+                        attrs, R.styleable.Spinner, defStyleAttr, defStyleRes);
+                mDropDownWidth = pa.getLayoutDimension(R.styleable.Spinner_dropDownWidth,
+                        ViewGroup.LayoutParams.WRAP_CONTENT);
+                if (pa.hasValueOrEmpty(R.styleable.Spinner_dropDownSelector)) {
+                    popup.setListSelector(pa.getDrawable(
+                            R.styleable.Spinner_dropDownSelector));
+                }
+                popup.setBackgroundDrawable(pa.getDrawable(R.styleable.Spinner_popupBackground));
+                popup.setPromptText(a.getString(R.styleable.Spinner_prompt));
+                pa.recycle();
+
+                mPopup = popup;
+                mForwardingListener = new ForwardingListener(this) {
+                    @Override
+                    public ShowableListMenu getPopup() {
+                        return popup;
+                    }
+
+                    @Override
+                    public boolean onForwardingStarted() {
+                        if (!mPopup.isShowing()) {
+                            mPopup.show(getTextDirection(), getTextAlignment());
+                        }
+                        return true;
+                    }
+                };
+                break;
+            }
+        }
+
+        mGravity = a.getInt(R.styleable.Spinner_gravity, Gravity.CENTER);
+        mDisableChildrenWhenDisabled = a.getBoolean(
+                R.styleable.Spinner_disableChildrenWhenDisabled, false);
+
+        a.recycle();
+
+        // Base constructor can call setAdapter before we initialize mPopup.
+        // Finish setting things up if this happened.
+        if (mTempAdapter != null) {
+            setAdapter(mTempAdapter);
+            mTempAdapter = null;
+        }
+    }
+
+    /**
+     * @return the context used to inflate the Spinner's popup or dialog window
+     */
+    public Context getPopupContext() {
+        return mPopupContext;
+    }
+
+    /**
+     * Set the background drawable for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+     *
+     * @param background Background drawable
+     *
+     * @attr ref android.R.styleable#Spinner_popupBackground
+     */
+    public void setPopupBackgroundDrawable(Drawable background) {
+        if (!(mPopup instanceof DropdownPopup)) {
+            Log.e(TAG, "setPopupBackgroundDrawable: incompatible spinner mode; ignoring...");
+            return;
+        }
+        mPopup.setBackgroundDrawable(background);
+    }
+
+    /**
+     * Set the background drawable for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+     *
+     * @param resId Resource ID of a background drawable
+     *
+     * @attr ref android.R.styleable#Spinner_popupBackground
+     */
+    public void setPopupBackgroundResource(@DrawableRes int resId) {
+        setPopupBackgroundDrawable(getPopupContext().getDrawable(resId));
+    }
+
+    /**
+     * Get the background drawable for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; other modes will return null.
+     *
+     * @return background Background drawable
+     *
+     * @attr ref android.R.styleable#Spinner_popupBackground
+     */
+    public Drawable getPopupBackground() {
+        return mPopup.getBackground();
+    }
+
+    /**
+     * @hide
+     */
+    @TestApi
+    public boolean isPopupShowing() {
+        return (mPopup != null) && mPopup.isShowing();
+    }
+
+    /**
+     * Set a vertical offset in pixels for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+     *
+     * @param pixels Vertical offset in pixels
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+     */
+    public void setDropDownVerticalOffset(int pixels) {
+        mPopup.setVerticalOffset(pixels);
+    }
+
+    /**
+     * Get the configured vertical offset in pixels for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
+     *
+     * @return Vertical offset in pixels
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownVerticalOffset
+     */
+    public int getDropDownVerticalOffset() {
+        return mPopup.getVerticalOffset();
+    }
+
+    /**
+     * Set a horizontal offset in pixels for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.
+     *
+     * @param pixels Horizontal offset in pixels
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+     */
+    public void setDropDownHorizontalOffset(int pixels) {
+        mPopup.setHorizontalOffset(pixels);
+    }
+
+    /**
+     * Get the configured horizontal offset in pixels for the spinner's popup window of choices.
+     * Only valid in {@link #MODE_DROPDOWN}; other modes will return 0.
+     *
+     * @return Horizontal offset in pixels
+     *
+     * @attr ref android.R.styleable#ListPopupWindow_dropDownHorizontalOffset
+     */
+    public int getDropDownHorizontalOffset() {
+        return mPopup.getHorizontalOffset();
+    }
+
+    /**
+     * Set the width of the spinner's popup window of choices in pixels. This value
+     * may also be set to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+     * to match the width of the Spinner itself, or
+     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+     * of contained dropdown list items.
+     *
+     * <p>Only valid in {@link #MODE_DROPDOWN}; this method is a no-op in other modes.</p>
+     *
+     * @param pixels Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+     *
+     * @attr ref android.R.styleable#Spinner_dropDownWidth
+     */
+    public void setDropDownWidth(int pixels) {
+        if (!(mPopup instanceof DropdownPopup)) {
+            Log.e(TAG, "Cannot set dropdown width for MODE_DIALOG, ignoring");
+            return;
+        }
+        mDropDownWidth = pixels;
+    }
+
+    /**
+     * Get the configured width of the spinner's popup window of choices in pixels.
+     * The returned value may also be {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+     * meaning the popup window will match the width of the Spinner itself, or
+     * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} to wrap to the measured size
+     * of contained dropdown list items.
+     *
+     * @return Width in pixels, WRAP_CONTENT, or MATCH_PARENT
+     *
+     * @attr ref android.R.styleable#Spinner_dropDownWidth
+     */
+    public int getDropDownWidth() {
+        return mDropDownWidth;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        if (mDisableChildrenWhenDisabled) {
+            final int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                getChildAt(i).setEnabled(enabled);
+            }
+        }
+    }
+
+    /**
+     * Describes how the selected item view is positioned. Currently only the horizontal component
+     * is used. The default is determined by the current theme.
+     *
+     * @param gravity See {@link android.view.Gravity}
+     *
+     * @attr ref android.R.styleable#Spinner_gravity
+     */
+    public void setGravity(int gravity) {
+        if (mGravity != gravity) {
+            if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == 0) {
+                gravity |= Gravity.START;
+            }
+            mGravity = gravity;
+            requestLayout();
+        }
+    }
+
+    /**
+     * Describes how the selected item view is positioned. The default is determined by the
+     * current theme.
+     *
+     * @return A {@link android.view.Gravity Gravity} value
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    /**
+     * Sets the {@link SpinnerAdapter} used to provide the data which backs
+     * this Spinner.
+     * <p>
+     * If this Spinner has a popup theme set in XML via the
+     * {@link android.R.styleable#Spinner_popupTheme popupTheme} attribute, the
+     * adapter should inflate drop-down views using the same theme. The easiest
+     * way to achieve this is by using {@link #getPopupContext()} to obtain a
+     * layout inflater for use in
+     * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}.
+     * <p>
+     * Spinner overrides {@link Adapter#getViewTypeCount()} on the
+     * Adapter associated with this view. Calling
+     * {@link Adapter#getItemViewType(int) getItemViewType(int)} on the object
+     * returned from {@link #getAdapter()} will always return 0. Calling
+     * {@link Adapter#getViewTypeCount() getViewTypeCount()} will always return
+     * 1. On API {@link Build.VERSION_CODES#LOLLIPOP} and above, attempting to set an
+     * adapter with more than one view type will throw an
+     * {@link IllegalArgumentException}.
+     *
+     * @param adapter the adapter to set
+     *
+     * @see AbsSpinner#setAdapter(SpinnerAdapter)
+     * @throws IllegalArgumentException if the adapter has more than one view
+     *         type
+     */
+    @Override
+    public void setAdapter(SpinnerAdapter adapter) {
+        // The super constructor may call setAdapter before we're prepared.
+        // Postpone doing anything until we've finished construction.
+        if (mPopup == null) {
+            mTempAdapter = adapter;
+            return;
+        }
+
+        super.setAdapter(adapter);
+
+        mRecycler.clear();
+
+        final int targetSdkVersion = mContext.getApplicationInfo().targetSdkVersion;
+        if (targetSdkVersion >= Build.VERSION_CODES.LOLLIPOP
+                && adapter != null && adapter.getViewTypeCount() != 1) {
+            throw new IllegalArgumentException("Spinner adapter view type count must be 1");
+        }
+
+        final Context popupContext = mPopupContext == null ? mContext : mPopupContext;
+        mPopup.setAdapter(new DropDownAdapter(adapter, popupContext.getTheme()));
+    }
+
+    @Override
+    public int getBaseline() {
+        View child = null;
+
+        if (getChildCount() > 0) {
+            child = getChildAt(0);
+        } else if (mAdapter != null && mAdapter.getCount() > 0) {
+            child = makeView(0, false);
+            mRecycler.put(0, child);
+        }
+
+        if (child != null) {
+            final int childBaseline = child.getBaseline();
+            return childBaseline >= 0 ? child.getTop() + childBaseline : -1;
+        } else {
+            return -1;
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mPopup != null && mPopup.isShowing()) {
+            mPopup.dismiss();
+        }
+    }
+
+    /**
+     * <p>A spinner does not support item click events. Calling this method
+     * will raise an exception.</p>
+     * <p>Instead use {@link AdapterView#setOnItemSelectedListener}.
+     *
+     * @param l this listener will be ignored
+     */
+    @Override
+    public void setOnItemClickListener(OnItemClickListener l) {
+        throw new RuntimeException("setOnItemClickListener cannot be used with a spinner.");
+    }
+
+    /**
+     * @hide internal use only
+     */
+    public void setOnItemClickListenerInt(OnItemClickListener l) {
+        super.setOnItemClickListener(l);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if (mForwardingListener != null && mForwardingListener.onTouch(this, event)) {
+            return true;
+        }
+
+        return super.onTouchEvent(event);
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+        if (mPopup != null && MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.AT_MOST) {
+            final int measuredWidth = getMeasuredWidth();
+            setMeasuredDimension(Math.min(Math.max(measuredWidth,
+                    measureContentWidth(getAdapter(), getBackground())),
+                    MeasureSpec.getSize(widthMeasureSpec)),
+                    getMeasuredHeight());
+        }
+    }
+
+    /**
+     * @see android.view.View#onLayout(boolean,int,int,int,int)
+     *
+     * Creates and positions all views
+     *
+     */
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        super.onLayout(changed, l, t, r, b);
+        mInLayout = true;
+        layout(0, false);
+        mInLayout = false;
+    }
+
+    /**
+     * Creates and positions all views for this Spinner.
+     *
+     * @param delta Change in the selected position. +1 means selection is moving to the right,
+     * so views are scrolling to the left. -1 means selection is moving to the left.
+     */
+    @Override
+    void layout(int delta, boolean animate) {
+        int childrenLeft = mSpinnerPadding.left;
+        int childrenWidth = mRight - mLeft - mSpinnerPadding.left - mSpinnerPadding.right;
+
+        if (mDataChanged) {
+            handleDataChanged();
+        }
+
+        // Handle the empty set by removing all views
+        if (mItemCount == 0) {
+            resetList();
+            return;
+        }
+
+        if (mNextSelectedPosition >= 0) {
+            setSelectedPositionInt(mNextSelectedPosition);
+        }
+
+        recycleAllViews();
+
+        // Clear out old views
+        removeAllViewsInLayout();
+
+        // Make selected view and position it
+        mFirstPosition = mSelectedPosition;
+
+        if (mAdapter != null) {
+            View sel = makeView(mSelectedPosition, true);
+            int width = sel.getMeasuredWidth();
+            int selectedOffset = childrenLeft;
+            final int layoutDirection = getLayoutDirection();
+            final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+            switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                case Gravity.CENTER_HORIZONTAL:
+                    selectedOffset = childrenLeft + (childrenWidth / 2) - (width / 2);
+                    break;
+                case Gravity.RIGHT:
+                    selectedOffset = childrenLeft + childrenWidth - width;
+                    break;
+            }
+            sel.offsetLeftAndRight(selectedOffset);
+        }
+
+        // Flush any cached views that did not get reused above
+        mRecycler.clear();
+
+        invalidate();
+
+        checkSelectionChanged();
+
+        mDataChanged = false;
+        mNeedSync = false;
+        setNextSelectedPositionInt(mSelectedPosition);
+    }
+
+    /**
+     * Obtain a view, either by pulling an existing view from the recycler or
+     * by getting a new one from the adapter. If we are animating, make sure
+     * there is enough information in the view's layout parameters to animate
+     * from the old to new positions.
+     *
+     * @param position Position in the spinner for the view to obtain
+     * @param addChild true to add the child to the spinner, false to obtain and configure only.
+     * @return A view for the given position
+     */
+    private View makeView(int position, boolean addChild) {
+        View child;
+
+        if (!mDataChanged) {
+            child = mRecycler.get(position);
+            if (child != null) {
+                // Position the view
+                setUpChild(child, addChild);
+
+                return child;
+            }
+        }
+
+        // Nothing found in the recycler -- ask the adapter for a view
+        child = mAdapter.getView(position, null, this);
+
+        // Position the view
+        setUpChild(child, addChild);
+
+        return child;
+    }
+
+    /**
+     * Helper for makeAndAddView to set the position of a view
+     * and fill out its layout paramters.
+     *
+     * @param child The view to position
+     * @param addChild true if the child should be added to the Spinner during setup
+     */
+    private void setUpChild(View child, boolean addChild) {
+
+        // Respect layout params that are already in the view. Otherwise
+        // make some up...
+        ViewGroup.LayoutParams lp = child.getLayoutParams();
+        if (lp == null) {
+            lp = generateDefaultLayoutParams();
+        }
+
+        addViewInLayout(child, 0, lp);
+
+        child.setSelected(hasFocus());
+        if (mDisableChildrenWhenDisabled) {
+            child.setEnabled(isEnabled());
+        }
+
+        // Get measure specs
+        int childHeightSpec = ViewGroup.getChildMeasureSpec(mHeightMeasureSpec,
+                mSpinnerPadding.top + mSpinnerPadding.bottom, lp.height);
+        int childWidthSpec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
+                mSpinnerPadding.left + mSpinnerPadding.right, lp.width);
+
+        // Measure child
+        child.measure(childWidthSpec, childHeightSpec);
+
+        int childLeft;
+        int childRight;
+
+        // Position vertically based on gravity setting
+        int childTop = mSpinnerPadding.top
+                + ((getMeasuredHeight() - mSpinnerPadding.bottom -
+                        mSpinnerPadding.top - child.getMeasuredHeight()) / 2);
+        int childBottom = childTop + child.getMeasuredHeight();
+
+        int width = child.getMeasuredWidth();
+        childLeft = 0;
+        childRight = childLeft + width;
+
+        child.layout(childLeft, childTop, childRight, childBottom);
+
+        if (!addChild) {
+            removeViewInLayout(child);
+        }
+    }
+
+    @Override
+    public boolean performClick() {
+        boolean handled = super.performClick();
+
+        if (!handled) {
+            handled = true;
+
+            if (!mPopup.isShowing()) {
+                mPopup.show(getTextDirection(), getTextAlignment());
+            }
+        }
+
+        return handled;
+    }
+
+    @Override
+    public void onClick(DialogInterface dialog, int which) {
+        setSelection(which);
+        dialog.dismiss();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Spinner.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        if (mAdapter != null) {
+            info.setCanOpenPopup(true);
+        }
+    }
+
+    /**
+     * Sets the prompt to display when the dialog is shown.
+     * @param prompt the prompt to set
+     */
+    public void setPrompt(CharSequence prompt) {
+        mPopup.setPromptText(prompt);
+    }
+
+    /**
+     * Sets the prompt to display when the dialog is shown.
+     * @param promptId the resource ID of the prompt to display when the dialog is shown
+     */
+    public void setPromptId(int promptId) {
+        setPrompt(getContext().getText(promptId));
+    }
+
+    /**
+     * @return The prompt to display when the dialog is shown
+     */
+    public CharSequence getPrompt() {
+        return mPopup.getHintText();
+    }
+
+    int measureContentWidth(SpinnerAdapter adapter, Drawable background) {
+        if (adapter == null) {
+            return 0;
+        }
+
+        int width = 0;
+        View itemView = null;
+        int itemType = 0;
+        final int widthMeasureSpec =
+            MeasureSpec.makeSafeMeasureSpec(getMeasuredWidth(), MeasureSpec.UNSPECIFIED);
+        final int heightMeasureSpec =
+            MeasureSpec.makeSafeMeasureSpec(getMeasuredHeight(), MeasureSpec.UNSPECIFIED);
+
+        // Make sure the number of items we'll measure is capped. If it's a huge data set
+        // with wildly varying sizes, oh well.
+        int start = Math.max(0, getSelectedItemPosition());
+        final int end = Math.min(adapter.getCount(), start + MAX_ITEMS_MEASURED);
+        final int count = end - start;
+        start = Math.max(0, start - (MAX_ITEMS_MEASURED - count));
+        for (int i = start; i < end; i++) {
+            final int positionType = adapter.getItemViewType(i);
+            if (positionType != itemType) {
+                itemType = positionType;
+                itemView = null;
+            }
+            itemView = adapter.getView(i, itemView, this);
+            if (itemView.getLayoutParams() == null) {
+                itemView.setLayoutParams(new ViewGroup.LayoutParams(
+                        ViewGroup.LayoutParams.WRAP_CONTENT,
+                        ViewGroup.LayoutParams.WRAP_CONTENT));
+            }
+            itemView.measure(widthMeasureSpec, heightMeasureSpec);
+            width = Math.max(width, itemView.getMeasuredWidth());
+        }
+
+        // Add background padding to measured width
+        if (background != null) {
+            background.getPadding(mTempRect);
+            width += mTempRect.left + mTempRect.right;
+        }
+
+        return width;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        final SavedState ss = new SavedState(super.onSaveInstanceState());
+        ss.showDropdown = mPopup != null && mPopup.isShowing();
+        return ss;
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        SavedState ss = (SavedState) state;
+
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        if (ss.showDropdown) {
+            ViewTreeObserver vto = getViewTreeObserver();
+            if (vto != null) {
+                final OnGlobalLayoutListener listener = new OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        if (!mPopup.isShowing()) {
+                            mPopup.show(getTextDirection(), getTextAlignment());
+                        }
+                        final ViewTreeObserver vto = getViewTreeObserver();
+                        if (vto != null) {
+                            vto.removeOnGlobalLayoutListener(this);
+                        }
+                    }
+                };
+                vto.addOnGlobalLayoutListener(listener);
+            }
+        }
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (getPointerIcon() == null && isClickable() && isEnabled()) {
+            return PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    static class SavedState extends AbsSpinner.SavedState {
+        boolean showDropdown;
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        private SavedState(Parcel in) {
+            super(in);
+            showDropdown = in.readByte() != 0;
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeByte((byte) (showDropdown ? 1 : 0));
+        }
+
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+            public SavedState createFromParcel(Parcel in) {
+                return new SavedState(in);
+            }
+
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    /**
+     * <p>Wrapper class for an Adapter. Transforms the embedded Adapter instance
+     * into a ListAdapter.</p>
+     */
+    private static class DropDownAdapter implements ListAdapter, SpinnerAdapter {
+        private SpinnerAdapter mAdapter;
+        private ListAdapter mListAdapter;
+
+        /**
+         * Creates a new ListAdapter wrapper for the specified adapter.
+         *
+         * @param adapter the SpinnerAdapter to transform into a ListAdapter
+         * @param dropDownTheme the theme against which to inflate drop-down
+         *                      views, may be {@null} to use default theme
+         */
+        public DropDownAdapter(@Nullable SpinnerAdapter adapter,
+                @Nullable Resources.Theme dropDownTheme) {
+            mAdapter = adapter;
+
+            if (adapter instanceof ListAdapter) {
+                mListAdapter = (ListAdapter) adapter;
+            }
+
+            if (dropDownTheme != null && adapter instanceof ThemedSpinnerAdapter) {
+                final ThemedSpinnerAdapter themedAdapter = (ThemedSpinnerAdapter) adapter;
+                if (themedAdapter.getDropDownViewTheme() == null) {
+                    themedAdapter.setDropDownViewTheme(dropDownTheme);
+                }
+            }
+        }
+
+        public int getCount() {
+            return mAdapter == null ? 0 : mAdapter.getCount();
+        }
+
+        public Object getItem(int position) {
+            return mAdapter == null ? null : mAdapter.getItem(position);
+        }
+
+        public long getItemId(int position) {
+            return mAdapter == null ? -1 : mAdapter.getItemId(position);
+        }
+
+        public View getView(int position, View convertView, ViewGroup parent) {
+            return getDropDownView(position, convertView, parent);
+        }
+
+        public View getDropDownView(int position, View convertView, ViewGroup parent) {
+            return (mAdapter == null) ? null : mAdapter.getDropDownView(position, convertView, parent);
+        }
+
+        public boolean hasStableIds() {
+            return mAdapter != null && mAdapter.hasStableIds();
+        }
+
+        public void registerDataSetObserver(DataSetObserver observer) {
+            if (mAdapter != null) {
+                mAdapter.registerDataSetObserver(observer);
+            }
+        }
+
+        public void unregisterDataSetObserver(DataSetObserver observer) {
+            if (mAdapter != null) {
+                mAdapter.unregisterDataSetObserver(observer);
+            }
+        }
+
+        /**
+         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
+         * Otherwise, return true.
+         */
+        public boolean areAllItemsEnabled() {
+            final ListAdapter adapter = mListAdapter;
+            if (adapter != null) {
+                return adapter.areAllItemsEnabled();
+            } else {
+                return true;
+            }
+        }
+
+        /**
+         * If the wrapped SpinnerAdapter is also a ListAdapter, delegate this call.
+         * Otherwise, return true.
+         */
+        public boolean isEnabled(int position) {
+            final ListAdapter adapter = mListAdapter;
+            if (adapter != null) {
+                return adapter.isEnabled(position);
+            } else {
+                return true;
+            }
+        }
+
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        public boolean isEmpty() {
+            return getCount() == 0;
+        }
+    }
+
+    /**
+     * Implements some sort of popup selection interface for selecting a spinner option.
+     * Allows for different spinner modes.
+     */
+    private interface SpinnerPopup {
+        public void setAdapter(ListAdapter adapter);
+
+        /**
+         * Show the popup
+         */
+        public void show(int textDirection, int textAlignment);
+
+        /**
+         * Dismiss the popup
+         */
+        public void dismiss();
+
+        /**
+         * @return true if the popup is showing, false otherwise.
+         */
+        public boolean isShowing();
+
+        /**
+         * Set hint text to be displayed to the user. This should provide
+         * a description of the choice being made.
+         * @param hintText Hint text to set.
+         */
+        public void setPromptText(CharSequence hintText);
+        public CharSequence getHintText();
+
+        public void setBackgroundDrawable(Drawable bg);
+        public void setVerticalOffset(int px);
+        public void setHorizontalOffset(int px);
+        public Drawable getBackground();
+        public int getVerticalOffset();
+        public int getHorizontalOffset();
+    }
+
+    private class DialogPopup implements SpinnerPopup, DialogInterface.OnClickListener {
+        private AlertDialog mPopup;
+        private ListAdapter mListAdapter;
+        private CharSequence mPrompt;
+
+        public void dismiss() {
+            if (mPopup != null) {
+                mPopup.dismiss();
+                mPopup = null;
+            }
+        }
+
+        public boolean isShowing() {
+            return mPopup != null ? mPopup.isShowing() : false;
+        }
+
+        public void setAdapter(ListAdapter adapter) {
+            mListAdapter = adapter;
+        }
+
+        public void setPromptText(CharSequence hintText) {
+            mPrompt = hintText;
+        }
+
+        public CharSequence getHintText() {
+            return mPrompt;
+        }
+
+        public void show(int textDirection, int textAlignment) {
+            if (mListAdapter == null) {
+                return;
+            }
+            AlertDialog.Builder builder = new AlertDialog.Builder(getPopupContext());
+            if (mPrompt != null) {
+                builder.setTitle(mPrompt);
+            }
+            mPopup = builder.setSingleChoiceItems(mListAdapter,
+                    getSelectedItemPosition(), this).create();
+            final ListView listView = mPopup.getListView();
+            listView.setTextDirection(textDirection);
+            listView.setTextAlignment(textAlignment);
+            mPopup.show();
+        }
+
+        public void onClick(DialogInterface dialog, int which) {
+            setSelection(which);
+            if (mOnItemClickListener != null) {
+                performItemClick(null, which, mListAdapter.getItemId(which));
+            }
+            dismiss();
+        }
+
+        @Override
+        public void setBackgroundDrawable(Drawable bg) {
+            Log.e(TAG, "Cannot set popup background for MODE_DIALOG, ignoring");
+        }
+
+        @Override
+        public void setVerticalOffset(int px) {
+            Log.e(TAG, "Cannot set vertical offset for MODE_DIALOG, ignoring");
+        }
+
+        @Override
+        public void setHorizontalOffset(int px) {
+            Log.e(TAG, "Cannot set horizontal offset for MODE_DIALOG, ignoring");
+        }
+
+        @Override
+        public Drawable getBackground() {
+            return null;
+        }
+
+        @Override
+        public int getVerticalOffset() {
+            return 0;
+        }
+
+        @Override
+        public int getHorizontalOffset() {
+            return 0;
+        }
+    }
+
+    private class DropdownPopup extends ListPopupWindow implements SpinnerPopup {
+        private CharSequence mHintText;
+        private ListAdapter mAdapter;
+
+        public DropdownPopup(
+                Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+            super(context, attrs, defStyleAttr, defStyleRes);
+
+            setAnchorView(Spinner.this);
+            setModal(true);
+            setPromptPosition(POSITION_PROMPT_ABOVE);
+            setOnItemClickListener(new OnItemClickListener() {
+                public void onItemClick(AdapterView parent, View v, int position, long id) {
+                    Spinner.this.setSelection(position);
+                    if (mOnItemClickListener != null) {
+                        Spinner.this.performItemClick(v, position, mAdapter.getItemId(position));
+                    }
+                    dismiss();
+                }
+            });
+        }
+
+        @Override
+        public void setAdapter(ListAdapter adapter) {
+            super.setAdapter(adapter);
+            mAdapter = adapter;
+        }
+
+        public CharSequence getHintText() {
+            return mHintText;
+        }
+
+        public void setPromptText(CharSequence hintText) {
+            // Hint text is ignored for dropdowns, but maintain it here.
+            mHintText = hintText;
+        }
+
+        void computeContentWidth() {
+            final Drawable background = getBackground();
+            int hOffset = 0;
+            if (background != null) {
+                background.getPadding(mTempRect);
+                hOffset = isLayoutRtl() ? mTempRect.right : -mTempRect.left;
+            } else {
+                mTempRect.left = mTempRect.right = 0;
+            }
+
+            final int spinnerPaddingLeft = Spinner.this.getPaddingLeft();
+            final int spinnerPaddingRight = Spinner.this.getPaddingRight();
+            final int spinnerWidth = Spinner.this.getWidth();
+
+            if (mDropDownWidth == WRAP_CONTENT) {
+                int contentWidth =  measureContentWidth(
+                        (SpinnerAdapter) mAdapter, getBackground());
+                final int contentWidthLimit = mContext.getResources()
+                        .getDisplayMetrics().widthPixels - mTempRect.left - mTempRect.right;
+                if (contentWidth > contentWidthLimit) {
+                    contentWidth = contentWidthLimit;
+                }
+                setContentWidth(Math.max(
+                       contentWidth, spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight));
+            } else if (mDropDownWidth == MATCH_PARENT) {
+                setContentWidth(spinnerWidth - spinnerPaddingLeft - spinnerPaddingRight);
+            } else {
+                setContentWidth(mDropDownWidth);
+            }
+
+            if (isLayoutRtl()) {
+                hOffset += spinnerWidth - spinnerPaddingRight - getWidth();
+            } else {
+                hOffset += spinnerPaddingLeft;
+            }
+            setHorizontalOffset(hOffset);
+        }
+
+        public void show(int textDirection, int textAlignment) {
+            final boolean wasShowing = isShowing();
+
+            computeContentWidth();
+
+            setInputMethodMode(ListPopupWindow.INPUT_METHOD_NOT_NEEDED);
+            super.show();
+            final ListView listView = getListView();
+            listView.setChoiceMode(ListView.CHOICE_MODE_SINGLE);
+            listView.setTextDirection(textDirection);
+            listView.setTextAlignment(textAlignment);
+            setSelection(Spinner.this.getSelectedItemPosition());
+
+            if (wasShowing) {
+                // Skip setting up the layout/dismiss listener below. If we were previously
+                // showing it will still stick around.
+                return;
+            }
+
+            // Make sure we hide if our anchor goes away.
+            // TODO: This might be appropriate to push all the way down to PopupWindow,
+            // but it may have other side effects to investigate first. (Text editing handles, etc.)
+            final ViewTreeObserver vto = getViewTreeObserver();
+            if (vto != null) {
+                final OnGlobalLayoutListener layoutListener = new OnGlobalLayoutListener() {
+                    @Override
+                    public void onGlobalLayout() {
+                        if (!Spinner.this.isVisibleToUser()) {
+                            dismiss();
+                        } else {
+                            computeContentWidth();
+
+                            // Use super.show here to update; we don't want to move the selected
+                            // position or adjust other things that would be reset otherwise.
+                            DropdownPopup.super.show();
+                        }
+                    }
+                };
+                vto.addOnGlobalLayoutListener(layoutListener);
+                setOnDismissListener(new OnDismissListener() {
+                    @Override public void onDismiss() {
+                        final ViewTreeObserver vto = getViewTreeObserver();
+                        if (vto != null) {
+                            vto.removeOnGlobalLayoutListener(layoutListener);
+                        }
+                    }
+                });
+            }
+        }
+    }
+
+}
diff --git a/android/widget/SpinnerAdapter.java b/android/widget/SpinnerAdapter.java
new file mode 100644
index 0000000..f99f45b
--- /dev/null
+++ b/android/widget/SpinnerAdapter.java
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Extended {@link Adapter} that is the bridge between a
+ * {@link android.widget.Spinner} and its data. A spinner adapter allows to
+ * define two different views: one that shows the data in the spinner itself
+ * and one that shows the data in the drop down list when the spinner is
+ * pressed.
+ */
+public interface SpinnerAdapter extends Adapter {
+    /**
+     * Gets a {@link android.view.View} that displays in the drop down popup
+     * the data at the specified position in the data set.
+     *
+     * @param position index of the item whose view we want.
+     * @param convertView the old view to reuse, if possible. Note: You should
+     *        check that this view is non-null and of an appropriate type before
+     *        using. If it is not possible to convert this view to display the
+     *        correct data, this method can create a new view.
+     * @param parent the parent that this view will eventually be attached to
+     * @return a {@link android.view.View} corresponding to the data at the
+     *         specified position.
+     */
+    public View getDropDownView(int position, View convertView, ViewGroup parent);
+}
diff --git a/android/widget/StackView.java b/android/widget/StackView.java
new file mode 100644
index 0000000..1b9055c
--- /dev/null
+++ b/android/widget/StackView.java
@@ -0,0 +1,1459 @@
+/* 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 android.widget;
+
+import android.animation.ObjectAnimator;
+import android.animation.PropertyValuesHolder;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Bitmap;
+import android.graphics.BlurMaskFilter;
+import android.graphics.Canvas;
+import android.graphics.Matrix;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.PorterDuffXfermode;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Region;
+import android.graphics.TableMaskFilter;
+import android.os.Bundle;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.view.InputDevice;
+import android.view.MotionEvent;
+import android.view.VelocityTracker;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.LinearInterpolator;
+import android.widget.RemoteViews.RemoteView;
+
+import java.lang.ref.WeakReference;
+
+@RemoteView
+/**
+ * A view that displays its children in a stack and allows users to discretely swipe
+ * through the children.
+ */
+public class StackView extends AdapterViewAnimator {
+    private final String TAG = "StackView";
+
+    /**
+     * Default animation parameters
+     */
+    private static final int DEFAULT_ANIMATION_DURATION = 400;
+    private static final int MINIMUM_ANIMATION_DURATION = 50;
+    private static final int STACK_RELAYOUT_DURATION = 100;
+
+    /**
+     * Parameters effecting the perspective visuals
+     */
+    private static final float PERSPECTIVE_SHIFT_FACTOR_Y = 0.1f;
+    private static final float PERSPECTIVE_SHIFT_FACTOR_X = 0.1f;
+
+    private float mPerspectiveShiftX;
+    private float mPerspectiveShiftY;
+    private float mNewPerspectiveShiftX;
+    private float mNewPerspectiveShiftY;
+
+    @SuppressWarnings({"FieldCanBeLocal"})
+    private static final float PERSPECTIVE_SCALE_FACTOR = 0f;
+
+    /**
+     * Represent the two possible stack modes, one where items slide up, and the other
+     * where items slide down. The perspective is also inverted between these two modes.
+     */
+    private static final int ITEMS_SLIDE_UP = 0;
+    private static final int ITEMS_SLIDE_DOWN = 1;
+
+    /**
+     * These specify the different gesture states
+     */
+    private static final int GESTURE_NONE = 0;
+    private static final int GESTURE_SLIDE_UP = 1;
+    private static final int GESTURE_SLIDE_DOWN = 2;
+
+    /**
+     * Specifies how far you need to swipe (up or down) before it
+     * will be consider a completed gesture when you lift your finger
+     */
+    private static final float SWIPE_THRESHOLD_RATIO = 0.2f;
+
+    /**
+     * Specifies the total distance, relative to the size of the stack,
+     * that views will be slid, either up or down
+     */
+    private static final float SLIDE_UP_RATIO = 0.7f;
+
+    /**
+     * Sentinel value for no current active pointer.
+     * Used by {@link #mActivePointerId}.
+     */
+    private static final int INVALID_POINTER = -1;
+
+    /**
+     * Number of active views in the stack. One fewer view is actually visible, as one is hidden.
+     */
+    private static final int NUM_ACTIVE_VIEWS = 5;
+
+    private static final int FRAME_PADDING = 4;
+
+    private final Rect mTouchRect = new Rect();
+
+    private static final int MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE = 5000;
+
+    private static final long MIN_TIME_BETWEEN_SCROLLS = 100;
+
+    /**
+     * These variables are all related to the current state of touch interaction
+     * with the stack
+     */
+    private float mInitialY;
+    private float mInitialX;
+    private int mActivePointerId;
+    private int mYVelocity = 0;
+    private int mSwipeGestureType = GESTURE_NONE;
+    private int mSlideAmount;
+    private int mSwipeThreshold;
+    private int mTouchSlop;
+    private int mMaximumVelocity;
+    private VelocityTracker mVelocityTracker;
+    private boolean mTransitionIsSetup = false;
+    private int mResOutColor;
+    private int mClickColor;
+
+    private static HolographicHelper sHolographicHelper;
+    private ImageView mHighlight;
+    private ImageView mClickFeedback;
+    private boolean mClickFeedbackIsValid = false;
+    private StackSlider mStackSlider;
+    private boolean mFirstLayoutHappened = false;
+    private long mLastInteractionTime = 0;
+    private long mLastScrollTime;
+    private int mStackMode;
+    private int mFramePadding;
+    private final Rect stackInvalidateRect = new Rect();
+
+    /**
+     * {@inheritDoc}
+     */
+    public StackView(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public StackView(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.stackViewStyle);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public StackView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    public StackView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.StackView, defStyleAttr, defStyleRes);
+
+        mResOutColor = a.getColor(
+                com.android.internal.R.styleable.StackView_resOutColor, 0);
+        mClickColor = a.getColor(
+                com.android.internal.R.styleable.StackView_clickColor, 0);
+
+        a.recycle();
+        initStackView();
+    }
+
+    private void initStackView() {
+        configureViewAnimator(NUM_ACTIVE_VIEWS, 1);
+        setStaticTransformationsEnabled(true);
+        final ViewConfiguration configuration = ViewConfiguration.get(getContext());
+        mTouchSlop = configuration.getScaledTouchSlop();
+        mMaximumVelocity = configuration.getScaledMaximumFlingVelocity();
+        mActivePointerId = INVALID_POINTER;
+
+        mHighlight = new ImageView(getContext());
+        mHighlight.setLayoutParams(new LayoutParams(mHighlight));
+        addViewInLayout(mHighlight, -1, new LayoutParams(mHighlight));
+
+        mClickFeedback = new ImageView(getContext());
+        mClickFeedback.setLayoutParams(new LayoutParams(mClickFeedback));
+        addViewInLayout(mClickFeedback, -1, new LayoutParams(mClickFeedback));
+        mClickFeedback.setVisibility(INVISIBLE);
+
+        mStackSlider = new StackSlider();
+
+        if (sHolographicHelper == null) {
+            sHolographicHelper = new HolographicHelper(mContext);
+        }
+        setClipChildren(false);
+        setClipToPadding(false);
+
+        // This sets the form of the StackView, which is currently to have the perspective-shifted
+        // views above the active view, and have items slide down when sliding out. The opposite is
+        // available by using ITEMS_SLIDE_UP.
+        mStackMode = ITEMS_SLIDE_DOWN;
+
+        // This is a flag to indicate the the stack is loading for the first time
+        mWhichChild = -1;
+
+        // Adjust the frame padding based on the density, since the highlight changes based
+        // on the density
+        final float density = mContext.getResources().getDisplayMetrics().density;
+        mFramePadding = (int) Math.ceil(density * FRAME_PADDING);
+    }
+
+    /**
+     * Animate the views between different relative indexes within the {@link AdapterViewAnimator}
+     */
+    void transformViewForTransition(int fromIndex, int toIndex, final View view, boolean animate) {
+        if (!animate) {
+            ((StackFrame) view).cancelSliderAnimator();
+            view.setRotationX(0f);
+            LayoutParams lp = (LayoutParams) view.getLayoutParams();
+            lp.setVerticalOffset(0);
+            lp.setHorizontalOffset(0);
+        }
+
+        if (fromIndex == -1 && toIndex == getNumActiveViews() -1) {
+            transformViewAtIndex(toIndex, view, false);
+            view.setVisibility(VISIBLE);
+            view.setAlpha(1.0f);
+        } else if (fromIndex == 0 && toIndex == 1) {
+            // Slide item in
+            ((StackFrame) view).cancelSliderAnimator();
+            view.setVisibility(VISIBLE);
+
+            int duration = Math.round(mStackSlider.getDurationForNeutralPosition(mYVelocity));
+            StackSlider animationSlider = new StackSlider(mStackSlider);
+            animationSlider.setView(view);
+
+            if (animate) {
+                PropertyValuesHolder slideInY = PropertyValuesHolder.ofFloat("YProgress", 0.0f);
+                PropertyValuesHolder slideInX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+                ObjectAnimator slideIn = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+                        slideInX, slideInY);
+                slideIn.setDuration(duration);
+                slideIn.setInterpolator(new LinearInterpolator());
+                ((StackFrame) view).setSliderAnimator(slideIn);
+                slideIn.start();
+            } else {
+                animationSlider.setYProgress(0f);
+                animationSlider.setXProgress(0f);
+            }
+        } else if (fromIndex == 1 && toIndex == 0) {
+            // Slide item out
+            ((StackFrame) view).cancelSliderAnimator();
+            int duration = Math.round(mStackSlider.getDurationForOffscreenPosition(mYVelocity));
+
+            StackSlider animationSlider = new StackSlider(mStackSlider);
+            animationSlider.setView(view);
+            if (animate) {
+                PropertyValuesHolder slideOutY = PropertyValuesHolder.ofFloat("YProgress", 1.0f);
+                PropertyValuesHolder slideOutX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+                ObjectAnimator slideOut = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+                        slideOutX, slideOutY);
+                slideOut.setDuration(duration);
+                slideOut.setInterpolator(new LinearInterpolator());
+                ((StackFrame) view).setSliderAnimator(slideOut);
+                slideOut.start();
+            } else {
+                animationSlider.setYProgress(1.0f);
+                animationSlider.setXProgress(0f);
+            }
+        } else if (toIndex == 0) {
+            // Make sure this view that is "waiting in the wings" is invisible
+            view.setAlpha(0.0f);
+            view.setVisibility(INVISIBLE);
+        } else if ((fromIndex == 0 || fromIndex == 1) && toIndex > 1) {
+            view.setVisibility(VISIBLE);
+            view.setAlpha(1.0f);
+            view.setRotationX(0f);
+            LayoutParams lp = (LayoutParams) view.getLayoutParams();
+            lp.setVerticalOffset(0);
+            lp.setHorizontalOffset(0);
+        } else if (fromIndex == -1) {
+            view.setAlpha(1.0f);
+            view.setVisibility(VISIBLE);
+        } else if (toIndex == -1) {
+            if (animate) {
+                postDelayed(new Runnable() {
+                    public void run() {
+                        view.setAlpha(0);
+                    }
+                }, STACK_RELAYOUT_DURATION);
+            } else {
+                view.setAlpha(0f);
+            }
+        }
+
+        // Implement the faked perspective
+        if (toIndex != -1) {
+            transformViewAtIndex(toIndex, view, animate);
+        }
+    }
+
+    private void transformViewAtIndex(int index, final View view, boolean animate) {
+        final float maxPerspectiveShiftY = mPerspectiveShiftY;
+        final float maxPerspectiveShiftX = mPerspectiveShiftX;
+
+        if (mStackMode == ITEMS_SLIDE_DOWN) {
+            index = mMaxNumActiveViews - index - 1;
+            if (index == mMaxNumActiveViews - 1) index--;
+        } else {
+            index--;
+            if (index < 0) index++;
+        }
+
+        float r = (index * 1.0f) / (mMaxNumActiveViews - 2);
+
+        final float scale = 1 - PERSPECTIVE_SCALE_FACTOR * (1 - r);
+
+        float perspectiveTranslationY = r * maxPerspectiveShiftY;
+        float scaleShiftCorrectionY = (scale - 1) *
+                (getMeasuredHeight() * (1 - PERSPECTIVE_SHIFT_FACTOR_Y) / 2.0f);
+        final float transY = perspectiveTranslationY + scaleShiftCorrectionY;
+
+        float perspectiveTranslationX = (1 - r) * maxPerspectiveShiftX;
+        float scaleShiftCorrectionX =  (1 - scale) *
+                (getMeasuredWidth() * (1 - PERSPECTIVE_SHIFT_FACTOR_X) / 2.0f);
+        final float transX = perspectiveTranslationX + scaleShiftCorrectionX;
+
+        // If this view is currently being animated for a certain position, we need to cancel
+        // this animation so as not to interfere with the new transformation.
+        if (view instanceof StackFrame) {
+            ((StackFrame) view).cancelTransformAnimator();
+        }
+
+        if (animate) {
+            PropertyValuesHolder translationX = PropertyValuesHolder.ofFloat("translationX", transX);
+            PropertyValuesHolder translationY = PropertyValuesHolder.ofFloat("translationY", transY);
+            PropertyValuesHolder scalePropX = PropertyValuesHolder.ofFloat("scaleX", scale);
+            PropertyValuesHolder scalePropY = PropertyValuesHolder.ofFloat("scaleY", scale);
+
+            ObjectAnimator oa = ObjectAnimator.ofPropertyValuesHolder(view, scalePropX, scalePropY,
+                    translationY, translationX);
+            oa.setDuration(STACK_RELAYOUT_DURATION);
+            if (view instanceof StackFrame) {
+                ((StackFrame) view).setTransformAnimator(oa);
+            }
+            oa.start();
+        } else {
+            view.setTranslationX(transX);
+            view.setTranslationY(transY);
+            view.setScaleX(scale);
+            view.setScaleY(scale);
+        }
+    }
+
+    private void setupStackSlider(View v, int mode) {
+        mStackSlider.setMode(mode);
+        if (v != null) {
+            mHighlight.setImageBitmap(sHolographicHelper.createResOutline(v, mResOutColor));
+            mHighlight.setRotation(v.getRotation());
+            mHighlight.setTranslationY(v.getTranslationY());
+            mHighlight.setTranslationX(v.getTranslationX());
+            mHighlight.bringToFront();
+            v.bringToFront();
+            mStackSlider.setView(v);
+
+            v.setVisibility(VISIBLE);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @android.view.RemotableViewMethod
+    public void showNext() {
+        if (mSwipeGestureType != GESTURE_NONE) return;
+        if (!mTransitionIsSetup) {
+            View v = getViewAtRelativeIndex(1);
+            if (v != null) {
+                setupStackSlider(v, StackSlider.NORMAL_MODE);
+                mStackSlider.setYProgress(0);
+                mStackSlider.setXProgress(0);
+            }
+        }
+        super.showNext();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    @android.view.RemotableViewMethod
+    public void showPrevious() {
+        if (mSwipeGestureType != GESTURE_NONE) return;
+        if (!mTransitionIsSetup) {
+            View v = getViewAtRelativeIndex(0);
+            if (v != null) {
+                setupStackSlider(v, StackSlider.NORMAL_MODE);
+                mStackSlider.setYProgress(1);
+                mStackSlider.setXProgress(0);
+            }
+        }
+        super.showPrevious();
+    }
+
+    @Override
+    void showOnly(int childIndex, boolean animate) {
+        super.showOnly(childIndex, animate);
+
+        // Here we need to make sure that the z-order of the children is correct
+        for (int i = mCurrentWindowEnd; i >= mCurrentWindowStart; i--) {
+            int index = modulo(i, getWindowSize());
+            ViewAndMetaData vm = mViewsMap.get(index);
+            if (vm != null) {
+                View v = mViewsMap.get(index).view;
+                if (v != null) v.bringToFront();
+            }
+        }
+        if (mHighlight != null) {
+            mHighlight.bringToFront();
+        }
+        mTransitionIsSetup = false;
+        mClickFeedbackIsValid = false;
+    }
+
+    void updateClickFeedback() {
+        if (!mClickFeedbackIsValid) {
+            View v = getViewAtRelativeIndex(1);
+            if (v != null) {
+                mClickFeedback.setImageBitmap(
+                        sHolographicHelper.createClickOutline(v, mClickColor));
+                mClickFeedback.setTranslationX(v.getTranslationX());
+                mClickFeedback.setTranslationY(v.getTranslationY());
+            }
+            mClickFeedbackIsValid = true;
+        }
+    }
+
+    @Override
+    void showTapFeedback(View v) {
+        updateClickFeedback();
+        mClickFeedback.setVisibility(VISIBLE);
+        mClickFeedback.bringToFront();
+        invalidate();
+    }
+
+    @Override
+    void hideTapFeedback(View v) {
+        mClickFeedback.setVisibility(INVISIBLE);
+        invalidate();
+    }
+
+    private void updateChildTransforms() {
+        for (int i = 0; i < getNumActiveViews(); i++) {
+            View v = getViewAtRelativeIndex(i);
+            if (v != null) {
+                transformViewAtIndex(i, v, false);
+            }
+        }
+    }
+
+    private static class StackFrame extends FrameLayout {
+        WeakReference<ObjectAnimator> transformAnimator;
+        WeakReference<ObjectAnimator> sliderAnimator;
+
+        public StackFrame(Context context) {
+            super(context);
+        }
+
+        void setTransformAnimator(ObjectAnimator oa) {
+            transformAnimator = new WeakReference<ObjectAnimator>(oa);
+        }
+
+        void setSliderAnimator(ObjectAnimator oa) {
+            sliderAnimator = new WeakReference<ObjectAnimator>(oa);
+        }
+
+        boolean cancelTransformAnimator() {
+            if (transformAnimator != null) {
+                ObjectAnimator oa = transformAnimator.get();
+                if (oa != null) {
+                    oa.cancel();
+                    return true;
+                }
+            }
+            return false;
+        }
+
+        boolean cancelSliderAnimator() {
+            if (sliderAnimator != null) {
+                ObjectAnimator oa = sliderAnimator.get();
+                if (oa != null) {
+                    oa.cancel();
+                    return true;
+                }
+            }
+            return false;
+        }
+    }
+
+    @Override
+    FrameLayout getFrameForChild() {
+        StackFrame fl = new StackFrame(mContext);
+        fl.setPadding(mFramePadding, mFramePadding, mFramePadding, mFramePadding);
+        return fl;
+    }
+
+    /**
+     * Apply any necessary tranforms for the child that is being added.
+     */
+    void applyTransformForChildAtIndex(View child, int relativeIndex) {
+    }
+
+    @Override
+    protected void dispatchDraw(Canvas canvas) {
+        boolean expandClipRegion = false;
+
+        canvas.getClipBounds(stackInvalidateRect);
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child =  getChildAt(i);
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if ((lp.horizontalOffset == 0 && lp.verticalOffset == 0) ||
+                    child.getAlpha() == 0f || child.getVisibility() != VISIBLE) {
+                lp.resetInvalidateRect();
+            }
+            Rect childInvalidateRect = lp.getInvalidateRect();
+            if (!childInvalidateRect.isEmpty()) {
+                expandClipRegion = true;
+                stackInvalidateRect.union(childInvalidateRect);
+            }
+        }
+
+        // We only expand the clip bounds if necessary.
+        if (expandClipRegion) {
+            canvas.save(Canvas.CLIP_SAVE_FLAG);
+            canvas.clipRect(stackInvalidateRect, Region.Op.UNION);
+            super.dispatchDraw(canvas);
+            canvas.restore();
+        } else {
+            super.dispatchDraw(canvas);
+        }
+    }
+
+    private void onLayout() {
+        if (!mFirstLayoutHappened) {
+            mFirstLayoutHappened = true;
+            updateChildTransforms();
+        }
+
+        final int newSlideAmount = Math.round(SLIDE_UP_RATIO * getMeasuredHeight());
+        if (mSlideAmount != newSlideAmount) {
+            mSlideAmount = newSlideAmount;
+            mSwipeThreshold = Math.round(SWIPE_THRESHOLD_RATIO * newSlideAmount);
+        }
+
+        if (Float.compare(mPerspectiveShiftY, mNewPerspectiveShiftY) != 0 ||
+                Float.compare(mPerspectiveShiftX, mNewPerspectiveShiftX) != 0) {
+
+            mPerspectiveShiftY = mNewPerspectiveShiftY;
+            mPerspectiveShiftX = mNewPerspectiveShiftX;
+            updateChildTransforms();
+        }
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) {
+            switch (event.getAction()) {
+                case MotionEvent.ACTION_SCROLL: {
+                    final float vscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL);
+                    if (vscroll < 0) {
+                        pacedScroll(false);
+                        return true;
+                    } else if (vscroll > 0) {
+                        pacedScroll(true);
+                        return true;
+                    }
+                }
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    // This ensures that the frequency of stack flips caused by scrolls is capped
+    private void pacedScroll(boolean up) {
+        long timeSinceLastScroll = System.currentTimeMillis() - mLastScrollTime;
+        if (timeSinceLastScroll > MIN_TIME_BETWEEN_SCROLLS) {
+            if (up) {
+                showPrevious();
+            } else {
+                showNext();
+            }
+            mLastScrollTime = System.currentTimeMillis();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onInterceptTouchEvent(MotionEvent ev) {
+        int action = ev.getAction();
+        switch(action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_DOWN: {
+                if (mActivePointerId == INVALID_POINTER) {
+                    mInitialX = ev.getX();
+                    mInitialY = ev.getY();
+                    mActivePointerId = ev.getPointerId(0);
+                }
+                break;
+            }
+            case MotionEvent.ACTION_MOVE: {
+                int pointerIndex = ev.findPointerIndex(mActivePointerId);
+                if (pointerIndex == INVALID_POINTER) {
+                    // no data for our primary pointer, this shouldn't happen, log it
+                    Log.d(TAG, "Error: No data for our primary pointer.");
+                    return false;
+                }
+                float newY = ev.getY(pointerIndex);
+                float deltaY = newY - mInitialY;
+
+                beginGestureIfNeeded(deltaY);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+                break;
+            }
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                mActivePointerId = INVALID_POINTER;
+                mSwipeGestureType = GESTURE_NONE;
+            }
+        }
+
+        return mSwipeGestureType != GESTURE_NONE;
+    }
+
+    private void beginGestureIfNeeded(float deltaY) {
+        if ((int) Math.abs(deltaY) > mTouchSlop && mSwipeGestureType == GESTURE_NONE) {
+            final int swipeGestureType = deltaY < 0 ? GESTURE_SLIDE_UP : GESTURE_SLIDE_DOWN;
+            cancelLongPress();
+            requestDisallowInterceptTouchEvent(true);
+
+            if (mAdapter == null) return;
+            final int adapterCount = getCount();
+
+            int activeIndex;
+            if (mStackMode == ITEMS_SLIDE_UP) {
+                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
+            } else {
+                activeIndex = (swipeGestureType == GESTURE_SLIDE_DOWN) ? 1 : 0;
+            }
+
+            boolean endOfStack = mLoopViews && adapterCount == 1
+                    && ((mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_UP)
+                    || (mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_DOWN));
+            boolean beginningOfStack = mLoopViews && adapterCount == 1
+                    && ((mStackMode == ITEMS_SLIDE_DOWN && swipeGestureType == GESTURE_SLIDE_UP)
+                    || (mStackMode == ITEMS_SLIDE_UP && swipeGestureType == GESTURE_SLIDE_DOWN));
+
+            int stackMode;
+            if (mLoopViews && !beginningOfStack && !endOfStack) {
+                stackMode = StackSlider.NORMAL_MODE;
+            } else if (mCurrentWindowStartUnbounded + activeIndex == -1 || beginningOfStack) {
+                activeIndex++;
+                stackMode = StackSlider.BEGINNING_OF_STACK_MODE;
+            } else if (mCurrentWindowStartUnbounded + activeIndex == adapterCount - 1 || endOfStack) {
+                stackMode = StackSlider.END_OF_STACK_MODE;
+            } else {
+                stackMode = StackSlider.NORMAL_MODE;
+            }
+
+            mTransitionIsSetup = stackMode == StackSlider.NORMAL_MODE;
+
+            View v = getViewAtRelativeIndex(activeIndex);
+            if (v == null) return;
+
+            setupStackSlider(v, stackMode);
+
+            // We only register this gesture if we've made it this far without a problem
+            mSwipeGestureType = swipeGestureType;
+            cancelHandleClick();
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        super.onTouchEvent(ev);
+
+        int action = ev.getAction();
+        int pointerIndex = ev.findPointerIndex(mActivePointerId);
+        if (pointerIndex == INVALID_POINTER) {
+            // no data for our primary pointer, this shouldn't happen, log it
+            Log.d(TAG, "Error: No data for our primary pointer.");
+            return false;
+        }
+
+        float newY = ev.getY(pointerIndex);
+        float newX = ev.getX(pointerIndex);
+        float deltaY = newY - mInitialY;
+        float deltaX = newX - mInitialX;
+        if (mVelocityTracker == null) {
+            mVelocityTracker = VelocityTracker.obtain();
+        }
+        mVelocityTracker.addMovement(ev);
+
+        switch (action & MotionEvent.ACTION_MASK) {
+            case MotionEvent.ACTION_MOVE: {
+                beginGestureIfNeeded(deltaY);
+
+                float rx = deltaX / (mSlideAmount * 1.0f);
+                if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
+                    float r = (deltaY - mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
+                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
+                    mStackSlider.setYProgress(1 - r);
+                    mStackSlider.setXProgress(rx);
+                    return true;
+                } else if (mSwipeGestureType == GESTURE_SLIDE_UP) {
+                    float r = -(deltaY + mTouchSlop * 1.0f) / mSlideAmount * 1.0f;
+                    if (mStackMode == ITEMS_SLIDE_DOWN) r = 1 - r;
+                    mStackSlider.setYProgress(r);
+                    mStackSlider.setXProgress(rx);
+                    return true;
+                }
+                break;
+            }
+            case MotionEvent.ACTION_UP: {
+                handlePointerUp(ev);
+                break;
+            }
+            case MotionEvent.ACTION_POINTER_UP: {
+                onSecondaryPointerUp(ev);
+                break;
+            }
+            case MotionEvent.ACTION_CANCEL: {
+                mActivePointerId = INVALID_POINTER;
+                mSwipeGestureType = GESTURE_NONE;
+                break;
+            }
+        }
+        return true;
+    }
+
+    private void onSecondaryPointerUp(MotionEvent ev) {
+        final int activePointerIndex = ev.getActionIndex();
+        final int pointerId = ev.getPointerId(activePointerIndex);
+        if (pointerId == mActivePointerId) {
+
+            int activeViewIndex = (mSwipeGestureType == GESTURE_SLIDE_DOWN) ? 0 : 1;
+
+            View v = getViewAtRelativeIndex(activeViewIndex);
+            if (v == null) return;
+
+            // Our primary pointer has gone up -- let's see if we can find
+            // another pointer on the view. If so, then we should replace
+            // our primary pointer with this new pointer and adjust things
+            // so that the view doesn't jump
+            for (int index = 0; index < ev.getPointerCount(); index++) {
+                if (index != activePointerIndex) {
+
+                    float x = ev.getX(index);
+                    float y = ev.getY(index);
+
+                    mTouchRect.set(v.getLeft(), v.getTop(), v.getRight(), v.getBottom());
+                    if (mTouchRect.contains(Math.round(x), Math.round(y))) {
+                        float oldX = ev.getX(activePointerIndex);
+                        float oldY = ev.getY(activePointerIndex);
+
+                        // adjust our frame of reference to avoid a jump
+                        mInitialY += (y - oldY);
+                        mInitialX += (x - oldX);
+
+                        mActivePointerId = ev.getPointerId(index);
+                        if (mVelocityTracker != null) {
+                            mVelocityTracker.clear();
+                        }
+                        // ok, we're good, we found a new pointer which is touching the active view
+                        return;
+                    }
+                }
+            }
+            // if we made it this far, it means we didn't find a satisfactory new pointer :(,
+            // so end the gesture
+            handlePointerUp(ev);
+        }
+    }
+
+    private void handlePointerUp(MotionEvent ev) {
+        int pointerIndex = ev.findPointerIndex(mActivePointerId);
+        float newY = ev.getY(pointerIndex);
+        int deltaY = (int) (newY - mInitialY);
+        mLastInteractionTime = System.currentTimeMillis();
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.computeCurrentVelocity(1000, mMaximumVelocity);
+            mYVelocity = (int) mVelocityTracker.getYVelocity(mActivePointerId);
+        }
+
+        if (mVelocityTracker != null) {
+            mVelocityTracker.recycle();
+            mVelocityTracker = null;
+        }
+
+        if (deltaY > mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_DOWN
+                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
+            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
+            // showNext();
+            mSwipeGestureType = GESTURE_NONE;
+
+            // Swipe threshold exceeded, swipe down
+            if (mStackMode == ITEMS_SLIDE_UP) {
+                showPrevious();
+            } else {
+                showNext();
+            }
+            mHighlight.bringToFront();
+        } else if (deltaY < -mSwipeThreshold && mSwipeGestureType == GESTURE_SLIDE_UP
+                && mStackSlider.mMode == StackSlider.NORMAL_MODE) {
+            // We reset the gesture variable, because otherwise we will ignore showPrevious() /
+            // showNext();
+            mSwipeGestureType = GESTURE_NONE;
+
+            // Swipe threshold exceeded, swipe up
+            if (mStackMode == ITEMS_SLIDE_UP) {
+                showNext();
+            } else {
+                showPrevious();
+            }
+
+            mHighlight.bringToFront();
+        } else if (mSwipeGestureType == GESTURE_SLIDE_UP ) {
+            // Didn't swipe up far enough, snap back down
+            int duration;
+            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 1 : 0;
+            if (mStackMode == ITEMS_SLIDE_UP || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
+                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
+            } else {
+                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
+            }
+
+            StackSlider animationSlider = new StackSlider(mStackSlider);
+            PropertyValuesHolder snapBackY = PropertyValuesHolder.ofFloat("YProgress", finalYProgress);
+            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+                    snapBackX, snapBackY);
+            pa.setDuration(duration);
+            pa.setInterpolator(new LinearInterpolator());
+            pa.start();
+        } else if (mSwipeGestureType == GESTURE_SLIDE_DOWN) {
+            // Didn't swipe down far enough, snap back up
+            float finalYProgress = (mStackMode == ITEMS_SLIDE_DOWN) ? 0 : 1;
+            int duration;
+            if (mStackMode == ITEMS_SLIDE_DOWN || mStackSlider.mMode != StackSlider.NORMAL_MODE) {
+                duration = Math.round(mStackSlider.getDurationForNeutralPosition());
+            } else {
+                duration = Math.round(mStackSlider.getDurationForOffscreenPosition());
+            }
+
+            StackSlider animationSlider = new StackSlider(mStackSlider);
+            PropertyValuesHolder snapBackY =
+                    PropertyValuesHolder.ofFloat("YProgress",finalYProgress);
+            PropertyValuesHolder snapBackX = PropertyValuesHolder.ofFloat("XProgress", 0.0f);
+            ObjectAnimator pa = ObjectAnimator.ofPropertyValuesHolder(animationSlider,
+                    snapBackX, snapBackY);
+            pa.setDuration(duration);
+            pa.start();
+        }
+
+        mActivePointerId = INVALID_POINTER;
+        mSwipeGestureType = GESTURE_NONE;
+    }
+
+    private class StackSlider {
+        View mView;
+        float mYProgress;
+        float mXProgress;
+
+        static final int NORMAL_MODE = 0;
+        static final int BEGINNING_OF_STACK_MODE = 1;
+        static final int END_OF_STACK_MODE = 2;
+
+        int mMode = NORMAL_MODE;
+
+        public StackSlider() {
+        }
+
+        public StackSlider(StackSlider copy) {
+            mView = copy.mView;
+            mYProgress = copy.mYProgress;
+            mXProgress = copy.mXProgress;
+            mMode = copy.mMode;
+        }
+
+        private float cubic(float r) {
+            return (float) (Math.pow(2 * r - 1, 3) + 1) / 2.0f;
+        }
+
+        private float highlightAlphaInterpolator(float r) {
+            float pivot = 0.4f;
+            if (r < pivot) {
+                return 0.85f * cubic(r / pivot);
+            } else {
+                return 0.85f * cubic(1 - (r - pivot) / (1 - pivot));
+            }
+        }
+
+        private float viewAlphaInterpolator(float r) {
+            float pivot = 0.3f;
+            if (r > pivot) {
+                return (r - pivot) / (1 - pivot);
+            } else {
+                return 0;
+            }
+        }
+
+        private float rotationInterpolator(float r) {
+            float pivot = 0.2f;
+            if (r < pivot) {
+                return 0;
+            } else {
+                return (r - pivot) / (1 - pivot);
+            }
+        }
+
+        void setView(View v) {
+            mView = v;
+        }
+
+        public void setYProgress(float r) {
+            // enforce r between 0 and 1
+            r = Math.min(1.0f, r);
+            r = Math.max(0, r);
+
+            mYProgress = r;
+            if (mView == null) return;
+
+            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
+
+            int stackDirection = (mStackMode == ITEMS_SLIDE_UP) ? 1 : -1;
+
+            // We need to prevent any clipping issues which may arise by setting a layer type.
+            // This doesn't come for free however, so we only want to enable it when required.
+            if (Float.compare(0f, mYProgress) != 0 && Float.compare(1.0f, mYProgress) != 0) {
+                if (mView.getLayerType() == LAYER_TYPE_NONE) {
+                    mView.setLayerType(LAYER_TYPE_HARDWARE, null);
+                }
+            } else {
+                if (mView.getLayerType() != LAYER_TYPE_NONE) {
+                    mView.setLayerType(LAYER_TYPE_NONE, null);
+                }
+            }
+
+            switch (mMode) {
+                case NORMAL_MODE:
+                    viewLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
+                    highlightLp.setVerticalOffset(Math.round(-r * stackDirection * mSlideAmount));
+                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
+
+                    float alpha = viewAlphaInterpolator(1 - r);
+
+                    // We make sure that views which can't be seen (have 0 alpha) are also invisible
+                    // so that they don't interfere with click events.
+                    if (mView.getAlpha() == 0 && alpha != 0 && mView.getVisibility() != VISIBLE) {
+                        mView.setVisibility(VISIBLE);
+                    } else if (alpha == 0 && mView.getAlpha() != 0
+                            && mView.getVisibility() == VISIBLE) {
+                        mView.setVisibility(INVISIBLE);
+                    }
+
+                    mView.setAlpha(alpha);
+                    mView.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
+                    mHighlight.setRotationX(stackDirection * 90.0f * rotationInterpolator(r));
+                    break;
+                case END_OF_STACK_MODE:
+                    r = r * 0.2f;
+                    viewLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
+                    highlightLp.setVerticalOffset(Math.round(-stackDirection * r * mSlideAmount));
+                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
+                    break;
+                case BEGINNING_OF_STACK_MODE:
+                    r = (1-r) * 0.2f;
+                    viewLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
+                    highlightLp.setVerticalOffset(Math.round(stackDirection * r * mSlideAmount));
+                    mHighlight.setAlpha(highlightAlphaInterpolator(r));
+                    break;
+            }
+        }
+
+        public void setXProgress(float r) {
+            // enforce r between 0 and 1
+            r = Math.min(2.0f, r);
+            r = Math.max(-2.0f, r);
+
+            mXProgress = r;
+
+            if (mView == null) return;
+            final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+            final LayoutParams highlightLp = (LayoutParams) mHighlight.getLayoutParams();
+
+            r *= 0.2f;
+            viewLp.setHorizontalOffset(Math.round(r * mSlideAmount));
+            highlightLp.setHorizontalOffset(Math.round(r * mSlideAmount));
+        }
+
+        void setMode(int mode) {
+            mMode = mode;
+        }
+
+        float getDurationForNeutralPosition() {
+            return getDuration(false, 0);
+        }
+
+        float getDurationForOffscreenPosition() {
+            return getDuration(true, 0);
+        }
+
+        float getDurationForNeutralPosition(float velocity) {
+            return getDuration(false, velocity);
+        }
+
+        float getDurationForOffscreenPosition(float velocity) {
+            return getDuration(true, velocity);
+        }
+
+        private float getDuration(boolean invert, float velocity) {
+            if (mView != null) {
+                final LayoutParams viewLp = (LayoutParams) mView.getLayoutParams();
+
+                float d = (float) Math.hypot(viewLp.horizontalOffset, viewLp.verticalOffset);
+                float maxd = (float) Math.hypot(mSlideAmount, 0.4f * mSlideAmount);
+                if (d > maxd) {
+                    // Because mSlideAmount is updated in onLayout(), it is possible that d > maxd
+                    // if we get onLayout() right before this method is called.
+                    d = maxd;
+                }
+
+                if (velocity == 0) {
+                    return (invert ? (1 - d / maxd) : d / maxd) * DEFAULT_ANIMATION_DURATION;
+                } else {
+                    float duration = invert ? d / Math.abs(velocity) :
+                            (maxd - d) / Math.abs(velocity);
+                    if (duration < MINIMUM_ANIMATION_DURATION ||
+                            duration > DEFAULT_ANIMATION_DURATION) {
+                        return getDuration(invert, 0);
+                    } else {
+                        return duration;
+                    }
+                }
+            }
+            return 0;
+        }
+
+        // Used for animations
+        @SuppressWarnings({"UnusedDeclaration"})
+        public float getYProgress() {
+            return mYProgress;
+        }
+
+        // Used for animations
+        @SuppressWarnings({"UnusedDeclaration"})
+        public float getXProgress() {
+            return mXProgress;
+        }
+    }
+
+    LayoutParams createOrReuseLayoutParams(View v) {
+        final ViewGroup.LayoutParams currentLp = v.getLayoutParams();
+        if (currentLp instanceof LayoutParams) {
+            LayoutParams lp = (LayoutParams) currentLp;
+            lp.setHorizontalOffset(0);
+            lp.setVerticalOffset(0);
+            lp.width = 0;
+            lp.width = 0;
+            return lp;
+        }
+        return new LayoutParams(v);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        checkForAndHandleDataChanged();
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+
+            int childRight = mPaddingLeft + child.getMeasuredWidth();
+            int childBottom = mPaddingTop + child.getMeasuredHeight();
+            LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            child.layout(mPaddingLeft + lp.horizontalOffset, mPaddingTop + lp.verticalOffset,
+                    childRight + lp.horizontalOffset, childBottom + lp.verticalOffset);
+
+        }
+        onLayout();
+    }
+
+    @Override
+    public void advance() {
+        long timeSinceLastInteraction = System.currentTimeMillis() - mLastInteractionTime;
+
+        if (mAdapter == null) return;
+        final int adapterCount = getCount();
+        if (adapterCount == 1 && mLoopViews) return;
+
+        if (mSwipeGestureType == GESTURE_NONE &&
+                timeSinceLastInteraction > MIN_TIME_BETWEEN_INTERACTION_AND_AUTOADVANCE) {
+            showNext();
+        }
+    }
+
+    private void measureChildren() {
+        final int count = getChildCount();
+
+        final int measuredWidth = getMeasuredWidth();
+        final int measuredHeight = getMeasuredHeight();
+
+        final int childWidth = Math.round(measuredWidth*(1-PERSPECTIVE_SHIFT_FACTOR_X))
+                - mPaddingLeft - mPaddingRight;
+        final int childHeight = Math.round(measuredHeight*(1-PERSPECTIVE_SHIFT_FACTOR_Y))
+                - mPaddingTop - mPaddingBottom;
+
+        int maxWidth = 0;
+        int maxHeight = 0;
+
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            child.measure(MeasureSpec.makeMeasureSpec(childWidth, MeasureSpec.AT_MOST),
+                    MeasureSpec.makeMeasureSpec(childHeight, MeasureSpec.AT_MOST));
+
+            if (child != mHighlight && child != mClickFeedback) {
+                final int childMeasuredWidth = child.getMeasuredWidth();
+                final int childMeasuredHeight = child.getMeasuredHeight();
+                if (childMeasuredWidth > maxWidth) {
+                    maxWidth = childMeasuredWidth;
+                }
+                if (childMeasuredHeight > maxHeight) {
+                    maxHeight = childMeasuredHeight;
+                }
+            }
+        }
+
+        mNewPerspectiveShiftX = PERSPECTIVE_SHIFT_FACTOR_X * measuredWidth;
+        mNewPerspectiveShiftY = PERSPECTIVE_SHIFT_FACTOR_Y * measuredHeight;
+
+        // If we have extra space, we try and spread the items out
+        if (maxWidth > 0 && count > 0 && maxWidth < childWidth) {
+            mNewPerspectiveShiftX = measuredWidth - maxWidth;
+        }
+
+        if (maxHeight > 0 && count > 0 && maxHeight < childHeight) {
+            mNewPerspectiveShiftY = measuredHeight - maxHeight;
+        }
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+        final int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+        final int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+
+        boolean haveChildRefSize = (mReferenceChildWidth != -1 && mReferenceChildHeight != -1);
+
+        // We need to deal with the case where our parent hasn't told us how
+        // big we should be. In this case we should
+        float factorY = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_Y);
+        if (heightSpecMode == MeasureSpec.UNSPECIFIED) {
+            heightSpecSize = haveChildRefSize ?
+                    Math.round(mReferenceChildHeight * (1 + factorY)) +
+                    mPaddingTop + mPaddingBottom : 0;
+        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+            if (haveChildRefSize) {
+                int height = Math.round(mReferenceChildHeight * (1 + factorY))
+                        + mPaddingTop + mPaddingBottom;
+                if (height <= heightSpecSize) {
+                    heightSpecSize = height;
+                } else {
+                    heightSpecSize |= MEASURED_STATE_TOO_SMALL;
+
+                }
+            } else {
+                heightSpecSize = 0;
+            }
+        }
+
+        float factorX = 1/(1 - PERSPECTIVE_SHIFT_FACTOR_X);
+        if (widthSpecMode == MeasureSpec.UNSPECIFIED) {
+            widthSpecSize = haveChildRefSize ?
+                    Math.round(mReferenceChildWidth * (1 + factorX)) +
+                    mPaddingLeft + mPaddingRight : 0;
+        } else if (heightSpecMode == MeasureSpec.AT_MOST) {
+            if (haveChildRefSize) {
+                int width = mReferenceChildWidth + mPaddingLeft + mPaddingRight;
+                if (width <= widthSpecSize) {
+                    widthSpecSize = width;
+                } else {
+                    widthSpecSize |= MEASURED_STATE_TOO_SMALL;
+                }
+            } else {
+                widthSpecSize = 0;
+            }
+        }
+        setMeasuredDimension(widthSpecSize, heightSpecSize);
+        measureChildren();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return StackView.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        info.setScrollable(getChildCount() > 1);
+        if (isEnabled()) {
+            if (getDisplayedChild() < getChildCount() - 1) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_FORWARD);
+            }
+            if (getDisplayedChild() > 0) {
+                info.addAction(AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD);
+            }
+        }
+    }
+
+    /** @hide */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (super.performAccessibilityActionInternal(action, arguments)) {
+            return true;
+        }
+        if (!isEnabled()) {
+            return false;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_SCROLL_FORWARD: {
+                if (getDisplayedChild() < getChildCount() - 1) {
+                    showNext();
+                    return true;
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SCROLL_BACKWARD: {
+                if (getDisplayedChild() > 0) {
+                    showPrevious();
+                    return true;
+                }
+            } return false;
+        }
+        return false;
+    }
+
+    class LayoutParams extends ViewGroup.LayoutParams {
+        int horizontalOffset;
+        int verticalOffset;
+        View mView;
+        private final Rect parentRect = new Rect();
+        private final Rect invalidateRect = new Rect();
+        private final RectF invalidateRectf = new RectF();
+        private final Rect globalInvalidateRect = new Rect();
+
+        LayoutParams(View view) {
+            super(0, 0);
+            width = 0;
+            height = 0;
+            horizontalOffset = 0;
+            verticalOffset = 0;
+            mView = view;
+        }
+
+        LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+            horizontalOffset = 0;
+            verticalOffset = 0;
+            width = 0;
+            height = 0;
+        }
+
+        void invalidateGlobalRegion(View v, Rect r) {
+            // We need to make a new rect here, so as not to modify the one passed
+            globalInvalidateRect.set(r);
+            globalInvalidateRect.union(0, 0, getWidth(), getHeight());
+            View p = v;
+            if (!(v.getParent() != null && v.getParent() instanceof View)) return;
+
+            boolean firstPass = true;
+            parentRect.set(0, 0, 0, 0);
+            while (p.getParent() != null && p.getParent() instanceof View
+                    && !parentRect.contains(globalInvalidateRect)) {
+                if (!firstPass) {
+                    globalInvalidateRect.offset(p.getLeft() - p.getScrollX(), p.getTop()
+                            - p.getScrollY());
+                }
+                firstPass = false;
+                p = (View) p.getParent();
+                parentRect.set(p.getScrollX(), p.getScrollY(),
+                        p.getWidth() + p.getScrollX(), p.getHeight() + p.getScrollY());
+                p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
+                        globalInvalidateRect.right, globalInvalidateRect.bottom);
+            }
+
+            p.invalidate(globalInvalidateRect.left, globalInvalidateRect.top,
+                    globalInvalidateRect.right, globalInvalidateRect.bottom);
+        }
+
+        Rect getInvalidateRect() {
+            return invalidateRect;
+        }
+
+        void resetInvalidateRect() {
+            invalidateRect.set(0, 0, 0, 0);
+        }
+
+        // This is public so that ObjectAnimator can access it
+        public void setVerticalOffset(int newVerticalOffset) {
+            setOffsets(horizontalOffset, newVerticalOffset);
+        }
+
+        public void setHorizontalOffset(int newHorizontalOffset) {
+            setOffsets(newHorizontalOffset, verticalOffset);
+        }
+
+        public void setOffsets(int newHorizontalOffset, int newVerticalOffset) {
+            int horizontalOffsetDelta = newHorizontalOffset - horizontalOffset;
+            horizontalOffset = newHorizontalOffset;
+            int verticalOffsetDelta = newVerticalOffset - verticalOffset;
+            verticalOffset = newVerticalOffset;
+
+            if (mView != null) {
+                mView.requestLayout();
+                int left = Math.min(mView.getLeft() + horizontalOffsetDelta, mView.getLeft());
+                int right = Math.max(mView.getRight() + horizontalOffsetDelta, mView.getRight());
+                int top = Math.min(mView.getTop() + verticalOffsetDelta, mView.getTop());
+                int bottom = Math.max(mView.getBottom() + verticalOffsetDelta, mView.getBottom());
+
+                invalidateRectf.set(left, top, right, bottom);
+
+                float xoffset = -invalidateRectf.left;
+                float yoffset = -invalidateRectf.top;
+                invalidateRectf.offset(xoffset, yoffset);
+                mView.getMatrix().mapRect(invalidateRectf);
+                invalidateRectf.offset(-xoffset, -yoffset);
+
+                invalidateRect.set((int) Math.floor(invalidateRectf.left),
+                        (int) Math.floor(invalidateRectf.top),
+                        (int) Math.ceil(invalidateRectf.right),
+                        (int) Math.ceil(invalidateRectf.bottom));
+
+                invalidateGlobalRegion(mView, invalidateRect);
+            }
+        }
+    }
+
+    private static class HolographicHelper {
+        private final Paint mHolographicPaint = new Paint();
+        private final Paint mErasePaint = new Paint();
+        private final Paint mBlurPaint = new Paint();
+        private static final int RES_OUT = 0;
+        private static final int CLICK_FEEDBACK = 1;
+        private float mDensity;
+        private BlurMaskFilter mSmallBlurMaskFilter;
+        private BlurMaskFilter mLargeBlurMaskFilter;
+        private final Canvas mCanvas = new Canvas();
+        private final Canvas mMaskCanvas = new Canvas();
+        private final int[] mTmpXY = new int[2];
+        private final Matrix mIdentityMatrix = new Matrix();
+
+        HolographicHelper(Context context) {
+            mDensity = context.getResources().getDisplayMetrics().density;
+
+            mHolographicPaint.setFilterBitmap(true);
+            mHolographicPaint.setMaskFilter(TableMaskFilter.CreateClipTable(0, 30));
+            mErasePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.DST_OUT));
+            mErasePaint.setFilterBitmap(true);
+
+            mSmallBlurMaskFilter = new BlurMaskFilter(2 * mDensity, BlurMaskFilter.Blur.NORMAL);
+            mLargeBlurMaskFilter = new BlurMaskFilter(4 * mDensity, BlurMaskFilter.Blur.NORMAL);
+        }
+
+        Bitmap createClickOutline(View v, int color) {
+            return createOutline(v, CLICK_FEEDBACK, color);
+        }
+
+        Bitmap createResOutline(View v, int color) {
+            return createOutline(v, RES_OUT, color);
+        }
+
+        Bitmap createOutline(View v, int type, int color) {
+            mHolographicPaint.setColor(color);
+            if (type == RES_OUT) {
+                mBlurPaint.setMaskFilter(mSmallBlurMaskFilter);
+            } else if (type == CLICK_FEEDBACK) {
+                mBlurPaint.setMaskFilter(mLargeBlurMaskFilter);
+            }
+
+            if (v.getMeasuredWidth() == 0 || v.getMeasuredHeight() == 0) {
+                return null;
+            }
+
+            Bitmap bitmap = Bitmap.createBitmap(v.getResources().getDisplayMetrics(),
+                    v.getMeasuredWidth(), v.getMeasuredHeight(), Bitmap.Config.ARGB_8888);
+            mCanvas.setBitmap(bitmap);
+
+            float rotationX = v.getRotationX();
+            float rotation = v.getRotation();
+            float translationY = v.getTranslationY();
+            float translationX = v.getTranslationX();
+            v.setRotationX(0);
+            v.setRotation(0);
+            v.setTranslationY(0);
+            v.setTranslationX(0);
+            v.draw(mCanvas);
+            v.setRotationX(rotationX);
+            v.setRotation(rotation);
+            v.setTranslationY(translationY);
+            v.setTranslationX(translationX);
+
+            drawOutline(mCanvas, bitmap);
+            mCanvas.setBitmap(null);
+            return bitmap;
+        }
+
+        void drawOutline(Canvas dest, Bitmap src) {
+            final int[] xy = mTmpXY;
+            Bitmap mask = src.extractAlpha(mBlurPaint, xy);
+            mMaskCanvas.setBitmap(mask);
+            mMaskCanvas.drawBitmap(src, -xy[0], -xy[1], mErasePaint);
+            dest.drawColor(0, PorterDuff.Mode.CLEAR);
+            dest.setMatrix(mIdentityMatrix);
+            dest.drawBitmap(mask, xy[0], xy[1], mHolographicPaint);
+            mMaskCanvas.setBitmap(null);
+            mask.recycle();
+        }
+    }
+}
diff --git a/android/widget/SuggestionsAdapter.java b/android/widget/SuggestionsAdapter.java
new file mode 100644
index 0000000..fbb8993
--- /dev/null
+++ b/android/widget/SuggestionsAdapter.java
@@ -0,0 +1,735 @@
+/*
+ * Copyright (C) 2009 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 android.widget;
+
+import android.app.SearchDialog;
+import android.app.SearchManager;
+import android.app.SearchableInfo;
+import android.content.ComponentName;
+import android.content.ContentResolver;
+import android.content.ContentResolver.OpenResourceIdResult;
+import android.content.Context;
+import android.content.pm.ActivityInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.PackageManager.NameNotFoundException;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.database.Cursor;
+import android.graphics.drawable.Drawable;
+import android.net.Uri;
+import android.os.Bundle;
+import android.text.Spannable;
+import android.text.SpannableString;
+import android.text.TextUtils;
+import android.text.style.TextAppearanceSpan;
+import android.util.Log;
+import android.util.TypedValue;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.WeakHashMap;
+
+/**
+ * Provides the contents for the suggestion drop-down list.in {@link SearchDialog}.
+ *
+ * @hide
+ */
+class SuggestionsAdapter extends ResourceCursorAdapter implements OnClickListener {
+
+    private static final boolean DBG = false;
+    private static final String LOG_TAG = "SuggestionsAdapter";
+    private static final int QUERY_LIMIT = 50;
+
+    static final int REFINE_NONE = 0;
+    static final int REFINE_BY_ENTRY = 1;
+    static final int REFINE_ALL = 2;
+
+    private final SearchManager mSearchManager;
+    private final SearchView mSearchView;
+    private final SearchableInfo mSearchable;
+    private final Context mProviderContext;
+    private final WeakHashMap<String, Drawable.ConstantState> mOutsideDrawablesCache;
+    private final int mCommitIconResId;
+
+    private boolean mClosed = false;
+    private int mQueryRefinement = REFINE_BY_ENTRY;
+
+    // URL color
+    private ColorStateList mUrlColor;
+
+    static final int INVALID_INDEX = -1;
+
+    // Cached column indexes, updated when the cursor changes.
+    private int mText1Col = INVALID_INDEX;
+    private int mText2Col = INVALID_INDEX;
+    private int mText2UrlCol = INVALID_INDEX;
+    private int mIconName1Col = INVALID_INDEX;
+    private int mIconName2Col = INVALID_INDEX;
+    private int mFlagsCol = INVALID_INDEX;
+
+    // private final Runnable mStartSpinnerRunnable;
+    // private final Runnable mStopSpinnerRunnable;
+
+    /**
+     * The amount of time we delay in the filter when the user presses the delete key.
+     * @see Filter#setDelayer(android.widget.Filter.Delayer).
+     */
+    private static final long DELETE_KEY_POST_DELAY = 500L;
+
+    public SuggestionsAdapter(Context context, SearchView searchView, SearchableInfo searchable,
+            WeakHashMap<String, Drawable.ConstantState> outsideDrawablesCache) {
+        super(context, searchView.getSuggestionRowLayout(), null /* no initial cursor */,
+                true /* auto-requery */);
+
+        mSearchManager = (SearchManager) mContext.getSystemService(Context.SEARCH_SERVICE);
+        mSearchView = searchView;
+        mSearchable = searchable;
+        mCommitIconResId = searchView.getSuggestionCommitIconResId();
+
+        // set up provider resources (gives us icons, etc.)
+        final Context activityContext = mSearchable.getActivityContext(mContext);
+        mProviderContext = mSearchable.getProviderContext(mContext, activityContext);
+
+        mOutsideDrawablesCache = outsideDrawablesCache;
+
+        // mStartSpinnerRunnable = new Runnable() {
+        // public void run() {
+        // // mSearchView.setWorking(true); // TODO:
+        // }
+        // };
+        //
+        // mStopSpinnerRunnable = new Runnable() {
+        // public void run() {
+        // // mSearchView.setWorking(false); // TODO:
+        // }
+        // };
+
+        // delay 500ms when deleting
+        getFilter().setDelayer(new Filter.Delayer() {
+
+            private int mPreviousLength = 0;
+
+            public long getPostingDelay(CharSequence constraint) {
+                if (constraint == null) return 0;
+
+                long delay = constraint.length() < mPreviousLength ? DELETE_KEY_POST_DELAY : 0;
+                mPreviousLength = constraint.length();
+                return delay;
+            }
+        });
+    }
+
+    /**
+     * Enables query refinement for all suggestions. This means that an additional icon
+     * will be shown for each entry. When clicked, the suggested text on that line will be
+     * copied to the query text field.
+     * <p>
+     *
+     * @param refineWhat which queries to refine. Possible values are
+     *                   {@link #REFINE_NONE}, {@link #REFINE_BY_ENTRY}, and
+     *                   {@link #REFINE_ALL}.
+     */
+    public void setQueryRefinement(int refineWhat) {
+        mQueryRefinement = refineWhat;
+    }
+
+    /**
+     * Returns the current query refinement preference.
+     * @return value of query refinement preference
+     */
+    public int getQueryRefinement() {
+        return mQueryRefinement;
+    }
+
+    /**
+     * Overridden to always return <code>false</code>, since we cannot be sure that
+     * suggestion sources return stable IDs.
+     */
+    @Override
+    public boolean hasStableIds() {
+        return false;
+    }
+
+    /**
+     * Use the search suggestions provider to obtain a live cursor.  This will be called
+     * in a worker thread, so it's OK if the query is slow (e.g. round trip for suggestions).
+     * The results will be processed in the UI thread and changeCursor() will be called.
+     */
+    @Override
+    public Cursor runQueryOnBackgroundThread(CharSequence constraint) {
+        if (DBG) Log.d(LOG_TAG, "runQueryOnBackgroundThread(" + constraint + ")");
+        String query = (constraint == null) ? "" : constraint.toString();
+        /**
+         * for in app search we show the progress spinner until the cursor is returned with
+         * the results.
+         */
+        Cursor cursor = null;
+        if (mSearchView.getVisibility() != View.VISIBLE
+                || mSearchView.getWindowVisibility() != View.VISIBLE) {
+            return null;
+        }
+        //mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+        try {
+            cursor = mSearchManager.getSuggestions(mSearchable, query, QUERY_LIMIT);
+            // trigger fill window so the spinner stays up until the results are copied over and
+            // closer to being ready
+            if (cursor != null) {
+                cursor.getCount();
+                return cursor;
+            }
+        } catch (RuntimeException e) {
+            Log.w(LOG_TAG, "Search suggestions query threw an exception.", e);
+        }
+        // If cursor is null or an exception was thrown, stop the spinner and return null.
+        // changeCursor doesn't get called if cursor is null
+        // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+        return null;
+    }
+
+    public void close() {
+        if (DBG) Log.d(LOG_TAG, "close()");
+        changeCursor(null);
+        mClosed = true;
+    }
+
+    @Override
+    public void notifyDataSetChanged() {
+        if (DBG) Log.d(LOG_TAG, "notifyDataSetChanged");
+        super.notifyDataSetChanged();
+
+        // mSearchView.onDataSetChanged(); // TODO:
+
+        updateSpinnerState(getCursor());
+    }
+
+    @Override
+    public void notifyDataSetInvalidated() {
+        if (DBG) Log.d(LOG_TAG, "notifyDataSetInvalidated");
+        super.notifyDataSetInvalidated();
+
+        updateSpinnerState(getCursor());
+    }
+
+    private void updateSpinnerState(Cursor cursor) {
+        Bundle extras = cursor != null ? cursor.getExtras() : null;
+        if (DBG) {
+            Log.d(LOG_TAG, "updateSpinnerState - extra = "
+                + (extras != null
+                        ? extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)
+                        : null));
+        }
+        // Check if the Cursor indicates that the query is not complete and show the spinner
+        if (extras != null
+                && extras.getBoolean(SearchManager.CURSOR_EXTRA_KEY_IN_PROGRESS)) {
+            // mSearchView.getWindow().getDecorView().post(mStartSpinnerRunnable); // TODO:
+            return;
+        }
+        // If cursor is null or is done, stop the spinner
+        // mSearchView.getWindow().getDecorView().post(mStopSpinnerRunnable); // TODO:
+    }
+
+    /**
+     * Cache columns.
+     */
+    @Override
+    public void changeCursor(Cursor c) {
+        if (DBG) Log.d(LOG_TAG, "changeCursor(" + c + ")");
+
+        if (mClosed) {
+            Log.w(LOG_TAG, "Tried to change cursor after adapter was closed.");
+            if (c != null) c.close();
+            return;
+        }
+
+        try {
+            super.changeCursor(c);
+
+            if (c != null) {
+                mText1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1);
+                mText2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2);
+                mText2UrlCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_2_URL);
+                mIconName1Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_1);
+                mIconName2Col = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_ICON_2);
+                mFlagsCol = c.getColumnIndex(SearchManager.SUGGEST_COLUMN_FLAGS);
+            }
+        } catch (Exception e) {
+            Log.e(LOG_TAG, "error changing cursor and caching columns", e);
+        }
+    }
+
+    /**
+     * Tags the view with cached child view look-ups.
+     */
+    @Override
+    public View newView(Context context, Cursor cursor, ViewGroup parent) {
+        final View v = super.newView(context, cursor, parent);
+        v.setTag(new ChildViewCache(v));
+
+        // Set up icon.
+        final ImageView iconRefine = v.findViewById(R.id.edit_query);
+        iconRefine.setImageResource(mCommitIconResId);
+
+        return v;
+    }
+
+    /**
+     * Cache of the child views of drop-drown list items, to avoid looking up the children
+     * each time the contents of a list item are changed.
+     */
+    private final static class ChildViewCache {
+        public final TextView mText1;
+        public final TextView mText2;
+        public final ImageView mIcon1;
+        public final ImageView mIcon2;
+        public final ImageView mIconRefine;
+
+        public ChildViewCache(View v) {
+            mText1 = v.findViewById(com.android.internal.R.id.text1);
+            mText2 = v.findViewById(com.android.internal.R.id.text2);
+            mIcon1 = v.findViewById(com.android.internal.R.id.icon1);
+            mIcon2 = v.findViewById(com.android.internal.R.id.icon2);
+            mIconRefine = v.findViewById(com.android.internal.R.id.edit_query);
+        }
+    }
+
+    @Override
+    public void bindView(View view, Context context, Cursor cursor) {
+        ChildViewCache views = (ChildViewCache) view.getTag();
+
+        int flags = 0;
+        if (mFlagsCol != INVALID_INDEX) {
+            flags = cursor.getInt(mFlagsCol);
+        }
+        if (views.mText1 != null) {
+            String text1 = getStringOrNull(cursor, mText1Col);
+            setViewText(views.mText1, text1);
+        }
+        if (views.mText2 != null) {
+            // First check TEXT_2_URL
+            CharSequence text2 = getStringOrNull(cursor, mText2UrlCol);
+            if (text2 != null) {
+                text2 = formatUrl(context, text2);
+            } else {
+                text2 = getStringOrNull(cursor, mText2Col);
+            }
+
+            // If no second line of text is indicated, allow the first line of text
+            // to be up to two lines if it wants to be.
+            if (TextUtils.isEmpty(text2)) {
+                if (views.mText1 != null) {
+                    views.mText1.setSingleLine(false);
+                    views.mText1.setMaxLines(2);
+                }
+            } else {
+                if (views.mText1 != null) {
+                    views.mText1.setSingleLine(true);
+                    views.mText1.setMaxLines(1);
+                }
+            }
+            setViewText(views.mText2, text2);
+        }
+
+        if (views.mIcon1 != null) {
+            setViewDrawable(views.mIcon1, getIcon1(cursor), View.INVISIBLE);
+        }
+        if (views.mIcon2 != null) {
+            setViewDrawable(views.mIcon2, getIcon2(cursor), View.GONE);
+        }
+        if (mQueryRefinement == REFINE_ALL
+                || (mQueryRefinement == REFINE_BY_ENTRY
+                        && (flags & SearchManager.FLAG_QUERY_REFINEMENT) != 0)) {
+            views.mIconRefine.setVisibility(View.VISIBLE);
+            views.mIconRefine.setTag(views.mText1.getText());
+            views.mIconRefine.setOnClickListener(this);
+        } else {
+            views.mIconRefine.setVisibility(View.GONE);
+        }
+    }
+
+    public void onClick(View v) {
+        Object tag = v.getTag();
+        if (tag instanceof CharSequence) {
+            mSearchView.onQueryRefine((CharSequence) tag);
+        }
+    }
+
+    private CharSequence formatUrl(Context context, CharSequence url) {
+        if (mUrlColor == null) {
+            // Lazily get the URL color from the current theme.
+            TypedValue colorValue = new TypedValue();
+            context.getTheme().resolveAttribute(R.attr.textColorSearchUrl, colorValue, true);
+            mUrlColor = context.getColorStateList(colorValue.resourceId);
+        }
+
+        SpannableString text = new SpannableString(url);
+        text.setSpan(new TextAppearanceSpan(null, 0, 0, mUrlColor, null),
+                0, url.length(),
+                Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
+        return text;
+    }
+
+    private void setViewText(TextView v, CharSequence text) {
+        // Set the text even if it's null, since we need to clear any previous text.
+        v.setText(text);
+
+        if (TextUtils.isEmpty(text)) {
+            v.setVisibility(View.GONE);
+        } else {
+            v.setVisibility(View.VISIBLE);
+        }
+    }
+
+    private Drawable getIcon1(Cursor cursor) {
+        if (mIconName1Col == INVALID_INDEX) {
+            return null;
+        }
+        String value = cursor.getString(mIconName1Col);
+        Drawable drawable = getDrawableFromResourceValue(value);
+        if (drawable != null) {
+            return drawable;
+        }
+        return getDefaultIcon1(cursor);
+    }
+
+    private Drawable getIcon2(Cursor cursor) {
+        if (mIconName2Col == INVALID_INDEX) {
+            return null;
+        }
+        String value = cursor.getString(mIconName2Col);
+        return getDrawableFromResourceValue(value);
+    }
+
+    /**
+     * Sets the drawable in an image view, makes sure the view is only visible if there
+     * is a drawable.
+     */
+    private void setViewDrawable(ImageView v, Drawable drawable, int nullVisibility) {
+        // Set the icon even if the drawable is null, since we need to clear any
+        // previous icon.
+        v.setImageDrawable(drawable);
+
+        if (drawable == null) {
+            v.setVisibility(nullVisibility);
+        } else {
+            v.setVisibility(View.VISIBLE);
+
+            // This is a hack to get any animated drawables (like a 'working' spinner)
+            // to animate. You have to setVisible true on an AnimationDrawable to get
+            // it to start animating, but it must first have been false or else the
+            // call to setVisible will be ineffective. We need to clear up the story
+            // about animated drawables in the future, see http://b/1878430.
+            drawable.setVisible(false, false);
+            drawable.setVisible(true, false);
+        }
+    }
+
+    /**
+     * Gets the text to show in the query field when a suggestion is selected.
+     *
+     * @param cursor The Cursor to read the suggestion data from. The Cursor should already
+     *        be moved to the suggestion that is to be read from.
+     * @return The text to show, or <code>null</code> if the query should not be
+     *         changed when selecting this suggestion.
+     */
+    @Override
+    public CharSequence convertToString(Cursor cursor) {
+        if (cursor == null) {
+            return null;
+        }
+
+        String query = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_QUERY);
+        if (query != null) {
+            return query;
+        }
+
+        if (mSearchable.shouldRewriteQueryFromData()) {
+            String data = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_INTENT_DATA);
+            if (data != null) {
+                return data;
+            }
+        }
+
+        if (mSearchable.shouldRewriteQueryFromText()) {
+            String text1 = getColumnString(cursor, SearchManager.SUGGEST_COLUMN_TEXT_1);
+            if (text1 != null) {
+                return text1;
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * This method is overridden purely to provide a bit of protection against
+     * flaky content providers.
+     *
+     * @see android.widget.ListAdapter#getView(int, View, ViewGroup)
+     */
+    @Override
+    public View getView(int position, View convertView, ViewGroup parent) {
+        try {
+            return super.getView(position, convertView, parent);
+        } catch (RuntimeException e) {
+            Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
+            // Put exception string in item title
+            View v = newView(mContext, mCursor, parent);
+            if (v != null) {
+                ChildViewCache views = (ChildViewCache) v.getTag();
+                TextView tv = views.mText1;
+                tv.setText(e.toString());
+            }
+            return v;
+        }
+    }
+
+    /**
+     * This method is overridden purely to provide a bit of protection against
+     * flaky content providers.
+     *
+     * @see android.widget.CursorAdapter#getDropDownView(int, View, ViewGroup)
+     */
+    @Override
+    public View getDropDownView(int position, View convertView, ViewGroup parent) {
+        try {
+            return super.getDropDownView(position, convertView, parent);
+        } catch (RuntimeException e) {
+            Log.w(LOG_TAG, "Search suggestions cursor threw exception.", e);
+            // Put exception string in item title
+            final Context context = mDropDownContext == null ? mContext : mDropDownContext;
+            final View v = newDropDownView(context, mCursor, parent);
+            if (v != null) {
+                final ChildViewCache views = (ChildViewCache) v.getTag();
+                final TextView tv = views.mText1;
+                tv.setText(e.toString());
+            }
+            return v;
+        }
+    }
+
+    /**
+     * Gets a drawable given a value provided by a suggestion provider.
+     *
+     * This value could be just the string value of a resource id
+     * (e.g., "2130837524"), in which case we will try to retrieve a drawable from
+     * the provider's resources. If the value is not an integer, it is
+     * treated as a Uri and opened with
+     * {@link ContentResolver#openOutputStream(android.net.Uri, String)}.
+     *
+     * All resources and URIs are read using the suggestion provider's context.
+     *
+     * If the string is not formatted as expected, or no drawable can be found for
+     * the provided value, this method returns null.
+     *
+     * @param drawableId a string like "2130837524",
+     *        "android.resource://com.android.alarmclock/2130837524",
+     *        or "content://contacts/photos/253".
+     * @return a Drawable, or null if none found
+     */
+    private Drawable getDrawableFromResourceValue(String drawableId) {
+        if (drawableId == null || drawableId.length() == 0 || "0".equals(drawableId)) {
+            return null;
+        }
+        try {
+            // First, see if it's just an integer
+            int resourceId = Integer.parseInt(drawableId);
+            // It's an int, look for it in the cache
+            String drawableUri = ContentResolver.SCHEME_ANDROID_RESOURCE
+                    + "://" + mProviderContext.getPackageName() + "/" + resourceId;
+            // Must use URI as cache key, since ints are app-specific
+            Drawable drawable = checkIconCache(drawableUri);
+            if (drawable != null) {
+                return drawable;
+            }
+            // Not cached, find it by resource ID
+            drawable = mProviderContext.getDrawable(resourceId);
+            // Stick it in the cache, using the URI as key
+            storeInIconCache(drawableUri, drawable);
+            return drawable;
+        } catch (NumberFormatException nfe) {
+            // It's not an integer, use it as a URI
+            Drawable drawable = checkIconCache(drawableId);
+            if (drawable != null) {
+                return drawable;
+            }
+            Uri uri = Uri.parse(drawableId);
+            drawable = getDrawable(uri);
+            storeInIconCache(drawableId, drawable);
+            return drawable;
+        } catch (Resources.NotFoundException nfe) {
+            // It was an integer, but it couldn't be found, bail out
+            Log.w(LOG_TAG, "Icon resource not found: " + drawableId);
+            return null;
+        }
+    }
+
+    /**
+     * Gets a drawable by URI, without using the cache.
+     *
+     * @return A drawable, or {@code null} if the drawable could not be loaded.
+     */
+    private Drawable getDrawable(Uri uri) {
+        try {
+            String scheme = uri.getScheme();
+            if (ContentResolver.SCHEME_ANDROID_RESOURCE.equals(scheme)) {
+                // Load drawables through Resources, to get the source density information
+                OpenResourceIdResult r =
+                    mProviderContext.getContentResolver().getResourceId(uri);
+                try {
+                    return r.r.getDrawable(r.id, mProviderContext.getTheme());
+                } catch (Resources.NotFoundException ex) {
+                    throw new FileNotFoundException("Resource does not exist: " + uri);
+                }
+            } else {
+                // Let the ContentResolver handle content and file URIs.
+                InputStream stream = mProviderContext.getContentResolver().openInputStream(uri);
+                if (stream == null) {
+                    throw new FileNotFoundException("Failed to open " + uri);
+                }
+                try {
+                    return Drawable.createFromStream(stream, null);
+                } finally {
+                    try {
+                        stream.close();
+                    } catch (IOException ex) {
+                        Log.e(LOG_TAG, "Error closing icon stream for " + uri, ex);
+                    }
+                }
+            }
+        } catch (FileNotFoundException fnfe) {
+            Log.w(LOG_TAG, "Icon not found: " + uri + ", " + fnfe.getMessage());
+            return null;
+        }
+    }
+
+    private Drawable checkIconCache(String resourceUri) {
+        Drawable.ConstantState cached = mOutsideDrawablesCache.get(resourceUri);
+        if (cached == null) {
+            return null;
+        }
+        if (DBG) Log.d(LOG_TAG, "Found icon in cache: " + resourceUri);
+        return cached.newDrawable();
+    }
+
+    private void storeInIconCache(String resourceUri, Drawable drawable) {
+        if (drawable != null) {
+            mOutsideDrawablesCache.put(resourceUri, drawable.getConstantState());
+        }
+    }
+
+    /**
+     * Gets the left-hand side icon that will be used for the current suggestion
+     * if the suggestion contains an icon column but no icon or a broken icon.
+     *
+     * @param cursor A cursor positioned at the current suggestion.
+     * @return A non-null drawable.
+     */
+    private Drawable getDefaultIcon1(Cursor cursor) {
+        // Check the component that gave us the suggestion
+        Drawable drawable = getActivityIconWithCache(mSearchable.getSearchActivity());
+        if (drawable != null) {
+            return drawable;
+        }
+
+        // Fall back to a default icon
+        return mContext.getPackageManager().getDefaultActivityIcon();
+    }
+
+    /**
+     * Gets the activity or application icon for an activity.
+     * Uses the local icon cache for fast repeated lookups.
+     *
+     * @param component Name of an activity.
+     * @return A drawable, or {@code null} if neither the activity nor the application
+     *         has an icon set.
+     */
+    private Drawable getActivityIconWithCache(ComponentName component) {
+        // First check the icon cache
+        String componentIconKey = component.flattenToShortString();
+        // Using containsKey() since we also store null values.
+        if (mOutsideDrawablesCache.containsKey(componentIconKey)) {
+            Drawable.ConstantState cached = mOutsideDrawablesCache.get(componentIconKey);
+            return cached == null ? null : cached.newDrawable(mProviderContext.getResources());
+        }
+        // Then try the activity or application icon
+        Drawable drawable = getActivityIcon(component);
+        // Stick it in the cache so we don't do this lookup again.
+        Drawable.ConstantState toCache = drawable == null ? null : drawable.getConstantState();
+        mOutsideDrawablesCache.put(componentIconKey, toCache);
+        return drawable;
+    }
+
+    /**
+     * Gets the activity or application icon for an activity.
+     *
+     * @param component Name of an activity.
+     * @return A drawable, or {@code null} if neither the acitivy or the application
+     *         have an icon set.
+     */
+    private Drawable getActivityIcon(ComponentName component) {
+        PackageManager pm = mContext.getPackageManager();
+        final ActivityInfo activityInfo;
+        try {
+            activityInfo = pm.getActivityInfo(component, PackageManager.GET_META_DATA);
+        } catch (NameNotFoundException ex) {
+            Log.w(LOG_TAG, ex.toString());
+            return null;
+        }
+        int iconId = activityInfo.getIconResource();
+        if (iconId == 0) return null;
+        String pkg = component.getPackageName();
+        Drawable drawable = pm.getDrawable(pkg, iconId, activityInfo.applicationInfo);
+        if (drawable == null) {
+            Log.w(LOG_TAG, "Invalid icon resource " + iconId + " for "
+                    + component.flattenToShortString());
+            return null;
+        }
+        return drawable;
+    }
+
+    /**
+     * Gets the value of a string column by name.
+     *
+     * @param cursor Cursor to read the value from.
+     * @param columnName The name of the column to read.
+     * @return The value of the given column, or <code>null</null>
+     *         if the cursor does not contain the given column.
+     */
+    public static String getColumnString(Cursor cursor, String columnName) {
+        int col = cursor.getColumnIndex(columnName);
+        return getStringOrNull(cursor, col);
+    }
+
+    private static String getStringOrNull(Cursor cursor, int col) {
+        if (col == INVALID_INDEX) {
+            return null;
+        }
+        try {
+            return cursor.getString(col);
+        } catch (Exception e) {
+            Log.e(LOG_TAG,
+                    "unexpected error retrieving valid column from cursor, "
+                            + "did the remote process die?", e);
+            return null;
+        }
+    }
+}
diff --git a/android/widget/Switch.java b/android/widget/Switch.java
new file mode 100644
index 0000000..2e1e963
--- /dev/null
+++ b/android/widget/Switch.java
@@ -0,0 +1,1472 @@
+/*
+ * 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 android.widget;
+
+import android.animation.ObjectAnimator;
+import android.annotation.DrawableRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StyleRes;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.Region.Op;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.os.Build.VERSION_CODES;
+import android.text.Layout;
+import android.text.StaticLayout;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.method.AllCapsTransformationMethod;
+import android.text.method.TransformationMethod2;
+import android.util.AttributeSet;
+import android.util.FloatProperty;
+import android.util.MathUtils;
+import android.view.Gravity;
+import android.view.MotionEvent;
+import android.view.SoundEffectConstants;
+import android.view.VelocityTracker;
+import android.view.ViewConfiguration;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+
+import com.android.internal.R;
+
+/**
+ * A Switch is a two-state toggle switch widget that can select between two
+ * options. The user may drag the "thumb" back and forth to choose the selected option,
+ * or simply tap to toggle as if it were a checkbox. The {@link #setText(CharSequence) text}
+ * property controls the text displayed in the label for the switch, whereas the
+ * {@link #setTextOff(CharSequence) off} and {@link #setTextOn(CharSequence) on} text
+ * controls the text on the thumb. Similarly, the
+ * {@link #setTextAppearance(android.content.Context, int) textAppearance} and the related
+ * setTypeface() methods control the typeface and style of label text, whereas the
+ * {@link #setSwitchTextAppearance(android.content.Context, int) switchTextAppearance} and
+ * the related setSwitchTypeface() methods control that of the thumb.
+ *
+ * <p>{@link android.support.v7.widget.SwitchCompat} is a version of
+ * the Switch widget which runs on devices back to API 7.</p>
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
+ * guide.</p>
+ *
+ * @attr ref android.R.styleable#Switch_textOn
+ * @attr ref android.R.styleable#Switch_textOff
+ * @attr ref android.R.styleable#Switch_switchMinWidth
+ * @attr ref android.R.styleable#Switch_switchPadding
+ * @attr ref android.R.styleable#Switch_switchTextAppearance
+ * @attr ref android.R.styleable#Switch_thumb
+ * @attr ref android.R.styleable#Switch_thumbTextPadding
+ * @attr ref android.R.styleable#Switch_track
+ */
+public class Switch extends CompoundButton {
+    private static final int THUMB_ANIMATION_DURATION = 250;
+
+    private static final int TOUCH_MODE_IDLE = 0;
+    private static final int TOUCH_MODE_DOWN = 1;
+    private static final int TOUCH_MODE_DRAGGING = 2;
+
+    // Enum for the "typeface" XML parameter.
+    private static final int SANS = 1;
+    private static final int SERIF = 2;
+    private static final int MONOSPACE = 3;
+
+    private Drawable mThumbDrawable;
+    private ColorStateList mThumbTintList = null;
+    private PorterDuff.Mode mThumbTintMode = null;
+    private boolean mHasThumbTint = false;
+    private boolean mHasThumbTintMode = false;
+
+    private Drawable mTrackDrawable;
+    private ColorStateList mTrackTintList = null;
+    private PorterDuff.Mode mTrackTintMode = null;
+    private boolean mHasTrackTint = false;
+    private boolean mHasTrackTintMode = false;
+
+    private int mThumbTextPadding;
+    private int mSwitchMinWidth;
+    private int mSwitchPadding;
+    private boolean mSplitTrack;
+    private CharSequence mTextOn;
+    private CharSequence mTextOff;
+    private boolean mShowText;
+    private boolean mUseFallbackLineSpacing;
+
+    private int mTouchMode;
+    private int mTouchSlop;
+    private float mTouchX;
+    private float mTouchY;
+    private VelocityTracker mVelocityTracker = VelocityTracker.obtain();
+    private int mMinFlingVelocity;
+
+    private float mThumbPosition;
+
+    /**
+     * Width required to draw the switch track and thumb. Includes padding and
+     * optical bounds for both the track and thumb.
+     */
+    private int mSwitchWidth;
+
+    /**
+     * Height required to draw the switch track and thumb. Includes padding and
+     * optical bounds for both the track and thumb.
+     */
+    private int mSwitchHeight;
+
+    /**
+     * Width of the thumb's content region. Does not include padding or
+     * optical bounds.
+     */
+    private int mThumbWidth;
+
+    /** Left bound for drawing the switch track and thumb. */
+    private int mSwitchLeft;
+
+    /** Top bound for drawing the switch track and thumb. */
+    private int mSwitchTop;
+
+    /** Right bound for drawing the switch track and thumb. */
+    private int mSwitchRight;
+
+    /** Bottom bound for drawing the switch track and thumb. */
+    private int mSwitchBottom;
+
+    private TextPaint mTextPaint;
+    private ColorStateList mTextColors;
+    private Layout mOnLayout;
+    private Layout mOffLayout;
+    private TransformationMethod2 mSwitchTransformationMethod;
+    private ObjectAnimator mPositionAnimator;
+
+    @SuppressWarnings("hiding")
+    private final Rect mTempRect = new Rect();
+
+    private static final int[] CHECKED_STATE_SET = {
+        R.attr.state_checked
+    };
+
+    /**
+     * Construct a new Switch with default styling.
+     *
+     * @param context The Context that will determine this widget's theming.
+     */
+    public Switch(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Construct a new Switch with default styling, overriding specific style
+     * attributes as requested.
+     *
+     * @param context The Context that will determine this widget's theming.
+     * @param attrs Specification of attributes that should deviate from default styling.
+     */
+    public Switch(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.switchStyle);
+    }
+
+    /**
+     * Construct a new Switch with a default style determined by the given theme attribute,
+     * overriding specific style attributes as requested.
+     *
+     * @param context The Context that will determine this widget's theming.
+     * @param attrs Specification of attributes that should deviate from the default styling.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     */
+    public Switch(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+
+    /**
+     * Construct a new Switch with a default style determined by the given theme
+     * attribute or style resource, overriding specific style attributes as
+     * requested.
+     *
+     * @param context The Context that will determine this widget's theming.
+     * @param attrs Specification of attributes that should deviate from the
+     *        default styling.
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     * @param defStyleRes A resource identifier of a style resource that
+     *        supplies default values for the view, used only if
+     *        defStyleAttr is 0 or can not be found in the theme. Can be 0
+     *        to not look for defaults.
+     */
+    public Switch(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+
+        final Resources res = getResources();
+        mTextPaint.density = res.getDisplayMetrics().density;
+        mTextPaint.setCompatibilityScaling(res.getCompatibilityInfo().applicationScale);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.Switch, defStyleAttr, defStyleRes);
+        mThumbDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_thumb);
+        if (mThumbDrawable != null) {
+            mThumbDrawable.setCallback(this);
+        }
+        mTrackDrawable = a.getDrawable(com.android.internal.R.styleable.Switch_track);
+        if (mTrackDrawable != null) {
+            mTrackDrawable.setCallback(this);
+        }
+        mTextOn = a.getText(com.android.internal.R.styleable.Switch_textOn);
+        mTextOff = a.getText(com.android.internal.R.styleable.Switch_textOff);
+        mShowText = a.getBoolean(com.android.internal.R.styleable.Switch_showText, true);
+        mThumbTextPadding = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.Switch_thumbTextPadding, 0);
+        mSwitchMinWidth = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.Switch_switchMinWidth, 0);
+        mSwitchPadding = a.getDimensionPixelSize(
+                com.android.internal.R.styleable.Switch_switchPadding, 0);
+        mSplitTrack = a.getBoolean(com.android.internal.R.styleable.Switch_splitTrack, false);
+
+        // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES.
+        // STOPSHIP if the above TODO is not done.
+        mUseFallbackLineSpacing =
+                context.getApplicationInfo().targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT;
+
+        ColorStateList thumbTintList = a.getColorStateList(
+                com.android.internal.R.styleable.Switch_thumbTint);
+        if (thumbTintList != null) {
+            mThumbTintList = thumbTintList;
+            mHasThumbTint = true;
+        }
+        PorterDuff.Mode thumbTintMode = Drawable.parseTintMode(
+                a.getInt(com.android.internal.R.styleable.Switch_thumbTintMode, -1), null);
+        if (mThumbTintMode != thumbTintMode) {
+            mThumbTintMode = thumbTintMode;
+            mHasThumbTintMode = true;
+        }
+        if (mHasThumbTint || mHasThumbTintMode) {
+            applyThumbTint();
+        }
+
+        ColorStateList trackTintList = a.getColorStateList(
+                com.android.internal.R.styleable.Switch_trackTint);
+        if (trackTintList != null) {
+            mTrackTintList = trackTintList;
+            mHasTrackTint = true;
+        }
+        PorterDuff.Mode trackTintMode = Drawable.parseTintMode(
+                a.getInt(com.android.internal.R.styleable.Switch_trackTintMode, -1), null);
+        if (mTrackTintMode != trackTintMode) {
+            mTrackTintMode = trackTintMode;
+            mHasTrackTintMode = true;
+        }
+        if (mHasTrackTint || mHasTrackTintMode) {
+            applyTrackTint();
+        }
+
+        final int appearance = a.getResourceId(
+                com.android.internal.R.styleable.Switch_switchTextAppearance, 0);
+        if (appearance != 0) {
+            setSwitchTextAppearance(context, appearance);
+        }
+        a.recycle();
+
+        final ViewConfiguration config = ViewConfiguration.get(context);
+        mTouchSlop = config.getScaledTouchSlop();
+        mMinFlingVelocity = config.getScaledMinimumFlingVelocity();
+
+        // Refresh display with current params
+        refreshDrawableState();
+        setChecked(isChecked());
+    }
+
+    /**
+     * Sets the switch text color, size, style, hint color, and highlight color
+     * from the specified TextAppearance resource.
+     *
+     * @attr ref android.R.styleable#Switch_switchTextAppearance
+     */
+    public void setSwitchTextAppearance(Context context, @StyleRes int resid) {
+        TypedArray appearance =
+                context.obtainStyledAttributes(resid,
+                        com.android.internal.R.styleable.TextAppearance);
+
+        ColorStateList colors;
+        int ts;
+
+        colors = appearance.getColorStateList(com.android.internal.R.styleable.
+                TextAppearance_textColor);
+        if (colors != null) {
+            mTextColors = colors;
+        } else {
+            // If no color set in TextAppearance, default to the view's textColor
+            mTextColors = getTextColors();
+        }
+
+        ts = appearance.getDimensionPixelSize(com.android.internal.R.styleable.
+                TextAppearance_textSize, 0);
+        if (ts != 0) {
+            if (ts != mTextPaint.getTextSize()) {
+                mTextPaint.setTextSize(ts);
+                requestLayout();
+            }
+        }
+
+        int typefaceIndex, styleIndex;
+
+        typefaceIndex = appearance.getInt(com.android.internal.R.styleable.
+                TextAppearance_typeface, -1);
+        styleIndex = appearance.getInt(com.android.internal.R.styleable.
+                TextAppearance_textStyle, -1);
+
+        setSwitchTypefaceByIndex(typefaceIndex, styleIndex);
+
+        boolean allCaps = appearance.getBoolean(com.android.internal.R.styleable.
+                TextAppearance_textAllCaps, false);
+        if (allCaps) {
+            mSwitchTransformationMethod = new AllCapsTransformationMethod(getContext());
+            mSwitchTransformationMethod.setLengthChangesAllowed(true);
+        } else {
+            mSwitchTransformationMethod = null;
+        }
+
+        appearance.recycle();
+    }
+
+    private void setSwitchTypefaceByIndex(int typefaceIndex, int styleIndex) {
+        Typeface tf = null;
+        switch (typefaceIndex) {
+            case SANS:
+                tf = Typeface.SANS_SERIF;
+                break;
+
+            case SERIF:
+                tf = Typeface.SERIF;
+                break;
+
+            case MONOSPACE:
+                tf = Typeface.MONOSPACE;
+                break;
+        }
+
+        setSwitchTypeface(tf, styleIndex);
+    }
+
+    /**
+     * Sets the typeface and style in which the text should be displayed on the
+     * switch, and turns on the fake bold and italic bits in the Paint if the
+     * Typeface that you provided does not have all the bits in the
+     * style that you specified.
+     */
+    public void setSwitchTypeface(Typeface tf, int style) {
+        if (style > 0) {
+            if (tf == null) {
+                tf = Typeface.defaultFromStyle(style);
+            } else {
+                tf = Typeface.create(tf, style);
+            }
+
+            setSwitchTypeface(tf);
+            // now compute what (if any) algorithmic styling is needed
+            int typefaceStyle = tf != null ? tf.getStyle() : 0;
+            int need = style & ~typefaceStyle;
+            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
+            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
+        } else {
+            mTextPaint.setFakeBoldText(false);
+            mTextPaint.setTextSkewX(0);
+            setSwitchTypeface(tf);
+        }
+    }
+
+    /**
+     * Sets the typeface in which the text should be displayed on the switch.
+     * Note that not all Typeface families actually have bold and italic
+     * variants, so you may need to use
+     * {@link #setSwitchTypeface(Typeface, int)} to get the appearance
+     * that you actually want.
+     *
+     * @attr ref android.R.styleable#TextView_typeface
+     * @attr ref android.R.styleable#TextView_textStyle
+     */
+    public void setSwitchTypeface(Typeface tf) {
+        if (mTextPaint.getTypeface() != tf) {
+            mTextPaint.setTypeface(tf);
+
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Set the amount of horizontal padding between the switch and the associated text.
+     *
+     * @param pixels Amount of padding in pixels
+     *
+     * @attr ref android.R.styleable#Switch_switchPadding
+     */
+    public void setSwitchPadding(int pixels) {
+        mSwitchPadding = pixels;
+        requestLayout();
+    }
+
+    /**
+     * Get the amount of horizontal padding between the switch and the associated text.
+     *
+     * @return Amount of padding in pixels
+     *
+     * @attr ref android.R.styleable#Switch_switchPadding
+     */
+    public int getSwitchPadding() {
+        return mSwitchPadding;
+    }
+
+    /**
+     * Set the minimum width of the switch in pixels. The switch's width will be the maximum
+     * of this value and its measured width as determined by the switch drawables and text used.
+     *
+     * @param pixels Minimum width of the switch in pixels
+     *
+     * @attr ref android.R.styleable#Switch_switchMinWidth
+     */
+    public void setSwitchMinWidth(int pixels) {
+        mSwitchMinWidth = pixels;
+        requestLayout();
+    }
+
+    /**
+     * Get the minimum width of the switch in pixels. The switch's width will be the maximum
+     * of this value and its measured width as determined by the switch drawables and text used.
+     *
+     * @return Minimum width of the switch in pixels
+     *
+     * @attr ref android.R.styleable#Switch_switchMinWidth
+     */
+    public int getSwitchMinWidth() {
+        return mSwitchMinWidth;
+    }
+
+    /**
+     * Set the horizontal padding around the text drawn on the switch itself.
+     *
+     * @param pixels Horizontal padding for switch thumb text in pixels
+     *
+     * @attr ref android.R.styleable#Switch_thumbTextPadding
+     */
+    public void setThumbTextPadding(int pixels) {
+        mThumbTextPadding = pixels;
+        requestLayout();
+    }
+
+    /**
+     * Get the horizontal padding around the text drawn on the switch itself.
+     *
+     * @return Horizontal padding for switch thumb text in pixels
+     *
+     * @attr ref android.R.styleable#Switch_thumbTextPadding
+     */
+    public int getThumbTextPadding() {
+        return mThumbTextPadding;
+    }
+
+    /**
+     * Set the drawable used for the track that the switch slides within.
+     *
+     * @param track Track drawable
+     *
+     * @attr ref android.R.styleable#Switch_track
+     */
+    public void setTrackDrawable(Drawable track) {
+        if (mTrackDrawable != null) {
+            mTrackDrawable.setCallback(null);
+        }
+        mTrackDrawable = track;
+        if (track != null) {
+            track.setCallback(this);
+        }
+        requestLayout();
+    }
+
+    /**
+     * Set the drawable used for the track that the switch slides within.
+     *
+     * @param resId Resource ID of a track drawable
+     *
+     * @attr ref android.R.styleable#Switch_track
+     */
+    public void setTrackResource(@DrawableRes int resId) {
+        setTrackDrawable(getContext().getDrawable(resId));
+    }
+
+    /**
+     * Get the drawable used for the track that the switch slides within.
+     *
+     * @return Track drawable
+     *
+     * @attr ref android.R.styleable#Switch_track
+     */
+    public Drawable getTrackDrawable() {
+        return mTrackDrawable;
+    }
+
+    /**
+     * Applies a tint to the track drawable. Does not modify the current
+     * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setTrackDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and tint
+     * mode using {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#Switch_trackTint
+     * @see #getTrackTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setTrackTintList(@Nullable ColorStateList tint) {
+        mTrackTintList = tint;
+        mHasTrackTint = true;
+
+        applyTrackTint();
+    }
+
+    /**
+     * @return the tint applied to the track drawable
+     * @attr ref android.R.styleable#Switch_trackTint
+     * @see #setTrackTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getTrackTintList() {
+        return mTrackTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setTrackTintList(ColorStateList)}} to the track drawable.
+     * The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#Switch_trackTintMode
+     * @see #getTrackTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setTrackTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mTrackTintMode = tintMode;
+        mHasTrackTintMode = true;
+
+        applyTrackTint();
+    }
+
+    /**
+     * @return the blending mode used to apply the tint to the track
+     *         drawable
+     * @attr ref android.R.styleable#Switch_trackTintMode
+     * @see #setTrackTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getTrackTintMode() {
+        return mTrackTintMode;
+    }
+
+    private void applyTrackTint() {
+        if (mTrackDrawable != null && (mHasTrackTint || mHasTrackTintMode)) {
+            mTrackDrawable = mTrackDrawable.mutate();
+
+            if (mHasTrackTint) {
+                mTrackDrawable.setTintList(mTrackTintList);
+            }
+
+            if (mHasTrackTintMode) {
+                mTrackDrawable.setTintMode(mTrackTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mTrackDrawable.isStateful()) {
+                mTrackDrawable.setState(getDrawableState());
+            }
+        }
+    }
+
+    /**
+     * Set the drawable used for the switch "thumb" - the piece that the user
+     * can physically touch and drag along the track.
+     *
+     * @param thumb Thumb drawable
+     *
+     * @attr ref android.R.styleable#Switch_thumb
+     */
+    public void setThumbDrawable(Drawable thumb) {
+        if (mThumbDrawable != null) {
+            mThumbDrawable.setCallback(null);
+        }
+        mThumbDrawable = thumb;
+        if (thumb != null) {
+            thumb.setCallback(this);
+        }
+        requestLayout();
+    }
+
+    /**
+     * Set the drawable used for the switch "thumb" - the piece that the user
+     * can physically touch and drag along the track.
+     *
+     * @param resId Resource ID of a thumb drawable
+     *
+     * @attr ref android.R.styleable#Switch_thumb
+     */
+    public void setThumbResource(@DrawableRes int resId) {
+        setThumbDrawable(getContext().getDrawable(resId));
+    }
+
+    /**
+     * Get the drawable used for the switch "thumb" - the piece that the user
+     * can physically touch and drag along the track.
+     *
+     * @return Thumb drawable
+     *
+     * @attr ref android.R.styleable#Switch_thumb
+     */
+    public Drawable getThumbDrawable() {
+        return mThumbDrawable;
+    }
+
+    /**
+     * Applies a tint to the thumb drawable. Does not modify the current
+     * tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to {@link #setThumbDrawable(Drawable)} will
+     * automatically mutate the drawable and apply the specified tint and tint
+     * mode using {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#Switch_thumbTint
+     * @see #getThumbTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setThumbTintList(@Nullable ColorStateList tint) {
+        mThumbTintList = tint;
+        mHasThumbTint = true;
+
+        applyThumbTint();
+    }
+
+    /**
+     * @return the tint applied to the thumb drawable
+     * @attr ref android.R.styleable#Switch_thumbTint
+     * @see #setThumbTintList(ColorStateList)
+     */
+    @Nullable
+    public ColorStateList getThumbTintList() {
+        return mThumbTintList;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setThumbTintList(ColorStateList)}} to the thumb drawable.
+     * The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#Switch_thumbTintMode
+     * @see #getThumbTintMode()
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setThumbTintMode(@Nullable PorterDuff.Mode tintMode) {
+        mThumbTintMode = tintMode;
+        mHasThumbTintMode = true;
+
+        applyThumbTint();
+    }
+
+    /**
+     * @return the blending mode used to apply the tint to the thumb
+     *         drawable
+     * @attr ref android.R.styleable#Switch_thumbTintMode
+     * @see #setThumbTintMode(PorterDuff.Mode)
+     */
+    @Nullable
+    public PorterDuff.Mode getThumbTintMode() {
+        return mThumbTintMode;
+    }
+
+    private void applyThumbTint() {
+        if (mThumbDrawable != null && (mHasThumbTint || mHasThumbTintMode)) {
+            mThumbDrawable = mThumbDrawable.mutate();
+
+            if (mHasThumbTint) {
+                mThumbDrawable.setTintList(mThumbTintList);
+            }
+
+            if (mHasThumbTintMode) {
+                mThumbDrawable.setTintMode(mThumbTintMode);
+            }
+
+            // The drawable (or one of its children) may not have been
+            // stateful before applying the tint, so let's try again.
+            if (mThumbDrawable.isStateful()) {
+                mThumbDrawable.setState(getDrawableState());
+            }
+        }
+    }
+
+    /**
+     * Specifies whether the track should be split by the thumb. When true,
+     * the thumb's optical bounds will be clipped out of the track drawable,
+     * then the thumb will be drawn into the resulting gap.
+     *
+     * @param splitTrack Whether the track should be split by the thumb
+     *
+     * @attr ref android.R.styleable#Switch_splitTrack
+     */
+    public void setSplitTrack(boolean splitTrack) {
+        mSplitTrack = splitTrack;
+        invalidate();
+    }
+
+    /**
+     * Returns whether the track should be split by the thumb.
+     *
+     * @attr ref android.R.styleable#Switch_splitTrack
+     */
+    public boolean getSplitTrack() {
+        return mSplitTrack;
+    }
+
+    /**
+     * Returns the text displayed when the button is in the checked state.
+     *
+     * @attr ref android.R.styleable#Switch_textOn
+     */
+    public CharSequence getTextOn() {
+        return mTextOn;
+    }
+
+    /**
+     * Sets the text displayed when the button is in the checked state.
+     *
+     * @attr ref android.R.styleable#Switch_textOn
+     */
+    public void setTextOn(CharSequence textOn) {
+        mTextOn = textOn;
+        requestLayout();
+    }
+
+    /**
+     * Returns the text displayed when the button is not in the checked state.
+     *
+     * @attr ref android.R.styleable#Switch_textOff
+     */
+    public CharSequence getTextOff() {
+        return mTextOff;
+    }
+
+    /**
+     * Sets the text displayed when the button is not in the checked state.
+     *
+     * @attr ref android.R.styleable#Switch_textOff
+     */
+    public void setTextOff(CharSequence textOff) {
+        mTextOff = textOff;
+        requestLayout();
+    }
+
+    /**
+     * Sets whether the on/off text should be displayed.
+     *
+     * @param showText {@code true} to display on/off text
+     * @attr ref android.R.styleable#Switch_showText
+     */
+    public void setShowText(boolean showText) {
+        if (mShowText != showText) {
+            mShowText = showText;
+            requestLayout();
+        }
+    }
+
+    /**
+     * @return whether the on/off text should be displayed
+     * @attr ref android.R.styleable#Switch_showText
+     */
+    public boolean getShowText() {
+        return mShowText;
+    }
+
+    @Override
+    public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        if (mShowText) {
+            if (mOnLayout == null) {
+                mOnLayout = makeLayout(mTextOn);
+            }
+
+            if (mOffLayout == null) {
+                mOffLayout = makeLayout(mTextOff);
+            }
+        }
+
+        final Rect padding = mTempRect;
+        final int thumbWidth;
+        final int thumbHeight;
+        if (mThumbDrawable != null) {
+            // Cached thumb width does not include padding.
+            mThumbDrawable.getPadding(padding);
+            thumbWidth = mThumbDrawable.getIntrinsicWidth() - padding.left - padding.right;
+            thumbHeight = mThumbDrawable.getIntrinsicHeight();
+        } else {
+            thumbWidth = 0;
+            thumbHeight = 0;
+        }
+
+        final int maxTextWidth;
+        if (mShowText) {
+            maxTextWidth = Math.max(mOnLayout.getWidth(), mOffLayout.getWidth())
+                    + mThumbTextPadding * 2;
+        } else {
+            maxTextWidth = 0;
+        }
+
+        mThumbWidth = Math.max(maxTextWidth, thumbWidth);
+
+        final int trackHeight;
+        if (mTrackDrawable != null) {
+            mTrackDrawable.getPadding(padding);
+            trackHeight = mTrackDrawable.getIntrinsicHeight();
+        } else {
+            padding.setEmpty();
+            trackHeight = 0;
+        }
+
+        // Adjust left and right padding to ensure there's enough room for the
+        // thumb's padding (when present).
+        int paddingLeft = padding.left;
+        int paddingRight = padding.right;
+        if (mThumbDrawable != null) {
+            final Insets inset = mThumbDrawable.getOpticalInsets();
+            paddingLeft = Math.max(paddingLeft, inset.left);
+            paddingRight = Math.max(paddingRight, inset.right);
+        }
+
+        final int switchWidth = Math.max(mSwitchMinWidth,
+                2 * mThumbWidth + paddingLeft + paddingRight);
+        final int switchHeight = Math.max(trackHeight, thumbHeight);
+        mSwitchWidth = switchWidth;
+        mSwitchHeight = switchHeight;
+
+        super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+
+        final int measuredHeight = getMeasuredHeight();
+        if (measuredHeight < switchHeight) {
+            setMeasuredDimension(getMeasuredWidthAndState(), switchHeight);
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEventInternal(event);
+
+        final CharSequence text = isChecked() ? mTextOn : mTextOff;
+        if (text != null) {
+            event.getText().add(text);
+        }
+    }
+
+    private Layout makeLayout(CharSequence text) {
+        final CharSequence transformed = (mSwitchTransformationMethod != null)
+                    ? mSwitchTransformationMethod.getTransformation(text, this)
+                    : text;
+
+        int width = (int) Math.ceil(Layout.getDesiredWidth(transformed, 0,
+                transformed.length(), mTextPaint, getTextDirectionHeuristic()));
+        return StaticLayout.Builder.obtain(transformed, 0, transformed.length(), mTextPaint, width)
+                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+                .build();
+    }
+
+    /**
+     * @return true if (x, y) is within the target area of the switch thumb
+     */
+    private boolean hitThumb(float x, float y) {
+        if (mThumbDrawable == null) {
+            return false;
+        }
+
+        // Relies on mTempRect, MUST be called first!
+        final int thumbOffset = getThumbOffset();
+
+        mThumbDrawable.getPadding(mTempRect);
+        final int thumbTop = mSwitchTop - mTouchSlop;
+        final int thumbLeft = mSwitchLeft + thumbOffset - mTouchSlop;
+        final int thumbRight = thumbLeft + mThumbWidth +
+                mTempRect.left + mTempRect.right + mTouchSlop;
+        final int thumbBottom = mSwitchBottom + mTouchSlop;
+        return x > thumbLeft && x < thumbRight && y > thumbTop && y < thumbBottom;
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        mVelocityTracker.addMovement(ev);
+        final int action = ev.getActionMasked();
+        switch (action) {
+            case MotionEvent.ACTION_DOWN: {
+                final float x = ev.getX();
+                final float y = ev.getY();
+                if (isEnabled() && hitThumb(x, y)) {
+                    mTouchMode = TOUCH_MODE_DOWN;
+                    mTouchX = x;
+                    mTouchY = y;
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_MOVE: {
+                switch (mTouchMode) {
+                    case TOUCH_MODE_IDLE:
+                        // Didn't target the thumb, treat normally.
+                        break;
+
+                    case TOUCH_MODE_DOWN: {
+                        final float x = ev.getX();
+                        final float y = ev.getY();
+                        if (Math.abs(x - mTouchX) > mTouchSlop ||
+                                Math.abs(y - mTouchY) > mTouchSlop) {
+                            mTouchMode = TOUCH_MODE_DRAGGING;
+                            getParent().requestDisallowInterceptTouchEvent(true);
+                            mTouchX = x;
+                            mTouchY = y;
+                            return true;
+                        }
+                        break;
+                    }
+
+                    case TOUCH_MODE_DRAGGING: {
+                        final float x = ev.getX();
+                        final int thumbScrollRange = getThumbScrollRange();
+                        final float thumbScrollOffset = x - mTouchX;
+                        float dPos;
+                        if (thumbScrollRange != 0) {
+                            dPos = thumbScrollOffset / thumbScrollRange;
+                        } else {
+                            // If the thumb scroll range is empty, just use the
+                            // movement direction to snap on or off.
+                            dPos = thumbScrollOffset > 0 ? 1 : -1;
+                        }
+                        if (isLayoutRtl()) {
+                            dPos = -dPos;
+                        }
+                        final float newPos = MathUtils.constrain(mThumbPosition + dPos, 0, 1);
+                        if (newPos != mThumbPosition) {
+                            mTouchX = x;
+                            setThumbPosition(newPos);
+                        }
+                        return true;
+                    }
+                }
+                break;
+            }
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL: {
+                if (mTouchMode == TOUCH_MODE_DRAGGING) {
+                    stopDrag(ev);
+                    // Allow super class to handle pressed state, etc.
+                    super.onTouchEvent(ev);
+                    return true;
+                }
+                mTouchMode = TOUCH_MODE_IDLE;
+                mVelocityTracker.clear();
+                break;
+            }
+        }
+
+        return super.onTouchEvent(ev);
+    }
+
+    private void cancelSuperTouch(MotionEvent ev) {
+        MotionEvent cancel = MotionEvent.obtain(ev);
+        cancel.setAction(MotionEvent.ACTION_CANCEL);
+        super.onTouchEvent(cancel);
+        cancel.recycle();
+    }
+
+    /**
+     * Called from onTouchEvent to end a drag operation.
+     *
+     * @param ev Event that triggered the end of drag mode - ACTION_UP or ACTION_CANCEL
+     */
+    private void stopDrag(MotionEvent ev) {
+        mTouchMode = TOUCH_MODE_IDLE;
+
+        // Commit the change if the event is up and not canceled and the switch
+        // has not been disabled during the drag.
+        final boolean commitChange = ev.getAction() == MotionEvent.ACTION_UP && isEnabled();
+        final boolean oldState = isChecked();
+        final boolean newState;
+        if (commitChange) {
+            mVelocityTracker.computeCurrentVelocity(1000);
+            final float xvel = mVelocityTracker.getXVelocity();
+            if (Math.abs(xvel) > mMinFlingVelocity) {
+                newState = isLayoutRtl() ? (xvel < 0) : (xvel > 0);
+            } else {
+                newState = getTargetCheckedState();
+            }
+        } else {
+            newState = oldState;
+        }
+
+        if (newState != oldState) {
+            playSoundEffect(SoundEffectConstants.CLICK);
+        }
+        // Always call setChecked so that the thumb is moved back to the correct edge
+        setChecked(newState);
+        cancelSuperTouch(ev);
+    }
+
+    private void animateThumbToCheckedState(boolean newCheckedState) {
+        final float targetPosition = newCheckedState ? 1 : 0;
+        mPositionAnimator = ObjectAnimator.ofFloat(this, THUMB_POS, targetPosition);
+        mPositionAnimator.setDuration(THUMB_ANIMATION_DURATION);
+        mPositionAnimator.setAutoCancel(true);
+        mPositionAnimator.start();
+    }
+
+    private void cancelPositionAnimator() {
+        if (mPositionAnimator != null) {
+            mPositionAnimator.cancel();
+        }
+    }
+
+    private boolean getTargetCheckedState() {
+        return mThumbPosition > 0.5f;
+    }
+
+    /**
+     * Sets the thumb position as a decimal value between 0 (off) and 1 (on).
+     *
+     * @param position new position between [0,1]
+     */
+    private void setThumbPosition(float position) {
+        mThumbPosition = position;
+        invalidate();
+    }
+
+    @Override
+    public void toggle() {
+        setChecked(!isChecked());
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        super.setChecked(checked);
+
+        // Calling the super method may result in setChecked() getting called
+        // recursively with a different value, so load the REAL value...
+        checked = isChecked();
+
+        if (isAttachedToWindow() && isLaidOut()) {
+            animateThumbToCheckedState(checked);
+        } else {
+            // Immediately move the thumb to the new position.
+            cancelPositionAnimator();
+            setThumbPosition(checked ? 1 : 0);
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        int opticalInsetLeft = 0;
+        int opticalInsetRight = 0;
+        if (mThumbDrawable != null) {
+            final Rect trackPadding = mTempRect;
+            if (mTrackDrawable != null) {
+                mTrackDrawable.getPadding(trackPadding);
+            } else {
+                trackPadding.setEmpty();
+            }
+
+            final Insets insets = mThumbDrawable.getOpticalInsets();
+            opticalInsetLeft = Math.max(0, insets.left - trackPadding.left);
+            opticalInsetRight = Math.max(0, insets.right - trackPadding.right);
+        }
+
+        final int switchRight;
+        final int switchLeft;
+        if (isLayoutRtl()) {
+            switchLeft = getPaddingLeft() + opticalInsetLeft;
+            switchRight = switchLeft + mSwitchWidth - opticalInsetLeft - opticalInsetRight;
+        } else {
+            switchRight = getWidth() - getPaddingRight() - opticalInsetRight;
+            switchLeft = switchRight - mSwitchWidth + opticalInsetLeft + opticalInsetRight;
+        }
+
+        final int switchTop;
+        final int switchBottom;
+        switch (getGravity() & Gravity.VERTICAL_GRAVITY_MASK) {
+            default:
+            case Gravity.TOP:
+                switchTop = getPaddingTop();
+                switchBottom = switchTop + mSwitchHeight;
+                break;
+
+            case Gravity.CENTER_VERTICAL:
+                switchTop = (getPaddingTop() + getHeight() - getPaddingBottom()) / 2 -
+                        mSwitchHeight / 2;
+                switchBottom = switchTop + mSwitchHeight;
+                break;
+
+            case Gravity.BOTTOM:
+                switchBottom = getHeight() - getPaddingBottom();
+                switchTop = switchBottom - mSwitchHeight;
+                break;
+        }
+
+        mSwitchLeft = switchLeft;
+        mSwitchTop = switchTop;
+        mSwitchBottom = switchBottom;
+        mSwitchRight = switchRight;
+    }
+
+    @Override
+    public void draw(Canvas c) {
+        final Rect padding = mTempRect;
+        final int switchLeft = mSwitchLeft;
+        final int switchTop = mSwitchTop;
+        final int switchRight = mSwitchRight;
+        final int switchBottom = mSwitchBottom;
+
+        int thumbInitialLeft = switchLeft + getThumbOffset();
+
+        final Insets thumbInsets;
+        if (mThumbDrawable != null) {
+            thumbInsets = mThumbDrawable.getOpticalInsets();
+        } else {
+            thumbInsets = Insets.NONE;
+        }
+
+        // Layout the track.
+        if (mTrackDrawable != null) {
+            mTrackDrawable.getPadding(padding);
+
+            // Adjust thumb position for track padding.
+            thumbInitialLeft += padding.left;
+
+            // If necessary, offset by the optical insets of the thumb asset.
+            int trackLeft = switchLeft;
+            int trackTop = switchTop;
+            int trackRight = switchRight;
+            int trackBottom = switchBottom;
+            if (thumbInsets != Insets.NONE) {
+                if (thumbInsets.left > padding.left) {
+                    trackLeft += thumbInsets.left - padding.left;
+                }
+                if (thumbInsets.top > padding.top) {
+                    trackTop += thumbInsets.top - padding.top;
+                }
+                if (thumbInsets.right > padding.right) {
+                    trackRight -= thumbInsets.right - padding.right;
+                }
+                if (thumbInsets.bottom > padding.bottom) {
+                    trackBottom -= thumbInsets.bottom - padding.bottom;
+                }
+            }
+            mTrackDrawable.setBounds(trackLeft, trackTop, trackRight, trackBottom);
+        }
+
+        // Layout the thumb.
+        if (mThumbDrawable != null) {
+            mThumbDrawable.getPadding(padding);
+
+            final int thumbLeft = thumbInitialLeft - padding.left;
+            final int thumbRight = thumbInitialLeft + mThumbWidth + padding.right;
+            mThumbDrawable.setBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+
+            final Drawable background = getBackground();
+            if (background != null) {
+                background.setHotspotBounds(thumbLeft, switchTop, thumbRight, switchBottom);
+            }
+        }
+
+        // Draw the background.
+        super.draw(c);
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        super.onDraw(canvas);
+
+        final Rect padding = mTempRect;
+        final Drawable trackDrawable = mTrackDrawable;
+        if (trackDrawable != null) {
+            trackDrawable.getPadding(padding);
+        } else {
+            padding.setEmpty();
+        }
+
+        final int switchTop = mSwitchTop;
+        final int switchBottom = mSwitchBottom;
+        final int switchInnerTop = switchTop + padding.top;
+        final int switchInnerBottom = switchBottom - padding.bottom;
+
+        final Drawable thumbDrawable = mThumbDrawable;
+        if (trackDrawable != null) {
+            if (mSplitTrack && thumbDrawable != null) {
+                final Insets insets = thumbDrawable.getOpticalInsets();
+                thumbDrawable.copyBounds(padding);
+                padding.left += insets.left;
+                padding.right -= insets.right;
+
+                final int saveCount = canvas.save();
+                canvas.clipRect(padding, Op.DIFFERENCE);
+                trackDrawable.draw(canvas);
+                canvas.restoreToCount(saveCount);
+            } else {
+                trackDrawable.draw(canvas);
+            }
+        }
+
+        final int saveCount = canvas.save();
+
+        if (thumbDrawable != null) {
+            thumbDrawable.draw(canvas);
+        }
+
+        final Layout switchText = getTargetCheckedState() ? mOnLayout : mOffLayout;
+        if (switchText != null) {
+            final int drawableState[] = getDrawableState();
+            if (mTextColors != null) {
+                mTextPaint.setColor(mTextColors.getColorForState(drawableState, 0));
+            }
+            mTextPaint.drawableState = drawableState;
+
+            final int cX;
+            if (thumbDrawable != null) {
+                final Rect bounds = thumbDrawable.getBounds();
+                cX = bounds.left + bounds.right;
+            } else {
+                cX = getWidth();
+            }
+
+            final int left = cX / 2 - switchText.getWidth() / 2;
+            final int top = (switchInnerTop + switchInnerBottom) / 2 - switchText.getHeight() / 2;
+            canvas.translate(left, top);
+            switchText.draw(canvas);
+        }
+
+        canvas.restoreToCount(saveCount);
+    }
+
+    @Override
+    public int getCompoundPaddingLeft() {
+        if (!isLayoutRtl()) {
+            return super.getCompoundPaddingLeft();
+        }
+        int padding = super.getCompoundPaddingLeft() + mSwitchWidth;
+        if (!TextUtils.isEmpty(getText())) {
+            padding += mSwitchPadding;
+        }
+        return padding;
+    }
+
+    @Override
+    public int getCompoundPaddingRight() {
+        if (isLayoutRtl()) {
+            return super.getCompoundPaddingRight();
+        }
+        int padding = super.getCompoundPaddingRight() + mSwitchWidth;
+        if (!TextUtils.isEmpty(getText())) {
+            padding += mSwitchPadding;
+        }
+        return padding;
+    }
+
+    /**
+     * Translates thumb position to offset according to current RTL setting and
+     * thumb scroll range. Accounts for both track and thumb padding.
+     *
+     * @return thumb offset
+     */
+    private int getThumbOffset() {
+        final float thumbPosition;
+        if (isLayoutRtl()) {
+            thumbPosition = 1 - mThumbPosition;
+        } else {
+            thumbPosition = mThumbPosition;
+        }
+        return (int) (thumbPosition * getThumbScrollRange() + 0.5f);
+    }
+
+    private int getThumbScrollRange() {
+        if (mTrackDrawable != null) {
+            final Rect padding = mTempRect;
+            mTrackDrawable.getPadding(padding);
+
+            final Insets insets;
+            if (mThumbDrawable != null) {
+                insets = mThumbDrawable.getOpticalInsets();
+            } else {
+                insets = Insets.NONE;
+            }
+
+            return mSwitchWidth - mThumbWidth - padding.left - padding.right
+                    - insets.left - insets.right;
+        } else {
+            return 0;
+        }
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState = super.onCreateDrawableState(extraSpace + 1);
+        if (isChecked()) {
+            mergeDrawableStates(drawableState, CHECKED_STATE_SET);
+        }
+        return drawableState;
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        final int[] state = getDrawableState();
+        boolean changed = false;
+
+        final Drawable thumbDrawable = mThumbDrawable;
+        if (thumbDrawable != null && thumbDrawable.isStateful()) {
+            changed |= thumbDrawable.setState(state);
+        }
+
+        final Drawable trackDrawable = mTrackDrawable;
+        if (trackDrawable != null && trackDrawable.isStateful()) {
+            changed |= trackDrawable.setState(state);
+        }
+
+        if (changed) {
+            invalidate();
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mThumbDrawable != null) {
+            mThumbDrawable.setHotspot(x, y);
+        }
+
+        if (mTrackDrawable != null) {
+            mTrackDrawable.setHotspot(x, y);
+        }
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        return super.verifyDrawable(who) || who == mThumbDrawable || who == mTrackDrawable;
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+
+        if (mThumbDrawable != null) {
+            mThumbDrawable.jumpToCurrentState();
+        }
+
+        if (mTrackDrawable != null) {
+            mTrackDrawable.jumpToCurrentState();
+        }
+
+        if (mPositionAnimator != null && mPositionAnimator.isStarted()) {
+            mPositionAnimator.end();
+            mPositionAnimator = null;
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return Switch.class.getName();
+    }
+
+    @Override
+    public void onProvideStructure(ViewStructure structure) {
+        super.onProvideStructure(structure);
+        onProvideAutoFillStructureForAssistOrAutofill(structure);
+    }
+
+    @Override
+    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+        super.onProvideAutofillStructure(structure, flags);
+        onProvideAutoFillStructureForAssistOrAutofill(structure);
+    }
+
+    // NOTE: currently there is no difference for Assist or AutoFill, so it doesn't take flags
+    private void onProvideAutoFillStructureForAssistOrAutofill(ViewStructure structure) {
+        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+        if (!TextUtils.isEmpty(switchText)) {
+            CharSequence oldText = structure.getText();
+            if (TextUtils.isEmpty(oldText)) {
+                structure.setText(switchText);
+            } else {
+                StringBuilder newText = new StringBuilder();
+                newText.append(oldText).append(' ').append(switchText);
+                structure.setText(newText);
+            }
+            // The style of the label text is provided via the base TextView class. This is more
+            // relevant than the style of the (optional) on/off text on the switch button itself,
+            // so ignore the size/color/style stored this.mTextPaint.
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+        CharSequence switchText = isChecked() ? mTextOn : mTextOff;
+        if (!TextUtils.isEmpty(switchText)) {
+            CharSequence oldText = info.getText();
+            if (TextUtils.isEmpty(oldText)) {
+                info.setText(switchText);
+            } else {
+                StringBuilder newText = new StringBuilder();
+                newText.append(oldText).append(' ').append(switchText);
+                info.setText(newText);
+            }
+        }
+    }
+
+    private static final FloatProperty<Switch> THUMB_POS = new FloatProperty<Switch>("thumbPos") {
+        @Override
+        public Float get(Switch object) {
+            return object.mThumbPosition;
+        }
+
+        @Override
+        public void setValue(Switch object, float value) {
+            object.setThumbPosition(value);
+        }
+    };
+}
diff --git a/android/widget/TabHost.java b/android/widget/TabHost.java
new file mode 100644
index 0000000..8696d0d
--- /dev/null
+++ b/android/widget/TabHost.java
@@ -0,0 +1,806 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.app.LocalActivityManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.SoundEffectConstants;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewTreeObserver;
+import android.view.Window;
+
+import com.android.internal.R;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Container for a tabbed window view. This object holds two children: a set of tab labels that the
+ * user clicks to select a specific tab, and a FrameLayout object that displays the contents of that
+ * page. The individual elements are typically controlled using this container object, rather than
+ * setting values on the child elements themselves.
+ *
+ */
+public class TabHost extends FrameLayout implements ViewTreeObserver.OnTouchModeChangeListener {
+
+    private static final int TABWIDGET_LOCATION_LEFT = 0;
+    private static final int TABWIDGET_LOCATION_TOP = 1;
+    private static final int TABWIDGET_LOCATION_RIGHT = 2;
+    private static final int TABWIDGET_LOCATION_BOTTOM = 3;
+    private TabWidget mTabWidget;
+    private FrameLayout mTabContent;
+    private List<TabSpec> mTabSpecs = new ArrayList<TabSpec>(2);
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected int mCurrentTab = -1;
+    private View mCurrentView = null;
+    /**
+     * This field should be made private, so it is hidden from the SDK.
+     * {@hide}
+     */
+    protected LocalActivityManager mLocalActivityManager = null;
+    private OnTabChangeListener mOnTabChangeListener;
+    private OnKeyListener mTabKeyListener;
+
+    private int mTabLayoutId;
+
+    public TabHost(Context context) {
+        super(context);
+        initTabHost();
+    }
+
+    public TabHost(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
+    }
+
+    public TabHost(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public TabHost(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.TabWidget, defStyleAttr, defStyleRes);
+
+        mTabLayoutId = a.getResourceId(R.styleable.TabWidget_tabLayout, 0);
+        a.recycle();
+
+        if (mTabLayoutId == 0) {
+            // In case the tabWidgetStyle does not inherit from Widget.TabWidget and tabLayout is
+            // not defined.
+            mTabLayoutId = R.layout.tab_indicator_holo;
+        }
+
+        initTabHost();
+    }
+
+    private void initTabHost() {
+        setFocusableInTouchMode(true);
+        setDescendantFocusability(FOCUS_AFTER_DESCENDANTS);
+
+        mCurrentTab = -1;
+        mCurrentView = null;
+    }
+
+    /**
+     * Creates a new {@link TabSpec} associated with this tab host.
+     *
+     * @param tag tag for the tab specification, must be non-null
+     * @throws IllegalArgumentException If the passed tag is null
+     */
+    @NonNull
+    public TabSpec newTabSpec(@NonNull String tag) {
+        if (tag == null) {
+            throw new IllegalArgumentException("tag must be non-null");
+        }
+        return new TabSpec(tag);
+    }
+
+
+
+    /**
+      * <p>Call setup() before adding tabs if loading TabHost using findViewById().
+      * <i><b>However</i></b>: You do not need to call setup() after getTabHost()
+      * in {@link android.app.TabActivity TabActivity}.
+      * Example:</p>
+<pre>mTabHost = (TabHost)findViewById(R.id.tabhost);
+mTabHost.setup();
+mTabHost.addTab(TAB_TAG_1, "Hello, world!", "Tab 1");
+      */
+    public void setup() {
+        mTabWidget = findViewById(com.android.internal.R.id.tabs);
+        if (mTabWidget == null) {
+            throw new RuntimeException(
+                    "Your TabHost must have a TabWidget whose id attribute is 'android.R.id.tabs'");
+        }
+
+        // KeyListener to attach to all tabs. Detects non-navigation keys
+        // and relays them to the tab content.
+        mTabKeyListener = new OnKeyListener() {
+            public boolean onKey(View v, int keyCode, KeyEvent event) {
+                if (KeyEvent.isModifierKey(keyCode)) {
+                    return false;
+                }
+                switch (keyCode) {
+                    case KeyEvent.KEYCODE_DPAD_CENTER:
+                    case KeyEvent.KEYCODE_DPAD_LEFT:
+                    case KeyEvent.KEYCODE_DPAD_RIGHT:
+                    case KeyEvent.KEYCODE_DPAD_UP:
+                    case KeyEvent.KEYCODE_DPAD_DOWN:
+                    case KeyEvent.KEYCODE_TAB:
+                    case KeyEvent.KEYCODE_SPACE:
+                    case KeyEvent.KEYCODE_ENTER:
+                        return false;
+
+                }
+                mTabContent.requestFocus(View.FOCUS_FORWARD);
+                return mTabContent.dispatchKeyEvent(event);
+            }
+
+        };
+
+        mTabWidget.setTabSelectionListener(new TabWidget.OnTabSelectionChanged() {
+            public void onTabSelectionChanged(int tabIndex, boolean clicked) {
+                setCurrentTab(tabIndex);
+                if (clicked) {
+                    mTabContent.requestFocus(View.FOCUS_FORWARD);
+                }
+            }
+        });
+
+        mTabContent = findViewById(com.android.internal.R.id.tabcontent);
+        if (mTabContent == null) {
+            throw new RuntimeException(
+                    "Your TabHost must have a FrameLayout whose id attribute is "
+                            + "'android.R.id.tabcontent'");
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void sendAccessibilityEventInternal(int eventType) {
+        /* avoid super class behavior - TabWidget sends the right events */
+    }
+
+    /**
+     * If you are using {@link TabSpec#setContent(android.content.Intent)}, this
+     * must be called since the activityGroup is needed to launch the local activity.
+     *
+     * This is done for you if you extend {@link android.app.TabActivity}.
+     * @param activityGroup Used to launch activities for tab content.
+     */
+    public void setup(LocalActivityManager activityGroup) {
+        setup();
+        mLocalActivityManager = activityGroup;
+    }
+
+    @Override
+    public void onTouchModeChanged(boolean isInTouchMode) {
+        // No longer used, but kept to maintain API compatibility.
+    }
+
+    /**
+     * Add a tab.
+     * @param tabSpec Specifies how to create the indicator and content.
+     * @throws IllegalArgumentException If the passed tab spec has null indicator strategy and / or
+     *      null content strategy.
+     */
+    public void addTab(TabSpec tabSpec) {
+
+        if (tabSpec.mIndicatorStrategy == null) {
+            throw new IllegalArgumentException("you must specify a way to create the tab indicator.");
+        }
+
+        if (tabSpec.mContentStrategy == null) {
+            throw new IllegalArgumentException("you must specify a way to create the tab content");
+        }
+        View tabIndicator = tabSpec.mIndicatorStrategy.createIndicatorView();
+        tabIndicator.setOnKeyListener(mTabKeyListener);
+
+        // If this is a custom view, then do not draw the bottom strips for
+        // the tab indicators.
+        if (tabSpec.mIndicatorStrategy instanceof ViewIndicatorStrategy) {
+            mTabWidget.setStripEnabled(false);
+        }
+
+        mTabWidget.addView(tabIndicator);
+        mTabSpecs.add(tabSpec);
+
+        if (mCurrentTab == -1) {
+            setCurrentTab(0);
+        }
+    }
+
+
+    /**
+     * Removes all tabs from the tab widget associated with this tab host.
+     */
+    public void clearAllTabs() {
+        mTabWidget.removeAllViews();
+        initTabHost();
+        mTabContent.removeAllViews();
+        mTabSpecs.clear();
+        requestLayout();
+        invalidate();
+    }
+
+    public TabWidget getTabWidget() {
+        return mTabWidget;
+    }
+
+    /**
+     * Returns the current tab.
+     *
+     * @return the current tab, may be {@code null} if no tab is set as current
+     */
+    @Nullable
+    public int getCurrentTab() {
+        return mCurrentTab;
+    }
+
+    /**
+     * Returns the tag for the current tab.
+     *
+     * @return the tag for the current tab, may be {@code null} if no tab is
+     *         set as current
+     */
+    @Nullable
+    public String getCurrentTabTag() {
+        if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+            return mTabSpecs.get(mCurrentTab).getTag();
+        }
+        return null;
+    }
+
+    /**
+     * Returns the view for the current tab.
+     *
+     * @return the view for the current tab, may be {@code null} if no tab is
+     *         set as current
+     */
+    @Nullable
+    public View getCurrentTabView() {
+        if (mCurrentTab >= 0 && mCurrentTab < mTabSpecs.size()) {
+            return mTabWidget.getChildTabViewAt(mCurrentTab);
+        }
+        return null;
+    }
+
+    public View getCurrentView() {
+        return mCurrentView;
+    }
+
+    /**
+     * Sets the current tab based on its tag.
+     *
+     * @param tag the tag for the tab to set as current
+     */
+    public void setCurrentTabByTag(String tag) {
+        for (int i = 0, count = mTabSpecs.size(); i < count; i++) {
+            if (mTabSpecs.get(i).getTag().equals(tag)) {
+                setCurrentTab(i);
+                break;
+            }
+        }
+    }
+
+    /**
+     * Get the FrameLayout which holds tab content
+     */
+    public FrameLayout getTabContentView() {
+        return mTabContent;
+    }
+
+    /**
+     * Get the location of the TabWidget.
+     *
+     * @return The TabWidget location.
+     */
+    private int getTabWidgetLocation() {
+        int location = TABWIDGET_LOCATION_TOP;
+
+        switch (mTabWidget.getOrientation()) {
+            case LinearLayout.VERTICAL:
+                location = (mTabContent.getLeft() < mTabWidget.getLeft()) ? TABWIDGET_LOCATION_RIGHT
+                        : TABWIDGET_LOCATION_LEFT;
+                break;
+            case LinearLayout.HORIZONTAL:
+            default:
+                location = (mTabContent.getTop() < mTabWidget.getTop()) ? TABWIDGET_LOCATION_BOTTOM
+                        : TABWIDGET_LOCATION_TOP;
+                break;
+        }
+        return location;
+    }
+
+    @Override
+    public boolean dispatchKeyEvent(KeyEvent event) {
+        final boolean handled = super.dispatchKeyEvent(event);
+
+        // unhandled key events change focus to tab indicator for embedded
+        // activities when there is nothing that will take focus from default
+        // focus searching
+        if (!handled
+                && (event.getAction() == KeyEvent.ACTION_DOWN)
+                && (mCurrentView != null)
+                && (mCurrentView.isRootNamespace())
+                && (mCurrentView.hasFocus())) {
+            int keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_UP;
+            int directionShouldChangeFocus = View.FOCUS_UP;
+            int soundEffect = SoundEffectConstants.NAVIGATION_UP;
+
+            switch (getTabWidgetLocation()) {
+                case TABWIDGET_LOCATION_LEFT:
+                    keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_LEFT;
+                    directionShouldChangeFocus = View.FOCUS_LEFT;
+                    soundEffect = SoundEffectConstants.NAVIGATION_LEFT;
+                    break;
+                case TABWIDGET_LOCATION_RIGHT:
+                    keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_RIGHT;
+                    directionShouldChangeFocus = View.FOCUS_RIGHT;
+                    soundEffect = SoundEffectConstants.NAVIGATION_RIGHT;
+                    break;
+                case TABWIDGET_LOCATION_BOTTOM:
+                    keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_DOWN;
+                    directionShouldChangeFocus = View.FOCUS_DOWN;
+                    soundEffect = SoundEffectConstants.NAVIGATION_DOWN;
+                    break;
+                case TABWIDGET_LOCATION_TOP:
+                default:
+                    keyCodeShouldChangeFocus = KeyEvent.KEYCODE_DPAD_UP;
+                    directionShouldChangeFocus = View.FOCUS_UP;
+                    soundEffect = SoundEffectConstants.NAVIGATION_UP;
+                    break;
+            }
+            if (event.getKeyCode() == keyCodeShouldChangeFocus
+                    && mCurrentView.findFocus().focusSearch(directionShouldChangeFocus) == null) {
+                mTabWidget.getChildTabViewAt(mCurrentTab).requestFocus();
+                playSoundEffect(soundEffect);
+                return true;
+            }
+        }
+        return handled;
+    }
+
+
+    @Override
+    public void dispatchWindowFocusChanged(boolean hasFocus) {
+        if (mCurrentView != null){
+            mCurrentView.dispatchWindowFocusChanged(hasFocus);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TabHost.class.getName();
+    }
+
+    public void setCurrentTab(int index) {
+        if (index < 0 || index >= mTabSpecs.size()) {
+            return;
+        }
+
+        if (index == mCurrentTab) {
+            return;
+        }
+
+        // notify old tab content
+        if (mCurrentTab != -1) {
+            mTabSpecs.get(mCurrentTab).mContentStrategy.tabClosed();
+        }
+
+        mCurrentTab = index;
+        final TabHost.TabSpec spec = mTabSpecs.get(index);
+
+        // Call the tab widget's focusCurrentTab(), instead of just
+        // selecting the tab.
+        mTabWidget.focusCurrentTab(mCurrentTab);
+
+        // tab content
+        mCurrentView = spec.mContentStrategy.getContentView();
+
+        if (mCurrentView.getParent() == null) {
+            mTabContent
+                    .addView(
+                            mCurrentView,
+                            new ViewGroup.LayoutParams(
+                                    ViewGroup.LayoutParams.MATCH_PARENT,
+                                    ViewGroup.LayoutParams.MATCH_PARENT));
+        }
+
+        if (!mTabWidget.hasFocus()) {
+            // if the tab widget didn't take focus (likely because we're in touch mode)
+            // give the current tab content view a shot
+            mCurrentView.requestFocus();
+        }
+
+        //mTabContent.requestFocus(View.FOCUS_FORWARD);
+        invokeOnTabChangeListener();
+    }
+
+    /**
+     * Register a callback to be invoked when the selected state of any of the items
+     * in this list changes
+     * @param l
+     * The callback that will run
+     */
+    public void setOnTabChangedListener(OnTabChangeListener l) {
+        mOnTabChangeListener = l;
+    }
+
+    private void invokeOnTabChangeListener() {
+        if (mOnTabChangeListener != null) {
+            mOnTabChangeListener.onTabChanged(getCurrentTabTag());
+        }
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when tab changed
+     */
+    public interface OnTabChangeListener {
+        void onTabChanged(String tabId);
+    }
+
+
+    /**
+     * Makes the content of a tab when it is selected. Use this if your tab
+     * content needs to be created on demand, i.e. you are not showing an
+     * existing view or starting an activity.
+     */
+    public interface TabContentFactory {
+        /**
+         * Callback to make the tab contents
+         *
+         * @param tag
+         *            Which tab was selected.
+         * @return The view to display the contents of the selected tab.
+         */
+        View createTabContent(String tag);
+    }
+
+
+    /**
+     * A tab has a tab indicator, content, and a tag that is used to keep
+     * track of it.  This builder helps choose among these options.
+     *
+     * For the tab indicator, your choices are:
+     * 1) set a label
+     * 2) set a label and an icon
+     *
+     * For the tab content, your choices are:
+     * 1) the id of a {@link View}
+     * 2) a {@link TabContentFactory} that creates the {@link View} content.
+     * 3) an {@link Intent} that launches an {@link android.app.Activity}.
+     */
+    public class TabSpec {
+
+        private final @NonNull String mTag;
+
+        private IndicatorStrategy mIndicatorStrategy;
+        private ContentStrategy mContentStrategy;
+
+        /**
+         * Constructs a new tab specification with the specified tag.
+         *
+         * @param tag the tag for the tag specification, must be non-null
+         */
+        private TabSpec(@NonNull String tag) {
+            mTag = tag;
+        }
+
+        /**
+         * Specify a label as the tab indicator.
+         */
+        public TabSpec setIndicator(CharSequence label) {
+            mIndicatorStrategy = new LabelIndicatorStrategy(label);
+            return this;
+        }
+
+        /**
+         * Specify a label and icon as the tab indicator.
+         */
+        public TabSpec setIndicator(CharSequence label, Drawable icon) {
+            mIndicatorStrategy = new LabelAndIconIndicatorStrategy(label, icon);
+            return this;
+        }
+
+        /**
+         * Specify a view as the tab indicator.
+         */
+        public TabSpec setIndicator(View view) {
+            mIndicatorStrategy = new ViewIndicatorStrategy(view);
+            return this;
+        }
+
+        /**
+         * Specify the id of the view that should be used as the content
+         * of the tab.
+         */
+        public TabSpec setContent(int viewId) {
+            mContentStrategy = new ViewIdContentStrategy(viewId);
+            return this;
+        }
+
+        /**
+         * Specify a {@link android.widget.TabHost.TabContentFactory} to use to
+         * create the content of the tab.
+         */
+        public TabSpec setContent(TabContentFactory contentFactory) {
+            mContentStrategy = new FactoryContentStrategy(mTag, contentFactory);
+            return this;
+        }
+
+        /**
+         * Specify an intent to use to launch an activity as the tab content.
+         */
+        public TabSpec setContent(Intent intent) {
+            mContentStrategy = new IntentContentStrategy(mTag, intent);
+            return this;
+        }
+
+        /**
+         * Returns the tag for this tab specification.
+         *
+         * @return the tag for this tab specification
+         */
+        @NonNull
+        public String getTag() {
+            return mTag;
+        }
+    }
+
+    /**
+     * Specifies what you do to create a tab indicator.
+     */
+    private static interface IndicatorStrategy {
+
+        /**
+         * Return the view for the indicator.
+         */
+        View createIndicatorView();
+    }
+
+    /**
+     * Specifies what you do to manage the tab content.
+     */
+    private static interface ContentStrategy {
+
+        /**
+         * Return the content view.  The view should may be cached locally.
+         */
+        View getContentView();
+
+        /**
+         * Perhaps do something when the tab associated with this content has
+         * been closed (i.e make it invisible, or remove it).
+         */
+        void tabClosed();
+    }
+
+    /**
+     * How to create a tab indicator that just has a label.
+     */
+    private class LabelIndicatorStrategy implements IndicatorStrategy {
+
+        private final CharSequence mLabel;
+
+        private LabelIndicatorStrategy(CharSequence label) {
+            mLabel = label;
+        }
+
+        public View createIndicatorView() {
+            final Context context = getContext();
+            LayoutInflater inflater =
+                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            View tabIndicator = inflater.inflate(mTabLayoutId,
+                    mTabWidget, // tab widget is the parent
+                    false); // no inflate params
+
+            final TextView tv = tabIndicator.findViewById(R.id.title);
+            tv.setText(mLabel);
+
+            if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
+                // Donut apps get old color scheme
+                tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
+                tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4));
+            }
+
+            return tabIndicator;
+        }
+    }
+
+    /**
+     * How we create a tab indicator that has a label and an icon
+     */
+    private class LabelAndIconIndicatorStrategy implements IndicatorStrategy {
+
+        private final CharSequence mLabel;
+        private final Drawable mIcon;
+
+        private LabelAndIconIndicatorStrategy(CharSequence label, Drawable icon) {
+            mLabel = label;
+            mIcon = icon;
+        }
+
+        public View createIndicatorView() {
+            final Context context = getContext();
+            LayoutInflater inflater =
+                    (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+            View tabIndicator = inflater.inflate(mTabLayoutId,
+                    mTabWidget, // tab widget is the parent
+                    false); // no inflate params
+
+            final TextView tv = tabIndicator.findViewById(R.id.title);
+            final ImageView iconView = tabIndicator.findViewById(R.id.icon);
+
+            // when icon is gone by default, we're in exclusive mode
+            final boolean exclusive = iconView.getVisibility() == View.GONE;
+            final boolean bindIcon = !exclusive || TextUtils.isEmpty(mLabel);
+
+            tv.setText(mLabel);
+
+            if (bindIcon && mIcon != null) {
+                iconView.setImageDrawable(mIcon);
+                iconView.setVisibility(VISIBLE);
+            }
+
+            if (context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT) {
+                // Donut apps get old color scheme
+                tabIndicator.setBackgroundResource(R.drawable.tab_indicator_v4);
+                tv.setTextColor(context.getColorStateList(R.color.tab_indicator_text_v4));
+            }
+
+            return tabIndicator;
+        }
+    }
+
+    /**
+     * How to create a tab indicator by specifying a view.
+     */
+    private class ViewIndicatorStrategy implements IndicatorStrategy {
+
+        private final View mView;
+
+        private ViewIndicatorStrategy(View view) {
+            mView = view;
+        }
+
+        public View createIndicatorView() {
+            return mView;
+        }
+    }
+
+    /**
+     * How to create the tab content via a view id.
+     */
+    private class ViewIdContentStrategy implements ContentStrategy {
+
+        private final View mView;
+
+        private ViewIdContentStrategy(int viewId) {
+            mView = mTabContent.findViewById(viewId);
+            if (mView != null) {
+                mView.setVisibility(View.GONE);
+            } else {
+                throw new RuntimeException("Could not create tab content because " +
+                        "could not find view with id " + viewId);
+            }
+        }
+
+        public View getContentView() {
+            mView.setVisibility(View.VISIBLE);
+            return mView;
+        }
+
+        public void tabClosed() {
+            mView.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * How tab content is managed using {@link TabContentFactory}.
+     */
+    private class FactoryContentStrategy implements ContentStrategy {
+        private View mTabContent;
+        private final CharSequence mTag;
+        private TabContentFactory mFactory;
+
+        public FactoryContentStrategy(CharSequence tag, TabContentFactory factory) {
+            mTag = tag;
+            mFactory = factory;
+        }
+
+        public View getContentView() {
+            if (mTabContent == null) {
+                mTabContent = mFactory.createTabContent(mTag.toString());
+            }
+            mTabContent.setVisibility(View.VISIBLE);
+            return mTabContent;
+        }
+
+        public void tabClosed() {
+            mTabContent.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * How tab content is managed via an {@link Intent}: the content view is the
+     * decorview of the launched activity.
+     */
+    private class IntentContentStrategy implements ContentStrategy {
+
+        private final String mTag;
+        private final Intent mIntent;
+
+        private View mLaunchedView;
+
+        private IntentContentStrategy(String tag, Intent intent) {
+            mTag = tag;
+            mIntent = intent;
+        }
+
+        public View getContentView() {
+            if (mLocalActivityManager == null) {
+                throw new IllegalStateException("Did you forget to call 'public void setup(LocalActivityManager activityGroup)'?");
+            }
+            final Window w = mLocalActivityManager.startActivity(
+                    mTag, mIntent);
+            final View wd = w != null ? w.getDecorView() : null;
+            if (mLaunchedView != wd && mLaunchedView != null) {
+                if (mLaunchedView.getParent() != null) {
+                    mTabContent.removeView(mLaunchedView);
+                }
+            }
+            mLaunchedView = wd;
+
+            // XXX Set FOCUS_AFTER_DESCENDANTS on embedded activities for now so they can get
+            // focus if none of their children have it. They need focus to be able to
+            // display menu items.
+            //
+            // Replace this with something better when Bug 628886 is fixed...
+            //
+            if (mLaunchedView != null) {
+                mLaunchedView.setVisibility(View.VISIBLE);
+                mLaunchedView.setFocusableInTouchMode(true);
+                ((ViewGroup) mLaunchedView).setDescendantFocusability(
+                        FOCUS_AFTER_DESCENDANTS);
+            }
+            return mLaunchedView;
+        }
+
+        public void tabClosed() {
+            if (mLaunchedView != null) {
+                mLaunchedView.setVisibility(View.GONE);
+            }
+        }
+    }
+
+}
diff --git a/android/widget/TabWidget.java b/android/widget/TabWidget.java
new file mode 100644
index 0000000..05f7c0a
--- /dev/null
+++ b/android/widget/TabWidget.java
@@ -0,0 +1,566 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.DrawableRes;
+import android.annotation.Nullable;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.Canvas;
+import android.graphics.Rect;
+import android.graphics.drawable.Drawable;
+import android.os.Build;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.View.OnFocusChangeListener;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.R;
+
+/**
+ *
+ * Displays a list of tab labels representing each page in the parent's tab
+ * collection.
+ * <p>
+ * The container object for this widget is {@link android.widget.TabHost TabHost}.
+ * When the user selects a tab, this object sends a message to the parent
+ * container, TabHost, to tell it to switch the displayed page. You typically
+ * won't use many methods directly on this object. The container TabHost is
+ * used to add labels, add the callback handler, and manage callbacks. You
+ * might call this object to iterate the list of tabs, or to tweak the layout
+ * of the tab list, but most methods should be called on the containing TabHost
+ * object.
+ *
+ * @attr ref android.R.styleable#TabWidget_divider
+ * @attr ref android.R.styleable#TabWidget_tabStripEnabled
+ * @attr ref android.R.styleable#TabWidget_tabStripLeft
+ * @attr ref android.R.styleable#TabWidget_tabStripRight
+ */
+public class TabWidget extends LinearLayout implements OnFocusChangeListener {
+    private final Rect mBounds = new Rect();
+
+    private OnTabSelectionChanged mSelectionChangedListener;
+
+    // This value will be set to 0 as soon as the first tab is added to TabHost.
+    private int mSelectedTab = -1;
+
+    private Drawable mLeftStrip;
+    private Drawable mRightStrip;
+
+    private boolean mDrawBottomStrips = true;
+    private boolean mStripMoved;
+
+    // When positive, the widths and heights of tabs will be imposed so that
+    // they fit in parent.
+    private int mImposedTabsHeight = -1;
+    private int[] mImposedTabWidths;
+
+    public TabWidget(Context context) {
+        this(context, null);
+    }
+
+    public TabWidget(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.tabWidgetStyle);
+    }
+
+    public TabWidget(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public TabWidget(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.TabWidget, defStyleAttr, defStyleRes);
+
+        mDrawBottomStrips = a.getBoolean(R.styleable.TabWidget_tabStripEnabled, mDrawBottomStrips);
+
+        // Tests the target SDK version, as set in the Manifest. Could not be
+        // set using styles.xml in a values-v? directory which targets the
+        // current platform SDK version instead.
+        final boolean isTargetSdkDonutOrLower =
+                context.getApplicationInfo().targetSdkVersion <= Build.VERSION_CODES.DONUT;
+
+        final boolean hasExplicitLeft = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripLeft);
+        if (hasExplicitLeft) {
+            mLeftStrip = a.getDrawable(R.styleable.TabWidget_tabStripLeft);
+        } else if (isTargetSdkDonutOrLower) {
+            mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left_v4);
+        } else {
+            mLeftStrip = context.getDrawable(R.drawable.tab_bottom_left);
+        }
+
+        final boolean hasExplicitRight = a.hasValueOrEmpty(R.styleable.TabWidget_tabStripRight);
+        if (hasExplicitRight) {
+            mRightStrip = a.getDrawable(R.styleable.TabWidget_tabStripRight);
+        } else if (isTargetSdkDonutOrLower) {
+            mRightStrip = context.getDrawable(R.drawable.tab_bottom_right_v4);
+        } else {
+            mRightStrip = context.getDrawable(R.drawable.tab_bottom_right);
+        }
+
+        a.recycle();
+
+        setChildrenDrawingOrderEnabled(true);
+    }
+
+    @Override
+    protected void onSizeChanged(int w, int h, int oldw, int oldh) {
+        mStripMoved = true;
+
+        super.onSizeChanged(w, h, oldw, oldh);
+    }
+
+    @Override
+    protected int getChildDrawingOrder(int childCount, int i) {
+        if (mSelectedTab == -1) {
+            return i;
+        } else {
+            // Always draw the selected tab last, so that drop shadows are drawn
+            // in the correct z-order.
+            if (i == childCount - 1) {
+                return mSelectedTab;
+            } else if (i >= mSelectedTab) {
+                return i + 1;
+            } else {
+                return i;
+            }
+        }
+    }
+
+    @Override
+    void measureChildBeforeLayout(View child, int childIndex, int widthMeasureSpec, int totalWidth,
+            int heightMeasureSpec, int totalHeight) {
+        if (!isMeasureWithLargestChildEnabled() && mImposedTabsHeight >= 0) {
+            widthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    totalWidth + mImposedTabWidths[childIndex], MeasureSpec.EXACTLY);
+            heightMeasureSpec = MeasureSpec.makeMeasureSpec(mImposedTabsHeight,
+                    MeasureSpec.EXACTLY);
+        }
+
+        super.measureChildBeforeLayout(child, childIndex,
+                widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
+    }
+
+    @Override
+    void measureHorizontal(int widthMeasureSpec, int heightMeasureSpec) {
+        if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.UNSPECIFIED) {
+            super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+            return;
+        }
+
+        // First, measure with no constraint
+        final int width = MeasureSpec.getSize(widthMeasureSpec);
+        final int unspecifiedWidth = MeasureSpec.makeSafeMeasureSpec(width,
+                MeasureSpec.UNSPECIFIED);
+        mImposedTabsHeight = -1;
+        super.measureHorizontal(unspecifiedWidth, heightMeasureSpec);
+
+        int extraWidth = getMeasuredWidth() - width;
+        if (extraWidth > 0) {
+            final int count = getChildCount();
+
+            int childCount = 0;
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                if (child.getVisibility() == GONE) continue;
+                childCount++;
+            }
+
+            if (childCount > 0) {
+                if (mImposedTabWidths == null || mImposedTabWidths.length != count) {
+                    mImposedTabWidths = new int[count];
+                }
+                for (int i = 0; i < count; i++) {
+                    final View child = getChildAt(i);
+                    if (child.getVisibility() == GONE) continue;
+                    final int childWidth = child.getMeasuredWidth();
+                    final int delta = extraWidth / childCount;
+                    final int newWidth = Math.max(0, childWidth - delta);
+                    mImposedTabWidths[i] = newWidth;
+                    // Make sure the extra width is evenly distributed, no int division remainder
+                    extraWidth -= childWidth - newWidth; // delta may have been clamped
+                    childCount--;
+                    mImposedTabsHeight = Math.max(mImposedTabsHeight, child.getMeasuredHeight());
+                }
+            }
+        }
+
+        // Measure again, this time with imposed tab widths and respecting
+        // initial spec request.
+        super.measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    /**
+     * Returns the tab indicator view at the given index.
+     *
+     * @param index the zero-based index of the tab indicator view to return
+     * @return the tab indicator view at the given index
+     */
+    public View getChildTabViewAt(int index) {
+        return getChildAt(index);
+    }
+
+    /**
+     * Returns the number of tab indicator views.
+     *
+     * @return the number of tab indicator views
+     */
+    public int getTabCount() {
+        return getChildCount();
+    }
+
+    /**
+     * Sets the drawable to use as a divider between the tab indicators.
+     *
+     * @param drawable the divider drawable
+     * @attr ref android.R.styleable#TabWidget_divider
+     */
+    @Override
+    public void setDividerDrawable(@Nullable Drawable drawable) {
+        super.setDividerDrawable(drawable);
+    }
+
+    /**
+     * Sets the drawable to use as a divider between the tab indicators.
+     *
+     * @param resId the resource identifier of the drawable to use as a divider
+     * @attr ref android.R.styleable#TabWidget_divider
+     */
+    public void setDividerDrawable(@DrawableRes int resId) {
+        setDividerDrawable(mContext.getDrawable(resId));
+    }
+
+    /**
+     * Sets the drawable to use as the left part of the strip below the tab
+     * indicators.
+     *
+     * @param drawable the left strip drawable
+     * @see #getLeftStripDrawable()
+     * @attr ref android.R.styleable#TabWidget_tabStripLeft
+     */
+    public void setLeftStripDrawable(@Nullable Drawable drawable) {
+        mLeftStrip = drawable;
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets the drawable to use as the left part of the strip below the tab
+     * indicators.
+     *
+     * @param resId the resource identifier of the drawable to use as the left
+     *              strip drawable
+     * @see #getLeftStripDrawable()
+     * @attr ref android.R.styleable#TabWidget_tabStripLeft
+     */
+    public void setLeftStripDrawable(@DrawableRes int resId) {
+        setLeftStripDrawable(mContext.getDrawable(resId));
+    }
+
+    /**
+     * @return the drawable used as the left part of the strip below the tab
+     *         indicators, may be {@code null}
+     * @see #setLeftStripDrawable(int)
+     * @see #setLeftStripDrawable(Drawable)
+     * @attr ref android.R.styleable#TabWidget_tabStripLeft
+     */
+    @Nullable
+    public Drawable getLeftStripDrawable() {
+        return mLeftStrip;
+    }
+
+    /**
+     * Sets the drawable to use as the right part of the strip below the tab
+     * indicators.
+     *
+     * @param drawable the right strip drawable
+     * @see #getRightStripDrawable()
+     * @attr ref android.R.styleable#TabWidget_tabStripRight
+     */
+    public void setRightStripDrawable(@Nullable Drawable drawable) {
+        mRightStrip = drawable;
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets the drawable to use as the right part of the strip below the tab
+     * indicators.
+     *
+     * @param resId the resource identifier of the drawable to use as the right
+     *              strip drawable
+     * @see #getRightStripDrawable()
+     * @attr ref android.R.styleable#TabWidget_tabStripRight
+     */
+    public void setRightStripDrawable(@DrawableRes int resId) {
+        setRightStripDrawable(mContext.getDrawable(resId));
+    }
+
+    /**
+     * @return the drawable used as the right part of the strip below the tab
+     *         indicators, may be {@code null}
+     * @see #setRightStripDrawable(int)
+     * @see #setRightStripDrawable(Drawable)
+     * @attr ref android.R.styleable#TabWidget_tabStripRight
+     */
+    @Nullable
+    public Drawable getRightStripDrawable() {
+        return mRightStrip;
+    }
+
+    /**
+     * Controls whether the bottom strips on the tab indicators are drawn or
+     * not.  The default is to draw them.  If the user specifies a custom
+     * view for the tab indicators, then the TabHost class calls this method
+     * to disable drawing of the bottom strips.
+     * @param stripEnabled true if the bottom strips should be drawn.
+     */
+    public void setStripEnabled(boolean stripEnabled) {
+        mDrawBottomStrips = stripEnabled;
+        invalidate();
+    }
+
+    /**
+     * Indicates whether the bottom strips on the tab indicators are drawn
+     * or not.
+     */
+    public boolean isStripEnabled() {
+        return mDrawBottomStrips;
+    }
+
+    @Override
+    public void childDrawableStateChanged(View child) {
+        if (getTabCount() > 0 && child == getChildTabViewAt(mSelectedTab)) {
+            // To make sure that the bottom strip is redrawn
+            invalidate();
+        }
+        super.childDrawableStateChanged(child);
+    }
+
+    @Override
+    public void dispatchDraw(Canvas canvas) {
+        super.dispatchDraw(canvas);
+
+        // Do nothing if there are no tabs.
+        if (getTabCount() == 0) return;
+
+        // If the user specified a custom view for the tab indicators, then
+        // do not draw the bottom strips.
+        if (!mDrawBottomStrips) {
+            // Skip drawing the bottom strips.
+            return;
+        }
+
+        final View selectedChild = getChildTabViewAt(mSelectedTab);
+
+        final Drawable leftStrip = mLeftStrip;
+        final Drawable rightStrip = mRightStrip;
+
+        leftStrip.setState(selectedChild.getDrawableState());
+        rightStrip.setState(selectedChild.getDrawableState());
+
+        if (mStripMoved) {
+            final Rect bounds = mBounds;
+            bounds.left = selectedChild.getLeft();
+            bounds.right = selectedChild.getRight();
+            final int myHeight = getHeight();
+            leftStrip.setBounds(Math.min(0, bounds.left - leftStrip.getIntrinsicWidth()),
+                    myHeight - leftStrip.getIntrinsicHeight(), bounds.left, myHeight);
+            rightStrip.setBounds(bounds.right, myHeight - rightStrip.getIntrinsicHeight(),
+                    Math.max(getWidth(), bounds.right + rightStrip.getIntrinsicWidth()), myHeight);
+            mStripMoved = false;
+        }
+
+        leftStrip.draw(canvas);
+        rightStrip.draw(canvas);
+    }
+
+    /**
+     * Sets the current tab.
+     * <p>
+     * This method is used to bring a tab to the front of the Widget,
+     * and is used to post to the rest of the UI that a different tab
+     * has been brought to the foreground.
+     * <p>
+     * Note, this is separate from the traditional "focus" that is
+     * employed from the view logic.
+     * <p>
+     * For instance, if we have a list in a tabbed view, a user may be
+     * navigating up and down the list, moving the UI focus (orange
+     * highlighting) through the list items.  The cursor movement does
+     * not effect the "selected" tab though, because what is being
+     * scrolled through is all on the same tab.  The selected tab only
+     * changes when we navigate between tabs (moving from the list view
+     * to the next tabbed view, in this example).
+     * <p>
+     * To move both the focus AND the selected tab at once, please use
+     * {@link #setCurrentTab}. Normally, the view logic takes care of
+     * adjusting the focus, so unless you're circumventing the UI,
+     * you'll probably just focus your interest here.
+     *
+     * @param index the index of the tab that you want to indicate as the
+     *              selected tab (tab brought to the front of the widget)
+     * @see #focusCurrentTab
+     */
+    public void setCurrentTab(int index) {
+        if (index < 0 || index >= getTabCount() || index == mSelectedTab) {
+            return;
+        }
+
+        if (mSelectedTab != -1) {
+            getChildTabViewAt(mSelectedTab).setSelected(false);
+        }
+        mSelectedTab = index;
+        getChildTabViewAt(mSelectedTab).setSelected(true);
+        mStripMoved = true;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TabWidget.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+        event.setItemCount(getTabCount());
+        event.setCurrentItemIndex(mSelectedTab);
+    }
+
+    /**
+     * Sets the current tab and focuses the UI on it.
+     * This method makes sure that the focused tab matches the selected
+     * tab, normally at {@link #setCurrentTab}.  Normally this would not
+     * be an issue if we go through the UI, since the UI is responsible
+     * for calling TabWidget.onFocusChanged(), but in the case where we
+     * are selecting the tab programmatically, we'll need to make sure
+     * focus keeps up.
+     *
+     *  @param index The tab that you want focused (highlighted in orange)
+     *  and selected (tab brought to the front of the widget)
+     *
+     *  @see #setCurrentTab
+     */
+    public void focusCurrentTab(int index) {
+        final int oldTab = mSelectedTab;
+
+        // set the tab
+        setCurrentTab(index);
+
+        // change the focus if applicable.
+        if (oldTab != index) {
+            getChildTabViewAt(index).requestFocus();
+        }
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+
+        final int count = getTabCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildTabViewAt(i);
+            child.setEnabled(enabled);
+        }
+    }
+
+    @Override
+    public void addView(View child) {
+        if (child.getLayoutParams() == null) {
+            final LinearLayout.LayoutParams lp = new LayoutParams(
+                    0, ViewGroup.LayoutParams.MATCH_PARENT, 1.0f);
+            lp.setMargins(0, 0, 0, 0);
+            child.setLayoutParams(lp);
+        }
+
+        // Ensure you can navigate to the tab with the keyboard, and you can touch it
+        child.setFocusable(true);
+        child.setClickable(true);
+
+        if (child.getPointerIcon() == null) {
+            child.setPointerIcon(PointerIcon.getSystemIcon(getContext(), PointerIcon.TYPE_HAND));
+        }
+
+        super.addView(child);
+
+        // TODO: detect this via geometry with a tabwidget listener rather
+        // than potentially interfere with the view's listener
+        child.setOnClickListener(new TabClickListener(getTabCount() - 1));
+    }
+
+    @Override
+    public void removeAllViews() {
+        super.removeAllViews();
+        mSelectedTab = -1;
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (!isEnabled()) {
+            return null;
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    /**
+     * Provides a way for {@link TabHost} to be notified that the user clicked
+     * on a tab indicator.
+     */
+    void setTabSelectionListener(OnTabSelectionChanged listener) {
+        mSelectionChangedListener = listener;
+    }
+
+    @Override
+    public void onFocusChange(View v, boolean hasFocus) {
+        // No-op. Tab selection is separate from keyboard focus.
+    }
+
+    // registered with each tab indicator so we can notify tab host
+    private class TabClickListener implements OnClickListener {
+        private final int mTabIndex;
+
+        private TabClickListener(int tabIndex) {
+            mTabIndex = tabIndex;
+        }
+
+        public void onClick(View v) {
+            mSelectionChangedListener.onTabSelectionChanged(mTabIndex, true);
+        }
+    }
+
+    /**
+     * Lets {@link TabHost} know that the user clicked on a tab indicator.
+     */
+    interface OnTabSelectionChanged {
+        /**
+         * Informs the TabHost which tab was selected. It also indicates
+         * if the tab was clicked/pressed or just focused into.
+         *
+         * @param tabIndex index of the tab that was selected
+         * @param clicked whether the selection changed due to a touch/click or
+         *                due to focus entering the tab through navigation.
+         *                {@code true} if it was due to a press/click and
+         *                {@code false} otherwise.
+         */
+        void onTabSelectionChanged(int tabIndex, boolean clicked);
+    }
+}
diff --git a/android/widget/TableLayout.java b/android/widget/TableLayout.java
new file mode 100644
index 0000000..8bb4d16
--- /dev/null
+++ b/android/widget/TableLayout.java
@@ -0,0 +1,780 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.SparseBooleanArray;
+import android.view.View;
+import android.view.ViewGroup;
+
+import com.android.internal.R;
+
+import java.util.regex.Pattern;
+
+/**
+ * <p>A layout that arranges its children into rows and columns.
+ * A TableLayout consists of a number of {@link android.widget.TableRow} objects,
+ * each defining a row (actually, you can have other children, which will be
+ * explained below). TableLayout containers do not display border lines for
+ * their rows, columns, or cells. Each row has zero or more cells; each cell can
+ * hold one {@link android.view.View View} object. The table has as many columns
+ * as the row with the most cells. A table can leave cells empty. Cells can span
+ * columns, as they can in HTML.</p>
+ *
+ * <p>The width of a column is defined by the row with the widest cell in that
+ * column. However, a TableLayout can specify certain columns as shrinkable or
+ * stretchable by calling
+ * {@link #setColumnShrinkable(int, boolean) setColumnShrinkable()}
+ * or {@link #setColumnStretchable(int, boolean) setColumnStretchable()}. If
+ * marked as shrinkable, the column width can be shrunk to fit the table into
+ * its parent object. If marked as stretchable, it can expand in width to fit
+ * any extra space. The total width of the table is defined by its parent
+ * container. It is important to remember that a column can be both shrinkable
+ * and stretchable. In such a situation, the column will change its size to
+ * always use up the available space, but never more. Finally, you can hide a
+ * column by calling
+ * {@link #setColumnCollapsed(int,boolean) setColumnCollapsed()}.</p>
+ *
+ * <p>The children of a TableLayout cannot specify the <code>layout_width</code>
+ * attribute. Width is always <code>MATCH_PARENT</code>. However, the
+ * <code>layout_height</code> attribute can be defined by a child; default value
+ * is {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}. If the child
+ * is a {@link android.widget.TableRow}, then the height is always
+ * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
+ *
+ * <p> Cells must be added to a row in increasing column order, both in code and
+ * XML. Column numbers are zero-based. If you don't specify a column number for
+ * a child cell, it will autoincrement to the next available column. If you skip
+ * a column number, it will be considered an empty cell in that row. See the
+ * TableLayout examples in ApiDemos for examples of creating tables in XML.</p>
+ *
+ * <p>Although the typical child of a TableLayout is a TableRow, you can
+ * actually use any View subclass as a direct child of TableLayout. The View
+ * will be displayed as a single row that spans all the table columns.</p>
+ *
+ */
+public class TableLayout extends LinearLayout {
+    private int[] mMaxWidths;
+    private SparseBooleanArray mStretchableColumns;
+    private SparseBooleanArray mShrinkableColumns;
+    private SparseBooleanArray mCollapsedColumns;
+
+    private boolean mShrinkAllColumns;
+    private boolean mStretchAllColumns;
+
+    private TableLayout.PassThroughHierarchyChangeListener mPassThroughListener;
+
+    private boolean mInitialized;
+
+    /**
+     * <p>Creates a new TableLayout for the given context.</p>
+     *
+     * @param context the application environment
+     */
+    public TableLayout(Context context) {
+        super(context);
+        initTableLayout();
+    }
+
+    /**
+     * <p>Creates a new TableLayout for the given context and with the
+     * specified set attributes.</p>
+     *
+     * @param context the application environment
+     * @param attrs a collection of attributes
+     */
+    public TableLayout(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.TableLayout);
+
+        String stretchedColumns = a.getString(R.styleable.TableLayout_stretchColumns);
+        if (stretchedColumns != null) {
+            if (stretchedColumns.charAt(0) == '*') {
+                mStretchAllColumns = true;
+            } else {
+                mStretchableColumns = parseColumns(stretchedColumns);
+            }
+        }
+
+        String shrinkedColumns = a.getString(R.styleable.TableLayout_shrinkColumns);
+        if (shrinkedColumns != null) {
+            if (shrinkedColumns.charAt(0) == '*') {
+                mShrinkAllColumns = true;
+            } else {
+                mShrinkableColumns = parseColumns(shrinkedColumns);
+            }
+        }
+
+        String collapsedColumns = a.getString(R.styleable.TableLayout_collapseColumns);
+        if (collapsedColumns != null) {
+            mCollapsedColumns = parseColumns(collapsedColumns);
+        }
+
+        a.recycle();
+        initTableLayout();
+    }
+
+    /**
+     * <p>Parses a sequence of columns ids defined in a CharSequence with the
+     * following pattern (regex): \d+(\s*,\s*\d+)*</p>
+     *
+     * <p>Examples: "1" or "13, 7, 6" or "".</p>
+     *
+     * <p>The result of the parsing is stored in a sparse boolean array. The
+     * parsed column ids are used as the keys of the sparse array. The values
+     * are always true.</p>
+     *
+     * @param sequence a sequence of column ids, can be empty but not null
+     * @return a sparse array of boolean mapping column indexes to the columns
+     *         collapse state
+     */
+    private static SparseBooleanArray parseColumns(String sequence) {
+        SparseBooleanArray columns = new SparseBooleanArray();
+        Pattern pattern = Pattern.compile("\\s*,\\s*");
+        String[] columnDefs = pattern.split(sequence);
+
+        for (String columnIdentifier : columnDefs) {
+            try {
+                int columnIndex = Integer.parseInt(columnIdentifier);
+                // only valid, i.e. positive, columns indexes are handled
+                if (columnIndex >= 0) {
+                    // putting true in this sparse array indicates that the
+                    // column index was defined in the XML file
+                    columns.put(columnIndex, true);
+                }
+            } catch (NumberFormatException e) {
+                // we just ignore columns that don't exist
+            }
+        }
+
+        return columns;
+    }
+
+    /**
+     * <p>Performs initialization common to prorgrammatic use and XML use of
+     * this widget.</p>
+     */
+    private void initTableLayout() {
+        if (mCollapsedColumns == null) {
+            mCollapsedColumns = new SparseBooleanArray();
+        }
+        if (mStretchableColumns == null) {
+            mStretchableColumns = new SparseBooleanArray();
+        }
+        if (mShrinkableColumns == null) {
+            mShrinkableColumns = new SparseBooleanArray();
+        }
+
+        // TableLayouts are always in vertical orientation; keep this tracked
+        // for shared LinearLayout code.
+        setOrientation(VERTICAL);
+
+        mPassThroughListener = new PassThroughHierarchyChangeListener();
+        // make sure to call the parent class method to avoid potential
+        // infinite loops
+        super.setOnHierarchyChangeListener(mPassThroughListener);
+
+        mInitialized = true;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setOnHierarchyChangeListener(
+            OnHierarchyChangeListener listener) {
+        // the user listener is delegated to our pass-through listener
+        mPassThroughListener.mOnHierarchyChangeListener = listener;
+    }
+
+    private void requestRowsLayout() {
+        if (mInitialized) {
+            final int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                getChildAt(i).requestLayout();
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void requestLayout() {
+        if (mInitialized) {
+            int count = getChildCount();
+            for (int i = 0; i < count; i++) {
+                getChildAt(i).forceLayout();
+            }
+        }
+
+        super.requestLayout();
+    }
+
+    /**
+     * <p>Indicates whether all columns are shrinkable or not.</p>
+     *
+     * @return true if all columns are shrinkable, false otherwise
+     *
+     * @attr ref android.R.styleable#TableLayout_shrinkColumns
+     */
+    public boolean isShrinkAllColumns() {
+        return mShrinkAllColumns;
+    }
+
+    /**
+     * <p>Convenience method to mark all columns as shrinkable.</p>
+     *
+     * @param shrinkAllColumns true to mark all columns shrinkable
+     *
+     * @attr ref android.R.styleable#TableLayout_shrinkColumns
+     */
+    public void setShrinkAllColumns(boolean shrinkAllColumns) {
+        mShrinkAllColumns = shrinkAllColumns;
+    }
+
+    /**
+     * <p>Indicates whether all columns are stretchable or not.</p>
+     *
+     * @return true if all columns are stretchable, false otherwise
+     *
+     * @attr ref android.R.styleable#TableLayout_stretchColumns
+     */
+    public boolean isStretchAllColumns() {
+        return mStretchAllColumns;
+    }
+
+    /**
+     * <p>Convenience method to mark all columns as stretchable.</p>
+     *
+     * @param stretchAllColumns true to mark all columns stretchable
+     *
+     * @attr ref android.R.styleable#TableLayout_stretchColumns
+     */
+    public void setStretchAllColumns(boolean stretchAllColumns) {
+        mStretchAllColumns = stretchAllColumns;
+    }
+
+    /**
+     * <p>Collapses or restores a given column. When collapsed, a column
+     * does not appear on screen and the extra space is reclaimed by the
+     * other columns. A column is collapsed/restored only when it belongs to
+     * a {@link android.widget.TableRow}.</p>
+     *
+     * <p>Calling this method requests a layout operation.</p>
+     *
+     * @param columnIndex the index of the column
+     * @param isCollapsed true if the column must be collapsed, false otherwise
+     *
+     * @attr ref android.R.styleable#TableLayout_collapseColumns
+     */
+    public void setColumnCollapsed(int columnIndex, boolean isCollapsed) {
+        // update the collapse status of the column
+        mCollapsedColumns.put(columnIndex, isCollapsed);
+
+        int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View view = getChildAt(i);
+            if (view instanceof TableRow) {
+                ((TableRow) view).setColumnCollapsed(columnIndex, isCollapsed);
+            }
+        }
+
+        requestRowsLayout();
+    }
+
+    /**
+     * <p>Returns the collapsed state of the specified column.</p>
+     *
+     * @param columnIndex the index of the column
+     * @return true if the column is collapsed, false otherwise
+     */
+    public boolean isColumnCollapsed(int columnIndex) {
+        return mCollapsedColumns.get(columnIndex);
+    }
+
+    /**
+     * <p>Makes the given column stretchable or not. When stretchable, a column
+     * takes up as much as available space as possible in its row.</p>
+     *
+     * <p>Calling this method requests a layout operation.</p>
+     *
+     * @param columnIndex the index of the column
+     * @param isStretchable true if the column must be stretchable,
+     *                      false otherwise. Default is false.
+     *
+     * @attr ref android.R.styleable#TableLayout_stretchColumns
+     */
+    public void setColumnStretchable(int columnIndex, boolean isStretchable) {
+        mStretchableColumns.put(columnIndex, isStretchable);
+        requestRowsLayout();
+    }
+
+    /**
+     * <p>Returns whether the specified column is stretchable or not.</p>
+     *
+     * @param columnIndex the index of the column
+     * @return true if the column is stretchable, false otherwise
+     */
+    public boolean isColumnStretchable(int columnIndex) {
+        return mStretchAllColumns || mStretchableColumns.get(columnIndex);
+    }
+
+    /**
+     * <p>Makes the given column shrinkable or not. When a row is too wide, the
+     * table can reclaim extra space from shrinkable columns.</p>
+     *
+     * <p>Calling this method requests a layout operation.</p>
+     *
+     * @param columnIndex the index of the column
+     * @param isShrinkable true if the column must be shrinkable,
+     *                     false otherwise. Default is false.
+     *
+     * @attr ref android.R.styleable#TableLayout_shrinkColumns
+     */
+    public void setColumnShrinkable(int columnIndex, boolean isShrinkable) {
+        mShrinkableColumns.put(columnIndex, isShrinkable);
+        requestRowsLayout();
+    }
+
+    /**
+     * <p>Returns whether the specified column is shrinkable or not.</p>
+     *
+     * @param columnIndex the index of the column
+     * @return true if the column is shrinkable, false otherwise. Default is false.
+     */
+    public boolean isColumnShrinkable(int columnIndex) {
+        return mShrinkAllColumns || mShrinkableColumns.get(columnIndex);
+    }
+
+    /**
+     * <p>Applies the columns collapse status to a new row added to this
+     * table. This method is invoked by PassThroughHierarchyChangeListener
+     * upon child insertion.</p>
+     *
+     * <p>This method only applies to {@link android.widget.TableRow}
+     * instances.</p>
+     *
+     * @param child the newly added child
+     */
+    private void trackCollapsedColumns(View child) {
+        if (child instanceof TableRow) {
+            final TableRow row = (TableRow) child;
+            final SparseBooleanArray collapsedColumns = mCollapsedColumns;
+            final int count = collapsedColumns.size();
+            for (int i = 0; i < count; i++) {
+                int columnIndex = collapsedColumns.keyAt(i);
+                boolean isCollapsed = collapsedColumns.valueAt(i);
+                // the collapse status is set only when the column should be
+                // collapsed; otherwise, this might affect the default
+                // visibility of the row's children
+                if (isCollapsed) {
+                    row.setColumnCollapsed(columnIndex, isCollapsed);
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addView(View child) {
+        super.addView(child);
+        requestRowsLayout();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addView(View child, int index) {
+        super.addView(child, index);
+        requestRowsLayout();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addView(View child, ViewGroup.LayoutParams params) {
+        super.addView(child, params);
+        requestRowsLayout();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        super.addView(child, index, params);
+        requestRowsLayout();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // enforce vertical layout
+        measureVertical(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        // enforce vertical layout
+        layoutVertical(l, t, r, b);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void measureChildBeforeLayout(View child, int childIndex,
+            int widthMeasureSpec, int totalWidth,
+            int heightMeasureSpec, int totalHeight) {
+        // when the measured child is a table row, we force the width of its
+        // children with the widths computed in findLargestCells()
+        if (child instanceof TableRow) {
+            ((TableRow) child).setColumnsWidthConstraints(mMaxWidths);
+        }
+
+        super.measureChildBeforeLayout(child, childIndex,
+                widthMeasureSpec, totalWidth, heightMeasureSpec, totalHeight);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void measureVertical(int widthMeasureSpec, int heightMeasureSpec) {
+        findLargestCells(widthMeasureSpec, heightMeasureSpec);
+        shrinkAndStretchColumns(widthMeasureSpec);
+
+        super.measureVertical(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    /**
+     * <p>Finds the largest cell in each column. For each column, the width of
+     * the largest cell is applied to all the other cells.</p>
+     *
+     * @param widthMeasureSpec the measure constraint imposed by our parent
+     */
+    private void findLargestCells(int widthMeasureSpec, int heightMeasureSpec) {
+        boolean firstRow = true;
+
+        // find the maximum width for each column
+        // the total number of columns is dynamically changed if we find
+        // wider rows as we go through the children
+        // the array is reused for each layout operation; the array can grow
+        // but never shrinks. Unused extra cells in the array are just ignored
+        // this behavior avoids to unnecessary grow the array after the first
+        // layout operation
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (child.getVisibility() == GONE) {
+                continue;
+            }
+
+            if (child instanceof TableRow) {
+                final TableRow row = (TableRow) child;
+                // forces the row's height
+                final ViewGroup.LayoutParams layoutParams = row.getLayoutParams();
+                layoutParams.height = LayoutParams.WRAP_CONTENT;
+
+                final int[] widths = row.getColumnsWidths(widthMeasureSpec, heightMeasureSpec);
+                final int newLength = widths.length;
+                // this is the first row, we just need to copy the values
+                if (firstRow) {
+                    if (mMaxWidths == null || mMaxWidths.length != newLength) {
+                        mMaxWidths = new int[newLength];
+                    }
+                    System.arraycopy(widths, 0, mMaxWidths, 0, newLength);
+                    firstRow = false;
+                } else {
+                    int length = mMaxWidths.length;
+                    final int difference = newLength - length;
+                    // the current row is wider than the previous rows, so
+                    // we just grow the array and copy the values
+                    if (difference > 0) {
+                        final int[] oldMaxWidths = mMaxWidths;
+                        mMaxWidths = new int[newLength];
+                        System.arraycopy(oldMaxWidths, 0, mMaxWidths, 0,
+                                oldMaxWidths.length);
+                        System.arraycopy(widths, oldMaxWidths.length,
+                                mMaxWidths, oldMaxWidths.length, difference);
+                    }
+
+                    // the row is narrower or of the same width as the previous
+                    // rows, so we find the maximum width for each column
+                    // if the row is narrower than the previous ones,
+                    // difference will be negative
+                    final int[] maxWidths = mMaxWidths;
+                    length = Math.min(length, newLength);
+                    for (int j = 0; j < length; j++) {
+                        maxWidths[j] = Math.max(maxWidths[j], widths[j]);
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * <p>Shrinks the columns if their total width is greater than the
+     * width allocated by widthMeasureSpec. When the total width is less
+     * than the allocated width, this method attempts to stretch columns
+     * to fill the remaining space.</p>
+     *
+     * @param widthMeasureSpec the width measure specification as indicated
+     *                         by this widget's parent
+     */
+    private void shrinkAndStretchColumns(int widthMeasureSpec) {
+        // when we have no row, mMaxWidths is not initialized and the loop
+        // below could cause a NPE
+        if (mMaxWidths == null) {
+            return;
+        }
+
+        // should we honor AT_MOST, EXACTLY and UNSPECIFIED?
+        int totalWidth = 0;
+        for (int width : mMaxWidths) {
+            totalWidth += width;
+        }
+
+        int size = MeasureSpec.getSize(widthMeasureSpec) - mPaddingLeft - mPaddingRight;
+
+        if ((totalWidth > size) && (mShrinkAllColumns || mShrinkableColumns.size() > 0)) {
+            // oops, the largest columns are wider than the row itself
+            // fairly redistribute the row's width among the columns
+            mutateColumnsWidth(mShrinkableColumns, mShrinkAllColumns, size, totalWidth);
+        } else if ((totalWidth < size) && (mStretchAllColumns || mStretchableColumns.size() > 0)) {
+            // if we have some space left, we distribute it among the
+            // expandable columns
+            mutateColumnsWidth(mStretchableColumns, mStretchAllColumns, size, totalWidth);
+        }
+    }
+
+    private void mutateColumnsWidth(SparseBooleanArray columns,
+            boolean allColumns, int size, int totalWidth) {
+        int skipped = 0;
+        final int[] maxWidths = mMaxWidths;
+        final int length = maxWidths.length;
+        final int count = allColumns ? length : columns.size();
+        final int totalExtraSpace = size - totalWidth;
+        int extraSpace = totalExtraSpace / count;
+
+        // Column's widths are changed: force child table rows to re-measure.
+        // (done by super.measureVertical after shrinkAndStretchColumns.)
+        final int nbChildren = getChildCount();
+        for (int i = 0; i < nbChildren; i++) {
+            View child = getChildAt(i);
+            if (child instanceof TableRow) {
+                child.forceLayout();
+            }
+        }
+
+        if (!allColumns) {
+            for (int i = 0; i < count; i++) {
+                int column = columns.keyAt(i);
+                if (columns.valueAt(i)) {
+                    if (column < length) {
+                        maxWidths[column] += extraSpace;
+                    } else {
+                        skipped++;
+                    }
+                }
+            }
+        } else {
+            for (int i = 0; i < count; i++) {
+                maxWidths[i] += extraSpace;
+            }
+
+            // we don't skip any column so we can return right away
+            return;
+        }
+
+        if (skipped > 0 && skipped < count) {
+            // reclaim any extra space we left to columns that don't exist
+            extraSpace = skipped * extraSpace / (count - skipped);
+            for (int i = 0; i < count; i++) {
+                int column = columns.keyAt(i);
+                if (columns.valueAt(i) && column < length) {
+                    if (extraSpace > maxWidths[column]) {
+                        maxWidths[column] = 0;
+                    } else {
+                        maxWidths[column] += extraSpace;
+                    }
+                }
+            }
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new TableLayout.LayoutParams(getContext(), attrs);
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
+     * and a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.
+     */
+    @Override
+    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof TableLayout.LayoutParams;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TableLayout.class.getName();
+    }
+
+    /**
+     * <p>This set of layout parameters enforces the width of each child to be
+     * {@link #MATCH_PARENT} and the height of each child to be
+     * {@link #WRAP_CONTENT}, but only if the height is not specified.</p>
+     */
+    @SuppressWarnings({"UnusedDeclaration"})
+    public static class LayoutParams extends LinearLayout.LayoutParams {
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int w, int h) {
+            super(MATCH_PARENT, h);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(int w, int h, float initWeight) {
+            super(MATCH_PARENT, h, initWeight);
+        }
+
+        /**
+         * <p>Sets the child width to
+         * {@link android.view.ViewGroup.LayoutParams} and the child height to
+         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+         */
+        public LayoutParams() {
+            super(MATCH_PARENT, WRAP_CONTENT);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams p) {
+            super(p);
+            width = MATCH_PARENT;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+            width = MATCH_PARENT;
+            if (source instanceof TableLayout.LayoutParams) {
+                weight = ((TableLayout.LayoutParams) source).weight;
+            }
+        }
+
+        /**
+         * <p>Fixes the row's width to
+         * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}; the row's
+         * height is fixed to
+         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} if no layout
+         * height is specified.</p>
+         *
+         * @param a the styled attributes set
+         * @param widthAttr the width attribute to fetch
+         * @param heightAttr the height attribute to fetch
+         */
+        @Override
+        protected void setBaseAttributes(TypedArray a,
+                int widthAttr, int heightAttr) {
+            this.width = MATCH_PARENT;
+            if (a.hasValue(heightAttr)) {
+                this.height = a.getLayoutDimension(heightAttr, "layout_height");
+            } else {
+                this.height = WRAP_CONTENT;
+            }
+        }
+    }
+
+    /**
+     * <p>A pass-through listener acts upon the events and dispatches them
+     * to another listener. This allows the table layout to set its own internal
+     * hierarchy change listener without preventing the user to setup his.</p>
+     */
+    private class PassThroughHierarchyChangeListener implements
+            OnHierarchyChangeListener {
+        private OnHierarchyChangeListener mOnHierarchyChangeListener;
+
+        /**
+         * {@inheritDoc}
+         */
+        public void onChildViewAdded(View parent, View child) {
+            trackCollapsedColumns(child);
+
+            if (mOnHierarchyChangeListener != null) {
+                mOnHierarchyChangeListener.onChildViewAdded(parent, child);
+            }
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public void onChildViewRemoved(View parent, View child) {
+            if (mOnHierarchyChangeListener != null) {
+                mOnHierarchyChangeListener.onChildViewRemoved(parent, child);
+            }
+        }
+    }
+}
diff --git a/android/widget/TableRow.java b/android/widget/TableRow.java
new file mode 100644
index 0000000..22931fc
--- /dev/null
+++ b/android/widget/TableRow.java
@@ -0,0 +1,549 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.NonNull;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.util.SparseIntArray;
+import android.view.Gravity;
+import android.view.View;
+import android.view.ViewDebug;
+import android.view.ViewGroup;
+import android.view.ViewHierarchyEncoder;
+
+/**
+ * <p>A layout that arranges its children horizontally. A TableRow should
+ * always be used as a child of a {@link android.widget.TableLayout}. If a
+ * TableRow's parent is not a TableLayout, the TableRow will behave as
+ * an horizontal {@link android.widget.LinearLayout}.</p>
+ *
+ * <p>The children of a TableRow do not need to specify the
+ * <code>layout_width</code> and <code>layout_height</code> attributes in the
+ * XML file. TableRow always enforces those values to be respectively
+ * {@link android.widget.TableLayout.LayoutParams#MATCH_PARENT} and
+ * {@link android.widget.TableLayout.LayoutParams#WRAP_CONTENT}.</p>
+ *
+ * <p>
+ * Also see {@link TableRow.LayoutParams android.widget.TableRow.LayoutParams}
+ * for layout attributes </p>
+ */
+public class TableRow extends LinearLayout {
+    private int mNumColumns = 0;
+    private int[] mColumnWidths;
+    private int[] mConstrainedColumnWidths;
+    private SparseIntArray mColumnToChildIndex;
+
+    private ChildrenTracker mChildrenTracker;
+
+    /**
+     * <p>Creates a new TableRow for the given context.</p>
+     *
+     * @param context the application environment
+     */
+    public TableRow(Context context) {
+        super(context);
+        initTableRow();
+    }
+
+    /**
+     * <p>Creates a new TableRow for the given context and with the
+     * specified set attributes.</p>
+     *
+     * @param context the application environment
+     * @param attrs a collection of attributes
+     */
+    public TableRow(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        initTableRow();
+    }
+
+    private void initTableRow() {
+        OnHierarchyChangeListener oldListener = mOnHierarchyChangeListener;
+        mChildrenTracker = new ChildrenTracker();
+        if (oldListener != null) {
+            mChildrenTracker.setOnHierarchyChangeListener(oldListener);
+        }
+        super.setOnHierarchyChangeListener(mChildrenTracker);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+        mChildrenTracker.setOnHierarchyChangeListener(listener);
+    }
+
+    /**
+     * <p>Collapses or restores a given column.</p>
+     *
+     * @param columnIndex the index of the column
+     * @param collapsed true if the column must be collapsed, false otherwise
+     * {@hide}
+     */
+    void setColumnCollapsed(int columnIndex, boolean collapsed) {
+        final View child = getVirtualChildAt(columnIndex);
+        if (child != null) {
+            child.setVisibility(collapsed ? GONE : VISIBLE);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        // enforce horizontal layout
+        measureHorizontal(widthMeasureSpec, heightMeasureSpec);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        // enforce horizontal layout
+        layoutHorizontal(l, t, r, b);
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public View getVirtualChildAt(int i) {
+        if (mColumnToChildIndex == null) {
+            mapIndexAndColumns();
+        }
+
+        final int deflectedIndex = mColumnToChildIndex.get(i, -1);
+        if (deflectedIndex != -1) {
+            return getChildAt(deflectedIndex);
+        }
+
+        return null;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public int getVirtualChildCount() {
+        if (mColumnToChildIndex == null) {
+            mapIndexAndColumns();
+        }
+        return mNumColumns;
+    }
+
+    private void mapIndexAndColumns() {
+        if (mColumnToChildIndex == null) {
+            int virtualCount = 0;
+            final int count = getChildCount();
+
+            mColumnToChildIndex = new SparseIntArray();
+            final SparseIntArray columnToChild = mColumnToChildIndex;
+
+            for (int i = 0; i < count; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+
+                if (layoutParams.column >= virtualCount) {
+                    virtualCount = layoutParams.column;
+                }
+
+                for (int j = 0; j < layoutParams.span; j++) {
+                    columnToChild.put(virtualCount++, i);
+                }
+            }
+
+            mNumColumns = virtualCount;
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    int measureNullChild(int childIndex) {
+        return mConstrainedColumnWidths[childIndex];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    void measureChildBeforeLayout(View child, int childIndex,
+            int widthMeasureSpec, int totalWidth,
+            int heightMeasureSpec, int totalHeight) {
+        if (mConstrainedColumnWidths != null) {
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+
+            int measureMode = MeasureSpec.EXACTLY;
+            int columnWidth = 0;
+
+            final int span = lp.span;
+            final int[] constrainedColumnWidths = mConstrainedColumnWidths;
+            for (int i = 0; i < span; i++) {
+                columnWidth += constrainedColumnWidths[childIndex + i];
+            }
+
+            final int gravity = lp.gravity;
+            final boolean isHorizontalGravity = Gravity.isHorizontal(gravity);
+
+            if (isHorizontalGravity) {
+                measureMode = MeasureSpec.AT_MOST;
+            }
+
+            // no need to care about padding here,
+            // ViewGroup.getChildMeasureSpec() would get rid of it anyway
+            // because of the EXACTLY measure spec we use
+            int childWidthMeasureSpec = MeasureSpec.makeMeasureSpec(
+                    Math.max(0, columnWidth - lp.leftMargin - lp.rightMargin), measureMode
+            );
+            int childHeightMeasureSpec = getChildMeasureSpec(heightMeasureSpec,
+                    mPaddingTop + mPaddingBottom + lp.topMargin +
+                    lp .bottomMargin + totalHeight, lp.height);
+
+            child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+
+            if (isHorizontalGravity) {
+                final int childWidth = child.getMeasuredWidth();
+                lp.mOffset[LayoutParams.LOCATION_NEXT] = columnWidth - childWidth;
+
+                final int layoutDirection = getLayoutDirection();
+                final int absoluteGravity = Gravity.getAbsoluteGravity(gravity, layoutDirection);
+                switch (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) {
+                    case Gravity.LEFT:
+                        // don't offset on X axis
+                        break;
+                    case Gravity.RIGHT:
+                        lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT];
+                        break;
+                    case Gravity.CENTER_HORIZONTAL:
+                        lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] / 2;
+                        break;
+                }
+            } else {
+                lp.mOffset[LayoutParams.LOCATION] = lp.mOffset[LayoutParams.LOCATION_NEXT] = 0;
+            }
+        } else {
+            // fail silently when column widths are not available
+            super.measureChildBeforeLayout(child, childIndex, widthMeasureSpec,
+                    totalWidth, heightMeasureSpec, totalHeight);
+        }
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    int getChildrenSkipCount(View child, int index) {
+        LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+
+        // when the span is 1 (default), we need to skip 0 child
+        return layoutParams.span - 1;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    int getLocationOffset(View child) {
+        return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION];
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    int getNextLocationOffset(View child) {
+        return ((TableRow.LayoutParams) child.getLayoutParams()).mOffset[LayoutParams.LOCATION_NEXT];
+    }
+
+    /**
+     * <p>Measures the preferred width of each child, including its margins.</p>
+     *
+     * @param widthMeasureSpec the width constraint imposed by our parent
+     *
+     * @return an array of integers corresponding to the width of each cell, or
+     *         column, in this row
+     * {@hide}
+     */
+    int[] getColumnsWidths(int widthMeasureSpec, int heightMeasureSpec) {
+        final int numColumns = getVirtualChildCount();
+        if (mColumnWidths == null || numColumns != mColumnWidths.length) {
+            mColumnWidths = new int[numColumns];
+        }
+
+        final int[] columnWidths = mColumnWidths;
+
+        for (int i = 0; i < numColumns; i++) {
+            final View child = getVirtualChildAt(i);
+            if (child != null && child.getVisibility() != GONE) {
+                final LayoutParams layoutParams = (LayoutParams) child.getLayoutParams();
+                if (layoutParams.span == 1) {
+                    int spec;
+                    switch (layoutParams.width) {
+                        case LayoutParams.WRAP_CONTENT:
+                            spec = getChildMeasureSpec(widthMeasureSpec, 0, LayoutParams.WRAP_CONTENT);
+                            break;
+                        case LayoutParams.MATCH_PARENT:
+                            spec = MeasureSpec.makeSafeMeasureSpec(
+                                    MeasureSpec.getSize(heightMeasureSpec),
+                                    MeasureSpec.UNSPECIFIED);
+                            break;
+                        default:
+                            spec = MeasureSpec.makeMeasureSpec(layoutParams.width, MeasureSpec.EXACTLY);
+                    }
+                    child.measure(spec, spec);
+
+                    final int width = child.getMeasuredWidth() + layoutParams.leftMargin +
+                            layoutParams.rightMargin;
+                    columnWidths[i] = width;
+                } else {
+                    columnWidths[i] = 0;
+                }
+            } else {
+                columnWidths[i] = 0;
+            }
+        }
+
+        return columnWidths;
+    }
+
+    /**
+     * <p>Sets the width of all of the columns in this row. At layout time,
+     * this row sets a fixed width, as defined by <code>columnWidths</code>,
+     * on each child (or cell, or column.)</p>
+     *
+     * @param columnWidths the fixed width of each column that this row must
+     *                     honor
+     * @throws IllegalArgumentException when columnWidths' length is smaller
+     *         than the number of children in this row
+     * {@hide}
+     */
+    void setColumnsWidthConstraints(int[] columnWidths) {
+        if (columnWidths == null || columnWidths.length < getVirtualChildCount()) {
+            throw new IllegalArgumentException(
+                    "columnWidths should be >= getVirtualChildCount()");
+        }
+
+        mConstrainedColumnWidths = columnWidths;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new TableRow.LayoutParams(getContext(), attrs);
+    }
+
+    /**
+     * Returns a set of layout parameters with a width of
+     * {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT},
+     * a height of {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT} and no spanning.
+     */
+    @Override
+    protected LinearLayout.LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams();
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return p instanceof TableRow.LayoutParams;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    protected LinearLayout.LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        return new LayoutParams(p);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TableRow.class.getName();
+    }
+
+    /**
+     * <p>Set of layout parameters used in table rows.</p>
+     *
+     * @see android.widget.TableLayout.LayoutParams
+     * 
+     * @attr ref android.R.styleable#TableRow_Cell_layout_column
+     * @attr ref android.R.styleable#TableRow_Cell_layout_span
+     */
+    public static class LayoutParams extends LinearLayout.LayoutParams {
+        /**
+         * <p>The column index of the cell represented by the widget.</p>
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int column;
+
+        /**
+         * <p>The number of columns the widgets spans over.</p>
+         */
+        @ViewDebug.ExportedProperty(category = "layout")
+        public int span;
+
+        private static final int LOCATION = 0;
+        private static final int LOCATION_NEXT = 1;
+
+        private int[] mOffset = new int[2];
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(Context c, AttributeSet attrs) {
+            super(c, attrs);
+
+            TypedArray a =
+                    c.obtainStyledAttributes(attrs,
+                            com.android.internal.R.styleable.TableRow_Cell);
+
+            column = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_column, -1);
+            span = a.getInt(com.android.internal.R.styleable.TableRow_Cell_layout_span, 1);
+            if (span <= 1) {
+                span = 1;
+            }
+
+            a.recycle();
+        }
+
+        /**
+         * <p>Sets the child width and the child height.</p>
+         *
+         * @param w the desired width
+         * @param h the desired height
+         */
+        public LayoutParams(int w, int h) {
+            super(w, h);
+            column = -1;
+            span = 1;
+        }
+
+        /**
+         * <p>Sets the child width, height and weight.</p>
+         *
+         * @param w the desired width
+         * @param h the desired height
+         * @param initWeight the desired weight
+         */
+        public LayoutParams(int w, int h, float initWeight) {
+            super(w, h, initWeight);
+            column = -1;
+            span = 1;
+        }
+
+        /**
+         * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams}
+         * and the child height to
+         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+         */
+        public LayoutParams() {
+            super(MATCH_PARENT, WRAP_CONTENT);
+            column = -1;
+            span = 1;
+        }
+
+        /**
+         * <p>Puts the view in the specified column.</p>
+         *
+         * <p>Sets the child width to {@link android.view.ViewGroup.LayoutParams#MATCH_PARENT}
+         * and the child height to
+         * {@link android.view.ViewGroup.LayoutParams#WRAP_CONTENT}.</p>
+         *
+         * @param column the column index for the view
+         */
+        public LayoutParams(int column) {
+            this();
+            this.column = column;
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(ViewGroup.LayoutParams p) {
+            super(p);
+        }
+
+        /**
+         * {@inheritDoc}
+         */
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+        }
+
+        @Override
+        protected void setBaseAttributes(TypedArray a, int widthAttr, int heightAttr) {
+            // We don't want to force users to specify a layout_width
+            if (a.hasValue(widthAttr)) {
+                width = a.getLayoutDimension(widthAttr, "layout_width");
+            } else {
+                width = MATCH_PARENT;
+            }
+
+            // We don't want to force users to specify a layout_height
+            if (a.hasValue(heightAttr)) {
+                height = a.getLayoutDimension(heightAttr, "layout_height");
+            } else {
+                height = WRAP_CONTENT;
+            }
+        }
+
+        /** @hide */
+        @Override
+        protected void encodeProperties(@NonNull ViewHierarchyEncoder encoder) {
+            super.encodeProperties(encoder);
+            encoder.addProperty("layout:column", column);
+            encoder.addProperty("layout:span", span);
+        }
+    }
+
+    // special transparent hierarchy change listener
+    private class ChildrenTracker implements OnHierarchyChangeListener {
+        private OnHierarchyChangeListener listener;
+
+        private void setOnHierarchyChangeListener(OnHierarchyChangeListener listener) {
+            this.listener = listener;
+        }
+
+        public void onChildViewAdded(View parent, View child) {
+            // dirties the index to column map
+            mColumnToChildIndex = null;
+
+            if (this.listener != null) {
+                this.listener.onChildViewAdded(parent, child);
+            }
+        }
+
+        public void onChildViewRemoved(View parent, View child) {
+            // dirties the index to column map
+            mColumnToChildIndex = null;
+
+            if (this.listener != null) {
+                this.listener.onChildViewRemoved(parent, child);
+            }
+        }
+    }
+}
diff --git a/android/widget/TextClock.java b/android/widget/TextClock.java
new file mode 100644
index 0000000..1279040
--- /dev/null
+++ b/android/widget/TextClock.java
@@ -0,0 +1,619 @@
+/*
+ * 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 android.widget;
+
+import static android.view.ViewDebug.ExportedProperty;
+import static android.widget.RemoteViews.RemoteView;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.content.BroadcastReceiver;
+import android.content.ContentResolver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.database.ContentObserver;
+import android.net.Uri;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.os.UserHandle;
+import android.provider.Settings;
+import android.text.format.DateFormat;
+import android.util.AttributeSet;
+import android.view.RemotableViewMethod;
+import android.view.ViewHierarchyEncoder;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Calendar;
+import java.util.TimeZone;
+
+/**
+ * <p><code>TextClock</code> can display the current date and/or time as
+ * a formatted string.</p>
+ *
+ * <p>This view honors the 24-hour format system setting. As such, it is
+ * possible and recommended to provide two different formatting patterns:
+ * one to display the date/time in 24-hour mode and one to display the
+ * date/time in 12-hour mode. Most callers will want to use the defaults,
+ * though, which will be appropriate for the user's locale.</p>
+ *
+ * <p>It is possible to determine whether the system is currently in
+ * 24-hour mode by calling {@link #is24HourModeEnabled()}.</p>
+ *
+ * <p>The rules used by this widget to decide how to format the date and
+ * time are the following:</p>
+ * <ul>
+ *     <li>In 24-hour mode:
+ *         <ul>
+ *             <li>Use the value returned by {@link #getFormat24Hour()} when non-null</li>
+ *             <li>Otherwise, use the value returned by {@link #getFormat12Hour()} when non-null</li>
+ *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code h:mm a}</li>
+ *         </ul>
+ *     </li>
+ *     <li>In 12-hour mode:
+ *         <ul>
+ *             <li>Use the value returned by {@link #getFormat12Hour()} when non-null</li>
+ *             <li>Otherwise, use the value returned by {@link #getFormat24Hour()} when non-null</li>
+ *             <li>Otherwise, use a default value appropriate for the user's locale, such as {@code HH:mm}</li>
+ *         </ul>
+ *     </li>
+ * </ul>
+ *
+ * <p>The {@link CharSequence} instances used as formatting patterns when calling either
+ * {@link #setFormat24Hour(CharSequence)} or {@link #setFormat12Hour(CharSequence)} can
+ * contain styling information. To do so, use a {@link android.text.Spanned} object.
+ * Note that if you customize these strings, it is your responsibility to supply strings
+ * appropriate for formatting dates and/or times in the user's locale.</p>
+ *
+ * @attr ref android.R.styleable#TextClock_format12Hour
+ * @attr ref android.R.styleable#TextClock_format24Hour
+ * @attr ref android.R.styleable#TextClock_timeZone
+ */
+@RemoteView
+public class TextClock extends TextView {
+    /**
+     * The default formatting pattern in 12-hour mode. This pattern is used
+     * if {@link #setFormat12Hour(CharSequence)} is called with a null pattern
+     * or if no pattern was specified when creating an instance of this class.
+     *
+     * This default pattern shows only the time, hours and minutes, and an am/pm
+     * indicator.
+     *
+     * @see #setFormat12Hour(CharSequence)
+     * @see #getFormat12Hour()
+     *
+     * @deprecated Let the system use locale-appropriate defaults instead.
+     */
+    @Deprecated
+    public static final CharSequence DEFAULT_FORMAT_12_HOUR = "h:mm a";
+
+    /**
+     * The default formatting pattern in 24-hour mode. This pattern is used
+     * if {@link #setFormat24Hour(CharSequence)} is called with a null pattern
+     * or if no pattern was specified when creating an instance of this class.
+     *
+     * This default pattern shows only the time, hours and minutes.
+     *
+     * @see #setFormat24Hour(CharSequence)
+     * @see #getFormat24Hour()
+     *
+     * @deprecated Let the system use locale-appropriate defaults instead.
+     */
+    @Deprecated
+    public static final CharSequence DEFAULT_FORMAT_24_HOUR = "H:mm";
+
+    private CharSequence mFormat12;
+    private CharSequence mFormat24;
+    private CharSequence mDescFormat12;
+    private CharSequence mDescFormat24;
+
+    @ExportedProperty
+    private CharSequence mFormat;
+    @ExportedProperty
+    private boolean mHasSeconds;
+
+    private CharSequence mDescFormat;
+
+    private boolean mRegistered;
+    private boolean mShouldRunTicker;
+
+    private Calendar mTime;
+    private String mTimeZone;
+
+    private boolean mShowCurrentUserTime;
+
+    private ContentObserver mFormatChangeObserver;
+    private class FormatChangeObserver extends ContentObserver {
+
+        public FormatChangeObserver(Handler handler) {
+            super(handler);
+        }
+
+        @Override
+        public void onChange(boolean selfChange) {
+            chooseFormat();
+            onTimeChanged();
+        }
+
+        @Override
+        public void onChange(boolean selfChange, Uri uri) {
+            chooseFormat();
+            onTimeChanged();
+        }
+    };
+
+    private final BroadcastReceiver mIntentReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (mTimeZone == null && Intent.ACTION_TIMEZONE_CHANGED.equals(intent.getAction())) {
+                final String timeZone = intent.getStringExtra("time-zone");
+                createTime(timeZone);
+            }
+            onTimeChanged();
+        }
+    };
+
+    private final Runnable mTicker = new Runnable() {
+        public void run() {
+            onTimeChanged();
+
+            long now = SystemClock.uptimeMillis();
+            long next = now + (1000 - now % 1000);
+
+            getHandler().postAtTime(mTicker, next);
+        }
+    };
+
+    /**
+     * Creates a new clock using the default patterns for the current locale.
+     *
+     * @param context The Context the view is running in, through which it can
+     *        access the current theme, resources, etc.
+     */
+    @SuppressWarnings("UnusedDeclaration")
+    public TextClock(Context context) {
+        super(context);
+        init();
+    }
+
+    /**
+     * Creates a new clock inflated from XML. This object's properties are
+     * intialized from the attributes specified in XML.
+     *
+     * This constructor uses a default style of 0, so the only attribute values
+     * applied are those in the Context's Theme and the given AttributeSet.
+     *
+     * @param context The Context the view is running in, through which it can
+     *        access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view
+     */
+    @SuppressWarnings("UnusedDeclaration")
+    public TextClock(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    /**
+     * Creates a new clock inflated from XML. This object's properties are
+     * intialized from the attributes specified in XML.
+     *
+     * @param context The Context the view is running in, through which it can
+     *        access the current theme, resources, etc.
+     * @param attrs The attributes of the XML tag that is inflating the view
+     * @param defStyleAttr An attribute in the current theme that contains a
+     *        reference to a style resource that supplies default values for
+     *        the view. Can be 0 to not look for defaults.
+     */
+    public TextClock(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public TextClock(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.TextClock, defStyleAttr, defStyleRes);
+        try {
+            mFormat12 = a.getText(R.styleable.TextClock_format12Hour);
+            mFormat24 = a.getText(R.styleable.TextClock_format24Hour);
+            mTimeZone = a.getString(R.styleable.TextClock_timeZone);
+        } finally {
+            a.recycle();
+        }
+
+        init();
+    }
+
+    private void init() {
+        if (mFormat12 == null || mFormat24 == null) {
+            LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
+            if (mFormat12 == null) {
+                mFormat12 = ld.timeFormat_hm;
+            }
+            if (mFormat24 == null) {
+                mFormat24 = ld.timeFormat_Hm;
+            }
+        }
+
+        createTime(mTimeZone);
+        chooseFormat();
+    }
+
+    private void createTime(String timeZone) {
+        if (timeZone != null) {
+            mTime = Calendar.getInstance(TimeZone.getTimeZone(timeZone));
+        } else {
+            mTime = Calendar.getInstance();
+        }
+    }
+
+    /**
+     * Returns the formatting pattern used to display the date and/or time
+     * in 12-hour mode. The formatting pattern syntax is described in
+     * {@link DateFormat}.
+     *
+     * @return A {@link CharSequence} or null.
+     *
+     * @see #setFormat12Hour(CharSequence)
+     * @see #is24HourModeEnabled()
+     */
+    @ExportedProperty
+    public CharSequence getFormat12Hour() {
+        return mFormat12;
+    }
+
+    /**
+     * <p>Specifies the formatting pattern used to display the date and/or time
+     * in 12-hour mode. The formatting pattern syntax is described in
+     * {@link DateFormat}.</p>
+     *
+     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
+     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
+     * are set to null, the default pattern for the current locale will be used
+     * instead.</p>
+     *
+     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
+     * you supply a format string generated by
+     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
+     * takes care of generating a format string adapted to the desired locale.</p>
+     *
+     *
+     * @param format A date/time formatting pattern as described in {@link DateFormat}
+     *
+     * @see #getFormat12Hour()
+     * @see #is24HourModeEnabled()
+     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
+     * @see DateFormat
+     *
+     * @attr ref android.R.styleable#TextClock_format12Hour
+     */
+    @RemotableViewMethod
+    public void setFormat12Hour(CharSequence format) {
+        mFormat12 = format;
+
+        chooseFormat();
+        onTimeChanged();
+    }
+
+    /**
+     * Like setFormat12Hour, but for the content description.
+     * @hide
+     */
+    public void setContentDescriptionFormat12Hour(CharSequence format) {
+        mDescFormat12 = format;
+
+        chooseFormat();
+        onTimeChanged();
+    }
+
+    /**
+     * Returns the formatting pattern used to display the date and/or time
+     * in 24-hour mode. The formatting pattern syntax is described in
+     * {@link DateFormat}.
+     *
+     * @return A {@link CharSequence} or null.
+     *
+     * @see #setFormat24Hour(CharSequence)
+     * @see #is24HourModeEnabled()
+     */
+    @ExportedProperty
+    public CharSequence getFormat24Hour() {
+        return mFormat24;
+    }
+
+    /**
+     * <p>Specifies the formatting pattern used to display the date and/or time
+     * in 24-hour mode. The formatting pattern syntax is described in
+     * {@link DateFormat}.</p>
+     *
+     * <p>If this pattern is set to null, {@link #getFormat24Hour()} will be used
+     * even in 12-hour mode. If both 24-hour and 12-hour formatting patterns
+     * are set to null, the default pattern for the current locale will be used
+     * instead.</p>
+     *
+     * <p><strong>Note:</strong> if styling is not needed, it is highly recommended
+     * you supply a format string generated by
+     * {@link DateFormat#getBestDateTimePattern(java.util.Locale, String)}. This method
+     * takes care of generating a format string adapted to the desired locale.</p>
+     *
+     * @param format A date/time formatting pattern as described in {@link DateFormat}
+     *
+     * @see #getFormat24Hour()
+     * @see #is24HourModeEnabled()
+     * @see DateFormat#getBestDateTimePattern(java.util.Locale, String)
+     * @see DateFormat
+     *
+     * @attr ref android.R.styleable#TextClock_format24Hour
+     */
+    @RemotableViewMethod
+    public void setFormat24Hour(CharSequence format) {
+        mFormat24 = format;
+
+        chooseFormat();
+        onTimeChanged();
+    }
+
+    /**
+     * Like setFormat24Hour, but for the content description.
+     * @hide
+     */
+    public void setContentDescriptionFormat24Hour(CharSequence format) {
+        mDescFormat24 = format;
+
+        chooseFormat();
+        onTimeChanged();
+    }
+
+    /**
+     * Sets whether this clock should always track the current user and not the user of the
+     * current process. This is used for single instance processes like the systemUI who need
+     * to display time for different users.
+     *
+     * @hide
+     */
+    public void setShowCurrentUserTime(boolean showCurrentUserTime) {
+        mShowCurrentUserTime = showCurrentUserTime;
+
+        chooseFormat();
+        onTimeChanged();
+        unregisterObserver();
+        registerObserver();
+    }
+
+    /**
+     * Indicates whether the system is currently using the 24-hour mode.
+     *
+     * When the system is in 24-hour mode, this view will use the pattern
+     * returned by {@link #getFormat24Hour()}. In 12-hour mode, the pattern
+     * returned by {@link #getFormat12Hour()} is used instead.
+     *
+     * If either one of the formats is null, the other format is used. If
+     * both formats are null, the default formats for the current locale are used.
+     *
+     * @return true if time should be displayed in 24-hour format, false if it
+     *         should be displayed in 12-hour format.
+     *
+     * @see #setFormat12Hour(CharSequence)
+     * @see #getFormat12Hour()
+     * @see #setFormat24Hour(CharSequence)
+     * @see #getFormat24Hour()
+     */
+    public boolean is24HourModeEnabled() {
+        if (mShowCurrentUserTime) {
+            return DateFormat.is24HourFormat(getContext(), ActivityManager.getCurrentUser());
+        } else {
+            return DateFormat.is24HourFormat(getContext());
+        }
+    }
+
+    /**
+     * Indicates which time zone is currently used by this view.
+     *
+     * @return The ID of the current time zone or null if the default time zone,
+     *         as set by the user, must be used
+     *
+     * @see TimeZone
+     * @see java.util.TimeZone#getAvailableIDs()
+     * @see #setTimeZone(String)
+     */
+    public String getTimeZone() {
+        return mTimeZone;
+    }
+
+    /**
+     * Sets the specified time zone to use in this clock. When the time zone
+     * is set through this method, system time zone changes (when the user
+     * sets the time zone in settings for instance) will be ignored.
+     *
+     * @param timeZone The desired time zone's ID as specified in {@link TimeZone}
+     *                 or null to user the time zone specified by the user
+     *                 (system time zone)
+     *
+     * @see #getTimeZone()
+     * @see java.util.TimeZone#getAvailableIDs()
+     * @see TimeZone#getTimeZone(String)
+     *
+     * @attr ref android.R.styleable#TextClock_timeZone
+     */
+    @RemotableViewMethod
+    public void setTimeZone(String timeZone) {
+        mTimeZone = timeZone;
+
+        createTime(timeZone);
+        onTimeChanged();
+    }
+
+    /**
+     * Returns the current format string. Always valid after constructor has
+     * finished, and will never be {@code null}.
+     *
+     * @hide
+     */
+    public CharSequence getFormat() {
+        return mFormat;
+    }
+
+    /**
+     * Selects either one of {@link #getFormat12Hour()} or {@link #getFormat24Hour()}
+     * depending on whether the user has selected 24-hour format.
+     */
+    private void chooseFormat() {
+        final boolean format24Requested = is24HourModeEnabled();
+
+        LocaleData ld = LocaleData.get(getContext().getResources().getConfiguration().locale);
+
+        if (format24Requested) {
+            mFormat = abc(mFormat24, mFormat12, ld.timeFormat_Hm);
+            mDescFormat = abc(mDescFormat24, mDescFormat12, mFormat);
+        } else {
+            mFormat = abc(mFormat12, mFormat24, ld.timeFormat_hm);
+            mDescFormat = abc(mDescFormat12, mDescFormat24, mFormat);
+        }
+
+        boolean hadSeconds = mHasSeconds;
+        mHasSeconds = DateFormat.hasSeconds(mFormat);
+
+        if (mShouldRunTicker && hadSeconds != mHasSeconds) {
+            if (hadSeconds) getHandler().removeCallbacks(mTicker);
+            else mTicker.run();
+        }
+    }
+
+    /**
+     * Returns a if not null, else return b if not null, else return c.
+     */
+    private static CharSequence abc(CharSequence a, CharSequence b, CharSequence c) {
+        return a == null ? (b == null ? c : b) : a;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (!mRegistered) {
+            mRegistered = true;
+
+            registerReceiver();
+            registerObserver();
+
+            createTime(mTimeZone);
+        }
+    }
+
+    @Override
+    public void onVisibilityAggregated(boolean isVisible) {
+        super.onVisibilityAggregated(isVisible);
+
+        if (!mShouldRunTicker && isVisible) {
+            mShouldRunTicker = true;
+            if (mHasSeconds) {
+                mTicker.run();
+            } else {
+                onTimeChanged();
+            }
+        } else if (mShouldRunTicker && !isVisible) {
+            mShouldRunTicker = false;
+            getHandler().removeCallbacks(mTicker);
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mRegistered) {
+            unregisterReceiver();
+            unregisterObserver();
+
+            mRegistered = false;
+        }
+    }
+
+    private void registerReceiver() {
+        final IntentFilter filter = new IntentFilter();
+
+        filter.addAction(Intent.ACTION_TIME_TICK);
+        filter.addAction(Intent.ACTION_TIME_CHANGED);
+        filter.addAction(Intent.ACTION_TIMEZONE_CHANGED);
+
+        // OK, this is gross but needed. This class is supported by the
+        // remote views mechanism and as a part of that the remote views
+        // can be inflated by a context for another user without the app
+        // having interact users permission - just for loading resources.
+        // For example, when adding widgets from a managed profile to the
+        // home screen. Therefore, we register the receiver as the user
+        // the app is running as not the one the context is for.
+        getContext().registerReceiverAsUser(mIntentReceiver, android.os.Process.myUserHandle(),
+                filter, null, getHandler());
+    }
+
+    private void registerObserver() {
+        if (mRegistered) {
+            if (mFormatChangeObserver == null) {
+                mFormatChangeObserver = new FormatChangeObserver(getHandler());
+            }
+            final ContentResolver resolver = getContext().getContentResolver();
+            if (mShowCurrentUserTime) {
+                resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+                        mFormatChangeObserver, UserHandle.USER_ALL);
+            } else {
+                resolver.registerContentObserver(Settings.System.CONTENT_URI, true,
+                        mFormatChangeObserver);
+            }
+        }
+    }
+
+    private void unregisterReceiver() {
+        getContext().unregisterReceiver(mIntentReceiver);
+    }
+
+    private void unregisterObserver() {
+        if (mFormatChangeObserver != null) {
+            final ContentResolver resolver = getContext().getContentResolver();
+            resolver.unregisterContentObserver(mFormatChangeObserver);
+        }
+    }
+
+    /**
+     * Update the displayed time if this view and its ancestors and window is visible
+     */
+    private void onTimeChanged() {
+        // mShouldRunTicker always equals the last value passed into onVisibilityAggregated
+        if (mShouldRunTicker) {
+            mTime.setTimeInMillis(System.currentTimeMillis());
+            setText(DateFormat.format(mFormat, mTime));
+            setContentDescription(DateFormat.format(mDescFormat, mTime));
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+
+        CharSequence s = getFormat12Hour();
+        stream.addProperty("format12Hour", s == null ? null : s.toString());
+
+        s = getFormat24Hour();
+        stream.addProperty("format24Hour", s == null ? null : s.toString());
+        stream.addProperty("format", mFormat == null ? null : mFormat.toString());
+        stream.addProperty("hasSeconds", mHasSeconds);
+    }
+}
diff --git a/android/widget/TextInputTimePickerView.java b/android/widget/TextInputTimePickerView.java
new file mode 100644
index 0000000..0cf8faa
--- /dev/null
+++ b/android/widget/TextInputTimePickerView.java
@@ -0,0 +1,253 @@
+/*
+ * Copyright (C) 2017 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 android.widget;
+
+import android.content.Context;
+import android.os.LocaleList;
+import android.text.Editable;
+import android.text.InputFilter;
+import android.text.TextWatcher;
+import android.util.AttributeSet;
+import android.util.MathUtils;
+import android.view.View;
+
+import com.android.internal.R;
+
+/**
+ * View to show text input based time picker with hour and minute fields and an optional AM/PM
+ * spinner.
+ *
+ * @hide
+ */
+public class TextInputTimePickerView extends RelativeLayout {
+    public static final int HOURS = 0;
+    public static final int MINUTES = 1;
+    public static final int AMPM = 2;
+
+    private static final int AM = 0;
+    private static final int PM = 1;
+
+    private final EditText mHourEditText;
+    private final EditText mMinuteEditText;
+    private final TextView mInputSeparatorView;
+    private final Spinner mAmPmSpinner;
+    private final TextView mErrorLabel;
+    private final TextView mHourLabel;
+    private final TextView mMinuteLabel;
+
+    private boolean mIs24Hour;
+    private boolean mHourFormatStartsAtZero;
+    private OnValueTypedListener mListener;
+
+    private boolean mErrorShowing;
+
+    interface OnValueTypedListener {
+        void onValueChanged(int inputType, int newValue);
+    }
+
+    public TextInputTimePickerView(Context context) {
+        this(context, null);
+    }
+
+    public TextInputTimePickerView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle) {
+        this(context, attrs, defStyle, 0);
+    }
+
+    public TextInputTimePickerView(Context context, AttributeSet attrs, int defStyle,
+            int defStyleRes) {
+        super(context, attrs, defStyle, defStyleRes);
+
+        inflate(context, R.layout.time_picker_text_input_material, this);
+
+        mHourEditText = findViewById(R.id.input_hour);
+        mMinuteEditText = findViewById(R.id.input_minute);
+        mInputSeparatorView = findViewById(R.id.input_separator);
+        mErrorLabel = findViewById(R.id.label_error);
+        mHourLabel = findViewById(R.id.label_hour);
+        mMinuteLabel = findViewById(R.id.label_minute);
+
+        mHourEditText.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+            @Override
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+            @Override
+            public void afterTextChanged(Editable editable) {
+                parseAndSetHourInternal(editable.toString());
+            }
+        });
+
+        mMinuteEditText.addTextChangedListener(new TextWatcher() {
+            @Override
+            public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+            @Override
+            public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {}
+
+            @Override
+            public void afterTextChanged(Editable editable) {
+                parseAndSetMinuteInternal(editable.toString());
+            }
+        });
+
+        mAmPmSpinner = findViewById(R.id.am_pm_spinner);
+        final String[] amPmStrings = TimePicker.getAmPmStrings(context);
+        ArrayAdapter<CharSequence> adapter =
+                new ArrayAdapter<CharSequence>(context, R.layout.simple_spinner_dropdown_item);
+        adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[0]));
+        adapter.add(TimePickerClockDelegate.obtainVerbatim(amPmStrings[1]));
+        mAmPmSpinner.setAdapter(adapter);
+        mAmPmSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
+            @Override
+            public void onItemSelected(AdapterView<?> adapterView, View view, int position,
+                    long id) {
+                if (position == 0) {
+                    mListener.onValueChanged(AMPM, AM);
+                } else {
+                    mListener.onValueChanged(AMPM, PM);
+                }
+            }
+
+            @Override
+            public void onNothingSelected(AdapterView<?> adapterView) {}
+        });
+    }
+
+    void setListener(OnValueTypedListener listener) {
+        mListener = listener;
+    }
+
+    void setHourFormat(int maxCharLength) {
+        mHourEditText.setFilters(new InputFilter[] {
+                new InputFilter.LengthFilter(maxCharLength)});
+        mMinuteEditText.setFilters(new InputFilter[] {
+                new InputFilter.LengthFilter(maxCharLength)});
+        final LocaleList locales = mContext.getResources().getConfiguration().getLocales();
+        mHourEditText.setImeHintLocales(locales);
+        mMinuteEditText.setImeHintLocales(locales);
+    }
+
+    boolean validateInput() {
+        final boolean inputValid = parseAndSetHourInternal(mHourEditText.getText().toString())
+                && parseAndSetMinuteInternal(mMinuteEditText.getText().toString());
+        setError(!inputValid);
+        return inputValid;
+    }
+
+    void updateSeparator(String separatorText) {
+        mInputSeparatorView.setText(separatorText);
+    }
+
+    private void setError(boolean enabled) {
+        mErrorShowing = enabled;
+
+        mErrorLabel.setVisibility(enabled ? View.VISIBLE : View.INVISIBLE);
+        mHourLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
+        mMinuteLabel.setVisibility(enabled ? View.INVISIBLE : View.VISIBLE);
+    }
+
+    /**
+     * Computes the display value and updates the text of the view.
+     * <p>
+     * This method should be called whenever the current value or display
+     * properties (leading zeroes, max digits) change.
+     */
+    void updateTextInputValues(int localizedHour, int minute, int amOrPm, boolean is24Hour,
+            boolean hourFormatStartsAtZero) {
+        final String format = "%d";
+
+        mIs24Hour = is24Hour;
+        mHourFormatStartsAtZero = hourFormatStartsAtZero;
+
+        mAmPmSpinner.setVisibility(is24Hour ? View.INVISIBLE : View.VISIBLE);
+
+        if (amOrPm == AM) {
+            mAmPmSpinner.setSelection(0);
+        } else {
+            mAmPmSpinner.setSelection(1);
+        }
+
+        mHourEditText.setText(String.format(format, localizedHour));
+        mMinuteEditText.setText(String.format(format, minute));
+
+        if (mErrorShowing) {
+            validateInput();
+        }
+    }
+
+    private boolean parseAndSetHourInternal(String input) {
+        try {
+            final int hour = Integer.parseInt(input);
+            if (!isValidLocalizedHour(hour)) {
+                final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+                final int maxHour = mIs24Hour ? 23 : 11 + minHour;
+                mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(
+                        MathUtils.constrain(hour, minHour, maxHour)));
+                return false;
+            }
+            mListener.onValueChanged(HOURS, getHourOfDayFromLocalizedHour(hour));
+            return true;
+        } catch (NumberFormatException e) {
+            // Do nothing since we cannot parse the input.
+            return false;
+        }
+    }
+
+    private boolean parseAndSetMinuteInternal(String input) {
+        try {
+            final int minutes = Integer.parseInt(input);
+            if (minutes < 0 || minutes > 59) {
+                mListener.onValueChanged(MINUTES, MathUtils.constrain(minutes, 0, 59));
+                return false;
+            }
+            mListener.onValueChanged(MINUTES, minutes);
+            return true;
+        } catch (NumberFormatException e) {
+            // Do nothing since we cannot parse the input.
+            return false;
+        }
+    }
+
+    private boolean isValidLocalizedHour(int localizedHour) {
+        final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+        final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
+        return localizedHour >= minHour && localizedHour <= maxHour;
+    }
+
+    private int getHourOfDayFromLocalizedHour(int localizedHour) {
+        int hourOfDay = localizedHour;
+        if (mIs24Hour) {
+            if (!mHourFormatStartsAtZero && localizedHour == 24) {
+                hourOfDay = 0;
+            }
+        } else {
+            if (!mHourFormatStartsAtZero && localizedHour == 12) {
+                hourOfDay = 0;
+            }
+            if (mAmPmSpinner.getSelectedItemPosition() == 1) {
+                hourOfDay += 12;
+            }
+        }
+        return hourOfDay;
+    }
+}
diff --git a/android/widget/TextSwitcher.java b/android/widget/TextSwitcher.java
new file mode 100644
index 0000000..ecd9a8c
--- /dev/null
+++ b/android/widget/TextSwitcher.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * Specialized {@link android.widget.ViewSwitcher} that contains
+ * only children of type {@link android.widget.TextView}.
+ *
+ * A TextSwitcher is useful to animate a label on screen. Whenever
+ * {@link #setText(CharSequence)} is called, TextSwitcher animates the current text
+ * out and animates the new text in. 
+ */
+public class TextSwitcher extends ViewSwitcher {
+    /**
+     * Creates a new empty TextSwitcher.
+     *
+     * @param context the application's environment
+     */
+    public TextSwitcher(Context context) {
+        super(context);
+    }
+
+    /**
+     * Creates a new empty TextSwitcher for the given context and with the
+     * specified set attributes.
+     *
+     * @param context the application environment
+     * @param attrs a collection of attributes
+     */
+    public TextSwitcher(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws IllegalArgumentException if child is not an instance of
+     *         {@link android.widget.TextView}
+     */
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (!(child instanceof TextView)) {
+            throw new IllegalArgumentException(
+                    "TextSwitcher children must be instances of TextView");
+        }
+
+        super.addView(child, index, params);
+    }
+
+    /**
+     * Sets the text of the next view and switches to the next view. This can
+     * be used to animate the old text out and animate the next text in.
+     *
+     * @param text the new text to display
+     */
+    public void setText(CharSequence text) {
+        final TextView t = (TextView) getNextView();
+        t.setText(text);
+        showNext();
+    }
+
+    /**
+     * Sets the text of the text view that is currently showing.  This does
+     * not perform the animations.
+     *
+     * @param text the new text to display
+     */
+    public void setCurrentText(CharSequence text) {
+        ((TextView)getCurrentView()).setText(text);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TextSwitcher.class.getName();
+    }
+}
diff --git a/android/widget/TextView.java b/android/widget/TextView.java
new file mode 100644
index 0000000..4b6c4d3
--- /dev/null
+++ b/android/widget/TextView.java
@@ -0,0 +1,12041 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX;
+import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
+import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
+
+import android.R;
+import android.annotation.CheckResult;
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.FloatRange;
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.Size;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.annotation.XmlRes;
+import android.app.Activity;
+import android.app.assist.AssistStructure;
+import android.content.ClipData;
+import android.content.ClipDescription;
+import android.content.ClipboardManager;
+import android.content.Context;
+import android.content.Intent;
+import android.content.UndoManager;
+import android.content.res.ColorStateList;
+import android.content.res.CompatibilityInfo;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.content.res.XmlResourceParser;
+import android.graphics.BaseCanvas;
+import android.graphics.Canvas;
+import android.graphics.Insets;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.PorterDuff;
+import android.graphics.Rect;
+import android.graphics.RectF;
+import android.graphics.Typeface;
+import android.graphics.drawable.Drawable;
+import android.graphics.fonts.FontVariationAxis;
+import android.icu.text.DecimalFormatSymbols;
+import android.os.AsyncTask;
+import android.os.Build.VERSION_CODES;
+import android.os.Bundle;
+import android.os.LocaleList;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.os.ParcelableParcel;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.BoringLayout;
+import android.text.DynamicLayout;
+import android.text.Editable;
+import android.text.GetChars;
+import android.text.GraphicsOperations;
+import android.text.InputFilter;
+import android.text.InputType;
+import android.text.Layout;
+import android.text.ParcelableSpan;
+import android.text.Selection;
+import android.text.SpanWatcher;
+import android.text.Spannable;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.SpannedString;
+import android.text.StaticLayout;
+import android.text.TextDirectionHeuristic;
+import android.text.TextDirectionHeuristics;
+import android.text.TextPaint;
+import android.text.TextUtils;
+import android.text.TextUtils.TruncateAt;
+import android.text.TextWatcher;
+import android.text.method.AllCapsTransformationMethod;
+import android.text.method.ArrowKeyMovementMethod;
+import android.text.method.DateKeyListener;
+import android.text.method.DateTimeKeyListener;
+import android.text.method.DialerKeyListener;
+import android.text.method.DigitsKeyListener;
+import android.text.method.KeyListener;
+import android.text.method.LinkMovementMethod;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+import android.text.method.SingleLineTransformationMethod;
+import android.text.method.TextKeyListener;
+import android.text.method.TimeKeyListener;
+import android.text.method.TransformationMethod;
+import android.text.method.TransformationMethod2;
+import android.text.method.WordIterator;
+import android.text.style.CharacterStyle;
+import android.text.style.ClickableSpan;
+import android.text.style.ParagraphStyle;
+import android.text.style.SpellCheckSpan;
+import android.text.style.SuggestionSpan;
+import android.text.style.URLSpan;
+import android.text.style.UpdateAppearance;
+import android.text.util.Linkify;
+import android.util.AttributeSet;
+import android.util.DisplayMetrics;
+import android.util.IntArray;
+import android.util.Log;
+import android.util.SparseIntArray;
+import android.util.TypedValue;
+import android.view.AccessibilityIterators.TextSegmentIterator;
+import android.view.ActionMode;
+import android.view.Choreographer;
+import android.view.ContextMenu;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HapticFeedbackConstants;
+import android.view.InputDevice;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.PointerIcon;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewDebug;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewHierarchyEncoder;
+import android.view.ViewParent;
+import android.view.ViewRootImpl;
+import android.view.ViewStructure;
+import android.view.ViewTreeObserver;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.animation.AnimationUtils;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.CompletionInfo;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.CursorAnchorInfo;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.ExtractedText;
+import android.view.inputmethod.ExtractedTextRequest;
+import android.view.inputmethod.InputConnection;
+import android.view.inputmethod.InputMethodManager;
+import android.view.textclassifier.TextClassificationManager;
+import android.view.textclassifier.TextClassifier;
+import android.view.textservice.SpellCheckerSubtype;
+import android.view.textservice.TextServicesManager;
+import android.widget.RemoteViews.RemoteView;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.logging.MetricsLogger;
+import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
+import com.android.internal.util.FastMath;
+import com.android.internal.widget.EditableInputConnection;
+
+import libcore.util.EmptyArray;
+
+import org.xmlpull.v1.XmlPullParserException;
+
+import java.io.IOException;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.ref.WeakReference;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * A user interface element that displays text to the user.
+ * To provide user-editable text, see {@link EditText}.
+ * <p>
+ * The following code sample shows a typical use, with an XML layout
+ * and code to modify the contents of the text view:
+ * </p>
+
+ * <pre>
+ * &lt;LinearLayout
+       xmlns:android="http://schemas.android.com/apk/res/android"
+       android:layout_width="match_parent"
+       android:layout_height="match_parent"&gt;
+ *    &lt;TextView
+ *        android:id="@+id/text_view_id"
+ *        android:layout_height="wrap_content"
+ *        android:layout_width="wrap_content"
+ *        android:text="@string/hello" /&gt;
+ * &lt;/LinearLayout&gt;
+ * </pre>
+ * <p>
+ * This code sample demonstrates how to modify the contents of the text view
+ * defined in the previous XML layout:
+ * </p>
+ * <pre>
+ * public class MainActivity extends Activity {
+ *
+ *    protected void onCreate(Bundle savedInstanceState) {
+ *         super.onCreate(savedInstanceState);
+ *         setContentView(R.layout.activity_main);
+ *         final TextView helloTextView = (TextView) findViewById(R.id.text_view_id);
+ *         helloTextView.setText(R.string.user_greeting);
+ *     }
+ * }
+ * </pre>
+ * <p>
+ * To customize the appearance of TextView, see <a href="https://developer.android.com/guide/topics/ui/themes.html">Styles and Themes</a>.
+ * </p>
+ * <p>
+ * <b>XML attributes</b>
+ * <p>
+ * See {@link android.R.styleable#TextView TextView Attributes},
+ * {@link android.R.styleable#View View Attributes}
+ *
+ * @attr ref android.R.styleable#TextView_text
+ * @attr ref android.R.styleable#TextView_bufferType
+ * @attr ref android.R.styleable#TextView_hint
+ * @attr ref android.R.styleable#TextView_textColor
+ * @attr ref android.R.styleable#TextView_textColorHighlight
+ * @attr ref android.R.styleable#TextView_textColorHint
+ * @attr ref android.R.styleable#TextView_textAppearance
+ * @attr ref android.R.styleable#TextView_textColorLink
+ * @attr ref android.R.styleable#TextView_textSize
+ * @attr ref android.R.styleable#TextView_textScaleX
+ * @attr ref android.R.styleable#TextView_fontFamily
+ * @attr ref android.R.styleable#TextView_typeface
+ * @attr ref android.R.styleable#TextView_textStyle
+ * @attr ref android.R.styleable#TextView_cursorVisible
+ * @attr ref android.R.styleable#TextView_maxLines
+ * @attr ref android.R.styleable#TextView_maxHeight
+ * @attr ref android.R.styleable#TextView_lines
+ * @attr ref android.R.styleable#TextView_height
+ * @attr ref android.R.styleable#TextView_minLines
+ * @attr ref android.R.styleable#TextView_minHeight
+ * @attr ref android.R.styleable#TextView_maxEms
+ * @attr ref android.R.styleable#TextView_maxWidth
+ * @attr ref android.R.styleable#TextView_ems
+ * @attr ref android.R.styleable#TextView_width
+ * @attr ref android.R.styleable#TextView_minEms
+ * @attr ref android.R.styleable#TextView_minWidth
+ * @attr ref android.R.styleable#TextView_gravity
+ * @attr ref android.R.styleable#TextView_scrollHorizontally
+ * @attr ref android.R.styleable#TextView_password
+ * @attr ref android.R.styleable#TextView_singleLine
+ * @attr ref android.R.styleable#TextView_selectAllOnFocus
+ * @attr ref android.R.styleable#TextView_includeFontPadding
+ * @attr ref android.R.styleable#TextView_maxLength
+ * @attr ref android.R.styleable#TextView_shadowColor
+ * @attr ref android.R.styleable#TextView_shadowDx
+ * @attr ref android.R.styleable#TextView_shadowDy
+ * @attr ref android.R.styleable#TextView_shadowRadius
+ * @attr ref android.R.styleable#TextView_autoLink
+ * @attr ref android.R.styleable#TextView_linksClickable
+ * @attr ref android.R.styleable#TextView_numeric
+ * @attr ref android.R.styleable#TextView_digits
+ * @attr ref android.R.styleable#TextView_phoneNumber
+ * @attr ref android.R.styleable#TextView_inputMethod
+ * @attr ref android.R.styleable#TextView_capitalize
+ * @attr ref android.R.styleable#TextView_autoText
+ * @attr ref android.R.styleable#TextView_editable
+ * @attr ref android.R.styleable#TextView_freezesText
+ * @attr ref android.R.styleable#TextView_ellipsize
+ * @attr ref android.R.styleable#TextView_drawableTop
+ * @attr ref android.R.styleable#TextView_drawableBottom
+ * @attr ref android.R.styleable#TextView_drawableRight
+ * @attr ref android.R.styleable#TextView_drawableLeft
+ * @attr ref android.R.styleable#TextView_drawableStart
+ * @attr ref android.R.styleable#TextView_drawableEnd
+ * @attr ref android.R.styleable#TextView_drawablePadding
+ * @attr ref android.R.styleable#TextView_drawableTint
+ * @attr ref android.R.styleable#TextView_drawableTintMode
+ * @attr ref android.R.styleable#TextView_lineSpacingExtra
+ * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+ * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+ * @attr ref android.R.styleable#TextView_inputType
+ * @attr ref android.R.styleable#TextView_imeOptions
+ * @attr ref android.R.styleable#TextView_privateImeOptions
+ * @attr ref android.R.styleable#TextView_imeActionLabel
+ * @attr ref android.R.styleable#TextView_imeActionId
+ * @attr ref android.R.styleable#TextView_editorExtras
+ * @attr ref android.R.styleable#TextView_elegantTextHeight
+ * @attr ref android.R.styleable#TextView_letterSpacing
+ * @attr ref android.R.styleable#TextView_fontFeatureSettings
+ * @attr ref android.R.styleable#TextView_breakStrategy
+ * @attr ref android.R.styleable#TextView_hyphenationFrequency
+ * @attr ref android.R.styleable#TextView_autoSizeTextType
+ * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+ * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+ * @attr ref android.R.styleable#TextView_autoSizePresetSizes
+ */
+@RemoteView
+public class TextView extends View implements ViewTreeObserver.OnPreDrawListener {
+    static final String LOG_TAG = "TextView";
+    static final boolean DEBUG_EXTRACT = false;
+    static final boolean DEBUG_AUTOFILL = false;
+    private static final float[] TEMP_POSITION = new float[2];
+
+    // Enum for the "typeface" XML parameter.
+    // TODO: How can we get this from the XML instead of hardcoding it here?
+    private static final int SANS = 1;
+    private static final int SERIF = 2;
+    private static final int MONOSPACE = 3;
+
+    // Enum for the "ellipsize" XML parameter.
+    private static final int ELLIPSIZE_NOT_SET = -1;
+    private static final int ELLIPSIZE_NONE = 0;
+    private static final int ELLIPSIZE_START = 1;
+    private static final int ELLIPSIZE_MIDDLE = 2;
+    private static final int ELLIPSIZE_END = 3;
+    private static final int ELLIPSIZE_MARQUEE = 4;
+
+    // Bitfield for the "numeric" XML parameter.
+    // TODO: How can we get this from the XML instead of hardcoding it here?
+    private static final int SIGNED = 2;
+    private static final int DECIMAL = 4;
+
+    /**
+     * Draw marquee text with fading edges as usual
+     */
+    private static final int MARQUEE_FADE_NORMAL = 0;
+
+    /**
+     * Draw marquee text as ellipsize end while inactive instead of with the fade.
+     * (Useful for devices where the fade can be expensive if overdone)
+     */
+    private static final int MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS = 1;
+
+    /**
+     * Draw marquee text with fading edges because it is currently active/animating.
+     */
+    private static final int MARQUEE_FADE_SWITCH_SHOW_FADE = 2;
+
+    private static final int LINES = 1;
+    private static final int EMS = LINES;
+    private static final int PIXELS = 2;
+
+    private static final RectF TEMP_RECTF = new RectF();
+
+    /** @hide */
+    static final int VERY_WIDE = 1024 * 1024; // XXX should be much larger
+    private static final int ANIMATED_SCROLL_GAP = 250;
+
+    private static final InputFilter[] NO_FILTERS = new InputFilter[0];
+    private static final Spanned EMPTY_SPANNED = new SpannedString("");
+
+    private static final int CHANGE_WATCHER_PRIORITY = 100;
+
+    // New state used to change background based on whether this TextView is multiline.
+    private static final int[] MULTILINE_STATE_SET = { R.attr.state_multiline };
+
+    // Accessibility action to share selected text.
+    private static final int ACCESSIBILITY_ACTION_SHARE = 0x10000000;
+
+    /**
+     * @hide
+     */
+    // Accessibility action start id for "process text" actions.
+    static final int ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID = 0x10000100;
+
+    /**
+     * @hide
+     */
+    static final int PROCESS_TEXT_REQUEST_CODE = 100;
+
+    /**
+     *  Return code of {@link #doKeyDown}.
+     */
+    private static final int KEY_EVENT_NOT_HANDLED = 0;
+    private static final int KEY_EVENT_HANDLED = -1;
+    private static final int KEY_DOWN_HANDLED_BY_KEY_LISTENER = 1;
+    private static final int KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD = 2;
+
+    private static final int FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY = 500;
+
+    // System wide time for last cut, copy or text changed action.
+    static long sLastCutCopyOrTextChangedTime;
+
+    private ColorStateList mTextColor;
+    private ColorStateList mHintTextColor;
+    private ColorStateList mLinkTextColor;
+    @ViewDebug.ExportedProperty(category = "text")
+    private int mCurTextColor;
+    private int mCurHintTextColor;
+    private boolean mFreezesText;
+
+    private Editable.Factory mEditableFactory = Editable.Factory.getInstance();
+    private Spannable.Factory mSpannableFactory = Spannable.Factory.getInstance();
+
+    private float mShadowRadius, mShadowDx, mShadowDy;
+    private int mShadowColor;
+
+    private boolean mPreDrawRegistered;
+    private boolean mPreDrawListenerDetached;
+
+    private TextClassifier mTextClassifier;
+
+    // A flag to prevent repeated movements from escaping the enclosing text view. The idea here is
+    // that if a user is holding down a movement key to traverse text, we shouldn't also traverse
+    // the view hierarchy. On the other hand, if the user is using the movement key to traverse
+    // views (i.e. the first movement was to traverse out of this view, or this view was traversed
+    // into by the user holding the movement key down) then we shouldn't prevent the focus from
+    // changing.
+    private boolean mPreventDefaultMovement;
+
+    private TextUtils.TruncateAt mEllipsize;
+
+    static class Drawables {
+        static final int LEFT = 0;
+        static final int TOP = 1;
+        static final int RIGHT = 2;
+        static final int BOTTOM = 3;
+
+        static final int DRAWABLE_NONE = -1;
+        static final int DRAWABLE_RIGHT = 0;
+        static final int DRAWABLE_LEFT = 1;
+
+        final Rect mCompoundRect = new Rect();
+
+        final Drawable[] mShowing = new Drawable[4];
+
+        ColorStateList mTintList;
+        PorterDuff.Mode mTintMode;
+        boolean mHasTint;
+        boolean mHasTintMode;
+
+        Drawable mDrawableStart, mDrawableEnd, mDrawableError, mDrawableTemp;
+        Drawable mDrawableLeftInitial, mDrawableRightInitial;
+
+        boolean mIsRtlCompatibilityMode;
+        boolean mOverride;
+
+        int mDrawableSizeTop, mDrawableSizeBottom, mDrawableSizeLeft, mDrawableSizeRight,
+                mDrawableSizeStart, mDrawableSizeEnd, mDrawableSizeError, mDrawableSizeTemp;
+
+        int mDrawableWidthTop, mDrawableWidthBottom, mDrawableHeightLeft, mDrawableHeightRight,
+                mDrawableHeightStart, mDrawableHeightEnd, mDrawableHeightError, mDrawableHeightTemp;
+
+        int mDrawablePadding;
+
+        int mDrawableSaved = DRAWABLE_NONE;
+
+        public Drawables(Context context) {
+            final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+            mIsRtlCompatibilityMode = targetSdkVersion < VERSION_CODES.JELLY_BEAN_MR1
+                    || !context.getApplicationInfo().hasRtlSupport();
+            mOverride = false;
+        }
+
+        /**
+         * @return {@code true} if this object contains metadata that needs to
+         *         be retained, {@code false} otherwise
+         */
+        public boolean hasMetadata() {
+            return mDrawablePadding != 0 || mHasTintMode || mHasTint;
+        }
+
+        /**
+         * Updates the list of displayed drawables to account for the current
+         * layout direction.
+         *
+         * @param layoutDirection the current layout direction
+         * @return {@code true} if the displayed drawables changed
+         */
+        public boolean resolveWithLayoutDirection(int layoutDirection) {
+            final Drawable previousLeft = mShowing[Drawables.LEFT];
+            final Drawable previousRight = mShowing[Drawables.RIGHT];
+
+            // First reset "left" and "right" drawables to their initial values
+            mShowing[Drawables.LEFT] = mDrawableLeftInitial;
+            mShowing[Drawables.RIGHT] = mDrawableRightInitial;
+
+            if (mIsRtlCompatibilityMode) {
+                // Use "start" drawable as "left" drawable if the "left" drawable was not defined
+                if (mDrawableStart != null && mShowing[Drawables.LEFT] == null) {
+                    mShowing[Drawables.LEFT] = mDrawableStart;
+                    mDrawableSizeLeft = mDrawableSizeStart;
+                    mDrawableHeightLeft = mDrawableHeightStart;
+                }
+                // Use "end" drawable as "right" drawable if the "right" drawable was not defined
+                if (mDrawableEnd != null && mShowing[Drawables.RIGHT] == null) {
+                    mShowing[Drawables.RIGHT] = mDrawableEnd;
+                    mDrawableSizeRight = mDrawableSizeEnd;
+                    mDrawableHeightRight = mDrawableHeightEnd;
+                }
+            } else {
+                // JB-MR1+ normal case: "start" / "end" drawables are overriding "left" / "right"
+                // drawable if and only if they have been defined
+                switch(layoutDirection) {
+                    case LAYOUT_DIRECTION_RTL:
+                        if (mOverride) {
+                            mShowing[Drawables.RIGHT] = mDrawableStart;
+                            mDrawableSizeRight = mDrawableSizeStart;
+                            mDrawableHeightRight = mDrawableHeightStart;
+
+                            mShowing[Drawables.LEFT] = mDrawableEnd;
+                            mDrawableSizeLeft = mDrawableSizeEnd;
+                            mDrawableHeightLeft = mDrawableHeightEnd;
+                        }
+                        break;
+
+                    case LAYOUT_DIRECTION_LTR:
+                    default:
+                        if (mOverride) {
+                            mShowing[Drawables.LEFT] = mDrawableStart;
+                            mDrawableSizeLeft = mDrawableSizeStart;
+                            mDrawableHeightLeft = mDrawableHeightStart;
+
+                            mShowing[Drawables.RIGHT] = mDrawableEnd;
+                            mDrawableSizeRight = mDrawableSizeEnd;
+                            mDrawableHeightRight = mDrawableHeightEnd;
+                        }
+                        break;
+                }
+            }
+
+            applyErrorDrawableIfNeeded(layoutDirection);
+
+            return mShowing[Drawables.LEFT] != previousLeft
+                    || mShowing[Drawables.RIGHT] != previousRight;
+        }
+
+        public void setErrorDrawable(Drawable dr, TextView tv) {
+            if (mDrawableError != dr && mDrawableError != null) {
+                mDrawableError.setCallback(null);
+            }
+            mDrawableError = dr;
+
+            if (mDrawableError != null) {
+                final Rect compoundRect = mCompoundRect;
+                final int[] state = tv.getDrawableState();
+
+                mDrawableError.setState(state);
+                mDrawableError.copyBounds(compoundRect);
+                mDrawableError.setCallback(tv);
+                mDrawableSizeError = compoundRect.width();
+                mDrawableHeightError = compoundRect.height();
+            } else {
+                mDrawableSizeError = mDrawableHeightError = 0;
+            }
+        }
+
+        private void applyErrorDrawableIfNeeded(int layoutDirection) {
+            // first restore the initial state if needed
+            switch (mDrawableSaved) {
+                case DRAWABLE_LEFT:
+                    mShowing[Drawables.LEFT] = mDrawableTemp;
+                    mDrawableSizeLeft = mDrawableSizeTemp;
+                    mDrawableHeightLeft = mDrawableHeightTemp;
+                    break;
+                case DRAWABLE_RIGHT:
+                    mShowing[Drawables.RIGHT] = mDrawableTemp;
+                    mDrawableSizeRight = mDrawableSizeTemp;
+                    mDrawableHeightRight = mDrawableHeightTemp;
+                    break;
+                case DRAWABLE_NONE:
+                default:
+            }
+            // then, if needed, assign the Error drawable to the correct location
+            if (mDrawableError != null) {
+                switch(layoutDirection) {
+                    case LAYOUT_DIRECTION_RTL:
+                        mDrawableSaved = DRAWABLE_LEFT;
+
+                        mDrawableTemp = mShowing[Drawables.LEFT];
+                        mDrawableSizeTemp = mDrawableSizeLeft;
+                        mDrawableHeightTemp = mDrawableHeightLeft;
+
+                        mShowing[Drawables.LEFT] = mDrawableError;
+                        mDrawableSizeLeft = mDrawableSizeError;
+                        mDrawableHeightLeft = mDrawableHeightError;
+                        break;
+                    case LAYOUT_DIRECTION_LTR:
+                    default:
+                        mDrawableSaved = DRAWABLE_RIGHT;
+
+                        mDrawableTemp = mShowing[Drawables.RIGHT];
+                        mDrawableSizeTemp = mDrawableSizeRight;
+                        mDrawableHeightTemp = mDrawableHeightRight;
+
+                        mShowing[Drawables.RIGHT] = mDrawableError;
+                        mDrawableSizeRight = mDrawableSizeError;
+                        mDrawableHeightRight = mDrawableHeightError;
+                        break;
+                }
+            }
+        }
+    }
+
+    Drawables mDrawables;
+
+    private CharWrapper mCharWrapper;
+
+    private Marquee mMarquee;
+    private boolean mRestartMarquee;
+
+    private int mMarqueeRepeatLimit = 3;
+
+    private int mLastLayoutDirection = -1;
+
+    /**
+     * On some devices the fading edges add a performance penalty if used
+     * extensively in the same layout. This mode indicates how the marquee
+     * is currently being shown, if applicable. (mEllipsize will == MARQUEE)
+     */
+    private int mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+
+    /**
+     * When mMarqueeFadeMode is not MARQUEE_FADE_NORMAL, this stores
+     * the layout that should be used when the mode switches.
+     */
+    private Layout mSavedMarqueeModeLayout;
+
+    @ViewDebug.ExportedProperty(category = "text")
+    private CharSequence mText;
+    private CharSequence mTransformed;
+    private BufferType mBufferType = BufferType.NORMAL;
+
+    private CharSequence mHint;
+    private Layout mHintLayout;
+
+    private MovementMethod mMovement;
+
+    private TransformationMethod mTransformation;
+    private boolean mAllowTransformationLengthChange;
+    private ChangeWatcher mChangeWatcher;
+
+    private ArrayList<TextWatcher> mListeners;
+
+    // display attributes
+    private final TextPaint mTextPaint;
+    private boolean mUserSetTextScaleX;
+    private Layout mLayout;
+    private boolean mLocalesChanged = false;
+
+    // True if setKeyListener() has been explicitly called
+    private boolean mListenerChanged = false;
+    // True if internationalized input should be used for numbers and date and time.
+    private final boolean mUseInternationalizedInput;
+    // True if fallback fonts that end up getting used should be allowed to affect line spacing.
+    /* package */ final boolean mUseFallbackLineSpacing;
+
+    @ViewDebug.ExportedProperty(category = "text")
+    private int mGravity = Gravity.TOP | Gravity.START;
+    private boolean mHorizontallyScrolling;
+
+    private int mAutoLinkMask;
+    private boolean mLinksClickable = true;
+
+    private float mSpacingMult = 1.0f;
+    private float mSpacingAdd = 0.0f;
+
+    private int mBreakStrategy;
+    private int mHyphenationFrequency;
+    private int mJustificationMode;
+
+    private int mMaximum = Integer.MAX_VALUE;
+    private int mMaxMode = LINES;
+    private int mMinimum = 0;
+    private int mMinMode = LINES;
+
+    private int mOldMaximum = mMaximum;
+    private int mOldMaxMode = mMaxMode;
+
+    private int mMaxWidth = Integer.MAX_VALUE;
+    private int mMaxWidthMode = PIXELS;
+    private int mMinWidth = 0;
+    private int mMinWidthMode = PIXELS;
+
+    private boolean mSingleLine;
+    private int mDesiredHeightAtMeasure = -1;
+    private boolean mIncludePad = true;
+    private int mDeferScroll = -1;
+
+    // tmp primitives, so we don't alloc them on each draw
+    private Rect mTempRect;
+    private long mLastScroll;
+    private Scroller mScroller;
+    private TextPaint mTempTextPaint;
+
+    private BoringLayout.Metrics mBoring, mHintBoring;
+    private BoringLayout mSavedLayout, mSavedHintLayout;
+
+    private TextDirectionHeuristic mTextDir;
+
+    private InputFilter[] mFilters = NO_FILTERS;
+
+    private volatile Locale mCurrentSpellCheckerLocaleCache;
+
+    // It is possible to have a selection even when mEditor is null (programmatically set, like when
+    // a link is pressed). These highlight-related fields do not go in mEditor.
+    int mHighlightColor = 0x6633B5E5;
+    private Path mHighlightPath;
+    private final Paint mHighlightPaint;
+    private boolean mHighlightPathBogus = true;
+
+    // Although these fields are specific to editable text, they are not added to Editor because
+    // they are defined by the TextView's style and are theme-dependent.
+    int mCursorDrawableRes;
+    // These six fields, could be moved to Editor, since we know their default values and we
+    // could condition the creation of the Editor to a non standard value. This is however
+    // brittle since the hardcoded values here (such as
+    // com.android.internal.R.drawable.text_select_handle_left) would have to be updated if the
+    // default style is modified.
+    int mTextSelectHandleLeftRes;
+    int mTextSelectHandleRightRes;
+    int mTextSelectHandleRes;
+    int mTextEditSuggestionItemLayout;
+    int mTextEditSuggestionContainerLayout;
+    int mTextEditSuggestionHighlightStyle;
+
+    /**
+     * {@link EditText} specific data, created on demand when one of the Editor fields is used.
+     * See {@link #createEditorIfNeeded()}.
+     */
+    private Editor mEditor;
+
+    private static final int DEVICE_PROVISIONED_UNKNOWN = 0;
+    private static final int DEVICE_PROVISIONED_NO = 1;
+    private static final int DEVICE_PROVISIONED_YES = 2;
+
+    /**
+     * Some special options such as sharing selected text should only be shown if the device
+     * is provisioned. Only check the provisioned state once for a given view instance.
+     */
+    private int mDeviceProvisionedState = DEVICE_PROVISIONED_UNKNOWN;
+
+    /**
+     * The TextView does not auto-size text (default).
+     */
+    public static final int AUTO_SIZE_TEXT_TYPE_NONE = 0;
+
+    /**
+     * The TextView scales text size both horizontally and vertically to fit within the
+     * container.
+     */
+    public static final int AUTO_SIZE_TEXT_TYPE_UNIFORM = 1;
+
+    /** @hide */
+    @IntDef({AUTO_SIZE_TEXT_TYPE_NONE, AUTO_SIZE_TEXT_TYPE_UNIFORM})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface AutoSizeTextType {}
+    // Default minimum size for auto-sizing text in scaled pixels.
+    private static final int DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP = 12;
+    // Default maximum size for auto-sizing text in scaled pixels.
+    private static final int DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP = 112;
+    // Default value for the step size in pixels.
+    private static final int DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX = 1;
+    // Use this to specify that any of the auto-size configuration int values have not been set.
+    private static final float UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE = -1f;
+    // Auto-size text type.
+    private int mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+    // Specify if auto-size text is needed.
+    private boolean mNeedsAutoSizeText = false;
+    // Step size for auto-sizing in pixels.
+    private float mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+    // Minimum text size for auto-sizing in pixels.
+    private float mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+    // Maximum text size for auto-sizing in pixels.
+    private float mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+    // Contains a (specified or computed) distinct sorted set of text sizes in pixels to pick from
+    // when auto-sizing text.
+    private int[] mAutoSizeTextSizesInPx = EmptyArray.INT;
+    // Specifies whether auto-size should use the provided auto size steps set or if it should
+    // build the steps set using mAutoSizeMinTextSizeInPx, mAutoSizeMaxTextSizeInPx and
+    // mAutoSizeStepGranularityInPx.
+    private boolean mHasPresetAutoSizeValues = false;
+
+    // Indicates whether the text was set from resources or dynamically, so it can be used to
+    // sanitize autofill requests.
+    private boolean mTextFromResource = false;
+
+    /**
+     * Kick-start the font cache for the zygote process (to pay the cost of
+     * initializing freetype for our default font only once).
+     * @hide
+     */
+    public static void preloadFontCache() {
+        Paint p = new Paint();
+        p.setAntiAlias(true);
+        // Ensure that the Typeface is loaded here.
+        // Typically, Typeface is preloaded by zygote but not on all devices, e.g. Android Auto.
+        // So, sets Typeface.DEFAULT explicitly here for ensuring that the Typeface is loaded here
+        // since Paint.measureText can not be called without Typeface static initializer.
+        p.setTypeface(Typeface.DEFAULT);
+        // We don't care about the result, just the side-effect of measuring.
+        p.measureText("H");
+    }
+
+    /**
+     * Interface definition for a callback to be invoked when an action is
+     * performed on the editor.
+     */
+    public interface OnEditorActionListener {
+        /**
+         * Called when an action is being performed.
+         *
+         * @param v The view that was clicked.
+         * @param actionId Identifier of the action.  This will be either the
+         * identifier you supplied, or {@link EditorInfo#IME_NULL
+         * EditorInfo.IME_NULL} if being called due to the enter key
+         * being pressed.
+         * @param event If triggered by an enter key, this is the event;
+         * otherwise, this is null.
+         * @return Return true if you have consumed the action, else false.
+         */
+        boolean onEditorAction(TextView v, int actionId, KeyEvent event);
+    }
+
+    public TextView(Context context) {
+        this(context, null);
+    }
+
+    public TextView(Context context, @Nullable AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.textViewStyle);
+    }
+
+    public TextView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    @SuppressWarnings("deprecation")
+    public TextView(
+            Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // TextView is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        mText = "";
+
+        final Resources res = getResources();
+        final CompatibilityInfo compat = res.getCompatibilityInfo();
+
+        mTextPaint = new TextPaint(Paint.ANTI_ALIAS_FLAG);
+        mTextPaint.density = res.getDisplayMetrics().density;
+        mTextPaint.setCompatibilityScaling(compat.applicationScale);
+
+        mHighlightPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
+        mHighlightPaint.setCompatibilityScaling(compat.applicationScale);
+
+        mMovement = getDefaultMovementMethod();
+
+        mTransformation = null;
+
+        final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
+        attributes.mTextColor = ColorStateList.valueOf(0xFF000000);
+        attributes.mTextSize = 15;
+        mBreakStrategy = Layout.BREAK_STRATEGY_SIMPLE;
+        mHyphenationFrequency = Layout.HYPHENATION_FREQUENCY_NONE;
+        mJustificationMode = Layout.JUSTIFICATION_MODE_NONE;
+
+        final Resources.Theme theme = context.getTheme();
+
+        /*
+         * Look the appearance up without checking first if it exists because
+         * almost every TextView has one and it greatly simplifies the logic
+         * to be able to parse the appearance first and then let specific tags
+         * for this View override it.
+         */
+        TypedArray a = theme.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.TextViewAppearance, defStyleAttr, defStyleRes);
+        TypedArray appearance = null;
+        int ap = a.getResourceId(
+                com.android.internal.R.styleable.TextViewAppearance_textAppearance, -1);
+        a.recycle();
+        if (ap != -1) {
+            appearance = theme.obtainStyledAttributes(
+                    ap, com.android.internal.R.styleable.TextAppearance);
+        }
+        if (appearance != null) {
+            readTextAppearance(context, appearance, attributes, false /* styleArray */);
+            attributes.mFontFamilyExplicit = false;
+            appearance.recycle();
+        }
+
+        boolean editable = getDefaultEditable();
+        CharSequence inputMethod = null;
+        int numeric = 0;
+        CharSequence digits = null;
+        boolean phone = false;
+        boolean autotext = false;
+        int autocap = -1;
+        int buffertype = 0;
+        boolean selectallonfocus = false;
+        Drawable drawableLeft = null, drawableTop = null, drawableRight = null,
+                drawableBottom = null, drawableStart = null, drawableEnd = null;
+        ColorStateList drawableTint = null;
+        PorterDuff.Mode drawableTintMode = null;
+        int drawablePadding = 0;
+        int ellipsize = ELLIPSIZE_NOT_SET;
+        boolean singleLine = false;
+        int maxlength = -1;
+        CharSequence text = "";
+        CharSequence hint = null;
+        boolean password = false;
+        float autoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        float autoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        float autoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        int inputType = EditorInfo.TYPE_NULL;
+        a = theme.obtainStyledAttributes(
+                    attrs, com.android.internal.R.styleable.TextView, defStyleAttr, defStyleRes);
+
+        readTextAppearance(context, a, attributes, true /* styleArray */);
+
+        int n = a.getIndexCount();
+
+        boolean fromResourceId = false;
+        for (int i = 0; i < n; i++) {
+            int attr = a.getIndex(i);
+
+            switch (attr) {
+                case com.android.internal.R.styleable.TextView_editable:
+                    editable = a.getBoolean(attr, editable);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_inputMethod:
+                    inputMethod = a.getText(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_numeric:
+                    numeric = a.getInt(attr, numeric);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_digits:
+                    digits = a.getText(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_phoneNumber:
+                    phone = a.getBoolean(attr, phone);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoText:
+                    autotext = a.getBoolean(attr, autotext);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_capitalize:
+                    autocap = a.getInt(attr, autocap);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_bufferType:
+                    buffertype = a.getInt(attr, buffertype);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_selectAllOnFocus:
+                    selectallonfocus = a.getBoolean(attr, selectallonfocus);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoLink:
+                    mAutoLinkMask = a.getInt(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_linksClickable:
+                    mLinksClickable = a.getBoolean(attr, true);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableLeft:
+                    drawableLeft = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableTop:
+                    drawableTop = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableRight:
+                    drawableRight = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableBottom:
+                    drawableBottom = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableStart:
+                    drawableStart = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableEnd:
+                    drawableEnd = a.getDrawable(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableTint:
+                    drawableTint = a.getColorStateList(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawableTintMode:
+                    drawableTintMode = Drawable.parseTintMode(a.getInt(attr, -1), drawableTintMode);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_drawablePadding:
+                    drawablePadding = a.getDimensionPixelSize(attr, drawablePadding);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_maxLines:
+                    setMaxLines(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_maxHeight:
+                    setMaxHeight(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_lines:
+                    setLines(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_height:
+                    setHeight(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_minLines:
+                    setMinLines(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_minHeight:
+                    setMinHeight(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_maxEms:
+                    setMaxEms(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_maxWidth:
+                    setMaxWidth(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_ems:
+                    setEms(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_width:
+                    setWidth(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_minEms:
+                    setMinEms(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_minWidth:
+                    setMinWidth(a.getDimensionPixelSize(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_gravity:
+                    setGravity(a.getInt(attr, -1));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_hint:
+                    hint = a.getText(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_text:
+                    fromResourceId = true;
+                    text = a.getText(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_scrollHorizontally:
+                    if (a.getBoolean(attr, false)) {
+                        setHorizontallyScrolling(true);
+                    }
+                    break;
+
+                case com.android.internal.R.styleable.TextView_singleLine:
+                    singleLine = a.getBoolean(attr, singleLine);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_ellipsize:
+                    ellipsize = a.getInt(attr, ellipsize);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_marqueeRepeatLimit:
+                    setMarqueeRepeatLimit(a.getInt(attr, mMarqueeRepeatLimit));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_includeFontPadding:
+                    if (!a.getBoolean(attr, true)) {
+                        setIncludeFontPadding(false);
+                    }
+                    break;
+
+                case com.android.internal.R.styleable.TextView_cursorVisible:
+                    if (!a.getBoolean(attr, true)) {
+                        setCursorVisible(false);
+                    }
+                    break;
+
+                case com.android.internal.R.styleable.TextView_maxLength:
+                    maxlength = a.getInt(attr, -1);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textScaleX:
+                    setTextScaleX(a.getFloat(attr, 1.0f));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_freezesText:
+                    mFreezesText = a.getBoolean(attr, false);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_enabled:
+                    setEnabled(a.getBoolean(attr, isEnabled()));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_password:
+                    password = a.getBoolean(attr, password);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_lineSpacingExtra:
+                    mSpacingAdd = a.getDimensionPixelSize(attr, (int) mSpacingAdd);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_lineSpacingMultiplier:
+                    mSpacingMult = a.getFloat(attr, mSpacingMult);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_inputType:
+                    inputType = a.getInt(attr, EditorInfo.TYPE_NULL);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_allowUndo:
+                    createEditorIfNeeded();
+                    mEditor.mAllowUndo = a.getBoolean(attr, true);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_imeOptions:
+                    createEditorIfNeeded();
+                    mEditor.createInputContentTypeIfNeeded();
+                    mEditor.mInputContentType.imeOptions = a.getInt(attr,
+                            mEditor.mInputContentType.imeOptions);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_imeActionLabel:
+                    createEditorIfNeeded();
+                    mEditor.createInputContentTypeIfNeeded();
+                    mEditor.mInputContentType.imeActionLabel = a.getText(attr);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_imeActionId:
+                    createEditorIfNeeded();
+                    mEditor.createInputContentTypeIfNeeded();
+                    mEditor.mInputContentType.imeActionId = a.getInt(attr,
+                            mEditor.mInputContentType.imeActionId);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_privateImeOptions:
+                    setPrivateImeOptions(a.getString(attr));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_editorExtras:
+                    try {
+                        setInputExtras(a.getResourceId(attr, 0));
+                    } catch (XmlPullParserException e) {
+                        Log.w(LOG_TAG, "Failure reading input extras", e);
+                    } catch (IOException e) {
+                        Log.w(LOG_TAG, "Failure reading input extras", e);
+                    }
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textCursorDrawable:
+                    mCursorDrawableRes = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textSelectHandleLeft:
+                    mTextSelectHandleLeftRes = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textSelectHandleRight:
+                    mTextSelectHandleRightRes = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textSelectHandle:
+                    mTextSelectHandleRes = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textEditSuggestionItemLayout:
+                    mTextEditSuggestionItemLayout = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textEditSuggestionContainerLayout:
+                    mTextEditSuggestionContainerLayout = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textEditSuggestionHighlightStyle:
+                    mTextEditSuggestionHighlightStyle = a.getResourceId(attr, 0);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_textIsSelectable:
+                    setTextIsSelectable(a.getBoolean(attr, false));
+                    break;
+
+                case com.android.internal.R.styleable.TextView_breakStrategy:
+                    mBreakStrategy = a.getInt(attr, Layout.BREAK_STRATEGY_SIMPLE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_hyphenationFrequency:
+                    mHyphenationFrequency = a.getInt(attr, Layout.HYPHENATION_FREQUENCY_NONE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoSizeTextType:
+                    mAutoSizeTextType = a.getInt(attr, AUTO_SIZE_TEXT_TYPE_NONE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoSizeStepGranularity:
+                    autoSizeStepGranularityInPx = a.getDimension(attr,
+                        UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoSizeMinTextSize:
+                    autoSizeMinTextSizeInPx = a.getDimension(attr,
+                        UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoSizeMaxTextSize:
+                    autoSizeMaxTextSizeInPx = a.getDimension(attr,
+                        UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE);
+                    break;
+
+                case com.android.internal.R.styleable.TextView_autoSizePresetSizes:
+                    final int autoSizeStepSizeArrayResId = a.getResourceId(attr, 0);
+                    if (autoSizeStepSizeArrayResId > 0) {
+                        final TypedArray autoSizePresetTextSizes = a.getResources()
+                                .obtainTypedArray(autoSizeStepSizeArrayResId);
+                        setupAutoSizeUniformPresetSizes(autoSizePresetTextSizes);
+                        autoSizePresetTextSizes.recycle();
+                    }
+                    break;
+                case com.android.internal.R.styleable.TextView_justificationMode:
+                    mJustificationMode = a.getInt(attr, Layout.JUSTIFICATION_MODE_NONE);
+                    break;
+            }
+        }
+
+        a.recycle();
+
+        BufferType bufferType = BufferType.EDITABLE;
+
+        final int variation =
+                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+        final boolean passwordInputType = variation
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD);
+        final boolean webPasswordInputType = variation
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD);
+        final boolean numberPasswordInputType = variation
+                == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
+
+        final int targetSdkVersion = context.getApplicationInfo().targetSdkVersion;
+        mUseInternationalizedInput = targetSdkVersion >= VERSION_CODES.O;
+        // TODO: replace CUR_DEVELOPMENT with P once P is added to android.os.Build.VERSION_CODES.
+        // STOPSHIP if the above TODO is not done.
+        mUseFallbackLineSpacing = targetSdkVersion >= VERSION_CODES.CUR_DEVELOPMENT;
+
+        if (inputMethod != null) {
+            Class<?> c;
+
+            try {
+                c = Class.forName(inputMethod.toString());
+            } catch (ClassNotFoundException ex) {
+                throw new RuntimeException(ex);
+            }
+
+            try {
+                createEditorIfNeeded();
+                mEditor.mKeyListener = (KeyListener) c.newInstance();
+            } catch (InstantiationException ex) {
+                throw new RuntimeException(ex);
+            } catch (IllegalAccessException ex) {
+                throw new RuntimeException(ex);
+            }
+            try {
+                mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
+                        ? inputType
+                        : mEditor.mKeyListener.getInputType();
+            } catch (IncompatibleClassChangeError e) {
+                mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+            }
+        } else if (digits != null) {
+            createEditorIfNeeded();
+            mEditor.mKeyListener = DigitsKeyListener.getInstance(digits.toString());
+            // If no input type was specified, we will default to generic
+            // text, since we can't tell the IME about the set of digits
+            // that was selected.
+            mEditor.mInputType = inputType != EditorInfo.TYPE_NULL
+                    ? inputType : EditorInfo.TYPE_CLASS_TEXT;
+        } else if (inputType != EditorInfo.TYPE_NULL) {
+            setInputType(inputType, true);
+            // If set, the input type overrides what was set using the deprecated singleLine flag.
+            singleLine = !isMultilineInputType(inputType);
+        } else if (phone) {
+            createEditorIfNeeded();
+            mEditor.mKeyListener = DialerKeyListener.getInstance();
+            mEditor.mInputType = inputType = EditorInfo.TYPE_CLASS_PHONE;
+        } else if (numeric != 0) {
+            createEditorIfNeeded();
+            mEditor.mKeyListener = DigitsKeyListener.getInstance(
+                    null,  // locale
+                    (numeric & SIGNED) != 0,
+                    (numeric & DECIMAL) != 0);
+            inputType = mEditor.mKeyListener.getInputType();
+            mEditor.mInputType = inputType;
+        } else if (autotext || autocap != -1) {
+            TextKeyListener.Capitalize cap;
+
+            inputType = EditorInfo.TYPE_CLASS_TEXT;
+
+            switch (autocap) {
+                case 1:
+                    cap = TextKeyListener.Capitalize.SENTENCES;
+                    inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES;
+                    break;
+
+                case 2:
+                    cap = TextKeyListener.Capitalize.WORDS;
+                    inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS;
+                    break;
+
+                case 3:
+                    cap = TextKeyListener.Capitalize.CHARACTERS;
+                    inputType |= EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS;
+                    break;
+
+                default:
+                    cap = TextKeyListener.Capitalize.NONE;
+                    break;
+            }
+
+            createEditorIfNeeded();
+            mEditor.mKeyListener = TextKeyListener.getInstance(autotext, cap);
+            mEditor.mInputType = inputType;
+        } else if (editable) {
+            createEditorIfNeeded();
+            mEditor.mKeyListener = TextKeyListener.getInstance();
+            mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+        } else if (isTextSelectable()) {
+            // Prevent text changes from keyboard.
+            if (mEditor != null) {
+                mEditor.mKeyListener = null;
+                mEditor.mInputType = EditorInfo.TYPE_NULL;
+            }
+            bufferType = BufferType.SPANNABLE;
+            // So that selection can be changed using arrow keys and touch is handled.
+            setMovementMethod(ArrowKeyMovementMethod.getInstance());
+        } else {
+            if (mEditor != null) mEditor.mKeyListener = null;
+
+            switch (buffertype) {
+                case 0:
+                    bufferType = BufferType.NORMAL;
+                    break;
+                case 1:
+                    bufferType = BufferType.SPANNABLE;
+                    break;
+                case 2:
+                    bufferType = BufferType.EDITABLE;
+                    break;
+            }
+        }
+
+        if (mEditor != null) {
+            mEditor.adjustInputType(password, passwordInputType, webPasswordInputType,
+                    numberPasswordInputType);
+        }
+
+        if (selectallonfocus) {
+            createEditorIfNeeded();
+            mEditor.mSelectAllOnFocus = true;
+
+            if (bufferType == BufferType.NORMAL) {
+                bufferType = BufferType.SPANNABLE;
+            }
+        }
+
+        // Set up the tint (if needed) before setting the drawables so that it
+        // gets applied correctly.
+        if (drawableTint != null || drawableTintMode != null) {
+            if (mDrawables == null) {
+                mDrawables = new Drawables(context);
+            }
+            if (drawableTint != null) {
+                mDrawables.mTintList = drawableTint;
+                mDrawables.mHasTint = true;
+            }
+            if (drawableTintMode != null) {
+                mDrawables.mTintMode = drawableTintMode;
+                mDrawables.mHasTintMode = true;
+            }
+        }
+
+        // This call will save the initial left/right drawables
+        setCompoundDrawablesWithIntrinsicBounds(
+                drawableLeft, drawableTop, drawableRight, drawableBottom);
+        setRelativeDrawablesIfNeeded(drawableStart, drawableEnd);
+        setCompoundDrawablePadding(drawablePadding);
+
+        // Same as setSingleLine(), but make sure the transformation method and the maximum number
+        // of lines of height are unchanged for multi-line TextViews.
+        setInputTypeSingleLine(singleLine);
+        applySingleLine(singleLine, singleLine, singleLine);
+
+        if (singleLine && getKeyListener() == null && ellipsize == ELLIPSIZE_NOT_SET) {
+            ellipsize = ELLIPSIZE_END;
+        }
+
+        switch (ellipsize) {
+            case ELLIPSIZE_START:
+                setEllipsize(TextUtils.TruncateAt.START);
+                break;
+            case ELLIPSIZE_MIDDLE:
+                setEllipsize(TextUtils.TruncateAt.MIDDLE);
+                break;
+            case ELLIPSIZE_END:
+                setEllipsize(TextUtils.TruncateAt.END);
+                break;
+            case ELLIPSIZE_MARQUEE:
+                if (ViewConfiguration.get(context).isFadingMarqueeEnabled()) {
+                    setHorizontalFadingEdgeEnabled(true);
+                    mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+                } else {
+                    setHorizontalFadingEdgeEnabled(false);
+                    mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+                }
+                setEllipsize(TextUtils.TruncateAt.MARQUEE);
+                break;
+        }
+
+        final boolean isPassword = password || passwordInputType || webPasswordInputType
+                || numberPasswordInputType;
+        final boolean isMonospaceEnforced = isPassword || (mEditor != null
+                && (mEditor.mInputType
+                & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION))
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD));
+        if (isMonospaceEnforced) {
+            attributes.mTypefaceIndex = MONOSPACE;
+        }
+
+        applyTextAppearance(attributes);
+
+        if (isPassword) {
+            setTransformationMethod(PasswordTransformationMethod.getInstance());
+        }
+
+        if (maxlength >= 0) {
+            setFilters(new InputFilter[] { new InputFilter.LengthFilter(maxlength) });
+        } else {
+            setFilters(NO_FILTERS);
+        }
+
+        setText(text, bufferType);
+        if (fromResourceId) {
+            mTextFromResource = true;
+        }
+
+        if (hint != null) setHint(hint);
+
+        /*
+         * Views are not normally clickable unless specified to be.
+         * However, TextViews that have input or movement methods *are*
+         * clickable by default. By setting clickable here, we implicitly set focusable as well
+         * if not overridden by the developer.
+         */
+        a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.View, defStyleAttr, defStyleRes);
+        boolean canInputOrMove = (mMovement != null || getKeyListener() != null);
+        boolean clickable = canInputOrMove || isClickable();
+        boolean longClickable = canInputOrMove || isLongClickable();
+        int focusable = getFocusable();
+
+        n = a.getIndexCount();
+        for (int i = 0; i < n; i++) {
+            int attr = a.getIndex(i);
+
+            switch (attr) {
+                case com.android.internal.R.styleable.View_focusable:
+                    TypedValue val = new TypedValue();
+                    if (a.getValue(attr, val)) {
+                        focusable = (val.type == TypedValue.TYPE_INT_BOOLEAN)
+                                ? (val.data == 0 ? NOT_FOCUSABLE : FOCUSABLE)
+                                : val.data;
+                    }
+                    break;
+
+                case com.android.internal.R.styleable.View_clickable:
+                    clickable = a.getBoolean(attr, clickable);
+                    break;
+
+                case com.android.internal.R.styleable.View_longClickable:
+                    longClickable = a.getBoolean(attr, longClickable);
+                    break;
+            }
+        }
+        a.recycle();
+
+        // Some apps were relying on the undefined behavior of focusable winning over
+        // focusableInTouchMode != focusable in TextViews if both were specified in XML (usually
+        // when starting with EditText and setting only focusable=false). To keep those apps from
+        // breaking, re-apply the focusable attribute here.
+        if (focusable != getFocusable()) {
+            setFocusable(focusable);
+        }
+        setClickable(clickable);
+        setLongClickable(longClickable);
+
+        if (mEditor != null) mEditor.prepareCursorControllers();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+
+        if (supportsAutoSizeText()) {
+            if (mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
+                // If uniform auto-size has been specified but preset values have not been set then
+                // replace the auto-size configuration values that have not been specified with the
+                // defaults.
+                if (!mHasPresetAutoSizeValues) {
+                    final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+
+                    if (autoSizeMinTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+                        autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+                                TypedValue.COMPLEX_UNIT_SP,
+                                DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
+                                displayMetrics);
+                    }
+
+                    if (autoSizeMaxTextSizeInPx == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+                        autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+                                TypedValue.COMPLEX_UNIT_SP,
+                                DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
+                                displayMetrics);
+                    }
+
+                    if (autoSizeStepGranularityInPx
+                            == UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE) {
+                        autoSizeStepGranularityInPx = DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX;
+                    }
+
+                    validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
+                            autoSizeMaxTextSizeInPx,
+                            autoSizeStepGranularityInPx);
+                }
+
+                setupAutoSizeText();
+            }
+        } else {
+            mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+        }
+    }
+
+    /**
+     * Specify whether this widget should automatically scale the text to try to perfectly fit
+     * within the layout bounds by using the default auto-size configuration.
+     *
+     * @param autoSizeTextType the type of auto-size. Must be one of
+     *        {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
+     *        {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
+     *
+     * @throws IllegalArgumentException if <code>autoSizeTextType</code> is none of the types above.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeTextType
+     *
+     * @see #getAutoSizeTextType()
+     */
+    public void setAutoSizeTextTypeWithDefaults(@AutoSizeTextType int autoSizeTextType) {
+        if (supportsAutoSizeText()) {
+            switch (autoSizeTextType) {
+                case AUTO_SIZE_TEXT_TYPE_NONE:
+                    clearAutoSizeConfiguration();
+                    break;
+                case AUTO_SIZE_TEXT_TYPE_UNIFORM:
+                    final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+                    final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            DEFAULT_AUTO_SIZE_MIN_TEXT_SIZE_IN_SP,
+                            displayMetrics);
+                    final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+                            TypedValue.COMPLEX_UNIT_SP,
+                            DEFAULT_AUTO_SIZE_MAX_TEXT_SIZE_IN_SP,
+                            displayMetrics);
+
+                    validateAndSetAutoSizeTextTypeUniformConfiguration(
+                            autoSizeMinTextSizeInPx,
+                            autoSizeMaxTextSizeInPx,
+                            DEFAULT_AUTO_SIZE_GRANULARITY_IN_PX);
+                    if (setupAutoSizeText()) {
+                        autoSizeText();
+                        invalidate();
+                    }
+                    break;
+                default:
+                    throw new IllegalArgumentException(
+                            "Unknown auto-size text type: " + autoSizeTextType);
+            }
+        }
+    }
+
+    /**
+     * Specify whether this widget should automatically scale the text to try to perfectly fit
+     * within the layout bounds. If all the configuration params are valid the type of auto-size is
+     * set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
+     *
+     * @param autoSizeMinTextSize the minimum text size available for auto-size
+     * @param autoSizeMaxTextSize the maximum text size available for auto-size
+     * @param autoSizeStepGranularity the auto-size step granularity. It is used in conjunction with
+     *                                the minimum and maximum text size in order to build the set of
+     *                                text sizes the system uses to choose from when auto-sizing
+     * @param unit the desired dimension unit for all sizes above. See {@link TypedValue} for the
+     *             possible dimension units
+     *
+     * @throws IllegalArgumentException if any of the configuration params are invalid.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeTextType
+     * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+     * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+     * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+     *
+     * @see #setAutoSizeTextTypeWithDefaults(int)
+     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+     * @see #getAutoSizeMinTextSize()
+     * @see #getAutoSizeMaxTextSize()
+     * @see #getAutoSizeStepGranularity()
+     * @see #getAutoSizeTextAvailableSizes()
+     */
+    public void setAutoSizeTextTypeUniformWithConfiguration(int autoSizeMinTextSize,
+            int autoSizeMaxTextSize, int autoSizeStepGranularity, int unit) {
+        if (supportsAutoSizeText()) {
+            final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+            final float autoSizeMinTextSizeInPx = TypedValue.applyDimension(
+                    unit, autoSizeMinTextSize, displayMetrics);
+            final float autoSizeMaxTextSizeInPx = TypedValue.applyDimension(
+                    unit, autoSizeMaxTextSize, displayMetrics);
+            final float autoSizeStepGranularityInPx = TypedValue.applyDimension(
+                    unit, autoSizeStepGranularity, displayMetrics);
+
+            validateAndSetAutoSizeTextTypeUniformConfiguration(autoSizeMinTextSizeInPx,
+                    autoSizeMaxTextSizeInPx,
+                    autoSizeStepGranularityInPx);
+
+            if (setupAutoSizeText()) {
+                autoSizeText();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Specify whether this widget should automatically scale the text to try to perfectly fit
+     * within the layout bounds. If at least one value from the <code>presetSizes</code> is valid
+     * then the type of auto-size is set to {@link #AUTO_SIZE_TEXT_TYPE_UNIFORM}.
+     *
+     * @param presetSizes an {@code int} array of sizes in pixels
+     * @param unit the desired dimension unit for the preset sizes above. See {@link TypedValue} for
+     *             the possible dimension units
+     *
+     * @throws IllegalArgumentException if all of the <code>presetSizes</code> are invalid.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeTextType
+     * @attr ref android.R.styleable#TextView_autoSizePresetSizes
+     *
+     * @see #setAutoSizeTextTypeWithDefaults(int)
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     * @see #getAutoSizeMinTextSize()
+     * @see #getAutoSizeMaxTextSize()
+     * @see #getAutoSizeTextAvailableSizes()
+     */
+    public void setAutoSizeTextTypeUniformWithPresetSizes(@NonNull int[] presetSizes, int unit) {
+        if (supportsAutoSizeText()) {
+            final int presetSizesLength = presetSizes.length;
+            if (presetSizesLength > 0) {
+                int[] presetSizesInPx = new int[presetSizesLength];
+
+                if (unit == TypedValue.COMPLEX_UNIT_PX) {
+                    presetSizesInPx = Arrays.copyOf(presetSizes, presetSizesLength);
+                } else {
+                    final DisplayMetrics displayMetrics = getResources().getDisplayMetrics();
+                    // Convert all to sizes to pixels.
+                    for (int i = 0; i < presetSizesLength; i++) {
+                        presetSizesInPx[i] = Math.round(TypedValue.applyDimension(unit,
+                            presetSizes[i], displayMetrics));
+                    }
+                }
+
+                mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(presetSizesInPx);
+                if (!setupAutoSizeUniformPresetSizesConfiguration()) {
+                    throw new IllegalArgumentException("None of the preset sizes is valid: "
+                            + Arrays.toString(presetSizes));
+                }
+            } else {
+                mHasPresetAutoSizeValues = false;
+            }
+
+            if (setupAutoSizeText()) {
+                autoSizeText();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Returns the type of auto-size set for this widget.
+     *
+     * @return an {@code int} corresponding to one of the auto-size types:
+     *         {@link TextView#AUTO_SIZE_TEXT_TYPE_NONE} or
+     *         {@link TextView#AUTO_SIZE_TEXT_TYPE_UNIFORM}
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeTextType
+     *
+     * @see #setAutoSizeTextTypeWithDefaults(int)
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+     */
+    @AutoSizeTextType
+    public int getAutoSizeTextType() {
+        return mAutoSizeTextType;
+    }
+
+    /**
+     * @return the current auto-size step granularity in pixels.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeStepGranularity
+     *
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     */
+    public int getAutoSizeStepGranularity() {
+        return Math.round(mAutoSizeStepGranularityInPx);
+    }
+
+    /**
+     * @return the current auto-size minimum text size in pixels (the default is 12sp). Note that
+     *         if auto-size has not been configured this function returns {@code -1}.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeMinTextSize
+     *
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+     */
+    public int getAutoSizeMinTextSize() {
+        return Math.round(mAutoSizeMinTextSizeInPx);
+    }
+
+    /**
+     * @return the current auto-size maximum text size in pixels (the default is 112sp). Note that
+     *         if auto-size has not been configured this function returns {@code -1}.
+     *
+     * @attr ref android.R.styleable#TextView_autoSizeMaxTextSize
+     *
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+     */
+    public int getAutoSizeMaxTextSize() {
+        return Math.round(mAutoSizeMaxTextSizeInPx);
+    }
+
+    /**
+     * @return the current auto-size {@code int} sizes array (in pixels).
+     *
+     * @see #setAutoSizeTextTypeUniformWithConfiguration(int, int, int, int)
+     * @see #setAutoSizeTextTypeUniformWithPresetSizes(int[], int)
+     */
+    public int[] getAutoSizeTextAvailableSizes() {
+        return mAutoSizeTextSizesInPx;
+    }
+
+    private void setupAutoSizeUniformPresetSizes(TypedArray textSizes) {
+        final int textSizesLength = textSizes.length();
+        final int[] parsedSizes = new int[textSizesLength];
+
+        if (textSizesLength > 0) {
+            for (int i = 0; i < textSizesLength; i++) {
+                parsedSizes[i] = textSizes.getDimensionPixelSize(i, -1);
+            }
+            mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(parsedSizes);
+            setupAutoSizeUniformPresetSizesConfiguration();
+        }
+    }
+
+    private boolean setupAutoSizeUniformPresetSizesConfiguration() {
+        final int sizesLength = mAutoSizeTextSizesInPx.length;
+        mHasPresetAutoSizeValues = sizesLength > 0;
+        if (mHasPresetAutoSizeValues) {
+            mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
+            mAutoSizeMinTextSizeInPx = mAutoSizeTextSizesInPx[0];
+            mAutoSizeMaxTextSizeInPx = mAutoSizeTextSizesInPx[sizesLength - 1];
+            mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        }
+        return mHasPresetAutoSizeValues;
+    }
+
+    /**
+     * If all params are valid then save the auto-size configuration.
+     *
+     * @throws IllegalArgumentException if any of the params are invalid
+     */
+    private void validateAndSetAutoSizeTextTypeUniformConfiguration(float autoSizeMinTextSizeInPx,
+            float autoSizeMaxTextSizeInPx, float autoSizeStepGranularityInPx) {
+        // First validate.
+        if (autoSizeMinTextSizeInPx <= 0) {
+            throw new IllegalArgumentException("Minimum auto-size text size ("
+                + autoSizeMinTextSizeInPx  + "px) is less or equal to (0px)");
+        }
+
+        if (autoSizeMaxTextSizeInPx <= autoSizeMinTextSizeInPx) {
+            throw new IllegalArgumentException("Maximum auto-size text size ("
+                + autoSizeMaxTextSizeInPx + "px) is less or equal to minimum auto-size "
+                + "text size (" + autoSizeMinTextSizeInPx + "px)");
+        }
+
+        if (autoSizeStepGranularityInPx <= 0) {
+            throw new IllegalArgumentException("The auto-size step granularity ("
+                + autoSizeStepGranularityInPx + "px) is less or equal to (0px)");
+        }
+
+        // All good, persist the configuration.
+        mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_UNIFORM;
+        mAutoSizeMinTextSizeInPx = autoSizeMinTextSizeInPx;
+        mAutoSizeMaxTextSizeInPx = autoSizeMaxTextSizeInPx;
+        mAutoSizeStepGranularityInPx = autoSizeStepGranularityInPx;
+        mHasPresetAutoSizeValues = false;
+    }
+
+    private void clearAutoSizeConfiguration() {
+        mAutoSizeTextType = AUTO_SIZE_TEXT_TYPE_NONE;
+        mAutoSizeMinTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        mAutoSizeMaxTextSizeInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        mAutoSizeStepGranularityInPx = UNSET_AUTO_SIZE_UNIFORM_CONFIGURATION_VALUE;
+        mAutoSizeTextSizesInPx = EmptyArray.INT;
+        mNeedsAutoSizeText = false;
+    }
+
+    // Returns distinct sorted positive values.
+    private int[] cleanupAutoSizePresetSizes(int[] presetValues) {
+        final int presetValuesLength = presetValues.length;
+        if (presetValuesLength == 0) {
+            return presetValues;
+        }
+        Arrays.sort(presetValues);
+
+        final IntArray uniqueValidSizes = new IntArray();
+        for (int i = 0; i < presetValuesLength; i++) {
+            final int currentPresetValue = presetValues[i];
+
+            if (currentPresetValue > 0
+                    && uniqueValidSizes.binarySearch(currentPresetValue) < 0) {
+                uniqueValidSizes.add(currentPresetValue);
+            }
+        }
+
+        return presetValuesLength == uniqueValidSizes.size()
+            ? presetValues
+            : uniqueValidSizes.toArray();
+    }
+
+    private boolean setupAutoSizeText() {
+        if (supportsAutoSizeText() && mAutoSizeTextType == AUTO_SIZE_TEXT_TYPE_UNIFORM) {
+            // Calculate the sizes set based on minimum size, maximum size and step size if we do
+            // not have a predefined set of sizes or if the current sizes array is empty.
+            if (!mHasPresetAutoSizeValues || mAutoSizeTextSizesInPx.length == 0) {
+                int autoSizeValuesLength = 1;
+                float currentSize = Math.round(mAutoSizeMinTextSizeInPx);
+                while (Math.round(currentSize + mAutoSizeStepGranularityInPx)
+                        <= Math.round(mAutoSizeMaxTextSizeInPx)) {
+                    autoSizeValuesLength++;
+                    currentSize += mAutoSizeStepGranularityInPx;
+                }
+
+                int[] autoSizeTextSizesInPx = new int[autoSizeValuesLength];
+                float sizeToAdd = mAutoSizeMinTextSizeInPx;
+                for (int i = 0; i < autoSizeValuesLength; i++) {
+                    autoSizeTextSizesInPx[i] = Math.round(sizeToAdd);
+                    sizeToAdd += mAutoSizeStepGranularityInPx;
+                }
+                mAutoSizeTextSizesInPx = cleanupAutoSizePresetSizes(autoSizeTextSizesInPx);
+            }
+
+            mNeedsAutoSizeText = true;
+        } else {
+            mNeedsAutoSizeText = false;
+        }
+
+        return mNeedsAutoSizeText;
+    }
+
+    private int[] parseDimensionArray(TypedArray dimens) {
+        if (dimens == null) {
+            return null;
+        }
+        int[] result = new int[dimens.length()];
+        for (int i = 0; i < result.length; i++) {
+            result[i] = dimens.getDimensionPixelSize(i, 0);
+        }
+        return result;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onActivityResult(int requestCode, int resultCode, Intent data) {
+        if (requestCode == PROCESS_TEXT_REQUEST_CODE) {
+            if (resultCode == Activity.RESULT_OK && data != null) {
+                CharSequence result = data.getCharSequenceExtra(Intent.EXTRA_PROCESS_TEXT);
+                if (result != null) {
+                    if (isTextEditable()) {
+                        replaceSelectionWithText(result);
+                        if (mEditor != null) {
+                            mEditor.refreshTextActionMode();
+                        }
+                    } else {
+                        if (result.length() > 0) {
+                            Toast.makeText(getContext(), String.valueOf(result), Toast.LENGTH_LONG)
+                                .show();
+                        }
+                    }
+                }
+            } else if (mText instanceof Spannable) {
+                // Reset the selection.
+                Selection.setSelection((Spannable) mText, getSelectionEnd());
+            }
+        }
+    }
+
+    private void setTypefaceFromAttrs(Typeface fontTypeface, String familyName, int typefaceIndex,
+            int styleIndex) {
+        Typeface tf = fontTypeface;
+        if (tf == null && familyName != null) {
+            tf = Typeface.create(familyName, styleIndex);
+        } else if (tf != null && tf.getStyle() != styleIndex) {
+            tf = Typeface.create(tf, styleIndex);
+        }
+        if (tf != null) {
+            setTypeface(tf);
+            return;
+        }
+        switch (typefaceIndex) {
+            case SANS:
+                tf = Typeface.SANS_SERIF;
+                break;
+
+            case SERIF:
+                tf = Typeface.SERIF;
+                break;
+
+            case MONOSPACE:
+                tf = Typeface.MONOSPACE;
+                break;
+        }
+
+        setTypeface(tf, styleIndex);
+    }
+
+    private void setRelativeDrawablesIfNeeded(Drawable start, Drawable end) {
+        boolean hasRelativeDrawables = (start != null) || (end != null);
+        if (hasRelativeDrawables) {
+            Drawables dr = mDrawables;
+            if (dr == null) {
+                mDrawables = dr = new Drawables(getContext());
+            }
+            mDrawables.mOverride = true;
+            final Rect compoundRect = dr.mCompoundRect;
+            int[] state = getDrawableState();
+            if (start != null) {
+                start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
+                start.setState(state);
+                start.copyBounds(compoundRect);
+                start.setCallback(this);
+
+                dr.mDrawableStart = start;
+                dr.mDrawableSizeStart = compoundRect.width();
+                dr.mDrawableHeightStart = compoundRect.height();
+            } else {
+                dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+            }
+            if (end != null) {
+                end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
+                end.setState(state);
+                end.copyBounds(compoundRect);
+                end.setCallback(this);
+
+                dr.mDrawableEnd = end;
+                dr.mDrawableSizeEnd = compoundRect.width();
+                dr.mDrawableHeightEnd = compoundRect.height();
+            } else {
+                dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+            }
+            resetResolvedDrawables();
+            resolveDrawables();
+            applyCompoundDrawableTint();
+        }
+    }
+
+    @android.view.RemotableViewMethod
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (enabled == isEnabled()) {
+            return;
+        }
+
+        if (!enabled) {
+            // Hide the soft input if the currently active TextView is disabled
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null && imm.isActive(this)) {
+                imm.hideSoftInputFromWindow(getWindowToken(), 0);
+            }
+        }
+
+        super.setEnabled(enabled);
+
+        if (enabled) {
+            // Make sure IME is updated with current editor info.
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) imm.restartInput(this);
+        }
+
+        // Will change text color
+        if (mEditor != null) {
+            mEditor.invalidateTextDisplayList();
+            mEditor.prepareCursorControllers();
+
+            // start or stop the cursor blinking as appropriate
+            mEditor.makeBlink();
+        }
+    }
+
+    /**
+     * Sets the typeface and style in which the text should be displayed,
+     * and turns on the fake bold and italic bits in the Paint if the
+     * Typeface that you provided does not have all the bits in the
+     * style that you specified.
+     *
+     * @attr ref android.R.styleable#TextView_typeface
+     * @attr ref android.R.styleable#TextView_textStyle
+     */
+    public void setTypeface(Typeface tf, int style) {
+        if (style > 0) {
+            if (tf == null) {
+                tf = Typeface.defaultFromStyle(style);
+            } else {
+                tf = Typeface.create(tf, style);
+            }
+
+            setTypeface(tf);
+            // now compute what (if any) algorithmic styling is needed
+            int typefaceStyle = tf != null ? tf.getStyle() : 0;
+            int need = style & ~typefaceStyle;
+            mTextPaint.setFakeBoldText((need & Typeface.BOLD) != 0);
+            mTextPaint.setTextSkewX((need & Typeface.ITALIC) != 0 ? -0.25f : 0);
+        } else {
+            mTextPaint.setFakeBoldText(false);
+            mTextPaint.setTextSkewX(0);
+            setTypeface(tf);
+        }
+    }
+
+    /**
+     * Subclasses override this to specify that they have a KeyListener
+     * by default even if not specifically called for in the XML options.
+     */
+    protected boolean getDefaultEditable() {
+        return false;
+    }
+
+    /**
+     * Subclasses override this to specify a default movement method.
+     */
+    protected MovementMethod getDefaultMovementMethod() {
+        return null;
+    }
+
+    /**
+     * Return the text that TextView is displaying. If {@link #setText(CharSequence)} was called
+     * with an argument of {@link android.widget.TextView.BufferType#SPANNABLE BufferType.SPANNABLE}
+     * or {@link android.widget.TextView.BufferType#EDITABLE BufferType.EDITABLE}, you can cast
+     * the return value from this method to Spannable or Editable, respectively.
+     *
+     * <p>The content of the return value should not be modified. If you want a modifiable one, you
+     * should make your own copy first.</p>
+     *
+     * @return The text displayed by the text view.
+     * @attr ref android.R.styleable#TextView_text
+     */
+    @ViewDebug.CapturedViewProperty
+    public CharSequence getText() {
+        return mText;
+    }
+
+    /**
+     * Returns the length, in characters, of the text managed by this TextView
+     * @return The length of the text managed by the TextView in characters.
+     */
+    public int length() {
+        return mText.length();
+    }
+
+    /**
+     * Return the text that TextView is displaying as an Editable object. If the text is not
+     * editable, null is returned.
+     *
+     * @see #getText
+     */
+    public Editable getEditableText() {
+        return (mText instanceof Editable) ? (Editable) mText : null;
+    }
+
+    /**
+     * Gets the vertical distance between lines of text, in pixels.
+     * Note that markup within the text can cause individual lines
+     * to be taller or shorter than this height, and the layout may
+     * contain additional first-or last-line padding.
+     * @return The height of one standard line in pixels.
+     */
+    public int getLineHeight() {
+        return FastMath.round(mTextPaint.getFontMetricsInt(null) * mSpacingMult + mSpacingAdd);
+    }
+
+    /**
+     * Gets the {@link android.text.Layout} that is currently being used to display the text.
+     * This value can be null if the text or width has recently changed.
+     * @return The Layout that is currently being used to display the text.
+     */
+    public final Layout getLayout() {
+        return mLayout;
+    }
+
+    /**
+     * @return the {@link android.text.Layout} that is currently being used to
+     * display the hint text. This can be null.
+     */
+    final Layout getHintLayout() {
+        return mHintLayout;
+    }
+
+    /**
+     * Retrieve the {@link android.content.UndoManager} that is currently associated
+     * with this TextView.  By default there is no associated UndoManager, so null
+     * is returned.  One can be associated with the TextView through
+     * {@link #setUndoManager(android.content.UndoManager, String)}
+     *
+     * @hide
+     */
+    public final UndoManager getUndoManager() {
+        // TODO: Consider supporting a global undo manager.
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+
+    /**
+     * @hide
+     */
+    @VisibleForTesting
+    public final Editor getEditorForTesting() {
+        return mEditor;
+    }
+
+    /**
+     * Associate an {@link android.content.UndoManager} with this TextView.  Once
+     * done, all edit operations on the TextView will result in appropriate
+     * {@link android.content.UndoOperation} objects pushed on the given UndoManager's
+     * stack.
+     *
+     * @param undoManager The {@link android.content.UndoManager} to associate with
+     * this TextView, or null to clear any existing association.
+     * @param tag String tag identifying this particular TextView owner in the
+     * UndoManager.  This is used to keep the correct association with the
+     * {@link android.content.UndoOwner} of any operations inside of the UndoManager.
+     *
+     * @hide
+     */
+    public final void setUndoManager(UndoManager undoManager, String tag) {
+        // TODO: Consider supporting a global undo manager. An implementation will need to:
+        // * createEditorIfNeeded()
+        // * Promote to BufferType.EDITABLE if needed.
+        // * Update the UndoManager and UndoOwner.
+        // Likewise it will need to be able to restore the default UndoManager.
+        throw new UnsupportedOperationException("not implemented");
+    }
+
+    /**
+     * Gets the current {@link KeyListener} for the TextView.
+     * This will frequently be null for non-EditText TextViews.
+     * @return the current key listener for this TextView.
+     *
+     * @attr ref android.R.styleable#TextView_numeric
+     * @attr ref android.R.styleable#TextView_digits
+     * @attr ref android.R.styleable#TextView_phoneNumber
+     * @attr ref android.R.styleable#TextView_inputMethod
+     * @attr ref android.R.styleable#TextView_capitalize
+     * @attr ref android.R.styleable#TextView_autoText
+     */
+    public final KeyListener getKeyListener() {
+        return mEditor == null ? null : mEditor.mKeyListener;
+    }
+
+    /**
+     * Sets the key listener to be used with this TextView.  This can be null
+     * to disallow user input.  Note that this method has significant and
+     * subtle interactions with soft keyboards and other input method:
+     * see {@link KeyListener#getInputType() KeyListener.getContentType()}
+     * for important details.  Calling this method will replace the current
+     * content type of the text view with the content type returned by the
+     * key listener.
+     * <p>
+     * Be warned that if you want a TextView with a key listener or movement
+     * method not to be focusable, or if you want a TextView without a
+     * key listener or movement method to be focusable, you must call
+     * {@link #setFocusable} again after calling this to get the focusability
+     * back the way you want it.
+     *
+     * @attr ref android.R.styleable#TextView_numeric
+     * @attr ref android.R.styleable#TextView_digits
+     * @attr ref android.R.styleable#TextView_phoneNumber
+     * @attr ref android.R.styleable#TextView_inputMethod
+     * @attr ref android.R.styleable#TextView_capitalize
+     * @attr ref android.R.styleable#TextView_autoText
+     */
+    public void setKeyListener(KeyListener input) {
+        mListenerChanged = true;
+        setKeyListenerOnly(input);
+        fixFocusableAndClickableSettings();
+
+        if (input != null) {
+            createEditorIfNeeded();
+            setInputTypeFromEditor();
+        } else {
+            if (mEditor != null) mEditor.mInputType = EditorInfo.TYPE_NULL;
+        }
+
+        InputMethodManager imm = InputMethodManager.peekInstance();
+        if (imm != null) imm.restartInput(this);
+    }
+
+    private void setInputTypeFromEditor() {
+        try {
+            mEditor.mInputType = mEditor.mKeyListener.getInputType();
+        } catch (IncompatibleClassChangeError e) {
+            mEditor.mInputType = EditorInfo.TYPE_CLASS_TEXT;
+        }
+        // Change inputType, without affecting transformation.
+        // No need to applySingleLine since mSingleLine is unchanged.
+        setInputTypeSingleLine(mSingleLine);
+    }
+
+    private void setKeyListenerOnly(KeyListener input) {
+        if (mEditor == null && input == null) return; // null is the default value
+
+        createEditorIfNeeded();
+        if (mEditor.mKeyListener != input) {
+            mEditor.mKeyListener = input;
+            if (input != null && !(mText instanceof Editable)) {
+                setText(mText);
+            }
+
+            setFilters((Editable) mText, mFilters);
+        }
+    }
+
+    /**
+     * Gets the {@link android.text.method.MovementMethod} being used for this TextView,
+     * which provides positioning, scrolling, and text selection functionality.
+     * This will frequently be null for non-EditText TextViews.
+     * @return the movement method being used for this TextView.
+     * @see android.text.method.MovementMethod
+     */
+    public final MovementMethod getMovementMethod() {
+        return mMovement;
+    }
+
+    /**
+     * Sets the {@link android.text.method.MovementMethod} for handling arrow key movement
+     * for this TextView. This can be null to disallow using the arrow keys to move the
+     * cursor or scroll the view.
+     * <p>
+     * Be warned that if you want a TextView with a key listener or movement
+     * method not to be focusable, or if you want a TextView without a
+     * key listener or movement method to be focusable, you must call
+     * {@link #setFocusable} again after calling this to get the focusability
+     * back the way you want it.
+     */
+    public final void setMovementMethod(MovementMethod movement) {
+        if (mMovement != movement) {
+            mMovement = movement;
+
+            if (movement != null && !(mText instanceof Spannable)) {
+                setText(mText);
+            }
+
+            fixFocusableAndClickableSettings();
+
+            // SelectionModifierCursorController depends on textCanBeSelected, which depends on
+            // mMovement
+            if (mEditor != null) mEditor.prepareCursorControllers();
+        }
+    }
+
+    private void fixFocusableAndClickableSettings() {
+        if (mMovement != null || (mEditor != null && mEditor.mKeyListener != null)) {
+            setFocusable(FOCUSABLE);
+            setClickable(true);
+            setLongClickable(true);
+        } else {
+            setFocusable(FOCUSABLE_AUTO);
+            setClickable(false);
+            setLongClickable(false);
+        }
+    }
+
+    /**
+     * Gets the current {@link android.text.method.TransformationMethod} for the TextView.
+     * This is frequently null, except for single-line and password fields.
+     * @return the current transformation method for this TextView.
+     *
+     * @attr ref android.R.styleable#TextView_password
+     * @attr ref android.R.styleable#TextView_singleLine
+     */
+    public final TransformationMethod getTransformationMethod() {
+        return mTransformation;
+    }
+
+    /**
+     * Sets the transformation that is applied to the text that this
+     * TextView is displaying.
+     *
+     * @attr ref android.R.styleable#TextView_password
+     * @attr ref android.R.styleable#TextView_singleLine
+     */
+    public final void setTransformationMethod(TransformationMethod method) {
+        if (method == mTransformation) {
+            // Avoid the setText() below if the transformation is
+            // the same.
+            return;
+        }
+        if (mTransformation != null) {
+            if (mText instanceof Spannable) {
+                ((Spannable) mText).removeSpan(mTransformation);
+            }
+        }
+
+        mTransformation = method;
+
+        if (method instanceof TransformationMethod2) {
+            TransformationMethod2 method2 = (TransformationMethod2) method;
+            mAllowTransformationLengthChange = !isTextSelectable() && !(mText instanceof Editable);
+            method2.setLengthChangesAllowed(mAllowTransformationLengthChange);
+        } else {
+            mAllowTransformationLengthChange = false;
+        }
+
+        setText(mText);
+
+        if (hasPasswordTransformationMethod()) {
+            notifyViewAccessibilityStateChangedIfNeeded(
+                    AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+        }
+
+        // PasswordTransformationMethod always have LTR text direction heuristics returned by
+        // getTextDirectionHeuristic, needs reset
+        mTextDir = getTextDirectionHeuristic();
+    }
+
+    /**
+     * Returns the top padding of the view, plus space for the top
+     * Drawable if any.
+     */
+    public int getCompoundPaddingTop() {
+        final Drawables dr = mDrawables;
+        if (dr == null || dr.mShowing[Drawables.TOP] == null) {
+            return mPaddingTop;
+        } else {
+            return mPaddingTop + dr.mDrawablePadding + dr.mDrawableSizeTop;
+        }
+    }
+
+    /**
+     * Returns the bottom padding of the view, plus space for the bottom
+     * Drawable if any.
+     */
+    public int getCompoundPaddingBottom() {
+        final Drawables dr = mDrawables;
+        if (dr == null || dr.mShowing[Drawables.BOTTOM] == null) {
+            return mPaddingBottom;
+        } else {
+            return mPaddingBottom + dr.mDrawablePadding + dr.mDrawableSizeBottom;
+        }
+    }
+
+    /**
+     * Returns the left padding of the view, plus space for the left
+     * Drawable if any.
+     */
+    public int getCompoundPaddingLeft() {
+        final Drawables dr = mDrawables;
+        if (dr == null || dr.mShowing[Drawables.LEFT] == null) {
+            return mPaddingLeft;
+        } else {
+            return mPaddingLeft + dr.mDrawablePadding + dr.mDrawableSizeLeft;
+        }
+    }
+
+    /**
+     * Returns the right padding of the view, plus space for the right
+     * Drawable if any.
+     */
+    public int getCompoundPaddingRight() {
+        final Drawables dr = mDrawables;
+        if (dr == null || dr.mShowing[Drawables.RIGHT] == null) {
+            return mPaddingRight;
+        } else {
+            return mPaddingRight + dr.mDrawablePadding + dr.mDrawableSizeRight;
+        }
+    }
+
+    /**
+     * Returns the start padding of the view, plus space for the start
+     * Drawable if any.
+     */
+    public int getCompoundPaddingStart() {
+        resolveDrawables();
+        switch(getLayoutDirection()) {
+            default:
+            case LAYOUT_DIRECTION_LTR:
+                return getCompoundPaddingLeft();
+            case LAYOUT_DIRECTION_RTL:
+                return getCompoundPaddingRight();
+        }
+    }
+
+    /**
+     * Returns the end padding of the view, plus space for the end
+     * Drawable if any.
+     */
+    public int getCompoundPaddingEnd() {
+        resolveDrawables();
+        switch(getLayoutDirection()) {
+            default:
+            case LAYOUT_DIRECTION_LTR:
+                return getCompoundPaddingRight();
+            case LAYOUT_DIRECTION_RTL:
+                return getCompoundPaddingLeft();
+        }
+    }
+
+    /**
+     * Returns the extended top padding of the view, including both the
+     * top Drawable if any and any extra space to keep more than maxLines
+     * of text from showing.  It is only valid to call this after measuring.
+     */
+    public int getExtendedPaddingTop() {
+        if (mMaxMode != LINES) {
+            return getCompoundPaddingTop();
+        }
+
+        if (mLayout == null) {
+            assumeLayout();
+        }
+
+        if (mLayout.getLineCount() <= mMaximum) {
+            return getCompoundPaddingTop();
+        }
+
+        int top = getCompoundPaddingTop();
+        int bottom = getCompoundPaddingBottom();
+        int viewht = getHeight() - top - bottom;
+        int layoutht = mLayout.getLineTop(mMaximum);
+
+        if (layoutht >= viewht) {
+            return top;
+        }
+
+        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        if (gravity == Gravity.TOP) {
+            return top;
+        } else if (gravity == Gravity.BOTTOM) {
+            return top + viewht - layoutht;
+        } else { // (gravity == Gravity.CENTER_VERTICAL)
+            return top + (viewht - layoutht) / 2;
+        }
+    }
+
+    /**
+     * Returns the extended bottom padding of the view, including both the
+     * bottom Drawable if any and any extra space to keep more than maxLines
+     * of text from showing.  It is only valid to call this after measuring.
+     */
+    public int getExtendedPaddingBottom() {
+        if (mMaxMode != LINES) {
+            return getCompoundPaddingBottom();
+        }
+
+        if (mLayout == null) {
+            assumeLayout();
+        }
+
+        if (mLayout.getLineCount() <= mMaximum) {
+            return getCompoundPaddingBottom();
+        }
+
+        int top = getCompoundPaddingTop();
+        int bottom = getCompoundPaddingBottom();
+        int viewht = getHeight() - top - bottom;
+        int layoutht = mLayout.getLineTop(mMaximum);
+
+        if (layoutht >= viewht) {
+            return bottom;
+        }
+
+        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        if (gravity == Gravity.TOP) {
+            return bottom + viewht - layoutht;
+        } else if (gravity == Gravity.BOTTOM) {
+            return bottom;
+        } else { // (gravity == Gravity.CENTER_VERTICAL)
+            return bottom + (viewht - layoutht) / 2;
+        }
+    }
+
+    /**
+     * Returns the total left padding of the view, including the left
+     * Drawable if any.
+     */
+    public int getTotalPaddingLeft() {
+        return getCompoundPaddingLeft();
+    }
+
+    /**
+     * Returns the total right padding of the view, including the right
+     * Drawable if any.
+     */
+    public int getTotalPaddingRight() {
+        return getCompoundPaddingRight();
+    }
+
+    /**
+     * Returns the total start padding of the view, including the start
+     * Drawable if any.
+     */
+    public int getTotalPaddingStart() {
+        return getCompoundPaddingStart();
+    }
+
+    /**
+     * Returns the total end padding of the view, including the end
+     * Drawable if any.
+     */
+    public int getTotalPaddingEnd() {
+        return getCompoundPaddingEnd();
+    }
+
+    /**
+     * Returns the total top padding of the view, including the top
+     * Drawable if any, the extra space to keep more than maxLines
+     * from showing, and the vertical offset for gravity, if any.
+     */
+    public int getTotalPaddingTop() {
+        return getExtendedPaddingTop() + getVerticalOffset(true);
+    }
+
+    /**
+     * Returns the total bottom padding of the view, including the bottom
+     * Drawable if any, the extra space to keep more than maxLines
+     * from showing, and the vertical offset for gravity, if any.
+     */
+    public int getTotalPaddingBottom() {
+        return getExtendedPaddingBottom() + getBottomVerticalOffset(true);
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the left of, above, to the
+     * right of, and below the text. Use {@code null} if you do not want a
+     * Drawable there. The Drawables must already have had
+     * {@link Drawable#setBounds} called.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawablesRelative} or related methods.
+     *
+     * @attr ref android.R.styleable#TextView_drawableLeft
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableRight
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    public void setCompoundDrawables(@Nullable Drawable left, @Nullable Drawable top,
+            @Nullable Drawable right, @Nullable Drawable bottom) {
+        Drawables dr = mDrawables;
+
+        // We're switching to absolute, discard relative.
+        if (dr != null) {
+            if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
+            dr.mDrawableStart = null;
+            if (dr.mDrawableEnd != null) dr.mDrawableEnd.setCallback(null);
+            dr.mDrawableEnd = null;
+            dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+            dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+        }
+
+        final boolean drawables = left != null || top != null || right != null || bottom != null;
+        if (!drawables) {
+            // Clearing drawables...  can we free the data structure?
+            if (dr != null) {
+                if (!dr.hasMetadata()) {
+                    mDrawables = null;
+                } else {
+                    // We need to retain the last set padding, so just clear
+                    // out all of the fields in the existing structure.
+                    for (int i = dr.mShowing.length - 1; i >= 0; i--) {
+                        if (dr.mShowing[i] != null) {
+                            dr.mShowing[i].setCallback(null);
+                        }
+                        dr.mShowing[i] = null;
+                    }
+                    dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+                    dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+                    dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+                    dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+                }
+            }
+        } else {
+            if (dr == null) {
+                mDrawables = dr = new Drawables(getContext());
+            }
+
+            mDrawables.mOverride = false;
+
+            if (dr.mShowing[Drawables.LEFT] != left && dr.mShowing[Drawables.LEFT] != null) {
+                dr.mShowing[Drawables.LEFT].setCallback(null);
+            }
+            dr.mShowing[Drawables.LEFT] = left;
+
+            if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
+                dr.mShowing[Drawables.TOP].setCallback(null);
+            }
+            dr.mShowing[Drawables.TOP] = top;
+
+            if (dr.mShowing[Drawables.RIGHT] != right && dr.mShowing[Drawables.RIGHT] != null) {
+                dr.mShowing[Drawables.RIGHT].setCallback(null);
+            }
+            dr.mShowing[Drawables.RIGHT] = right;
+
+            if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
+                dr.mShowing[Drawables.BOTTOM].setCallback(null);
+            }
+            dr.mShowing[Drawables.BOTTOM] = bottom;
+
+            final Rect compoundRect = dr.mCompoundRect;
+            int[] state;
+
+            state = getDrawableState();
+
+            if (left != null) {
+                left.setState(state);
+                left.copyBounds(compoundRect);
+                left.setCallback(this);
+                dr.mDrawableSizeLeft = compoundRect.width();
+                dr.mDrawableHeightLeft = compoundRect.height();
+            } else {
+                dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+            }
+
+            if (right != null) {
+                right.setState(state);
+                right.copyBounds(compoundRect);
+                right.setCallback(this);
+                dr.mDrawableSizeRight = compoundRect.width();
+                dr.mDrawableHeightRight = compoundRect.height();
+            } else {
+                dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+            }
+
+            if (top != null) {
+                top.setState(state);
+                top.copyBounds(compoundRect);
+                top.setCallback(this);
+                dr.mDrawableSizeTop = compoundRect.height();
+                dr.mDrawableWidthTop = compoundRect.width();
+            } else {
+                dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+            }
+
+            if (bottom != null) {
+                bottom.setState(state);
+                bottom.copyBounds(compoundRect);
+                bottom.setCallback(this);
+                dr.mDrawableSizeBottom = compoundRect.height();
+                dr.mDrawableWidthBottom = compoundRect.width();
+            } else {
+                dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+            }
+        }
+
+        // Save initial left/right drawables
+        if (dr != null) {
+            dr.mDrawableLeftInitial = left;
+            dr.mDrawableRightInitial = right;
+        }
+
+        resetResolvedDrawables();
+        resolveDrawables();
+        applyCompoundDrawableTint();
+        invalidate();
+        requestLayout();
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the left of, above, to the
+     * right of, and below the text. Use 0 if you do not want a Drawable there.
+     * The Drawables' bounds will be set to their intrinsic bounds.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawablesRelative} or related methods.
+     *
+     * @param left Resource identifier of the left Drawable.
+     * @param top Resource identifier of the top Drawable.
+     * @param right Resource identifier of the right Drawable.
+     * @param bottom Resource identifier of the bottom Drawable.
+     *
+     * @attr ref android.R.styleable#TextView_drawableLeft
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableRight
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablesWithIntrinsicBounds(@DrawableRes int left,
+            @DrawableRes int top, @DrawableRes int right, @DrawableRes int bottom) {
+        final Context context = getContext();
+        setCompoundDrawablesWithIntrinsicBounds(left != 0 ? context.getDrawable(left) : null,
+                top != 0 ? context.getDrawable(top) : null,
+                right != 0 ? context.getDrawable(right) : null,
+                bottom != 0 ? context.getDrawable(bottom) : null);
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the left of, above, to the
+     * right of, and below the text. Use {@code null} if you do not want a
+     * Drawable there. The Drawables' bounds will be set to their intrinsic
+     * bounds.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawablesRelative} or related methods.
+     *
+     * @attr ref android.R.styleable#TextView_drawableLeft
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableRight
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablesWithIntrinsicBounds(@Nullable Drawable left,
+            @Nullable Drawable top, @Nullable Drawable right, @Nullable Drawable bottom) {
+
+        if (left != null) {
+            left.setBounds(0, 0, left.getIntrinsicWidth(), left.getIntrinsicHeight());
+        }
+        if (right != null) {
+            right.setBounds(0, 0, right.getIntrinsicWidth(), right.getIntrinsicHeight());
+        }
+        if (top != null) {
+            top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
+        }
+        if (bottom != null) {
+            bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
+        }
+        setCompoundDrawables(left, top, right, bottom);
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the start of, above, to the end
+     * of, and below the text. Use {@code null} if you do not want a Drawable
+     * there. The Drawables must already have had {@link Drawable#setBounds}
+     * called.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawables} or related methods.
+     *
+     * @attr ref android.R.styleable#TextView_drawableStart
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableEnd
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablesRelative(@Nullable Drawable start, @Nullable Drawable top,
+            @Nullable Drawable end, @Nullable Drawable bottom) {
+        Drawables dr = mDrawables;
+
+        // We're switching to relative, discard absolute.
+        if (dr != null) {
+            if (dr.mShowing[Drawables.LEFT] != null) {
+                dr.mShowing[Drawables.LEFT].setCallback(null);
+            }
+            dr.mShowing[Drawables.LEFT] = dr.mDrawableLeftInitial = null;
+            if (dr.mShowing[Drawables.RIGHT] != null) {
+                dr.mShowing[Drawables.RIGHT].setCallback(null);
+            }
+            dr.mShowing[Drawables.RIGHT] = dr.mDrawableRightInitial = null;
+            dr.mDrawableSizeLeft = dr.mDrawableHeightLeft = 0;
+            dr.mDrawableSizeRight = dr.mDrawableHeightRight = 0;
+        }
+
+        final boolean drawables = start != null || top != null
+                || end != null || bottom != null;
+
+        if (!drawables) {
+            // Clearing drawables...  can we free the data structure?
+            if (dr != null) {
+                if (!dr.hasMetadata()) {
+                    mDrawables = null;
+                } else {
+                    // We need to retain the last set padding, so just clear
+                    // out all of the fields in the existing structure.
+                    if (dr.mDrawableStart != null) dr.mDrawableStart.setCallback(null);
+                    dr.mDrawableStart = null;
+                    if (dr.mShowing[Drawables.TOP] != null) {
+                        dr.mShowing[Drawables.TOP].setCallback(null);
+                    }
+                    dr.mShowing[Drawables.TOP] = null;
+                    if (dr.mDrawableEnd != null) {
+                        dr.mDrawableEnd.setCallback(null);
+                    }
+                    dr.mDrawableEnd = null;
+                    if (dr.mShowing[Drawables.BOTTOM] != null) {
+                        dr.mShowing[Drawables.BOTTOM].setCallback(null);
+                    }
+                    dr.mShowing[Drawables.BOTTOM] = null;
+                    dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+                    dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+                    dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+                    dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+                }
+            }
+        } else {
+            if (dr == null) {
+                mDrawables = dr = new Drawables(getContext());
+            }
+
+            mDrawables.mOverride = true;
+
+            if (dr.mDrawableStart != start && dr.mDrawableStart != null) {
+                dr.mDrawableStart.setCallback(null);
+            }
+            dr.mDrawableStart = start;
+
+            if (dr.mShowing[Drawables.TOP] != top && dr.mShowing[Drawables.TOP] != null) {
+                dr.mShowing[Drawables.TOP].setCallback(null);
+            }
+            dr.mShowing[Drawables.TOP] = top;
+
+            if (dr.mDrawableEnd != end && dr.mDrawableEnd != null) {
+                dr.mDrawableEnd.setCallback(null);
+            }
+            dr.mDrawableEnd = end;
+
+            if (dr.mShowing[Drawables.BOTTOM] != bottom && dr.mShowing[Drawables.BOTTOM] != null) {
+                dr.mShowing[Drawables.BOTTOM].setCallback(null);
+            }
+            dr.mShowing[Drawables.BOTTOM] = bottom;
+
+            final Rect compoundRect = dr.mCompoundRect;
+            int[] state;
+
+            state = getDrawableState();
+
+            if (start != null) {
+                start.setState(state);
+                start.copyBounds(compoundRect);
+                start.setCallback(this);
+                dr.mDrawableSizeStart = compoundRect.width();
+                dr.mDrawableHeightStart = compoundRect.height();
+            } else {
+                dr.mDrawableSizeStart = dr.mDrawableHeightStart = 0;
+            }
+
+            if (end != null) {
+                end.setState(state);
+                end.copyBounds(compoundRect);
+                end.setCallback(this);
+                dr.mDrawableSizeEnd = compoundRect.width();
+                dr.mDrawableHeightEnd = compoundRect.height();
+            } else {
+                dr.mDrawableSizeEnd = dr.mDrawableHeightEnd = 0;
+            }
+
+            if (top != null) {
+                top.setState(state);
+                top.copyBounds(compoundRect);
+                top.setCallback(this);
+                dr.mDrawableSizeTop = compoundRect.height();
+                dr.mDrawableWidthTop = compoundRect.width();
+            } else {
+                dr.mDrawableSizeTop = dr.mDrawableWidthTop = 0;
+            }
+
+            if (bottom != null) {
+                bottom.setState(state);
+                bottom.copyBounds(compoundRect);
+                bottom.setCallback(this);
+                dr.mDrawableSizeBottom = compoundRect.height();
+                dr.mDrawableWidthBottom = compoundRect.width();
+            } else {
+                dr.mDrawableSizeBottom = dr.mDrawableWidthBottom = 0;
+            }
+        }
+
+        resetResolvedDrawables();
+        resolveDrawables();
+        invalidate();
+        requestLayout();
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the start of, above, to the end
+     * of, and below the text. Use 0 if you do not want a Drawable there. The
+     * Drawables' bounds will be set to their intrinsic bounds.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawables} or related methods.
+     *
+     * @param start Resource identifier of the start Drawable.
+     * @param top Resource identifier of the top Drawable.
+     * @param end Resource identifier of the end Drawable.
+     * @param bottom Resource identifier of the bottom Drawable.
+     *
+     * @attr ref android.R.styleable#TextView_drawableStart
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableEnd
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(@DrawableRes int start,
+            @DrawableRes int top, @DrawableRes int end, @DrawableRes int bottom) {
+        final Context context = getContext();
+        setCompoundDrawablesRelativeWithIntrinsicBounds(
+                start != 0 ? context.getDrawable(start) : null,
+                top != 0 ? context.getDrawable(top) : null,
+                end != 0 ? context.getDrawable(end) : null,
+                bottom != 0 ? context.getDrawable(bottom) : null);
+    }
+
+    /**
+     * Sets the Drawables (if any) to appear to the start of, above, to the end
+     * of, and below the text. Use {@code null} if you do not want a Drawable
+     * there. The Drawables' bounds will be set to their intrinsic bounds.
+     * <p>
+     * Calling this method will overwrite any Drawables previously set using
+     * {@link #setCompoundDrawables} or related methods.
+     *
+     * @attr ref android.R.styleable#TextView_drawableStart
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableEnd
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablesRelativeWithIntrinsicBounds(@Nullable Drawable start,
+            @Nullable Drawable top, @Nullable Drawable end, @Nullable Drawable bottom) {
+
+        if (start != null) {
+            start.setBounds(0, 0, start.getIntrinsicWidth(), start.getIntrinsicHeight());
+        }
+        if (end != null) {
+            end.setBounds(0, 0, end.getIntrinsicWidth(), end.getIntrinsicHeight());
+        }
+        if (top != null) {
+            top.setBounds(0, 0, top.getIntrinsicWidth(), top.getIntrinsicHeight());
+        }
+        if (bottom != null) {
+            bottom.setBounds(0, 0, bottom.getIntrinsicWidth(), bottom.getIntrinsicHeight());
+        }
+        setCompoundDrawablesRelative(start, top, end, bottom);
+    }
+
+    /**
+     * Returns drawables for the left, top, right, and bottom borders.
+     *
+     * @attr ref android.R.styleable#TextView_drawableLeft
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableRight
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @NonNull
+    public Drawable[] getCompoundDrawables() {
+        final Drawables dr = mDrawables;
+        if (dr != null) {
+            return dr.mShowing.clone();
+        } else {
+            return new Drawable[] { null, null, null, null };
+        }
+    }
+
+    /**
+     * Returns drawables for the start, top, end, and bottom borders.
+     *
+     * @attr ref android.R.styleable#TextView_drawableStart
+     * @attr ref android.R.styleable#TextView_drawableTop
+     * @attr ref android.R.styleable#TextView_drawableEnd
+     * @attr ref android.R.styleable#TextView_drawableBottom
+     */
+    @NonNull
+    public Drawable[] getCompoundDrawablesRelative() {
+        final Drawables dr = mDrawables;
+        if (dr != null) {
+            return new Drawable[] {
+                dr.mDrawableStart, dr.mShowing[Drawables.TOP],
+                dr.mDrawableEnd, dr.mShowing[Drawables.BOTTOM]
+            };
+        } else {
+            return new Drawable[] { null, null, null, null };
+        }
+    }
+
+    /**
+     * Sets the size of the padding between the compound drawables and
+     * the text.
+     *
+     * @attr ref android.R.styleable#TextView_drawablePadding
+     */
+    @android.view.RemotableViewMethod
+    public void setCompoundDrawablePadding(int pad) {
+        Drawables dr = mDrawables;
+        if (pad == 0) {
+            if (dr != null) {
+                dr.mDrawablePadding = pad;
+            }
+        } else {
+            if (dr == null) {
+                mDrawables = dr = new Drawables(getContext());
+            }
+            dr.mDrawablePadding = pad;
+        }
+
+        invalidate();
+        requestLayout();
+    }
+
+    /**
+     * Returns the padding between the compound drawables and the text.
+     *
+     * @attr ref android.R.styleable#TextView_drawablePadding
+     */
+    public int getCompoundDrawablePadding() {
+        final Drawables dr = mDrawables;
+        return dr != null ? dr.mDrawablePadding : 0;
+    }
+
+    /**
+     * Applies a tint to the compound drawables. Does not modify the
+     * current tint mode, which is {@link PorterDuff.Mode#SRC_IN} by default.
+     * <p>
+     * Subsequent calls to
+     * {@link #setCompoundDrawables(Drawable, Drawable, Drawable, Drawable)}
+     * and related methods will automatically mutate the drawables and apply
+     * the specified tint and tint mode using
+     * {@link Drawable#setTintList(ColorStateList)}.
+     *
+     * @param tint the tint to apply, may be {@code null} to clear tint
+     *
+     * @attr ref android.R.styleable#TextView_drawableTint
+     * @see #getCompoundDrawableTintList()
+     * @see Drawable#setTintList(ColorStateList)
+     */
+    public void setCompoundDrawableTintList(@Nullable ColorStateList tint) {
+        if (mDrawables == null) {
+            mDrawables = new Drawables(getContext());
+        }
+        mDrawables.mTintList = tint;
+        mDrawables.mHasTint = true;
+
+        applyCompoundDrawableTint();
+    }
+
+    /**
+     * @return the tint applied to the compound drawables
+     * @attr ref android.R.styleable#TextView_drawableTint
+     * @see #setCompoundDrawableTintList(ColorStateList)
+     */
+    public ColorStateList getCompoundDrawableTintList() {
+        return mDrawables != null ? mDrawables.mTintList : null;
+    }
+
+    /**
+     * Specifies the blending mode used to apply the tint specified by
+     * {@link #setCompoundDrawableTintList(ColorStateList)} to the compound
+     * drawables. The default mode is {@link PorterDuff.Mode#SRC_IN}.
+     *
+     * @param tintMode the blending mode used to apply the tint, may be
+     *                 {@code null} to clear tint
+     * @attr ref android.R.styleable#TextView_drawableTintMode
+     * @see #setCompoundDrawableTintList(ColorStateList)
+     * @see Drawable#setTintMode(PorterDuff.Mode)
+     */
+    public void setCompoundDrawableTintMode(@Nullable PorterDuff.Mode tintMode) {
+        if (mDrawables == null) {
+            mDrawables = new Drawables(getContext());
+        }
+        mDrawables.mTintMode = tintMode;
+        mDrawables.mHasTintMode = true;
+
+        applyCompoundDrawableTint();
+    }
+
+    /**
+     * Returns the blending mode used to apply the tint to the compound
+     * drawables, if specified.
+     *
+     * @return the blending mode used to apply the tint to the compound
+     *         drawables
+     * @attr ref android.R.styleable#TextView_drawableTintMode
+     * @see #setCompoundDrawableTintMode(PorterDuff.Mode)
+     */
+    public PorterDuff.Mode getCompoundDrawableTintMode() {
+        return mDrawables != null ? mDrawables.mTintMode : null;
+    }
+
+    private void applyCompoundDrawableTint() {
+        if (mDrawables == null) {
+            return;
+        }
+
+        if (mDrawables.mHasTint || mDrawables.mHasTintMode) {
+            final ColorStateList tintList = mDrawables.mTintList;
+            final PorterDuff.Mode tintMode = mDrawables.mTintMode;
+            final boolean hasTint = mDrawables.mHasTint;
+            final boolean hasTintMode = mDrawables.mHasTintMode;
+            final int[] state = getDrawableState();
+
+            for (Drawable dr : mDrawables.mShowing) {
+                if (dr == null) {
+                    continue;
+                }
+
+                if (dr == mDrawables.mDrawableError) {
+                    // From a developer's perspective, the error drawable isn't
+                    // a compound drawable. Don't apply the generic compound
+                    // drawable tint to it.
+                    continue;
+                }
+
+                dr.mutate();
+
+                if (hasTint) {
+                    dr.setTintList(tintList);
+                }
+
+                if (hasTintMode) {
+                    dr.setTintMode(tintMode);
+                }
+
+                // The drawable (or one of its children) may not have been
+                // stateful before applying the tint, so let's try again.
+                if (dr.isStateful()) {
+                    dr.setState(state);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void setPadding(int left, int top, int right, int bottom) {
+        if (left != mPaddingLeft
+                || right != mPaddingRight
+                || top != mPaddingTop
+                ||  bottom != mPaddingBottom) {
+            nullLayouts();
+        }
+
+        // the super call will requestLayout()
+        super.setPadding(left, top, right, bottom);
+        invalidate();
+    }
+
+    @Override
+    public void setPaddingRelative(int start, int top, int end, int bottom) {
+        if (start != getPaddingStart()
+                || end != getPaddingEnd()
+                || top != mPaddingTop
+                || bottom != mPaddingBottom) {
+            nullLayouts();
+        }
+
+        // the super call will requestLayout()
+        super.setPaddingRelative(start, top, end, bottom);
+        invalidate();
+    }
+
+    /**
+     * Gets the autolink mask of the text.  See {@link
+     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+     * possible values.
+     *
+     * @attr ref android.R.styleable#TextView_autoLink
+     */
+    public final int getAutoLinkMask() {
+        return mAutoLinkMask;
+    }
+
+    /**
+     * Sets the text appearance from the specified style resource.
+     * <p>
+     * Use a framework-defined {@code TextAppearance} style like
+     * {@link android.R.style#TextAppearance_Material_Body1 @android:style/TextAppearance.Material.Body1}
+     * or see {@link android.R.styleable#TextAppearance TextAppearance} for the
+     * set of attributes that can be used in a custom style.
+     *
+     * @param resId the resource identifier of the style to apply
+     * @attr ref android.R.styleable#TextView_textAppearance
+     */
+    @SuppressWarnings("deprecation")
+    public void setTextAppearance(@StyleRes int resId) {
+        setTextAppearance(mContext, resId);
+    }
+
+    /**
+     * Sets the text color, size, style, hint color, and highlight color
+     * from the specified TextAppearance resource.
+     *
+     * @deprecated Use {@link #setTextAppearance(int)} instead.
+     */
+    @Deprecated
+    public void setTextAppearance(Context context, @StyleRes int resId) {
+        final TypedArray ta = context.obtainStyledAttributes(resId, R.styleable.TextAppearance);
+        final TextAppearanceAttributes attributes = new TextAppearanceAttributes();
+        readTextAppearance(context, ta, attributes, false /* styleArray */);
+        ta.recycle();
+        applyTextAppearance(attributes);
+    }
+
+    /**
+     * Set of attributes that can be defined in a Text Appearance. This is used to simplify the code
+     * that reads these attributes in the constructor and in {@link #setTextAppearance}.
+     */
+    private static class TextAppearanceAttributes {
+        int mTextColorHighlight = 0;
+        ColorStateList mTextColor = null;
+        ColorStateList mTextColorHint = null;
+        ColorStateList mTextColorLink = null;
+        int mTextSize = 0;
+        String mFontFamily = null;
+        Typeface mFontTypeface = null;
+        boolean mFontFamilyExplicit = false;
+        int mTypefaceIndex = -1;
+        int mStyleIndex = -1;
+        boolean mAllCaps = false;
+        int mShadowColor = 0;
+        float mShadowDx = 0, mShadowDy = 0, mShadowRadius = 0;
+        boolean mHasElegant = false;
+        boolean mElegant = false;
+        boolean mHasLetterSpacing = false;
+        float mLetterSpacing = 0;
+        String mFontFeatureSettings = null;
+
+        @Override
+        public String toString() {
+            return "TextAppearanceAttributes {\n"
+                    + "    mTextColorHighlight:" + mTextColorHighlight + "\n"
+                    + "    mTextColor:" + mTextColor + "\n"
+                    + "    mTextColorHint:" + mTextColorHint + "\n"
+                    + "    mTextColorLink:" + mTextColorLink + "\n"
+                    + "    mTextSize:" + mTextSize + "\n"
+                    + "    mFontFamily:" + mFontFamily + "\n"
+                    + "    mFontTypeface:" + mFontTypeface + "\n"
+                    + "    mFontFamilyExplicit:" + mFontFamilyExplicit + "\n"
+                    + "    mTypefaceIndex:" + mTypefaceIndex + "\n"
+                    + "    mStyleIndex:" + mStyleIndex + "\n"
+                    + "    mAllCaps:" + mAllCaps + "\n"
+                    + "    mShadowColor:" + mShadowColor + "\n"
+                    + "    mShadowDx:" + mShadowDx + "\n"
+                    + "    mShadowDy:" + mShadowDy + "\n"
+                    + "    mShadowRadius:" + mShadowRadius + "\n"
+                    + "    mHasElegant:" + mHasElegant + "\n"
+                    + "    mElegant:" + mElegant + "\n"
+                    + "    mHasLetterSpacing:" + mHasLetterSpacing + "\n"
+                    + "    mLetterSpacing:" + mLetterSpacing + "\n"
+                    + "    mFontFeatureSettings:" + mFontFeatureSettings + "\n"
+                    + "}";
+        }
+    }
+
+    // Maps styleable attributes that exist both in TextView style and TextAppearance.
+    private static final SparseIntArray sAppearanceValues = new SparseIntArray();
+    static {
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHighlight,
+                com.android.internal.R.styleable.TextAppearance_textColorHighlight);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColor,
+                com.android.internal.R.styleable.TextAppearance_textColor);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorHint,
+                com.android.internal.R.styleable.TextAppearance_textColorHint);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textColorLink,
+                com.android.internal.R.styleable.TextAppearance_textColorLink);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textSize,
+                com.android.internal.R.styleable.TextAppearance_textSize);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_typeface,
+                com.android.internal.R.styleable.TextAppearance_typeface);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFamily,
+                com.android.internal.R.styleable.TextAppearance_fontFamily);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textStyle,
+                com.android.internal.R.styleable.TextAppearance_textStyle);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_textAllCaps,
+                com.android.internal.R.styleable.TextAppearance_textAllCaps);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowColor,
+                com.android.internal.R.styleable.TextAppearance_shadowColor);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDx,
+                com.android.internal.R.styleable.TextAppearance_shadowDx);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowDy,
+                com.android.internal.R.styleable.TextAppearance_shadowDy);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_shadowRadius,
+                com.android.internal.R.styleable.TextAppearance_shadowRadius);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_elegantTextHeight,
+                com.android.internal.R.styleable.TextAppearance_elegantTextHeight);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_letterSpacing,
+                com.android.internal.R.styleable.TextAppearance_letterSpacing);
+        sAppearanceValues.put(com.android.internal.R.styleable.TextView_fontFeatureSettings,
+                com.android.internal.R.styleable.TextAppearance_fontFeatureSettings);
+    }
+
+    /**
+     * Read the Text Appearance attributes from a given TypedArray and set its values to the given
+     * set. If the TypedArray contains a value that was already set in the given attributes, that
+     * will be overriden.
+     *
+     * @param context The Context to be used
+     * @param appearance The TypedArray to read properties from
+     * @param attributes the TextAppearanceAttributes to fill in
+     * @param styleArray Whether the given TypedArray is a style or a TextAppearance. This defines
+     *                   what attribute indexes will be used to read the properties.
+     */
+    private void readTextAppearance(Context context, TypedArray appearance,
+            TextAppearanceAttributes attributes, boolean styleArray) {
+        final int n = appearance.getIndexCount();
+        for (int i = 0; i < n; i++) {
+            final int attr = appearance.getIndex(i);
+            int index = attr;
+            // Translate style array index ids to TextAppearance ids.
+            if (styleArray) {
+                index = sAppearanceValues.get(attr, -1);
+                if (index == -1) {
+                    // This value is not part of a Text Appearance and should be ignored.
+                    continue;
+                }
+            }
+            switch (index) {
+                case com.android.internal.R.styleable.TextAppearance_textColorHighlight:
+                    attributes.mTextColorHighlight =
+                            appearance.getColor(attr, attributes.mTextColorHighlight);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textColor:
+                    attributes.mTextColor = appearance.getColorStateList(attr);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textColorHint:
+                    attributes.mTextColorHint = appearance.getColorStateList(attr);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textColorLink:
+                    attributes.mTextColorLink = appearance.getColorStateList(attr);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textSize:
+                    attributes.mTextSize =
+                            appearance.getDimensionPixelSize(attr, attributes.mTextSize);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_typeface:
+                    attributes.mTypefaceIndex = appearance.getInt(attr, attributes.mTypefaceIndex);
+                    if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
+                        attributes.mFontFamily = null;
+                    }
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_fontFamily:
+                    if (!context.isRestricted() && context.canLoadUnsafeResources()) {
+                        try {
+                            attributes.mFontTypeface = appearance.getFont(attr);
+                        } catch (UnsupportedOperationException | Resources.NotFoundException e) {
+                            // Expected if it is not a font resource.
+                        }
+                    }
+                    if (attributes.mFontTypeface == null) {
+                        attributes.mFontFamily = appearance.getString(attr);
+                    }
+                    attributes.mFontFamilyExplicit = true;
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textStyle:
+                    attributes.mStyleIndex = appearance.getInt(attr, attributes.mStyleIndex);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_textAllCaps:
+                    attributes.mAllCaps = appearance.getBoolean(attr, attributes.mAllCaps);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_shadowColor:
+                    attributes.mShadowColor = appearance.getInt(attr, attributes.mShadowColor);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_shadowDx:
+                    attributes.mShadowDx = appearance.getFloat(attr, attributes.mShadowDx);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_shadowDy:
+                    attributes.mShadowDy = appearance.getFloat(attr, attributes.mShadowDy);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_shadowRadius:
+                    attributes.mShadowRadius = appearance.getFloat(attr, attributes.mShadowRadius);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_elegantTextHeight:
+                    attributes.mHasElegant = true;
+                    attributes.mElegant = appearance.getBoolean(attr, attributes.mElegant);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_letterSpacing:
+                    attributes.mHasLetterSpacing = true;
+                    attributes.mLetterSpacing =
+                            appearance.getFloat(attr, attributes.mLetterSpacing);
+                    break;
+                case com.android.internal.R.styleable.TextAppearance_fontFeatureSettings:
+                    attributes.mFontFeatureSettings = appearance.getString(attr);
+                    break;
+                default:
+            }
+        }
+    }
+
+    private void applyTextAppearance(TextAppearanceAttributes attributes) {
+        if (attributes.mTextColor != null) {
+            setTextColor(attributes.mTextColor);
+        }
+
+        if (attributes.mTextColorHint != null) {
+            setHintTextColor(attributes.mTextColorHint);
+        }
+
+        if (attributes.mTextColorLink != null) {
+            setLinkTextColor(attributes.mTextColorLink);
+        }
+
+        if (attributes.mTextColorHighlight != 0) {
+            setHighlightColor(attributes.mTextColorHighlight);
+        }
+
+        if (attributes.mTextSize != 0) {
+            setRawTextSize(attributes.mTextSize, true /* shouldRequestLayout */);
+        }
+
+        if (attributes.mTypefaceIndex != -1 && !attributes.mFontFamilyExplicit) {
+            attributes.mFontFamily = null;
+        }
+        setTypefaceFromAttrs(attributes.mFontTypeface, attributes.mFontFamily,
+                attributes.mTypefaceIndex, attributes.mStyleIndex);
+
+        if (attributes.mShadowColor != 0) {
+            setShadowLayer(attributes.mShadowRadius, attributes.mShadowDx, attributes.mShadowDy,
+                    attributes.mShadowColor);
+        }
+
+        if (attributes.mAllCaps) {
+            setTransformationMethod(new AllCapsTransformationMethod(getContext()));
+        }
+
+        if (attributes.mHasElegant) {
+            setElegantTextHeight(attributes.mElegant);
+        }
+
+        if (attributes.mHasLetterSpacing) {
+            setLetterSpacing(attributes.mLetterSpacing);
+        }
+
+        if (attributes.mFontFeatureSettings != null) {
+            setFontFeatureSettings(attributes.mFontFeatureSettings);
+        }
+    }
+
+    /**
+     * Get the default primary {@link Locale} of the text in this TextView. This will always be
+     * the first member of {@link #getTextLocales()}.
+     * @return the default primary {@link Locale} of the text in this TextView.
+     */
+    @NonNull
+    public Locale getTextLocale() {
+        return mTextPaint.getTextLocale();
+    }
+
+    /**
+     * Get the default {@link LocaleList} of the text in this TextView.
+     * @return the default {@link LocaleList} of the text in this TextView.
+     */
+    @NonNull @Size(min = 1)
+    public LocaleList getTextLocales() {
+        return mTextPaint.getTextLocales();
+    }
+
+    private void changeListenerLocaleTo(@Nullable Locale locale) {
+        if (mListenerChanged) {
+            // If a listener has been explicitly set, don't change it. We may break something.
+            return;
+        }
+        // The following null check is not absolutely necessary since all calling points of
+        // changeListenerLocaleTo() guarantee a non-null mEditor at the moment. But this is left
+        // here in case others would want to call this method in the future.
+        if (mEditor != null) {
+            KeyListener listener = mEditor.mKeyListener;
+            if (listener instanceof DigitsKeyListener) {
+                listener = DigitsKeyListener.getInstance(locale, (DigitsKeyListener) listener);
+            } else if (listener instanceof DateKeyListener) {
+                listener = DateKeyListener.getInstance(locale);
+            } else if (listener instanceof TimeKeyListener) {
+                listener = TimeKeyListener.getInstance(locale);
+            } else if (listener instanceof DateTimeKeyListener) {
+                listener = DateTimeKeyListener.getInstance(locale);
+            } else {
+                return;
+            }
+            final boolean wasPasswordType = isPasswordInputType(mEditor.mInputType);
+            setKeyListenerOnly(listener);
+            setInputTypeFromEditor();
+            if (wasPasswordType) {
+                final int newInputClass = mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS;
+                if (newInputClass == EditorInfo.TYPE_CLASS_TEXT) {
+                    mEditor.mInputType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+                } else if (newInputClass == EditorInfo.TYPE_CLASS_NUMBER) {
+                    mEditor.mInputType |= EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
+                }
+            }
+        }
+    }
+
+    /**
+     * Set the default {@link Locale} of the text in this TextView to a one-member
+     * {@link LocaleList} containing just the given Locale.
+     *
+     * @param locale the {@link Locale} for drawing text, must not be null.
+     *
+     * @see #setTextLocales
+     */
+    public void setTextLocale(@NonNull Locale locale) {
+        mLocalesChanged = true;
+        mTextPaint.setTextLocale(locale);
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Set the default {@link LocaleList} of the text in this TextView to the given value.
+     *
+     * This value is used to choose appropriate typefaces for ambiguous characters (typically used
+     * for CJK locales to disambiguate Hanzi/Kanji/Hanja characters). It also affects
+     * other aspects of text display, including line breaking.
+     *
+     * @param locales the {@link LocaleList} for drawing text, must not be null or empty.
+     *
+     * @see Paint#setTextLocales
+     */
+    public void setTextLocales(@NonNull @Size(min = 1) LocaleList locales) {
+        mLocalesChanged = true;
+        mTextPaint.setTextLocales(locales);
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void onConfigurationChanged(Configuration newConfig) {
+        super.onConfigurationChanged(newConfig);
+        if (!mLocalesChanged) {
+            mTextPaint.setTextLocales(LocaleList.getDefault());
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * @return the size (in pixels) of the default text size in this TextView.
+     */
+    @ViewDebug.ExportedProperty(category = "text")
+    public float getTextSize() {
+        return mTextPaint.getTextSize();
+    }
+
+    /**
+     * @return the size (in scaled pixels) of the default text size in this TextView.
+     * @hide
+     */
+    @ViewDebug.ExportedProperty(category = "text")
+    public float getScaledTextSize() {
+        return mTextPaint.getTextSize() / mTextPaint.density;
+    }
+
+    /** @hide */
+    @ViewDebug.ExportedProperty(category = "text", mapping = {
+            @ViewDebug.IntToString(from = Typeface.NORMAL, to = "NORMAL"),
+            @ViewDebug.IntToString(from = Typeface.BOLD, to = "BOLD"),
+            @ViewDebug.IntToString(from = Typeface.ITALIC, to = "ITALIC"),
+            @ViewDebug.IntToString(from = Typeface.BOLD_ITALIC, to = "BOLD_ITALIC")
+    })
+    public int getTypefaceStyle() {
+        Typeface typeface = mTextPaint.getTypeface();
+        return typeface != null ? typeface.getStyle() : Typeface.NORMAL;
+    }
+
+    /**
+     * Set the default text size to the given value, interpreted as "scaled
+     * pixel" units.  This size is adjusted based on the current density and
+     * user font size preference.
+     *
+     * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
+     *
+     * @param size The scaled pixel size.
+     *
+     * @attr ref android.R.styleable#TextView_textSize
+     */
+    @android.view.RemotableViewMethod
+    public void setTextSize(float size) {
+        setTextSize(TypedValue.COMPLEX_UNIT_SP, size);
+    }
+
+    /**
+     * Set the default text size to a given unit and value. See {@link
+     * TypedValue} for the possible dimension units.
+     *
+     * <p>Note: if this TextView has the auto-size feature enabled than this function is no-op.
+     *
+     * @param unit The desired dimension unit.
+     * @param size The desired size in the given units.
+     *
+     * @attr ref android.R.styleable#TextView_textSize
+     */
+    public void setTextSize(int unit, float size) {
+        if (!isAutoSizeEnabled()) {
+            setTextSizeInternal(unit, size, true /* shouldRequestLayout */);
+        }
+    }
+
+    private void setTextSizeInternal(int unit, float size, boolean shouldRequestLayout) {
+        Context c = getContext();
+        Resources r;
+
+        if (c == null) {
+            r = Resources.getSystem();
+        } else {
+            r = c.getResources();
+        }
+
+        setRawTextSize(TypedValue.applyDimension(unit, size, r.getDisplayMetrics()),
+                shouldRequestLayout);
+    }
+
+    private void setRawTextSize(float size, boolean shouldRequestLayout) {
+        if (size != mTextPaint.getTextSize()) {
+            mTextPaint.setTextSize(size);
+
+            if (shouldRequestLayout && mLayout != null) {
+                // Do not auto-size right after setting the text size.
+                mNeedsAutoSizeText = false;
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Gets the extent by which text should be stretched horizontally.
+     * This will usually be 1.0.
+     * @return The horizontal scale factor.
+     */
+    public float getTextScaleX() {
+        return mTextPaint.getTextScaleX();
+    }
+
+    /**
+     * Sets the horizontal scale factor for text. The default value
+     * is 1.0. Values greater than 1.0 stretch the text wider.
+     * Values less than 1.0 make the text narrower. By default, this value is 1.0.
+     * @param size The horizontal scale factor.
+     * @attr ref android.R.styleable#TextView_textScaleX
+     */
+    @android.view.RemotableViewMethod
+    public void setTextScaleX(float size) {
+        if (size != mTextPaint.getTextScaleX()) {
+            mUserSetTextScaleX = true;
+            mTextPaint.setTextScaleX(size);
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Sets the typeface and style in which the text should be displayed.
+     * Note that not all Typeface families actually have bold and italic
+     * variants, so you may need to use
+     * {@link #setTypeface(Typeface, int)} to get the appearance
+     * that you actually want.
+     *
+     * @see #getTypeface()
+     *
+     * @attr ref android.R.styleable#TextView_fontFamily
+     * @attr ref android.R.styleable#TextView_typeface
+     * @attr ref android.R.styleable#TextView_textStyle
+     */
+    public void setTypeface(Typeface tf) {
+        if (mTextPaint.getTypeface() != tf) {
+            mTextPaint.setTypeface(tf);
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Gets the current {@link Typeface} that is used to style the text.
+     * @return The current Typeface.
+     *
+     * @see #setTypeface(Typeface)
+     *
+     * @attr ref android.R.styleable#TextView_fontFamily
+     * @attr ref android.R.styleable#TextView_typeface
+     * @attr ref android.R.styleable#TextView_textStyle
+     */
+    public Typeface getTypeface() {
+        return mTextPaint.getTypeface();
+    }
+
+    /**
+     * Set the TextView's elegant height metrics flag. This setting selects font
+     * variants that have not been compacted to fit Latin-based vertical
+     * metrics, and also increases top and bottom bounds to provide more space.
+     *
+     * @param elegant set the paint's elegant metrics flag.
+     *
+     * @see Paint#isElegantTextHeight(boolean)
+     *
+     * @attr ref android.R.styleable#TextView_elegantTextHeight
+     */
+    public void setElegantTextHeight(boolean elegant) {
+        if (elegant != mTextPaint.isElegantTextHeight()) {
+            mTextPaint.setElegantTextHeight(elegant);
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Get the value of the TextView's elegant height metrics flag. This setting selects font
+     * variants that have not been compacted to fit Latin-based vertical
+     * metrics, and also increases top and bottom bounds to provide more space.
+     * @return {@code true} if the elegant height metrics flag is set.
+     *
+     * @see #setElegantTextHeight(boolean)
+     * @see Paint#setElegantTextHeight(boolean)
+     */
+    public boolean isElegantTextHeight() {
+        return mTextPaint.isElegantTextHeight();
+    }
+
+    /**
+     * Gets the text letter-space value, which determines the spacing between characters.
+     * The value returned is in ems. Normally, this value is 0.0.
+     * @return The text letter-space value in ems.
+     *
+     * @see #setLetterSpacing(float)
+     * @see Paint#setLetterSpacing
+     */
+    public float getLetterSpacing() {
+        return mTextPaint.getLetterSpacing();
+    }
+
+    /**
+     * Sets text letter-spacing in em units.  Typical values
+     * for slight expansion will be around 0.05.  Negative values tighten text.
+     *
+     * @see #getLetterSpacing()
+     * @see Paint#getLetterSpacing
+     *
+     * @param letterSpacing A text letter-space value in ems.
+     * @attr ref android.R.styleable#TextView_letterSpacing
+     */
+    @android.view.RemotableViewMethod
+    public void setLetterSpacing(float letterSpacing) {
+        if (letterSpacing != mTextPaint.getLetterSpacing()) {
+            mTextPaint.setLetterSpacing(letterSpacing);
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Returns the font feature settings. The format is the same as the CSS
+     * font-feature-settings attribute:
+     * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+     *     https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+     *
+     * @return the currently set font feature settings.  Default is null.
+     *
+     * @see #setFontFeatureSettings(String)
+     * @see Paint#setFontFeatureSettings(String) Paint.setFontFeatureSettings(String)
+     */
+    @Nullable
+    public String getFontFeatureSettings() {
+        return mTextPaint.getFontFeatureSettings();
+    }
+
+    /**
+     * Returns the font variation settings.
+     *
+     * @return the currently set font variation settings.  Returns null if no variation is
+     * specified.
+     *
+     * @see #setFontVariationSettings(String)
+     * @see Paint#setFontVariationSettings(String) Paint.setFontVariationSettings(String)
+     */
+    @Nullable
+    public String getFontVariationSettings() {
+        return mTextPaint.getFontVariationSettings();
+    }
+
+    /**
+     * Sets the break strategy for breaking paragraphs into lines. The default value for
+     * TextView is {@link Layout#BREAK_STRATEGY_HIGH_QUALITY}, and the default value for
+     * EditText is {@link Layout#BREAK_STRATEGY_SIMPLE}, the latter to avoid the
+     * text "dancing" when being edited.
+     *
+     * @attr ref android.R.styleable#TextView_breakStrategy
+     * @see #getBreakStrategy()
+     */
+    public void setBreakStrategy(@Layout.BreakStrategy int breakStrategy) {
+        mBreakStrategy = breakStrategy;
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Gets the current strategy for breaking paragraphs into lines.
+     * @return the current strategy for breaking paragraphs into lines.
+     *
+     * @attr ref android.R.styleable#TextView_breakStrategy
+     * @see #setBreakStrategy(int)
+     */
+    @Layout.BreakStrategy
+    public int getBreakStrategy() {
+        return mBreakStrategy;
+    }
+
+    /**
+     * Sets the frequency of automatic hyphenation to use when determining word breaks.
+     * The default value for both TextView and {@link EditText} is
+     * {@link Layout#HYPHENATION_FREQUENCY_NORMAL}.
+     * Note that the default hyphenation frequency value is set from the theme.
+     *
+     * @param hyphenationFrequency The hyphenation frequency to use.
+     * @attr ref android.R.styleable#TextView_hyphenationFrequency
+     * @see #getHyphenationFrequency()
+     */
+    public void setHyphenationFrequency(@Layout.HyphenationFrequency int hyphenationFrequency) {
+        mHyphenationFrequency = hyphenationFrequency;
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * Gets the current frequency of automatic hyphenation to be used when determining word breaks.
+     * @return the current frequency of automatic hyphenation to be used when determining word
+     * breaks.
+     *
+     * @attr ref android.R.styleable#TextView_hyphenationFrequency
+     * @see #setHyphenationFrequency(int)
+     */
+    @Layout.HyphenationFrequency
+    public int getHyphenationFrequency() {
+        return mHyphenationFrequency;
+    }
+
+    /**
+     * Set justification mode. The default value is {@link Layout#JUSTIFICATION_MODE_NONE}. If the
+     * last line is too short for justification, the last line will be displayed with the
+     * alignment set by {@link android.view.View#setTextAlignment}.
+     *
+     * @see #getJustificationMode()
+     */
+    @Layout.JustificationMode
+    public void setJustificationMode(@Layout.JustificationMode int justificationMode) {
+        mJustificationMode = justificationMode;
+        if (mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    /**
+     * @return true if currently paragraph justification mode.
+     *
+     * @see #setJustificationMode(int)
+     */
+    public @Layout.JustificationMode int getJustificationMode() {
+        return mJustificationMode;
+    }
+
+    /**
+     * Sets font feature settings. The format is the same as the CSS
+     * font-feature-settings attribute:
+     * <a href="https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop">
+     *     https://www.w3.org/TR/css-fonts-3/#font-feature-settings-prop</a>
+     *
+     * @param fontFeatureSettings font feature settings represented as CSS compatible string
+     *
+     * @see #getFontFeatureSettings()
+     * @see Paint#getFontFeatureSettings() Paint.getFontFeatureSettings()
+     *
+     * @attr ref android.R.styleable#TextView_fontFeatureSettings
+     */
+    @android.view.RemotableViewMethod
+    public void setFontFeatureSettings(@Nullable String fontFeatureSettings) {
+        if (fontFeatureSettings != mTextPaint.getFontFeatureSettings()) {
+            mTextPaint.setFontFeatureSettings(fontFeatureSettings);
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+
+    /**
+     * Sets TrueType or OpenType font variation settings. The settings string is constructed from
+     * multiple pairs of axis tag and style values. The axis tag must contain four ASCII characters
+     * and must be wrapped with single quotes (U+0027) or double quotes (U+0022). Axis strings that
+     * are longer or shorter than four characters, or contain characters outside of U+0020..U+007E
+     * are invalid. If a specified axis name is not defined in the font, the settings will be
+     * ignored.
+     *
+     * <p>
+     * Examples,
+     * <ul>
+     * <li>Set font width to 150.
+     * <pre>
+     * <code>
+     *   TextView textView = (TextView) findViewById(R.id.textView);
+     *   textView.setFontVariationSettings("'wdth' 150");
+     * </code>
+     * </pre>
+     * </li>
+     *
+     * <li>Set the font slant to 20 degrees and ask for italic style.
+     * <pre>
+     * <code>
+     *   TextView textView = (TextView) findViewById(R.id.textView);
+     *   textView.setFontVariationSettings("'slnt' 20, 'ital' 1");
+     * </code>
+     * </pre>
+     * </p>
+     * </li>
+     * </ul>
+     *
+     * @param fontVariationSettings font variation settings. You can pass null or empty string as
+     *                              no variation settings.
+     * @return true if the given settings is effective to at least one font file underlying this
+     *         TextView. This function also returns true for empty settings string. Otherwise
+     *         returns false.
+     *
+     * @throws IllegalArgumentException If given string is not a valid font variation settings
+     *                                  format.
+     *
+     * @see #getFontVariationSettings()
+     * @see FontVariationAxis
+     */
+    public boolean setFontVariationSettings(@Nullable String fontVariationSettings) {
+        final String existingSettings = mTextPaint.getFontVariationSettings();
+        if (fontVariationSettings == existingSettings
+                || (fontVariationSettings != null
+                        && fontVariationSettings.equals(existingSettings))) {
+            return true;
+        }
+        boolean effective = mTextPaint.setFontVariationSettings(fontVariationSettings);
+
+        if (effective && mLayout != null) {
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+        return effective;
+    }
+
+    /**
+     * Sets the text color for all the states (normal, selected,
+     * focused) to be this color.
+     *
+     * @param color A color value in the form 0xAARRGGBB.
+     * Do not pass a resource ID. To get a color value from a resource ID, call
+     * {@link android.support.v4.content.ContextCompat#getColor(Context, int) getColor}.
+     *
+     * @see #setTextColor(ColorStateList)
+     * @see #getTextColors()
+     *
+     * @attr ref android.R.styleable#TextView_textColor
+     */
+    @android.view.RemotableViewMethod
+    public void setTextColor(@ColorInt int color) {
+        mTextColor = ColorStateList.valueOf(color);
+        updateTextColors();
+    }
+
+    /**
+     * Sets the text color.
+     *
+     * @see #setTextColor(int)
+     * @see #getTextColors()
+     * @see #setHintTextColor(ColorStateList)
+     * @see #setLinkTextColor(ColorStateList)
+     *
+     * @attr ref android.R.styleable#TextView_textColor
+     */
+    @android.view.RemotableViewMethod
+    public void setTextColor(ColorStateList colors) {
+        if (colors == null) {
+            throw new NullPointerException();
+        }
+
+        mTextColor = colors;
+        updateTextColors();
+    }
+
+    /**
+     * Gets the text colors for the different states (normal, selected, focused) of the TextView.
+     *
+     * @see #setTextColor(ColorStateList)
+     * @see #setTextColor(int)
+     *
+     * @attr ref android.R.styleable#TextView_textColor
+     */
+    public final ColorStateList getTextColors() {
+        return mTextColor;
+    }
+
+    /**
+     * Return the current color selected for normal text.
+     *
+     * @return Returns the current text color.
+     */
+    @ColorInt
+    public final int getCurrentTextColor() {
+        return mCurTextColor;
+    }
+
+    /**
+     * Sets the color used to display the selection highlight.
+     *
+     * @attr ref android.R.styleable#TextView_textColorHighlight
+     */
+    @android.view.RemotableViewMethod
+    public void setHighlightColor(@ColorInt int color) {
+        if (mHighlightColor != color) {
+            mHighlightColor = color;
+            invalidate();
+        }
+    }
+
+    /**
+     * @return the color used to display the selection highlight
+     *
+     * @see #setHighlightColor(int)
+     *
+     * @attr ref android.R.styleable#TextView_textColorHighlight
+     */
+    @ColorInt
+    public int getHighlightColor() {
+        return mHighlightColor;
+    }
+
+    /**
+     * Sets whether the soft input method will be made visible when this
+     * TextView gets focused. The default is true.
+     */
+    @android.view.RemotableViewMethod
+    public final void setShowSoftInputOnFocus(boolean show) {
+        createEditorIfNeeded();
+        mEditor.mShowSoftInputOnFocus = show;
+    }
+
+    /**
+     * Returns whether the soft input method will be made visible when this
+     * TextView gets focused. The default is true.
+     */
+    public final boolean getShowSoftInputOnFocus() {
+        // When there is no Editor, return default true value
+        return mEditor == null || mEditor.mShowSoftInputOnFocus;
+    }
+
+    /**
+     * Gives the text a shadow of the specified blur radius and color, the specified
+     * distance from its drawn position.
+     * <p>
+     * The text shadow produced does not interact with the properties on view
+     * that are responsible for real time shadows,
+     * {@link View#getElevation() elevation} and
+     * {@link View#getTranslationZ() translationZ}.
+     *
+     * @see Paint#setShadowLayer(float, float, float, int)
+     *
+     * @attr ref android.R.styleable#TextView_shadowColor
+     * @attr ref android.R.styleable#TextView_shadowDx
+     * @attr ref android.R.styleable#TextView_shadowDy
+     * @attr ref android.R.styleable#TextView_shadowRadius
+     */
+    public void setShadowLayer(float radius, float dx, float dy, int color) {
+        mTextPaint.setShadowLayer(radius, dx, dy, color);
+
+        mShadowRadius = radius;
+        mShadowDx = dx;
+        mShadowDy = dy;
+        mShadowColor = color;
+
+        // Will change text clip region
+        if (mEditor != null) {
+            mEditor.invalidateTextDisplayList();
+            mEditor.invalidateHandlesAndActionMode();
+        }
+        invalidate();
+    }
+
+    /**
+     * Gets the radius of the shadow layer.
+     *
+     * @return the radius of the shadow layer. If 0, the shadow layer is not visible
+     *
+     * @see #setShadowLayer(float, float, float, int)
+     *
+     * @attr ref android.R.styleable#TextView_shadowRadius
+     */
+    public float getShadowRadius() {
+        return mShadowRadius;
+    }
+
+    /**
+     * @return the horizontal offset of the shadow layer
+     *
+     * @see #setShadowLayer(float, float, float, int)
+     *
+     * @attr ref android.R.styleable#TextView_shadowDx
+     */
+    public float getShadowDx() {
+        return mShadowDx;
+    }
+
+    /**
+     * Gets the vertical offset of the shadow layer.
+     * @return The vertical offset of the shadow layer.
+     *
+     * @see #setShadowLayer(float, float, float, int)
+     *
+     * @attr ref android.R.styleable#TextView_shadowDy
+     */
+    public float getShadowDy() {
+        return mShadowDy;
+    }
+
+    /**
+     * Gets the color of the shadow layer.
+     * @return the color of the shadow layer
+     *
+     * @see #setShadowLayer(float, float, float, int)
+     *
+     * @attr ref android.R.styleable#TextView_shadowColor
+     */
+    @ColorInt
+    public int getShadowColor() {
+        return mShadowColor;
+    }
+
+    /**
+     * Gets the {@link TextPaint} used for the text.
+     * Use this only to consult the Paint's properties and not to change them.
+     * @return The base paint used for the text.
+     */
+    public TextPaint getPaint() {
+        return mTextPaint;
+    }
+
+    /**
+     * Sets the autolink mask of the text.  See {@link
+     * android.text.util.Linkify#ALL Linkify.ALL} and peers for
+     * possible values.
+     *
+     * @attr ref android.R.styleable#TextView_autoLink
+     */
+    @android.view.RemotableViewMethod
+    public final void setAutoLinkMask(int mask) {
+        mAutoLinkMask = mask;
+    }
+
+    /**
+     * Sets whether the movement method will automatically be set to
+     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+     * set to nonzero and links are detected in {@link #setText}.
+     * The default is true.
+     *
+     * @attr ref android.R.styleable#TextView_linksClickable
+     */
+    @android.view.RemotableViewMethod
+    public final void setLinksClickable(boolean whether) {
+        mLinksClickable = whether;
+    }
+
+    /**
+     * Returns whether the movement method will automatically be set to
+     * {@link LinkMovementMethod} if {@link #setAutoLinkMask} has been
+     * set to nonzero and links are detected in {@link #setText}.
+     * The default is true.
+     *
+     * @attr ref android.R.styleable#TextView_linksClickable
+     */
+    public final boolean getLinksClickable() {
+        return mLinksClickable;
+    }
+
+    /**
+     * Returns the list of {@link android.text.style.URLSpan URLSpans} attached to the text
+     * (by {@link Linkify} or otherwise) if any.  You can call
+     * {@link URLSpan#getURL} on them to find where they link to
+     * or use {@link Spanned#getSpanStart} and {@link Spanned#getSpanEnd}
+     * to find the region of the text they are attached to.
+     */
+    public URLSpan[] getUrls() {
+        if (mText instanceof Spanned) {
+            return ((Spanned) mText).getSpans(0, mText.length(), URLSpan.class);
+        } else {
+            return new URLSpan[0];
+        }
+    }
+
+    /**
+     * Sets the color of the hint text for all the states (disabled, focussed, selected...) of this
+     * TextView.
+     *
+     * @see #setHintTextColor(ColorStateList)
+     * @see #getHintTextColors()
+     * @see #setTextColor(int)
+     *
+     * @attr ref android.R.styleable#TextView_textColorHint
+     */
+    @android.view.RemotableViewMethod
+    public final void setHintTextColor(@ColorInt int color) {
+        mHintTextColor = ColorStateList.valueOf(color);
+        updateTextColors();
+    }
+
+    /**
+     * Sets the color of the hint text.
+     *
+     * @see #getHintTextColors()
+     * @see #setHintTextColor(int)
+     * @see #setTextColor(ColorStateList)
+     * @see #setLinkTextColor(ColorStateList)
+     *
+     * @attr ref android.R.styleable#TextView_textColorHint
+     */
+    public final void setHintTextColor(ColorStateList colors) {
+        mHintTextColor = colors;
+        updateTextColors();
+    }
+
+    /**
+     * @return the color of the hint text, for the different states of this TextView.
+     *
+     * @see #setHintTextColor(ColorStateList)
+     * @see #setHintTextColor(int)
+     * @see #setTextColor(ColorStateList)
+     * @see #setLinkTextColor(ColorStateList)
+     *
+     * @attr ref android.R.styleable#TextView_textColorHint
+     */
+    public final ColorStateList getHintTextColors() {
+        return mHintTextColor;
+    }
+
+    /**
+     * <p>Return the current color selected to paint the hint text.</p>
+     *
+     * @return Returns the current hint text color.
+     */
+    @ColorInt
+    public final int getCurrentHintTextColor() {
+        return mHintTextColor != null ? mCurHintTextColor : mCurTextColor;
+    }
+
+    /**
+     * Sets the color of links in the text.
+     *
+     * @see #setLinkTextColor(ColorStateList)
+     * @see #getLinkTextColors()
+     *
+     * @attr ref android.R.styleable#TextView_textColorLink
+     */
+    @android.view.RemotableViewMethod
+    public final void setLinkTextColor(@ColorInt int color) {
+        mLinkTextColor = ColorStateList.valueOf(color);
+        updateTextColors();
+    }
+
+    /**
+     * Sets the color of links in the text.
+     *
+     * @see #setLinkTextColor(int)
+     * @see #getLinkTextColors()
+     * @see #setTextColor(ColorStateList)
+     * @see #setHintTextColor(ColorStateList)
+     *
+     * @attr ref android.R.styleable#TextView_textColorLink
+     */
+    public final void setLinkTextColor(ColorStateList colors) {
+        mLinkTextColor = colors;
+        updateTextColors();
+    }
+
+    /**
+     * @return the list of colors used to paint the links in the text, for the different states of
+     * this TextView
+     *
+     * @see #setLinkTextColor(ColorStateList)
+     * @see #setLinkTextColor(int)
+     *
+     * @attr ref android.R.styleable#TextView_textColorLink
+     */
+    public final ColorStateList getLinkTextColors() {
+        return mLinkTextColor;
+    }
+
+    /**
+     * Sets the horizontal alignment of the text and the
+     * vertical gravity that will be used when there is extra space
+     * in the TextView beyond what is required for the text itself.
+     *
+     * @see android.view.Gravity
+     * @attr ref android.R.styleable#TextView_gravity
+     */
+    public void setGravity(int gravity) {
+        if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) == 0) {
+            gravity |= Gravity.START;
+        }
+        if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == 0) {
+            gravity |= Gravity.TOP;
+        }
+
+        boolean newLayout = false;
+
+        if ((gravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)
+                != (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK)) {
+            newLayout = true;
+        }
+
+        if (gravity != mGravity) {
+            invalidate();
+        }
+
+        mGravity = gravity;
+
+        if (mLayout != null && newLayout) {
+            // XXX this is heavy-handed because no actual content changes.
+            int want = mLayout.getWidth();
+            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+                    mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(), true);
+        }
+    }
+
+    /**
+     * Returns the horizontal and vertical alignment of this TextView.
+     *
+     * @see android.view.Gravity
+     * @attr ref android.R.styleable#TextView_gravity
+     */
+    public int getGravity() {
+        return mGravity;
+    }
+
+    /**
+     * Gets the flags on the Paint being used to display the text.
+     * @return The flags on the Paint being used to display the text.
+     * @see Paint#getFlags
+     */
+    public int getPaintFlags() {
+        return mTextPaint.getFlags();
+    }
+
+    /**
+     * Sets flags on the Paint being used to display the text and
+     * reflows the text if they are different from the old flags.
+     * @see Paint#setFlags
+     */
+    @android.view.RemotableViewMethod
+    public void setPaintFlags(int flags) {
+        if (mTextPaint.getFlags() != flags) {
+            mTextPaint.setFlags(flags);
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Sets whether the text should be allowed to be wider than the
+     * View is.  If false, it will be wrapped to the width of the View.
+     *
+     * @attr ref android.R.styleable#TextView_scrollHorizontally
+     */
+    public void setHorizontallyScrolling(boolean whether) {
+        if (mHorizontallyScrolling != whether) {
+            mHorizontallyScrolling = whether;
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Returns whether the text is allowed to be wider than the View is.
+     * If false, the text will be wrapped to the width of the View.
+     *
+     * @attr ref android.R.styleable#TextView_scrollHorizontally
+     * @hide
+     */
+    public boolean getHorizontallyScrolling() {
+        return mHorizontallyScrolling;
+    }
+
+    /**
+     * Sets the height of the TextView to be at least {@code minLines} tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides other previous minimum height configurations such
+     * as {@link #setMinHeight(int)} or {@link #setHeight(int)}. {@link #setSingleLine()} will set
+     * this value to 1.
+     *
+     * @param minLines the minimum height of TextView in terms of number of lines
+     *
+     * @see #getMinLines()
+     * @see #setLines(int)
+     *
+     * @attr ref android.R.styleable#TextView_minLines
+     */
+    @android.view.RemotableViewMethod
+    public void setMinLines(int minLines) {
+        mMinimum = minLines;
+        mMinMode = LINES;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the minimum height of TextView in terms of number of lines or -1 if the minimum
+     * height was set using {@link #setMinHeight(int)} or {@link #setHeight(int)}.
+     *
+     * @return the minimum height of TextView in terms of number of lines or -1 if the minimum
+     *         height is not defined in lines
+     *
+     * @see #setMinLines(int)
+     * @see #setLines(int)
+     *
+     * @attr ref android.R.styleable#TextView_minLines
+     */
+    public int getMinLines() {
+        return mMinMode == LINES ? mMinimum : -1;
+    }
+
+    /**
+     * Sets the height of the TextView to be at least {@code minPixels} tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides previous minimum height configurations such as
+     * {@link #setMinLines(int)} or {@link #setLines(int)}.
+     * <p>
+     * The value given here is different than {@link #setMinimumHeight(int)}. Between
+     * {@code minHeight} and the value set in {@link #setMinimumHeight(int)}, the greater one is
+     * used to decide the final height.
+     *
+     * @param minPixels the minimum height of TextView in terms of pixels
+     *
+     * @see #getMinHeight()
+     * @see #setHeight(int)
+     *
+     * @attr ref android.R.styleable#TextView_minHeight
+     */
+    @android.view.RemotableViewMethod
+    public void setMinHeight(int minPixels) {
+        mMinimum = minPixels;
+        mMinMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the minimum height of TextView in terms of pixels or -1 if the minimum height was
+     * set using {@link #setMinLines(int)} or {@link #setLines(int)}.
+     *
+     * @return the minimum height of TextView in terms of pixels or -1 if the minimum height is not
+     *         defined in pixels
+     *
+     * @see #setMinHeight(int)
+     * @see #setHeight(int)
+     *
+     * @attr ref android.R.styleable#TextView_minHeight
+     */
+    public int getMinHeight() {
+        return mMinMode == PIXELS ? mMinimum : -1;
+    }
+
+    /**
+     * Sets the height of the TextView to be at most {@code maxLines} tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides previous maximum height configurations such as
+     * {@link #setMaxHeight(int)} or {@link #setLines(int)}.
+     *
+     * @param maxLines the maximum height of TextView in terms of number of lines
+     *
+     * @see #getMaxLines()
+     * @see #setLines(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxLines
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxLines(int maxLines) {
+        mMaximum = maxLines;
+        mMaxMode = LINES;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the maximum height of TextView in terms of number of lines or -1 if the
+     * maximum height was set using {@link #setMaxHeight(int)} or {@link #setHeight(int)}.
+     *
+     * @return the maximum height of TextView in terms of number of lines. -1 if the maximum height
+     *         is not defined in lines.
+     *
+     * @see #setMaxLines(int)
+     * @see #setLines(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxLines
+     */
+    public int getMaxLines() {
+        return mMaxMode == LINES ? mMaximum : -1;
+    }
+
+    /**
+     * Sets the height of the TextView to be at most {@code maxPixels} tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides previous maximum height configurations such as
+     * {@link #setMaxLines(int)} or {@link #setLines(int)}.
+     *
+     * @param maxPixels the maximum height of TextView in terms of pixels
+     *
+     * @see #getMaxHeight()
+     * @see #setHeight(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxHeight
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxHeight(int maxPixels) {
+        mMaximum = maxPixels;
+        mMaxMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the maximum height of TextView in terms of pixels or -1 if the maximum height was
+     * set using {@link #setMaxLines(int)} or {@link #setLines(int)}.
+     *
+     * @return the maximum height of TextView in terms of pixels or -1 if the maximum height
+     *         is not defined in pixels
+     *
+     * @see #setMaxHeight(int)
+     * @see #setHeight(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxHeight
+     */
+    public int getMaxHeight() {
+        return mMaxMode == PIXELS ? mMaximum : -1;
+    }
+
+    /**
+     * Sets the height of the TextView to be exactly {@code lines} tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides previous minimum/maximum height configurations
+     * such as {@link #setMinLines(int)} or {@link #setMaxLines(int)}. {@link #setSingleLine()} will
+     * set this value to 1.
+     *
+     * @param lines the exact height of the TextView in terms of lines
+     *
+     * @see #setHeight(int)
+     *
+     * @attr ref android.R.styleable#TextView_lines
+     */
+    @android.view.RemotableViewMethod
+    public void setLines(int lines) {
+        mMaximum = mMinimum = lines;
+        mMaxMode = mMinMode = LINES;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets the height of the TextView to be exactly <code>pixels</code> tall.
+     * <p>
+     * This value is used for height calculation if LayoutParams does not force TextView to have an
+     * exact height. Setting this value overrides previous minimum/maximum height configurations
+     * such as {@link #setMinHeight(int)} or {@link #setMaxHeight(int)}.
+     *
+     * @param pixels the exact height of the TextView in terms of pixels
+     *
+     * @see #setLines(int)
+     *
+     * @attr ref android.R.styleable#TextView_height
+     */
+    @android.view.RemotableViewMethod
+    public void setHeight(int pixels) {
+        mMaximum = mMinimum = pixels;
+        mMaxMode = mMinMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets the width of the TextView to be at least {@code minEms} wide.
+     * <p>
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous minimum width configurations such as
+     * {@link #setMinWidth(int)} or {@link #setWidth(int)}.
+     *
+     * @param minEms the minimum width of TextView in terms of ems
+     *
+     * @see #getMinEms()
+     * @see #setEms(int)
+     *
+     * @attr ref android.R.styleable#TextView_minEms
+     */
+    @android.view.RemotableViewMethod
+    public void setMinEms(int minEms) {
+        mMinWidth = minEms;
+        mMinWidthMode = EMS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the minimum width of TextView in terms of ems or -1 if the minimum width was set
+     * using {@link #setMinWidth(int)} or {@link #setWidth(int)}.
+     *
+     * @return the minimum width of TextView in terms of ems. -1 if the minimum width is not
+     *         defined in ems
+     *
+     * @see #setMinEms(int)
+     * @see #setEms(int)
+     *
+     * @attr ref android.R.styleable#TextView_minEms
+     */
+    public int getMinEms() {
+        return mMinWidthMode == EMS ? mMinWidth : -1;
+    }
+
+    /**
+     * Sets the width of the TextView to be at least {@code minPixels} wide.
+     * <p>
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous minimum width configurations such as
+     * {@link #setMinEms(int)} or {@link #setEms(int)}.
+     * <p>
+     * The value given here is different than {@link #setMinimumWidth(int)}. Between
+     * {@code minWidth} and the value set in {@link #setMinimumWidth(int)}, the greater one is used
+     * to decide the final width.
+     *
+     * @param minPixels the minimum width of TextView in terms of pixels
+     *
+     * @see #getMinWidth()
+     * @see #setWidth(int)
+     *
+     * @attr ref android.R.styleable#TextView_minWidth
+     */
+    @android.view.RemotableViewMethod
+    public void setMinWidth(int minPixels) {
+        mMinWidth = minPixels;
+        mMinWidthMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the minimum width of TextView in terms of pixels or -1 if the minimum width was set
+     * using {@link #setMinEms(int)} or {@link #setEms(int)}.
+     *
+     * @return the minimum width of TextView in terms of pixels or -1 if the minimum width is not
+     *         defined in pixels
+     *
+     * @see #setMinWidth(int)
+     * @see #setWidth(int)
+     *
+     * @attr ref android.R.styleable#TextView_minWidth
+     */
+    public int getMinWidth() {
+        return mMinWidthMode == PIXELS ? mMinWidth : -1;
+    }
+
+    /**
+     * Sets the width of the TextView to be at most {@code maxEms} wide.
+     * <p>
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous maximum width configurations such as
+     * {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
+     *
+     * @param maxEms the maximum width of TextView in terms of ems
+     *
+     * @see #getMaxEms()
+     * @see #setEms(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxEms
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxEms(int maxEms) {
+        mMaxWidth = maxEms;
+        mMaxWidthMode = EMS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the maximum width of TextView in terms of ems or -1 if the maximum width was set
+     * using {@link #setMaxWidth(int)} or {@link #setWidth(int)}.
+     *
+     * @return the maximum width of TextView in terms of ems or -1 if the maximum width is not
+     *         defined in ems
+     *
+     * @see #setMaxEms(int)
+     * @see #setEms(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxEms
+     */
+    public int getMaxEms() {
+        return mMaxWidthMode == EMS ? mMaxWidth : -1;
+    }
+
+    /**
+     * Sets the width of the TextView to be at most {@code maxPixels} wide.
+     * <p>
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous maximum width configurations such as
+     * {@link #setMaxEms(int)} or {@link #setEms(int)}.
+     *
+     * @param maxPixels the maximum width of TextView in terms of pixels
+     *
+     * @see #getMaxWidth()
+     * @see #setWidth(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxWidth
+     */
+    @android.view.RemotableViewMethod
+    public void setMaxWidth(int maxPixels) {
+        mMaxWidth = maxPixels;
+        mMaxWidthMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Returns the maximum width of TextView in terms of pixels or -1 if the maximum width was set
+     * using {@link #setMaxEms(int)} or {@link #setEms(int)}.
+     *
+     * @return the maximum width of TextView in terms of pixels. -1 if the maximum width is not
+     *         defined in pixels
+     *
+     * @see #setMaxWidth(int)
+     * @see #setWidth(int)
+     *
+     * @attr ref android.R.styleable#TextView_maxWidth
+     */
+    public int getMaxWidth() {
+        return mMaxWidthMode == PIXELS ? mMaxWidth : -1;
+    }
+
+    /**
+     * Sets the width of the TextView to be exactly {@code ems} wide.
+     *
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous minimum/maximum configurations such as
+     * {@link #setMinEms(int)} or {@link #setMaxEms(int)}.
+     *
+     * @param ems the exact width of the TextView in terms of ems
+     *
+     * @see #setWidth(int)
+     *
+     * @attr ref android.R.styleable#TextView_ems
+     */
+    @android.view.RemotableViewMethod
+    public void setEms(int ems) {
+        mMaxWidth = mMinWidth = ems;
+        mMaxWidthMode = mMinWidthMode = EMS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets the width of the TextView to be exactly {@code pixels} wide.
+     * <p>
+     * This value is used for width calculation if LayoutParams does not force TextView to have an
+     * exact width. Setting this value overrides previous minimum/maximum width configurations
+     * such as {@link #setMinWidth(int)} or {@link #setMaxWidth(int)}.
+     *
+     * @param pixels the exact width of the TextView in terms of pixels
+     *
+     * @see #setEms(int)
+     *
+     * @attr ref android.R.styleable#TextView_width
+     */
+    @android.view.RemotableViewMethod
+    public void setWidth(int pixels) {
+        mMaxWidth = mMinWidth = pixels;
+        mMaxWidthMode = mMinWidthMode = PIXELS;
+
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets line spacing for this TextView.  Each line other than the last line will have its height
+     * multiplied by {@code mult} and have {@code add} added to it.
+     *
+     *
+     * @attr ref android.R.styleable#TextView_lineSpacingExtra
+     * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+     */
+    public void setLineSpacing(float add, float mult) {
+        if (mSpacingAdd != add || mSpacingMult != mult) {
+            mSpacingAdd = add;
+            mSpacingMult = mult;
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Gets the line spacing multiplier
+     *
+     * @return the value by which each line's height is multiplied to get its actual height.
+     *
+     * @see #setLineSpacing(float, float)
+     * @see #getLineSpacingExtra()
+     *
+     * @attr ref android.R.styleable#TextView_lineSpacingMultiplier
+     */
+    public float getLineSpacingMultiplier() {
+        return mSpacingMult;
+    }
+
+    /**
+     * Gets the line spacing extra space
+     *
+     * @return the extra space that is added to the height of each lines of this TextView.
+     *
+     * @see #setLineSpacing(float, float)
+     * @see #getLineSpacingMultiplier()
+     *
+     * @attr ref android.R.styleable#TextView_lineSpacingExtra
+     */
+    public float getLineSpacingExtra() {
+        return mSpacingAdd;
+    }
+
+    /**
+     * Convenience method to append the specified text to the TextView's
+     * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+     * if it was not already editable.
+     *
+     * @param text text to be appended to the already displayed text
+     */
+    public final void append(CharSequence text) {
+        append(text, 0, text.length());
+    }
+
+    /**
+     * Convenience method to append the specified text slice to the TextView's
+     * display buffer, upgrading it to {@link android.widget.TextView.BufferType#EDITABLE}
+     * if it was not already editable.
+     *
+     * @param text text to be appended to the already displayed text
+     * @param start the index of the first character in the {@code text}
+     * @param end the index of the character following the last character in the {@code text}
+     *
+     * @see Appendable#append(CharSequence, int, int)
+     */
+    public void append(CharSequence text, int start, int end) {
+        if (!(mText instanceof Editable)) {
+            setText(mText, BufferType.EDITABLE);
+        }
+
+        ((Editable) mText).append(text, start, end);
+
+        if (mAutoLinkMask != 0) {
+            boolean linksWereAdded = Linkify.addLinks((Spannable) mText, mAutoLinkMask);
+            // Do not change the movement method for text that support text selection as it
+            // would prevent an arbitrary cursor displacement.
+            if (linksWereAdded && mLinksClickable && !textCanBeSelected()) {
+                setMovementMethod(LinkMovementMethod.getInstance());
+            }
+        }
+    }
+
+    private void updateTextColors() {
+        boolean inval = false;
+        final int[] drawableState = getDrawableState();
+        int color = mTextColor.getColorForState(drawableState, 0);
+        if (color != mCurTextColor) {
+            mCurTextColor = color;
+            inval = true;
+        }
+        if (mLinkTextColor != null) {
+            color = mLinkTextColor.getColorForState(drawableState, 0);
+            if (color != mTextPaint.linkColor) {
+                mTextPaint.linkColor = color;
+                inval = true;
+            }
+        }
+        if (mHintTextColor != null) {
+            color = mHintTextColor.getColorForState(drawableState, 0);
+            if (color != mCurHintTextColor) {
+                mCurHintTextColor = color;
+                if (mText.length() == 0) {
+                    inval = true;
+                }
+            }
+        }
+        if (inval) {
+            // Text needs to be redrawn with the new color
+            if (mEditor != null) mEditor.invalidateTextDisplayList();
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+
+        if (mTextColor != null && mTextColor.isStateful()
+                || (mHintTextColor != null && mHintTextColor.isStateful())
+                || (mLinkTextColor != null && mLinkTextColor.isStateful())) {
+            updateTextColors();
+        }
+
+        if (mDrawables != null) {
+            final int[] state = getDrawableState();
+            for (Drawable dr : mDrawables.mShowing) {
+                if (dr != null && dr.isStateful() && dr.setState(state)) {
+                    invalidateDrawable(dr);
+                }
+            }
+        }
+    }
+
+    @Override
+    public void drawableHotspotChanged(float x, float y) {
+        super.drawableHotspotChanged(x, y);
+
+        if (mDrawables != null) {
+            for (Drawable dr : mDrawables.mShowing) {
+                if (dr != null) {
+                    dr.setHotspot(x, y);
+                }
+            }
+        }
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+
+        // Save state if we are forced to
+        final boolean freezesText = getFreezesText();
+        boolean hasSelection = false;
+        int start = -1;
+        int end = -1;
+
+        if (mText != null) {
+            start = getSelectionStart();
+            end = getSelectionEnd();
+            if (start >= 0 || end >= 0) {
+                // Or save state if there is a selection
+                hasSelection = true;
+            }
+        }
+
+        if (freezesText || hasSelection) {
+            SavedState ss = new SavedState(superState);
+
+            if (freezesText) {
+                if (mText instanceof Spanned) {
+                    final Spannable sp = new SpannableStringBuilder(mText);
+
+                    if (mEditor != null) {
+                        removeMisspelledSpans(sp);
+                        sp.removeSpan(mEditor.mSuggestionRangeSpan);
+                    }
+
+                    ss.text = sp;
+                } else {
+                    ss.text = mText.toString();
+                }
+            }
+
+            if (hasSelection) {
+                // XXX Should also save the current scroll position!
+                ss.selStart = start;
+                ss.selEnd = end;
+            }
+
+            if (isFocused() && start >= 0 && end >= 0) {
+                ss.frozenWithFocus = true;
+            }
+
+            ss.error = getError();
+
+            if (mEditor != null) {
+                ss.editorState = mEditor.saveInstanceState();
+            }
+            return ss;
+        }
+
+        return superState;
+    }
+
+    void removeMisspelledSpans(Spannable spannable) {
+        SuggestionSpan[] suggestionSpans = spannable.getSpans(0, spannable.length(),
+                SuggestionSpan.class);
+        for (int i = 0; i < suggestionSpans.length; i++) {
+            int flags = suggestionSpans[i].getFlags();
+            if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
+                    && (flags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+                spannable.removeSpan(suggestionSpans[i]);
+            }
+        }
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (!(state instanceof SavedState)) {
+            super.onRestoreInstanceState(state);
+            return;
+        }
+
+        SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        // XXX restore buffer type too, as well as lots of other stuff
+        if (ss.text != null) {
+            setText(ss.text);
+        }
+
+        if (ss.selStart >= 0 && ss.selEnd >= 0) {
+            if (mText instanceof Spannable) {
+                int len = mText.length();
+
+                if (ss.selStart > len || ss.selEnd > len) {
+                    String restored = "";
+
+                    if (ss.text != null) {
+                        restored = "(restored) ";
+                    }
+
+                    Log.e(LOG_TAG, "Saved cursor position " + ss.selStart + "/" + ss.selEnd
+                            + " out of range for " + restored + "text " + mText);
+                } else {
+                    Selection.setSelection((Spannable) mText, ss.selStart, ss.selEnd);
+
+                    if (ss.frozenWithFocus) {
+                        createEditorIfNeeded();
+                        mEditor.mFrozenWithFocus = true;
+                    }
+                }
+            }
+        }
+
+        if (ss.error != null) {
+            final CharSequence error = ss.error;
+            // Display the error later, after the first layout pass
+            post(new Runnable() {
+                public void run() {
+                    if (mEditor == null || !mEditor.mErrorWasChanged) {
+                        setError(error);
+                    }
+                }
+            });
+        }
+
+        if (ss.editorState != null) {
+            createEditorIfNeeded();
+            mEditor.restoreInstanceState(ss.editorState);
+        }
+    }
+
+    /**
+     * Control whether this text view saves its entire text contents when
+     * freezing to an icicle, in addition to dynamic state such as cursor
+     * position.  By default this is false, not saving the text.  Set to true
+     * if the text in the text view is not being saved somewhere else in
+     * persistent storage (such as in a content provider) so that if the
+     * view is later thawed the user will not lose their data. For
+     * {@link android.widget.EditText} it is always enabled, regardless of
+     * the value of the attribute.
+     *
+     * @param freezesText Controls whether a frozen icicle should include the
+     * entire text data: true to include it, false to not.
+     *
+     * @attr ref android.R.styleable#TextView_freezesText
+     */
+    @android.view.RemotableViewMethod
+    public void setFreezesText(boolean freezesText) {
+        mFreezesText = freezesText;
+    }
+
+    /**
+     * Return whether this text view is including its entire text contents
+     * in frozen icicles. For {@link android.widget.EditText} it always returns true.
+     *
+     * @return Returns true if text is included, false if it isn't.
+     *
+     * @see #setFreezesText
+     */
+    public boolean getFreezesText() {
+        return mFreezesText;
+    }
+
+    ///////////////////////////////////////////////////////////////////////////
+
+    /**
+     * Sets the Factory used to create new {@link Editable Editables}.
+     *
+     * @param factory {@link android.text.Editable.Factory Editable.Factory} to be used
+     *
+     * @see android.text.Editable.Factory
+     * @see android.widget.TextView.BufferType#EDITABLE
+     */
+    public final void setEditableFactory(Editable.Factory factory) {
+        mEditableFactory = factory;
+        setText(mText);
+    }
+
+    /**
+     * Sets the Factory used to create new {@link Spannable Spannables}.
+     *
+     * @param factory {@link android.text.Spannable.Factory Spannable.Factory} to be used
+     *
+     * @see android.text.Spannable.Factory
+     * @see android.widget.TextView.BufferType#SPANNABLE
+     */
+    public final void setSpannableFactory(Spannable.Factory factory) {
+        mSpannableFactory = factory;
+        setText(mText);
+    }
+
+    /**
+     * Sets the text to be displayed. TextView <em>does not</em> accept
+     * HTML-like formatting, which you can do with text strings in XML resource files.
+     * To style your strings, attach android.text.style.* objects to a
+     * {@link android.text.SpannableString}, or see the
+     * <a href="{@docRoot}guide/topics/resources/available-resources.html#stringresources">
+     * Available Resource Types</a> documentation for an example of setting
+     * formatted text in the XML resource file.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     *
+     * @attr ref android.R.styleable#TextView_text
+     */
+    @android.view.RemotableViewMethod
+    public final void setText(CharSequence text) {
+        setText(text, mBufferType);
+    }
+
+    /**
+     * Sets the text to be displayed but retains the cursor position. Same as
+     * {@link #setText(CharSequence)} except that the cursor position (if any) is retained in the
+     * new text.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     *
+     * @see #setText(CharSequence)
+     */
+    @android.view.RemotableViewMethod
+    public final void setTextKeepState(CharSequence text) {
+        setTextKeepState(text, mBufferType);
+    }
+
+    /**
+     * Sets the text to be displayed and the {@link android.widget.TextView.BufferType}.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
+     *
+     * @see #setText(CharSequence)
+     * @see android.widget.TextView.BufferType
+     * @see #setSpannableFactory(Spannable.Factory)
+     * @see #setEditableFactory(Editable.Factory)
+     *
+     * @attr ref android.R.styleable#TextView_text
+     * @attr ref android.R.styleable#TextView_bufferType
+     */
+    public void setText(CharSequence text, BufferType type) {
+        setText(text, type, true, 0);
+
+        if (mCharWrapper != null) {
+            mCharWrapper.mChars = null;
+        }
+    }
+
+    private void setText(CharSequence text, BufferType type,
+                         boolean notifyBefore, int oldlen) {
+        mTextFromResource = false;
+        if (text == null) {
+            text = "";
+        }
+
+        // If suggestions are not enabled, remove the suggestion spans from the text
+        if (!isSuggestionsEnabled()) {
+            text = removeSuggestionSpans(text);
+        }
+
+        if (!mUserSetTextScaleX) mTextPaint.setTextScaleX(1.0f);
+
+        if (text instanceof Spanned
+                && ((Spanned) text).getSpanStart(TextUtils.TruncateAt.MARQUEE) >= 0) {
+            if (ViewConfiguration.get(mContext).isFadingMarqueeEnabled()) {
+                setHorizontalFadingEdgeEnabled(true);
+                mMarqueeFadeMode = MARQUEE_FADE_NORMAL;
+            } else {
+                setHorizontalFadingEdgeEnabled(false);
+                mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+            }
+            setEllipsize(TextUtils.TruncateAt.MARQUEE);
+        }
+
+        int n = mFilters.length;
+        for (int i = 0; i < n; i++) {
+            CharSequence out = mFilters[i].filter(text, 0, text.length(), EMPTY_SPANNED, 0, 0);
+            if (out != null) {
+                text = out;
+            }
+        }
+
+        if (notifyBefore) {
+            if (mText != null) {
+                oldlen = mText.length();
+                sendBeforeTextChanged(mText, 0, oldlen, text.length());
+            } else {
+                sendBeforeTextChanged("", 0, 0, text.length());
+            }
+        }
+
+        boolean needEditableForNotification = false;
+
+        if (mListeners != null && mListeners.size() != 0) {
+            needEditableForNotification = true;
+        }
+
+        if (type == BufferType.EDITABLE || getKeyListener() != null
+                || needEditableForNotification) {
+            createEditorIfNeeded();
+            mEditor.forgetUndoRedo();
+            Editable t = mEditableFactory.newEditable(text);
+            text = t;
+            setFilters(t, mFilters);
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) imm.restartInput(this);
+        } else if (type == BufferType.SPANNABLE || mMovement != null) {
+            text = mSpannableFactory.newSpannable(text);
+        } else if (!(text instanceof CharWrapper)) {
+            text = TextUtils.stringOrSpannedString(text);
+        }
+
+        if (mAutoLinkMask != 0) {
+            Spannable s2;
+
+            if (type == BufferType.EDITABLE || text instanceof Spannable) {
+                s2 = (Spannable) text;
+            } else {
+                s2 = mSpannableFactory.newSpannable(text);
+            }
+
+            if (Linkify.addLinks(s2, mAutoLinkMask)) {
+                text = s2;
+                type = (type == BufferType.EDITABLE) ? BufferType.EDITABLE : BufferType.SPANNABLE;
+
+                /*
+                 * We must go ahead and set the text before changing the
+                 * movement method, because setMovementMethod() may call
+                 * setText() again to try to upgrade the buffer type.
+                 */
+                mText = text;
+
+                // Do not change the movement method for text that support text selection as it
+                // would prevent an arbitrary cursor displacement.
+                if (mLinksClickable && !textCanBeSelected()) {
+                    setMovementMethod(LinkMovementMethod.getInstance());
+                }
+            }
+        }
+
+        mBufferType = type;
+        mText = text;
+
+        if (mTransformation == null) {
+            mTransformed = text;
+        } else {
+            mTransformed = mTransformation.getTransformation(text, this);
+        }
+
+        final int textLength = text.length();
+
+        if (text instanceof Spannable && !mAllowTransformationLengthChange) {
+            Spannable sp = (Spannable) text;
+
+            // Remove any ChangeWatchers that might have come from other TextViews.
+            final ChangeWatcher[] watchers = sp.getSpans(0, sp.length(), ChangeWatcher.class);
+            final int count = watchers.length;
+            for (int i = 0; i < count; i++) {
+                sp.removeSpan(watchers[i]);
+            }
+
+            if (mChangeWatcher == null) mChangeWatcher = new ChangeWatcher();
+
+            sp.setSpan(mChangeWatcher, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE
+                    | (CHANGE_WATCHER_PRIORITY << Spanned.SPAN_PRIORITY_SHIFT));
+
+            if (mEditor != null) mEditor.addSpanWatchers(sp);
+
+            if (mTransformation != null) {
+                sp.setSpan(mTransformation, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
+            }
+
+            if (mMovement != null) {
+                mMovement.initialize(this, (Spannable) text);
+
+                /*
+                 * Initializing the movement method will have set the
+                 * selection, so reset mSelectionMoved to keep that from
+                 * interfering with the normal on-focus selection-setting.
+                 */
+                if (mEditor != null) mEditor.mSelectionMoved = false;
+            }
+        }
+
+        if (mLayout != null) {
+            checkForRelayout();
+        }
+
+        sendOnTextChanged(text, 0, oldlen, textLength);
+        onTextChanged(text, 0, oldlen, textLength);
+
+        notifyViewAccessibilityStateChangedIfNeeded(AccessibilityEvent.CONTENT_CHANGE_TYPE_TEXT);
+
+        if (needEditableForNotification) {
+            sendAfterTextChanged((Editable) text);
+        } else {
+            // Always notify AutoFillManager - it will return right away if autofill is disabled.
+            notifyAutoFillManagerAfterTextChangedIfNeeded();
+        }
+
+        // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
+        if (mEditor != null) mEditor.prepareCursorControllers();
+    }
+
+    /**
+     * Sets the TextView to display the specified slice of the specified
+     * char array. You must promise that you will not change the contents
+     * of the array except for right before another call to setText(),
+     * since the TextView has no way to know that the text
+     * has changed and that it needs to invalidate and re-layout.
+     *
+     * @param text char array to be displayed
+     * @param start start index in the char array
+     * @param len length of char count after {@code start}
+     */
+    public final void setText(char[] text, int start, int len) {
+        int oldlen = 0;
+
+        if (start < 0 || len < 0 || start + len > text.length) {
+            throw new IndexOutOfBoundsException(start + ", " + len);
+        }
+
+        /*
+         * We must do the before-notification here ourselves because if
+         * the old text is a CharWrapper we destroy it before calling
+         * into the normal path.
+         */
+        if (mText != null) {
+            oldlen = mText.length();
+            sendBeforeTextChanged(mText, 0, oldlen, len);
+        } else {
+            sendBeforeTextChanged("", 0, 0, len);
+        }
+
+        if (mCharWrapper == null) {
+            mCharWrapper = new CharWrapper(text, start, len);
+        } else {
+            mCharWrapper.set(text, start, len);
+        }
+
+        setText(mCharWrapper, mBufferType, false, oldlen);
+    }
+
+    /**
+     * Sets the text to be displayed and the {@link android.widget.TextView.BufferType} but retains
+     * the cursor position. Same as
+     * {@link #setText(CharSequence, android.widget.TextView.BufferType)} except that the cursor
+     * position (if any) is retained in the new text.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param text text to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
+     *
+     * @see #setText(CharSequence, android.widget.TextView.BufferType)
+     */
+    public final void setTextKeepState(CharSequence text, BufferType type) {
+        int start = getSelectionStart();
+        int end = getSelectionEnd();
+        int len = text.length();
+
+        setText(text, type);
+
+        if (start >= 0 || end >= 0) {
+            if (mText instanceof Spannable) {
+                Selection.setSelection((Spannable) mText,
+                                       Math.max(0, Math.min(start, len)),
+                                       Math.max(0, Math.min(end, len)));
+            }
+        }
+    }
+
+    /**
+     * Sets the text to be displayed using a string resource identifier.
+     *
+     * @param resid the resource identifier of the string resource to be displayed
+     *
+     * @see #setText(CharSequence)
+     *
+     * @attr ref android.R.styleable#TextView_text
+     */
+    @android.view.RemotableViewMethod
+    public final void setText(@StringRes int resid) {
+        setText(getContext().getResources().getText(resid));
+        mTextFromResource = true;
+    }
+
+    /**
+     * Sets the text to be displayed using a string resource identifier and the
+     * {@link android.widget.TextView.BufferType}.
+     * <p/>
+     * When required, TextView will use {@link android.text.Spannable.Factory} to create final or
+     * intermediate {@link Spannable Spannables}. Likewise it will use
+     * {@link android.text.Editable.Factory} to create final or intermediate
+     * {@link Editable Editables}.
+     *
+     * @param resid the resource identifier of the string resource to be displayed
+     * @param type a {@link android.widget.TextView.BufferType} which defines whether the text is
+     *              stored as a static text, styleable/spannable text, or editable text
+     *
+     * @see #setText(int)
+     * @see #setText(CharSequence)
+     * @see android.widget.TextView.BufferType
+     * @see #setSpannableFactory(Spannable.Factory)
+     * @see #setEditableFactory(Editable.Factory)
+     *
+     * @attr ref android.R.styleable#TextView_text
+     * @attr ref android.R.styleable#TextView_bufferType
+     */
+    public final void setText(@StringRes int resid, BufferType type) {
+        setText(getContext().getResources().getText(resid), type);
+        mTextFromResource = true;
+    }
+
+    /**
+     * Sets the text to be displayed when the text of the TextView is empty.
+     * Null means to use the normal empty text. The hint does not currently
+     * participate in determining the size of the view.
+     *
+     * @attr ref android.R.styleable#TextView_hint
+     */
+    @android.view.RemotableViewMethod
+    public final void setHint(CharSequence hint) {
+        mHint = TextUtils.stringOrSpannedString(hint);
+
+        if (mLayout != null) {
+            checkForRelayout();
+        }
+
+        if (mText.length() == 0) {
+            invalidate();
+        }
+
+        // Invalidate display list if hint is currently used
+        if (mEditor != null && mText.length() == 0 && mHint != null) {
+            mEditor.invalidateTextDisplayList();
+        }
+    }
+
+    /**
+     * Sets the text to be displayed when the text of the TextView is empty,
+     * from a resource.
+     *
+     * @attr ref android.R.styleable#TextView_hint
+     */
+    @android.view.RemotableViewMethod
+    public final void setHint(@StringRes int resid) {
+        setHint(getContext().getResources().getText(resid));
+    }
+
+    /**
+     * Returns the hint that is displayed when the text of the TextView
+     * is empty.
+     *
+     * @attr ref android.R.styleable#TextView_hint
+     */
+    @ViewDebug.CapturedViewProperty
+    public CharSequence getHint() {
+        return mHint;
+    }
+
+    boolean isSingleLine() {
+        return mSingleLine;
+    }
+
+    private static boolean isMultilineInputType(int type) {
+        return (type & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE))
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE);
+    }
+
+    /**
+     * Removes the suggestion spans.
+     */
+    CharSequence removeSuggestionSpans(CharSequence text) {
+        if (text instanceof Spanned) {
+            Spannable spannable;
+            if (text instanceof Spannable) {
+                spannable = (Spannable) text;
+            } else {
+                spannable = mSpannableFactory.newSpannable(text);
+                text = spannable;
+            }
+
+            SuggestionSpan[] spans = spannable.getSpans(0, text.length(), SuggestionSpan.class);
+            for (int i = 0; i < spans.length; i++) {
+                spannable.removeSpan(spans[i]);
+            }
+        }
+        return text;
+    }
+
+    /**
+     * Set the type of the content with a constant as defined for {@link EditorInfo#inputType}. This
+     * will take care of changing the key listener, by calling {@link #setKeyListener(KeyListener)},
+     * to match the given content type.  If the given content type is {@link EditorInfo#TYPE_NULL}
+     * then a soft keyboard will not be displayed for this text view.
+     *
+     * Note that the maximum number of displayed lines (see {@link #setMaxLines(int)}) will be
+     * modified if you change the {@link EditorInfo#TYPE_TEXT_FLAG_MULTI_LINE} flag of the input
+     * type.
+     *
+     * @see #getInputType()
+     * @see #setRawInputType(int)
+     * @see android.text.InputType
+     * @attr ref android.R.styleable#TextView_inputType
+     */
+    public void setInputType(int type) {
+        final boolean wasPassword = isPasswordInputType(getInputType());
+        final boolean wasVisiblePassword = isVisiblePasswordInputType(getInputType());
+        setInputType(type, false);
+        final boolean isPassword = isPasswordInputType(type);
+        final boolean isVisiblePassword = isVisiblePasswordInputType(type);
+        boolean forceUpdate = false;
+        if (isPassword) {
+            setTransformationMethod(PasswordTransformationMethod.getInstance());
+            setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0);
+        } else if (isVisiblePassword) {
+            if (mTransformation == PasswordTransformationMethod.getInstance()) {
+                forceUpdate = true;
+            }
+            setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, MONOSPACE, 0);
+        } else if (wasPassword || wasVisiblePassword) {
+            // not in password mode, clean up typeface and transformation
+            setTypefaceFromAttrs(null/* fontTypeface */, null /* fontFamily */, -1, -1);
+            if (mTransformation == PasswordTransformationMethod.getInstance()) {
+                forceUpdate = true;
+            }
+        }
+
+        boolean singleLine = !isMultilineInputType(type);
+
+        // We need to update the single line mode if it has changed or we
+        // were previously in password mode.
+        if (mSingleLine != singleLine || forceUpdate) {
+            // Change single line mode, but only change the transformation if
+            // we are not in password mode.
+            applySingleLine(singleLine, !isPassword, true);
+        }
+
+        if (!isSuggestionsEnabled()) {
+            mText = removeSuggestionSpans(mText);
+        }
+
+        InputMethodManager imm = InputMethodManager.peekInstance();
+        if (imm != null) imm.restartInput(this);
+    }
+
+    /**
+     * It would be better to rely on the input type for everything. A password inputType should have
+     * a password transformation. We should hence use isPasswordInputType instead of this method.
+     *
+     * We should:
+     * - Call setInputType in setKeyListener instead of changing the input type directly (which
+     * would install the correct transformation).
+     * - Refuse the installation of a non-password transformation in setTransformation if the input
+     * type is password.
+     *
+     * However, this is like this for legacy reasons and we cannot break existing apps. This method
+     * is useful since it matches what the user can see (obfuscated text or not).
+     *
+     * @return true if the current transformation method is of the password type.
+     */
+    boolean hasPasswordTransformationMethod() {
+        return mTransformation instanceof PasswordTransformationMethod;
+    }
+
+    static boolean isPasswordInputType(int inputType) {
+        final int variation =
+                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+        return variation
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD)
+                || variation
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD)
+                || variation
+                == (EditorInfo.TYPE_CLASS_NUMBER | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD);
+    }
+
+    private static boolean isVisiblePasswordInputType(int inputType) {
+        final int variation =
+                inputType & (EditorInfo.TYPE_MASK_CLASS | EditorInfo.TYPE_MASK_VARIATION);
+        return variation
+                == (EditorInfo.TYPE_CLASS_TEXT | EditorInfo.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD);
+    }
+
+    /**
+     * Directly change the content type integer of the text view, without
+     * modifying any other state.
+     * @see #setInputType(int)
+     * @see android.text.InputType
+     * @attr ref android.R.styleable#TextView_inputType
+     */
+    public void setRawInputType(int type) {
+        if (type == InputType.TYPE_NULL && mEditor == null) return; //TYPE_NULL is the default value
+        createEditorIfNeeded();
+        mEditor.mInputType = type;
+    }
+
+    /**
+     * @return {@code null} if the key listener should use pre-O (locale-independent). Otherwise
+     *         a {@code Locale} object that can be used to customize key various listeners.
+     * @see DateKeyListener#getInstance(Locale)
+     * @see DateTimeKeyListener#getInstance(Locale)
+     * @see DigitsKeyListener#getInstance(Locale)
+     * @see TimeKeyListener#getInstance(Locale)
+     */
+    @Nullable
+    private Locale getCustomLocaleForKeyListenerOrNull() {
+        if (!mUseInternationalizedInput) {
+            // If the application does not target O, stick to the previous behavior.
+            return null;
+        }
+        final LocaleList locales = getImeHintLocales();
+        if (locales == null) {
+            // If the application does not explicitly specify IME hint locale, also stick to the
+            // previous behavior.
+            return null;
+        }
+        return locales.get(0);
+    }
+
+    private void setInputType(int type, boolean direct) {
+        final int cls = type & EditorInfo.TYPE_MASK_CLASS;
+        KeyListener input;
+        if (cls == EditorInfo.TYPE_CLASS_TEXT) {
+            boolean autotext = (type & EditorInfo.TYPE_TEXT_FLAG_AUTO_CORRECT) != 0;
+            TextKeyListener.Capitalize cap;
+            if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_CHARACTERS) != 0) {
+                cap = TextKeyListener.Capitalize.CHARACTERS;
+            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_WORDS) != 0) {
+                cap = TextKeyListener.Capitalize.WORDS;
+            } else if ((type & EditorInfo.TYPE_TEXT_FLAG_CAP_SENTENCES) != 0) {
+                cap = TextKeyListener.Capitalize.SENTENCES;
+            } else {
+                cap = TextKeyListener.Capitalize.NONE;
+            }
+            input = TextKeyListener.getInstance(autotext, cap);
+        } else if (cls == EditorInfo.TYPE_CLASS_NUMBER) {
+            final Locale locale = getCustomLocaleForKeyListenerOrNull();
+            input = DigitsKeyListener.getInstance(
+                    locale,
+                    (type & EditorInfo.TYPE_NUMBER_FLAG_SIGNED) != 0,
+                    (type & EditorInfo.TYPE_NUMBER_FLAG_DECIMAL) != 0);
+            if (locale != null) {
+                // Override type, if necessary for i18n.
+                int newType = input.getInputType();
+                final int newClass = newType & EditorInfo.TYPE_MASK_CLASS;
+                if (newClass != EditorInfo.TYPE_CLASS_NUMBER) {
+                    // The class is different from the original class. So we need to override
+                    // 'type'. But we want to keep the password flag if it's there.
+                    if ((type & EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD) != 0) {
+                        newType |= EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
+                    }
+                    type = newType;
+                }
+            }
+        } else if (cls == EditorInfo.TYPE_CLASS_DATETIME) {
+            final Locale locale = getCustomLocaleForKeyListenerOrNull();
+            switch (type & EditorInfo.TYPE_MASK_VARIATION) {
+                case EditorInfo.TYPE_DATETIME_VARIATION_DATE:
+                    input = DateKeyListener.getInstance(locale);
+                    break;
+                case EditorInfo.TYPE_DATETIME_VARIATION_TIME:
+                    input = TimeKeyListener.getInstance(locale);
+                    break;
+                default:
+                    input = DateTimeKeyListener.getInstance(locale);
+                    break;
+            }
+            if (mUseInternationalizedInput) {
+                type = input.getInputType(); // Override type, if necessary for i18n.
+            }
+        } else if (cls == EditorInfo.TYPE_CLASS_PHONE) {
+            input = DialerKeyListener.getInstance();
+        } else {
+            input = TextKeyListener.getInstance();
+        }
+        setRawInputType(type);
+        mListenerChanged = false;
+        if (direct) {
+            createEditorIfNeeded();
+            mEditor.mKeyListener = input;
+        } else {
+            setKeyListenerOnly(input);
+        }
+    }
+
+    /**
+     * Get the type of the editable content.
+     *
+     * @see #setInputType(int)
+     * @see android.text.InputType
+     */
+    public int getInputType() {
+        return mEditor == null ? EditorInfo.TYPE_NULL : mEditor.mInputType;
+    }
+
+    /**
+     * Change the editor type integer associated with the text view, which
+     * is reported to an Input Method Editor (IME) with {@link EditorInfo#imeOptions}
+     * when it has focus.
+     * @see #getImeOptions
+     * @see android.view.inputmethod.EditorInfo
+     * @attr ref android.R.styleable#TextView_imeOptions
+     */
+    public void setImeOptions(int imeOptions) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.imeOptions = imeOptions;
+    }
+
+    /**
+     * Get the type of the Input Method Editor (IME).
+     * @return the type of the IME
+     * @see #setImeOptions(int)
+     * @see android.view.inputmethod.EditorInfo
+     */
+    public int getImeOptions() {
+        return mEditor != null && mEditor.mInputContentType != null
+                ? mEditor.mInputContentType.imeOptions : EditorInfo.IME_NULL;
+    }
+
+    /**
+     * Change the custom IME action associated with the text view, which
+     * will be reported to an IME with {@link EditorInfo#actionLabel}
+     * and {@link EditorInfo#actionId} when it has focus.
+     * @see #getImeActionLabel
+     * @see #getImeActionId
+     * @see android.view.inputmethod.EditorInfo
+     * @attr ref android.R.styleable#TextView_imeActionLabel
+     * @attr ref android.R.styleable#TextView_imeActionId
+     */
+    public void setImeActionLabel(CharSequence label, int actionId) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.imeActionLabel = label;
+        mEditor.mInputContentType.imeActionId = actionId;
+    }
+
+    /**
+     * Get the IME action label previous set with {@link #setImeActionLabel}.
+     *
+     * @see #setImeActionLabel
+     * @see android.view.inputmethod.EditorInfo
+     */
+    public CharSequence getImeActionLabel() {
+        return mEditor != null && mEditor.mInputContentType != null
+                ? mEditor.mInputContentType.imeActionLabel : null;
+    }
+
+    /**
+     * Get the IME action ID previous set with {@link #setImeActionLabel}.
+     *
+     * @see #setImeActionLabel
+     * @see android.view.inputmethod.EditorInfo
+     */
+    public int getImeActionId() {
+        return mEditor != null && mEditor.mInputContentType != null
+                ? mEditor.mInputContentType.imeActionId : 0;
+    }
+
+    /**
+     * Set a special listener to be called when an action is performed
+     * on the text view.  This will be called when the enter key is pressed,
+     * or when an action supplied to the IME is selected by the user.  Setting
+     * this means that the normal hard key event will not insert a newline
+     * into the text view, even if it is multi-line; holding down the ALT
+     * modifier will, however, allow the user to insert a newline character.
+     */
+    public void setOnEditorActionListener(OnEditorActionListener l) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.onEditorActionListener = l;
+    }
+
+    /**
+     * Called when an attached input method calls
+     * {@link InputConnection#performEditorAction(int)
+     * InputConnection.performEditorAction()}
+     * for this text view.  The default implementation will call your action
+     * listener supplied to {@link #setOnEditorActionListener}, or perform
+     * a standard operation for {@link EditorInfo#IME_ACTION_NEXT
+     * EditorInfo.IME_ACTION_NEXT}, {@link EditorInfo#IME_ACTION_PREVIOUS
+     * EditorInfo.IME_ACTION_PREVIOUS}, or {@link EditorInfo#IME_ACTION_DONE
+     * EditorInfo.IME_ACTION_DONE}.
+     *
+     * <p>For backwards compatibility, if no IME options have been set and the
+     * text view would not normally advance focus on enter, then
+     * the NEXT and DONE actions received here will be turned into an enter
+     * key down/up pair to go through the normal key handling.
+     *
+     * @param actionCode The code of the action being performed.
+     *
+     * @see #setOnEditorActionListener
+     */
+    public void onEditorAction(int actionCode) {
+        final Editor.InputContentType ict = mEditor == null ? null : mEditor.mInputContentType;
+        if (ict != null) {
+            if (ict.onEditorActionListener != null) {
+                if (ict.onEditorActionListener.onEditorAction(this,
+                        actionCode, null)) {
+                    return;
+                }
+            }
+
+            // This is the handling for some default action.
+            // Note that for backwards compatibility we don't do this
+            // default handling if explicit ime options have not been given,
+            // instead turning this into the normal enter key codes that an
+            // app may be expecting.
+            if (actionCode == EditorInfo.IME_ACTION_NEXT) {
+                View v = focusSearch(FOCUS_FORWARD);
+                if (v != null) {
+                    if (!v.requestFocus(FOCUS_FORWARD)) {
+                        throw new IllegalStateException("focus search returned a view "
+                                + "that wasn't able to take focus!");
+                    }
+                }
+                return;
+
+            } else if (actionCode == EditorInfo.IME_ACTION_PREVIOUS) {
+                View v = focusSearch(FOCUS_BACKWARD);
+                if (v != null) {
+                    if (!v.requestFocus(FOCUS_BACKWARD)) {
+                        throw new IllegalStateException("focus search returned a view "
+                                + "that wasn't able to take focus!");
+                    }
+                }
+                return;
+
+            } else if (actionCode == EditorInfo.IME_ACTION_DONE) {
+                InputMethodManager imm = InputMethodManager.peekInstance();
+                if (imm != null && imm.isActive(this)) {
+                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                }
+                return;
+            }
+        }
+
+        ViewRootImpl viewRootImpl = getViewRootImpl();
+        if (viewRootImpl != null) {
+            long eventTime = SystemClock.uptimeMillis();
+            viewRootImpl.dispatchKeyFromIme(
+                    new KeyEvent(eventTime, eventTime,
+                    KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_ENTER, 0, 0,
+                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+                    | KeyEvent.FLAG_EDITOR_ACTION));
+            viewRootImpl.dispatchKeyFromIme(
+                    new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+                    KeyEvent.ACTION_UP, KeyEvent.KEYCODE_ENTER, 0, 0,
+                    KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+                    KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE
+                    | KeyEvent.FLAG_EDITOR_ACTION));
+        }
+    }
+
+    /**
+     * Set the private content type of the text, which is the
+     * {@link EditorInfo#privateImeOptions EditorInfo.privateImeOptions}
+     * field that will be filled in when creating an input connection.
+     *
+     * @see #getPrivateImeOptions()
+     * @see EditorInfo#privateImeOptions
+     * @attr ref android.R.styleable#TextView_privateImeOptions
+     */
+    public void setPrivateImeOptions(String type) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.privateImeOptions = type;
+    }
+
+    /**
+     * Get the private type of the content.
+     *
+     * @see #setPrivateImeOptions(String)
+     * @see EditorInfo#privateImeOptions
+     */
+    public String getPrivateImeOptions() {
+        return mEditor != null && mEditor.mInputContentType != null
+                ? mEditor.mInputContentType.privateImeOptions : null;
+    }
+
+    /**
+     * Set the extra input data of the text, which is the
+     * {@link EditorInfo#extras TextBoxAttribute.extras}
+     * Bundle that will be filled in when creating an input connection.  The
+     * given integer is the resource identifier of an XML resource holding an
+     * {@link android.R.styleable#InputExtras &lt;input-extras&gt;} XML tree.
+     *
+     * @see #getInputExtras(boolean)
+     * @see EditorInfo#extras
+     * @attr ref android.R.styleable#TextView_editorExtras
+     */
+    public void setInputExtras(@XmlRes int xmlResId) throws XmlPullParserException, IOException {
+        createEditorIfNeeded();
+        XmlResourceParser parser = getResources().getXml(xmlResId);
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.extras = new Bundle();
+        getResources().parseBundleExtras(parser, mEditor.mInputContentType.extras);
+    }
+
+    /**
+     * Retrieve the input extras currently associated with the text view, which
+     * can be viewed as well as modified.
+     *
+     * @param create If true, the extras will be created if they don't already
+     * exist.  Otherwise, null will be returned if none have been created.
+     * @see #setInputExtras(int)
+     * @see EditorInfo#extras
+     * @attr ref android.R.styleable#TextView_editorExtras
+     */
+    public Bundle getInputExtras(boolean create) {
+        if (mEditor == null && !create) return null;
+        createEditorIfNeeded();
+        if (mEditor.mInputContentType == null) {
+            if (!create) return null;
+            mEditor.createInputContentTypeIfNeeded();
+        }
+        if (mEditor.mInputContentType.extras == null) {
+            if (!create) return null;
+            mEditor.mInputContentType.extras = new Bundle();
+        }
+        return mEditor.mInputContentType.extras;
+    }
+
+    /**
+     * Change "hint" locales associated with the text view, which will be reported to an IME with
+     * {@link EditorInfo#hintLocales} when it has focus.
+     *
+     * Starting with Android O, this also causes internationalized listeners to be created (or
+     * change locale) based on the first locale in the input locale list.
+     *
+     * <p><strong>Note:</strong> If you want new "hint" to take effect immediately you need to
+     * call {@link InputMethodManager#restartInput(View)}.</p>
+     * @param hintLocales List of the languages that the user is supposed to switch to no matter
+     * what input method subtype is currently used. Set {@code null} to clear the current "hint".
+     * @see #getImeHintLocales()
+     * @see android.view.inputmethod.EditorInfo#hintLocales
+     */
+    public void setImeHintLocales(@Nullable LocaleList hintLocales) {
+        createEditorIfNeeded();
+        mEditor.createInputContentTypeIfNeeded();
+        mEditor.mInputContentType.imeHintLocales = hintLocales;
+        if (mUseInternationalizedInput) {
+            changeListenerLocaleTo(hintLocales == null ? null : hintLocales.get(0));
+        }
+    }
+
+    /**
+     * @return The current languages list "hint". {@code null} when no "hint" is available.
+     * @see #setImeHintLocales(LocaleList)
+     * @see android.view.inputmethod.EditorInfo#hintLocales
+     */
+    @Nullable
+    public LocaleList getImeHintLocales() {
+        if (mEditor == null) {
+            return null;
+        }
+        if (mEditor.mInputContentType == null) {
+            return null;
+        }
+        return mEditor.mInputContentType.imeHintLocales;
+    }
+
+    /**
+     * Returns the error message that was set to be displayed with
+     * {@link #setError}, or <code>null</code> if no error was set
+     * or if it the error was cleared by the widget after user input.
+     */
+    public CharSequence getError() {
+        return mEditor == null ? null : mEditor.mError;
+    }
+
+    /**
+     * Sets the right-hand compound drawable of the TextView to the "error"
+     * icon and sets an error message that will be displayed in a popup when
+     * the TextView has focus.  The icon and error message will be reset to
+     * null when any key events cause changes to the TextView's text.  If the
+     * <code>error</code> is <code>null</code>, the error message and icon
+     * will be cleared.
+     */
+    @android.view.RemotableViewMethod
+    public void setError(CharSequence error) {
+        if (error == null) {
+            setError(null, null);
+        } else {
+            Drawable dr = getContext().getDrawable(
+                    com.android.internal.R.drawable.indicator_input_error);
+
+            dr.setBounds(0, 0, dr.getIntrinsicWidth(), dr.getIntrinsicHeight());
+            setError(error, dr);
+        }
+    }
+
+    /**
+     * Sets the right-hand compound drawable of the TextView to the specified
+     * icon and sets an error message that will be displayed in a popup when
+     * the TextView has focus.  The icon and error message will be reset to
+     * null when any key events cause changes to the TextView's text.  The
+     * drawable must already have had {@link Drawable#setBounds} set on it.
+     * If the <code>error</code> is <code>null</code>, the error message will
+     * be cleared (and you should provide a <code>null</code> icon as well).
+     */
+    public void setError(CharSequence error, Drawable icon) {
+        createEditorIfNeeded();
+        mEditor.setError(error, icon);
+        notifyViewAccessibilityStateChangedIfNeeded(
+                AccessibilityEvent.CONTENT_CHANGE_TYPE_UNDEFINED);
+    }
+
+    @Override
+    protected boolean setFrame(int l, int t, int r, int b) {
+        boolean result = super.setFrame(l, t, r, b);
+
+        if (mEditor != null) mEditor.setFrame();
+
+        restartMarqueeIfNeeded();
+
+        return result;
+    }
+
+    private void restartMarqueeIfNeeded() {
+        if (mRestartMarquee && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+            mRestartMarquee = false;
+            startMarquee();
+        }
+    }
+
+    /**
+     * Sets the list of input filters that will be used if the buffer is
+     * Editable. Has no effect otherwise.
+     *
+     * @attr ref android.R.styleable#TextView_maxLength
+     */
+    public void setFilters(InputFilter[] filters) {
+        if (filters == null) {
+            throw new IllegalArgumentException();
+        }
+
+        mFilters = filters;
+
+        if (mText instanceof Editable) {
+            setFilters((Editable) mText, filters);
+        }
+    }
+
+    /**
+     * Sets the list of input filters on the specified Editable,
+     * and includes mInput in the list if it is an InputFilter.
+     */
+    private void setFilters(Editable e, InputFilter[] filters) {
+        if (mEditor != null) {
+            final boolean undoFilter = mEditor.mUndoInputFilter != null;
+            final boolean keyFilter = mEditor.mKeyListener instanceof InputFilter;
+            int num = 0;
+            if (undoFilter) num++;
+            if (keyFilter) num++;
+            if (num > 0) {
+                InputFilter[] nf = new InputFilter[filters.length + num];
+
+                System.arraycopy(filters, 0, nf, 0, filters.length);
+                num = 0;
+                if (undoFilter) {
+                    nf[filters.length] = mEditor.mUndoInputFilter;
+                    num++;
+                }
+                if (keyFilter) {
+                    nf[filters.length + num] = (InputFilter) mEditor.mKeyListener;
+                }
+
+                e.setFilters(nf);
+                return;
+            }
+        }
+        e.setFilters(filters);
+    }
+
+    /**
+     * Returns the current list of input filters.
+     *
+     * @attr ref android.R.styleable#TextView_maxLength
+     */
+    public InputFilter[] getFilters() {
+        return mFilters;
+    }
+
+    /////////////////////////////////////////////////////////////////////////
+
+    private int getBoxHeight(Layout l) {
+        Insets opticalInsets = isLayoutModeOptical(mParent) ? getOpticalInsets() : Insets.NONE;
+        int padding = (l == mHintLayout)
+                ? getCompoundPaddingTop() + getCompoundPaddingBottom()
+                : getExtendedPaddingTop() + getExtendedPaddingBottom();
+        return getMeasuredHeight() - padding + opticalInsets.top + opticalInsets.bottom;
+    }
+
+    int getVerticalOffset(boolean forceNormal) {
+        int voffset = 0;
+        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+        Layout l = mLayout;
+        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+            l = mHintLayout;
+        }
+
+        if (gravity != Gravity.TOP) {
+            int boxht = getBoxHeight(l);
+            int textht = l.getHeight();
+
+            if (textht < boxht) {
+                if (gravity == Gravity.BOTTOM) {
+                    voffset = boxht - textht;
+                } else { // (gravity == Gravity.CENTER_VERTICAL)
+                    voffset = (boxht - textht) >> 1;
+                }
+            }
+        }
+        return voffset;
+    }
+
+    private int getBottomVerticalOffset(boolean forceNormal) {
+        int voffset = 0;
+        final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+
+        Layout l = mLayout;
+        if (!forceNormal && mText.length() == 0 && mHintLayout != null) {
+            l = mHintLayout;
+        }
+
+        if (gravity != Gravity.BOTTOM) {
+            int boxht = getBoxHeight(l);
+            int textht = l.getHeight();
+
+            if (textht < boxht) {
+                if (gravity == Gravity.TOP) {
+                    voffset = boxht - textht;
+                } else { // (gravity == Gravity.CENTER_VERTICAL)
+                    voffset = (boxht - textht) >> 1;
+                }
+            }
+        }
+        return voffset;
+    }
+
+    void invalidateCursorPath() {
+        if (mHighlightPathBogus) {
+            invalidateCursor();
+        } else {
+            final int horizontalPadding = getCompoundPaddingLeft();
+            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
+
+            if (mEditor.mCursorDrawable == null) {
+                synchronized (TEMP_RECTF) {
+                    /*
+                     * The reason for this concern about the thickness of the
+                     * cursor and doing the floor/ceil on the coordinates is that
+                     * some EditTexts (notably textfields in the Browser) have
+                     * anti-aliased text where not all the characters are
+                     * necessarily at integer-multiple locations.  This should
+                     * make sure the entire cursor gets invalidated instead of
+                     * sometimes missing half a pixel.
+                     */
+                    float thick = (float) Math.ceil(mTextPaint.getStrokeWidth());
+                    if (thick < 1.0f) {
+                        thick = 1.0f;
+                    }
+
+                    thick /= 2.0f;
+
+                    // mHighlightPath is guaranteed to be non null at that point.
+                    mHighlightPath.computeBounds(TEMP_RECTF, false);
+
+                    invalidate((int) Math.floor(horizontalPadding + TEMP_RECTF.left - thick),
+                            (int) Math.floor(verticalPadding + TEMP_RECTF.top - thick),
+                            (int) Math.ceil(horizontalPadding + TEMP_RECTF.right + thick),
+                            (int) Math.ceil(verticalPadding + TEMP_RECTF.bottom + thick));
+                }
+            } else {
+                final Rect bounds = mEditor.mCursorDrawable.getBounds();
+                invalidate(bounds.left + horizontalPadding, bounds.top + verticalPadding,
+                        bounds.right + horizontalPadding, bounds.bottom + verticalPadding);
+            }
+        }
+    }
+
+    void invalidateCursor() {
+        int where = getSelectionEnd();
+
+        invalidateCursor(where, where, where);
+    }
+
+    private void invalidateCursor(int a, int b, int c) {
+        if (a >= 0 || b >= 0 || c >= 0) {
+            int start = Math.min(Math.min(a, b), c);
+            int end = Math.max(Math.max(a, b), c);
+            invalidateRegion(start, end, true /* Also invalidates blinking cursor */);
+        }
+    }
+
+    /**
+     * Invalidates the region of text enclosed between the start and end text offsets.
+     */
+    void invalidateRegion(int start, int end, boolean invalidateCursor) {
+        if (mLayout == null) {
+            invalidate();
+        } else {
+            int lineStart = mLayout.getLineForOffset(start);
+            int top = mLayout.getLineTop(lineStart);
+
+            // This is ridiculous, but the descent from the line above
+            // can hang down into the line we really want to redraw,
+            // so we have to invalidate part of the line above to make
+            // sure everything that needs to be redrawn really is.
+            // (But not the whole line above, because that would cause
+            // the same problem with the descenders on the line above it!)
+            if (lineStart > 0) {
+                top -= mLayout.getLineDescent(lineStart - 1);
+            }
+
+            int lineEnd;
+
+            if (start == end) {
+                lineEnd = lineStart;
+            } else {
+                lineEnd = mLayout.getLineForOffset(end);
+            }
+
+            int bottom = mLayout.getLineBottom(lineEnd);
+
+            // mEditor can be null in case selection is set programmatically.
+            if (invalidateCursor && mEditor != null && mEditor.mCursorDrawable != null) {
+                final Rect bounds = mEditor.mCursorDrawable.getBounds();
+                top = Math.min(top, bounds.top);
+                bottom = Math.max(bottom, bounds.bottom);
+            }
+
+            final int compoundPaddingLeft = getCompoundPaddingLeft();
+            final int verticalPadding = getExtendedPaddingTop() + getVerticalOffset(true);
+
+            int left, right;
+            if (lineStart == lineEnd && !invalidateCursor) {
+                left = (int) mLayout.getPrimaryHorizontal(start);
+                right = (int) (mLayout.getPrimaryHorizontal(end) + 1.0);
+                left += compoundPaddingLeft;
+                right += compoundPaddingLeft;
+            } else {
+                // Rectangle bounding box when the region spans several lines
+                left = compoundPaddingLeft;
+                right = getWidth() - getCompoundPaddingRight();
+            }
+
+            invalidate(mScrollX + left, verticalPadding + top,
+                    mScrollX + right, verticalPadding + bottom);
+        }
+    }
+
+    private void registerForPreDraw() {
+        if (!mPreDrawRegistered) {
+            getViewTreeObserver().addOnPreDrawListener(this);
+            mPreDrawRegistered = true;
+        }
+    }
+
+    private void unregisterForPreDraw() {
+        getViewTreeObserver().removeOnPreDrawListener(this);
+        mPreDrawRegistered = false;
+        mPreDrawListenerDetached = false;
+    }
+
+    /**
+     * {@inheritDoc}
+     */
+    @Override
+    public boolean onPreDraw() {
+        if (mLayout == null) {
+            assumeLayout();
+        }
+
+        if (mMovement != null) {
+            /* This code also provides auto-scrolling when a cursor is moved using a
+             * CursorController (insertion point or selection limits).
+             * For selection, ensure start or end is visible depending on controller's state.
+             */
+            int curs = getSelectionEnd();
+            // Do not create the controller if it is not already created.
+            if (mEditor != null && mEditor.mSelectionModifierCursorController != null
+                    && mEditor.mSelectionModifierCursorController.isSelectionStartDragged()) {
+                curs = getSelectionStart();
+            }
+
+            /*
+             * TODO: This should really only keep the end in view if
+             * it already was before the text changed.  I'm not sure
+             * of a good way to tell from here if it was.
+             */
+            if (curs < 0 && (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+                curs = mText.length();
+            }
+
+            if (curs >= 0) {
+                bringPointIntoView(curs);
+            }
+        } else {
+            bringTextIntoView();
+        }
+
+        // This has to be checked here since:
+        // - onFocusChanged cannot start it when focus is given to a view with selected text (after
+        //   a screen rotation) since layout is not yet initialized at that point.
+        if (mEditor != null && mEditor.mCreatedWithASelection) {
+            mEditor.refreshTextActionMode();
+            mEditor.mCreatedWithASelection = false;
+        }
+
+        unregisterForPreDraw();
+
+        return true;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (mEditor != null) mEditor.onAttachedToWindow();
+
+        if (mPreDrawListenerDetached) {
+            getViewTreeObserver().addOnPreDrawListener(this);
+            mPreDrawListenerDetached = false;
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void onDetachedFromWindowInternal() {
+        if (mPreDrawRegistered) {
+            getViewTreeObserver().removeOnPreDrawListener(this);
+            mPreDrawListenerDetached = true;
+        }
+
+        resetResolvedDrawables();
+
+        if (mEditor != null) mEditor.onDetachedFromWindow();
+
+        super.onDetachedFromWindowInternal();
+    }
+
+    @Override
+    public void onScreenStateChanged(int screenState) {
+        super.onScreenStateChanged(screenState);
+        if (mEditor != null) mEditor.onScreenStateChanged(screenState);
+    }
+
+    @Override
+    protected boolean isPaddingOffsetRequired() {
+        return mShadowRadius != 0 || mDrawables != null;
+    }
+
+    @Override
+    protected int getLeftPaddingOffset() {
+        return getCompoundPaddingLeft() - mPaddingLeft
+                + (int) Math.min(0, mShadowDx - mShadowRadius);
+    }
+
+    @Override
+    protected int getTopPaddingOffset() {
+        return (int) Math.min(0, mShadowDy - mShadowRadius);
+    }
+
+    @Override
+    protected int getBottomPaddingOffset() {
+        return (int) Math.max(0, mShadowDy + mShadowRadius);
+    }
+
+    @Override
+    protected int getRightPaddingOffset() {
+        return -(getCompoundPaddingRight() - mPaddingRight)
+                + (int) Math.max(0, mShadowDx + mShadowRadius);
+    }
+
+    @Override
+    protected boolean verifyDrawable(@NonNull Drawable who) {
+        final boolean verified = super.verifyDrawable(who);
+        if (!verified && mDrawables != null) {
+            for (Drawable dr : mDrawables.mShowing) {
+                if (who == dr) {
+                    return true;
+                }
+            }
+        }
+        return verified;
+    }
+
+    @Override
+    public void jumpDrawablesToCurrentState() {
+        super.jumpDrawablesToCurrentState();
+        if (mDrawables != null) {
+            for (Drawable dr : mDrawables.mShowing) {
+                if (dr != null) {
+                    dr.jumpToCurrentState();
+                }
+            }
+        }
+    }
+
+    @Override
+    public void invalidateDrawable(@NonNull Drawable drawable) {
+        boolean handled = false;
+
+        if (verifyDrawable(drawable)) {
+            final Rect dirty = drawable.getBounds();
+            int scrollX = mScrollX;
+            int scrollY = mScrollY;
+
+            // IMPORTANT: The coordinates below are based on the coordinates computed
+            // for each compound drawable in onDraw(). Make sure to update each section
+            // accordingly.
+            final TextView.Drawables drawables = mDrawables;
+            if (drawables != null) {
+                if (drawable == drawables.mShowing[Drawables.LEFT]) {
+                    final int compoundPaddingTop = getCompoundPaddingTop();
+                    final int compoundPaddingBottom = getCompoundPaddingBottom();
+                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+
+                    scrollX += mPaddingLeft;
+                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightLeft) / 2;
+                    handled = true;
+                } else if (drawable == drawables.mShowing[Drawables.RIGHT]) {
+                    final int compoundPaddingTop = getCompoundPaddingTop();
+                    final int compoundPaddingBottom = getCompoundPaddingBottom();
+                    final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+
+                    scrollX += (mRight - mLeft - mPaddingRight - drawables.mDrawableSizeRight);
+                    scrollY += compoundPaddingTop + (vspace - drawables.mDrawableHeightRight) / 2;
+                    handled = true;
+                } else if (drawable == drawables.mShowing[Drawables.TOP]) {
+                    final int compoundPaddingLeft = getCompoundPaddingLeft();
+                    final int compoundPaddingRight = getCompoundPaddingRight();
+                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
+
+                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthTop) / 2;
+                    scrollY += mPaddingTop;
+                    handled = true;
+                } else if (drawable == drawables.mShowing[Drawables.BOTTOM]) {
+                    final int compoundPaddingLeft = getCompoundPaddingLeft();
+                    final int compoundPaddingRight = getCompoundPaddingRight();
+                    final int hspace = mRight - mLeft - compoundPaddingRight - compoundPaddingLeft;
+
+                    scrollX += compoundPaddingLeft + (hspace - drawables.mDrawableWidthBottom) / 2;
+                    scrollY += (mBottom - mTop - mPaddingBottom - drawables.mDrawableSizeBottom);
+                    handled = true;
+                }
+            }
+
+            if (handled) {
+                invalidate(dirty.left + scrollX, dirty.top + scrollY,
+                        dirty.right + scrollX, dirty.bottom + scrollY);
+            }
+        }
+
+        if (!handled) {
+            super.invalidateDrawable(drawable);
+        }
+    }
+
+    @Override
+    public boolean hasOverlappingRendering() {
+        // horizontal fading edge causes SaveLayerAlpha, which doesn't support alpha modulation
+        return ((getBackground() != null && getBackground().getCurrent() != null)
+                || mText instanceof Spannable || hasSelection()
+                || isHorizontalFadingEdgeEnabled());
+    }
+
+    /**
+     *
+     * Returns the state of the {@code textIsSelectable} flag (See
+     * {@link #setTextIsSelectable setTextIsSelectable()}). Although you have to set this flag
+     * to allow users to select and copy text in a non-editable TextView, the content of an
+     * {@link EditText} can always be selected, independently of the value of this flag.
+     * <p>
+     *
+     * @return True if the text displayed in this TextView can be selected by the user.
+     *
+     * @attr ref android.R.styleable#TextView_textIsSelectable
+     */
+    public boolean isTextSelectable() {
+        return mEditor == null ? false : mEditor.mTextIsSelectable;
+    }
+
+    /**
+     * Sets whether the content of this view is selectable by the user. The default is
+     * {@code false}, meaning that the content is not selectable.
+     * <p>
+     * When you use a TextView to display a useful piece of information to the user (such as a
+     * contact's address), make it selectable, so that the user can select and copy its
+     * content. You can also use set the XML attribute
+     * {@link android.R.styleable#TextView_textIsSelectable} to "true".
+     * <p>
+     * When you call this method to set the value of {@code textIsSelectable}, it sets
+     * the flags {@code focusable}, {@code focusableInTouchMode}, {@code clickable},
+     * and {@code longClickable} to the same value. These flags correspond to the attributes
+     * {@link android.R.styleable#View_focusable android:focusable},
+     * {@link android.R.styleable#View_focusableInTouchMode android:focusableInTouchMode},
+     * {@link android.R.styleable#View_clickable android:clickable}, and
+     * {@link android.R.styleable#View_longClickable android:longClickable}. To restore any of these
+     * flags to a state you had set previously, call one or more of the following methods:
+     * {@link #setFocusable(boolean) setFocusable()},
+     * {@link #setFocusableInTouchMode(boolean) setFocusableInTouchMode()},
+     * {@link #setClickable(boolean) setClickable()} or
+     * {@link #setLongClickable(boolean) setLongClickable()}.
+     *
+     * @param selectable Whether the content of this TextView should be selectable.
+     */
+    public void setTextIsSelectable(boolean selectable) {
+        if (!selectable && mEditor == null) return; // false is default value with no edit data
+
+        createEditorIfNeeded();
+        if (mEditor.mTextIsSelectable == selectable) return;
+
+        mEditor.mTextIsSelectable = selectable;
+        setFocusableInTouchMode(selectable);
+        setFocusable(FOCUSABLE_AUTO);
+        setClickable(selectable);
+        setLongClickable(selectable);
+
+        // mInputType should already be EditorInfo.TYPE_NULL and mInput should be null
+
+        setMovementMethod(selectable ? ArrowKeyMovementMethod.getInstance() : null);
+        setText(mText, selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
+
+        // Called by setText above, but safer in case of future code changes
+        mEditor.prepareCursorControllers();
+    }
+
+    @Override
+    protected int[] onCreateDrawableState(int extraSpace) {
+        final int[] drawableState;
+
+        if (mSingleLine) {
+            drawableState = super.onCreateDrawableState(extraSpace);
+        } else {
+            drawableState = super.onCreateDrawableState(extraSpace + 1);
+            mergeDrawableStates(drawableState, MULTILINE_STATE_SET);
+        }
+
+        if (isTextSelectable()) {
+            // Disable pressed state, which was introduced when TextView was made clickable.
+            // Prevents text color change.
+            // setClickable(false) would have a similar effect, but it also disables focus changes
+            // and long press actions, which are both needed by text selection.
+            final int length = drawableState.length;
+            for (int i = 0; i < length; i++) {
+                if (drawableState[i] == R.attr.state_pressed) {
+                    final int[] nonPressedState = new int[length - 1];
+                    System.arraycopy(drawableState, 0, nonPressedState, 0, i);
+                    System.arraycopy(drawableState, i + 1, nonPressedState, i, length - i - 1);
+                    return nonPressedState;
+                }
+            }
+        }
+
+        return drawableState;
+    }
+
+    private Path getUpdatedHighlightPath() {
+        Path highlight = null;
+        Paint highlightPaint = mHighlightPaint;
+
+        final int selStart = getSelectionStart();
+        final int selEnd = getSelectionEnd();
+        if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
+            if (selStart == selEnd) {
+                if (mEditor != null && mEditor.isCursorVisible()
+                        && (SystemClock.uptimeMillis() - mEditor.mShowCursor)
+                        % (2 * Editor.BLINK) < Editor.BLINK) {
+                    if (mHighlightPathBogus) {
+                        if (mHighlightPath == null) mHighlightPath = new Path();
+                        mHighlightPath.reset();
+                        mLayout.getCursorPath(selStart, mHighlightPath, mText);
+                        mEditor.updateCursorPosition();
+                        mHighlightPathBogus = false;
+                    }
+
+                    // XXX should pass to skin instead of drawing directly
+                    highlightPaint.setColor(mCurTextColor);
+                    highlightPaint.setStyle(Paint.Style.STROKE);
+                    highlight = mHighlightPath;
+                }
+            } else {
+                if (mHighlightPathBogus) {
+                    if (mHighlightPath == null) mHighlightPath = new Path();
+                    mHighlightPath.reset();
+                    mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
+                    mHighlightPathBogus = false;
+                }
+
+                // XXX should pass to skin instead of drawing directly
+                highlightPaint.setColor(mHighlightColor);
+                highlightPaint.setStyle(Paint.Style.FILL);
+
+                highlight = mHighlightPath;
+            }
+        }
+        return highlight;
+    }
+
+    /**
+     * @hide
+     */
+    public int getHorizontalOffsetForDrawables() {
+        return 0;
+    }
+
+    @Override
+    protected void onDraw(Canvas canvas) {
+        restartMarqueeIfNeeded();
+
+        // Draw the background for this view
+        super.onDraw(canvas);
+
+        final int compoundPaddingLeft = getCompoundPaddingLeft();
+        final int compoundPaddingTop = getCompoundPaddingTop();
+        final int compoundPaddingRight = getCompoundPaddingRight();
+        final int compoundPaddingBottom = getCompoundPaddingBottom();
+        final int scrollX = mScrollX;
+        final int scrollY = mScrollY;
+        final int right = mRight;
+        final int left = mLeft;
+        final int bottom = mBottom;
+        final int top = mTop;
+        final boolean isLayoutRtl = isLayoutRtl();
+        final int offset = getHorizontalOffsetForDrawables();
+        final int leftOffset = isLayoutRtl ? 0 : offset;
+        final int rightOffset = isLayoutRtl ? offset : 0;
+
+        final Drawables dr = mDrawables;
+        if (dr != null) {
+            /*
+             * Compound, not extended, because the icon is not clipped
+             * if the text height is smaller.
+             */
+
+            int vspace = bottom - top - compoundPaddingBottom - compoundPaddingTop;
+            int hspace = right - left - compoundPaddingRight - compoundPaddingLeft;
+
+            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+            // Make sure to update invalidateDrawable() when changing this code.
+            if (dr.mShowing[Drawables.LEFT] != null) {
+                canvas.save();
+                canvas.translate(scrollX + mPaddingLeft + leftOffset,
+                        scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightLeft) / 2);
+                dr.mShowing[Drawables.LEFT].draw(canvas);
+                canvas.restore();
+            }
+
+            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+            // Make sure to update invalidateDrawable() when changing this code.
+            if (dr.mShowing[Drawables.RIGHT] != null) {
+                canvas.save();
+                canvas.translate(scrollX + right - left - mPaddingRight
+                        - dr.mDrawableSizeRight - rightOffset,
+                         scrollY + compoundPaddingTop + (vspace - dr.mDrawableHeightRight) / 2);
+                dr.mShowing[Drawables.RIGHT].draw(canvas);
+                canvas.restore();
+            }
+
+            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+            // Make sure to update invalidateDrawable() when changing this code.
+            if (dr.mShowing[Drawables.TOP] != null) {
+                canvas.save();
+                canvas.translate(scrollX + compoundPaddingLeft
+                        + (hspace - dr.mDrawableWidthTop) / 2, scrollY + mPaddingTop);
+                dr.mShowing[Drawables.TOP].draw(canvas);
+                canvas.restore();
+            }
+
+            // IMPORTANT: The coordinates computed are also used in invalidateDrawable()
+            // Make sure to update invalidateDrawable() when changing this code.
+            if (dr.mShowing[Drawables.BOTTOM] != null) {
+                canvas.save();
+                canvas.translate(scrollX + compoundPaddingLeft
+                        + (hspace - dr.mDrawableWidthBottom) / 2,
+                         scrollY + bottom - top - mPaddingBottom - dr.mDrawableSizeBottom);
+                dr.mShowing[Drawables.BOTTOM].draw(canvas);
+                canvas.restore();
+            }
+        }
+
+        int color = mCurTextColor;
+
+        if (mLayout == null) {
+            assumeLayout();
+        }
+
+        Layout layout = mLayout;
+
+        if (mHint != null && mText.length() == 0) {
+            if (mHintTextColor != null) {
+                color = mCurHintTextColor;
+            }
+
+            layout = mHintLayout;
+        }
+
+        mTextPaint.setColor(color);
+        mTextPaint.drawableState = getDrawableState();
+
+        canvas.save();
+        /*  Would be faster if we didn't have to do this. Can we chop the
+            (displayable) text so that we don't need to do this ever?
+        */
+
+        int extendedPaddingTop = getExtendedPaddingTop();
+        int extendedPaddingBottom = getExtendedPaddingBottom();
+
+        final int vspace = mBottom - mTop - compoundPaddingBottom - compoundPaddingTop;
+        final int maxScrollY = mLayout.getHeight() - vspace;
+
+        float clipLeft = compoundPaddingLeft + scrollX;
+        float clipTop = (scrollY == 0) ? 0 : extendedPaddingTop + scrollY;
+        float clipRight = right - left - getCompoundPaddingRight() + scrollX;
+        float clipBottom = bottom - top + scrollY
+                - ((scrollY == maxScrollY) ? 0 : extendedPaddingBottom);
+
+        if (mShadowRadius != 0) {
+            clipLeft += Math.min(0, mShadowDx - mShadowRadius);
+            clipRight += Math.max(0, mShadowDx + mShadowRadius);
+
+            clipTop += Math.min(0, mShadowDy - mShadowRadius);
+            clipBottom += Math.max(0, mShadowDy + mShadowRadius);
+        }
+
+        canvas.clipRect(clipLeft, clipTop, clipRight, clipBottom);
+
+        int voffsetText = 0;
+        int voffsetCursor = 0;
+
+        // translate in by our padding
+        /* shortcircuit calling getVerticaOffset() */
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+            voffsetText = getVerticalOffset(false);
+            voffsetCursor = getVerticalOffset(true);
+        }
+        canvas.translate(compoundPaddingLeft, extendedPaddingTop + voffsetText);
+
+        final int layoutDirection = getLayoutDirection();
+        final int absoluteGravity = Gravity.getAbsoluteGravity(mGravity, layoutDirection);
+        if (isMarqueeFadeEnabled()) {
+            if (!mSingleLine && getLineCount() == 1 && canMarquee()
+                    && (absoluteGravity & Gravity.HORIZONTAL_GRAVITY_MASK) != Gravity.LEFT) {
+                final int width = mRight - mLeft;
+                final int padding = getCompoundPaddingLeft() + getCompoundPaddingRight();
+                final float dx = mLayout.getLineRight(0) - (width - padding);
+                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+            }
+
+            if (mMarquee != null && mMarquee.isRunning()) {
+                final float dx = -mMarquee.getScroll();
+                canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+            }
+        }
+
+        final int cursorOffsetVertical = voffsetCursor - voffsetText;
+
+        Path highlight = getUpdatedHighlightPath();
+        if (mEditor != null) {
+            mEditor.onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
+        } else {
+            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+        }
+
+        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+            final float dx = mMarquee.getGhostOffset();
+            canvas.translate(layout.getParagraphDirection(0) * dx, 0.0f);
+            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+        }
+
+        canvas.restore();
+    }
+
+    @Override
+    public void getFocusedRect(Rect r) {
+        if (mLayout == null) {
+            super.getFocusedRect(r);
+            return;
+        }
+
+        int selEnd = getSelectionEnd();
+        if (selEnd < 0) {
+            super.getFocusedRect(r);
+            return;
+        }
+
+        int selStart = getSelectionStart();
+        if (selStart < 0 || selStart >= selEnd) {
+            int line = mLayout.getLineForOffset(selEnd);
+            r.top = mLayout.getLineTop(line);
+            r.bottom = mLayout.getLineBottom(line);
+            r.left = (int) mLayout.getPrimaryHorizontal(selEnd) - 2;
+            r.right = r.left + 4;
+        } else {
+            int lineStart = mLayout.getLineForOffset(selStart);
+            int lineEnd = mLayout.getLineForOffset(selEnd);
+            r.top = mLayout.getLineTop(lineStart);
+            r.bottom = mLayout.getLineBottom(lineEnd);
+            if (lineStart == lineEnd) {
+                r.left = (int) mLayout.getPrimaryHorizontal(selStart);
+                r.right = (int) mLayout.getPrimaryHorizontal(selEnd);
+            } else {
+                // Selection extends across multiple lines -- make the focused
+                // rect cover the entire width.
+                if (mHighlightPathBogus) {
+                    if (mHighlightPath == null) mHighlightPath = new Path();
+                    mHighlightPath.reset();
+                    mLayout.getSelectionPath(selStart, selEnd, mHighlightPath);
+                    mHighlightPathBogus = false;
+                }
+                synchronized (TEMP_RECTF) {
+                    mHighlightPath.computeBounds(TEMP_RECTF, true);
+                    r.left = (int) TEMP_RECTF.left - 1;
+                    r.right = (int) TEMP_RECTF.right + 1;
+                }
+            }
+        }
+
+        // Adjust for padding and gravity.
+        int paddingLeft = getCompoundPaddingLeft();
+        int paddingTop = getExtendedPaddingTop();
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+            paddingTop += getVerticalOffset(false);
+        }
+        r.offset(paddingLeft, paddingTop);
+        int paddingBottom = getExtendedPaddingBottom();
+        r.bottom += paddingBottom;
+    }
+
+    /**
+     * Return the number of lines of text, or 0 if the internal Layout has not
+     * been built.
+     */
+    public int getLineCount() {
+        return mLayout != null ? mLayout.getLineCount() : 0;
+    }
+
+    /**
+     * Return the baseline for the specified line (0...getLineCount() - 1)
+     * If bounds is not null, return the top, left, right, bottom extents
+     * of the specified line in it. If the internal Layout has not been built,
+     * return 0 and set bounds to (0, 0, 0, 0)
+     * @param line which line to examine (0..getLineCount() - 1)
+     * @param bounds Optional. If not null, it returns the extent of the line
+     * @return the Y-coordinate of the baseline
+     */
+    public int getLineBounds(int line, Rect bounds) {
+        if (mLayout == null) {
+            if (bounds != null) {
+                bounds.set(0, 0, 0, 0);
+            }
+            return 0;
+        } else {
+            int baseline = mLayout.getLineBounds(line, bounds);
+
+            int voffset = getExtendedPaddingTop();
+            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+                voffset += getVerticalOffset(true);
+            }
+            if (bounds != null) {
+                bounds.offset(getCompoundPaddingLeft(), voffset);
+            }
+            return baseline + voffset;
+        }
+    }
+
+    @Override
+    public int getBaseline() {
+        if (mLayout == null) {
+            return super.getBaseline();
+        }
+
+        return getBaselineOffset() + mLayout.getLineBaseline(0);
+    }
+
+    int getBaselineOffset() {
+        int voffset = 0;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+            voffset = getVerticalOffset(true);
+        }
+
+        if (isLayoutModeOptical(mParent)) {
+            voffset -= getOpticalInsets().top;
+        }
+
+        return getExtendedPaddingTop() + voffset;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected int getFadeTop(boolean offsetRequired) {
+        if (mLayout == null) return 0;
+
+        int voffset = 0;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+            voffset = getVerticalOffset(true);
+        }
+
+        if (offsetRequired) voffset += getTopPaddingOffset();
+
+        return getExtendedPaddingTop() + voffset;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected int getFadeHeight(boolean offsetRequired) {
+        return mLayout != null ? mLayout.getHeight() : 0;
+    }
+
+    @Override
+    public PointerIcon onResolvePointerIcon(MotionEvent event, int pointerIndex) {
+        if (mText instanceof Spannable && mLinksClickable) {
+            final float x = event.getX(pointerIndex);
+            final float y = event.getY(pointerIndex);
+            final int offset = getOffsetForPosition(x, y);
+            final ClickableSpan[] clickables = ((Spannable) mText).getSpans(offset, offset,
+                    ClickableSpan.class);
+            if (clickables.length > 0) {
+                return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_HAND);
+            }
+        }
+        if (isTextSelectable() || isTextEditable()) {
+            return PointerIcon.getSystemIcon(mContext, PointerIcon.TYPE_TEXT);
+        }
+        return super.onResolvePointerIcon(event, pointerIndex);
+    }
+
+    @Override
+    public boolean onKeyPreIme(int keyCode, KeyEvent event) {
+        // Note: If the IME is in fullscreen mode and IMS#mExtractEditText is in text action mode,
+        // InputMethodService#onKeyDown and InputMethodService#onKeyUp are responsible to call
+        // InputMethodService#mExtractEditText.maybeHandleBackInTextActionMode(event).
+        if (keyCode == KeyEvent.KEYCODE_BACK && handleBackInTextActionModeIfNeeded(event)) {
+            return true;
+        }
+        return super.onKeyPreIme(keyCode, event);
+    }
+
+    /**
+     * @hide
+     */
+    public boolean handleBackInTextActionModeIfNeeded(KeyEvent event) {
+        // Do nothing unless mEditor is in text action mode.
+        if (mEditor == null || mEditor.getTextActionMode() == null) {
+            return false;
+        }
+
+        if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0) {
+            KeyEvent.DispatcherState state = getKeyDispatcherState();
+            if (state != null) {
+                state.startTracking(event, this);
+            }
+            return true;
+        } else if (event.getAction() == KeyEvent.ACTION_UP) {
+            KeyEvent.DispatcherState state = getKeyDispatcherState();
+            if (state != null) {
+                state.handleUpEvent(event);
+            }
+            if (event.isTracking() && !event.isCanceled()) {
+                stopTextActionMode();
+                return true;
+            }
+        }
+        return false;
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event) {
+        final int which = doKeyDown(keyCode, event, null);
+        if (which == KEY_EVENT_NOT_HANDLED) {
+            return super.onKeyDown(keyCode, event);
+        }
+
+        return true;
+    }
+
+    @Override
+    public boolean onKeyMultiple(int keyCode, int repeatCount, KeyEvent event) {
+        KeyEvent down = KeyEvent.changeAction(event, KeyEvent.ACTION_DOWN);
+        final int which = doKeyDown(keyCode, down, event);
+        if (which == KEY_EVENT_NOT_HANDLED) {
+            // Go through default dispatching.
+            return super.onKeyMultiple(keyCode, repeatCount, event);
+        }
+        if (which == KEY_EVENT_HANDLED) {
+            // Consumed the whole thing.
+            return true;
+        }
+
+        repeatCount--;
+
+        // We are going to dispatch the remaining events to either the input
+        // or movement method.  To do this, we will just send a repeated stream
+        // of down and up events until we have done the complete repeatCount.
+        // It would be nice if those interfaces had an onKeyMultiple() method,
+        // but adding that is a more complicated change.
+        KeyEvent up = KeyEvent.changeAction(event, KeyEvent.ACTION_UP);
+        if (which == KEY_DOWN_HANDLED_BY_KEY_LISTENER) {
+            // mEditor and mEditor.mInput are not null from doKeyDown
+            mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
+            while (--repeatCount > 0) {
+                mEditor.mKeyListener.onKeyDown(this, (Editable) mText, keyCode, down);
+                mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, up);
+            }
+            hideErrorIfUnchanged();
+
+        } else if (which == KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD) {
+            // mMovement is not null from doKeyDown
+            mMovement.onKeyUp(this, (Spannable) mText, keyCode, up);
+            while (--repeatCount > 0) {
+                mMovement.onKeyDown(this, (Spannable) mText, keyCode, down);
+                mMovement.onKeyUp(this, (Spannable) mText, keyCode, up);
+            }
+        }
+
+        return true;
+    }
+
+    /**
+     * Returns true if pressing ENTER in this field advances focus instead
+     * of inserting the character.  This is true mostly in single-line fields,
+     * but also in mail addresses and subjects which will display on multiple
+     * lines but where it doesn't make sense to insert newlines.
+     */
+    private boolean shouldAdvanceFocusOnEnter() {
+        if (getKeyListener() == null) {
+            return false;
+        }
+
+        if (mSingleLine) {
+            return true;
+        }
+
+        if (mEditor != null
+                && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+                        == EditorInfo.TYPE_CLASS_TEXT) {
+            int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+            if (variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_ADDRESS
+                    || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT) {
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns true if pressing TAB in this field advances focus instead
+     * of inserting the character.  Insert tabs only in multi-line editors.
+     */
+    private boolean shouldAdvanceFocusOnTab() {
+        if (getKeyListener() != null && !mSingleLine && mEditor != null
+                && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+                        == EditorInfo.TYPE_CLASS_TEXT) {
+            int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+            if (variation == EditorInfo.TYPE_TEXT_FLAG_IME_MULTI_LINE
+                    || variation == EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    private boolean isDirectionalNavigationKey(int keyCode) {
+        switch(keyCode) {
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+                return true;
+        }
+        return false;
+    }
+
+    private int doKeyDown(int keyCode, KeyEvent event, KeyEvent otherEvent) {
+        if (!isEnabled()) {
+            return KEY_EVENT_NOT_HANDLED;
+        }
+
+        // If this is the initial keydown, we don't want to prevent a movement away from this view.
+        // While this shouldn't be necessary because any time we're preventing default movement we
+        // should be restricting the focus to remain within this view, thus we'll also receive
+        // the key up event, occasionally key up events will get dropped and we don't want to
+        // prevent the user from traversing out of this on the next key down.
+        if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
+            mPreventDefaultMovement = false;
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_ENTER:
+                if (event.hasNoModifiers()) {
+                    // When mInputContentType is set, we know that we are
+                    // running in a "modern" cupcake environment, so don't need
+                    // to worry about the application trying to capture
+                    // enter key events.
+                    if (mEditor != null && mEditor.mInputContentType != null) {
+                        // If there is an action listener, given them a
+                        // chance to consume the event.
+                        if (mEditor.mInputContentType.onEditorActionListener != null
+                                && mEditor.mInputContentType.onEditorActionListener.onEditorAction(
+                                        this, EditorInfo.IME_NULL, event)) {
+                            mEditor.mInputContentType.enterDown = true;
+                            // We are consuming the enter key for them.
+                            return KEY_EVENT_HANDLED;
+                        }
+                    }
+
+                    // If our editor should move focus when enter is pressed, or
+                    // this is a generated event from an IME action button, then
+                    // don't let it be inserted into the text.
+                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+                            || shouldAdvanceFocusOnEnter()) {
+                        if (hasOnClickListeners()) {
+                            return KEY_EVENT_NOT_HANDLED;
+                        }
+                        return KEY_EVENT_HANDLED;
+                    }
+                }
+                break;
+
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (event.hasNoModifiers()) {
+                    if (shouldAdvanceFocusOnEnter()) {
+                        return KEY_EVENT_NOT_HANDLED;
+                    }
+                }
+                break;
+
+            case KeyEvent.KEYCODE_TAB:
+                if (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_SHIFT_ON)) {
+                    if (shouldAdvanceFocusOnTab()) {
+                        return KEY_EVENT_NOT_HANDLED;
+                    }
+                }
+                break;
+
+                // Has to be done on key down (and not on key up) to correctly be intercepted.
+            case KeyEvent.KEYCODE_BACK:
+                if (mEditor != null && mEditor.getTextActionMode() != null) {
+                    stopTextActionMode();
+                    return KEY_EVENT_HANDLED;
+                }
+                break;
+
+            case KeyEvent.KEYCODE_CUT:
+                if (event.hasNoModifiers() && canCut()) {
+                    if (onTextContextMenuItem(ID_CUT)) {
+                        return KEY_EVENT_HANDLED;
+                    }
+                }
+                break;
+
+            case KeyEvent.KEYCODE_COPY:
+                if (event.hasNoModifiers() && canCopy()) {
+                    if (onTextContextMenuItem(ID_COPY)) {
+                        return KEY_EVENT_HANDLED;
+                    }
+                }
+                break;
+
+            case KeyEvent.KEYCODE_PASTE:
+                if (event.hasNoModifiers() && canPaste()) {
+                    if (onTextContextMenuItem(ID_PASTE)) {
+                        return KEY_EVENT_HANDLED;
+                    }
+                }
+                break;
+        }
+
+        if (mEditor != null && mEditor.mKeyListener != null) {
+            boolean doDown = true;
+            if (otherEvent != null) {
+                try {
+                    beginBatchEdit();
+                    final boolean handled = mEditor.mKeyListener.onKeyOther(this, (Editable) mText,
+                            otherEvent);
+                    hideErrorIfUnchanged();
+                    doDown = false;
+                    if (handled) {
+                        return KEY_EVENT_HANDLED;
+                    }
+                } catch (AbstractMethodError e) {
+                    // onKeyOther was added after 1.0, so if it isn't
+                    // implemented we need to try to dispatch as a regular down.
+                } finally {
+                    endBatchEdit();
+                }
+            }
+
+            if (doDown) {
+                beginBatchEdit();
+                final boolean handled = mEditor.mKeyListener.onKeyDown(this, (Editable) mText,
+                        keyCode, event);
+                endBatchEdit();
+                hideErrorIfUnchanged();
+                if (handled) return KEY_DOWN_HANDLED_BY_KEY_LISTENER;
+            }
+        }
+
+        // bug 650865: sometimes we get a key event before a layout.
+        // don't try to move around if we don't know the layout.
+
+        if (mMovement != null && mLayout != null) {
+            boolean doDown = true;
+            if (otherEvent != null) {
+                try {
+                    boolean handled = mMovement.onKeyOther(this, (Spannable) mText,
+                            otherEvent);
+                    doDown = false;
+                    if (handled) {
+                        return KEY_EVENT_HANDLED;
+                    }
+                } catch (AbstractMethodError e) {
+                    // onKeyOther was added after 1.0, so if it isn't
+                    // implemented we need to try to dispatch as a regular down.
+                }
+            }
+            if (doDown) {
+                if (mMovement.onKeyDown(this, (Spannable) mText, keyCode, event)) {
+                    if (event.getRepeatCount() == 0 && !KeyEvent.isModifierKey(keyCode)) {
+                        mPreventDefaultMovement = true;
+                    }
+                    return KEY_DOWN_HANDLED_BY_MOVEMENT_METHOD;
+                }
+            }
+            // Consume arrows from keyboard devices to prevent focus leaving the editor.
+            // DPAD/JOY devices (Gamepads, TV remotes) often lack a TAB key so allow those
+            // to move focus with arrows.
+            if (event.getSource() == InputDevice.SOURCE_KEYBOARD
+                    && isDirectionalNavigationKey(keyCode)) {
+                return KEY_EVENT_HANDLED;
+            }
+        }
+
+        return mPreventDefaultMovement && !KeyEvent.isModifierKey(keyCode)
+                ? KEY_EVENT_HANDLED : KEY_EVENT_NOT_HANDLED;
+    }
+
+    /**
+     * Resets the mErrorWasChanged flag, so that future calls to {@link #setError(CharSequence)}
+     * can be recorded.
+     * @hide
+     */
+    public void resetErrorChangedFlag() {
+        /*
+         * Keep track of what the error was before doing the input
+         * so that if an input filter changed the error, we leave
+         * that error showing.  Otherwise, we take down whatever
+         * error was showing when the user types something.
+         */
+        if (mEditor != null) mEditor.mErrorWasChanged = false;
+    }
+
+    /**
+     * @hide
+     */
+    public void hideErrorIfUnchanged() {
+        if (mEditor != null && mEditor.mError != null && !mEditor.mErrorWasChanged) {
+            setError(null, null);
+        }
+    }
+
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        if (!isEnabled()) {
+            return super.onKeyUp(keyCode, event);
+        }
+
+        if (!KeyEvent.isModifierKey(keyCode)) {
+            mPreventDefaultMovement = false;
+        }
+
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+                if (event.hasNoModifiers()) {
+                    /*
+                     * If there is a click listener, just call through to
+                     * super, which will invoke it.
+                     *
+                     * If there isn't a click listener, try to show the soft
+                     * input method.  (It will also
+                     * call performClick(), but that won't do anything in
+                     * this case.)
+                     */
+                    if (!hasOnClickListeners()) {
+                        if (mMovement != null && mText instanceof Editable
+                                && mLayout != null && onCheckIsTextEditor()) {
+                            InputMethodManager imm = InputMethodManager.peekInstance();
+                            viewClicked(imm);
+                            if (imm != null && getShowSoftInputOnFocus()) {
+                                imm.showSoftInput(this, 0);
+                            }
+                        }
+                    }
+                }
+                return super.onKeyUp(keyCode, event);
+
+            case KeyEvent.KEYCODE_ENTER:
+                if (event.hasNoModifiers()) {
+                    if (mEditor != null && mEditor.mInputContentType != null
+                            && mEditor.mInputContentType.onEditorActionListener != null
+                            && mEditor.mInputContentType.enterDown) {
+                        mEditor.mInputContentType.enterDown = false;
+                        if (mEditor.mInputContentType.onEditorActionListener.onEditorAction(
+                                this, EditorInfo.IME_NULL, event)) {
+                            return true;
+                        }
+                    }
+
+                    if ((event.getFlags() & KeyEvent.FLAG_EDITOR_ACTION) != 0
+                            || shouldAdvanceFocusOnEnter()) {
+                        /*
+                         * If there is a click listener, just call through to
+                         * super, which will invoke it.
+                         *
+                         * If there isn't a click listener, try to advance focus,
+                         * but still call through to super, which will reset the
+                         * pressed state and longpress state.  (It will also
+                         * call performClick(), but that won't do anything in
+                         * this case.)
+                         */
+                        if (!hasOnClickListeners()) {
+                            View v = focusSearch(FOCUS_DOWN);
+
+                            if (v != null) {
+                                if (!v.requestFocus(FOCUS_DOWN)) {
+                                    throw new IllegalStateException("focus search returned a view "
+                                            + "that wasn't able to take focus!");
+                                }
+
+                                /*
+                                 * Return true because we handled the key; super
+                                 * will return false because there was no click
+                                 * listener.
+                                 */
+                                super.onKeyUp(keyCode, event);
+                                return true;
+                            } else if ((event.getFlags()
+                                    & KeyEvent.FLAG_EDITOR_ACTION) != 0) {
+                                // No target for next focus, but make sure the IME
+                                // if this came from it.
+                                InputMethodManager imm = InputMethodManager.peekInstance();
+                                if (imm != null && imm.isActive(this)) {
+                                    imm.hideSoftInputFromWindow(getWindowToken(), 0);
+                                }
+                            }
+                        }
+                    }
+                    return super.onKeyUp(keyCode, event);
+                }
+                break;
+        }
+
+        if (mEditor != null && mEditor.mKeyListener != null) {
+            if (mEditor.mKeyListener.onKeyUp(this, (Editable) mText, keyCode, event)) {
+                return true;
+            }
+        }
+
+        if (mMovement != null && mLayout != null) {
+            if (mMovement.onKeyUp(this, (Spannable) mText, keyCode, event)) {
+                return true;
+            }
+        }
+
+        return super.onKeyUp(keyCode, event);
+    }
+
+    @Override
+    public boolean onCheckIsTextEditor() {
+        return mEditor != null && mEditor.mInputType != EditorInfo.TYPE_NULL;
+    }
+
+    @Override
+    public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
+        if (onCheckIsTextEditor() && isEnabled()) {
+            mEditor.createInputMethodStateIfNeeded();
+            outAttrs.inputType = getInputType();
+            if (mEditor.mInputContentType != null) {
+                outAttrs.imeOptions = mEditor.mInputContentType.imeOptions;
+                outAttrs.privateImeOptions = mEditor.mInputContentType.privateImeOptions;
+                outAttrs.actionLabel = mEditor.mInputContentType.imeActionLabel;
+                outAttrs.actionId = mEditor.mInputContentType.imeActionId;
+                outAttrs.extras = mEditor.mInputContentType.extras;
+                outAttrs.hintLocales = mEditor.mInputContentType.imeHintLocales;
+            } else {
+                outAttrs.imeOptions = EditorInfo.IME_NULL;
+                outAttrs.hintLocales = null;
+            }
+            if (focusSearch(FOCUS_DOWN) != null) {
+                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_NEXT;
+            }
+            if (focusSearch(FOCUS_UP) != null) {
+                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NAVIGATE_PREVIOUS;
+            }
+            if ((outAttrs.imeOptions & EditorInfo.IME_MASK_ACTION)
+                    == EditorInfo.IME_ACTION_UNSPECIFIED) {
+                if ((outAttrs.imeOptions & EditorInfo.IME_FLAG_NAVIGATE_NEXT) != 0) {
+                    // An action has not been set, but the enter key will move to
+                    // the next focus, so set the action to that.
+                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_NEXT;
+                } else {
+                    // An action has not been set, and there is no focus to move
+                    // to, so let's just supply a "done" action.
+                    outAttrs.imeOptions |= EditorInfo.IME_ACTION_DONE;
+                }
+                if (!shouldAdvanceFocusOnEnter()) {
+                    outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+                }
+            }
+            if (isMultilineInputType(outAttrs.inputType)) {
+                // Multi-line text editors should always show an enter key.
+                outAttrs.imeOptions |= EditorInfo.IME_FLAG_NO_ENTER_ACTION;
+            }
+            outAttrs.hintText = mHint;
+            if (mText instanceof Editable) {
+                InputConnection ic = new EditableInputConnection(this);
+                outAttrs.initialSelStart = getSelectionStart();
+                outAttrs.initialSelEnd = getSelectionEnd();
+                outAttrs.initialCapsMode = ic.getCursorCapsMode(getInputType());
+                return ic;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * If this TextView contains editable content, extract a portion of it
+     * based on the information in <var>request</var> in to <var>outText</var>.
+     * @return Returns true if the text was successfully extracted, else false.
+     */
+    public boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+        createEditorIfNeeded();
+        return mEditor.extractText(request, outText);
+    }
+
+    /**
+     * This is used to remove all style-impacting spans from text before new
+     * extracted text is being replaced into it, so that we don't have any
+     * lingering spans applied during the replace.
+     */
+    static void removeParcelableSpans(Spannable spannable, int start, int end) {
+        Object[] spans = spannable.getSpans(start, end, ParcelableSpan.class);
+        int i = spans.length;
+        while (i > 0) {
+            i--;
+            spannable.removeSpan(spans[i]);
+        }
+    }
+
+    /**
+     * Apply to this text view the given extracted text, as previously
+     * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
+     */
+    public void setExtractedText(ExtractedText text) {
+        Editable content = getEditableText();
+        if (text.text != null) {
+            if (content == null) {
+                setText(text.text, TextView.BufferType.EDITABLE);
+            } else {
+                int start = 0;
+                int end = content.length();
+
+                if (text.partialStartOffset >= 0) {
+                    final int N = content.length();
+                    start = text.partialStartOffset;
+                    if (start > N) start = N;
+                    end = text.partialEndOffset;
+                    if (end > N) end = N;
+                }
+
+                removeParcelableSpans(content, start, end);
+                if (TextUtils.equals(content.subSequence(start, end), text.text)) {
+                    if (text.text instanceof Spanned) {
+                        // OK to copy spans only.
+                        TextUtils.copySpansFrom((Spanned) text.text, 0, end - start,
+                                Object.class, content, start);
+                    }
+                } else {
+                    content.replace(start, end, text.text);
+                }
+            }
+        }
+
+        // Now set the selection position...  make sure it is in range, to
+        // avoid crashes.  If this is a partial update, it is possible that
+        // the underlying text may have changed, causing us problems here.
+        // Also we just don't want to trust clients to do the right thing.
+        Spannable sp = (Spannable) getText();
+        final int N = sp.length();
+        int start = text.selectionStart;
+        if (start < 0) {
+            start = 0;
+        } else if (start > N) {
+            start = N;
+        }
+        int end = text.selectionEnd;
+        if (end < 0) {
+            end = 0;
+        } else if (end > N) {
+            end = N;
+        }
+        Selection.setSelection(sp, start, end);
+
+        // Finally, update the selection mode.
+        if ((text.flags & ExtractedText.FLAG_SELECTING) != 0) {
+            MetaKeyKeyListener.startSelecting(this, sp);
+        } else {
+            MetaKeyKeyListener.stopSelecting(this, sp);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public void setExtracting(ExtractedTextRequest req) {
+        if (mEditor.mInputMethodState != null) {
+            mEditor.mInputMethodState.mExtractedTextRequest = req;
+        }
+        // This would stop a possible selection mode, but no such mode is started in case
+        // extracted mode will start. Some text is selected though, and will trigger an action mode
+        // in the extracted view.
+        mEditor.hideCursorAndSpanControllers();
+        stopTextActionMode();
+        if (mEditor.mSelectionModifierCursorController != null) {
+            mEditor.mSelectionModifierCursorController.resetTouchOffsets();
+        }
+    }
+
+    /**
+     * Called by the framework in response to a text completion from
+     * the current input method, provided by it calling
+     * {@link InputConnection#commitCompletion
+     * InputConnection.commitCompletion()}.  The default implementation does
+     * nothing; text views that are supporting auto-completion should override
+     * this to do their desired behavior.
+     *
+     * @param text The auto complete text the user has selected.
+     */
+    public void onCommitCompletion(CompletionInfo text) {
+        // intentionally empty
+    }
+
+    /**
+     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+     * dictionary) from the current input method, provided by it calling
+     * {@link InputConnection#commitCorrection(CorrectionInfo) InputConnection.commitCorrection()}.
+     * The default implementation flashes the background of the corrected word to provide
+     * feedback to the user.
+     *
+     * @param info The auto correct info about the text that was corrected.
+     */
+    public void onCommitCorrection(CorrectionInfo info) {
+        if (mEditor != null) mEditor.onCommitCorrection(info);
+    }
+
+    public void beginBatchEdit() {
+        if (mEditor != null) mEditor.beginBatchEdit();
+    }
+
+    public void endBatchEdit() {
+        if (mEditor != null) mEditor.endBatchEdit();
+    }
+
+    /**
+     * Called by the framework in response to a request to begin a batch
+     * of edit operations through a call to link {@link #beginBatchEdit()}.
+     */
+    public void onBeginBatchEdit() {
+        // intentionally empty
+    }
+
+    /**
+     * Called by the framework in response to a request to end a batch
+     * of edit operations through a call to link {@link #endBatchEdit}.
+     */
+    public void onEndBatchEdit() {
+        // intentionally empty
+    }
+
+    /**
+     * Called by the framework in response to a private command from the
+     * current method, provided by it calling
+     * {@link InputConnection#performPrivateCommand
+     * InputConnection.performPrivateCommand()}.
+     *
+     * @param action The action name of the command.
+     * @param data Any additional data for the command.  This may be null.
+     * @return Return true if you handled the command, else false.
+     */
+    public boolean onPrivateIMECommand(String action, Bundle data) {
+        return false;
+    }
+
+    private void nullLayouts() {
+        if (mLayout instanceof BoringLayout && mSavedLayout == null) {
+            mSavedLayout = (BoringLayout) mLayout;
+        }
+        if (mHintLayout instanceof BoringLayout && mSavedHintLayout == null) {
+            mSavedHintLayout = (BoringLayout) mHintLayout;
+        }
+
+        mSavedMarqueeModeLayout = mLayout = mHintLayout = null;
+
+        mBoring = mHintBoring = null;
+
+        // Since it depends on the value of mLayout
+        if (mEditor != null) mEditor.prepareCursorControllers();
+    }
+
+    /**
+     * Make a new Layout based on the already-measured size of the view,
+     * on the assumption that it was measured correctly at some point.
+     */
+    private void assumeLayout() {
+        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+
+        if (width < 1) {
+            width = 0;
+        }
+
+        int physicalWidth = width;
+
+        if (mHorizontallyScrolling) {
+            width = VERY_WIDE;
+        }
+
+        makeNewLayout(width, physicalWidth, UNKNOWN_BORING, UNKNOWN_BORING,
+                      physicalWidth, false);
+    }
+
+    private Layout.Alignment getLayoutAlignment() {
+        Layout.Alignment alignment;
+        switch (getTextAlignment()) {
+            case TEXT_ALIGNMENT_GRAVITY:
+                switch (mGravity & Gravity.RELATIVE_HORIZONTAL_GRAVITY_MASK) {
+                    case Gravity.START:
+                        alignment = Layout.Alignment.ALIGN_NORMAL;
+                        break;
+                    case Gravity.END:
+                        alignment = Layout.Alignment.ALIGN_OPPOSITE;
+                        break;
+                    case Gravity.LEFT:
+                        alignment = Layout.Alignment.ALIGN_LEFT;
+                        break;
+                    case Gravity.RIGHT:
+                        alignment = Layout.Alignment.ALIGN_RIGHT;
+                        break;
+                    case Gravity.CENTER_HORIZONTAL:
+                        alignment = Layout.Alignment.ALIGN_CENTER;
+                        break;
+                    default:
+                        alignment = Layout.Alignment.ALIGN_NORMAL;
+                        break;
+                }
+                break;
+            case TEXT_ALIGNMENT_TEXT_START:
+                alignment = Layout.Alignment.ALIGN_NORMAL;
+                break;
+            case TEXT_ALIGNMENT_TEXT_END:
+                alignment = Layout.Alignment.ALIGN_OPPOSITE;
+                break;
+            case TEXT_ALIGNMENT_CENTER:
+                alignment = Layout.Alignment.ALIGN_CENTER;
+                break;
+            case TEXT_ALIGNMENT_VIEW_START:
+                alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
+                        ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
+                break;
+            case TEXT_ALIGNMENT_VIEW_END:
+                alignment = (getLayoutDirection() == LAYOUT_DIRECTION_RTL)
+                        ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
+                break;
+            case TEXT_ALIGNMENT_INHERIT:
+                // This should never happen as we have already resolved the text alignment
+                // but better safe than sorry so we just fall through
+            default:
+                alignment = Layout.Alignment.ALIGN_NORMAL;
+                break;
+        }
+        return alignment;
+    }
+
+    /**
+     * The width passed in is now the desired layout width,
+     * not the full view width with padding.
+     * {@hide}
+     */
+    protected void makeNewLayout(int wantWidth, int hintWidth,
+                                 BoringLayout.Metrics boring,
+                                 BoringLayout.Metrics hintBoring,
+                                 int ellipsisWidth, boolean bringIntoView) {
+        stopMarquee();
+
+        // Update "old" cached values
+        mOldMaximum = mMaximum;
+        mOldMaxMode = mMaxMode;
+
+        mHighlightPathBogus = true;
+
+        if (wantWidth < 0) {
+            wantWidth = 0;
+        }
+        if (hintWidth < 0) {
+            hintWidth = 0;
+        }
+
+        Layout.Alignment alignment = getLayoutAlignment();
+        final boolean testDirChange = mSingleLine && mLayout != null
+                && (alignment == Layout.Alignment.ALIGN_NORMAL
+                        || alignment == Layout.Alignment.ALIGN_OPPOSITE);
+        int oldDir = 0;
+        if (testDirChange) oldDir = mLayout.getParagraphDirection(0);
+        boolean shouldEllipsize = mEllipsize != null && getKeyListener() == null;
+        final boolean switchEllipsize = mEllipsize == TruncateAt.MARQUEE
+                && mMarqueeFadeMode != MARQUEE_FADE_NORMAL;
+        TruncateAt effectiveEllipsize = mEllipsize;
+        if (mEllipsize == TruncateAt.MARQUEE
+                && mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
+            effectiveEllipsize = TruncateAt.END_SMALL;
+        }
+
+        if (mTextDir == null) {
+            mTextDir = getTextDirectionHeuristic();
+        }
+
+        mLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment, shouldEllipsize,
+                effectiveEllipsize, effectiveEllipsize == mEllipsize);
+        if (switchEllipsize) {
+            TruncateAt oppositeEllipsize = effectiveEllipsize == TruncateAt.MARQUEE
+                    ? TruncateAt.END : TruncateAt.MARQUEE;
+            mSavedMarqueeModeLayout = makeSingleLayout(wantWidth, boring, ellipsisWidth, alignment,
+                    shouldEllipsize, oppositeEllipsize, effectiveEllipsize != mEllipsize);
+        }
+
+        shouldEllipsize = mEllipsize != null;
+        mHintLayout = null;
+
+        if (mHint != null) {
+            if (shouldEllipsize) hintWidth = wantWidth;
+
+            if (hintBoring == UNKNOWN_BORING) {
+                hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir,
+                                                   mHintBoring);
+                if (hintBoring != null) {
+                    mHintBoring = hintBoring;
+                }
+            }
+
+            if (hintBoring != null) {
+                if (hintBoring.width <= hintWidth
+                        && (!shouldEllipsize || hintBoring.width <= ellipsisWidth)) {
+                    if (mSavedHintLayout != null) {
+                        mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
+                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
+                                hintBoring, mIncludePad);
+                    } else {
+                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
+                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
+                                hintBoring, mIncludePad);
+                    }
+
+                    mSavedHintLayout = (BoringLayout) mHintLayout;
+                } else if (shouldEllipsize && hintBoring.width <= hintWidth) {
+                    if (mSavedHintLayout != null) {
+                        mHintLayout = mSavedHintLayout.replaceOrMake(mHint, mTextPaint,
+                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
+                                hintBoring, mIncludePad, mEllipsize,
+                                ellipsisWidth);
+                    } else {
+                        mHintLayout = BoringLayout.make(mHint, mTextPaint,
+                                hintWidth, alignment, mSpacingMult, mSpacingAdd,
+                                hintBoring, mIncludePad, mEllipsize,
+                                ellipsisWidth);
+                    }
+                }
+            }
+            // TODO: code duplication with makeSingleLayout()
+            if (mHintLayout == null) {
+                StaticLayout.Builder builder = StaticLayout.Builder.obtain(mHint, 0,
+                        mHint.length(), mTextPaint, hintWidth)
+                        .setAlignment(alignment)
+                        .setTextDirection(mTextDir)
+                        .setLineSpacing(mSpacingAdd, mSpacingMult)
+                        .setIncludePad(mIncludePad)
+                        .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+                        .setBreakStrategy(mBreakStrategy)
+                        .setHyphenationFrequency(mHyphenationFrequency)
+                        .setJustificationMode(mJustificationMode)
+                        .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+                if (shouldEllipsize) {
+                    builder.setEllipsize(mEllipsize)
+                            .setEllipsizedWidth(ellipsisWidth);
+                }
+                mHintLayout = builder.build();
+            }
+        }
+
+        if (bringIntoView || (testDirChange && oldDir != mLayout.getParagraphDirection(0))) {
+            registerForPreDraw();
+        }
+
+        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+            if (!compressText(ellipsisWidth)) {
+                final int height = mLayoutParams.height;
+                // If the size of the view does not depend on the size of the text, try to
+                // start the marquee immediately
+                if (height != LayoutParams.WRAP_CONTENT && height != LayoutParams.MATCH_PARENT) {
+                    startMarquee();
+                } else {
+                    // Defer the start of the marquee until we know our width (see setFrame())
+                    mRestartMarquee = true;
+                }
+            }
+        }
+
+        // CursorControllers need a non-null mLayout
+        if (mEditor != null) mEditor.prepareCursorControllers();
+    }
+
+    /**
+     * @hide
+     */
+    protected Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
+            Layout.Alignment alignment, boolean shouldEllipsize, TruncateAt effectiveEllipsize,
+            boolean useSaved) {
+        Layout result = null;
+        if (mText instanceof Spannable) {
+            final DynamicLayout.Builder builder = DynamicLayout.Builder.obtain(mText, mTextPaint,
+                    wantWidth)
+                    .setDisplayText(mTransformed)
+                    .setAlignment(alignment)
+                    .setTextDirection(mTextDir)
+                    .setLineSpacing(mSpacingAdd, mSpacingMult)
+                    .setIncludePad(mIncludePad)
+                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+                    .setBreakStrategy(mBreakStrategy)
+                    .setHyphenationFrequency(mHyphenationFrequency)
+                    .setJustificationMode(mJustificationMode)
+                    .setEllipsize(getKeyListener() == null ? effectiveEllipsize : null)
+                    .setEllipsizedWidth(ellipsisWidth);
+            result = builder.build();
+        } else {
+            if (boring == UNKNOWN_BORING) {
+                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
+                if (boring != null) {
+                    mBoring = boring;
+                }
+            }
+
+            if (boring != null) {
+                if (boring.width <= wantWidth
+                        && (effectiveEllipsize == null || boring.width <= ellipsisWidth)) {
+                    if (useSaved && mSavedLayout != null) {
+                        result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
+                                boring, mIncludePad);
+                    } else {
+                        result = BoringLayout.make(mTransformed, mTextPaint,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
+                                boring, mIncludePad);
+                    }
+
+                    if (useSaved) {
+                        mSavedLayout = (BoringLayout) result;
+                    }
+                } else if (shouldEllipsize && boring.width <= wantWidth) {
+                    if (useSaved && mSavedLayout != null) {
+                        result = mSavedLayout.replaceOrMake(mTransformed, mTextPaint,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
+                                boring, mIncludePad, effectiveEllipsize,
+                                ellipsisWidth);
+                    } else {
+                        result = BoringLayout.make(mTransformed, mTextPaint,
+                                wantWidth, alignment, mSpacingMult, mSpacingAdd,
+                                boring, mIncludePad, effectiveEllipsize,
+                                ellipsisWidth);
+                    }
+                }
+            }
+        }
+        if (result == null) {
+            StaticLayout.Builder builder = StaticLayout.Builder.obtain(mTransformed,
+                    0, mTransformed.length(), mTextPaint, wantWidth)
+                    .setAlignment(alignment)
+                    .setTextDirection(mTextDir)
+                    .setLineSpacing(mSpacingAdd, mSpacingMult)
+                    .setIncludePad(mIncludePad)
+                    .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+                    .setBreakStrategy(mBreakStrategy)
+                    .setHyphenationFrequency(mHyphenationFrequency)
+                    .setJustificationMode(mJustificationMode)
+                    .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE);
+            if (shouldEllipsize) {
+                builder.setEllipsize(effectiveEllipsize)
+                        .setEllipsizedWidth(ellipsisWidth);
+            }
+            result = builder.build();
+        }
+        return result;
+    }
+
+    private boolean compressText(float width) {
+        if (isHardwareAccelerated()) return false;
+
+        // Only compress the text if it hasn't been compressed by the previous pass
+        if (width > 0.0f && mLayout != null && getLineCount() == 1 && !mUserSetTextScaleX
+                && mTextPaint.getTextScaleX() == 1.0f) {
+            final float textWidth = mLayout.getLineWidth(0);
+            final float overflow = (textWidth + 1.0f - width) / width;
+            if (overflow > 0.0f && overflow <= Marquee.MARQUEE_DELTA_MAX) {
+                mTextPaint.setTextScaleX(1.0f - overflow - 0.005f);
+                post(new Runnable() {
+                    public void run() {
+                        requestLayout();
+                    }
+                });
+                return true;
+            }
+        }
+
+        return false;
+    }
+
+    private static int desired(Layout layout) {
+        int n = layout.getLineCount();
+        CharSequence text = layout.getText();
+        float max = 0;
+
+        // if any line was wrapped, we can't use it.
+        // but it's ok for the last line not to have a newline
+
+        for (int i = 0; i < n - 1; i++) {
+            if (text.charAt(layout.getLineEnd(i) - 1) != '\n') {
+                return -1;
+            }
+        }
+
+        for (int i = 0; i < n; i++) {
+            max = Math.max(max, layout.getLineWidth(i));
+        }
+
+        return (int) Math.ceil(max);
+    }
+
+    /**
+     * Set whether the TextView includes extra top and bottom padding to make
+     * room for accents that go above the normal ascent and descent.
+     * The default is true.
+     *
+     * @see #getIncludeFontPadding()
+     *
+     * @attr ref android.R.styleable#TextView_includeFontPadding
+     */
+    public void setIncludeFontPadding(boolean includepad) {
+        if (mIncludePad != includepad) {
+            mIncludePad = includepad;
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Gets whether the TextView includes extra top and bottom padding to make
+     * room for accents that go above the normal ascent and descent.
+     *
+     * @see #setIncludeFontPadding(boolean)
+     *
+     * @attr ref android.R.styleable#TextView_includeFontPadding
+     */
+    public boolean getIncludeFontPadding() {
+        return mIncludePad;
+    }
+
+    private static final BoringLayout.Metrics UNKNOWN_BORING = new BoringLayout.Metrics();
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int widthMode = MeasureSpec.getMode(widthMeasureSpec);
+        int heightMode = MeasureSpec.getMode(heightMeasureSpec);
+        int widthSize = MeasureSpec.getSize(widthMeasureSpec);
+        int heightSize = MeasureSpec.getSize(heightMeasureSpec);
+
+        int width;
+        int height;
+
+        BoringLayout.Metrics boring = UNKNOWN_BORING;
+        BoringLayout.Metrics hintBoring = UNKNOWN_BORING;
+
+        if (mTextDir == null) {
+            mTextDir = getTextDirectionHeuristic();
+        }
+
+        int des = -1;
+        boolean fromexisting = false;
+        final float widthLimit = (widthMode == MeasureSpec.AT_MOST)
+                ?  (float) widthSize : Float.MAX_VALUE;
+
+        if (widthMode == MeasureSpec.EXACTLY) {
+            // Parent has told us how big to be. So be it.
+            width = widthSize;
+        } else {
+            if (mLayout != null && mEllipsize == null) {
+                des = desired(mLayout);
+            }
+
+            if (des < 0) {
+                boring = BoringLayout.isBoring(mTransformed, mTextPaint, mTextDir, mBoring);
+                if (boring != null) {
+                    mBoring = boring;
+                }
+            } else {
+                fromexisting = true;
+            }
+
+            if (boring == null || boring == UNKNOWN_BORING) {
+                if (des < 0) {
+                    des = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mTransformed, 0,
+                            mTransformed.length(), mTextPaint, mTextDir, widthLimit));
+                }
+                width = des;
+            } else {
+                width = boring.width;
+            }
+
+            final Drawables dr = mDrawables;
+            if (dr != null) {
+                width = Math.max(width, dr.mDrawableWidthTop);
+                width = Math.max(width, dr.mDrawableWidthBottom);
+            }
+
+            if (mHint != null) {
+                int hintDes = -1;
+                int hintWidth;
+
+                if (mHintLayout != null && mEllipsize == null) {
+                    hintDes = desired(mHintLayout);
+                }
+
+                if (hintDes < 0) {
+                    hintBoring = BoringLayout.isBoring(mHint, mTextPaint, mTextDir, mHintBoring);
+                    if (hintBoring != null) {
+                        mHintBoring = hintBoring;
+                    }
+                }
+
+                if (hintBoring == null || hintBoring == UNKNOWN_BORING) {
+                    if (hintDes < 0) {
+                        hintDes = (int) Math.ceil(Layout.getDesiredWidthWithLimit(mHint, 0,
+                                mHint.length(), mTextPaint, mTextDir, widthLimit));
+                    }
+                    hintWidth = hintDes;
+                } else {
+                    hintWidth = hintBoring.width;
+                }
+
+                if (hintWidth > width) {
+                    width = hintWidth;
+                }
+            }
+
+            width += getCompoundPaddingLeft() + getCompoundPaddingRight();
+
+            if (mMaxWidthMode == EMS) {
+                width = Math.min(width, mMaxWidth * getLineHeight());
+            } else {
+                width = Math.min(width, mMaxWidth);
+            }
+
+            if (mMinWidthMode == EMS) {
+                width = Math.max(width, mMinWidth * getLineHeight());
+            } else {
+                width = Math.max(width, mMinWidth);
+            }
+
+            // Check against our minimum width
+            width = Math.max(width, getSuggestedMinimumWidth());
+
+            if (widthMode == MeasureSpec.AT_MOST) {
+                width = Math.min(widthSize, width);
+            }
+        }
+
+        int want = width - getCompoundPaddingLeft() - getCompoundPaddingRight();
+        int unpaddedWidth = want;
+
+        if (mHorizontallyScrolling) want = VERY_WIDE;
+
+        int hintWant = want;
+        int hintWidth = (mHintLayout == null) ? hintWant : mHintLayout.getWidth();
+
+        if (mLayout == null) {
+            makeNewLayout(want, hintWant, boring, hintBoring,
+                          width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+        } else {
+            final boolean layoutChanged = (mLayout.getWidth() != want) || (hintWidth != hintWant)
+                    || (mLayout.getEllipsizedWidth()
+                            != width - getCompoundPaddingLeft() - getCompoundPaddingRight());
+
+            final boolean widthChanged = (mHint == null) && (mEllipsize == null)
+                    && (want > mLayout.getWidth())
+                    && (mLayout instanceof BoringLayout
+                            || (fromexisting && des >= 0 && des <= want));
+
+            final boolean maximumChanged = (mMaxMode != mOldMaxMode) || (mMaximum != mOldMaximum);
+
+            if (layoutChanged || maximumChanged) {
+                if (!maximumChanged && widthChanged) {
+                    mLayout.increaseWidthTo(want);
+                } else {
+                    makeNewLayout(want, hintWant, boring, hintBoring,
+                            width - getCompoundPaddingLeft() - getCompoundPaddingRight(), false);
+                }
+            } else {
+                // Nothing has changed
+            }
+        }
+
+        if (heightMode == MeasureSpec.EXACTLY) {
+            // Parent has told us how big to be. So be it.
+            height = heightSize;
+            mDesiredHeightAtMeasure = -1;
+        } else {
+            int desired = getDesiredHeight();
+
+            height = desired;
+            mDesiredHeightAtMeasure = desired;
+
+            if (heightMode == MeasureSpec.AT_MOST) {
+                height = Math.min(desired, heightSize);
+            }
+        }
+
+        int unpaddedHeight = height - getCompoundPaddingTop() - getCompoundPaddingBottom();
+        if (mMaxMode == LINES && mLayout.getLineCount() > mMaximum) {
+            unpaddedHeight = Math.min(unpaddedHeight, mLayout.getLineTop(mMaximum));
+        }
+
+        /*
+         * We didn't let makeNewLayout() register to bring the cursor into view,
+         * so do it here if there is any possibility that it is needed.
+         */
+        if (mMovement != null
+                || mLayout.getWidth() > unpaddedWidth
+                || mLayout.getHeight() > unpaddedHeight) {
+            registerForPreDraw();
+        } else {
+            scrollTo(0, 0);
+        }
+
+        setMeasuredDimension(width, height);
+    }
+
+    /**
+     * Automatically computes and sets the text size.
+     */
+    private void autoSizeText() {
+        if (!isAutoSizeEnabled()) {
+            return;
+        }
+
+        if (mNeedsAutoSizeText) {
+            if (getMeasuredWidth() <= 0 || getMeasuredHeight() <= 0) {
+                return;
+            }
+
+            final int availableWidth = mHorizontallyScrolling
+                    ? VERY_WIDE
+                    : getMeasuredWidth() - getTotalPaddingLeft() - getTotalPaddingRight();
+            final int availableHeight = getMeasuredHeight() - getExtendedPaddingBottom()
+                    - getExtendedPaddingTop();
+
+            if (availableWidth <= 0 || availableHeight <= 0) {
+                return;
+            }
+
+            synchronized (TEMP_RECTF) {
+                TEMP_RECTF.setEmpty();
+                TEMP_RECTF.right = availableWidth;
+                TEMP_RECTF.bottom = availableHeight;
+                final float optimalTextSize = findLargestTextSizeWhichFits(TEMP_RECTF);
+
+                if (optimalTextSize != getTextSize()) {
+                    setTextSizeInternal(TypedValue.COMPLEX_UNIT_PX, optimalTextSize,
+                            false /* shouldRequestLayout */);
+
+                    makeNewLayout(availableWidth, 0 /* hintWidth */, UNKNOWN_BORING, UNKNOWN_BORING,
+                            mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+                            false /* bringIntoView */);
+                }
+            }
+        }
+        // Always try to auto-size if enabled. Functions that do not want to trigger auto-sizing
+        // after the next layout pass should set this to false.
+        mNeedsAutoSizeText = true;
+    }
+
+    /**
+     * Performs a binary search to find the largest text size that will still fit within the size
+     * available to this view.
+     */
+    private int findLargestTextSizeWhichFits(RectF availableSpace) {
+        final int sizesCount = mAutoSizeTextSizesInPx.length;
+        if (sizesCount == 0) {
+            throw new IllegalStateException("No available text sizes to choose from.");
+        }
+
+        int bestSizeIndex = 0;
+        int lowIndex = bestSizeIndex + 1;
+        int highIndex = sizesCount - 1;
+        int sizeToTryIndex;
+        while (lowIndex <= highIndex) {
+            sizeToTryIndex = (lowIndex + highIndex) / 2;
+            if (suggestedSizeFitsInSpace(mAutoSizeTextSizesInPx[sizeToTryIndex], availableSpace)) {
+                bestSizeIndex = lowIndex;
+                lowIndex = sizeToTryIndex + 1;
+            } else {
+                highIndex = sizeToTryIndex - 1;
+                bestSizeIndex = highIndex;
+            }
+        }
+
+        return mAutoSizeTextSizesInPx[bestSizeIndex];
+    }
+
+    private boolean suggestedSizeFitsInSpace(int suggestedSizeInPx, RectF availableSpace) {
+        final CharSequence text = mTransformed != null
+                ? mTransformed
+                : getText();
+        final int maxLines = getMaxLines();
+        if (mTempTextPaint == null) {
+            mTempTextPaint = new TextPaint();
+        } else {
+            mTempTextPaint.reset();
+        }
+        mTempTextPaint.set(getPaint());
+        mTempTextPaint.setTextSize(suggestedSizeInPx);
+
+        final StaticLayout.Builder layoutBuilder = StaticLayout.Builder.obtain(
+                text, 0, text.length(),  mTempTextPaint, Math.round(availableSpace.right));
+
+        layoutBuilder.setAlignment(getLayoutAlignment())
+                .setLineSpacing(getLineSpacingExtra(), getLineSpacingMultiplier())
+                .setIncludePad(getIncludeFontPadding())
+                .setUseLineSpacingFromFallbacks(mUseFallbackLineSpacing)
+                .setBreakStrategy(getBreakStrategy())
+                .setHyphenationFrequency(getHyphenationFrequency())
+                .setJustificationMode(getJustificationMode())
+                .setMaxLines(mMaxMode == LINES ? mMaximum : Integer.MAX_VALUE)
+                .setTextDirection(getTextDirectionHeuristic());
+
+        final StaticLayout layout = layoutBuilder.build();
+
+        // Lines overflow.
+        if (maxLines != -1 && layout.getLineCount() > maxLines) {
+            return false;
+        }
+
+        // Height overflow.
+        if (layout.getHeight() > availableSpace.bottom) {
+            return false;
+        }
+
+        return true;
+    }
+
+    private int getDesiredHeight() {
+        return Math.max(
+                getDesiredHeight(mLayout, true),
+                getDesiredHeight(mHintLayout, mEllipsize != null));
+    }
+
+    private int getDesiredHeight(Layout layout, boolean cap) {
+        if (layout == null) {
+            return 0;
+        }
+
+        /*
+        * Don't cap the hint to a certain number of lines.
+        * (Do cap it, though, if we have a maximum pixel height.)
+        */
+        int desired = layout.getHeight(cap);
+
+        final Drawables dr = mDrawables;
+        if (dr != null) {
+            desired = Math.max(desired, dr.mDrawableHeightLeft);
+            desired = Math.max(desired, dr.mDrawableHeightRight);
+        }
+
+        int linecount = layout.getLineCount();
+        final int padding = getCompoundPaddingTop() + getCompoundPaddingBottom();
+        desired += padding;
+
+        if (mMaxMode != LINES) {
+            desired = Math.min(desired, mMaximum);
+        } else if (cap && linecount > mMaximum && layout instanceof DynamicLayout) {
+            desired = layout.getLineTop(mMaximum);
+
+            if (dr != null) {
+                desired = Math.max(desired, dr.mDrawableHeightLeft);
+                desired = Math.max(desired, dr.mDrawableHeightRight);
+            }
+
+            desired += padding;
+            linecount = mMaximum;
+        }
+
+        if (mMinMode == LINES) {
+            if (linecount < mMinimum) {
+                desired += getLineHeight() * (mMinimum - linecount);
+            }
+        } else {
+            desired = Math.max(desired, mMinimum);
+        }
+
+        // Check against our minimum height
+        desired = Math.max(desired, getSuggestedMinimumHeight());
+
+        return desired;
+    }
+
+    /**
+     * Check whether a change to the existing text layout requires a
+     * new view layout.
+     */
+    private void checkForResize() {
+        boolean sizeChanged = false;
+
+        if (mLayout != null) {
+            // Check if our width changed
+            if (mLayoutParams.width == LayoutParams.WRAP_CONTENT) {
+                sizeChanged = true;
+                invalidate();
+            }
+
+            // Check if our height changed
+            if (mLayoutParams.height == LayoutParams.WRAP_CONTENT) {
+                int desiredHeight = getDesiredHeight();
+
+                if (desiredHeight != this.getHeight()) {
+                    sizeChanged = true;
+                }
+            } else if (mLayoutParams.height == LayoutParams.MATCH_PARENT) {
+                if (mDesiredHeightAtMeasure >= 0) {
+                    int desiredHeight = getDesiredHeight();
+
+                    if (desiredHeight != mDesiredHeightAtMeasure) {
+                        sizeChanged = true;
+                    }
+                }
+            }
+        }
+
+        if (sizeChanged) {
+            requestLayout();
+            // caller will have already invalidated
+        }
+    }
+
+    /**
+     * Check whether entirely new text requires a new view layout
+     * or merely a new text layout.
+     */
+    private void checkForRelayout() {
+        // If we have a fixed width, we can just swap in a new text layout
+        // if the text height stays the same or if the view height is fixed.
+
+        if ((mLayoutParams.width != LayoutParams.WRAP_CONTENT
+                || (mMaxWidthMode == mMinWidthMode && mMaxWidth == mMinWidth))
+                && (mHint == null || mHintLayout != null)
+                && (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight() > 0)) {
+            // Static width, so try making a new text layout.
+
+            int oldht = mLayout.getHeight();
+            int want = mLayout.getWidth();
+            int hintWant = mHintLayout == null ? 0 : mHintLayout.getWidth();
+
+            /*
+             * No need to bring the text into view, since the size is not
+             * changing (unless we do the requestLayout(), in which case it
+             * will happen at measure).
+             */
+            makeNewLayout(want, hintWant, UNKNOWN_BORING, UNKNOWN_BORING,
+                          mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight(),
+                          false);
+
+            if (mEllipsize != TextUtils.TruncateAt.MARQUEE) {
+                // In a fixed-height view, so use our new text layout.
+                if (mLayoutParams.height != LayoutParams.WRAP_CONTENT
+                        && mLayoutParams.height != LayoutParams.MATCH_PARENT) {
+                    autoSizeText();
+                    invalidate();
+                    return;
+                }
+
+                // Dynamic height, but height has stayed the same,
+                // so use our new text layout.
+                if (mLayout.getHeight() == oldht
+                        && (mHintLayout == null || mHintLayout.getHeight() == oldht)) {
+                    autoSizeText();
+                    invalidate();
+                    return;
+                }
+            }
+
+            // We lose: the height has changed and we have a dynamic height.
+            // Request a new view layout using our new text layout.
+            requestLayout();
+            invalidate();
+        } else {
+            // Dynamic width, so we have no choice but to request a new
+            // view layout with a new text layout.
+            nullLayouts();
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+        if (mDeferScroll >= 0) {
+            int curs = mDeferScroll;
+            mDeferScroll = -1;
+            bringPointIntoView(Math.min(curs, mText.length()));
+        }
+        // Call auto-size after the width and height have been calculated.
+        autoSizeText();
+    }
+
+    private boolean isShowingHint() {
+        return TextUtils.isEmpty(mText) && !TextUtils.isEmpty(mHint);
+    }
+
+    /**
+     * Returns true if anything changed.
+     */
+    private boolean bringTextIntoView() {
+        Layout layout = isShowingHint() ? mHintLayout : mLayout;
+        int line = 0;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+            line = layout.getLineCount() - 1;
+        }
+
+        Layout.Alignment a = layout.getParagraphAlignment(line);
+        int dir = layout.getParagraphDirection(line);
+        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+        int ht = layout.getHeight();
+
+        int scrollx, scrolly;
+
+        // Convert to left, center, or right alignment.
+        if (a == Layout.Alignment.ALIGN_NORMAL) {
+            a = dir == Layout.DIR_LEFT_TO_RIGHT
+                    ? Layout.Alignment.ALIGN_LEFT : Layout.Alignment.ALIGN_RIGHT;
+        } else if (a == Layout.Alignment.ALIGN_OPPOSITE) {
+            a = dir == Layout.DIR_LEFT_TO_RIGHT
+                    ? Layout.Alignment.ALIGN_RIGHT : Layout.Alignment.ALIGN_LEFT;
+        }
+
+        if (a == Layout.Alignment.ALIGN_CENTER) {
+            /*
+             * Keep centered if possible, or, if it is too wide to fit,
+             * keep leading edge in view.
+             */
+
+            int left = (int) Math.floor(layout.getLineLeft(line));
+            int right = (int) Math.ceil(layout.getLineRight(line));
+
+            if (right - left < hspace) {
+                scrollx = (right + left) / 2 - hspace / 2;
+            } else {
+                if (dir < 0) {
+                    scrollx = right - hspace;
+                } else {
+                    scrollx = left;
+                }
+            }
+        } else if (a == Layout.Alignment.ALIGN_RIGHT) {
+            int right = (int) Math.ceil(layout.getLineRight(line));
+            scrollx = right - hspace;
+        } else { // a == Layout.Alignment.ALIGN_LEFT (will also be the default)
+            scrollx = (int) Math.floor(layout.getLineLeft(line));
+        }
+
+        if (ht < vspace) {
+            scrolly = 0;
+        } else {
+            if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+                scrolly = ht - vspace;
+            } else {
+                scrolly = 0;
+            }
+        }
+
+        if (scrollx != mScrollX || scrolly != mScrollY) {
+            scrollTo(scrollx, scrolly);
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    /**
+     * Move the point, specified by the offset, into the view if it is needed.
+     * This has to be called after layout. Returns true if anything changed.
+     */
+    public boolean bringPointIntoView(int offset) {
+        if (isLayoutRequested()) {
+            mDeferScroll = offset;
+            return false;
+        }
+        boolean changed = false;
+
+        Layout layout = isShowingHint() ? mHintLayout : mLayout;
+
+        if (layout == null) return changed;
+
+        int line = layout.getLineForOffset(offset);
+
+        int grav;
+
+        switch (layout.getParagraphAlignment(line)) {
+            case ALIGN_LEFT:
+                grav = 1;
+                break;
+            case ALIGN_RIGHT:
+                grav = -1;
+                break;
+            case ALIGN_NORMAL:
+                grav = layout.getParagraphDirection(line);
+                break;
+            case ALIGN_OPPOSITE:
+                grav = -layout.getParagraphDirection(line);
+                break;
+            case ALIGN_CENTER:
+            default:
+                grav = 0;
+                break;
+        }
+
+        // We only want to clamp the cursor to fit within the layout width
+        // in left-to-right modes, because in a right to left alignment,
+        // we want to scroll to keep the line-right on the screen, as other
+        // lines are likely to have text flush with the right margin, which
+        // we want to keep visible.
+        // A better long-term solution would probably be to measure both
+        // the full line and a blank-trimmed version, and, for example, use
+        // the latter measurement for centering and right alignment, but for
+        // the time being we only implement the cursor clamping in left to
+        // right where it is most likely to be annoying.
+        final boolean clamped = grav > 0;
+        // FIXME: Is it okay to truncate this, or should we round?
+        final int x = (int) layout.getPrimaryHorizontal(offset, clamped);
+        final int top = layout.getLineTop(line);
+        final int bottom = layout.getLineTop(line + 1);
+
+        int left = (int) Math.floor(layout.getLineLeft(line));
+        int right = (int) Math.ceil(layout.getLineRight(line));
+        int ht = layout.getHeight();
+
+        int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+        int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+        if (!mHorizontallyScrolling && right - left > hspace && right > x) {
+            // If cursor has been clamped, make sure we don't scroll.
+            right = Math.max(x, left + hspace);
+        }
+
+        int hslack = (bottom - top) / 2;
+        int vslack = hslack;
+
+        if (vslack > vspace / 4) {
+            vslack = vspace / 4;
+        }
+        if (hslack > hspace / 4) {
+            hslack = hspace / 4;
+        }
+
+        int hs = mScrollX;
+        int vs = mScrollY;
+
+        if (top - vs < vslack) {
+            vs = top - vslack;
+        }
+        if (bottom - vs > vspace - vslack) {
+            vs = bottom - (vspace - vslack);
+        }
+        if (ht - vs < vspace) {
+            vs = ht - vspace;
+        }
+        if (0 - vs > 0) {
+            vs = 0;
+        }
+
+        if (grav != 0) {
+            if (x - hs < hslack) {
+                hs = x - hslack;
+            }
+            if (x - hs > hspace - hslack) {
+                hs = x - (hspace - hslack);
+            }
+        }
+
+        if (grav < 0) {
+            if (left - hs > 0) {
+                hs = left;
+            }
+            if (right - hs < hspace) {
+                hs = right - hspace;
+            }
+        } else if (grav > 0) {
+            if (right - hs < hspace) {
+                hs = right - hspace;
+            }
+            if (left - hs > 0) {
+                hs = left;
+            }
+        } else /* grav == 0 */ {
+            if (right - left <= hspace) {
+                /*
+                 * If the entire text fits, center it exactly.
+                 */
+                hs = left - (hspace - (right - left)) / 2;
+            } else if (x > right - hslack) {
+                /*
+                 * If we are near the right edge, keep the right edge
+                 * at the edge of the view.
+                 */
+                hs = right - hspace;
+            } else if (x < left + hslack) {
+                /*
+                 * If we are near the left edge, keep the left edge
+                 * at the edge of the view.
+                 */
+                hs = left;
+            } else if (left > hs) {
+                /*
+                 * Is there whitespace visible at the left?  Fix it if so.
+                 */
+                hs = left;
+            } else if (right < hs + hspace) {
+                /*
+                 * Is there whitespace visible at the right?  Fix it if so.
+                 */
+                hs = right - hspace;
+            } else {
+                /*
+                 * Otherwise, float as needed.
+                 */
+                if (x - hs < hslack) {
+                    hs = x - hslack;
+                }
+                if (x - hs > hspace - hslack) {
+                    hs = x - (hspace - hslack);
+                }
+            }
+        }
+
+        if (hs != mScrollX || vs != mScrollY) {
+            if (mScroller == null) {
+                scrollTo(hs, vs);
+            } else {
+                long duration = AnimationUtils.currentAnimationTimeMillis() - mLastScroll;
+                int dx = hs - mScrollX;
+                int dy = vs - mScrollY;
+
+                if (duration > ANIMATED_SCROLL_GAP) {
+                    mScroller.startScroll(mScrollX, mScrollY, dx, dy);
+                    awakenScrollBars(mScroller.getDuration());
+                    invalidate();
+                } else {
+                    if (!mScroller.isFinished()) {
+                        mScroller.abortAnimation();
+                    }
+
+                    scrollBy(dx, dy);
+                }
+
+                mLastScroll = AnimationUtils.currentAnimationTimeMillis();
+            }
+
+            changed = true;
+        }
+
+        if (isFocused()) {
+            // This offsets because getInterestingRect() is in terms of viewport coordinates, but
+            // requestRectangleOnScreen() is in terms of content coordinates.
+
+            // The offsets here are to ensure the rectangle we are using is
+            // within our view bounds, in case the cursor is on the far left
+            // or right.  If it isn't withing the bounds, then this request
+            // will be ignored.
+            if (mTempRect == null) mTempRect = new Rect();
+            mTempRect.set(x - 2, top, x + 2, bottom);
+            getInterestingRect(mTempRect, line);
+            mTempRect.offset(mScrollX, mScrollY);
+
+            if (requestRectangleOnScreen(mTempRect)) {
+                changed = true;
+            }
+        }
+
+        return changed;
+    }
+
+    /**
+     * Move the cursor, if needed, so that it is at an offset that is visible
+     * to the user.  This will not move the cursor if it represents more than
+     * one character (a selection range).  This will only work if the
+     * TextView contains spannable text; otherwise it will do nothing.
+     *
+     * @return True if the cursor was actually moved, false otherwise.
+     */
+    public boolean moveCursorToVisibleOffset() {
+        if (!(mText instanceof Spannable)) {
+            return false;
+        }
+        int start = getSelectionStart();
+        int end = getSelectionEnd();
+        if (start != end) {
+            return false;
+        }
+
+        // First: make sure the line is visible on screen:
+
+        int line = mLayout.getLineForOffset(start);
+
+        final int top = mLayout.getLineTop(line);
+        final int bottom = mLayout.getLineTop(line + 1);
+        final int vspace = mBottom - mTop - getExtendedPaddingTop() - getExtendedPaddingBottom();
+        int vslack = (bottom - top) / 2;
+        if (vslack > vspace / 4) {
+            vslack = vspace / 4;
+        }
+        final int vs = mScrollY;
+
+        if (top < (vs + vslack)) {
+            line = mLayout.getLineForVertical(vs + vslack + (bottom - top));
+        } else if (bottom > (vspace + vs - vslack)) {
+            line = mLayout.getLineForVertical(vspace + vs - vslack - (bottom - top));
+        }
+
+        // Next: make sure the character is visible on screen:
+
+        final int hspace = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+        final int hs = mScrollX;
+        final int leftChar = mLayout.getOffsetForHorizontal(line, hs);
+        final int rightChar = mLayout.getOffsetForHorizontal(line, hspace + hs);
+
+        // line might contain bidirectional text
+        final int lowChar = leftChar < rightChar ? leftChar : rightChar;
+        final int highChar = leftChar > rightChar ? leftChar : rightChar;
+
+        int newStart = start;
+        if (newStart < lowChar) {
+            newStart = lowChar;
+        } else if (newStart > highChar) {
+            newStart = highChar;
+        }
+
+        if (newStart != start) {
+            Selection.setSelection((Spannable) mText, newStart);
+            return true;
+        }
+
+        return false;
+    }
+
+    @Override
+    public void computeScroll() {
+        if (mScroller != null) {
+            if (mScroller.computeScrollOffset()) {
+                mScrollX = mScroller.getCurrX();
+                mScrollY = mScroller.getCurrY();
+                invalidateParentCaches();
+                postInvalidate();  // So we draw again
+            }
+        }
+    }
+
+    private void getInterestingRect(Rect r, int line) {
+        convertFromViewportToContentCoordinates(r);
+
+        // Rectangle can can be expanded on first and last line to take
+        // padding into account.
+        // TODO Take left/right padding into account too?
+        if (line == 0) r.top -= getExtendedPaddingTop();
+        if (line == mLayout.getLineCount() - 1) r.bottom += getExtendedPaddingBottom();
+    }
+
+    private void convertFromViewportToContentCoordinates(Rect r) {
+        final int horizontalOffset = viewportToContentHorizontalOffset();
+        r.left += horizontalOffset;
+        r.right += horizontalOffset;
+
+        final int verticalOffset = viewportToContentVerticalOffset();
+        r.top += verticalOffset;
+        r.bottom += verticalOffset;
+    }
+
+    int viewportToContentHorizontalOffset() {
+        return getCompoundPaddingLeft() - mScrollX;
+    }
+
+    int viewportToContentVerticalOffset() {
+        int offset = getExtendedPaddingTop() - mScrollY;
+        if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
+            offset += getVerticalOffset(false);
+        }
+        return offset;
+    }
+
+    @Override
+    public void debug(int depth) {
+        super.debug(depth);
+
+        String output = debugIndent(depth);
+        output += "frame={" + mLeft + ", " + mTop + ", " + mRight
+                + ", " + mBottom + "} scroll={" + mScrollX + ", " + mScrollY
+                + "} ";
+
+        if (mText != null) {
+
+            output += "mText=\"" + mText + "\" ";
+            if (mLayout != null) {
+                output += "mLayout width=" + mLayout.getWidth()
+                        + " height=" + mLayout.getHeight();
+            }
+        } else {
+            output += "mText=NULL";
+        }
+        Log.d(VIEW_LOG_TAG, output);
+    }
+
+    /**
+     * Convenience for {@link Selection#getSelectionStart}.
+     */
+    @ViewDebug.ExportedProperty(category = "text")
+    public int getSelectionStart() {
+        return Selection.getSelectionStart(getText());
+    }
+
+    /**
+     * Convenience for {@link Selection#getSelectionEnd}.
+     */
+    @ViewDebug.ExportedProperty(category = "text")
+    public int getSelectionEnd() {
+        return Selection.getSelectionEnd(getText());
+    }
+
+    /**
+     * Return true iff there is a selection inside this text view.
+     */
+    public boolean hasSelection() {
+        final int selectionStart = getSelectionStart();
+        final int selectionEnd = getSelectionEnd();
+
+        return selectionStart >= 0 && selectionStart != selectionEnd;
+    }
+
+    String getSelectedText() {
+        if (!hasSelection()) {
+            return null;
+        }
+
+        final int start = getSelectionStart();
+        final int end = getSelectionEnd();
+        return String.valueOf(
+                start > end ? mText.subSequence(end, start) : mText.subSequence(start, end));
+    }
+
+    /**
+     * Sets the properties of this field (lines, horizontally scrolling,
+     * transformation method) to be for a single-line input.
+     *
+     * @attr ref android.R.styleable#TextView_singleLine
+     */
+    public void setSingleLine() {
+        setSingleLine(true);
+    }
+
+    /**
+     * Sets the properties of this field to transform input to ALL CAPS
+     * display. This may use a "small caps" formatting if available.
+     * This setting will be ignored if this field is editable or selectable.
+     *
+     * This call replaces the current transformation method. Disabling this
+     * will not necessarily restore the previous behavior from before this
+     * was enabled.
+     *
+     * @see #setTransformationMethod(TransformationMethod)
+     * @attr ref android.R.styleable#TextView_textAllCaps
+     */
+    public void setAllCaps(boolean allCaps) {
+        if (allCaps) {
+            setTransformationMethod(new AllCapsTransformationMethod(getContext()));
+        } else {
+            setTransformationMethod(null);
+        }
+    }
+
+    /**
+     *
+     * Checks whether the transformation method applied to this TextView is set to ALL CAPS. This
+     * settings is internally ignored if this field is editable or selectable.
+     * @return Whether the current transformation method is for ALL CAPS.
+     *
+     * @see #setAllCaps(boolean)
+     * @see #setTransformationMethod(TransformationMethod)
+     */
+    public boolean isAllCaps() {
+        final TransformationMethod method = getTransformationMethod();
+        return method != null && method instanceof AllCapsTransformationMethod;
+    }
+
+    /**
+     * If true, sets the properties of this field (number of lines, horizontally scrolling,
+     * transformation method) to be for a single-line input; if false, restores these to the default
+     * conditions.
+     *
+     * Note that the default conditions are not necessarily those that were in effect prior this
+     * method, and you may want to reset these properties to your custom values.
+     *
+     * @attr ref android.R.styleable#TextView_singleLine
+     */
+    @android.view.RemotableViewMethod
+    public void setSingleLine(boolean singleLine) {
+        // Could be used, but may break backward compatibility.
+        // if (mSingleLine == singleLine) return;
+        setInputTypeSingleLine(singleLine);
+        applySingleLine(singleLine, true, true);
+    }
+
+    /**
+     * Adds or remove the EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE on the mInputType.
+     * @param singleLine
+     */
+    private void setInputTypeSingleLine(boolean singleLine) {
+        if (mEditor != null
+                && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+                        == EditorInfo.TYPE_CLASS_TEXT) {
+            if (singleLine) {
+                mEditor.mInputType &= ~EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+            } else {
+                mEditor.mInputType |= EditorInfo.TYPE_TEXT_FLAG_MULTI_LINE;
+            }
+        }
+    }
+
+    private void applySingleLine(boolean singleLine, boolean applyTransformation,
+            boolean changeMaxLines) {
+        mSingleLine = singleLine;
+        if (singleLine) {
+            setLines(1);
+            setHorizontallyScrolling(true);
+            if (applyTransformation) {
+                setTransformationMethod(SingleLineTransformationMethod.getInstance());
+            }
+        } else {
+            if (changeMaxLines) {
+                setMaxLines(Integer.MAX_VALUE);
+            }
+            setHorizontallyScrolling(false);
+            if (applyTransformation) {
+                setTransformationMethod(null);
+            }
+        }
+    }
+
+    /**
+     * Causes words in the text that are longer than the view's width
+     * to be ellipsized instead of broken in the middle.  You may also
+     * want to {@link #setSingleLine} or {@link #setHorizontallyScrolling}
+     * to constrain the text to a single line.  Use <code>null</code>
+     * to turn off ellipsizing.
+     *
+     * If {@link #setMaxLines} has been used to set two or more lines,
+     * only {@link android.text.TextUtils.TruncateAt#END} and
+     * {@link android.text.TextUtils.TruncateAt#MARQUEE} are supported
+     * (other ellipsizing types will not do anything).
+     *
+     * @attr ref android.R.styleable#TextView_ellipsize
+     */
+    public void setEllipsize(TextUtils.TruncateAt where) {
+        // TruncateAt is an enum. != comparison is ok between these singleton objects.
+        if (mEllipsize != where) {
+            mEllipsize = where;
+
+            if (mLayout != null) {
+                nullLayouts();
+                requestLayout();
+                invalidate();
+            }
+        }
+    }
+
+    /**
+     * Sets how many times to repeat the marquee animation. Only applied if the
+     * TextView has marquee enabled. Set to -1 to repeat indefinitely.
+     *
+     * @see #getMarqueeRepeatLimit()
+     *
+     * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+     */
+    public void setMarqueeRepeatLimit(int marqueeLimit) {
+        mMarqueeRepeatLimit = marqueeLimit;
+    }
+
+    /**
+     * Gets the number of times the marquee animation is repeated. Only meaningful if the
+     * TextView has marquee enabled.
+     *
+     * @return the number of times the marquee animation is repeated. -1 if the animation
+     * repeats indefinitely
+     *
+     * @see #setMarqueeRepeatLimit(int)
+     *
+     * @attr ref android.R.styleable#TextView_marqueeRepeatLimit
+     */
+    public int getMarqueeRepeatLimit() {
+        return mMarqueeRepeatLimit;
+    }
+
+    /**
+     * Returns where, if anywhere, words that are longer than the view
+     * is wide should be ellipsized.
+     */
+    @ViewDebug.ExportedProperty
+    public TextUtils.TruncateAt getEllipsize() {
+        return mEllipsize;
+    }
+
+    /**
+     * Set the TextView so that when it takes focus, all the text is
+     * selected.
+     *
+     * @attr ref android.R.styleable#TextView_selectAllOnFocus
+     */
+    @android.view.RemotableViewMethod
+    public void setSelectAllOnFocus(boolean selectAllOnFocus) {
+        createEditorIfNeeded();
+        mEditor.mSelectAllOnFocus = selectAllOnFocus;
+
+        if (selectAllOnFocus && !(mText instanceof Spannable)) {
+            setText(mText, BufferType.SPANNABLE);
+        }
+    }
+
+    /**
+     * Set whether the cursor is visible. The default is true. Note that this property only
+     * makes sense for editable TextView.
+     *
+     * @see #isCursorVisible()
+     *
+     * @attr ref android.R.styleable#TextView_cursorVisible
+     */
+    @android.view.RemotableViewMethod
+    public void setCursorVisible(boolean visible) {
+        if (visible && mEditor == null) return; // visible is the default value with no edit data
+        createEditorIfNeeded();
+        if (mEditor.mCursorVisible != visible) {
+            mEditor.mCursorVisible = visible;
+            invalidate();
+
+            mEditor.makeBlink();
+
+            // InsertionPointCursorController depends on mCursorVisible
+            mEditor.prepareCursorControllers();
+        }
+    }
+
+    /**
+     * @return whether or not the cursor is visible (assuming this TextView is editable)
+     *
+     * @see #setCursorVisible(boolean)
+     *
+     * @attr ref android.R.styleable#TextView_cursorVisible
+     */
+    public boolean isCursorVisible() {
+        // true is the default value
+        return mEditor == null ? true : mEditor.mCursorVisible;
+    }
+
+    private boolean canMarquee() {
+        int width = mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight();
+        return width > 0 && (mLayout.getLineWidth(0) > width
+                || (mMarqueeFadeMode != MARQUEE_FADE_NORMAL && mSavedMarqueeModeLayout != null
+                        && mSavedMarqueeModeLayout.getLineWidth(0) > width));
+    }
+
+    private void startMarquee() {
+        // Do not ellipsize EditText
+        if (getKeyListener() != null) return;
+
+        if (compressText(getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight())) {
+            return;
+        }
+
+        if ((mMarquee == null || mMarquee.isStopped()) && (isFocused() || isSelected())
+                && getLineCount() == 1 && canMarquee()) {
+
+            if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS) {
+                mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_FADE;
+                final Layout tmp = mLayout;
+                mLayout = mSavedMarqueeModeLayout;
+                mSavedMarqueeModeLayout = tmp;
+                setHorizontalFadingEdgeEnabled(true);
+                requestLayout();
+                invalidate();
+            }
+
+            if (mMarquee == null) mMarquee = new Marquee(this);
+            mMarquee.start(mMarqueeRepeatLimit);
+        }
+    }
+
+    private void stopMarquee() {
+        if (mMarquee != null && !mMarquee.isStopped()) {
+            mMarquee.stop();
+        }
+
+        if (mMarqueeFadeMode == MARQUEE_FADE_SWITCH_SHOW_FADE) {
+            mMarqueeFadeMode = MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+            final Layout tmp = mSavedMarqueeModeLayout;
+            mSavedMarqueeModeLayout = mLayout;
+            mLayout = tmp;
+            setHorizontalFadingEdgeEnabled(false);
+            requestLayout();
+            invalidate();
+        }
+    }
+
+    private void startStopMarquee(boolean start) {
+        if (mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+            if (start) {
+                startMarquee();
+            } else {
+                stopMarquee();
+            }
+        }
+    }
+
+    /**
+     * This method is called when the text is changed, in case any subclasses
+     * would like to know.
+     *
+     * Within <code>text</code>, the <code>lengthAfter</code> characters
+     * beginning at <code>start</code> have just replaced old text that had
+     * length <code>lengthBefore</code>. It is an error to attempt to make
+     * changes to <code>text</code> from this callback.
+     *
+     * @param text The text the TextView is displaying
+     * @param start The offset of the start of the range of the text that was
+     * modified
+     * @param lengthBefore The length of the former text that has been replaced
+     * @param lengthAfter The length of the replacement modified text
+     */
+    protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
+        // intentionally empty, template pattern method can be overridden by subclasses
+    }
+
+    /**
+     * This method is called when the selection has changed, in case any
+     * subclasses would like to know.
+     *
+     * @param selStart The new selection start location.
+     * @param selEnd The new selection end location.
+     */
+    protected void onSelectionChanged(int selStart, int selEnd) {
+        sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED);
+    }
+
+    /**
+     * Adds a TextWatcher to the list of those whose methods are called
+     * whenever this TextView's text changes.
+     * <p>
+     * In 1.0, the {@link TextWatcher#afterTextChanged} method was erroneously
+     * not called after {@link #setText} calls.  Now, doing {@link #setText}
+     * if there are any text changed listeners forces the buffer type to
+     * Editable if it would not otherwise be and does call this method.
+     */
+    public void addTextChangedListener(TextWatcher watcher) {
+        if (mListeners == null) {
+            mListeners = new ArrayList<TextWatcher>();
+        }
+
+        mListeners.add(watcher);
+    }
+
+    /**
+     * Removes the specified TextWatcher from the list of those whose
+     * methods are called
+     * whenever this TextView's text changes.
+     */
+    public void removeTextChangedListener(TextWatcher watcher) {
+        if (mListeners != null) {
+            int i = mListeners.indexOf(watcher);
+
+            if (i >= 0) {
+                mListeners.remove(i);
+            }
+        }
+    }
+
+    private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) {
+        if (mListeners != null) {
+            final ArrayList<TextWatcher> list = mListeners;
+            final int count = list.size();
+            for (int i = 0; i < count; i++) {
+                list.get(i).beforeTextChanged(text, start, before, after);
+            }
+        }
+
+        // The spans that are inside or intersect the modified region no longer make sense
+        removeIntersectingNonAdjacentSpans(start, start + before, SpellCheckSpan.class);
+        removeIntersectingNonAdjacentSpans(start, start + before, SuggestionSpan.class);
+    }
+
+    // Removes all spans that are inside or actually overlap the start..end range
+    private <T> void removeIntersectingNonAdjacentSpans(int start, int end, Class<T> type) {
+        if (!(mText instanceof Editable)) return;
+        Editable text = (Editable) mText;
+
+        T[] spans = text.getSpans(start, end, type);
+        final int length = spans.length;
+        for (int i = 0; i < length; i++) {
+            final int spanStart = text.getSpanStart(spans[i]);
+            final int spanEnd = text.getSpanEnd(spans[i]);
+            if (spanEnd == start || spanStart == end) break;
+            text.removeSpan(spans[i]);
+        }
+    }
+
+    void removeAdjacentSuggestionSpans(final int pos) {
+        if (!(mText instanceof Editable)) return;
+        final Editable text = (Editable) mText;
+
+        final SuggestionSpan[] spans = text.getSpans(pos, pos, SuggestionSpan.class);
+        final int length = spans.length;
+        for (int i = 0; i < length; i++) {
+            final int spanStart = text.getSpanStart(spans[i]);
+            final int spanEnd = text.getSpanEnd(spans[i]);
+            if (spanEnd == pos || spanStart == pos) {
+                if (SpellChecker.haveWordBoundariesChanged(text, pos, pos, spanStart, spanEnd)) {
+                    text.removeSpan(spans[i]);
+                }
+            }
+        }
+    }
+
+    /**
+     * Not private so it can be called from an inner class without going
+     * through a thunk.
+     */
+    void sendOnTextChanged(CharSequence text, int start, int before, int after) {
+        if (mListeners != null) {
+            final ArrayList<TextWatcher> list = mListeners;
+            final int count = list.size();
+            for (int i = 0; i < count; i++) {
+                list.get(i).onTextChanged(text, start, before, after);
+            }
+        }
+
+        if (mEditor != null) mEditor.sendOnTextChanged(start, before, after);
+    }
+
+    /**
+     * Not private so it can be called from an inner class without going
+     * through a thunk.
+     */
+    void sendAfterTextChanged(Editable text) {
+        if (mListeners != null) {
+            final ArrayList<TextWatcher> list = mListeners;
+            final int count = list.size();
+            for (int i = 0; i < count; i++) {
+                list.get(i).afterTextChanged(text);
+            }
+        }
+
+        // Always notify AutoFillManager - it will return right away if autofill is disabled.
+        notifyAutoFillManagerAfterTextChangedIfNeeded();
+
+        hideErrorIfUnchanged();
+    }
+
+    private void notifyAutoFillManagerAfterTextChangedIfNeeded() {
+        // It is important to not check whether the view is important for autofill
+        // since the user can trigger autofill manually on not important views.
+        if (!isAutofillable()) {
+            return;
+        }
+        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+        if (afm != null) {
+            if (DEBUG_AUTOFILL) {
+                Log.v(LOG_TAG, "sendAfterTextChanged(): notify AFM for text=" + mText);
+            }
+            afm.notifyValueChanged(TextView.this);
+        }
+    }
+
+    private boolean isAutofillable() {
+        // It is important to not check whether the view is important for autofill
+        // since the user can trigger autofill manually on not important views.
+        return getAutofillType() != AUTOFILL_TYPE_NONE;
+    }
+
+    void updateAfterEdit() {
+        invalidate();
+        int curs = getSelectionStart();
+
+        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+            registerForPreDraw();
+        }
+
+        checkForResize();
+
+        if (curs >= 0) {
+            mHighlightPathBogus = true;
+            if (mEditor != null) mEditor.makeBlink();
+            bringPointIntoView(curs);
+        }
+    }
+
+    /**
+     * Not private so it can be called from an inner class without going
+     * through a thunk.
+     */
+    void handleTextChanged(CharSequence buffer, int start, int before, int after) {
+        sLastCutCopyOrTextChangedTime = 0;
+
+        final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
+        if (ims == null || ims.mBatchEditNesting == 0) {
+            updateAfterEdit();
+        }
+        if (ims != null) {
+            ims.mContentChanged = true;
+            if (ims.mChangedStart < 0) {
+                ims.mChangedStart = start;
+                ims.mChangedEnd = start + before;
+            } else {
+                ims.mChangedStart = Math.min(ims.mChangedStart, start);
+                ims.mChangedEnd = Math.max(ims.mChangedEnd, start + before - ims.mChangedDelta);
+            }
+            ims.mChangedDelta += after - before;
+        }
+        resetErrorChangedFlag();
+        sendOnTextChanged(buffer, start, before, after);
+        onTextChanged(buffer, start, before, after);
+    }
+
+    /**
+     * Not private so it can be called from an inner class without going
+     * through a thunk.
+     */
+    void spanChange(Spanned buf, Object what, int oldStart, int newStart, int oldEnd, int newEnd) {
+        // XXX Make the start and end move together if this ends up
+        // spending too much time invalidating.
+
+        boolean selChanged = false;
+        int newSelStart = -1, newSelEnd = -1;
+
+        final Editor.InputMethodState ims = mEditor == null ? null : mEditor.mInputMethodState;
+
+        if (what == Selection.SELECTION_END) {
+            selChanged = true;
+            newSelEnd = newStart;
+
+            if (oldStart >= 0 || newStart >= 0) {
+                invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
+                checkForResize();
+                registerForPreDraw();
+                if (mEditor != null) mEditor.makeBlink();
+            }
+        }
+
+        if (what == Selection.SELECTION_START) {
+            selChanged = true;
+            newSelStart = newStart;
+
+            if (oldStart >= 0 || newStart >= 0) {
+                int end = Selection.getSelectionEnd(buf);
+                invalidateCursor(end, oldStart, newStart);
+            }
+        }
+
+        if (selChanged) {
+            mHighlightPathBogus = true;
+            if (mEditor != null && !isFocused()) mEditor.mSelectionMoved = true;
+
+            if ((buf.getSpanFlags(what) & Spanned.SPAN_INTERMEDIATE) == 0) {
+                if (newSelStart < 0) {
+                    newSelStart = Selection.getSelectionStart(buf);
+                }
+                if (newSelEnd < 0) {
+                    newSelEnd = Selection.getSelectionEnd(buf);
+                }
+
+                if (mEditor != null) {
+                    mEditor.refreshTextActionMode();
+                    if (!hasSelection()
+                            && mEditor.getTextActionMode() == null && hasTransientState()) {
+                        // User generated selection has been removed.
+                        setHasTransientState(false);
+                    }
+                }
+                onSelectionChanged(newSelStart, newSelEnd);
+            }
+        }
+
+        if (what instanceof UpdateAppearance || what instanceof ParagraphStyle
+                || what instanceof CharacterStyle) {
+            if (ims == null || ims.mBatchEditNesting == 0) {
+                invalidate();
+                mHighlightPathBogus = true;
+                checkForResize();
+            } else {
+                ims.mContentChanged = true;
+            }
+            if (mEditor != null) {
+                if (oldStart >= 0) mEditor.invalidateTextDisplayList(mLayout, oldStart, oldEnd);
+                if (newStart >= 0) mEditor.invalidateTextDisplayList(mLayout, newStart, newEnd);
+                mEditor.invalidateHandlesAndActionMode();
+            }
+        }
+
+        if (MetaKeyKeyListener.isMetaTracker(buf, what)) {
+            mHighlightPathBogus = true;
+            if (ims != null && MetaKeyKeyListener.isSelectingMetaTracker(buf, what)) {
+                ims.mSelectionModeChanged = true;
+            }
+
+            if (Selection.getSelectionStart(buf) >= 0) {
+                if (ims == null || ims.mBatchEditNesting == 0) {
+                    invalidateCursor();
+                } else {
+                    ims.mCursorChanged = true;
+                }
+            }
+        }
+
+        if (what instanceof ParcelableSpan) {
+            // If this is a span that can be sent to a remote process,
+            // the current extract editor would be interested in it.
+            if (ims != null && ims.mExtractedTextRequest != null) {
+                if (ims.mBatchEditNesting != 0) {
+                    if (oldStart >= 0) {
+                        if (ims.mChangedStart > oldStart) {
+                            ims.mChangedStart = oldStart;
+                        }
+                        if (ims.mChangedStart > oldEnd) {
+                            ims.mChangedStart = oldEnd;
+                        }
+                    }
+                    if (newStart >= 0) {
+                        if (ims.mChangedStart > newStart) {
+                            ims.mChangedStart = newStart;
+                        }
+                        if (ims.mChangedStart > newEnd) {
+                            ims.mChangedStart = newEnd;
+                        }
+                    }
+                } else {
+                    if (DEBUG_EXTRACT) {
+                        Log.v(LOG_TAG, "Span change outside of batch: "
+                                + oldStart + "-" + oldEnd + ","
+                                + newStart + "-" + newEnd + " " + what);
+                    }
+                    ims.mContentChanged = true;
+                }
+            }
+        }
+
+        if (mEditor != null && mEditor.mSpellChecker != null && newStart < 0
+                && what instanceof SpellCheckSpan) {
+            mEditor.mSpellChecker.onSpellCheckSpanRemoved((SpellCheckSpan) what);
+        }
+    }
+
+    @Override
+    protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
+        if (isTemporarilyDetached()) {
+            // If we are temporarily in the detach state, then do nothing.
+            super.onFocusChanged(focused, direction, previouslyFocusedRect);
+            return;
+        }
+
+        if (mEditor != null) mEditor.onFocusChanged(focused, direction);
+
+        if (focused) {
+            if (mText instanceof Spannable) {
+                Spannable sp = (Spannable) mText;
+                MetaKeyKeyListener.resetMetaState(sp);
+            }
+        }
+
+        startStopMarquee(focused);
+
+        if (mTransformation != null) {
+            mTransformation.onFocusChanged(this, mText, focused, direction, previouslyFocusedRect);
+        }
+
+        super.onFocusChanged(focused, direction, previouslyFocusedRect);
+    }
+
+    @Override
+    public void onWindowFocusChanged(boolean hasWindowFocus) {
+        super.onWindowFocusChanged(hasWindowFocus);
+
+        if (mEditor != null) mEditor.onWindowFocusChanged(hasWindowFocus);
+
+        startStopMarquee(hasWindowFocus);
+    }
+
+    @Override
+    protected void onVisibilityChanged(View changedView, int visibility) {
+        super.onVisibilityChanged(changedView, visibility);
+        if (mEditor != null && visibility != VISIBLE) {
+            mEditor.hideCursorAndSpanControllers();
+            stopTextActionMode();
+        }
+    }
+
+    /**
+     * Use {@link BaseInputConnection#removeComposingSpans
+     * BaseInputConnection.removeComposingSpans()} to remove any IME composing
+     * state from this text view.
+     */
+    public void clearComposingText() {
+        if (mText instanceof Spannable) {
+            BaseInputConnection.removeComposingSpans((Spannable) mText);
+        }
+    }
+
+    @Override
+    public void setSelected(boolean selected) {
+        boolean wasSelected = isSelected();
+
+        super.setSelected(selected);
+
+        if (selected != wasSelected && mEllipsize == TextUtils.TruncateAt.MARQUEE) {
+            if (selected) {
+                startMarquee();
+            } else {
+                stopMarquee();
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        final int action = event.getActionMasked();
+        if (mEditor != null) {
+            mEditor.onTouchEvent(event);
+
+            if (mEditor.mSelectionModifierCursorController != null
+                    && mEditor.mSelectionModifierCursorController.isDragAcceleratorActive()) {
+                return true;
+            }
+        }
+
+        final boolean superResult = super.onTouchEvent(event);
+
+        /*
+         * Don't handle the release after a long press, because it will move the selection away from
+         * whatever the menu action was trying to affect. If the long press should have triggered an
+         * insertion action mode, we can now actually show it.
+         */
+        if (mEditor != null && mEditor.mDiscardNextActionUp && action == MotionEvent.ACTION_UP) {
+            mEditor.mDiscardNextActionUp = false;
+
+            if (mEditor.mIsInsertionActionModeStartPending) {
+                mEditor.startInsertionActionMode();
+                mEditor.mIsInsertionActionModeStartPending = false;
+            }
+            return superResult;
+        }
+
+        final boolean touchIsFinished = (action == MotionEvent.ACTION_UP)
+                && (mEditor == null || !mEditor.mIgnoreActionUpEvent) && isFocused();
+
+        if ((mMovement != null || onCheckIsTextEditor()) && isEnabled()
+                && mText instanceof Spannable && mLayout != null) {
+            boolean handled = false;
+
+            if (mMovement != null) {
+                handled |= mMovement.onTouchEvent(this, (Spannable) mText, event);
+            }
+
+            final boolean textIsSelectable = isTextSelectable();
+            if (touchIsFinished && mLinksClickable && mAutoLinkMask != 0 && textIsSelectable) {
+                // The LinkMovementMethod which should handle taps on links has not been installed
+                // on non editable text that support text selection.
+                // We reproduce its behavior here to open links for these.
+                ClickableSpan[] links = ((Spannable) mText).getSpans(getSelectionStart(),
+                    getSelectionEnd(), ClickableSpan.class);
+
+                if (links.length > 0) {
+                    links[0].onClick(this);
+                    handled = true;
+                }
+            }
+
+            if (touchIsFinished && (isTextEditable() || textIsSelectable)) {
+                // Show the IME, except when selecting in read-only text.
+                final InputMethodManager imm = InputMethodManager.peekInstance();
+                viewClicked(imm);
+                if (isTextEditable() && mEditor.mShowSoftInputOnFocus && imm != null) {
+                    imm.showSoftInput(this, 0);
+                }
+
+                // The above condition ensures that the mEditor is not null
+                mEditor.onTouchUpEvent(event);
+
+                handled = true;
+            }
+
+            if (handled) {
+                return true;
+            }
+        }
+
+        return superResult;
+    }
+
+    @Override
+    public boolean onGenericMotionEvent(MotionEvent event) {
+        if (mMovement != null && mText instanceof Spannable && mLayout != null) {
+            try {
+                if (mMovement.onGenericMotionEvent(this, (Spannable) mText, event)) {
+                    return true;
+                }
+            } catch (AbstractMethodError ex) {
+                // onGenericMotionEvent was added to the MovementMethod interface in API 12.
+                // Ignore its absence in case third party applications implemented the
+                // interface directly.
+            }
+        }
+        return super.onGenericMotionEvent(event);
+    }
+
+    @Override
+    protected void onCreateContextMenu(ContextMenu menu) {
+        if (mEditor != null) {
+            mEditor.onCreateContextMenu(menu);
+        }
+    }
+
+    @Override
+    public boolean showContextMenu() {
+        if (mEditor != null) {
+            mEditor.setContextMenuAnchor(Float.NaN, Float.NaN);
+        }
+        return super.showContextMenu();
+    }
+
+    @Override
+    public boolean showContextMenu(float x, float y) {
+        if (mEditor != null) {
+            mEditor.setContextMenuAnchor(x, y);
+        }
+        return super.showContextMenu(x, y);
+    }
+
+    /**
+     * @return True iff this TextView contains a text that can be edited, or if this is
+     * a selectable TextView.
+     */
+    boolean isTextEditable() {
+        return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
+    }
+
+    /**
+     * Returns true, only while processing a touch gesture, if the initial
+     * touch down event caused focus to move to the text view and as a result
+     * its selection changed.  Only valid while processing the touch gesture
+     * of interest, in an editable text view.
+     */
+    public boolean didTouchFocusSelect() {
+        return mEditor != null && mEditor.mTouchFocusSelected;
+    }
+
+    @Override
+    public void cancelLongPress() {
+        super.cancelLongPress();
+        if (mEditor != null) mEditor.mIgnoreActionUpEvent = true;
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent event) {
+        if (mMovement != null && mText instanceof Spannable && mLayout != null) {
+            if (mMovement.onTrackballEvent(this, (Spannable) mText, event)) {
+                return true;
+            }
+        }
+
+        return super.onTrackballEvent(event);
+    }
+
+    /**
+     * Sets the Scroller used for producing a scrolling animation
+     *
+     * @param s A Scroller instance
+     */
+    public void setScroller(Scroller s) {
+        mScroller = s;
+    }
+
+    @Override
+    protected float getLeftFadingEdgeStrength() {
+        if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
+            final Marquee marquee = mMarquee;
+            if (marquee.shouldDrawLeftFade()) {
+                return getHorizontalFadingEdgeStrength(marquee.getScroll(), 0.0f);
+            } else {
+                return 0.0f;
+            }
+        } else if (getLineCount() == 1) {
+            final float lineLeft = getLayout().getLineLeft(0);
+            if (lineLeft > mScrollX) return 0.0f;
+            return getHorizontalFadingEdgeStrength(mScrollX, lineLeft);
+        }
+        return super.getLeftFadingEdgeStrength();
+    }
+
+    @Override
+    protected float getRightFadingEdgeStrength() {
+        if (isMarqueeFadeEnabled() && mMarquee != null && !mMarquee.isStopped()) {
+            final Marquee marquee = mMarquee;
+            return getHorizontalFadingEdgeStrength(marquee.getMaxFadeScroll(), marquee.getScroll());
+        } else if (getLineCount() == 1) {
+            final float rightEdge = mScrollX +
+                    (getWidth() - getCompoundPaddingLeft() - getCompoundPaddingRight());
+            final float lineRight = getLayout().getLineRight(0);
+            if (lineRight < rightEdge) return 0.0f;
+            return getHorizontalFadingEdgeStrength(rightEdge, lineRight);
+        }
+        return super.getRightFadingEdgeStrength();
+    }
+
+    /**
+     * Calculates the fading edge strength as the ratio of the distance between two
+     * horizontal positions to {@link View#getHorizontalFadingEdgeLength()}. Uses the absolute
+     * value for the distance calculation.
+     *
+     * @param position1 A horizontal position.
+     * @param position2 A horizontal position.
+     * @return Fading edge strength between [0.0f, 1.0f].
+     */
+    @FloatRange(from = 0.0, to = 1.0)
+    private float getHorizontalFadingEdgeStrength(float position1, float position2) {
+        final int horizontalFadingEdgeLength = getHorizontalFadingEdgeLength();
+        if (horizontalFadingEdgeLength == 0) return 0.0f;
+        final float diff = Math.abs(position1 - position2);
+        if (diff > horizontalFadingEdgeLength) return 1.0f;
+        return diff / horizontalFadingEdgeLength;
+    }
+
+    private boolean isMarqueeFadeEnabled() {
+        return mEllipsize == TextUtils.TruncateAt.MARQUEE
+                && mMarqueeFadeMode != MARQUEE_FADE_SWITCH_SHOW_ELLIPSIS;
+    }
+
+    @Override
+    protected int computeHorizontalScrollRange() {
+        if (mLayout != null) {
+            return mSingleLine && (mGravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.LEFT
+                    ? (int) mLayout.getLineWidth(0) : mLayout.getWidth();
+        }
+
+        return super.computeHorizontalScrollRange();
+    }
+
+    @Override
+    protected int computeVerticalScrollRange() {
+        if (mLayout != null) {
+            return mLayout.getHeight();
+        }
+        return super.computeVerticalScrollRange();
+    }
+
+    @Override
+    protected int computeVerticalScrollExtent() {
+        return getHeight() - getCompoundPaddingTop() - getCompoundPaddingBottom();
+    }
+
+    @Override
+    public void findViewsWithText(ArrayList<View> outViews, CharSequence searched, int flags) {
+        super.findViewsWithText(outViews, searched, flags);
+        if (!outViews.contains(this) && (flags & FIND_VIEWS_WITH_TEXT) != 0
+                && !TextUtils.isEmpty(searched) && !TextUtils.isEmpty(mText)) {
+            String searchedLowerCase = searched.toString().toLowerCase();
+            String textLowerCase = mText.toString().toLowerCase();
+            if (textLowerCase.contains(searchedLowerCase)) {
+                outViews.add(this);
+            }
+        }
+    }
+
+    /**
+     * Type of the text buffer that defines the characteristics of the text such as static,
+     * styleable, or editable.
+     */
+    public enum BufferType {
+        NORMAL, SPANNABLE, EDITABLE
+    }
+
+    /**
+     * Returns the TextView_textColor attribute from the TypedArray, if set, or
+     * the TextAppearance_textColor from the TextView_textAppearance attribute,
+     * if TextView_textColor was not set directly.
+     *
+     * @removed
+     */
+    public static ColorStateList getTextColors(Context context, TypedArray attrs) {
+        if (attrs == null) {
+            // Preserve behavior prior to removal of this API.
+            throw new NullPointerException();
+        }
+
+        // It's not safe to use this method from apps. The parameter 'attrs'
+        // must have been obtained using the TextView filter array which is not
+        // available to the SDK. As such, we grab a default TypedArray with the
+        // right filter instead here.
+        final TypedArray a = context.obtainStyledAttributes(R.styleable.TextView);
+        ColorStateList colors = a.getColorStateList(R.styleable.TextView_textColor);
+        if (colors == null) {
+            final int ap = a.getResourceId(R.styleable.TextView_textAppearance, 0);
+            if (ap != 0) {
+                final TypedArray appearance = context.obtainStyledAttributes(
+                        ap, R.styleable.TextAppearance);
+                colors = appearance.getColorStateList(R.styleable.TextAppearance_textColor);
+                appearance.recycle();
+            }
+        }
+        a.recycle();
+
+        return colors;
+    }
+
+    /**
+     * Returns the default color from the TextView_textColor attribute from the
+     * AttributeSet, if set, or the default color from the
+     * TextAppearance_textColor from the TextView_textAppearance attribute, if
+     * TextView_textColor was not set directly.
+     *
+     * @removed
+     */
+    public static int getTextColor(Context context, TypedArray attrs, int def) {
+        final ColorStateList colors = getTextColors(context, attrs);
+        if (colors == null) {
+            return def;
+        } else {
+            return colors.getDefaultColor();
+        }
+    }
+
+    @Override
+    public boolean onKeyShortcut(int keyCode, KeyEvent event) {
+        if (event.hasModifiers(KeyEvent.META_CTRL_ON)) {
+            // Handle Ctrl-only shortcuts.
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_A:
+                    if (canSelectText()) {
+                        return onTextContextMenuItem(ID_SELECT_ALL);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_Z:
+                    if (canUndo()) {
+                        return onTextContextMenuItem(ID_UNDO);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_X:
+                    if (canCut()) {
+                        return onTextContextMenuItem(ID_CUT);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_C:
+                    if (canCopy()) {
+                        return onTextContextMenuItem(ID_COPY);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_V:
+                    if (canPaste()) {
+                        return onTextContextMenuItem(ID_PASTE);
+                    }
+                    break;
+            }
+        } else if (event.hasModifiers(KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)) {
+            // Handle Ctrl-Shift shortcuts.
+            switch (keyCode) {
+                case KeyEvent.KEYCODE_Z:
+                    if (canRedo()) {
+                        return onTextContextMenuItem(ID_REDO);
+                    }
+                    break;
+                case KeyEvent.KEYCODE_V:
+                    if (canPaste()) {
+                        return onTextContextMenuItem(ID_PASTE_AS_PLAIN_TEXT);
+                    }
+            }
+        }
+        return super.onKeyShortcut(keyCode, event);
+    }
+
+    /**
+     * Unlike {@link #textCanBeSelected()}, this method is based on the <i>current</i> state of the
+     * TextView. {@link #textCanBeSelected()} has to be true (this is one of the conditions to have
+     * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not
+     * sufficient.
+     */
+    boolean canSelectText() {
+        return mText.length() != 0 && mEditor != null && mEditor.hasSelectionController();
+    }
+
+    /**
+     * Test based on the <i>intrinsic</i> charateristics of the TextView.
+     * The text must be spannable and the movement method must allow for arbitary selection.
+     *
+     * See also {@link #canSelectText()}.
+     */
+    boolean textCanBeSelected() {
+        // prepareCursorController() relies on this method.
+        // If you change this condition, make sure prepareCursorController is called anywhere
+        // the value of this condition might be changed.
+        if (mMovement == null || !mMovement.canSelectArbitrarily()) return false;
+        return isTextEditable()
+                || (isTextSelectable() && mText instanceof Spannable && isEnabled());
+    }
+
+    private Locale getTextServicesLocale(boolean allowNullLocale) {
+        // Start fetching the text services locale asynchronously.
+        updateTextServicesLocaleAsync();
+        // If !allowNullLocale and there is no cached text services locale, just return the default
+        // locale.
+        return (mCurrentSpellCheckerLocaleCache == null && !allowNullLocale) ? Locale.getDefault()
+                : mCurrentSpellCheckerLocaleCache;
+    }
+
+    /**
+     * This is a temporary method. Future versions may support multi-locale text.
+     * Caveat: This method may not return the latest text services locale, but this should be
+     * acceptable and it's more important to make this method asynchronous.
+     *
+     * @return The locale that should be used for a word iterator
+     * in this TextView, based on the current spell checker settings,
+     * the current IME's locale, or the system default locale.
+     * Please note that a word iterator in this TextView is different from another word iterator
+     * used by SpellChecker.java of TextView. This method should be used for the former.
+     * @hide
+     */
+    // TODO: Support multi-locale
+    // TODO: Update the text services locale immediately after the keyboard locale is switched
+    // by catching intent of keyboard switch event
+    public Locale getTextServicesLocale() {
+        return getTextServicesLocale(false /* allowNullLocale */);
+    }
+
+    /**
+     * @return {@code true} if this TextView is specialized for showing and interacting with the
+     * extracted text in a full-screen input method.
+     * @hide
+     */
+    public boolean isInExtractedMode() {
+        return false;
+    }
+
+    /**
+     * @return {@code true} if this widget supports auto-sizing text and has been configured to
+     * auto-size.
+     */
+    private boolean isAutoSizeEnabled() {
+        return supportsAutoSizeText() && mAutoSizeTextType != AUTO_SIZE_TEXT_TYPE_NONE;
+    }
+
+    /**
+     * @return {@code true} if this TextView supports auto-sizing text to fit within its container.
+     * @hide
+     */
+    protected boolean supportsAutoSizeText() {
+        return true;
+    }
+
+    /**
+     * This is a temporary method. Future versions may support multi-locale text.
+     * Caveat: This method may not return the latest spell checker locale, but this should be
+     * acceptable and it's more important to make this method asynchronous.
+     *
+     * @return The locale that should be used for a spell checker in this TextView,
+     * based on the current spell checker settings, the current IME's locale, or the system default
+     * locale.
+     * @hide
+     */
+    public Locale getSpellCheckerLocale() {
+        return getTextServicesLocale(true /* allowNullLocale */);
+    }
+
+    private void updateTextServicesLocaleAsync() {
+        // AsyncTask.execute() uses a serial executor which means we don't have
+        // to lock around updateTextServicesLocaleLocked() to prevent it from
+        // being executed n times in parallel.
+        AsyncTask.execute(new Runnable() {
+            @Override
+            public void run() {
+                updateTextServicesLocaleLocked();
+            }
+        });
+    }
+
+    private void updateTextServicesLocaleLocked() {
+        final TextServicesManager textServicesManager = (TextServicesManager)
+                mContext.getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE);
+        final SpellCheckerSubtype subtype = textServicesManager.getCurrentSpellCheckerSubtype(true);
+        final Locale locale;
+        if (subtype != null) {
+            locale = subtype.getLocaleObject();
+        } else {
+            locale = null;
+        }
+        mCurrentSpellCheckerLocaleCache = locale;
+    }
+
+    void onLocaleChanged() {
+        mEditor.onLocaleChanged();
+    }
+
+    /**
+     * This method is used by the ArrowKeyMovementMethod to jump from one word to the other.
+     * Made available to achieve a consistent behavior.
+     * @hide
+     */
+    public WordIterator getWordIterator() {
+        if (mEditor != null) {
+            return mEditor.getWordIterator();
+        } else {
+            return null;
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onPopulateAccessibilityEventInternal(event);
+
+        final CharSequence text = getTextForAccessibility();
+        if (!TextUtils.isEmpty(text)) {
+            event.getText().add(text);
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TextView.class.getName();
+    }
+
+    @Override
+    public void onProvideStructure(ViewStructure structure) {
+        super.onProvideStructure(structure);
+        onProvideAutoStructureForAssistOrAutofill(structure, false);
+    }
+
+    @Override
+    public void onProvideAutofillStructure(ViewStructure structure, int flags) {
+        super.onProvideAutofillStructure(structure, flags);
+        onProvideAutoStructureForAssistOrAutofill(structure, true);
+    }
+
+    private void onProvideAutoStructureForAssistOrAutofill(ViewStructure structure,
+            boolean forAutofill) {
+        final boolean isPassword = hasPasswordTransformationMethod()
+                || isPasswordInputType(getInputType());
+        if (forAutofill) {
+            structure.setDataIsSensitive(!mTextFromResource);
+        }
+
+        if (!isPassword || forAutofill) {
+            if (mLayout == null) {
+                assumeLayout();
+            }
+            Layout layout = mLayout;
+            final int lineCount = layout.getLineCount();
+            if (lineCount <= 1) {
+                // Simple case: this is a single line.
+                final CharSequence text = getText();
+                if (forAutofill) {
+                    structure.setText(text);
+                } else {
+                    structure.setText(text, getSelectionStart(), getSelectionEnd());
+                }
+            } else {
+                // Complex case: multi-line, could be scrolled or within a scroll container
+                // so some lines are not visible.
+                final int[] tmpCords = new int[2];
+                getLocationInWindow(tmpCords);
+                final int topWindowLocation = tmpCords[1];
+                View root = this;
+                ViewParent viewParent = getParent();
+                while (viewParent instanceof View) {
+                    root = (View) viewParent;
+                    viewParent = root.getParent();
+                }
+                final int windowHeight = root.getHeight();
+                final int topLine;
+                final int bottomLine;
+                if (topWindowLocation >= 0) {
+                    // The top of the view is fully within its window; start text at line 0.
+                    topLine = getLineAtCoordinateUnclamped(0);
+                    bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1);
+                } else {
+                    // The top of hte window has scrolled off the top of the window; figure out
+                    // the starting line for this.
+                    topLine = getLineAtCoordinateUnclamped(-topWindowLocation);
+                    bottomLine = getLineAtCoordinateUnclamped(windowHeight - 1 - topWindowLocation);
+                }
+                // We want to return some contextual lines above/below the lines that are
+                // actually visible.
+                int expandedTopLine = topLine - (bottomLine - topLine) / 2;
+                if (expandedTopLine < 0) {
+                    expandedTopLine = 0;
+                }
+                int expandedBottomLine = bottomLine + (bottomLine - topLine) / 2;
+                if (expandedBottomLine >= lineCount) {
+                    expandedBottomLine = lineCount - 1;
+                }
+
+                // Convert lines into character offsets.
+                int expandedTopChar = layout.getLineStart(expandedTopLine);
+                int expandedBottomChar = layout.getLineEnd(expandedBottomLine);
+
+                // Take into account selection -- if there is a selection, we need to expand
+                // the text we are returning to include that selection.
+                final int selStart = getSelectionStart();
+                final int selEnd = getSelectionEnd();
+                if (selStart < selEnd) {
+                    if (selStart < expandedTopChar) {
+                        expandedTopChar = selStart;
+                    }
+                    if (selEnd > expandedBottomChar) {
+                        expandedBottomChar = selEnd;
+                    }
+                }
+
+                // Get the text and trim it to the range we are reporting.
+                CharSequence text = getText();
+                if (expandedTopChar > 0 || expandedBottomChar < text.length()) {
+                    text = text.subSequence(expandedTopChar, expandedBottomChar);
+                }
+
+                if (forAutofill) {
+                    structure.setText(text);
+                } else {
+                    structure.setText(text, selStart - expandedTopChar, selEnd - expandedTopChar);
+
+                    final int[] lineOffsets = new int[bottomLine - topLine + 1];
+                    final int[] lineBaselines = new int[bottomLine - topLine + 1];
+                    final int baselineOffset = getBaselineOffset();
+                    for (int i = topLine; i <= bottomLine; i++) {
+                        lineOffsets[i - topLine] = layout.getLineStart(i);
+                        lineBaselines[i - topLine] = layout.getLineBaseline(i) + baselineOffset;
+                    }
+                    structure.setTextLines(lineOffsets, lineBaselines);
+                }
+            }
+
+            if (!forAutofill) {
+                // Extract style information that applies to the TextView as a whole.
+                int style = 0;
+                int typefaceStyle = getTypefaceStyle();
+                if ((typefaceStyle & Typeface.BOLD) != 0) {
+                    style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
+                }
+                if ((typefaceStyle & Typeface.ITALIC) != 0) {
+                    style |= AssistStructure.ViewNode.TEXT_STYLE_ITALIC;
+                }
+
+                // Global styles can also be set via TextView.setPaintFlags().
+                int paintFlags = mTextPaint.getFlags();
+                if ((paintFlags & Paint.FAKE_BOLD_TEXT_FLAG) != 0) {
+                    style |= AssistStructure.ViewNode.TEXT_STYLE_BOLD;
+                }
+                if ((paintFlags & Paint.UNDERLINE_TEXT_FLAG) != 0) {
+                    style |= AssistStructure.ViewNode.TEXT_STYLE_UNDERLINE;
+                }
+                if ((paintFlags & Paint.STRIKE_THRU_TEXT_FLAG) != 0) {
+                    style |= AssistStructure.ViewNode.TEXT_STYLE_STRIKE_THRU;
+                }
+
+                // TextView does not have its own text background color. A background is either part
+                // of the View (and can be any drawable) or a BackgroundColorSpan inside the text.
+                structure.setTextStyle(getTextSize(), getCurrentTextColor(),
+                        AssistStructure.ViewNode.TEXT_COLOR_UNDEFINED /* bgColor */, style);
+            }
+        }
+        structure.setHint(getHint());
+        structure.setInputType(getInputType());
+    }
+
+    boolean canRequestAutofill() {
+        if (!isAutofillable()) {
+            return false;
+        }
+        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+        if (afm != null) {
+            return afm.isEnabled();
+        }
+        return false;
+    }
+
+    private void requestAutofill() {
+        final AutofillManager afm = mContext.getSystemService(AutofillManager.class);
+        if (afm != null) {
+            afm.requestAutofill(this);
+        }
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!value.isText() || !isTextEditable()) {
+            Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+            return;
+        }
+
+        final CharSequence autofilledValue = value.getTextValue();
+
+        // First autofill it...
+        setText(autofilledValue, mBufferType, true, 0);
+
+        // ...then move cursor to the end.
+        final CharSequence text = getText();
+        if ((text instanceof Spannable)) {
+            Selection.setSelection((Spannable) text, text.length());
+        }
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isTextEditable() ? AUTOFILL_TYPE_TEXT : AUTOFILL_TYPE_NONE;
+    }
+
+    /**
+     * Gets the {@link TextView}'s current text for AutoFill. The value is trimmed to 100K
+     * {@code char}s if longer.
+     *
+     * @return current text, {@code null} if the text is not editable
+     *
+     * @see View#getAutofillValue()
+     */
+    @Override
+    @Nullable
+    public AutofillValue getAutofillValue() {
+        if (isTextEditable()) {
+            final CharSequence text = TextUtils.trimToParcelableSize(getText());
+            return AutofillValue.forText(text);
+        }
+        return null;
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+
+        final boolean isPassword = hasPasswordTransformationMethod();
+        event.setPassword(isPassword);
+
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED) {
+            event.setFromIndex(Selection.getSelectionStart(mText));
+            event.setToIndex(Selection.getSelectionEnd(mText));
+            event.setItemCount(mText.length());
+        }
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityNodeInfoInternal(AccessibilityNodeInfo info) {
+        super.onInitializeAccessibilityNodeInfoInternal(info);
+
+        final boolean isPassword = hasPasswordTransformationMethod();
+        info.setPassword(isPassword);
+        info.setText(getTextForAccessibility());
+        info.setHintText(mHint);
+        info.setShowingHintText(isShowingHint());
+
+        if (mBufferType == BufferType.EDITABLE) {
+            info.setEditable(true);
+            if (isEnabled()) {
+                info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_SET_TEXT);
+            }
+        }
+
+        if (mEditor != null) {
+            info.setInputType(mEditor.mInputType);
+
+            if (mEditor.mError != null) {
+                info.setContentInvalid(true);
+                info.setError(mEditor.mError);
+            }
+        }
+
+        if (!TextUtils.isEmpty(mText)) {
+            info.addAction(AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY);
+            info.addAction(AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY);
+            info.setMovementGranularities(AccessibilityNodeInfo.MOVEMENT_GRANULARITY_CHARACTER
+                    | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_WORD
+                    | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE
+                    | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PARAGRAPH
+                    | AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE);
+            info.addAction(AccessibilityNodeInfo.ACTION_SET_SELECTION);
+            info.setAvailableExtraData(
+                    Arrays.asList(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY));
+        }
+
+        if (isFocused()) {
+            if (canCopy()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_COPY);
+            }
+            if (canPaste()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_PASTE);
+            }
+            if (canCut()) {
+                info.addAction(AccessibilityNodeInfo.ACTION_CUT);
+            }
+            if (canShare()) {
+                info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+                        ACCESSIBILITY_ACTION_SHARE,
+                        getResources().getString(com.android.internal.R.string.share)));
+            }
+            if (canProcessText()) {  // also implies mEditor is not null.
+                mEditor.mProcessTextIntentActionsHandler.onInitializeAccessibilityNodeInfo(info);
+            }
+        }
+
+        // Check for known input filter types.
+        final int numFilters = mFilters.length;
+        for (int i = 0; i < numFilters; i++) {
+            final InputFilter filter = mFilters[i];
+            if (filter instanceof InputFilter.LengthFilter) {
+                info.setMaxTextLength(((InputFilter.LengthFilter) filter).getMax());
+            }
+        }
+
+        if (!isSingleLine()) {
+            info.setMultiLine(true);
+        }
+    }
+
+    @Override
+    public void addExtraDataToAccessibilityNodeInfo(
+            AccessibilityNodeInfo info, String extraDataKey, Bundle arguments) {
+        // The only extra data we support requires arguments.
+        if (arguments == null) {
+            return;
+        }
+        if (extraDataKey.equals(EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY)) {
+            int positionInfoStartIndex = arguments.getInt(
+                    EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_START_INDEX, -1);
+            int positionInfoLength = arguments.getInt(
+                    EXTRA_DATA_TEXT_CHARACTER_LOCATION_ARG_LENGTH, -1);
+            if ((positionInfoLength <= 0) || (positionInfoStartIndex < 0)
+                    || (positionInfoStartIndex >= mText.length())) {
+                Log.e(LOG_TAG, "Invalid arguments for accessibility character locations");
+                return;
+            }
+            RectF[] boundingRects = new RectF[positionInfoLength];
+            final CursorAnchorInfo.Builder builder = new CursorAnchorInfo.Builder();
+            populateCharacterBounds(builder, positionInfoStartIndex,
+                    positionInfoStartIndex + positionInfoLength,
+                    viewportToContentHorizontalOffset(), viewportToContentVerticalOffset());
+            CursorAnchorInfo cursorAnchorInfo = builder.setMatrix(null).build();
+            for (int i = 0; i < positionInfoLength; i++) {
+                int flags = cursorAnchorInfo.getCharacterBoundsFlags(positionInfoStartIndex + i);
+                if ((flags & FLAG_HAS_VISIBLE_REGION) == FLAG_HAS_VISIBLE_REGION) {
+                    RectF bounds = cursorAnchorInfo
+                            .getCharacterBounds(positionInfoStartIndex + i);
+                    if (bounds != null) {
+                        mapRectFromViewToScreenCoords(bounds, true);
+                        boundingRects[i] = bounds;
+                    }
+                }
+            }
+            info.getExtras().putParcelableArray(extraDataKey, boundingRects);
+        }
+    }
+
+    /**
+     * Populate requested character bounds in a {@link CursorAnchorInfo.Builder}
+     *
+     * @param builder The builder to populate
+     * @param startIndex The starting character index to populate
+     * @param endIndex The ending character index to populate
+     * @param viewportToContentHorizontalOffset The horizontal offset from the viewport to the
+     * content
+     * @param viewportToContentVerticalOffset The vertical offset from the viewport to the content
+     * @hide
+     */
+    public void populateCharacterBounds(CursorAnchorInfo.Builder builder,
+            int startIndex, int endIndex, float viewportToContentHorizontalOffset,
+            float viewportToContentVerticalOffset) {
+        final int minLine = mLayout.getLineForOffset(startIndex);
+        final int maxLine = mLayout.getLineForOffset(endIndex - 1);
+        for (int line = minLine; line <= maxLine; ++line) {
+            final int lineStart = mLayout.getLineStart(line);
+            final int lineEnd = mLayout.getLineEnd(line);
+            final int offsetStart = Math.max(lineStart, startIndex);
+            final int offsetEnd = Math.min(lineEnd, endIndex);
+            final boolean ltrLine =
+                    mLayout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
+            final float[] widths = new float[offsetEnd - offsetStart];
+            mLayout.getPaint().getTextWidths(mText, offsetStart, offsetEnd, widths);
+            final float top = mLayout.getLineTop(line);
+            final float bottom = mLayout.getLineBottom(line);
+            for (int offset = offsetStart; offset < offsetEnd; ++offset) {
+                final float charWidth = widths[offset - offsetStart];
+                final boolean isRtl = mLayout.isRtlCharAt(offset);
+                final float primary = mLayout.getPrimaryHorizontal(offset);
+                final float secondary = mLayout.getSecondaryHorizontal(offset);
+                // TODO: This doesn't work perfectly for text with custom styles and
+                // TAB chars.
+                final float left;
+                final float right;
+                if (ltrLine) {
+                    if (isRtl) {
+                        left = secondary - charWidth;
+                        right = secondary;
+                    } else {
+                        left = primary;
+                        right = primary + charWidth;
+                    }
+                } else {
+                    if (!isRtl) {
+                        left = secondary;
+                        right = secondary + charWidth;
+                    } else {
+                        left = primary - charWidth;
+                        right = primary;
+                    }
+                }
+                // TODO: Check top-right and bottom-left as well.
+                final float localLeft = left + viewportToContentHorizontalOffset;
+                final float localRight = right + viewportToContentHorizontalOffset;
+                final float localTop = top + viewportToContentVerticalOffset;
+                final float localBottom = bottom + viewportToContentVerticalOffset;
+                final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
+                final boolean isBottomRightVisible =
+                        isPositionVisible(localRight, localBottom);
+                int characterBoundsFlags = 0;
+                if (isTopLeftVisible || isBottomRightVisible) {
+                    characterBoundsFlags |= FLAG_HAS_VISIBLE_REGION;
+                }
+                if (!isTopLeftVisible || !isBottomRightVisible) {
+                    characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
+                }
+                if (isRtl) {
+                    characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
+                }
+                // Here offset is the index in Java chars.
+                builder.addCharacterBounds(offset, localLeft, localTop, localRight,
+                        localBottom, characterBoundsFlags);
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isPositionVisible(final float positionX, final float positionY) {
+        synchronized (TEMP_POSITION) {
+            final float[] position = TEMP_POSITION;
+            position[0] = positionX;
+            position[1] = positionY;
+            View view = this;
+
+            while (view != null) {
+                if (view != this) {
+                    // Local scroll is already taken into account in positionX/Y
+                    position[0] -= view.getScrollX();
+                    position[1] -= view.getScrollY();
+                }
+
+                if (position[0] < 0 || position[1] < 0 || position[0] > view.getWidth()
+                        || position[1] > view.getHeight()) {
+                    return false;
+                }
+
+                if (!view.getMatrix().isIdentity()) {
+                    view.getMatrix().mapPoints(position);
+                }
+
+                position[0] += view.getLeft();
+                position[1] += view.getTop();
+
+                final ViewParent parent = view.getParent();
+                if (parent instanceof View) {
+                    view = (View) parent;
+                } else {
+                    // We've reached the ViewRoot, stop iterating
+                    view = null;
+                }
+            }
+        }
+
+        // We've been able to walk up the view hierarchy and the position was never clipped
+        return true;
+    }
+
+    /**
+     * Performs an accessibility action after it has been offered to the
+     * delegate.
+     *
+     * @hide
+     */
+    @Override
+    public boolean performAccessibilityActionInternal(int action, Bundle arguments) {
+        if (mEditor != null
+                && mEditor.mProcessTextIntentActionsHandler.performAccessibilityAction(action)) {
+            return true;
+        }
+        switch (action) {
+            case AccessibilityNodeInfo.ACTION_CLICK: {
+                return performAccessibilityActionClick(arguments);
+            }
+            case AccessibilityNodeInfo.ACTION_COPY: {
+                if (isFocused() && canCopy()) {
+                    if (onTextContextMenuItem(ID_COPY)) {
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_PASTE: {
+                if (isFocused() && canPaste()) {
+                    if (onTextContextMenuItem(ID_PASTE)) {
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_CUT: {
+                if (isFocused() && canCut()) {
+                    if (onTextContextMenuItem(ID_CUT)) {
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SET_SELECTION: {
+                ensureIterableTextForAccessibilitySelectable();
+                CharSequence text = getIterableTextForAccessibility();
+                if (text == null) {
+                    return false;
+                }
+                final int start = (arguments != null) ? arguments.getInt(
+                        AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_START_INT, -1) : -1;
+                final int end = (arguments != null) ? arguments.getInt(
+                        AccessibilityNodeInfo.ACTION_ARGUMENT_SELECTION_END_INT, -1) : -1;
+                if ((getSelectionStart() != start || getSelectionEnd() != end)) {
+                    // No arguments clears the selection.
+                    if (start == end && end == -1) {
+                        Selection.removeSelection((Spannable) text);
+                        return true;
+                    }
+                    if (start >= 0 && start <= end && end <= text.length()) {
+                        Selection.setSelection((Spannable) text, start, end);
+                        // Make sure selection mode is engaged.
+                        if (mEditor != null) {
+                            mEditor.startSelectionActionModeAsync(false);
+                        }
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_NEXT_AT_MOVEMENT_GRANULARITY:
+            case AccessibilityNodeInfo.ACTION_PREVIOUS_AT_MOVEMENT_GRANULARITY: {
+                ensureIterableTextForAccessibilitySelectable();
+                return super.performAccessibilityActionInternal(action, arguments);
+            }
+            case ACCESSIBILITY_ACTION_SHARE: {
+                if (isFocused() && canShare()) {
+                    if (onTextContextMenuItem(ID_SHARE)) {
+                        return true;
+                    }
+                }
+            } return false;
+            case AccessibilityNodeInfo.ACTION_SET_TEXT: {
+                if (!isEnabled() || (mBufferType != BufferType.EDITABLE)) {
+                    return false;
+                }
+                CharSequence text = (arguments != null) ? arguments.getCharSequence(
+                        AccessibilityNodeInfo.ACTION_ARGUMENT_SET_TEXT_CHARSEQUENCE) : null;
+                setText(text);
+                if (mText != null) {
+                    int updatedTextLength = mText.length();
+                    if (updatedTextLength > 0) {
+                        Selection.setSelection((Spannable) mText, updatedTextLength);
+                    }
+                }
+            } return true;
+            default: {
+                return super.performAccessibilityActionInternal(action, arguments);
+            }
+        }
+    }
+
+    private boolean performAccessibilityActionClick(Bundle arguments) {
+        boolean handled = false;
+
+        if (!isEnabled()) {
+            return false;
+        }
+
+        if (isClickable() || isLongClickable()) {
+            // Simulate View.onTouchEvent for an ACTION_UP event
+            if (isFocusable() && !isFocused()) {
+                requestFocus();
+            }
+
+            performClick();
+            handled = true;
+        }
+
+        // Show the IME, except when selecting in read-only text.
+        if ((mMovement != null || onCheckIsTextEditor()) && hasSpannableText() && mLayout != null
+                && (isTextEditable() || isTextSelectable()) && isFocused()) {
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            viewClicked(imm);
+            if (!isTextSelectable() && mEditor.mShowSoftInputOnFocus && imm != null) {
+                handled |= imm.showSoftInput(this, 0);
+            }
+        }
+
+        return handled;
+    }
+
+    private boolean hasSpannableText() {
+        return mText != null && mText instanceof Spannable;
+    }
+
+    /** @hide */
+    @Override
+    public void sendAccessibilityEventInternal(int eventType) {
+        if (eventType == AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED && mEditor != null) {
+            mEditor.mProcessTextIntentActionsHandler.initializeAccessibilityActions();
+        }
+
+        super.sendAccessibilityEventInternal(eventType);
+    }
+
+    @Override
+    public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
+        // Do not send scroll events since first they are not interesting for
+        // accessibility and second such events a generated too frequently.
+        // For details see the implementation of bringTextIntoView().
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            return;
+        }
+        super.sendAccessibilityEventUnchecked(event);
+    }
+
+    /**
+     * Returns the text that should be exposed to accessibility services.
+     * <p>
+     * This approximates what is displayed visually. If the user has specified
+     * that accessibility services should speak passwords, this method will
+     * bypass any password transformation method and return unobscured text.
+     *
+     * @return the text that should be exposed to accessibility services, may
+     *         be {@code null} if no text is set
+     */
+    @Nullable
+    private CharSequence getTextForAccessibility() {
+        // If the text is empty, we must be showing the hint text.
+        if (TextUtils.isEmpty(mText)) {
+            return mHint;
+        }
+
+        // Otherwise, return whatever text is being displayed.
+        return TextUtils.trimToParcelableSize(mTransformed);
+    }
+
+    void sendAccessibilityEventTypeViewTextChanged(CharSequence beforeText,
+            int fromIndex, int removedCount, int addedCount) {
+        AccessibilityEvent event =
+                AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
+        event.setFromIndex(fromIndex);
+        event.setRemovedCount(removedCount);
+        event.setAddedCount(addedCount);
+        event.setBeforeText(beforeText);
+        sendAccessibilityEventUnchecked(event);
+    }
+
+    /**
+     * Returns whether this text view is a current input method target.  The
+     * default implementation just checks with {@link InputMethodManager}.
+     * @return True if the TextView is a current input method target; false otherwise.
+     */
+    public boolean isInputMethodTarget() {
+        InputMethodManager imm = InputMethodManager.peekInstance();
+        return imm != null && imm.isActive(this);
+    }
+
+    static final int ID_SELECT_ALL = android.R.id.selectAll;
+    static final int ID_UNDO = android.R.id.undo;
+    static final int ID_REDO = android.R.id.redo;
+    static final int ID_CUT = android.R.id.cut;
+    static final int ID_COPY = android.R.id.copy;
+    static final int ID_PASTE = android.R.id.paste;
+    static final int ID_SHARE = android.R.id.shareText;
+    static final int ID_PASTE_AS_PLAIN_TEXT = android.R.id.pasteAsPlainText;
+    static final int ID_REPLACE = android.R.id.replaceText;
+    static final int ID_ASSIST = android.R.id.textAssist;
+    static final int ID_AUTOFILL = android.R.id.autofill;
+
+    /**
+     * Called when a context menu option for the text view is selected.  Currently
+     * this will be one of {@link android.R.id#selectAll}, {@link android.R.id#cut},
+     * {@link android.R.id#copy}, {@link android.R.id#paste} or {@link android.R.id#shareText}.
+     *
+     * @return true if the context menu item action was performed.
+     */
+    public boolean onTextContextMenuItem(int id) {
+        int min = 0;
+        int max = mText.length();
+
+        if (isFocused()) {
+            final int selStart = getSelectionStart();
+            final int selEnd = getSelectionEnd();
+
+            min = Math.max(0, Math.min(selStart, selEnd));
+            max = Math.max(0, Math.max(selStart, selEnd));
+        }
+
+        switch (id) {
+            case ID_SELECT_ALL:
+                final boolean hadSelection = hasSelection();
+                selectAllText();
+                if (mEditor != null && hadSelection) {
+                    mEditor.invalidateActionModeAsync();
+                }
+                return true;
+
+            case ID_UNDO:
+                if (mEditor != null) {
+                    mEditor.undo();
+                }
+                return true;  // Returns true even if nothing was undone.
+
+            case ID_REDO:
+                if (mEditor != null) {
+                    mEditor.redo();
+                }
+                return true;  // Returns true even if nothing was undone.
+
+            case ID_PASTE:
+                paste(min, max, true /* withFormatting */);
+                return true;
+
+            case ID_PASTE_AS_PLAIN_TEXT:
+                paste(min, max, false /* withFormatting */);
+                return true;
+
+            case ID_CUT:
+                final ClipData cutData = ClipData.newPlainText(null, getTransformedText(min, max));
+                if (setPrimaryClip(cutData)) {
+                    deleteText_internal(min, max);
+                } else {
+                    Toast.makeText(getContext(),
+                            com.android.internal.R.string.failed_to_copy_to_clipboard,
+                            Toast.LENGTH_SHORT).show();
+                }
+                return true;
+
+            case ID_COPY:
+                final ClipData copyData = ClipData.newPlainText(null, getTransformedText(min, max));
+                if (setPrimaryClip(copyData)) {
+                    stopTextActionMode();
+                } else {
+                    Toast.makeText(getContext(),
+                            com.android.internal.R.string.failed_to_copy_to_clipboard,
+                            Toast.LENGTH_SHORT).show();
+                }
+                return true;
+
+            case ID_REPLACE:
+                if (mEditor != null) {
+                    mEditor.replace();
+                }
+                return true;
+
+            case ID_SHARE:
+                shareSelectedText();
+                return true;
+
+            case ID_AUTOFILL:
+                requestAutofill();
+                stopTextActionMode();
+                return true;
+        }
+        return false;
+    }
+
+    CharSequence getTransformedText(int start, int end) {
+        return removeSuggestionSpans(mTransformed.subSequence(start, end));
+    }
+
+    @Override
+    public boolean performLongClick() {
+        boolean handled = false;
+        boolean performedHapticFeedback = false;
+
+        if (mEditor != null) {
+            mEditor.mIsBeingLongClicked = true;
+        }
+
+        if (super.performLongClick()) {
+            handled = true;
+            performedHapticFeedback = true;
+        }
+
+        if (mEditor != null) {
+            handled |= mEditor.performLongClick(handled);
+            mEditor.mIsBeingLongClicked = false;
+        }
+
+        if (handled) {
+            if (!performedHapticFeedback) {
+              performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
+            }
+            if (mEditor != null) mEditor.mDiscardNextActionUp = true;
+        } else {
+            MetricsLogger.action(
+                    mContext,
+                    MetricsEvent.TEXT_LONGPRESS,
+                    TextViewMetrics.SUBTYPE_LONG_PRESS_OTHER);
+        }
+
+        return handled;
+    }
+
+    @Override
+    protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
+        super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
+        if (mEditor != null) {
+            mEditor.onScrollChanged();
+        }
+    }
+
+    /**
+     * Return whether or not suggestions are enabled on this TextView. The suggestions are generated
+     * by the IME or by the spell checker as the user types. This is done by adding
+     * {@link SuggestionSpan}s to the text.
+     *
+     * When suggestions are enabled (default), this list of suggestions will be displayed when the
+     * user asks for them on these parts of the text. This value depends on the inputType of this
+     * TextView.
+     *
+     * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}.
+     *
+     * In addition, the type variation must be one of
+     * {@link InputType#TYPE_TEXT_VARIATION_NORMAL},
+     * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT},
+     * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE},
+     * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or
+     * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}.
+     *
+     * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set.
+     *
+     * @return true if the suggestions popup window is enabled, based on the inputType.
+     */
+    public boolean isSuggestionsEnabled() {
+        if (mEditor == null) return false;
+        if ((mEditor.mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) {
+            return false;
+        }
+        if ((mEditor.mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false;
+
+        final int variation = mEditor.mInputType & EditorInfo.TYPE_MASK_VARIATION;
+        return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL
+                || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT
+                || variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE
+                || variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE
+                || variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT);
+    }
+
+    /**
+     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
+     * selection is initiated in this View.
+     *
+     * <p>The standard implementation populates the menu with a subset of Select All, Cut, Copy,
+     * Paste, Replace and Share actions, depending on what this View supports.
+     *
+     * <p>A custom implementation can add new entries in the default menu in its
+     * {@link android.view.ActionMode.Callback#onPrepareActionMode(ActionMode, android.view.Menu)}
+     * method. The default actions can also be removed from the menu using
+     * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
+     * {@link android.R.id#cut}, {@link android.R.id#copy}, {@link android.R.id#paste},
+     * {@link android.R.id#replaceText} or {@link android.R.id#shareText} ids as parameters.
+     *
+     * <p>Returning false from
+     * {@link android.view.ActionMode.Callback#onCreateActionMode(ActionMode, android.view.Menu)}
+     * will prevent the action mode from being started.
+     *
+     * <p>Action click events should be handled by the custom implementation of
+     * {@link android.view.ActionMode.Callback#onActionItemClicked(ActionMode,
+     * android.view.MenuItem)}.
+     *
+     * <p>Note that text selection mode is not started when a TextView receives focus and the
+     * {@link android.R.attr#selectAllOnFocus} flag has been set. The content is highlighted in
+     * that case, to allow for quick replacement.
+     */
+    public void setCustomSelectionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        createEditorIfNeeded();
+        mEditor.mCustomSelectionActionModeCallback = actionModeCallback;
+    }
+
+    /**
+     * Retrieves the value set in {@link #setCustomSelectionActionModeCallback}. Default is null.
+     *
+     * @return The current custom selection callback.
+     */
+    public ActionMode.Callback getCustomSelectionActionModeCallback() {
+        return mEditor == null ? null : mEditor.mCustomSelectionActionModeCallback;
+    }
+
+    /**
+     * If provided, this ActionMode.Callback will be used to create the ActionMode when text
+     * insertion is initiated in this View.
+     * The standard implementation populates the menu with a subset of Select All,
+     * Paste and Replace actions, depending on what this View supports.
+     *
+     * <p>A custom implementation can add new entries in the default menu in its
+     * {@link android.view.ActionMode.Callback#onPrepareActionMode(android.view.ActionMode,
+     * android.view.Menu)} method. The default actions can also be removed from the menu using
+     * {@link android.view.Menu#removeItem(int)} and passing {@link android.R.id#selectAll},
+     * {@link android.R.id#paste} or {@link android.R.id#replaceText} ids as parameters.</p>
+     *
+     * <p>Returning false from
+     * {@link android.view.ActionMode.Callback#onCreateActionMode(android.view.ActionMode,
+     * android.view.Menu)} will prevent the action mode from being started.</p>
+     *
+     * <p>Action click events should be handled by the custom implementation of
+     * {@link android.view.ActionMode.Callback#onActionItemClicked(android.view.ActionMode,
+     * android.view.MenuItem)}.</p>
+     *
+     * <p>Note that text insertion mode is not started when a TextView receives focus and the
+     * {@link android.R.attr#selectAllOnFocus} flag has been set.</p>
+     */
+    public void setCustomInsertionActionModeCallback(ActionMode.Callback actionModeCallback) {
+        createEditorIfNeeded();
+        mEditor.mCustomInsertionActionModeCallback = actionModeCallback;
+    }
+
+    /**
+     * Retrieves the value set in {@link #setCustomInsertionActionModeCallback}. Default is null.
+     *
+     * @return The current custom insertion callback.
+     */
+    public ActionMode.Callback getCustomInsertionActionModeCallback() {
+        return mEditor == null ? null : mEditor.mCustomInsertionActionModeCallback;
+    }
+
+    /**
+     * Sets the {@link TextClassifier} for this TextView.
+     */
+    public void setTextClassifier(@Nullable TextClassifier textClassifier) {
+        mTextClassifier = textClassifier;
+    }
+
+    /**
+     * Returns the {@link TextClassifier} used by this TextView.
+     * If no TextClassifier has been set, this TextView uses the default set by the
+     * {@link TextClassificationManager}.
+     */
+    @NonNull
+    public TextClassifier getTextClassifier() {
+        if (mTextClassifier == null) {
+            TextClassificationManager tcm =
+                    mContext.getSystemService(TextClassificationManager.class);
+            if (tcm != null) {
+                mTextClassifier = tcm.getTextClassifier();
+            } else {
+                mTextClassifier = TextClassifier.NO_OP;
+            }
+        }
+        return mTextClassifier;
+    }
+
+    /**
+     * @hide
+     */
+    protected void stopTextActionMode() {
+        if (mEditor != null) {
+            mEditor.stopTextActionMode();
+        }
+    }
+
+    boolean canUndo() {
+        return mEditor != null && mEditor.canUndo();
+    }
+
+    boolean canRedo() {
+        return mEditor != null && mEditor.canRedo();
+    }
+
+    boolean canCut() {
+        if (hasPasswordTransformationMethod()) {
+            return false;
+        }
+
+        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null
+                && mEditor.mKeyListener != null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    boolean canCopy() {
+        if (hasPasswordTransformationMethod()) {
+            return false;
+        }
+
+        if (mText.length() > 0 && hasSelection() && mEditor != null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    boolean canShare() {
+        if (!getContext().canStartActivityForResult() || !isDeviceProvisioned()) {
+            return false;
+        }
+        return canCopy();
+    }
+
+    boolean isDeviceProvisioned() {
+        if (mDeviceProvisionedState == DEVICE_PROVISIONED_UNKNOWN) {
+            mDeviceProvisionedState = Settings.Global.getInt(
+                    mContext.getContentResolver(), Settings.Global.DEVICE_PROVISIONED, 0) != 0
+                    ? DEVICE_PROVISIONED_YES
+                    : DEVICE_PROVISIONED_NO;
+        }
+        return mDeviceProvisionedState == DEVICE_PROVISIONED_YES;
+    }
+
+    boolean canPaste() {
+        return (mText instanceof Editable
+                && mEditor != null && mEditor.mKeyListener != null
+                && getSelectionStart() >= 0
+                && getSelectionEnd() >= 0
+                && ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
+                        .hasPrimaryClip());
+    }
+
+    boolean canPasteAsPlainText() {
+        if (!canPaste()) {
+            return false;
+        }
+
+        final ClipData clipData =
+                ((ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE))
+                        .getPrimaryClip();
+        final ClipDescription description = clipData.getDescription();
+        final boolean isPlainType = description.hasMimeType(ClipDescription.MIMETYPE_TEXT_PLAIN);
+        final CharSequence text = clipData.getItemAt(0).getText();
+        if (isPlainType && (text instanceof Spanned)) {
+            Spanned spanned = (Spanned) text;
+            if (TextUtils.hasStyleSpan(spanned)) {
+                return true;
+            }
+        }
+        return description.hasMimeType(ClipDescription.MIMETYPE_TEXT_HTML);
+    }
+
+    boolean canProcessText() {
+        if (getId() == View.NO_ID) {
+            return false;
+        }
+        return canShare();
+    }
+
+    boolean canSelectAllText() {
+        return canSelectText() && !hasPasswordTransformationMethod()
+                && !(getSelectionStart() == 0 && getSelectionEnd() == mText.length());
+    }
+
+    boolean selectAllText() {
+        if (mEditor != null) {
+            // Hide the toolbar before changing the selection to avoid flickering.
+            mEditor.hideFloatingToolbar(FLOATING_TOOLBAR_SELECT_ALL_REFRESH_DELAY);
+        }
+        final int length = mText.length();
+        Selection.setSelection((Spannable) mText, 0, length);
+        return length > 0;
+    }
+
+    void replaceSelectionWithText(CharSequence text) {
+        ((Editable) mText).replace(getSelectionStart(), getSelectionEnd(), text);
+    }
+
+    /**
+     * Paste clipboard content between min and max positions.
+     */
+    private void paste(int min, int max, boolean withFormatting) {
+        ClipboardManager clipboard =
+                (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+        ClipData clip = clipboard.getPrimaryClip();
+        if (clip != null) {
+            boolean didFirst = false;
+            for (int i = 0; i < clip.getItemCount(); i++) {
+                final CharSequence paste;
+                if (withFormatting) {
+                    paste = clip.getItemAt(i).coerceToStyledText(getContext());
+                } else {
+                    // Get an item as text and remove all spans by toString().
+                    final CharSequence text = clip.getItemAt(i).coerceToText(getContext());
+                    paste = (text instanceof Spanned) ? text.toString() : text;
+                }
+                if (paste != null) {
+                    if (!didFirst) {
+                        Selection.setSelection((Spannable) mText, max);
+                        ((Editable) mText).replace(min, max, paste);
+                        didFirst = true;
+                    } else {
+                        ((Editable) mText).insert(getSelectionEnd(), "\n");
+                        ((Editable) mText).insert(getSelectionEnd(), paste);
+                    }
+                }
+            }
+            sLastCutCopyOrTextChangedTime = 0;
+        }
+    }
+
+    private void shareSelectedText() {
+        String selectedText = getSelectedText();
+        if (selectedText != null && !selectedText.isEmpty()) {
+            Intent sharingIntent = new Intent(android.content.Intent.ACTION_SEND);
+            sharingIntent.setType("text/plain");
+            sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
+            selectedText = TextUtils.trimToParcelableSize(selectedText);
+            sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
+            getContext().startActivity(Intent.createChooser(sharingIntent, null));
+            Selection.setSelection((Spannable) mText, getSelectionEnd());
+        }
+    }
+
+    @CheckResult
+    private boolean setPrimaryClip(ClipData clip) {
+        ClipboardManager clipboard =
+                (ClipboardManager) getContext().getSystemService(Context.CLIPBOARD_SERVICE);
+        try {
+            clipboard.setPrimaryClip(clip);
+        } catch (Throwable t) {
+            return false;
+        }
+        sLastCutCopyOrTextChangedTime = SystemClock.uptimeMillis();
+        return true;
+    }
+
+    /**
+     * Get the character offset closest to the specified absolute position. A typical use case is to
+     * pass the result of {@link MotionEvent#getX()} and {@link MotionEvent#getY()} to this method.
+     *
+     * @param x The horizontal absolute position of a point on screen
+     * @param y The vertical absolute position of a point on screen
+     * @return the character offset for the character whose position is closest to the specified
+     *  position. Returns -1 if there is no layout.
+     */
+    public int getOffsetForPosition(float x, float y) {
+        if (getLayout() == null) return -1;
+        final int line = getLineAtCoordinate(y);
+        final int offset = getOffsetAtCoordinate(line, x);
+        return offset;
+    }
+
+    float convertToLocalHorizontalCoordinate(float x) {
+        x -= getTotalPaddingLeft();
+        // Clamp the position to inside of the view.
+        x = Math.max(0.0f, x);
+        x = Math.min(getWidth() - getTotalPaddingRight() - 1, x);
+        x += getScrollX();
+        return x;
+    }
+
+    int getLineAtCoordinate(float y) {
+        y -= getTotalPaddingTop();
+        // Clamp the position to inside of the view.
+        y = Math.max(0.0f, y);
+        y = Math.min(getHeight() - getTotalPaddingBottom() - 1, y);
+        y += getScrollY();
+        return getLayout().getLineForVertical((int) y);
+    }
+
+    int getLineAtCoordinateUnclamped(float y) {
+        y -= getTotalPaddingTop();
+        y += getScrollY();
+        return getLayout().getLineForVertical((int) y);
+    }
+
+    int getOffsetAtCoordinate(int line, float x) {
+        x = convertToLocalHorizontalCoordinate(x);
+        return getLayout().getOffsetForHorizontal(line, x);
+    }
+
+    @Override
+    public boolean onDragEvent(DragEvent event) {
+        switch (event.getAction()) {
+            case DragEvent.ACTION_DRAG_STARTED:
+                return mEditor != null && mEditor.hasInsertionController();
+
+            case DragEvent.ACTION_DRAG_ENTERED:
+                TextView.this.requestFocus();
+                return true;
+
+            case DragEvent.ACTION_DRAG_LOCATION:
+                if (mText instanceof Spannable) {
+                    final int offset = getOffsetForPosition(event.getX(), event.getY());
+                    Selection.setSelection((Spannable) mText, offset);
+                }
+                return true;
+
+            case DragEvent.ACTION_DROP:
+                if (mEditor != null) mEditor.onDrop(event);
+                return true;
+
+            case DragEvent.ACTION_DRAG_ENDED:
+            case DragEvent.ACTION_DRAG_EXITED:
+            default:
+                return true;
+        }
+    }
+
+    boolean isInBatchEditMode() {
+        if (mEditor == null) return false;
+        final Editor.InputMethodState ims = mEditor.mInputMethodState;
+        if (ims != null) {
+            return ims.mBatchEditNesting > 0;
+        }
+        return mEditor.mInBatchEditControllers;
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+
+        final TextDirectionHeuristic newTextDir = getTextDirectionHeuristic();
+        if (mTextDir != newTextDir) {
+            mTextDir = newTextDir;
+            if (mLayout != null) {
+                checkForRelayout();
+            }
+        }
+    }
+
+    /**
+     * @hide
+     */
+    protected TextDirectionHeuristic getTextDirectionHeuristic() {
+        if (hasPasswordTransformationMethod()) {
+            // passwords fields should be LTR
+            return TextDirectionHeuristics.LTR;
+        }
+
+        if (mEditor != null
+                && (mEditor.mInputType & EditorInfo.TYPE_MASK_CLASS)
+                    == EditorInfo.TYPE_CLASS_PHONE) {
+            // Phone numbers must be in the direction of the locale's digits. Most locales have LTR
+            // digits, but some locales, such as those written in the Adlam or N'Ko scripts, have
+            // RTL digits.
+            final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(getTextLocale());
+            final String zero = symbols.getDigitStrings()[0];
+            // In case the zero digit is multi-codepoint, just use the first codepoint to determine
+            // direction.
+            final int firstCodepoint = zero.codePointAt(0);
+            final byte digitDirection = Character.getDirectionality(firstCodepoint);
+            if (digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT
+                    || digitDirection == Character.DIRECTIONALITY_RIGHT_TO_LEFT_ARABIC) {
+                return TextDirectionHeuristics.RTL;
+            } else {
+                return TextDirectionHeuristics.LTR;
+            }
+        }
+
+        // Always need to resolve layout direction first
+        final boolean defaultIsRtl = (getLayoutDirection() == LAYOUT_DIRECTION_RTL);
+
+        // Now, we can select the heuristic
+        switch (getTextDirection()) {
+            default:
+            case TEXT_DIRECTION_FIRST_STRONG:
+                return (defaultIsRtl ? TextDirectionHeuristics.FIRSTSTRONG_RTL :
+                        TextDirectionHeuristics.FIRSTSTRONG_LTR);
+            case TEXT_DIRECTION_ANY_RTL:
+                return TextDirectionHeuristics.ANYRTL_LTR;
+            case TEXT_DIRECTION_LTR:
+                return TextDirectionHeuristics.LTR;
+            case TEXT_DIRECTION_RTL:
+                return TextDirectionHeuristics.RTL;
+            case TEXT_DIRECTION_LOCALE:
+                return TextDirectionHeuristics.LOCALE;
+            case TEXT_DIRECTION_FIRST_STRONG_LTR:
+                return TextDirectionHeuristics.FIRSTSTRONG_LTR;
+            case TEXT_DIRECTION_FIRST_STRONG_RTL:
+                return TextDirectionHeuristics.FIRSTSTRONG_RTL;
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void onResolveDrawables(int layoutDirection) {
+        // No need to resolve twice
+        if (mLastLayoutDirection == layoutDirection) {
+            return;
+        }
+        mLastLayoutDirection = layoutDirection;
+
+        // Resolve drawables
+        if (mDrawables != null) {
+            if (mDrawables.resolveWithLayoutDirection(layoutDirection)) {
+                prepareDrawableForDisplay(mDrawables.mShowing[Drawables.LEFT]);
+                prepareDrawableForDisplay(mDrawables.mShowing[Drawables.RIGHT]);
+                applyCompoundDrawableTint();
+            }
+        }
+    }
+
+    /**
+     * Prepares a drawable for display by propagating layout direction and
+     * drawable state.
+     *
+     * @param dr the drawable to prepare
+     */
+    private void prepareDrawableForDisplay(@Nullable Drawable dr) {
+        if (dr == null) {
+            return;
+        }
+
+        dr.setLayoutDirection(getLayoutDirection());
+
+        if (dr.isStateful()) {
+            dr.setState(getDrawableState());
+            dr.jumpToCurrentState();
+        }
+    }
+
+    /**
+     * @hide
+     */
+    protected void resetResolvedDrawables() {
+        super.resetResolvedDrawables();
+        mLastLayoutDirection = -1;
+    }
+
+    /**
+     * @hide
+     */
+    protected void viewClicked(InputMethodManager imm) {
+        if (imm != null) {
+            imm.viewClicked(this);
+        }
+    }
+
+    /**
+     * Deletes the range of text [start, end[.
+     * @hide
+     */
+    protected void deleteText_internal(int start, int end) {
+        ((Editable) mText).delete(start, end);
+    }
+
+    /**
+     * Replaces the range of text [start, end[ by replacement text
+     * @hide
+     */
+    protected void replaceText_internal(int start, int end, CharSequence text) {
+        ((Editable) mText).replace(start, end, text);
+    }
+
+    /**
+     * Sets a span on the specified range of text
+     * @hide
+     */
+    protected void setSpan_internal(Object span, int start, int end, int flags) {
+        ((Editable) mText).setSpan(span, start, end, flags);
+    }
+
+    /**
+     * Moves the cursor to the specified offset position in text
+     * @hide
+     */
+    protected void setCursorPosition_internal(int start, int end) {
+        Selection.setSelection(((Editable) mText), start, end);
+    }
+
+    /**
+     * An Editor should be created as soon as any of the editable-specific fields (grouped
+     * inside the Editor object) is assigned to a non-default value.
+     * This method will create the Editor if needed.
+     *
+     * A standard TextView (as well as buttons, checkboxes...) should not qualify and hence will
+     * have a null Editor, unlike an EditText. Inconsistent in-between states will have an
+     * Editor for backward compatibility, as soon as one of these fields is assigned.
+     *
+     * Also note that for performance reasons, the mEditor is created when needed, but not
+     * reset when no more edit-specific fields are needed.
+     */
+    private void createEditorIfNeeded() {
+        if (mEditor == null) {
+            mEditor = new Editor(this);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public CharSequence getIterableTextForAccessibility() {
+        return mText;
+    }
+
+    private void ensureIterableTextForAccessibilitySelectable() {
+        if (!(mText instanceof Spannable)) {
+            setText(mText, BufferType.SPANNABLE);
+        }
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public TextSegmentIterator getIteratorForGranularity(int granularity) {
+        switch (granularity) {
+            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_LINE: {
+                Spannable text = (Spannable) getIterableTextForAccessibility();
+                if (!TextUtils.isEmpty(text) && getLayout() != null) {
+                    AccessibilityIterators.LineTextSegmentIterator iterator =
+                            AccessibilityIterators.LineTextSegmentIterator.getInstance();
+                    iterator.initialize(text, getLayout());
+                    return iterator;
+                }
+            } break;
+            case AccessibilityNodeInfo.MOVEMENT_GRANULARITY_PAGE: {
+                Spannable text = (Spannable) getIterableTextForAccessibility();
+                if (!TextUtils.isEmpty(text) && getLayout() != null) {
+                    AccessibilityIterators.PageTextSegmentIterator iterator =
+                            AccessibilityIterators.PageTextSegmentIterator.getInstance();
+                    iterator.initialize(this);
+                    return iterator;
+                }
+            } break;
+        }
+        return super.getIteratorForGranularity(granularity);
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int getAccessibilitySelectionStart() {
+        return getSelectionStart();
+    }
+
+    /**
+     * @hide
+     */
+    public boolean isAccessibilitySelectionExtendable() {
+        return true;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public int getAccessibilitySelectionEnd() {
+        return getSelectionEnd();
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    public void setAccessibilitySelection(int start, int end) {
+        if (getAccessibilitySelectionStart() == start
+                && getAccessibilitySelectionEnd() == end) {
+            return;
+        }
+        CharSequence text = getIterableTextForAccessibility();
+        if (Math.min(start, end) >= 0 && Math.max(start, end) <= text.length()) {
+            Selection.setSelection((Spannable) text, start, end);
+        } else {
+            Selection.removeSelection((Spannable) text);
+        }
+        // Hide all selection controllers used for adjusting selection
+        // since we are doing so explicitlty by other means and these
+        // controllers interact with how selection behaves.
+        if (mEditor != null) {
+            mEditor.hideCursorAndSpanControllers();
+            mEditor.stopTextActionMode();
+        }
+    }
+
+    /** @hide */
+    @Override
+    protected void encodeProperties(@NonNull ViewHierarchyEncoder stream) {
+        super.encodeProperties(stream);
+
+        TruncateAt ellipsize = getEllipsize();
+        stream.addProperty("text:ellipsize", ellipsize == null ? null : ellipsize.name());
+        stream.addProperty("text:textSize", getTextSize());
+        stream.addProperty("text:scaledTextSize", getScaledTextSize());
+        stream.addProperty("text:typefaceStyle", getTypefaceStyle());
+        stream.addProperty("text:selectionStart", getSelectionStart());
+        stream.addProperty("text:selectionEnd", getSelectionEnd());
+        stream.addProperty("text:curTextColor", mCurTextColor);
+        stream.addProperty("text:text", mText == null ? null : mText.toString());
+        stream.addProperty("text:gravity", mGravity);
+    }
+
+    /**
+     * User interface state that is stored by TextView for implementing
+     * {@link View#onSaveInstanceState}.
+     */
+    public static class SavedState extends BaseSavedState {
+        int selStart = -1;
+        int selEnd = -1;
+        CharSequence text;
+        boolean frozenWithFocus;
+        CharSequence error;
+        ParcelableParcel editorState;  // Optional state from Editor.
+
+        SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(selStart);
+            out.writeInt(selEnd);
+            out.writeInt(frozenWithFocus ? 1 : 0);
+            TextUtils.writeToParcel(text, out, flags);
+
+            if (error == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                TextUtils.writeToParcel(error, out, flags);
+            }
+
+            if (editorState == null) {
+                out.writeInt(0);
+            } else {
+                out.writeInt(1);
+                editorState.writeToParcel(out, flags);
+            }
+        }
+
+        @Override
+        public String toString() {
+            String str = "TextView.SavedState{"
+                    + Integer.toHexString(System.identityHashCode(this))
+                    + " start=" + selStart + " end=" + selEnd;
+            if (text != null) {
+                str += " text=" + text;
+            }
+            return str + "}";
+        }
+
+        @SuppressWarnings("hiding")
+        public static final Parcelable.Creator<SavedState> CREATOR =
+                new Parcelable.Creator<SavedState>() {
+                    public SavedState createFromParcel(Parcel in) {
+                        return new SavedState(in);
+                    }
+
+                    public SavedState[] newArray(int size) {
+                        return new SavedState[size];
+                    }
+                };
+
+        private SavedState(Parcel in) {
+            super(in);
+            selStart = in.readInt();
+            selEnd = in.readInt();
+            frozenWithFocus = (in.readInt() != 0);
+            text = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+
+            if (in.readInt() != 0) {
+                error = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(in);
+            }
+
+            if (in.readInt() != 0) {
+                editorState = ParcelableParcel.CREATOR.createFromParcel(in);
+            }
+        }
+    }
+
+    private static class CharWrapper implements CharSequence, GetChars, GraphicsOperations {
+        private char[] mChars;
+        private int mStart, mLength;
+
+        public CharWrapper(char[] chars, int start, int len) {
+            mChars = chars;
+            mStart = start;
+            mLength = len;
+        }
+
+        /* package */ void set(char[] chars, int start, int len) {
+            mChars = chars;
+            mStart = start;
+            mLength = len;
+        }
+
+        public int length() {
+            return mLength;
+        }
+
+        public char charAt(int off) {
+            return mChars[off + mStart];
+        }
+
+        @Override
+        public String toString() {
+            return new String(mChars, mStart, mLength);
+        }
+
+        public CharSequence subSequence(int start, int end) {
+            if (start < 0 || end < 0 || start > mLength || end > mLength) {
+                throw new IndexOutOfBoundsException(start + ", " + end);
+            }
+
+            return new String(mChars, start + mStart, end - start);
+        }
+
+        public void getChars(int start, int end, char[] buf, int off) {
+            if (start < 0 || end < 0 || start > mLength || end > mLength) {
+                throw new IndexOutOfBoundsException(start + ", " + end);
+            }
+
+            System.arraycopy(mChars, start + mStart, buf, off, end - start);
+        }
+
+        @Override
+        public void drawText(BaseCanvas c, int start, int end,
+                             float x, float y, Paint p) {
+            c.drawText(mChars, start + mStart, end - start, x, y, p);
+        }
+
+        @Override
+        public void drawTextRun(BaseCanvas c, int start, int end,
+                int contextStart, int contextEnd, float x, float y, boolean isRtl, Paint p) {
+            int count = end - start;
+            int contextCount = contextEnd - contextStart;
+            c.drawTextRun(mChars, start + mStart, count, contextStart + mStart,
+                    contextCount, x, y, isRtl, p);
+        }
+
+        public float measureText(int start, int end, Paint p) {
+            return p.measureText(mChars, start + mStart, end - start);
+        }
+
+        public int getTextWidths(int start, int end, float[] widths, Paint p) {
+            return p.getTextWidths(mChars, start + mStart, end - start, widths);
+        }
+
+        public float getTextRunAdvances(int start, int end, int contextStart,
+                int contextEnd, boolean isRtl, float[] advances, int advancesIndex,
+                Paint p) {
+            int count = end - start;
+            int contextCount = contextEnd - contextStart;
+            return p.getTextRunAdvances(mChars, start + mStart, count,
+                    contextStart + mStart, contextCount, isRtl, advances,
+                    advancesIndex);
+        }
+
+        public int getTextRunCursor(int contextStart, int contextEnd, int dir,
+                int offset, int cursorOpt, Paint p) {
+            int contextCount = contextEnd - contextStart;
+            return p.getTextRunCursor(mChars, contextStart + mStart,
+                    contextCount, dir, offset + mStart, cursorOpt);
+        }
+    }
+
+    private static final class Marquee {
+        // TODO: Add an option to configure this
+        private static final float MARQUEE_DELTA_MAX = 0.07f;
+        private static final int MARQUEE_DELAY = 1200;
+        private static final int MARQUEE_DP_PER_SECOND = 30;
+
+        private static final byte MARQUEE_STOPPED = 0x0;
+        private static final byte MARQUEE_STARTING = 0x1;
+        private static final byte MARQUEE_RUNNING = 0x2;
+
+        private final WeakReference<TextView> mView;
+        private final Choreographer mChoreographer;
+
+        private byte mStatus = MARQUEE_STOPPED;
+        private final float mPixelsPerSecond;
+        private float mMaxScroll;
+        private float mMaxFadeScroll;
+        private float mGhostStart;
+        private float mGhostOffset;
+        private float mFadeStop;
+        private int mRepeatLimit;
+
+        private float mScroll;
+        private long mLastAnimationMs;
+
+        Marquee(TextView v) {
+            final float density = v.getContext().getResources().getDisplayMetrics().density;
+            mPixelsPerSecond = MARQUEE_DP_PER_SECOND * density;
+            mView = new WeakReference<TextView>(v);
+            mChoreographer = Choreographer.getInstance();
+        }
+
+        private Choreographer.FrameCallback mTickCallback = new Choreographer.FrameCallback() {
+            @Override
+            public void doFrame(long frameTimeNanos) {
+                tick();
+            }
+        };
+
+        private Choreographer.FrameCallback mStartCallback = new Choreographer.FrameCallback() {
+            @Override
+            public void doFrame(long frameTimeNanos) {
+                mStatus = MARQUEE_RUNNING;
+                mLastAnimationMs = mChoreographer.getFrameTime();
+                tick();
+            }
+        };
+
+        private Choreographer.FrameCallback mRestartCallback = new Choreographer.FrameCallback() {
+            @Override
+            public void doFrame(long frameTimeNanos) {
+                if (mStatus == MARQUEE_RUNNING) {
+                    if (mRepeatLimit >= 0) {
+                        mRepeatLimit--;
+                    }
+                    start(mRepeatLimit);
+                }
+            }
+        };
+
+        void tick() {
+            if (mStatus != MARQUEE_RUNNING) {
+                return;
+            }
+
+            mChoreographer.removeFrameCallback(mTickCallback);
+
+            final TextView textView = mView.get();
+            if (textView != null && (textView.isFocused() || textView.isSelected())) {
+                long currentMs = mChoreographer.getFrameTime();
+                long deltaMs = currentMs - mLastAnimationMs;
+                mLastAnimationMs = currentMs;
+                float deltaPx = deltaMs / 1000f * mPixelsPerSecond;
+                mScroll += deltaPx;
+                if (mScroll > mMaxScroll) {
+                    mScroll = mMaxScroll;
+                    mChoreographer.postFrameCallbackDelayed(mRestartCallback, MARQUEE_DELAY);
+                } else {
+                    mChoreographer.postFrameCallback(mTickCallback);
+                }
+                textView.invalidate();
+            }
+        }
+
+        void stop() {
+            mStatus = MARQUEE_STOPPED;
+            mChoreographer.removeFrameCallback(mStartCallback);
+            mChoreographer.removeFrameCallback(mRestartCallback);
+            mChoreographer.removeFrameCallback(mTickCallback);
+            resetScroll();
+        }
+
+        private void resetScroll() {
+            mScroll = 0.0f;
+            final TextView textView = mView.get();
+            if (textView != null) textView.invalidate();
+        }
+
+        void start(int repeatLimit) {
+            if (repeatLimit == 0) {
+                stop();
+                return;
+            }
+            mRepeatLimit = repeatLimit;
+            final TextView textView = mView.get();
+            if (textView != null && textView.mLayout != null) {
+                mStatus = MARQUEE_STARTING;
+                mScroll = 0.0f;
+                final int textWidth = textView.getWidth() - textView.getCompoundPaddingLeft()
+                        - textView.getCompoundPaddingRight();
+                final float lineWidth = textView.mLayout.getLineWidth(0);
+                final float gap = textWidth / 3.0f;
+                mGhostStart = lineWidth - textWidth + gap;
+                mMaxScroll = mGhostStart + textWidth;
+                mGhostOffset = lineWidth + gap;
+                mFadeStop = lineWidth + textWidth / 6.0f;
+                mMaxFadeScroll = mGhostStart + lineWidth + lineWidth;
+
+                textView.invalidate();
+                mChoreographer.postFrameCallback(mStartCallback);
+            }
+        }
+
+        float getGhostOffset() {
+            return mGhostOffset;
+        }
+
+        float getScroll() {
+            return mScroll;
+        }
+
+        float getMaxFadeScroll() {
+            return mMaxFadeScroll;
+        }
+
+        boolean shouldDrawLeftFade() {
+            return mScroll <= mFadeStop;
+        }
+
+        boolean shouldDrawGhost() {
+            return mStatus == MARQUEE_RUNNING && mScroll > mGhostStart;
+        }
+
+        boolean isRunning() {
+            return mStatus == MARQUEE_RUNNING;
+        }
+
+        boolean isStopped() {
+            return mStatus == MARQUEE_STOPPED;
+        }
+    }
+
+    private class ChangeWatcher implements TextWatcher, SpanWatcher {
+
+        private CharSequence mBeforeText;
+
+        public void beforeTextChanged(CharSequence buffer, int start,
+                                      int before, int after) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "beforeTextChanged start=" + start
+                        + " before=" + before + " after=" + after + ": " + buffer);
+            }
+
+            if (AccessibilityManager.getInstance(mContext).isEnabled()
+                    && !isPasswordInputType(getInputType()) && !hasPasswordTransformationMethod()) {
+                mBeforeText = buffer.toString();
+            }
+
+            TextView.this.sendBeforeTextChanged(buffer, start, before, after);
+        }
+
+        public void onTextChanged(CharSequence buffer, int start, int before, int after) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "onTextChanged start=" + start
+                        + " before=" + before + " after=" + after + ": " + buffer);
+            }
+            TextView.this.handleTextChanged(buffer, start, before, after);
+
+            if (AccessibilityManager.getInstance(mContext).isEnabled()
+                    && (isFocused() || isSelected() && isShown())) {
+                sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
+                mBeforeText = null;
+            }
+        }
+
+        public void afterTextChanged(Editable buffer) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "afterTextChanged: " + buffer);
+            }
+            TextView.this.sendAfterTextChanged(buffer);
+
+            if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) {
+                MetaKeyKeyListener.stopSelecting(TextView.this, buffer);
+            }
+        }
+
+        public void onSpanChanged(Spannable buf, Object what, int s, int e, int st, int en) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "onSpanChanged s=" + s + " e=" + e
+                        + " st=" + st + " en=" + en + " what=" + what + ": " + buf);
+            }
+            TextView.this.spanChange(buf, what, s, st, e, en);
+        }
+
+        public void onSpanAdded(Spannable buf, Object what, int s, int e) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "onSpanAdded s=" + s + " e=" + e + " what=" + what + ": " + buf);
+            }
+            TextView.this.spanChange(buf, what, -1, s, -1, e);
+        }
+
+        public void onSpanRemoved(Spannable buf, Object what, int s, int e) {
+            if (DEBUG_EXTRACT) {
+                Log.v(LOG_TAG, "onSpanRemoved s=" + s + " e=" + e + " what=" + what + ": " + buf);
+            }
+            TextView.this.spanChange(buf, what, s, -1, e, -1);
+        }
+    }
+}
diff --git a/android/widget/TextViewAutoSizeLayoutPerfTest.java b/android/widget/TextViewAutoSizeLayoutPerfTest.java
new file mode 100644
index 0000000..c310166
--- /dev/null
+++ b/android/widget/TextViewAutoSizeLayoutPerfTest.java
@@ -0,0 +1,98 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.app.Activity;
+import android.os.Looper;
+import android.os.Bundle;
+import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
+import android.view.View;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+import com.android.perftests.core.R;
+
+import java.util.Locale;
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewAutoSizeLayoutPerfTest {
+    @Parameters(name = "{0}")
+    public static Collection layouts() {
+        return Arrays.asList(new Object[][] {
+                { "Basic TextView - no autosize", R.layout.test_basic_textview_layout},
+                { "Autosize TextView 5 sizes", R.layout.test_autosize_textview_5},
+                { "Autosize TextView 10 sizes", R.layout.test_autosize_textview_10},
+                { "Autosize TextView 50 sizes", R.layout.test_autosize_textview_50},
+                { "Autosize TextView 100 sizes", R.layout.test_autosize_textview_100},
+                { "Autosize TextView 300 sizes", R.layout.test_autosize_textview_300},
+                { "Autosize TextView 500 sizes", R.layout.test_autosize_textview_500},
+                { "Autosize TextView 1000 sizes", R.layout.test_autosize_textview_1000},
+                { "Autosize TextView 10000 sizes", R.layout.test_autosize_textview_10000},
+                { "Autosize TextView 100000 sizes", R.layout.test_autosize_textview_100000}
+        });
+    }
+
+    private int mLayoutId;
+
+    public TextViewAutoSizeLayoutPerfTest(String key, int layoutId) {
+        mLayoutId = layoutId;
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule =
+            new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testConstruction() throws Throwable {
+        mActivityRule.runOnUiThread(() -> {
+            assertTrue("We should be running on the main thread",
+                    Looper.getMainLooper().getThread() == Thread.currentThread());
+            assertTrue("We should be running on the main thread",
+                    Looper.myLooper() == Looper.getMainLooper());
+            BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+            Activity activity = mActivityRule.getActivity();
+            activity.setContentView(mLayoutId);
+
+            while (state.keepRunning()) {
+                TextView textView = new TextView(activity);
+                // TextView#onLayout() gets called, which triggers TextView#autoSizeText()
+                // which is the method we want to benchmark.
+                textView.requestLayout();
+            }
+        });
+    }
+}
diff --git a/android/widget/TextViewFontFamilyLayoutPerfTest.java b/android/widget/TextViewFontFamilyLayoutPerfTest.java
new file mode 100644
index 0000000..4b6da6b
--- /dev/null
+++ b/android/widget/TextViewFontFamilyLayoutPerfTest.java
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2017 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 android.widget;
+
+import android.content.Context;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.view.LayoutInflater;
+
+import com.android.perftests.core.R;
+
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewFontFamilyLayoutPerfTest {
+    @Parameters(name = "{0}")
+    public static Collection layouts() {
+        return Arrays.asList(new Object[][] {
+                { "String fontFamily attribute", R.layout.test_textview_font_family_string},
+                { "File fontFamily attribute", R.layout.test_textview_font_family_file},
+                { "XML fontFamily attribute", R.layout.test_textview_font_family_xml},
+        });
+    }
+
+    private int mLayoutId;
+
+    public TextViewFontFamilyLayoutPerfTest(String key, int layoutId) {
+        mLayoutId = layoutId;
+    }
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testConstruction() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+        final LayoutInflater inflator =
+                (LayoutInflater) context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+        while (state.keepRunning()) {
+            inflator.inflate(mLayoutId, null, false);
+        }
+    }
+}
diff --git a/android/widget/TextViewMetrics.java b/android/widget/TextViewMetrics.java
new file mode 100644
index 0000000..96d1794
--- /dev/null
+++ b/android/widget/TextViewMetrics.java
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+/**
+ * {@link com.android.internal.logging.MetricsLogger} values for TextView.
+ *
+ * @hide
+ */
+public final class TextViewMetrics {
+
+    private TextViewMetrics() {}
+
+    /**
+     * Long press on TextView - no special classification.
+     */
+    public static final int SUBTYPE_LONG_PRESS_OTHER = 0;
+    /**
+     * Long press on TextView - selection started.
+     */
+    public static final int SUBTYPE_LONG_PRESS_SELECTION = 1;
+    /**
+     * Long press on TextView - drag and drop started.
+     */
+    public static final int SUBTYPE_LONG_PRESS_DRAG_AND_DROP = 2;
+
+    /**
+     * Assist menu item (shown or clicked) - classification: other.
+     */
+    public static final int SUBTYPE_ASSIST_MENU_ITEM_OTHER = 0;
+
+    /**
+     * Assist menu item (shown or clicked) - classification: email.
+     */
+    public static final int SUBTYPE_ASSIST_MENU_ITEM_EMAIL = 1;
+
+    /**
+     * Assist menu item (shown or clicked) - classification: phone.
+     */
+    public static final int SUBTYPE_ASSIST_MENU_ITEM_PHONE = 2;
+
+    /**
+     * Assist menu item (shown or clicked) - classification: address.
+     */
+    public static final int SUBTYPE_ASSIST_MENU_ITEM_ADDRESS = 3;
+
+    /**
+     * Assist menu item (shown or clicked) - classification: url.
+     */
+    public static final int SUBTYPE_ASSIST_MENU_ITEM_URL = 4;
+}
diff --git a/android/widget/TextViewOnMeasurePerfTest.java b/android/widget/TextViewOnMeasurePerfTest.java
new file mode 100644
index 0000000..a14dd25
--- /dev/null
+++ b/android/widget/TextViewOnMeasurePerfTest.java
@@ -0,0 +1,156 @@
+/*
+ * Copyright (C) 2017 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 android.widget;
+
+import static android.view.View.MeasureSpec.AT_MOST;
+import static android.view.View.MeasureSpec.EXACTLY;
+import static android.view.View.MeasureSpec.UNSPECIFIED;
+
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.graphics.Typeface;
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.PerfStatusReporter;
+import android.support.test.InstrumentationRegistry;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
+import android.text.style.TextAppearanceSpan;
+import android.view.LayoutInflater;
+
+import com.android.perftests.core.R;
+
+import java.util.Random;
+import java.util.Locale;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+
+import static org.junit.Assert.assertTrue;
+
+@LargeTest
+@RunWith(AndroidJUnit4.class)
+public class TextViewOnMeasurePerfTest {
+    private static final String MULTILINE_TEXT =
+        "Lorem ipsum dolor sit amet, \n"+
+        "consectetur adipiscing elit, \n" +
+        "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. \n" +
+        "Ut enim ad minim veniam, \n" +
+        "quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.\n" +
+        "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat " +
+        "nulla pariatur.\n" +
+        "Excepteur sint occaecat cupidatat non proident, \n" +
+        "sunt in culpa qui officia deserunt mollit anim id est laborum.\n";
+
+    private static final int VIEW_WIDTH = 1000;
+    private static final int VIEW_HEIGHT = 1000;
+    private static final CharSequence COMPLEX_MULTILINE_TEXT;
+    static {
+        final SpannableStringBuilder ssb = new SpannableStringBuilder();
+
+        // To emphasize, append multiline text 10 times.
+        for (int i = 0; i < 10; ++i) {
+            ssb.append(MULTILINE_TEXT);
+        }
+
+        final ColorStateList[] COLORS = {
+                ColorStateList.valueOf(0xFFFF0000),  // RED
+                ColorStateList.valueOf(0xFF00FF00),  // GREEN
+                ColorStateList.valueOf(0xFF0000FF),  // BLUE
+        };
+
+        final int[] STYLES = {
+                Typeface.NORMAL, Typeface.BOLD, Typeface.ITALIC, Typeface.BOLD_ITALIC
+        };
+
+        final String[] FAMILIES = { "sans-serif", "serif", "monospace" };
+
+        // Append random span to text.
+        final Random random = new Random(0);
+        for (int pos = 0; pos < ssb.length();) {
+            final TextAppearanceSpan span = new TextAppearanceSpan(
+                FAMILIES[random.nextInt(FAMILIES.length)],
+                STYLES[random.nextInt(STYLES.length)],
+                24 + random.nextInt(32),  // text size. minimum 24
+                COLORS[random.nextInt(COLORS.length)],
+                COLORS[random.nextInt(COLORS.length)]);
+            int spanLength = 1 + random.nextInt(9);  // Up to 9 span length.
+            if (pos + spanLength > ssb.length()) {
+                spanLength = ssb.length() - pos;
+            }
+            ssb.setSpan(span, pos, pos + spanLength, Spanned.SPAN_INCLUSIVE_EXCLUSIVE);
+            pos += spanLength;
+        }
+        COMPLEX_MULTILINE_TEXT = ssb;
+    }
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testMeasure_AtMost() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(AT_MOST | VIEW_WIDTH, AT_MOST | VIEW_HEIGHT);
+        }
+    }
+
+    @Test
+    public void testMeasure_Exactly() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(EXACTLY | VIEW_WIDTH, EXACTLY | VIEW_HEIGHT);
+        }
+    }
+
+    @Test
+    public void testMeasure_Unspecified() throws Throwable {
+        final Context context = InstrumentationRegistry.getTargetContext();
+        final BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        final TextView textView = new TextView(context);
+        textView.setText(COMPLEX_MULTILINE_TEXT);
+
+        while (state.keepRunning()) {
+            // Changing locale to invalidate internal layout.
+            textView.setTextLocale(Locale.UK);
+            textView.setTextLocale(Locale.US);
+
+            textView.measure(UNSPECIFIED, UNSPECIFIED);
+        }
+    }
+}
diff --git a/android/widget/TextViewSetTextLocalePerfTest.java b/android/widget/TextViewSetTextLocalePerfTest.java
new file mode 100644
index 0000000..7fc5e4f
--- /dev/null
+++ b/android/widget/TextViewSetTextLocalePerfTest.java
@@ -0,0 +1,80 @@
+/*
+ * Copyright (C) 2016 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 android.widget;
+
+import android.app.Activity;
+import android.os.Bundle;
+import android.perftests.utils.PerfStatusReporter;
+import android.util.Log;
+
+import android.perftests.utils.BenchmarkState;
+import android.perftests.utils.StubActivity;
+import android.support.test.filters.LargeTest;
+import android.support.test.runner.AndroidJUnit4;
+import android.support.test.rule.ActivityTestRule;
+import android.support.test.InstrumentationRegistry;
+
+import java.util.Locale;
+import java.util.Collection;
+import java.util.Arrays;
+
+import org.junit.Test;
+import org.junit.Rule;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+import org.junit.runner.RunWith;
+
+@LargeTest
+@RunWith(Parameterized.class)
+public class TextViewSetTextLocalePerfTest {
+    @Parameters(name = "{0}")
+    public static Collection locales() {
+        return Arrays.asList(new Object[][] {
+            { "SameLocale", "en-US", "en-US" },
+            { "DifferentLocale", "en-US", "ja-JP"}
+        });
+    }
+
+    private String mMetricKey;
+    private Locale mFirstLocale;
+    private Locale mSecondLocale;
+
+    public TextViewSetTextLocalePerfTest(
+            String metricKey, String firstLocale, String secondLocale) {
+        mMetricKey = metricKey;
+        mFirstLocale = Locale.forLanguageTag(firstLocale);
+        mSecondLocale = Locale.forLanguageTag(secondLocale);
+    }
+
+    @Rule
+    public ActivityTestRule<StubActivity> mActivityRule = new ActivityTestRule(StubActivity.class);
+
+    @Rule
+    public PerfStatusReporter mPerfStatusReporter = new PerfStatusReporter();
+
+    @Test
+    public void testSetTextLocale() {
+        TextView textView = new TextView(mActivityRule.getActivity());
+
+        BenchmarkState state = mPerfStatusReporter.getBenchmarkState();
+
+        while (state.keepRunning()) {
+            textView.setTextLocale(mFirstLocale);
+            textView.setTextLocale(mSecondLocale);
+        }
+    }
+}
diff --git a/android/widget/ThemedSpinnerAdapter.java b/android/widget/ThemedSpinnerAdapter.java
new file mode 100644
index 0000000..6d92620
--- /dev/null
+++ b/android/widget/ThemedSpinnerAdapter.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2015 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 android.widget;
+
+import android.annotation.Nullable;
+import android.content.res.Resources;
+import android.content.res.Resources.Theme;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * An extension of SpinnerAdapter that is capable of inflating drop-down views
+ * against a different theme than normal views.
+ * <p>
+ * Classes that implement this interface should use the theme provided to
+ * {@link #setDropDownViewTheme(Theme)} when creating views in
+ * {@link SpinnerAdapter#getDropDownView(int, View, ViewGroup)}.
+ */
+public interface ThemedSpinnerAdapter extends SpinnerAdapter {
+    /**
+     * Sets the {@link Resources.Theme} against which drop-down views are
+     * inflated.
+     *
+     * @param theme the context against which to inflate drop-down views, or
+     *              {@code null} to use the default theme
+     * @see SpinnerAdapter#getDropDownView(int, View, ViewGroup)
+     */
+    void setDropDownViewTheme(@Nullable Resources.Theme theme);
+
+    /**
+     * Returns the value previously set by a call to
+     * {@link #setDropDownViewTheme(Theme)}.
+     *
+     * @return the {@link Resources.Theme} against which drop-down views are
+     *         inflated, or {@code null} if one has not been explicitly set
+     */
+    @Nullable
+    Resources.Theme getDropDownViewTheme();
+}
diff --git a/android/widget/TimePicker.java b/android/widget/TimePicker.java
new file mode 100644
index 0000000..ae6881e
--- /dev/null
+++ b/android/widget/TimePicker.java
@@ -0,0 +1,577 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.IntRange;
+import android.annotation.NonNull;
+import android.annotation.TestApi;
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.icu.util.Calendar;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.MathUtils;
+import android.view.View;
+import android.view.ViewStructure;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.autofill.AutofillManager;
+import android.view.autofill.AutofillValue;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Locale;
+
+/**
+ * A widget for selecting the time of day, in either 24-hour or AM/PM mode.
+ * <p>
+ * For a dialog using this view, see {@link android.app.TimePickerDialog}. See
+ * the <a href="{@docRoot}guide/topics/ui/controls/pickers.html">Pickers</a>
+ * guide for more information.
+ *
+ * @attr ref android.R.styleable#TimePicker_timePickerMode
+ */
+@Widget
+public class TimePicker extends FrameLayout {
+    private static final String LOG_TAG = TimePicker.class.getSimpleName();
+
+    /**
+     * Presentation mode for the Holo-style time picker that uses a set of
+     * {@link android.widget.NumberPicker}s.
+     *
+     * @see #getMode()
+     * @hide Visible for testing only.
+     */
+    @TestApi
+    public static final int MODE_SPINNER = 1;
+
+    /**
+     * Presentation mode for the Material-style time picker that uses a clock
+     * face.
+     *
+     * @see #getMode()
+     * @hide Visible for testing only.
+     */
+    @TestApi
+    public static final int MODE_CLOCK = 2;
+
+    /** @hide */
+    @IntDef({MODE_SPINNER, MODE_CLOCK})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface TimePickerMode {}
+
+    private final TimePickerDelegate mDelegate;
+
+    @TimePickerMode
+    private final int mMode;
+
+    /**
+     * The callback interface used to indicate the time has been adjusted.
+     */
+    public interface OnTimeChangedListener {
+
+        /**
+         * @param view The view associated with this listener.
+         * @param hourOfDay The current hour.
+         * @param minute The current minute.
+         */
+        void onTimeChanged(TimePicker view, int hourOfDay, int minute);
+    }
+
+    public TimePicker(Context context) {
+        this(context, null);
+    }
+
+    public TimePicker(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.timePickerStyle);
+    }
+
+    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public TimePicker(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        // DatePicker is important by default, unless app developer overrode attribute.
+        if (getImportantForAutofill() == IMPORTANT_FOR_AUTOFILL_AUTO) {
+            setImportantForAutofill(IMPORTANT_FOR_AUTOFILL_YES);
+        }
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+        final boolean isDialogMode = a.getBoolean(R.styleable.TimePicker_dialogMode, false);
+        final int requestedMode = a.getInt(R.styleable.TimePicker_timePickerMode, MODE_SPINNER);
+        a.recycle();
+
+        if (requestedMode == MODE_CLOCK && isDialogMode) {
+            // You want MODE_CLOCK? YOU CAN'T HANDLE MODE_CLOCK! Well, maybe
+            // you can depending on your screen size. Let's check...
+            mMode = context.getResources().getInteger(R.integer.time_picker_mode);
+        } else {
+            mMode = requestedMode;
+        }
+
+        switch (mMode) {
+            case MODE_CLOCK:
+                mDelegate = new TimePickerClockDelegate(
+                        this, context, attrs, defStyleAttr, defStyleRes);
+                break;
+            case MODE_SPINNER:
+            default:
+                mDelegate = new TimePickerSpinnerDelegate(
+                        this, context, attrs, defStyleAttr, defStyleRes);
+                break;
+        }
+        mDelegate.setAutoFillChangeListener((v, h, m) -> {
+            final AutofillManager afm = context.getSystemService(AutofillManager.class);
+            if (afm != null) {
+                afm.notifyValueChanged(this);
+            }
+        });
+    }
+
+    /**
+     * @return the picker's presentation mode, one of {@link #MODE_CLOCK} or
+     *         {@link #MODE_SPINNER}
+     * @attr ref android.R.styleable#TimePicker_timePickerMode
+     * @hide Visible for testing only.
+     */
+    @TimePickerMode
+    @TestApi
+    public int getMode() {
+        return mMode;
+    }
+
+    /**
+     * Sets the currently selected hour using 24-hour time.
+     *
+     * @param hour the hour to set, in the range (0-23)
+     * @see #getHour()
+     */
+    public void setHour(@IntRange(from = 0, to = 23) int hour) {
+        mDelegate.setHour(MathUtils.constrain(hour, 0, 23));
+    }
+
+    /**
+     * Returns the currently selected hour using 24-hour time.
+     *
+     * @return the currently selected hour, in the range (0-23)
+     * @see #setHour(int)
+     */
+    public int getHour() {
+        return mDelegate.getHour();
+    }
+
+    /**
+     * Sets the currently selected minute.
+     *
+     * @param minute the minute to set, in the range (0-59)
+     * @see #getMinute()
+     */
+    public void setMinute(@IntRange(from = 0, to = 59) int minute) {
+        mDelegate.setMinute(MathUtils.constrain(minute, 0, 59));
+    }
+
+    /**
+     * Returns the currently selected minute.
+     *
+     * @return the currently selected minute, in the range (0-59)
+     * @see #setMinute(int)
+     */
+    public int getMinute() {
+        return mDelegate.getMinute();
+    }
+
+    /**
+     * Sets the currently selected hour using 24-hour time.
+     *
+     * @param currentHour the hour to set, in the range (0-23)
+     * @deprecated Use {@link #setHour(int)}
+     */
+    @Deprecated
+    public void setCurrentHour(@NonNull Integer currentHour) {
+        setHour(currentHour);
+    }
+
+    /**
+     * @return the currently selected hour, in the range (0-23)
+     * @deprecated Use {@link #getHour()}
+     */
+    @NonNull
+    @Deprecated
+    public Integer getCurrentHour() {
+        return getHour();
+    }
+
+    /**
+     * Sets the currently selected minute.
+     *
+     * @param currentMinute the minute to set, in the range (0-59)
+     * @deprecated Use {@link #setMinute(int)}
+     */
+    @Deprecated
+    public void setCurrentMinute(@NonNull Integer currentMinute) {
+        setMinute(currentMinute);
+    }
+
+    /**
+     * @return the currently selected minute, in the range (0-59)
+     * @deprecated Use {@link #getMinute()}
+     */
+    @NonNull
+    @Deprecated
+    public Integer getCurrentMinute() {
+        return getMinute();
+    }
+
+    /**
+     * Sets whether this widget displays time in 24-hour mode or 12-hour mode
+     * with an AM/PM picker.
+     *
+     * @param is24HourView {@code true} to display in 24-hour mode,
+     *                     {@code false} for 12-hour mode with AM/PM
+     * @see #is24HourView()
+     */
+    public void setIs24HourView(@NonNull Boolean is24HourView) {
+        if (is24HourView == null) {
+            return;
+        }
+
+        mDelegate.setIs24Hour(is24HourView);
+    }
+
+    /**
+     * @return {@code true} if this widget displays time in 24-hour mode,
+     *         {@code false} otherwise}
+     * @see #setIs24HourView(Boolean)
+     */
+    public boolean is24HourView() {
+        return mDelegate.is24Hour();
+    }
+
+    /**
+     * Set the callback that indicates the time has been adjusted by the user.
+     *
+     * @param onTimeChangedListener the callback, should not be null.
+     */
+    public void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener) {
+        mDelegate.setOnTimeChangedListener(onTimeChangedListener);
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        super.setEnabled(enabled);
+        mDelegate.setEnabled(enabled);
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mDelegate.isEnabled();
+    }
+
+    @Override
+    public int getBaseline() {
+        return mDelegate.getBaseline();
+    }
+
+    /**
+     * Validates whether current input by the user is a valid time based on the locale. TimePicker
+     * will show an error message to the user if the time is not valid.
+     *
+     * @return {@code true} if the input is valid, {@code false} otherwise
+     */
+    public boolean validateInput() {
+        return mDelegate.validateInput();
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        Parcelable superState = super.onSaveInstanceState();
+        return mDelegate.onSaveInstanceState(superState);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        BaseSavedState ss = (BaseSavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+        mDelegate.onRestoreInstanceState(ss);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TimePicker.class.getName();
+    }
+
+    /** @hide */
+    @Override
+    public boolean dispatchPopulateAccessibilityEventInternal(AccessibilityEvent event) {
+        return mDelegate.dispatchPopulateAccessibilityEvent(event);
+    }
+
+    /** @hide */
+    @TestApi
+    public View getHourView() {
+        return mDelegate.getHourView();
+    }
+
+    /** @hide */
+    @TestApi
+    public View getMinuteView() {
+        return mDelegate.getMinuteView();
+    }
+
+    /** @hide */
+    @TestApi
+    public View getAmView() {
+        return mDelegate.getAmView();
+    }
+
+    /** @hide */
+    @TestApi
+    public View getPmView() {
+        return mDelegate.getPmView();
+    }
+
+    /**
+     * A delegate interface that defined the public API of the TimePicker. Allows different
+     * TimePicker implementations. This would need to be implemented by the TimePicker delegates
+     * for the real behavior.
+     */
+    interface TimePickerDelegate {
+        void setHour(@IntRange(from = 0, to = 23) int hour);
+        int getHour();
+
+        void setMinute(@IntRange(from = 0, to = 59) int minute);
+        int getMinute();
+
+        void setDate(@IntRange(from = 0, to = 23) int hour,
+                @IntRange(from = 0, to = 59) int minute);
+
+        void autofill(AutofillValue value);
+        AutofillValue getAutofillValue();
+
+        void setIs24Hour(boolean is24Hour);
+        boolean is24Hour();
+
+        boolean validateInput();
+
+        void setOnTimeChangedListener(OnTimeChangedListener onTimeChangedListener);
+        void setAutoFillChangeListener(OnTimeChangedListener autoFillChangeListener);
+
+        void setEnabled(boolean enabled);
+        boolean isEnabled();
+
+        int getBaseline();
+
+        Parcelable onSaveInstanceState(Parcelable superState);
+        void onRestoreInstanceState(Parcelable state);
+
+        boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event);
+        void onPopulateAccessibilityEvent(AccessibilityEvent event);
+
+        /** @hide */
+        @TestApi View getHourView();
+
+        /** @hide */
+        @TestApi View getMinuteView();
+
+        /** @hide */
+        @TestApi View getAmView();
+
+        /** @hide */
+        @TestApi View getPmView();
+    }
+
+    static String[] getAmPmStrings(Context context) {
+        final Locale locale = context.getResources().getConfiguration().locale;
+        final LocaleData d = LocaleData.get(locale);
+
+        final String[] result = new String[2];
+        result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
+        result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
+        return result;
+    }
+
+    /**
+     * An abstract class which can be used as a start for TimePicker implementations
+     */
+    abstract static class AbstractTimePickerDelegate implements TimePickerDelegate {
+        protected final TimePicker mDelegator;
+        protected final Context mContext;
+        protected final Locale mLocale;
+
+        protected OnTimeChangedListener mOnTimeChangedListener;
+        protected OnTimeChangedListener mAutoFillChangeListener;
+
+        // The value that was passed to autofill() - it must be stored because it getAutofillValue()
+        // must return the exact same value that was autofilled, otherwise the widget will not be
+        // properly highlighted after autofill().
+        private long mAutofilledValue;
+
+        public AbstractTimePickerDelegate(@NonNull TimePicker delegator, @NonNull Context context) {
+            mDelegator = delegator;
+            mContext = context;
+            mLocale = context.getResources().getConfiguration().locale;
+        }
+
+        @Override
+        public void setOnTimeChangedListener(OnTimeChangedListener callback) {
+            mOnTimeChangedListener = callback;
+        }
+
+        @Override
+        public void setAutoFillChangeListener(OnTimeChangedListener callback) {
+            mAutoFillChangeListener = callback;
+        }
+
+        @Override
+        public final void autofill(AutofillValue value) {
+            if (value == null || !value.isDate()) {
+                Log.w(LOG_TAG, value + " could not be autofilled into " + this);
+                return;
+            }
+
+            final long time = value.getDateValue();
+
+            final Calendar cal = Calendar.getInstance(mLocale);
+            cal.setTimeInMillis(time);
+            setDate(cal.get(Calendar.HOUR_OF_DAY), cal.get(Calendar.MINUTE));
+
+            // Must set mAutofilledValue *after* calling subclass method to make sure the value
+            // returned by getAutofillValue() matches it.
+            mAutofilledValue = time;
+        }
+
+        @Override
+        public final AutofillValue getAutofillValue() {
+            if (mAutofilledValue != 0) {
+                return AutofillValue.forDate(mAutofilledValue);
+            }
+
+            final Calendar cal = Calendar.getInstance(mLocale);
+            cal.set(Calendar.HOUR_OF_DAY, getHour());
+            cal.set(Calendar.MINUTE, getMinute());
+            return AutofillValue.forDate(cal.getTimeInMillis());
+        }
+
+        /**
+         * This method must be called every time the value of the hour and/or minute is changed by
+         * a subclass method.
+         */
+        protected void resetAutofilledValue() {
+            mAutofilledValue = 0;
+        }
+
+        protected static class SavedState extends View.BaseSavedState {
+            private final int mHour;
+            private final int mMinute;
+            private final boolean mIs24HourMode;
+            private final int mCurrentItemShowing;
+
+            public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode) {
+                this(superState, hour, minute, is24HourMode, 0);
+            }
+
+            public SavedState(Parcelable superState, int hour, int minute, boolean is24HourMode,
+                    int currentItemShowing) {
+                super(superState);
+                mHour = hour;
+                mMinute = minute;
+                mIs24HourMode = is24HourMode;
+                mCurrentItemShowing = currentItemShowing;
+            }
+
+            private SavedState(Parcel in) {
+                super(in);
+                mHour = in.readInt();
+                mMinute = in.readInt();
+                mIs24HourMode = (in.readInt() == 1);
+                mCurrentItemShowing = in.readInt();
+            }
+
+            public int getHour() {
+                return mHour;
+            }
+
+            public int getMinute() {
+                return mMinute;
+            }
+
+            public boolean is24HourMode() {
+                return mIs24HourMode;
+            }
+
+            public int getCurrentItemShowing() {
+                return mCurrentItemShowing;
+            }
+
+            @Override
+            public void writeToParcel(Parcel dest, int flags) {
+                super.writeToParcel(dest, flags);
+                dest.writeInt(mHour);
+                dest.writeInt(mMinute);
+                dest.writeInt(mIs24HourMode ? 1 : 0);
+                dest.writeInt(mCurrentItemShowing);
+            }
+
+            @SuppressWarnings({"unused", "hiding"})
+            public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+                public SavedState createFromParcel(Parcel in) {
+                    return new SavedState(in);
+                }
+
+                public SavedState[] newArray(int size) {
+                    return new SavedState[size];
+                }
+            };
+        }
+    }
+
+    @Override
+    public void dispatchProvideAutofillStructure(ViewStructure structure, int flags) {
+        // This view is self-sufficient for autofill, so it needs to call
+        // onProvideAutoFillStructure() to fill itself, but it does not need to call
+        // dispatchProvideAutoFillStructure() to fill its children.
+        structure.setAutofillId(getAutofillId());
+        onProvideAutofillStructure(structure, flags);
+    }
+
+    @Override
+    public void autofill(AutofillValue value) {
+        if (!isEnabled()) return;
+
+        mDelegate.autofill(value);
+    }
+
+    @Override
+    public @AutofillType int getAutofillType() {
+        return isEnabled() ? AUTOFILL_TYPE_DATE : AUTOFILL_TYPE_NONE;
+    }
+
+    @Override
+    public AutofillValue getAutofillValue() {
+        return isEnabled() ? mDelegate.getAutofillValue() : null;
+    }
+}
diff --git a/android/widget/TimePickerClockDelegate.java b/android/widget/TimePickerClockDelegate.java
new file mode 100644
index 0000000..706b0ce
--- /dev/null
+++ b/android/widget/TimePickerClockDelegate.java
@@ -0,0 +1,1087 @@
+/*
+ * Copyright (C) 2013 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.Nullable;
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.res.ColorStateList;
+import android.content.res.Resources;
+import android.content.res.TypedArray;
+import android.icu.text.DecimalFormatSymbols;
+import android.os.Parcelable;
+import android.text.SpannableStringBuilder;
+import android.text.TextUtils;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.text.style.TtsSpan;
+import android.util.AttributeSet;
+import android.util.StateSet;
+import android.view.HapticFeedbackConstants;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.AccessibilityDelegate;
+import android.view.View.MeasureSpec;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityNodeInfo;
+import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction;
+import android.view.inputmethod.InputMethodManager;
+import android.widget.RadialTimePickerView.OnValueSelectedListener;
+import android.widget.TextInputTimePickerView.OnValueTypedListener;
+
+import com.android.internal.R;
+import com.android.internal.widget.NumericTextView;
+import com.android.internal.widget.NumericTextView.OnValueChangedListener;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.util.Calendar;
+
+/**
+ * A delegate implementing the radial clock-based TimePicker.
+ */
+class TimePickerClockDelegate extends TimePicker.AbstractTimePickerDelegate {
+    /**
+     * Delay in milliseconds before valid but potentially incomplete, for
+     * example "1" but not "12", keyboard edits are propagated from the
+     * hour / minute fields to the radial picker.
+     */
+    private static final long DELAY_COMMIT_MILLIS = 2000;
+
+    @IntDef({FROM_EXTERNAL_API, FROM_RADIAL_PICKER, FROM_INPUT_PICKER})
+    @Retention(RetentionPolicy.SOURCE)
+    private @interface ChangeSource {}
+    private static final int FROM_EXTERNAL_API = 0;
+    private static final int FROM_RADIAL_PICKER = 1;
+    private static final int FROM_INPUT_PICKER = 2;
+
+    // Index used by RadialPickerLayout
+    private static final int HOUR_INDEX = RadialTimePickerView.HOURS;
+    private static final int MINUTE_INDEX = RadialTimePickerView.MINUTES;
+
+    private static final int[] ATTRS_TEXT_COLOR = new int[] {R.attr.textColor};
+    private static final int[] ATTRS_DISABLED_ALPHA = new int[] {R.attr.disabledAlpha};
+
+    private static final int AM = 0;
+    private static final int PM = 1;
+
+    private static final int HOURS_IN_HALF_DAY = 12;
+
+    private final NumericTextView mHourView;
+    private final NumericTextView mMinuteView;
+    private final View mAmPmLayout;
+    private final RadioButton mAmLabel;
+    private final RadioButton mPmLabel;
+    private final RadialTimePickerView mRadialTimePickerView;
+    private final TextView mSeparatorView;
+
+    private boolean mRadialPickerModeEnabled = true;
+    private final ImageButton mRadialTimePickerModeButton;
+    private final String mRadialTimePickerModeEnabledDescription;
+    private final String mTextInputPickerModeEnabledDescription;
+    private final View mRadialTimePickerHeader;
+    private final View mTextInputPickerHeader;
+
+    private final TextInputTimePickerView mTextInputPickerView;
+
+    private final Calendar mTempCalendar;
+
+    // Accessibility strings.
+    private final String mSelectHours;
+    private final String mSelectMinutes;
+
+    private boolean mIsEnabled = true;
+    private boolean mAllowAutoAdvance;
+    private int mCurrentHour;
+    private int mCurrentMinute;
+    private boolean mIs24Hour;
+
+    // The portrait layout puts AM/PM at the right by default.
+    private boolean mIsAmPmAtLeft = false;
+    // The landscape layouts put AM/PM at the bottom by default.
+    private boolean mIsAmPmAtTop = false;
+
+    // Localization data.
+    private boolean mHourFormatShowLeadingZero;
+    private boolean mHourFormatStartsAtZero;
+
+    // Most recent time announcement values for accessibility.
+    private CharSequence mLastAnnouncedText;
+    private boolean mLastAnnouncedIsHour;
+
+    public TimePickerClockDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        // process style attributes
+        final TypedArray a = mContext.obtainStyledAttributes(attrs,
+                R.styleable.TimePicker, defStyleAttr, defStyleRes);
+        final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
+                Context.LAYOUT_INFLATER_SERVICE);
+        final Resources res = mContext.getResources();
+
+        mSelectHours = res.getString(R.string.select_hours);
+        mSelectMinutes = res.getString(R.string.select_minutes);
+
+        final int layoutResourceId = a.getResourceId(R.styleable.TimePicker_internalLayout,
+                R.layout.time_picker_material);
+        final View mainView = inflater.inflate(layoutResourceId, delegator);
+        mainView.setSaveFromParentEnabled(false);
+        mRadialTimePickerHeader = mainView.findViewById(R.id.time_header);
+        mRadialTimePickerHeader.setOnTouchListener(new NearestTouchDelegate());
+
+        // Set up hour/minute labels.
+        mHourView = (NumericTextView) mainView.findViewById(R.id.hours);
+        mHourView.setOnClickListener(mClickListener);
+        mHourView.setOnFocusChangeListener(mFocusListener);
+        mHourView.setOnDigitEnteredListener(mDigitEnteredListener);
+        mHourView.setAccessibilityDelegate(
+                new ClickActionDelegate(context, R.string.select_hours));
+        mSeparatorView = (TextView) mainView.findViewById(R.id.separator);
+        mMinuteView = (NumericTextView) mainView.findViewById(R.id.minutes);
+        mMinuteView.setOnClickListener(mClickListener);
+        mMinuteView.setOnFocusChangeListener(mFocusListener);
+        mMinuteView.setOnDigitEnteredListener(mDigitEnteredListener);
+        mMinuteView.setAccessibilityDelegate(
+                new ClickActionDelegate(context, R.string.select_minutes));
+        mMinuteView.setRange(0, 59);
+
+        // Set up AM/PM labels.
+        mAmPmLayout = mainView.findViewById(R.id.ampm_layout);
+        mAmPmLayout.setOnTouchListener(new NearestTouchDelegate());
+
+        final String[] amPmStrings = TimePicker.getAmPmStrings(context);
+        mAmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.am_label);
+        mAmLabel.setText(obtainVerbatim(amPmStrings[0]));
+        mAmLabel.setOnClickListener(mClickListener);
+        ensureMinimumTextWidth(mAmLabel);
+
+        mPmLabel = (RadioButton) mAmPmLayout.findViewById(R.id.pm_label);
+        mPmLabel.setText(obtainVerbatim(amPmStrings[1]));
+        mPmLabel.setOnClickListener(mClickListener);
+        ensureMinimumTextWidth(mPmLabel);
+
+        // For the sake of backwards compatibility, attempt to extract the text
+        // color from the header time text appearance. If it's set, we'll let
+        // that override the "real" header text color.
+        ColorStateList headerTextColor = null;
+
+        @SuppressWarnings("deprecation")
+        final int timeHeaderTextAppearance = a.getResourceId(
+                R.styleable.TimePicker_headerTimeTextAppearance, 0);
+        if (timeHeaderTextAppearance != 0) {
+            final TypedArray textAppearance = mContext.obtainStyledAttributes(null,
+                    ATTRS_TEXT_COLOR, 0, timeHeaderTextAppearance);
+            final ColorStateList legacyHeaderTextColor = textAppearance.getColorStateList(0);
+            headerTextColor = applyLegacyColorFixes(legacyHeaderTextColor);
+            textAppearance.recycle();
+        }
+
+        if (headerTextColor == null) {
+            headerTextColor = a.getColorStateList(R.styleable.TimePicker_headerTextColor);
+        }
+
+        mTextInputPickerHeader = mainView.findViewById(R.id.input_header);
+
+        if (headerTextColor != null) {
+            mHourView.setTextColor(headerTextColor);
+            mSeparatorView.setTextColor(headerTextColor);
+            mMinuteView.setTextColor(headerTextColor);
+            mAmLabel.setTextColor(headerTextColor);
+            mPmLabel.setTextColor(headerTextColor);
+        }
+
+        // Set up header background, if available.
+        if (a.hasValueOrEmpty(R.styleable.TimePicker_headerBackground)) {
+            mRadialTimePickerHeader.setBackground(a.getDrawable(
+                    R.styleable.TimePicker_headerBackground));
+            mTextInputPickerHeader.setBackground(a.getDrawable(
+                    R.styleable.TimePicker_headerBackground));
+        }
+
+        a.recycle();
+
+        mRadialTimePickerView = (RadialTimePickerView) mainView.findViewById(R.id.radial_picker);
+        mRadialTimePickerView.applyAttributes(attrs, defStyleAttr, defStyleRes);
+        mRadialTimePickerView.setOnValueSelectedListener(mOnValueSelectedListener);
+
+        mTextInputPickerView = (TextInputTimePickerView) mainView.findViewById(R.id.input_mode);
+        mTextInputPickerView.setListener(mOnValueTypedListener);
+
+        mRadialTimePickerModeButton =
+                (ImageButton) mainView.findViewById(R.id.toggle_mode);
+        mRadialTimePickerModeButton.setOnClickListener(new View.OnClickListener() {
+            @Override
+            public void onClick(View v) {
+                toggleRadialPickerMode();
+            }
+        });
+        mRadialTimePickerModeEnabledDescription = context.getResources().getString(
+                R.string.time_picker_radial_mode_description);
+        mTextInputPickerModeEnabledDescription = context.getResources().getString(
+                R.string.time_picker_text_input_mode_description);
+
+        mAllowAutoAdvance = true;
+
+        updateHourFormat();
+
+        // Initialize with current time.
+        mTempCalendar = Calendar.getInstance(mLocale);
+        final int currentHour = mTempCalendar.get(Calendar.HOUR_OF_DAY);
+        final int currentMinute = mTempCalendar.get(Calendar.MINUTE);
+        initialize(currentHour, currentMinute, mIs24Hour, HOUR_INDEX);
+    }
+
+    private void toggleRadialPickerMode() {
+        if (mRadialPickerModeEnabled) {
+            mRadialTimePickerView.setVisibility(View.GONE);
+            mRadialTimePickerHeader.setVisibility(View.GONE);
+            mTextInputPickerHeader.setVisibility(View.VISIBLE);
+            mTextInputPickerView.setVisibility(View.VISIBLE);
+            mRadialTimePickerModeButton.setImageResource(R.drawable.btn_clock_material);
+            mRadialTimePickerModeButton.setContentDescription(
+                    mRadialTimePickerModeEnabledDescription);
+            mRadialPickerModeEnabled = false;
+        } else {
+            mRadialTimePickerView.setVisibility(View.VISIBLE);
+            mRadialTimePickerHeader.setVisibility(View.VISIBLE);
+            mTextInputPickerHeader.setVisibility(View.GONE);
+            mTextInputPickerView.setVisibility(View.GONE);
+            mRadialTimePickerModeButton.setImageResource(R.drawable.btn_keyboard_key_material);
+            mRadialTimePickerModeButton.setContentDescription(
+                    mTextInputPickerModeEnabledDescription);
+            updateTextInputPicker();
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (imm != null) {
+                imm.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            }
+            mRadialPickerModeEnabled = true;
+        }
+    }
+
+    @Override
+    public boolean validateInput() {
+        return mTextInputPickerView.validateInput();
+    }
+
+    /**
+     * Ensures that a TextView is wide enough to contain its text without
+     * wrapping or clipping. Measures the specified view and sets the minimum
+     * width to the view's desired width.
+     *
+     * @param v the text view to measure
+     */
+    private static void ensureMinimumTextWidth(TextView v) {
+        v.measure(MeasureSpec.UNSPECIFIED, MeasureSpec.UNSPECIFIED);
+
+        // Set both the TextView and the View version of minimum
+        // width because they are subtly different.
+        final int minWidth = v.getMeasuredWidth();
+        v.setMinWidth(minWidth);
+        v.setMinimumWidth(minWidth);
+    }
+
+    /**
+     * Updates hour formatting based on the current locale and 24-hour mode.
+     * <p>
+     * Determines how the hour should be formatted, sets member variables for
+     * leading zero and starting hour, and sets the hour view's presentation.
+     */
+    private void updateHourFormat() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(
+                mLocale, mIs24Hour ? "Hm" : "hm");
+        final int lengthPattern = bestDateTimePattern.length();
+        boolean showLeadingZero = false;
+        char hourFormat = '\0';
+
+        for (int i = 0; i < lengthPattern; i++) {
+            final char c = bestDateTimePattern.charAt(i);
+            if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+                hourFormat = c;
+                if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+                    showLeadingZero = true;
+                }
+                break;
+            }
+        }
+
+        mHourFormatShowLeadingZero = showLeadingZero;
+        mHourFormatStartsAtZero = hourFormat == 'K' || hourFormat == 'H';
+
+        // Update hour text field.
+        final int minHour = mHourFormatStartsAtZero ? 0 : 1;
+        final int maxHour = (mIs24Hour ? 23 : 11) + minHour;
+        mHourView.setRange(minHour, maxHour);
+        mHourView.setShowLeadingZeroes(mHourFormatShowLeadingZero);
+
+        final String[] digits = DecimalFormatSymbols.getInstance(mLocale).getDigitStrings();
+        int maxCharLength = 0;
+        for (int i = 0; i < 10; i++) {
+            maxCharLength = Math.max(maxCharLength, digits[i].length());
+        }
+        mTextInputPickerView.setHourFormat(maxCharLength * 2);
+    }
+
+    static final CharSequence obtainVerbatim(String text) {
+        return new SpannableStringBuilder().append(text,
+                new TtsSpan.VerbatimBuilder(text).build(), 0);
+    }
+
+    /**
+     * The legacy text color might have been poorly defined. Ensures that it
+     * has an appropriate activated state, using the selected state if one
+     * exists or modifying the default text color otherwise.
+     *
+     * @param color a legacy text color, or {@code null}
+     * @return a color state list with an appropriate activated state, or
+     *         {@code null} if a valid activated state could not be generated
+     */
+    @Nullable
+    private ColorStateList applyLegacyColorFixes(@Nullable ColorStateList color) {
+        if (color == null || color.hasState(R.attr.state_activated)) {
+            return color;
+        }
+
+        final int activatedColor;
+        final int defaultColor;
+        if (color.hasState(R.attr.state_selected)) {
+            activatedColor = color.getColorForState(StateSet.get(
+                    StateSet.VIEW_STATE_ENABLED | StateSet.VIEW_STATE_SELECTED), 0);
+            defaultColor = color.getColorForState(StateSet.get(
+                    StateSet.VIEW_STATE_ENABLED), 0);
+        } else {
+            activatedColor = color.getDefaultColor();
+
+            // Generate a non-activated color using the disabled alpha.
+            final TypedArray ta = mContext.obtainStyledAttributes(ATTRS_DISABLED_ALPHA);
+            final float disabledAlpha = ta.getFloat(0, 0.30f);
+            defaultColor = multiplyAlphaComponent(activatedColor, disabledAlpha);
+        }
+
+        if (activatedColor == 0 || defaultColor == 0) {
+            // We somehow failed to obtain the colors.
+            return null;
+        }
+
+        final int[][] stateSet = new int[][] {{ R.attr.state_activated }, {}};
+        final int[] colors = new int[] { activatedColor, defaultColor };
+        return new ColorStateList(stateSet, colors);
+    }
+
+    private int multiplyAlphaComponent(int color, float alphaMod) {
+        final int srcRgb = color & 0xFFFFFF;
+        final int srcAlpha = (color >> 24) & 0xFF;
+        final int dstAlpha = (int) (srcAlpha * alphaMod + 0.5f);
+        return srcRgb | (dstAlpha << 24);
+    }
+
+    private static class ClickActionDelegate extends AccessibilityDelegate {
+        private final AccessibilityAction mClickAction;
+
+        public ClickActionDelegate(Context context, int resId) {
+            mClickAction = new AccessibilityAction(
+                    AccessibilityNodeInfo.ACTION_CLICK, context.getString(resId));
+        }
+
+        @Override
+        public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+            super.onInitializeAccessibilityNodeInfo(host, info);
+
+            info.addAction(mClickAction);
+        }
+    }
+
+    private void initialize(int hourOfDay, int minute, boolean is24HourView, int index) {
+        mCurrentHour = hourOfDay;
+        mCurrentMinute = minute;
+        mIs24Hour = is24HourView;
+        updateUI(index);
+    }
+
+    private void updateUI(int index) {
+        updateHeaderAmPm();
+        updateHeaderHour(mCurrentHour, false);
+        updateHeaderSeparator();
+        updateHeaderMinute(mCurrentMinute, false);
+        updateRadialPicker(index);
+        updateTextInputPicker();
+
+        mDelegator.invalidate();
+    }
+
+    private void updateTextInputPicker() {
+        mTextInputPickerView.updateTextInputValues(getLocalizedHour(mCurrentHour), mCurrentMinute,
+                mCurrentHour < 12 ? AM : PM, mIs24Hour, mHourFormatStartsAtZero);
+    }
+
+    private void updateRadialPicker(int index) {
+        mRadialTimePickerView.initialize(mCurrentHour, mCurrentMinute, mIs24Hour);
+        setCurrentItemShowing(index, false, true);
+    }
+
+    private void updateHeaderAmPm() {
+        if (mIs24Hour) {
+            mAmPmLayout.setVisibility(View.GONE);
+        } else {
+            // Find the location of AM/PM based on locale information.
+            final String dateTimePattern = DateFormat.getBestDateTimePattern(mLocale, "hm");
+            final boolean isAmPmAtStart = dateTimePattern.startsWith("a");
+            setAmPmStart(isAmPmAtStart);
+            updateAmPmLabelStates(mCurrentHour < 12 ? AM : PM);
+        }
+    }
+
+    private void setAmPmStart(boolean isAmPmAtStart) {
+        final RelativeLayout.LayoutParams params =
+                (RelativeLayout.LayoutParams) mAmPmLayout.getLayoutParams();
+        if (params.getRule(RelativeLayout.RIGHT_OF) != 0
+                || params.getRule(RelativeLayout.LEFT_OF) != 0) {
+            // Horizontal mode, with AM/PM appearing to left/right of hours and minutes.
+            final boolean isAmPmAtLeft;
+            if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_LTR) {
+                isAmPmAtLeft = isAmPmAtStart;
+            } else {
+                isAmPmAtLeft = !isAmPmAtStart;
+            }
+            if (mIsAmPmAtLeft == isAmPmAtLeft) {
+                // AM/PM is already at the correct location. No change needed.
+                return;
+            }
+
+            if (isAmPmAtLeft) {
+                params.removeRule(RelativeLayout.RIGHT_OF);
+                params.addRule(RelativeLayout.LEFT_OF, mHourView.getId());
+            } else {
+                params.removeRule(RelativeLayout.LEFT_OF);
+                params.addRule(RelativeLayout.RIGHT_OF, mMinuteView.getId());
+            }
+            mIsAmPmAtLeft = isAmPmAtLeft;
+        } else if (params.getRule(RelativeLayout.BELOW) != 0
+                || params.getRule(RelativeLayout.ABOVE) != 0) {
+            // Vertical mode, with AM/PM appearing to top/bottom of hours and minutes.
+            if (mIsAmPmAtTop == isAmPmAtStart) {
+                // AM/PM is already at the correct location. No change needed.
+                return;
+            }
+
+            final int otherViewId;
+            if (isAmPmAtStart) {
+                otherViewId = params.getRule(RelativeLayout.BELOW);
+                params.removeRule(RelativeLayout.BELOW);
+                params.addRule(RelativeLayout.ABOVE, otherViewId);
+            } else {
+                otherViewId = params.getRule(RelativeLayout.ABOVE);
+                params.removeRule(RelativeLayout.ABOVE);
+                params.addRule(RelativeLayout.BELOW, otherViewId);
+            }
+
+            // Switch the top and bottom paddings on the other view.
+            final View otherView = mRadialTimePickerHeader.findViewById(otherViewId);
+            final int top = otherView.getPaddingTop();
+            final int bottom = otherView.getPaddingBottom();
+            final int left = otherView.getPaddingLeft();
+            final int right = otherView.getPaddingRight();
+            otherView.setPadding(left, bottom, right, top);
+
+            mIsAmPmAtTop = isAmPmAtStart;
+        }
+
+        mAmPmLayout.setLayoutParams(params);
+    }
+
+    @Override
+    public void setDate(int hour, int minute) {
+        setHourInternal(hour, FROM_EXTERNAL_API, true, false);
+        setMinuteInternal(minute, FROM_EXTERNAL_API, false);
+
+        onTimeChanged();
+    }
+
+    /**
+     * Set the current hour.
+     */
+    @Override
+    public void setHour(int hour) {
+        setHourInternal(hour, FROM_EXTERNAL_API, true, true);
+    }
+
+    private void setHourInternal(int hour, @ChangeSource int source, boolean announce,
+            boolean notify) {
+        if (mCurrentHour == hour) {
+            return;
+        }
+
+        resetAutofilledValue();
+        mCurrentHour = hour;
+        updateHeaderHour(hour, announce);
+        updateHeaderAmPm();
+
+        if (source != FROM_RADIAL_PICKER) {
+            mRadialTimePickerView.setCurrentHour(hour);
+            mRadialTimePickerView.setAmOrPm(hour < 12 ? AM : PM);
+        }
+        if (source != FROM_INPUT_PICKER) {
+            updateTextInputPicker();
+        }
+
+        mDelegator.invalidate();
+        if (notify) {
+            onTimeChanged();
+        }
+    }
+
+    /**
+     * @return the current hour in the range (0-23)
+     */
+    @Override
+    public int getHour() {
+        final int currentHour = mRadialTimePickerView.getCurrentHour();
+        if (mIs24Hour) {
+            return currentHour;
+        }
+
+        if (mRadialTimePickerView.getAmOrPm() == PM) {
+            return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+        } else {
+            return currentHour % HOURS_IN_HALF_DAY;
+        }
+    }
+
+    /**
+     * Set the current minute (0-59).
+     */
+    @Override
+    public void setMinute(int minute) {
+        setMinuteInternal(minute, FROM_EXTERNAL_API, true);
+    }
+
+    private void setMinuteInternal(int minute, @ChangeSource int source, boolean notify) {
+        if (mCurrentMinute == minute) {
+            return;
+        }
+
+        resetAutofilledValue();
+        mCurrentMinute = minute;
+        updateHeaderMinute(minute, true);
+
+        if (source != FROM_RADIAL_PICKER) {
+            mRadialTimePickerView.setCurrentMinute(minute);
+        }
+        if (source != FROM_INPUT_PICKER) {
+            updateTextInputPicker();
+        }
+
+        mDelegator.invalidate();
+        if (notify) {
+            onTimeChanged();
+        }
+    }
+
+    /**
+     * @return The current minute.
+     */
+    @Override
+    public int getMinute() {
+        return mRadialTimePickerView.getCurrentMinute();
+    }
+
+    /**
+     * Sets whether time is displayed in 24-hour mode or 12-hour mode with
+     * AM/PM indicators.
+     *
+     * @param is24Hour {@code true} to display time in 24-hour mode or
+     *        {@code false} for 12-hour mode with AM/PM
+     */
+    public void setIs24Hour(boolean is24Hour) {
+        if (mIs24Hour != is24Hour) {
+            mIs24Hour = is24Hour;
+            mCurrentHour = getHour();
+
+            updateHourFormat();
+            updateUI(mRadialTimePickerView.getCurrentItemShowing());
+        }
+    }
+
+    /**
+     * @return {@code true} if time is displayed in 24-hour mode, or
+     *         {@code false} if time is displayed in 12-hour mode with AM/PM
+     *         indicators
+     */
+    @Override
+    public boolean is24Hour() {
+        return mIs24Hour;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mHourView.setEnabled(enabled);
+        mMinuteView.setEnabled(enabled);
+        mAmLabel.setEnabled(enabled);
+        mPmLabel.setEnabled(enabled);
+        mRadialTimePickerView.setEnabled(enabled);
+        mIsEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    @Override
+    public int getBaseline() {
+        // does not support baseline alignment
+        return -1;
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        return new SavedState(superState, getHour(), getMinute(),
+                is24Hour(), getCurrentItemShowing());
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            final SavedState ss = (SavedState) state;
+            initialize(ss.getHour(), ss.getMinute(), ss.is24HourMode(), ss.getCurrentItemShowing());
+            mRadialTimePickerView.invalidate();
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    @Override
+    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+        int flags = DateUtils.FORMAT_SHOW_TIME;
+        if (mIs24Hour) {
+            flags |= DateUtils.FORMAT_24HOUR;
+        } else {
+            flags |= DateUtils.FORMAT_12HOUR;
+        }
+
+        mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
+        mTempCalendar.set(Calendar.MINUTE, getMinute());
+
+        final String selectedTime = DateUtils.formatDateTime(mContext,
+                mTempCalendar.getTimeInMillis(), flags);
+        final String selectionMode = mRadialTimePickerView.getCurrentItemShowing() == HOUR_INDEX ?
+                mSelectHours : mSelectMinutes;
+        event.getText().add(selectedTime + " " + selectionMode);
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getHourView() {
+        return mHourView;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getMinuteView() {
+        return mMinuteView;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getAmView() {
+        return mAmLabel;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getPmView() {
+        return mPmLabel;
+    }
+
+    /**
+     * @return the index of the current item showing
+     */
+    private int getCurrentItemShowing() {
+        return mRadialTimePickerView.getCurrentItemShowing();
+    }
+
+    /**
+     * Propagate the time change
+     */
+    private void onTimeChanged() {
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        if (mOnTimeChangedListener != null) {
+            mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+        }
+        if (mAutoFillChangeListener != null) {
+            mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
+        }
+    }
+
+    private void tryVibrate() {
+        mDelegator.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK);
+    }
+
+    private void updateAmPmLabelStates(int amOrPm) {
+        final boolean isAm = amOrPm == AM;
+        mAmLabel.setActivated(isAm);
+        mAmLabel.setChecked(isAm);
+
+        final boolean isPm = amOrPm == PM;
+        mPmLabel.setActivated(isPm);
+        mPmLabel.setChecked(isPm);
+    }
+
+    /**
+     * Converts hour-of-day (0-23) time into a localized hour number.
+     * <p>
+     * The localized value may be in the range (0-23), (1-24), (0-11), or
+     * (1-12) depending on the locale. This method does not handle leading
+     * zeroes.
+     *
+     * @param hourOfDay the hour-of-day (0-23)
+     * @return a localized hour number
+     */
+    private int getLocalizedHour(int hourOfDay) {
+        if (!mIs24Hour) {
+            // Convert to hour-of-am-pm.
+            hourOfDay %= 12;
+        }
+
+        if (!mHourFormatStartsAtZero && hourOfDay == 0) {
+            // Convert to clock-hour (either of-day or of-am-pm).
+            hourOfDay = mIs24Hour ? 24 : 12;
+        }
+
+        return hourOfDay;
+    }
+
+    private void updateHeaderHour(int hourOfDay, boolean announce) {
+        final int localizedHour = getLocalizedHour(hourOfDay);
+        mHourView.setValue(localizedHour);
+
+        if (announce) {
+            tryAnnounceForAccessibility(mHourView.getText(), true);
+        }
+    }
+
+    private void updateHeaderMinute(int minuteOfHour, boolean announce) {
+        mMinuteView.setValue(minuteOfHour);
+
+        if (announce) {
+            tryAnnounceForAccessibility(mMinuteView.getText(), false);
+        }
+    }
+
+    /**
+     * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+     *
+     * See http://unicode.org/cldr/trac/browser/trunk/common/main
+     *
+     * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+     * separator as the character which is just after the hour marker in the returned pattern.
+     */
+    private void updateHeaderSeparator() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+                (mIs24Hour) ? "Hm" : "hm");
+        final String separatorText;
+        // See http://www.unicode.org/reports/tr35/tr35-dates.html for hour formats
+        final char[] hourFormats = {'H', 'h', 'K', 'k'};
+        int hIndex = lastIndexOfAny(bestDateTimePattern, hourFormats);
+        if (hIndex == -1) {
+            // Default case
+            separatorText = ":";
+        } else {
+            separatorText = Character.toString(bestDateTimePattern.charAt(hIndex + 1));
+        }
+        mSeparatorView.setText(separatorText);
+        mTextInputPickerView.updateSeparator(separatorText);
+    }
+
+    static private int lastIndexOfAny(String str, char[] any) {
+        final int lengthAny = any.length;
+        if (lengthAny > 0) {
+            for (int i = str.length() - 1; i >= 0; i--) {
+                char c = str.charAt(i);
+                for (int j = 0; j < lengthAny; j++) {
+                    if (c == any[j]) {
+                        return i;
+                    }
+                }
+            }
+        }
+        return -1;
+    }
+
+    private void tryAnnounceForAccessibility(CharSequence text, boolean isHour) {
+        if (mLastAnnouncedIsHour != isHour || !text.equals(mLastAnnouncedText)) {
+            // TODO: Find a better solution, potentially live regions?
+            mDelegator.announceForAccessibility(text);
+            mLastAnnouncedText = text;
+            mLastAnnouncedIsHour = isHour;
+        }
+    }
+
+    /**
+     * Show either Hours or Minutes.
+     */
+    private void setCurrentItemShowing(int index, boolean animateCircle, boolean announce) {
+        mRadialTimePickerView.setCurrentItemShowing(index, animateCircle);
+
+        if (index == HOUR_INDEX) {
+            if (announce) {
+                mDelegator.announceForAccessibility(mSelectHours);
+            }
+        } else {
+            if (announce) {
+                mDelegator.announceForAccessibility(mSelectMinutes);
+            }
+        }
+
+        mHourView.setActivated(index == HOUR_INDEX);
+        mMinuteView.setActivated(index == MINUTE_INDEX);
+    }
+
+    private void setAmOrPm(int amOrPm) {
+        updateAmPmLabelStates(amOrPm);
+
+        if (mRadialTimePickerView.setAmOrPm(amOrPm)) {
+            mCurrentHour = getHour();
+            updateTextInputPicker();
+            if (mOnTimeChangedListener != null) {
+                mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+            }
+        }
+    }
+
+    /** Listener for RadialTimePickerView interaction. */
+    private final OnValueSelectedListener mOnValueSelectedListener = new OnValueSelectedListener() {
+        @Override
+        public void onValueSelected(int pickerType, int newValue, boolean autoAdvance) {
+            boolean valueChanged = false;
+            switch (pickerType) {
+                case RadialTimePickerView.HOURS:
+                    if (getHour() != newValue) {
+                        valueChanged = true;
+                    }
+                    final boolean isTransition = mAllowAutoAdvance && autoAdvance;
+                    setHourInternal(newValue, FROM_RADIAL_PICKER, !isTransition, true);
+                    if (isTransition) {
+                        setCurrentItemShowing(MINUTE_INDEX, true, false);
+
+                        final int localizedHour = getLocalizedHour(newValue);
+                        mDelegator.announceForAccessibility(localizedHour + ". " + mSelectMinutes);
+                    }
+                    break;
+                case RadialTimePickerView.MINUTES:
+                    if (getMinute() != newValue) {
+                        valueChanged = true;
+                    }
+                    setMinuteInternal(newValue, FROM_RADIAL_PICKER, true);
+                    break;
+            }
+
+            if (mOnTimeChangedListener != null && valueChanged) {
+                mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(), getMinute());
+            }
+        }
+    };
+
+    private final OnValueTypedListener mOnValueTypedListener = new OnValueTypedListener() {
+        @Override
+        public void onValueChanged(int pickerType, int newValue) {
+            switch (pickerType) {
+                case TextInputTimePickerView.HOURS:
+                    setHourInternal(newValue, FROM_INPUT_PICKER, false, true);
+                    break;
+                case TextInputTimePickerView.MINUTES:
+                    setMinuteInternal(newValue, FROM_INPUT_PICKER, true);
+                    break;
+                case TextInputTimePickerView.AMPM:
+                    setAmOrPm(newValue);
+                    break;
+            }
+        }
+    };
+
+    /** Listener for keyboard interaction. */
+    private final OnValueChangedListener mDigitEnteredListener = new OnValueChangedListener() {
+        @Override
+        public void onValueChanged(NumericTextView view, int value,
+                boolean isValid, boolean isFinished) {
+            final Runnable commitCallback;
+            final View nextFocusTarget;
+            if (view == mHourView) {
+                commitCallback = mCommitHour;
+                nextFocusTarget = view.isFocused() ? mMinuteView : null;
+            } else if (view == mMinuteView) {
+                commitCallback = mCommitMinute;
+                nextFocusTarget = null;
+            } else {
+                return;
+            }
+
+            view.removeCallbacks(commitCallback);
+
+            if (isValid) {
+                if (isFinished) {
+                    // Done with hours entry, make visual updates
+                    // immediately and move to next focus if needed.
+                    commitCallback.run();
+
+                    if (nextFocusTarget != null) {
+                        nextFocusTarget.requestFocus();
+                    }
+                } else {
+                    // May still be making changes. Postpone visual
+                    // updates to prevent distracting the user.
+                    view.postDelayed(commitCallback, DELAY_COMMIT_MILLIS);
+                }
+            }
+        }
+    };
+
+    private final Runnable mCommitHour = new Runnable() {
+        @Override
+        public void run() {
+            setHour(mHourView.getValue());
+        }
+    };
+
+    private final Runnable mCommitMinute = new Runnable() {
+        @Override
+        public void run() {
+            setMinute(mMinuteView.getValue());
+        }
+    };
+
+    private final View.OnFocusChangeListener mFocusListener = new View.OnFocusChangeListener() {
+        @Override
+        public void onFocusChange(View v, boolean focused) {
+            if (focused) {
+                switch (v.getId()) {
+                    case R.id.am_label:
+                        setAmOrPm(AM);
+                        break;
+                    case R.id.pm_label:
+                        setAmOrPm(PM);
+                        break;
+                    case R.id.hours:
+                        setCurrentItemShowing(HOUR_INDEX, true, true);
+                        break;
+                    case R.id.minutes:
+                        setCurrentItemShowing(MINUTE_INDEX, true, true);
+                        break;
+                    default:
+                        // Failed to handle this click, don't vibrate.
+                        return;
+                }
+
+                tryVibrate();
+            }
+        }
+    };
+
+    private final View.OnClickListener mClickListener = new View.OnClickListener() {
+        @Override
+        public void onClick(View v) {
+
+            final int amOrPm;
+            switch (v.getId()) {
+                case R.id.am_label:
+                    setAmOrPm(AM);
+                    break;
+                case R.id.pm_label:
+                    setAmOrPm(PM);
+                    break;
+                case R.id.hours:
+                    setCurrentItemShowing(HOUR_INDEX, true, true);
+                    break;
+                case R.id.minutes:
+                    setCurrentItemShowing(MINUTE_INDEX, true, true);
+                    break;
+                default:
+                    // Failed to handle this click, don't vibrate.
+                    return;
+            }
+
+            tryVibrate();
+        }
+    };
+
+    /**
+     * Delegates unhandled touches in a view group to the nearest child view.
+     */
+    private static class NearestTouchDelegate implements View.OnTouchListener {
+            private View mInitialTouchTarget;
+
+            @Override
+            public boolean onTouch(View view, MotionEvent motionEvent) {
+                final int actionMasked = motionEvent.getActionMasked();
+                if (actionMasked == MotionEvent.ACTION_DOWN) {
+                    if (view instanceof ViewGroup) {
+                        mInitialTouchTarget = findNearestChild((ViewGroup) view,
+                                (int) motionEvent.getX(), (int) motionEvent.getY());
+                    } else {
+                        mInitialTouchTarget = null;
+                    }
+                }
+
+                final View child = mInitialTouchTarget;
+                if (child == null) {
+                    return false;
+                }
+
+                final float offsetX = view.getScrollX() - child.getLeft();
+                final float offsetY = view.getScrollY() - child.getTop();
+                motionEvent.offsetLocation(offsetX, offsetY);
+                final boolean handled = child.dispatchTouchEvent(motionEvent);
+                motionEvent.offsetLocation(-offsetX, -offsetY);
+
+                if (actionMasked == MotionEvent.ACTION_UP
+                        || actionMasked == MotionEvent.ACTION_CANCEL) {
+                    mInitialTouchTarget = null;
+                }
+
+                return handled;
+            }
+
+        private View findNearestChild(ViewGroup v, int x, int y) {
+            View bestChild = null;
+            int bestDist = Integer.MAX_VALUE;
+
+            for (int i = 0, count = v.getChildCount(); i < count; i++) {
+                final View child = v.getChildAt(i);
+                final int dX = x - (child.getLeft() + child.getWidth() / 2);
+                final int dY = y - (child.getTop() + child.getHeight() / 2);
+                final int dist = dX * dX + dY * dY;
+                if (bestDist > dist) {
+                    bestChild = child;
+                    bestDist = dist;
+                }
+            }
+
+            return bestChild;
+        }
+    }
+}
diff --git a/android/widget/TimePickerSpinnerDelegate.java b/android/widget/TimePickerSpinnerDelegate.java
new file mode 100644
index 0000000..cc79b9c
--- /dev/null
+++ b/android/widget/TimePickerSpinnerDelegate.java
@@ -0,0 +1,585 @@
+/*
+ * Copyright (C) 2013 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 android.widget;
+
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_AUTO;
+import static android.view.View.IMPORTANT_FOR_ACCESSIBILITY_YES;
+
+import android.annotation.TestApi;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.os.Parcelable;
+import android.text.format.DateFormat;
+import android.text.format.DateUtils;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodManager;
+
+import com.android.internal.R;
+
+import libcore.icu.LocaleData;
+
+import java.util.Calendar;
+
+/**
+ * A delegate implementing the basic spinner-based TimePicker.
+ */
+class TimePickerSpinnerDelegate extends TimePicker.AbstractTimePickerDelegate {
+    private static final boolean DEFAULT_ENABLED_STATE = true;
+    private static final int HOURS_IN_HALF_DAY = 12;
+
+    private final NumberPicker mHourSpinner;
+    private final NumberPicker mMinuteSpinner;
+    private final NumberPicker mAmPmSpinner;
+    private final EditText mHourSpinnerInput;
+    private final EditText mMinuteSpinnerInput;
+    private final EditText mAmPmSpinnerInput;
+    private final TextView mDivider;
+
+    // Note that the legacy implementation of the TimePicker is
+    // using a button for toggling between AM/PM while the new
+    // version uses a NumberPicker spinner. Therefore the code
+    // accommodates these two cases to be backwards compatible.
+    private final Button mAmPmButton;
+
+    private final String[] mAmPmStrings;
+
+    private final Calendar mTempCalendar;
+
+    private boolean mIsEnabled = DEFAULT_ENABLED_STATE;
+    private boolean mHourWithTwoDigit;
+    private char mHourFormat;
+
+    private boolean mIs24HourView;
+    private boolean mIsAm;
+
+    public TimePickerSpinnerDelegate(TimePicker delegator, Context context, AttributeSet attrs,
+            int defStyleAttr, int defStyleRes) {
+        super(delegator, context);
+
+        // process style attributes
+        final TypedArray a = mContext.obtainStyledAttributes(
+                attrs, R.styleable.TimePicker, defStyleAttr, defStyleRes);
+        final int layoutResourceId = a.getResourceId(
+                R.styleable.TimePicker_legacyLayout, R.layout.time_picker_legacy);
+        a.recycle();
+
+        final LayoutInflater inflater = LayoutInflater.from(mContext);
+        final View view = inflater.inflate(layoutResourceId, mDelegator, true);
+        view.setSaveFromParentEnabled(false);
+
+        // hour
+        mHourSpinner = delegator.findViewById(R.id.hour);
+        mHourSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+                updateInputState();
+                if (!is24Hour()) {
+                    if ((oldVal == HOURS_IN_HALF_DAY - 1 && newVal == HOURS_IN_HALF_DAY) ||
+                            (oldVal == HOURS_IN_HALF_DAY && newVal == HOURS_IN_HALF_DAY - 1)) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                }
+                onTimeChanged();
+            }
+        });
+        mHourSpinnerInput = mHourSpinner.findViewById(R.id.numberpicker_input);
+        mHourSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+        // divider (only for the new widget style)
+        mDivider = mDelegator.findViewById(R.id.divider);
+        if (mDivider != null) {
+            setDividerText();
+        }
+
+        // minute
+        mMinuteSpinner = mDelegator.findViewById(R.id.minute);
+        mMinuteSpinner.setMinValue(0);
+        mMinuteSpinner.setMaxValue(59);
+        mMinuteSpinner.setOnLongPressUpdateInterval(100);
+        mMinuteSpinner.setFormatter(NumberPicker.getTwoDigitFormatter());
+        mMinuteSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+            public void onValueChange(NumberPicker spinner, int oldVal, int newVal) {
+                updateInputState();
+                int minValue = mMinuteSpinner.getMinValue();
+                int maxValue = mMinuteSpinner.getMaxValue();
+                if (oldVal == maxValue && newVal == minValue) {
+                    int newHour = mHourSpinner.getValue() + 1;
+                    if (!is24Hour() && newHour == HOURS_IN_HALF_DAY) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                    mHourSpinner.setValue(newHour);
+                } else if (oldVal == minValue && newVal == maxValue) {
+                    int newHour = mHourSpinner.getValue() - 1;
+                    if (!is24Hour() && newHour == HOURS_IN_HALF_DAY - 1) {
+                        mIsAm = !mIsAm;
+                        updateAmPmControl();
+                    }
+                    mHourSpinner.setValue(newHour);
+                }
+                onTimeChanged();
+            }
+        });
+        mMinuteSpinnerInput = mMinuteSpinner.findViewById(R.id.numberpicker_input);
+        mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+
+        // Get the localized am/pm strings and use them in the spinner.
+        mAmPmStrings = getAmPmStrings(context);
+
+        // am/pm
+        final View amPmView = mDelegator.findViewById(R.id.amPm);
+        if (amPmView instanceof Button) {
+            mAmPmSpinner = null;
+            mAmPmSpinnerInput = null;
+            mAmPmButton = (Button) amPmView;
+            mAmPmButton.setOnClickListener(new View.OnClickListener() {
+                public void onClick(View button) {
+                    button.requestFocus();
+                    mIsAm = !mIsAm;
+                    updateAmPmControl();
+                    onTimeChanged();
+                }
+            });
+        } else {
+            mAmPmButton = null;
+            mAmPmSpinner = (NumberPicker) amPmView;
+            mAmPmSpinner.setMinValue(0);
+            mAmPmSpinner.setMaxValue(1);
+            mAmPmSpinner.setDisplayedValues(mAmPmStrings);
+            mAmPmSpinner.setOnValueChangedListener(new NumberPicker.OnValueChangeListener() {
+                public void onValueChange(NumberPicker picker, int oldVal, int newVal) {
+                    updateInputState();
+                    picker.requestFocus();
+                    mIsAm = !mIsAm;
+                    updateAmPmControl();
+                    onTimeChanged();
+                }
+            });
+            mAmPmSpinnerInput = mAmPmSpinner.findViewById(R.id.numberpicker_input);
+            mAmPmSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+        }
+
+        if (isAmPmAtStart()) {
+            // Move the am/pm view to the beginning
+            ViewGroup amPmParent = delegator.findViewById(R.id.timePickerLayout);
+            amPmParent.removeView(amPmView);
+            amPmParent.addView(amPmView, 0);
+            // Swap layout margins if needed. They may be not symmetrical (Old Standard Theme
+            // for example and not for Holo Theme)
+            ViewGroup.MarginLayoutParams lp =
+                    (ViewGroup.MarginLayoutParams) amPmView.getLayoutParams();
+            final int startMargin = lp.getMarginStart();
+            final int endMargin = lp.getMarginEnd();
+            if (startMargin != endMargin) {
+                lp.setMarginStart(endMargin);
+                lp.setMarginEnd(startMargin);
+            }
+        }
+
+        getHourFormatData();
+
+        // update controls to initial state
+        updateHourControl();
+        updateMinuteControl();
+        updateAmPmControl();
+
+        // set to current time
+        mTempCalendar = Calendar.getInstance(mLocale);
+        setHour(mTempCalendar.get(Calendar.HOUR_OF_DAY));
+        setMinute(mTempCalendar.get(Calendar.MINUTE));
+
+        if (!isEnabled()) {
+            setEnabled(false);
+        }
+
+        // set the content descriptions
+        setContentDescriptions();
+
+        // If not explicitly specified this view is important for accessibility.
+        if (mDelegator.getImportantForAccessibility() == IMPORTANT_FOR_ACCESSIBILITY_AUTO) {
+            mDelegator.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_YES);
+        }
+    }
+
+    @Override
+    public boolean validateInput() {
+        return true;
+    }
+
+    private void getHourFormatData() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+                (mIs24HourView) ? "Hm" : "hm");
+        final int lengthPattern = bestDateTimePattern.length();
+        mHourWithTwoDigit = false;
+        char hourFormat = '\0';
+        // Check if the returned pattern is single or double 'H', 'h', 'K', 'k'. We also save
+        // the hour format that we found.
+        for (int i = 0; i < lengthPattern; i++) {
+            final char c = bestDateTimePattern.charAt(i);
+            if (c == 'H' || c == 'h' || c == 'K' || c == 'k') {
+                mHourFormat = c;
+                if (i + 1 < lengthPattern && c == bestDateTimePattern.charAt(i + 1)) {
+                    mHourWithTwoDigit = true;
+                }
+                break;
+            }
+        }
+    }
+
+    private boolean isAmPmAtStart() {
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+                "hm" /* skeleton */);
+
+        return bestDateTimePattern.startsWith("a");
+    }
+
+    /**
+     * The time separator is defined in the Unicode CLDR and cannot be supposed to be ":".
+     *
+     * See http://unicode.org/cldr/trac/browser/trunk/common/main
+     *
+     * We pass the correct "skeleton" depending on 12 or 24 hours view and then extract the
+     * separator as the character which is just after the hour marker in the returned pattern.
+     */
+    private void setDividerText() {
+        final String skeleton = (mIs24HourView) ? "Hm" : "hm";
+        final String bestDateTimePattern = DateFormat.getBestDateTimePattern(mLocale,
+                skeleton);
+        final String separatorText;
+        int hourIndex = bestDateTimePattern.lastIndexOf('H');
+        if (hourIndex == -1) {
+            hourIndex = bestDateTimePattern.lastIndexOf('h');
+        }
+        if (hourIndex == -1) {
+            // Default case
+            separatorText = ":";
+        } else {
+            int minuteIndex = bestDateTimePattern.indexOf('m', hourIndex + 1);
+            if  (minuteIndex == -1) {
+                separatorText = Character.toString(bestDateTimePattern.charAt(hourIndex + 1));
+            } else {
+                separatorText = bestDateTimePattern.substring(hourIndex + 1, minuteIndex);
+            }
+        }
+        mDivider.setText(separatorText);
+    }
+
+    @Override
+    public void setDate(int hour, int minute) {
+        setCurrentHour(hour, false);
+        setCurrentMinute(minute, false);
+
+        onTimeChanged();
+    }
+
+    @Override
+    public void setHour(int hour) {
+        setCurrentHour(hour, true);
+    }
+
+    private void setCurrentHour(int currentHour, boolean notifyTimeChanged) {
+        // why was Integer used in the first place?
+        if (currentHour == getHour()) {
+            return;
+        }
+        resetAutofilledValue();
+        if (!is24Hour()) {
+            // convert [0,23] ordinal to wall clock display
+            if (currentHour >= HOURS_IN_HALF_DAY) {
+                mIsAm = false;
+                if (currentHour > HOURS_IN_HALF_DAY) {
+                    currentHour = currentHour - HOURS_IN_HALF_DAY;
+                }
+            } else {
+                mIsAm = true;
+                if (currentHour == 0) {
+                    currentHour = HOURS_IN_HALF_DAY;
+                }
+            }
+            updateAmPmControl();
+        }
+        mHourSpinner.setValue(currentHour);
+        if (notifyTimeChanged) {
+            onTimeChanged();
+        }
+    }
+
+    @Override
+    public int getHour() {
+        int currentHour = mHourSpinner.getValue();
+        if (is24Hour()) {
+            return currentHour;
+        } else if (mIsAm) {
+            return currentHour % HOURS_IN_HALF_DAY;
+        } else {
+            return (currentHour % HOURS_IN_HALF_DAY) + HOURS_IN_HALF_DAY;
+        }
+    }
+
+    @Override
+    public void setMinute(int minute) {
+        setCurrentMinute(minute, true);
+    }
+
+    private void setCurrentMinute(int minute, boolean notifyTimeChanged) {
+        if (minute == getMinute()) {
+            return;
+        }
+        resetAutofilledValue();
+        mMinuteSpinner.setValue(minute);
+        if (notifyTimeChanged) {
+            onTimeChanged();
+        }
+    }
+
+    @Override
+    public int getMinute() {
+        return mMinuteSpinner.getValue();
+    }
+
+    public void setIs24Hour(boolean is24Hour) {
+        if (mIs24HourView == is24Hour) {
+            return;
+        }
+        // cache the current hour since spinner range changes and BEFORE changing mIs24HourView!!
+        int currentHour = getHour();
+        // Order is important here.
+        mIs24HourView = is24Hour;
+        getHourFormatData();
+        updateHourControl();
+        // set value after spinner range is updated
+        setCurrentHour(currentHour, false);
+        updateMinuteControl();
+        updateAmPmControl();
+    }
+
+    @Override
+    public boolean is24Hour() {
+        return mIs24HourView;
+    }
+
+    @Override
+    public void setEnabled(boolean enabled) {
+        mMinuteSpinner.setEnabled(enabled);
+        if (mDivider != null) {
+            mDivider.setEnabled(enabled);
+        }
+        mHourSpinner.setEnabled(enabled);
+        if (mAmPmSpinner != null) {
+            mAmPmSpinner.setEnabled(enabled);
+        } else {
+            mAmPmButton.setEnabled(enabled);
+        }
+        mIsEnabled = enabled;
+    }
+
+    @Override
+    public boolean isEnabled() {
+        return mIsEnabled;
+    }
+
+    @Override
+    public int getBaseline() {
+        return mHourSpinner.getBaseline();
+    }
+
+    @Override
+    public Parcelable onSaveInstanceState(Parcelable superState) {
+        return new SavedState(superState, getHour(), getMinute(), is24Hour());
+    }
+
+    @Override
+    public void onRestoreInstanceState(Parcelable state) {
+        if (state instanceof SavedState) {
+            final SavedState ss = (SavedState) state;
+            setHour(ss.getHour());
+            setMinute(ss.getMinute());
+        }
+    }
+
+    @Override
+    public boolean dispatchPopulateAccessibilityEvent(AccessibilityEvent event) {
+        onPopulateAccessibilityEvent(event);
+        return true;
+    }
+
+    @Override
+    public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
+        int flags = DateUtils.FORMAT_SHOW_TIME;
+        if (mIs24HourView) {
+            flags |= DateUtils.FORMAT_24HOUR;
+        } else {
+            flags |= DateUtils.FORMAT_12HOUR;
+        }
+        mTempCalendar.set(Calendar.HOUR_OF_DAY, getHour());
+        mTempCalendar.set(Calendar.MINUTE, getMinute());
+        String selectedDateUtterance = DateUtils.formatDateTime(mContext,
+                mTempCalendar.getTimeInMillis(), flags);
+        event.getText().add(selectedDateUtterance);
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getHourView() {
+        return mHourSpinnerInput;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getMinuteView() {
+        return mMinuteSpinnerInput;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getAmView() {
+        return mAmPmSpinnerInput;
+    }
+
+    /** @hide */
+    @Override
+    @TestApi
+    public View getPmView() {
+        return mAmPmSpinnerInput;
+    }
+
+    private void updateInputState() {
+        // Make sure that if the user changes the value and the IME is active
+        // for one of the inputs if this widget, the IME is closed. If the user
+        // changed the value via the IME and there is a next input the IME will
+        // be shown, otherwise the user chose another means of changing the
+        // value and having the IME up makes no sense.
+        InputMethodManager inputMethodManager = InputMethodManager.peekInstance();
+        if (inputMethodManager != null) {
+            if (inputMethodManager.isActive(mHourSpinnerInput)) {
+                mHourSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mMinuteSpinnerInput)) {
+                mMinuteSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            } else if (inputMethodManager.isActive(mAmPmSpinnerInput)) {
+                mAmPmSpinnerInput.clearFocus();
+                inputMethodManager.hideSoftInputFromWindow(mDelegator.getWindowToken(), 0);
+            }
+        }
+    }
+
+    private void updateAmPmControl() {
+        if (is24Hour()) {
+            if (mAmPmSpinner != null) {
+                mAmPmSpinner.setVisibility(View.GONE);
+            } else {
+                mAmPmButton.setVisibility(View.GONE);
+            }
+        } else {
+            int index = mIsAm ? Calendar.AM : Calendar.PM;
+            if (mAmPmSpinner != null) {
+                mAmPmSpinner.setValue(index);
+                mAmPmSpinner.setVisibility(View.VISIBLE);
+            } else {
+                mAmPmButton.setText(mAmPmStrings[index]);
+                mAmPmButton.setVisibility(View.VISIBLE);
+            }
+        }
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+    }
+
+    private void onTimeChanged() {
+        mDelegator.sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_SELECTED);
+        if (mOnTimeChangedListener != null) {
+            mOnTimeChangedListener.onTimeChanged(mDelegator, getHour(),
+                    getMinute());
+        }
+        if (mAutoFillChangeListener != null) {
+            mAutoFillChangeListener.onTimeChanged(mDelegator, getHour(), getMinute());
+        }
+    }
+
+    private void updateHourControl() {
+        if (is24Hour()) {
+            // 'k' means 1-24 hour
+            if (mHourFormat == 'k') {
+                mHourSpinner.setMinValue(1);
+                mHourSpinner.setMaxValue(24);
+            } else {
+                mHourSpinner.setMinValue(0);
+                mHourSpinner.setMaxValue(23);
+            }
+        } else {
+            // 'K' means 0-11 hour
+            if (mHourFormat == 'K') {
+                mHourSpinner.setMinValue(0);
+                mHourSpinner.setMaxValue(11);
+            } else {
+                mHourSpinner.setMinValue(1);
+                mHourSpinner.setMaxValue(12);
+            }
+        }
+        mHourSpinner.setFormatter(mHourWithTwoDigit ? NumberPicker.getTwoDigitFormatter() : null);
+    }
+
+    private void updateMinuteControl() {
+        if (is24Hour()) {
+            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_DONE);
+        } else {
+            mMinuteSpinnerInput.setImeOptions(EditorInfo.IME_ACTION_NEXT);
+        }
+    }
+
+    private void setContentDescriptions() {
+        // Minute
+        trySetContentDescription(mMinuteSpinner, R.id.increment,
+                R.string.time_picker_increment_minute_button);
+        trySetContentDescription(mMinuteSpinner, R.id.decrement,
+                R.string.time_picker_decrement_minute_button);
+        // Hour
+        trySetContentDescription(mHourSpinner, R.id.increment,
+                R.string.time_picker_increment_hour_button);
+        trySetContentDescription(mHourSpinner, R.id.decrement,
+                R.string.time_picker_decrement_hour_button);
+        // AM/PM
+        if (mAmPmSpinner != null) {
+            trySetContentDescription(mAmPmSpinner, R.id.increment,
+                    R.string.time_picker_increment_set_pm_button);
+            trySetContentDescription(mAmPmSpinner, R.id.decrement,
+                    R.string.time_picker_decrement_set_am_button);
+        }
+    }
+
+    private void trySetContentDescription(View root, int viewId, int contDescResId) {
+        View target = root.findViewById(viewId);
+        if (target != null) {
+            target.setContentDescription(mContext.getString(contDescResId));
+        }
+    }
+
+    public static String[] getAmPmStrings(Context context) {
+        String[] result = new String[2];
+        LocaleData d = LocaleData.get(context.getResources().getConfiguration().locale);
+        result[0] = d.amPm[0].length() > 4 ? d.narrowAm : d.amPm[0];
+        result[1] = d.amPm[1].length() > 4 ? d.narrowPm : d.amPm[1];
+        return result;
+    }
+}
diff --git a/android/widget/Toast.java b/android/widget/Toast.java
new file mode 100644
index 0000000..d807120
--- /dev/null
+++ b/android/widget/Toast.java
@@ -0,0 +1,535 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.annotation.IntDef;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.app.INotificationManager;
+import android.app.ITransientNotification;
+import android.content.Context;
+import android.content.res.Configuration;
+import android.content.res.Resources;
+import android.graphics.PixelFormat;
+import android.os.Handler;
+import android.os.IBinder;
+import android.os.Looper;
+import android.os.Message;
+import android.os.RemoteException;
+import android.os.ServiceManager;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.WindowManager;
+import android.view.accessibility.AccessibilityEvent;
+import android.view.accessibility.AccessibilityManager;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+/**
+ * A toast is a view containing a quick little message for the user.  The toast class
+ * helps you create and show those.
+ * {@more}
+ *
+ * <p>
+ * When the view is shown to the user, appears as a floating view over the
+ * application.  It will never receive focus.  The user will probably be in the
+ * middle of typing something else.  The idea is to be as unobtrusive as
+ * possible, while still showing the user the information you want them to see.
+ * Two examples are the volume control, and the brief message saying that your
+ * settings have been saved.
+ * <p>
+ * The easiest way to use this class is to call one of the static methods that constructs
+ * everything you need and returns a new Toast object.
+ *
+ * <div class="special reference">
+ * <h3>Developer Guides</h3>
+ * <p>For information about creating Toast notifications, read the
+ * <a href="{@docRoot}guide/topics/ui/notifiers/toasts.html">Toast Notifications</a> developer
+ * guide.</p>
+ * </div>
+ */
+public class Toast {
+    static final String TAG = "Toast";
+    static final boolean localLOGV = false;
+
+    /** @hide */
+    @IntDef({LENGTH_SHORT, LENGTH_LONG})
+    @Retention(RetentionPolicy.SOURCE)
+    public @interface Duration {}
+
+    /**
+     * Show the view or text notification for a short period of time.  This time
+     * could be user-definable.  This is the default.
+     * @see #setDuration
+     */
+    public static final int LENGTH_SHORT = 0;
+
+    /**
+     * Show the view or text notification for a long period of time.  This time
+     * could be user-definable.
+     * @see #setDuration
+     */
+    public static final int LENGTH_LONG = 1;
+
+    final Context mContext;
+    final TN mTN;
+    int mDuration;
+    View mNextView;
+
+    /**
+     * Construct an empty Toast object.  You must call {@link #setView} before you
+     * can call {@link #show}.
+     *
+     * @param context  The context to use.  Usually your {@link android.app.Application}
+     *                 or {@link android.app.Activity} object.
+     */
+    public Toast(Context context) {
+        this(context, null);
+    }
+
+    /**
+     * Constructs an empty Toast object.  If looper is null, Looper.myLooper() is used.
+     * @hide
+     */
+    public Toast(@NonNull Context context, @Nullable Looper looper) {
+        mContext = context;
+        mTN = new TN(context.getPackageName(), looper);
+        mTN.mY = context.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.toast_y_offset);
+        mTN.mGravity = context.getResources().getInteger(
+                com.android.internal.R.integer.config_toastDefaultGravity);
+    }
+
+    /**
+     * Show the view for the specified duration.
+     */
+    public void show() {
+        if (mNextView == null) {
+            throw new RuntimeException("setView must have been called");
+        }
+
+        INotificationManager service = getService();
+        String pkg = mContext.getOpPackageName();
+        TN tn = mTN;
+        tn.mNextView = mNextView;
+
+        try {
+            service.enqueueToast(pkg, tn, mDuration);
+        } catch (RemoteException e) {
+            // Empty
+        }
+    }
+
+    /**
+     * Close the view if it's showing, or don't show it if it isn't showing yet.
+     * You do not normally have to call this.  Normally view will disappear on its own
+     * after the appropriate duration.
+     */
+    public void cancel() {
+        mTN.cancel();
+    }
+
+    /**
+     * Set the view to show.
+     * @see #getView
+     */
+    public void setView(View view) {
+        mNextView = view;
+    }
+
+    /**
+     * Return the view.
+     * @see #setView
+     */
+    public View getView() {
+        return mNextView;
+    }
+
+    /**
+     * Set how long to show the view for.
+     * @see #LENGTH_SHORT
+     * @see #LENGTH_LONG
+     */
+    public void setDuration(@Duration int duration) {
+        mDuration = duration;
+        mTN.mDuration = duration;
+    }
+
+    /**
+     * Return the duration.
+     * @see #setDuration
+     */
+    @Duration
+    public int getDuration() {
+        return mDuration;
+    }
+
+    /**
+     * Set the margins of the view.
+     *
+     * @param horizontalMargin The horizontal margin, in percentage of the
+     *        container width, between the container's edges and the
+     *        notification
+     * @param verticalMargin The vertical margin, in percentage of the
+     *        container height, between the container's edges and the
+     *        notification
+     */
+    public void setMargin(float horizontalMargin, float verticalMargin) {
+        mTN.mHorizontalMargin = horizontalMargin;
+        mTN.mVerticalMargin = verticalMargin;
+    }
+
+    /**
+     * Return the horizontal margin.
+     */
+    public float getHorizontalMargin() {
+        return mTN.mHorizontalMargin;
+    }
+
+    /**
+     * Return the vertical margin.
+     */
+    public float getVerticalMargin() {
+        return mTN.mVerticalMargin;
+    }
+
+    /**
+     * Set the location at which the notification should appear on the screen.
+     * @see android.view.Gravity
+     * @see #getGravity
+     */
+    public void setGravity(int gravity, int xOffset, int yOffset) {
+        mTN.mGravity = gravity;
+        mTN.mX = xOffset;
+        mTN.mY = yOffset;
+    }
+
+     /**
+     * Get the location at which the notification should appear on the screen.
+     * @see android.view.Gravity
+     * @see #getGravity
+     */
+    public int getGravity() {
+        return mTN.mGravity;
+    }
+
+    /**
+     * Return the X offset in pixels to apply to the gravity's location.
+     */
+    public int getXOffset() {
+        return mTN.mX;
+    }
+
+    /**
+     * Return the Y offset in pixels to apply to the gravity's location.
+     */
+    public int getYOffset() {
+        return mTN.mY;
+    }
+
+    /**
+     * Gets the LayoutParams for the Toast window.
+     * @hide
+     */
+    public WindowManager.LayoutParams getWindowParams() {
+        return mTN.mParams;
+    }
+
+    /**
+     * Make a standard toast that just contains a text view.
+     *
+     * @param context  The context to use.  Usually your {@link android.app.Application}
+     *                 or {@link android.app.Activity} object.
+     * @param text     The text to show.  Can be formatted text.
+     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
+     *                 {@link #LENGTH_LONG}
+     *
+     */
+    public static Toast makeText(Context context, CharSequence text, @Duration int duration) {
+        return makeText(context, null, text, duration);
+    }
+
+    /**
+     * Make a standard toast to display using the specified looper.
+     * If looper is null, Looper.myLooper() is used.
+     * @hide
+     */
+    public static Toast makeText(@NonNull Context context, @Nullable Looper looper,
+            @NonNull CharSequence text, @Duration int duration) {
+        Toast result = new Toast(context, looper);
+
+        LayoutInflater inflate = (LayoutInflater)
+                context.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        View v = inflate.inflate(com.android.internal.R.layout.transient_notification, null);
+        TextView tv = (TextView)v.findViewById(com.android.internal.R.id.message);
+        tv.setText(text);
+
+        result.mNextView = v;
+        result.mDuration = duration;
+
+        return result;
+    }
+
+    /**
+     * Make a standard toast that just contains a text view with the text from a resource.
+     *
+     * @param context  The context to use.  Usually your {@link android.app.Application}
+     *                 or {@link android.app.Activity} object.
+     * @param resId    The resource id of the string resource to use.  Can be formatted text.
+     * @param duration How long to display the message.  Either {@link #LENGTH_SHORT} or
+     *                 {@link #LENGTH_LONG}
+     *
+     * @throws Resources.NotFoundException if the resource can't be found.
+     */
+    public static Toast makeText(Context context, @StringRes int resId, @Duration int duration)
+                                throws Resources.NotFoundException {
+        return makeText(context, context.getResources().getText(resId), duration);
+    }
+
+    /**
+     * Update the text in a Toast that was previously created using one of the makeText() methods.
+     * @param resId The new text for the Toast.
+     */
+    public void setText(@StringRes int resId) {
+        setText(mContext.getText(resId));
+    }
+
+    /**
+     * Update the text in a Toast that was previously created using one of the makeText() methods.
+     * @param s The new text for the Toast.
+     */
+    public void setText(CharSequence s) {
+        if (mNextView == null) {
+            throw new RuntimeException("This Toast was not created with Toast.makeText()");
+        }
+        TextView tv = mNextView.findViewById(com.android.internal.R.id.message);
+        if (tv == null) {
+            throw new RuntimeException("This Toast was not created with Toast.makeText()");
+        }
+        tv.setText(s);
+    }
+
+    // =======================================================================================
+    // All the gunk below is the interaction with the Notification Service, which handles
+    // the proper ordering of these system-wide.
+    // =======================================================================================
+
+    private static INotificationManager sService;
+
+    static private INotificationManager getService() {
+        if (sService != null) {
+            return sService;
+        }
+        sService = INotificationManager.Stub.asInterface(ServiceManager.getService("notification"));
+        return sService;
+    }
+
+    private static class TN extends ITransientNotification.Stub {
+        private final WindowManager.LayoutParams mParams = new WindowManager.LayoutParams();
+
+        private static final int SHOW = 0;
+        private static final int HIDE = 1;
+        private static final int CANCEL = 2;
+        final Handler mHandler;
+
+        int mGravity;
+        int mX, mY;
+        float mHorizontalMargin;
+        float mVerticalMargin;
+
+
+        View mView;
+        View mNextView;
+        int mDuration;
+
+        WindowManager mWM;
+
+        String mPackageName;
+
+        static final long SHORT_DURATION_TIMEOUT = 4000;
+        static final long LONG_DURATION_TIMEOUT = 7000;
+
+        TN(String packageName, @Nullable Looper looper) {
+            // XXX This should be changed to use a Dialog, with a Theme.Toast
+            // defined that sets up the layout params appropriately.
+            final WindowManager.LayoutParams params = mParams;
+            params.height = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.width = WindowManager.LayoutParams.WRAP_CONTENT;
+            params.format = PixelFormat.TRANSLUCENT;
+            params.windowAnimations = com.android.internal.R.style.Animation_Toast;
+            params.type = WindowManager.LayoutParams.TYPE_TOAST;
+            params.setTitle("Toast");
+            params.flags = WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
+                    | WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE
+                    | WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
+
+            mPackageName = packageName;
+
+            if (looper == null) {
+                // Use Looper.myLooper() if looper is not specified.
+                looper = Looper.myLooper();
+                if (looper == null) {
+                    throw new RuntimeException(
+                            "Can't toast on a thread that has not called Looper.prepare()");
+                }
+            }
+            mHandler = new Handler(looper, null) {
+                @Override
+                public void handleMessage(Message msg) {
+                    switch (msg.what) {
+                        case SHOW: {
+                            IBinder token = (IBinder) msg.obj;
+                            handleShow(token);
+                            break;
+                        }
+                        case HIDE: {
+                            handleHide();
+                            // Don't do this in handleHide() because it is also invoked by
+                            // handleShow()
+                            mNextView = null;
+                            break;
+                        }
+                        case CANCEL: {
+                            handleHide();
+                            // Don't do this in handleHide() because it is also invoked by
+                            // handleShow()
+                            mNextView = null;
+                            try {
+                                getService().cancelToast(mPackageName, TN.this);
+                            } catch (RemoteException e) {
+                            }
+                            break;
+                        }
+                    }
+                }
+            };
+        }
+
+        /**
+         * schedule handleShow into the right thread
+         */
+        @Override
+        public void show(IBinder windowToken) {
+            if (localLOGV) Log.v(TAG, "SHOW: " + this);
+            mHandler.obtainMessage(SHOW, windowToken).sendToTarget();
+        }
+
+        /**
+         * schedule handleHide into the right thread
+         */
+        @Override
+        public void hide() {
+            if (localLOGV) Log.v(TAG, "HIDE: " + this);
+            mHandler.obtainMessage(HIDE).sendToTarget();
+        }
+
+        public void cancel() {
+            if (localLOGV) Log.v(TAG, "CANCEL: " + this);
+            mHandler.obtainMessage(CANCEL).sendToTarget();
+        }
+
+        public void handleShow(IBinder windowToken) {
+            if (localLOGV) Log.v(TAG, "HANDLE SHOW: " + this + " mView=" + mView
+                    + " mNextView=" + mNextView);
+            // If a cancel/hide is pending - no need to show - at this point
+            // the window token is already invalid and no need to do any work.
+            if (mHandler.hasMessages(CANCEL) || mHandler.hasMessages(HIDE)) {
+                return;
+            }
+            if (mView != mNextView) {
+                // remove the old view if necessary
+                handleHide();
+                mView = mNextView;
+                Context context = mView.getContext().getApplicationContext();
+                String packageName = mView.getContext().getOpPackageName();
+                if (context == null) {
+                    context = mView.getContext();
+                }
+                mWM = (WindowManager)context.getSystemService(Context.WINDOW_SERVICE);
+                // We can resolve the Gravity here by using the Locale for getting
+                // the layout direction
+                final Configuration config = mView.getContext().getResources().getConfiguration();
+                final int gravity = Gravity.getAbsoluteGravity(mGravity, config.getLayoutDirection());
+                mParams.gravity = gravity;
+                if ((gravity & Gravity.HORIZONTAL_GRAVITY_MASK) == Gravity.FILL_HORIZONTAL) {
+                    mParams.horizontalWeight = 1.0f;
+                }
+                if ((gravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.FILL_VERTICAL) {
+                    mParams.verticalWeight = 1.0f;
+                }
+                mParams.x = mX;
+                mParams.y = mY;
+                mParams.verticalMargin = mVerticalMargin;
+                mParams.horizontalMargin = mHorizontalMargin;
+                mParams.packageName = packageName;
+                mParams.hideTimeoutMilliseconds = mDuration ==
+                    Toast.LENGTH_LONG ? LONG_DURATION_TIMEOUT : SHORT_DURATION_TIMEOUT;
+                mParams.token = windowToken;
+                if (mView.getParent() != null) {
+                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+                    mWM.removeView(mView);
+                }
+                if (localLOGV) Log.v(TAG, "ADD! " + mView + " in " + this);
+                // Since the notification manager service cancels the token right
+                // after it notifies us to cancel the toast there is an inherent
+                // race and we may attempt to add a window after the token has been
+                // invalidated. Let us hedge against that.
+                try {
+                    mWM.addView(mView, mParams);
+                    trySendAccessibilityEvent();
+                } catch (WindowManager.BadTokenException e) {
+                    /* ignore */
+                }
+            }
+        }
+
+        private void trySendAccessibilityEvent() {
+            AccessibilityManager accessibilityManager =
+                    AccessibilityManager.getInstance(mView.getContext());
+            if (!accessibilityManager.isEnabled()) {
+                return;
+            }
+            // treat toasts as notifications since they are used to
+            // announce a transient piece of information to the user
+            AccessibilityEvent event = AccessibilityEvent.obtain(
+                    AccessibilityEvent.TYPE_NOTIFICATION_STATE_CHANGED);
+            event.setClassName(getClass().getName());
+            event.setPackageName(mView.getContext().getPackageName());
+            mView.dispatchPopulateAccessibilityEvent(event);
+            accessibilityManager.sendAccessibilityEvent(event);
+        }
+
+        public void handleHide() {
+            if (localLOGV) Log.v(TAG, "HANDLE HIDE: " + this + " mView=" + mView);
+            if (mView != null) {
+                // note: checking parent() just to make sure the view has
+                // been added...  i have seen cases where we get here when
+                // the view isn't yet added, so let's try not to crash.
+                if (mView.getParent() != null) {
+                    if (localLOGV) Log.v(TAG, "REMOVE! " + mView + " in " + this);
+                    mWM.removeViewImmediate(mView);
+                }
+
+                mView = null;
+            }
+        }
+    }
+}
diff --git a/android/widget/ToggleButton.java b/android/widget/ToggleButton.java
new file mode 100644
index 0000000..6a8449e
--- /dev/null
+++ b/android/widget/ToggleButton.java
@@ -0,0 +1,158 @@
+/*
+ * Copyright (C) 2007 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 android.widget;
+
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.graphics.drawable.LayerDrawable;
+import android.util.AttributeSet;
+
+/**
+ * Displays checked/unchecked states as a button
+ * with a "light" indicator and by default accompanied with the text "ON" or "OFF".
+ *
+ * <p>See the <a href="{@docRoot}guide/topics/ui/controls/togglebutton.html">Toggle Buttons</a>
+ * guide.</p>
+ * 
+ * @attr ref android.R.styleable#ToggleButton_textOn
+ * @attr ref android.R.styleable#ToggleButton_textOff
+ * @attr ref android.R.styleable#ToggleButton_disabledAlpha
+ */
+public class ToggleButton extends CompoundButton {
+    private CharSequence mTextOn;
+    private CharSequence mTextOff;
+    
+    private Drawable mIndicatorDrawable;
+
+    private static final int NO_ALPHA = 0xFF;
+    private float mDisabledAlpha;
+
+    public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.ToggleButton, defStyleAttr, defStyleRes);
+        mTextOn = a.getText(com.android.internal.R.styleable.ToggleButton_textOn);
+        mTextOff = a.getText(com.android.internal.R.styleable.ToggleButton_textOff);
+        mDisabledAlpha = a.getFloat(com.android.internal.R.styleable.ToggleButton_disabledAlpha, 0.5f);
+        syncTextState();
+        a.recycle();
+    }
+
+    public ToggleButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ToggleButton(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.buttonStyleToggle);
+    }
+
+    public ToggleButton(Context context) {
+        this(context, null);
+    }
+
+    @Override
+    public void setChecked(boolean checked) {
+        super.setChecked(checked);
+        
+        syncTextState();
+    }
+
+    private void syncTextState() {
+        boolean checked = isChecked();
+        if (checked && mTextOn != null) {
+            setText(mTextOn);
+        } else if (!checked && mTextOff != null) {
+            setText(mTextOff);
+        }
+    }
+
+    /**
+     * Returns the text for when the button is in the checked state.
+     * 
+     * @return The text.
+     */
+    public CharSequence getTextOn() {
+        return mTextOn;
+    }
+
+    /**
+     * Sets the text for when the button is in the checked state.
+     *  
+     * @param textOn The text.
+     */
+    public void setTextOn(CharSequence textOn) {
+        mTextOn = textOn;
+    }
+
+    /**
+     * Returns the text for when the button is not in the checked state.
+     * 
+     * @return The text.
+     */
+    public CharSequence getTextOff() {
+        return mTextOff;
+    }
+
+    /**
+     * Sets the text for when the button is not in the checked state.
+     * 
+     * @param textOff The text.
+     */
+    public void setTextOff(CharSequence textOff) {
+        mTextOff = textOff;
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+        
+        updateReferenceToIndicatorDrawable(getBackground());
+    }
+
+    @Override
+    public void setBackgroundDrawable(Drawable d) {
+        super.setBackgroundDrawable(d);
+        
+        updateReferenceToIndicatorDrawable(d);
+    }
+
+    private void updateReferenceToIndicatorDrawable(Drawable backgroundDrawable) {
+        if (backgroundDrawable instanceof LayerDrawable) {
+            LayerDrawable layerDrawable = (LayerDrawable) backgroundDrawable;
+            mIndicatorDrawable =
+                    layerDrawable.findDrawableByLayerId(com.android.internal.R.id.toggle);
+        } else {
+            mIndicatorDrawable = null;
+        }
+    }
+    
+    @Override
+    protected void drawableStateChanged() {
+        super.drawableStateChanged();
+        
+        if (mIndicatorDrawable != null) {
+            mIndicatorDrawable.setAlpha(isEnabled() ? NO_ALPHA : (int) (NO_ALPHA * mDisabledAlpha));
+        }
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ToggleButton.class.getName();
+    }
+}
diff --git a/android/widget/Toolbar.java b/android/widget/Toolbar.java
new file mode 100644
index 0000000..bf3085d
--- /dev/null
+++ b/android/widget/Toolbar.java
@@ -0,0 +1,2400 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.annotation.ColorInt;
+import android.annotation.DrawableRes;
+import android.annotation.MenuRes;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.annotation.StringRes;
+import android.annotation.StyleRes;
+import android.annotation.TestApi;
+import android.app.ActionBar;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.graphics.drawable.Drawable;
+import android.os.Parcel;
+import android.os.Parcelable;
+import android.text.Layout;
+import android.text.TextUtils;
+import android.util.AttributeSet;
+import android.view.CollapsibleActionView;
+import android.view.ContextThemeWrapper;
+import android.view.Gravity;
+import android.view.Menu;
+import android.view.MenuInflater;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.ViewParent;
+
+import com.android.internal.R;
+import com.android.internal.view.menu.MenuBuilder;
+import com.android.internal.view.menu.MenuItemImpl;
+import com.android.internal.view.menu.MenuPresenter;
+import com.android.internal.view.menu.MenuView;
+import com.android.internal.view.menu.SubMenuBuilder;
+import com.android.internal.widget.DecorToolbar;
+import com.android.internal.widget.ToolbarWidgetWrapper;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A standard toolbar for use within application content.
+ *
+ * <p>A Toolbar is a generalization of {@link android.app.ActionBar action bars} for use
+ * within application layouts. While an action bar is traditionally part of an
+ * {@link android.app.Activity Activity's} opaque window decor controlled by the framework,
+ * a Toolbar may be placed at any arbitrary level of nesting within a view hierarchy.
+ * An application may choose to designate a Toolbar as the action bar for an Activity
+ * using the {@link android.app.Activity#setActionBar(Toolbar) setActionBar()} method.</p>
+ *
+ * <p>Toolbar supports a more focused feature set than ActionBar. From start to end, a toolbar
+ * may contain a combination of the following optional elements:
+ *
+ * <ul>
+ *     <li><em>A navigation button.</em> This may be an Up arrow, navigation menu toggle, close,
+ *     collapse, done or another glyph of the app's choosing. This button should always be used
+ *     to access other navigational destinations within the container of the Toolbar and
+ *     its signified content or otherwise leave the current context signified by the Toolbar.
+ *     The navigation button is vertically aligned within the Toolbar's
+ *     {@link android.R.styleable#View_minHeight minimum height}, if set.</li>
+ *     <li><em>A branded logo image.</em> This may extend to the height of the bar and can be
+ *     arbitrarily wide.</li>
+ *     <li><em>A title and subtitle.</em> The title should be a signpost for the Toolbar's current
+ *     position in the navigation hierarchy and the content contained there. The subtitle,
+ *     if present should indicate any extended information about the current content.
+ *     If an app uses a logo image it should strongly consider omitting a title and subtitle.</li>
+ *     <li><em>One or more custom views.</em> The application may add arbitrary child views
+ *     to the Toolbar. They will appear at this position within the layout. If a child view's
+ *     {@link LayoutParams} indicates a {@link Gravity} value of
+ *     {@link Gravity#CENTER_HORIZONTAL CENTER_HORIZONTAL} the view will attempt to center
+ *     within the available space remaining in the Toolbar after all other elements have been
+ *     measured.</li>
+ *     <li><em>An {@link ActionMenuView action menu}.</em> The menu of actions will pin to the
+ *     end of the Toolbar offering a few
+ *     <a href="http://developer.android.com/design/patterns/actionbar.html#ActionButtons">
+ *     frequent, important or typical</a> actions along with an optional overflow menu for
+ *     additional actions. Action buttons are vertically aligned within the Toolbar's
+ *     {@link android.R.styleable#View_minHeight minimum height}, if set.</li>
+ * </ul>
+ * </p>
+ *
+ * <p>In modern Android UIs developers should lean more on a visually distinct color scheme for
+ * toolbars than on their application icon. The use of application icon plus title as a standard
+ * layout is discouraged on API 21 devices and newer.</p>
+ *
+ * @attr ref android.R.styleable#Toolbar_buttonGravity
+ * @attr ref android.R.styleable#Toolbar_collapseContentDescription
+ * @attr ref android.R.styleable#Toolbar_collapseIcon
+ * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+ * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+ * @attr ref android.R.styleable#Toolbar_contentInsetRight
+ * @attr ref android.R.styleable#Toolbar_contentInsetStart
+ * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+ * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+ * @attr ref android.R.styleable#Toolbar_gravity
+ * @attr ref android.R.styleable#Toolbar_logo
+ * @attr ref android.R.styleable#Toolbar_logoDescription
+ * @attr ref android.R.styleable#Toolbar_maxButtonHeight
+ * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+ * @attr ref android.R.styleable#Toolbar_navigationIcon
+ * @attr ref android.R.styleable#Toolbar_popupTheme
+ * @attr ref android.R.styleable#Toolbar_subtitle
+ * @attr ref android.R.styleable#Toolbar_subtitleTextAppearance
+ * @attr ref android.R.styleable#Toolbar_subtitleTextColor
+ * @attr ref android.R.styleable#Toolbar_title
+ * @attr ref android.R.styleable#Toolbar_titleMargin
+ * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+ * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+ * @attr ref android.R.styleable#Toolbar_titleMarginStart
+ * @attr ref android.R.styleable#Toolbar_titleMarginTop
+ * @attr ref android.R.styleable#Toolbar_titleTextAppearance
+ * @attr ref android.R.styleable#Toolbar_titleTextColor
+ */
+public class Toolbar extends ViewGroup {
+    private static final String TAG = "Toolbar";
+
+    private ActionMenuView mMenuView;
+    private TextView mTitleTextView;
+    private TextView mSubtitleTextView;
+    private ImageButton mNavButtonView;
+    private ImageView mLogoView;
+
+    private Drawable mCollapseIcon;
+    private CharSequence mCollapseDescription;
+    private ImageButton mCollapseButtonView;
+    View mExpandedActionView;
+
+    /** Context against which to inflate popup menus. */
+    private Context mPopupContext;
+
+    /** Theme resource against which to inflate popup menus. */
+    private int mPopupTheme;
+
+    private int mTitleTextAppearance;
+    private int mSubtitleTextAppearance;
+    private int mNavButtonStyle;
+
+    private int mButtonGravity;
+
+    private int mMaxButtonHeight;
+
+    private int mTitleMarginStart;
+    private int mTitleMarginEnd;
+    private int mTitleMarginTop;
+    private int mTitleMarginBottom;
+
+    private RtlSpacingHelper mContentInsets;
+    private int mContentInsetStartWithNavigation;
+    private int mContentInsetEndWithActions;
+
+    private int mGravity = Gravity.START | Gravity.CENTER_VERTICAL;
+
+    private CharSequence mTitleText;
+    private CharSequence mSubtitleText;
+
+    private int mTitleTextColor;
+    private int mSubtitleTextColor;
+
+    private boolean mEatingTouch;
+
+    // Clear me after use.
+    private final ArrayList<View> mTempViews = new ArrayList<View>();
+
+    // Used to hold views that will be removed while we have an expanded action view.
+    private final ArrayList<View> mHiddenViews = new ArrayList<>();
+
+    private final int[] mTempMargins = new int[2];
+
+    private OnMenuItemClickListener mOnMenuItemClickListener;
+
+    private final ActionMenuView.OnMenuItemClickListener mMenuViewItemClickListener =
+            new ActionMenuView.OnMenuItemClickListener() {
+                @Override
+                public boolean onMenuItemClick(MenuItem item) {
+                    if (mOnMenuItemClickListener != null) {
+                        return mOnMenuItemClickListener.onMenuItemClick(item);
+                    }
+                    return false;
+                }
+            };
+
+    private ToolbarWidgetWrapper mWrapper;
+    private ActionMenuPresenter mOuterActionMenuPresenter;
+    private ExpandedActionViewMenuPresenter mExpandedMenuPresenter;
+    private MenuPresenter.Callback mActionMenuPresenterCallback;
+    private MenuBuilder.Callback mMenuBuilderCallback;
+
+    private boolean mCollapsible;
+
+    private final Runnable mShowOverflowMenuRunnable = new Runnable() {
+        @Override public void run() {
+            showOverflowMenu();
+        }
+    };
+
+    public Toolbar(Context context) {
+        this(context, null);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs) {
+        this(context, attrs, com.android.internal.R.attr.toolbarStyle);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public Toolbar(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(attrs, R.styleable.Toolbar,
+                defStyleAttr, defStyleRes);
+
+        mTitleTextAppearance = a.getResourceId(R.styleable.Toolbar_titleTextAppearance, 0);
+        mSubtitleTextAppearance = a.getResourceId(R.styleable.Toolbar_subtitleTextAppearance, 0);
+        mNavButtonStyle = a.getResourceId(R.styleable.Toolbar_navigationButtonStyle, 0);
+        mGravity = a.getInteger(R.styleable.Toolbar_gravity, mGravity);
+        mButtonGravity = a.getInteger(R.styleable.Toolbar_buttonGravity, Gravity.TOP);
+        mTitleMarginStart = mTitleMarginEnd = mTitleMarginTop = mTitleMarginBottom =
+                a.getDimensionPixelOffset(R.styleable.Toolbar_titleMargin, 0);
+
+        final int marginStart = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginStart, -1);
+        if (marginStart >= 0) {
+            mTitleMarginStart = marginStart;
+        }
+
+        final int marginEnd = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginEnd, -1);
+        if (marginEnd >= 0) {
+            mTitleMarginEnd = marginEnd;
+        }
+
+        final int marginTop = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginTop, -1);
+        if (marginTop >= 0) {
+            mTitleMarginTop = marginTop;
+        }
+
+        final int marginBottom = a.getDimensionPixelOffset(R.styleable.Toolbar_titleMarginBottom,
+                -1);
+        if (marginBottom >= 0) {
+            mTitleMarginBottom = marginBottom;
+        }
+
+        mMaxButtonHeight = a.getDimensionPixelSize(R.styleable.Toolbar_maxButtonHeight, -1);
+
+        final int contentInsetStart =
+                a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetStart,
+                        RtlSpacingHelper.UNDEFINED);
+        final int contentInsetEnd =
+                a.getDimensionPixelOffset(R.styleable.Toolbar_contentInsetEnd,
+                        RtlSpacingHelper.UNDEFINED);
+        final int contentInsetLeft =
+                a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetLeft, 0);
+        final int contentInsetRight =
+                a.getDimensionPixelSize(R.styleable.Toolbar_contentInsetRight, 0);
+
+        ensureContentInsets();
+        mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight);
+
+        if (contentInsetStart != RtlSpacingHelper.UNDEFINED ||
+                contentInsetEnd != RtlSpacingHelper.UNDEFINED) {
+            mContentInsets.setRelative(contentInsetStart, contentInsetEnd);
+        }
+
+        mContentInsetStartWithNavigation = a.getDimensionPixelOffset(
+                R.styleable.Toolbar_contentInsetStartWithNavigation, RtlSpacingHelper.UNDEFINED);
+        mContentInsetEndWithActions = a.getDimensionPixelOffset(
+                R.styleable.Toolbar_contentInsetEndWithActions, RtlSpacingHelper.UNDEFINED);
+
+        mCollapseIcon = a.getDrawable(R.styleable.Toolbar_collapseIcon);
+        mCollapseDescription = a.getText(R.styleable.Toolbar_collapseContentDescription);
+
+        final CharSequence title = a.getText(R.styleable.Toolbar_title);
+        if (!TextUtils.isEmpty(title)) {
+            setTitle(title);
+        }
+
+        final CharSequence subtitle = a.getText(R.styleable.Toolbar_subtitle);
+        if (!TextUtils.isEmpty(subtitle)) {
+            setSubtitle(subtitle);
+        }
+
+        // Set the default context, since setPopupTheme() may be a no-op.
+        mPopupContext = mContext;
+        setPopupTheme(a.getResourceId(R.styleable.Toolbar_popupTheme, 0));
+
+        final Drawable navIcon = a.getDrawable(R.styleable.Toolbar_navigationIcon);
+        if (navIcon != null) {
+            setNavigationIcon(navIcon);
+        }
+
+        final CharSequence navDesc = a.getText(
+                R.styleable.Toolbar_navigationContentDescription);
+        if (!TextUtils.isEmpty(navDesc)) {
+            setNavigationContentDescription(navDesc);
+        }
+
+        final Drawable logo = a.getDrawable(R.styleable.Toolbar_logo);
+        if (logo != null) {
+            setLogo(logo);
+        }
+
+        final CharSequence logoDesc = a.getText(R.styleable.Toolbar_logoDescription);
+        if (!TextUtils.isEmpty(logoDesc)) {
+            setLogoDescription(logoDesc);
+        }
+
+        if (a.hasValue(R.styleable.Toolbar_titleTextColor)) {
+            setTitleTextColor(a.getColor(R.styleable.Toolbar_titleTextColor, 0xffffffff));
+        }
+
+        if (a.hasValue(R.styleable.Toolbar_subtitleTextColor)) {
+            setSubtitleTextColor(a.getColor(R.styleable.Toolbar_subtitleTextColor, 0xffffffff));
+        }
+        a.recycle();
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // If the container is a cluster, unmark itself as a cluster to avoid having nested
+        // clusters.
+        ViewParent parent = getParent();
+        while (parent != null && parent instanceof ViewGroup) {
+            final ViewGroup vgParent = (ViewGroup) parent;
+            if (vgParent.isKeyboardNavigationCluster()) {
+                setKeyboardNavigationCluster(false);
+                if (vgParent.getTouchscreenBlocksFocus()) {
+                    setTouchscreenBlocksFocus(false);
+                }
+                break;
+            }
+            parent = vgParent.getParent();
+        }
+    }
+
+    /**
+     * Specifies the theme to use when inflating popup menus. By default, uses
+     * the same theme as the toolbar itself.
+     *
+     * @param resId theme used to inflate popup menus
+     * @see #getPopupTheme()
+     */
+    public void setPopupTheme(@StyleRes int resId) {
+        if (mPopupTheme != resId) {
+            mPopupTheme = resId;
+            if (resId == 0) {
+                mPopupContext = mContext;
+            } else {
+                mPopupContext = new ContextThemeWrapper(mContext, resId);
+            }
+        }
+    }
+
+    /**
+     * @return resource identifier of the theme used to inflate popup menus, or
+     *         0 if menus are inflated against the toolbar theme
+     * @see #setPopupTheme(int)
+     */
+    public int getPopupTheme() {
+        return mPopupTheme;
+    }
+
+    /**
+     * Sets the title margin.
+     *
+     * @param start the starting title margin in pixels
+     * @param top the top title margin in pixels
+     * @param end the ending title margin in pixels
+     * @param bottom the bottom title margin in pixels
+     * @see #getTitleMarginStart()
+     * @see #getTitleMarginTop()
+     * @see #getTitleMarginEnd()
+     * @see #getTitleMarginBottom()
+     * @attr ref android.R.styleable#Toolbar_titleMargin
+     */
+    public void setTitleMargin(int start, int top, int end, int bottom) {
+        mTitleMarginStart = start;
+        mTitleMarginTop = top;
+        mTitleMarginEnd = end;
+        mTitleMarginBottom = bottom;
+
+        requestLayout();
+    }
+
+    /**
+     * @return the starting title margin in pixels
+     * @see #setTitleMarginStart(int)
+     * @attr ref android.R.styleable#Toolbar_titleMarginStart
+     */
+    public int getTitleMarginStart() {
+        return mTitleMarginStart;
+    }
+
+    /**
+     * Sets the starting title margin in pixels.
+     *
+     * @param margin the starting title margin in pixels
+     * @see #getTitleMarginStart()
+     * @attr ref android.R.styleable#Toolbar_titleMarginStart
+     */
+    public void setTitleMarginStart(int margin) {
+        mTitleMarginStart = margin;
+
+        requestLayout();
+    }
+
+    /**
+     * @return the top title margin in pixels
+     * @see #setTitleMarginTop(int)
+     * @attr ref android.R.styleable#Toolbar_titleMarginTop
+     */
+    public int getTitleMarginTop() {
+        return mTitleMarginTop;
+    }
+
+    /**
+     * Sets the top title margin in pixels.
+     *
+     * @param margin the top title margin in pixels
+     * @see #getTitleMarginTop()
+     * @attr ref android.R.styleable#Toolbar_titleMarginTop
+     */
+    public void setTitleMarginTop(int margin) {
+        mTitleMarginTop = margin;
+
+        requestLayout();
+    }
+
+    /**
+     * @return the ending title margin in pixels
+     * @see #setTitleMarginEnd(int)
+     * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+     */
+    public int getTitleMarginEnd() {
+        return mTitleMarginEnd;
+    }
+
+    /**
+     * Sets the ending title margin in pixels.
+     *
+     * @param margin the ending title margin in pixels
+     * @see #getTitleMarginEnd()
+     * @attr ref android.R.styleable#Toolbar_titleMarginEnd
+     */
+    public void setTitleMarginEnd(int margin) {
+        mTitleMarginEnd = margin;
+
+        requestLayout();
+    }
+
+    /**
+     * @return the bottom title margin in pixels
+     * @see #setTitleMarginBottom(int)
+     * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+     */
+    public int getTitleMarginBottom() {
+        return mTitleMarginBottom;
+    }
+
+    /**
+     * Sets the bottom title margin in pixels.
+     *
+     * @param margin the bottom title margin in pixels
+     * @see #getTitleMarginBottom()
+     * @attr ref android.R.styleable#Toolbar_titleMarginBottom
+     */
+    public void setTitleMarginBottom(int margin) {
+        mTitleMarginBottom = margin;
+        requestLayout();
+    }
+
+    @Override
+    public void onRtlPropertiesChanged(@ResolvedLayoutDir int layoutDirection) {
+        super.onRtlPropertiesChanged(layoutDirection);
+        ensureContentInsets();
+        mContentInsets.setDirection(layoutDirection == LAYOUT_DIRECTION_RTL);
+    }
+
+    /**
+     * Set a logo drawable from a resource id.
+     *
+     * <p>This drawable should generally take the place of title text. The logo cannot be
+     * clicked. Apps using a logo should also supply a description using
+     * {@link #setLogoDescription(int)}.</p>
+     *
+     * @param resId ID of a drawable resource
+     */
+    public void setLogo(@DrawableRes int resId) {
+        setLogo(getContext().getDrawable(resId));
+    }
+
+    /** @hide */
+    public boolean canShowOverflowMenu() {
+        return getVisibility() == VISIBLE && mMenuView != null && mMenuView.isOverflowReserved();
+    }
+
+    /**
+     * Check whether the overflow menu is currently showing. This may not reflect
+     * a pending show operation in progress.
+     *
+     * @return true if the overflow menu is currently showing
+     */
+    public boolean isOverflowMenuShowing() {
+        return mMenuView != null && mMenuView.isOverflowMenuShowing();
+    }
+
+    /** @hide */
+    public boolean isOverflowMenuShowPending() {
+        return mMenuView != null && mMenuView.isOverflowMenuShowPending();
+    }
+
+    /**
+     * Show the overflow items from the associated menu.
+     *
+     * @return true if the menu was able to be shown, false otherwise
+     */
+    public boolean showOverflowMenu() {
+        return mMenuView != null && mMenuView.showOverflowMenu();
+    }
+
+    /**
+     * Hide the overflow items from the associated menu.
+     *
+     * @return true if the menu was able to be hidden, false otherwise
+     */
+    public boolean hideOverflowMenu() {
+        return mMenuView != null && mMenuView.hideOverflowMenu();
+    }
+
+    /** @hide */
+    public void setMenu(MenuBuilder menu, ActionMenuPresenter outerPresenter) {
+        if (menu == null && mMenuView == null) {
+            return;
+        }
+
+        ensureMenuView();
+        final MenuBuilder oldMenu = mMenuView.peekMenu();
+        if (oldMenu == menu) {
+            return;
+        }
+
+        if (oldMenu != null) {
+            oldMenu.removeMenuPresenter(mOuterActionMenuPresenter);
+            oldMenu.removeMenuPresenter(mExpandedMenuPresenter);
+        }
+
+        if (mExpandedMenuPresenter == null) {
+            mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+        }
+
+        outerPresenter.setExpandedActionViewsExclusive(true);
+        if (menu != null) {
+            menu.addMenuPresenter(outerPresenter, mPopupContext);
+            menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+        } else {
+            outerPresenter.initForMenu(mPopupContext, null);
+            mExpandedMenuPresenter.initForMenu(mPopupContext, null);
+            outerPresenter.updateMenuView(true);
+            mExpandedMenuPresenter.updateMenuView(true);
+        }
+        mMenuView.setPopupTheme(mPopupTheme);
+        mMenuView.setPresenter(outerPresenter);
+        mOuterActionMenuPresenter = outerPresenter;
+    }
+
+    /**
+     * Dismiss all currently showing popup menus, including overflow or submenus.
+     */
+    public void dismissPopupMenus() {
+        if (mMenuView != null) {
+            mMenuView.dismissPopupMenus();
+        }
+    }
+
+    /** @hide */
+    public boolean isTitleTruncated() {
+        if (mTitleTextView == null) {
+            return false;
+        }
+
+        final Layout titleLayout = mTitleTextView.getLayout();
+        if (titleLayout == null) {
+            return false;
+        }
+
+        final int lineCount = titleLayout.getLineCount();
+        for (int i = 0; i < lineCount; i++) {
+            if (titleLayout.getEllipsisCount(i) > 0) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Set a logo drawable.
+     *
+     * <p>This drawable should generally take the place of title text. The logo cannot be
+     * clicked. Apps using a logo should also supply a description using
+     * {@link #setLogoDescription(int)}.</p>
+     *
+     * @param drawable Drawable to use as a logo
+     */
+    public void setLogo(Drawable drawable) {
+        if (drawable != null) {
+            ensureLogoView();
+            if (!isChildOrHidden(mLogoView)) {
+                addSystemView(mLogoView, true);
+            }
+        } else if (mLogoView != null && isChildOrHidden(mLogoView)) {
+            removeView(mLogoView);
+            mHiddenViews.remove(mLogoView);
+        }
+        if (mLogoView != null) {
+            mLogoView.setImageDrawable(drawable);
+        }
+    }
+
+    /**
+     * Return the current logo drawable.
+     *
+     * @return The current logo drawable
+     * @see #setLogo(int)
+     * @see #setLogo(android.graphics.drawable.Drawable)
+     */
+    public Drawable getLogo() {
+        return mLogoView != null ? mLogoView.getDrawable() : null;
+    }
+
+    /**
+     * Set a description of the toolbar's logo.
+     *
+     * <p>This description will be used for accessibility or other similar descriptions
+     * of the UI.</p>
+     *
+     * @param resId String resource id
+     */
+    public void setLogoDescription(@StringRes int resId) {
+        setLogoDescription(getContext().getText(resId));
+    }
+
+    /**
+     * Set a description of the toolbar's logo.
+     *
+     * <p>This description will be used for accessibility or other similar descriptions
+     * of the UI.</p>
+     *
+     * @param description Description to set
+     */
+    public void setLogoDescription(CharSequence description) {
+        if (!TextUtils.isEmpty(description)) {
+            ensureLogoView();
+        }
+        if (mLogoView != null) {
+            mLogoView.setContentDescription(description);
+        }
+    }
+
+    /**
+     * Return the description of the toolbar's logo.
+     *
+     * @return A description of the logo
+     */
+    public CharSequence getLogoDescription() {
+        return mLogoView != null ? mLogoView.getContentDescription() : null;
+    }
+
+    private void ensureLogoView() {
+        if (mLogoView == null) {
+            mLogoView = new ImageView(getContext());
+        }
+    }
+
+    /**
+     * Check whether this Toolbar is currently hosting an expanded action view.
+     *
+     * <p>An action view may be expanded either directly from the
+     * {@link android.view.MenuItem MenuItem} it belongs to or by user action. If the Toolbar
+     * has an expanded action view it can be collapsed using the {@link #collapseActionView()}
+     * method.</p>
+     *
+     * @return true if the Toolbar has an expanded action view
+     */
+    public boolean hasExpandedActionView() {
+        return mExpandedMenuPresenter != null &&
+                mExpandedMenuPresenter.mCurrentExpandedItem != null;
+    }
+
+    /**
+     * Collapse a currently expanded action view. If this Toolbar does not have an
+     * expanded action view this method has no effect.
+     *
+     * <p>An action view may be expanded either directly from the
+     * {@link android.view.MenuItem MenuItem} it belongs to or by user action.</p>
+     *
+     * @see #hasExpandedActionView()
+     */
+    public void collapseActionView() {
+        final MenuItemImpl item = mExpandedMenuPresenter == null ? null :
+                mExpandedMenuPresenter.mCurrentExpandedItem;
+        if (item != null) {
+            item.collapseActionView();
+        }
+    }
+
+    /**
+     * Returns the title of this toolbar.
+     *
+     * @return The current title.
+     */
+    public CharSequence getTitle() {
+        return mTitleText;
+    }
+
+    /**
+     * Set the title of this toolbar.
+     *
+     * <p>A title should be used as the anchor for a section of content. It should
+     * describe or name the content being viewed.</p>
+     *
+     * @param resId Resource ID of a string to set as the title
+     */
+    public void setTitle(@StringRes int resId) {
+        setTitle(getContext().getText(resId));
+    }
+
+    /**
+     * Set the title of this toolbar.
+     *
+     * <p>A title should be used as the anchor for a section of content. It should
+     * describe or name the content being viewed.</p>
+     *
+     * @param title Title to set
+     */
+    public void setTitle(CharSequence title) {
+        if (!TextUtils.isEmpty(title)) {
+            if (mTitleTextView == null) {
+                final Context context = getContext();
+                mTitleTextView = new TextView(context);
+                mTitleTextView.setSingleLine();
+                mTitleTextView.setEllipsize(TextUtils.TruncateAt.END);
+                if (mTitleTextAppearance != 0) {
+                    mTitleTextView.setTextAppearance(mTitleTextAppearance);
+                }
+                if (mTitleTextColor != 0) {
+                    mTitleTextView.setTextColor(mTitleTextColor);
+                }
+            }
+            if (!isChildOrHidden(mTitleTextView)) {
+                addSystemView(mTitleTextView, true);
+            }
+        } else if (mTitleTextView != null && isChildOrHidden(mTitleTextView)) {
+            removeView(mTitleTextView);
+            mHiddenViews.remove(mTitleTextView);
+        }
+        if (mTitleTextView != null) {
+            mTitleTextView.setText(title);
+        }
+        mTitleText = title;
+    }
+
+    /**
+     * Return the subtitle of this toolbar.
+     *
+     * @return The current subtitle
+     */
+    public CharSequence getSubtitle() {
+        return mSubtitleText;
+    }
+
+    /**
+     * Set the subtitle of this toolbar.
+     *
+     * <p>Subtitles should express extended information about the current content.</p>
+     *
+     * @param resId String resource ID
+     */
+    public void setSubtitle(@StringRes int resId) {
+        setSubtitle(getContext().getText(resId));
+    }
+
+    /**
+     * Set the subtitle of this toolbar.
+     *
+     * <p>Subtitles should express extended information about the current content.</p>
+     *
+     * @param subtitle Subtitle to set
+     */
+    public void setSubtitle(CharSequence subtitle) {
+        if (!TextUtils.isEmpty(subtitle)) {
+            if (mSubtitleTextView == null) {
+                final Context context = getContext();
+                mSubtitleTextView = new TextView(context);
+                mSubtitleTextView.setSingleLine();
+                mSubtitleTextView.setEllipsize(TextUtils.TruncateAt.END);
+                if (mSubtitleTextAppearance != 0) {
+                    mSubtitleTextView.setTextAppearance(mSubtitleTextAppearance);
+                }
+                if (mSubtitleTextColor != 0) {
+                    mSubtitleTextView.setTextColor(mSubtitleTextColor);
+                }
+            }
+            if (!isChildOrHidden(mSubtitleTextView)) {
+                addSystemView(mSubtitleTextView, true);
+            }
+        } else if (mSubtitleTextView != null && isChildOrHidden(mSubtitleTextView)) {
+            removeView(mSubtitleTextView);
+            mHiddenViews.remove(mSubtitleTextView);
+        }
+        if (mSubtitleTextView != null) {
+            mSubtitleTextView.setText(subtitle);
+        }
+        mSubtitleText = subtitle;
+    }
+
+    /**
+     * Sets the text color, size, style, hint color, and highlight color
+     * from the specified TextAppearance resource.
+     */
+    public void setTitleTextAppearance(Context context, @StyleRes int resId) {
+        mTitleTextAppearance = resId;
+        if (mTitleTextView != null) {
+            mTitleTextView.setTextAppearance(resId);
+        }
+    }
+
+    /**
+     * Sets the text color, size, style, hint color, and highlight color
+     * from the specified TextAppearance resource.
+     */
+    public void setSubtitleTextAppearance(Context context, @StyleRes int resId) {
+        mSubtitleTextAppearance = resId;
+        if (mSubtitleTextView != null) {
+            mSubtitleTextView.setTextAppearance(resId);
+        }
+    }
+
+    /**
+     * Sets the text color of the title, if present.
+     *
+     * @param color The new text color in 0xAARRGGBB format
+     */
+    public void setTitleTextColor(@ColorInt int color) {
+        mTitleTextColor = color;
+        if (mTitleTextView != null) {
+            mTitleTextView.setTextColor(color);
+        }
+    }
+
+    /**
+     * Sets the text color of the subtitle, if present.
+     *
+     * @param color The new text color in 0xAARRGGBB format
+     */
+    public void setSubtitleTextColor(@ColorInt int color) {
+        mSubtitleTextColor = color;
+        if (mSubtitleTextView != null) {
+            mSubtitleTextView.setTextColor(color);
+        }
+    }
+
+    /**
+     * Retrieve the currently configured content description for the navigation button view.
+     * This will be used to describe the navigation action to users through mechanisms such
+     * as screen readers or tooltips.
+     *
+     * @return The navigation button's content description
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+     */
+    @Nullable
+    public CharSequence getNavigationContentDescription() {
+        return mNavButtonView != null ? mNavButtonView.getContentDescription() : null;
+    }
+
+    /**
+     * Set a content description for the navigation button if one is present. The content
+     * description will be read via screen readers or other accessibility systems to explain
+     * the action of the navigation button.
+     *
+     * @param resId Resource ID of a content description string to set, or 0 to
+     *              clear the description
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+     */
+    public void setNavigationContentDescription(@StringRes int resId) {
+        setNavigationContentDescription(resId != 0 ? getContext().getText(resId) : null);
+    }
+
+    /**
+     * Set a content description for the navigation button if one is present. The content
+     * description will be read via screen readers or other accessibility systems to explain
+     * the action of the navigation button.
+     *
+     * @param description Content description to set, or <code>null</code> to
+     *                    clear the content description
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationContentDescription
+     */
+    public void setNavigationContentDescription(@Nullable CharSequence description) {
+        if (!TextUtils.isEmpty(description)) {
+            ensureNavButtonView();
+        }
+        if (mNavButtonView != null) {
+            mNavButtonView.setContentDescription(description);
+        }
+    }
+
+    /**
+     * Set the icon to use for the toolbar's navigation button.
+     *
+     * <p>The navigation button appears at the start of the toolbar if present. Setting an icon
+     * will make the navigation button visible.</p>
+     *
+     * <p>If you use a navigation icon you should also set a description for its action using
+     * {@link #setNavigationContentDescription(int)}. This is used for accessibility and
+     * tooltips.</p>
+     *
+     * @param resId Resource ID of a drawable to set
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationIcon
+     */
+    public void setNavigationIcon(@DrawableRes int resId) {
+        setNavigationIcon(getContext().getDrawable(resId));
+    }
+
+    /**
+     * Set the icon to use for the toolbar's navigation button.
+     *
+     * <p>The navigation button appears at the start of the toolbar if present. Setting an icon
+     * will make the navigation button visible.</p>
+     *
+     * <p>If you use a navigation icon you should also set a description for its action using
+     * {@link #setNavigationContentDescription(int)}. This is used for accessibility and
+     * tooltips.</p>
+     *
+     * @param icon Drawable to set, may be null to clear the icon
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationIcon
+     */
+    public void setNavigationIcon(@Nullable Drawable icon) {
+        if (icon != null) {
+            ensureNavButtonView();
+            if (!isChildOrHidden(mNavButtonView)) {
+                addSystemView(mNavButtonView, true);
+            }
+        } else if (mNavButtonView != null && isChildOrHidden(mNavButtonView)) {
+            removeView(mNavButtonView);
+            mHiddenViews.remove(mNavButtonView);
+        }
+        if (mNavButtonView != null) {
+            mNavButtonView.setImageDrawable(icon);
+        }
+    }
+
+    /**
+     * Return the current drawable used as the navigation icon.
+     *
+     * @return The navigation icon drawable
+     *
+     * @attr ref android.R.styleable#Toolbar_navigationIcon
+     */
+    @Nullable
+    public Drawable getNavigationIcon() {
+        return mNavButtonView != null ? mNavButtonView.getDrawable() : null;
+    }
+
+    /**
+     * Set a listener to respond to navigation events.
+     *
+     * <p>This listener will be called whenever the user clicks the navigation button
+     * at the start of the toolbar. An icon must be set for the navigation button to appear.</p>
+     *
+     * @param listener Listener to set
+     * @see #setNavigationIcon(android.graphics.drawable.Drawable)
+     */
+    public void setNavigationOnClickListener(OnClickListener listener) {
+        ensureNavButtonView();
+        mNavButtonView.setOnClickListener(listener);
+    }
+
+    /**
+     * @hide
+     */
+    @Nullable
+    @TestApi
+    public View getNavigationView() {
+        return mNavButtonView;
+    }
+
+    /**
+     * Return the Menu shown in the toolbar.
+     *
+     * <p>Applications that wish to populate the toolbar's menu can do so from here. To use
+     * an XML menu resource, use {@link #inflateMenu(int)}.</p>
+     *
+     * @return The toolbar's Menu
+     */
+    public Menu getMenu() {
+        ensureMenu();
+        return mMenuView.getMenu();
+    }
+
+    /**
+     * Set the icon to use for the overflow button.
+     *
+     * @param icon Drawable to set, may be null to clear the icon
+     */
+    public void setOverflowIcon(@Nullable Drawable icon) {
+        ensureMenu();
+        mMenuView.setOverflowIcon(icon);
+    }
+
+    /**
+     * Return the current drawable used as the overflow icon.
+     *
+     * @return The overflow icon drawable
+     */
+    @Nullable
+    public Drawable getOverflowIcon() {
+        ensureMenu();
+        return mMenuView.getOverflowIcon();
+    }
+
+    private void ensureMenu() {
+        ensureMenuView();
+        if (mMenuView.peekMenu() == null) {
+            // Initialize a new menu for the first time.
+            final MenuBuilder menu = (MenuBuilder) mMenuView.getMenu();
+            if (mExpandedMenuPresenter == null) {
+                mExpandedMenuPresenter = new ExpandedActionViewMenuPresenter();
+            }
+            mMenuView.setExpandedActionViewsExclusive(true);
+            menu.addMenuPresenter(mExpandedMenuPresenter, mPopupContext);
+        }
+    }
+
+    private void ensureMenuView() {
+        if (mMenuView == null) {
+            mMenuView = new ActionMenuView(getContext());
+            mMenuView.setPopupTheme(mPopupTheme);
+            mMenuView.setOnMenuItemClickListener(mMenuViewItemClickListener);
+            mMenuView.setMenuCallbacks(mActionMenuPresenterCallback, mMenuBuilderCallback);
+            final LayoutParams lp = generateDefaultLayoutParams();
+            lp.gravity = Gravity.END | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+            mMenuView.setLayoutParams(lp);
+            addSystemView(mMenuView, false);
+        }
+    }
+
+    private MenuInflater getMenuInflater() {
+        return new MenuInflater(getContext());
+    }
+
+    /**
+     * Inflate a menu resource into this toolbar.
+     *
+     * <p>Inflate an XML menu resource into this toolbar. Existing items in the menu will not
+     * be modified or removed.</p>
+     *
+     * @param resId ID of a menu resource to inflate
+     */
+    public void inflateMenu(@MenuRes int resId) {
+        getMenuInflater().inflate(resId, getMenu());
+    }
+
+    /**
+     * Set a listener to respond to menu item click events.
+     *
+     * <p>This listener will be invoked whenever a user selects a menu item from
+     * the action buttons presented at the end of the toolbar or the associated overflow.</p>
+     *
+     * @param listener Listener to set
+     */
+    public void setOnMenuItemClickListener(OnMenuItemClickListener listener) {
+        mOnMenuItemClickListener = listener;
+    }
+
+    /**
+     * Sets the content insets for this toolbar relative to layout direction.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @param contentInsetStart Content inset for the toolbar starting edge
+     * @param contentInsetEnd Content inset for the toolbar ending edge
+     *
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetStart()
+     * @see #getContentInsetEnd()
+     * @see #getContentInsetLeft()
+     * @see #getContentInsetRight()
+     * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+     * @attr ref android.R.styleable#Toolbar_contentInsetStart
+     */
+    public void setContentInsetsRelative(int contentInsetStart, int contentInsetEnd) {
+        ensureContentInsets();
+        mContentInsets.setRelative(contentInsetStart, contentInsetEnd);
+    }
+
+    /**
+     * Gets the starting content inset for this toolbar.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @return The starting content inset for this toolbar
+     *
+     * @see #setContentInsetsRelative(int, int)
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetEnd()
+     * @see #getContentInsetLeft()
+     * @see #getContentInsetRight()
+     * @attr ref android.R.styleable#Toolbar_contentInsetStart
+     */
+    public int getContentInsetStart() {
+        return mContentInsets != null ? mContentInsets.getStart() : 0;
+    }
+
+    /**
+     * Gets the ending content inset for this toolbar.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @return The ending content inset for this toolbar
+     *
+     * @see #setContentInsetsRelative(int, int)
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetStart()
+     * @see #getContentInsetLeft()
+     * @see #getContentInsetRight()
+     * @attr ref android.R.styleable#Toolbar_contentInsetEnd
+     */
+    public int getContentInsetEnd() {
+        return mContentInsets != null ? mContentInsets.getEnd() : 0;
+    }
+
+    /**
+     * Sets the content insets for this toolbar.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @param contentInsetLeft Content inset for the toolbar's left edge
+     * @param contentInsetRight Content inset for the toolbar's right edge
+     *
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetStart()
+     * @see #getContentInsetEnd()
+     * @see #getContentInsetLeft()
+     * @see #getContentInsetRight()
+     * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+     * @attr ref android.R.styleable#Toolbar_contentInsetRight
+     */
+    public void setContentInsetsAbsolute(int contentInsetLeft, int contentInsetRight) {
+        ensureContentInsets();
+        mContentInsets.setAbsolute(contentInsetLeft, contentInsetRight);
+    }
+
+    /**
+     * Gets the left content inset for this toolbar.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @return The left content inset for this toolbar
+     *
+     * @see #setContentInsetsRelative(int, int)
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetStart()
+     * @see #getContentInsetEnd()
+     * @see #getContentInsetRight()
+     * @attr ref android.R.styleable#Toolbar_contentInsetLeft
+     */
+    public int getContentInsetLeft() {
+        return mContentInsets != null ? mContentInsets.getLeft() : 0;
+    }
+
+    /**
+     * Gets the right content inset for this toolbar.
+     *
+     * <p>The content inset affects the valid area for Toolbar content other than
+     * the navigation button and menu. Insets define the minimum margin for these components
+     * and can be used to effectively align Toolbar content along well-known gridlines.</p>
+     *
+     * @return The right content inset for this toolbar
+     *
+     * @see #setContentInsetsRelative(int, int)
+     * @see #setContentInsetsAbsolute(int, int)
+     * @see #getContentInsetStart()
+     * @see #getContentInsetEnd()
+     * @see #getContentInsetLeft()
+     * @attr ref android.R.styleable#Toolbar_contentInsetRight
+     */
+    public int getContentInsetRight() {
+        return mContentInsets != null ? mContentInsets.getRight() : 0;
+    }
+
+    /**
+     * Gets the start content inset to use when a navigation button is present.
+     *
+     * <p>Different content insets are often called for when additional buttons are present
+     * in the toolbar, as well as at different toolbar sizes. The larger value of
+     * {@link #getContentInsetStart()} and this value will be used during layout.</p>
+     *
+     * @return the start content inset used when a navigation icon has been set in pixels
+     *
+     * @see #setContentInsetStartWithNavigation(int)
+     * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+     */
+    public int getContentInsetStartWithNavigation() {
+        return mContentInsetStartWithNavigation != RtlSpacingHelper.UNDEFINED
+                ? mContentInsetStartWithNavigation
+                : getContentInsetStart();
+    }
+
+    /**
+     * Sets the start content inset to use when a navigation button is present.
+     *
+     * <p>Different content insets are often called for when additional buttons are present
+     * in the toolbar, as well as at different toolbar sizes. The larger value of
+     * {@link #getContentInsetStart()} and this value will be used during layout.</p>
+     *
+     * @param insetStartWithNavigation the inset to use when a navigation icon has been set
+     *                                 in pixels
+     *
+     * @see #getContentInsetStartWithNavigation()
+     * @attr ref android.R.styleable#Toolbar_contentInsetStartWithNavigation
+     */
+    public void setContentInsetStartWithNavigation(int insetStartWithNavigation) {
+        if (insetStartWithNavigation < 0) {
+            insetStartWithNavigation = RtlSpacingHelper.UNDEFINED;
+        }
+        if (insetStartWithNavigation != mContentInsetStartWithNavigation) {
+            mContentInsetStartWithNavigation = insetStartWithNavigation;
+            if (getNavigationIcon() != null) {
+                requestLayout();
+            }
+        }
+    }
+
+    /**
+     * Gets the end content inset to use when action buttons are present.
+     *
+     * <p>Different content insets are often called for when additional buttons are present
+     * in the toolbar, as well as at different toolbar sizes. The larger value of
+     * {@link #getContentInsetEnd()} and this value will be used during layout.</p>
+     *
+     * @return the end content inset used when a menu has been set in pixels
+     *
+     * @see #setContentInsetEndWithActions(int)
+     * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+     */
+    public int getContentInsetEndWithActions() {
+        return mContentInsetEndWithActions != RtlSpacingHelper.UNDEFINED
+                ? mContentInsetEndWithActions
+                : getContentInsetEnd();
+    }
+
+    /**
+     * Sets the start content inset to use when action buttons are present.
+     *
+     * <p>Different content insets are often called for when additional buttons are present
+     * in the toolbar, as well as at different toolbar sizes. The larger value of
+     * {@link #getContentInsetEnd()} and this value will be used during layout.</p>
+     *
+     * @param insetEndWithActions the inset to use when a menu has been set in pixels
+     *
+     * @see #setContentInsetEndWithActions(int)
+     * @attr ref android.R.styleable#Toolbar_contentInsetEndWithActions
+     */
+    public void setContentInsetEndWithActions(int insetEndWithActions) {
+        if (insetEndWithActions < 0) {
+            insetEndWithActions = RtlSpacingHelper.UNDEFINED;
+        }
+        if (insetEndWithActions != mContentInsetEndWithActions) {
+            mContentInsetEndWithActions = insetEndWithActions;
+            if (getNavigationIcon() != null) {
+                requestLayout();
+            }
+        }
+    }
+
+    /**
+     * Gets the content inset that will be used on the starting side of the bar in the current
+     * toolbar configuration.
+     *
+     * @return the current content inset start in pixels
+     *
+     * @see #getContentInsetStartWithNavigation()
+     */
+    public int getCurrentContentInsetStart() {
+        return getNavigationIcon() != null
+                ? Math.max(getContentInsetStart(), Math.max(mContentInsetStartWithNavigation, 0))
+                : getContentInsetStart();
+    }
+
+    /**
+     * Gets the content inset that will be used on the ending side of the bar in the current
+     * toolbar configuration.
+     *
+     * @return the current content inset end in pixels
+     *
+     * @see #getContentInsetEndWithActions()
+     */
+    public int getCurrentContentInsetEnd() {
+        boolean hasActions = false;
+        if (mMenuView != null) {
+            final MenuBuilder mb = mMenuView.peekMenu();
+            hasActions = mb != null && mb.hasVisibleItems();
+        }
+        return hasActions
+                ? Math.max(getContentInsetEnd(), Math.max(mContentInsetEndWithActions, 0))
+                : getContentInsetEnd();
+    }
+
+    /**
+     * Gets the content inset that will be used on the left side of the bar in the current
+     * toolbar configuration.
+     *
+     * @return the current content inset left in pixels
+     *
+     * @see #getContentInsetStartWithNavigation()
+     * @see #getContentInsetEndWithActions()
+     */
+    public int getCurrentContentInsetLeft() {
+        return isLayoutRtl()
+                ? getCurrentContentInsetEnd()
+                : getCurrentContentInsetStart();
+    }
+
+    /**
+     * Gets the content inset that will be used on the right side of the bar in the current
+     * toolbar configuration.
+     *
+     * @return the current content inset right in pixels
+     *
+     * @see #getContentInsetStartWithNavigation()
+     * @see #getContentInsetEndWithActions()
+     */
+    public int getCurrentContentInsetRight() {
+        return isLayoutRtl()
+                ? getCurrentContentInsetStart()
+                : getCurrentContentInsetEnd();
+    }
+
+    private void ensureNavButtonView() {
+        if (mNavButtonView == null) {
+            mNavButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle);
+            final LayoutParams lp = generateDefaultLayoutParams();
+            lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+            mNavButtonView.setLayoutParams(lp);
+        }
+    }
+
+    private void ensureCollapseButtonView() {
+        if (mCollapseButtonView == null) {
+            mCollapseButtonView = new ImageButton(getContext(), null, 0, mNavButtonStyle);
+            mCollapseButtonView.setImageDrawable(mCollapseIcon);
+            mCollapseButtonView.setContentDescription(mCollapseDescription);
+            final LayoutParams lp = generateDefaultLayoutParams();
+            lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+            lp.mViewType = LayoutParams.EXPANDED;
+            mCollapseButtonView.setLayoutParams(lp);
+            mCollapseButtonView.setOnClickListener(new OnClickListener() {
+                @Override
+                public void onClick(View v) {
+                    collapseActionView();
+                }
+            });
+        }
+    }
+
+    private void addSystemView(View v, boolean allowHide) {
+        final ViewGroup.LayoutParams vlp = v.getLayoutParams();
+        final LayoutParams lp;
+        if (vlp == null) {
+            lp = generateDefaultLayoutParams();
+        } else if (!checkLayoutParams(vlp)) {
+            lp = generateLayoutParams(vlp);
+        } else {
+            lp = (LayoutParams) vlp;
+        }
+        lp.mViewType = LayoutParams.SYSTEM;
+
+        if (allowHide && mExpandedActionView != null) {
+            v.setLayoutParams(lp);
+            mHiddenViews.add(v);
+        } else {
+            addView(v, lp);
+        }
+    }
+
+    @Override
+    protected Parcelable onSaveInstanceState() {
+        SavedState state = new SavedState(super.onSaveInstanceState());
+
+        if (mExpandedMenuPresenter != null && mExpandedMenuPresenter.mCurrentExpandedItem != null) {
+            state.expandedMenuItemId = mExpandedMenuPresenter.mCurrentExpandedItem.getItemId();
+        }
+
+        state.isOverflowOpen = isOverflowMenuShowing();
+
+        return state;
+    }
+
+    @Override
+    protected void onRestoreInstanceState(Parcelable state) {
+        final SavedState ss = (SavedState) state;
+        super.onRestoreInstanceState(ss.getSuperState());
+
+        final Menu menu = mMenuView != null ? mMenuView.peekMenu() : null;
+        if (ss.expandedMenuItemId != 0 && mExpandedMenuPresenter != null && menu != null) {
+            final MenuItem item = menu.findItem(ss.expandedMenuItemId);
+            if (item != null) {
+                item.expandActionView();
+            }
+        }
+
+        if (ss.isOverflowOpen) {
+            postShowOverflowMenu();
+        }
+    }
+
+    private void postShowOverflowMenu() {
+        removeCallbacks(mShowOverflowMenuRunnable);
+        post(mShowOverflowMenuRunnable);
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        removeCallbacks(mShowOverflowMenuRunnable);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        // Toolbars always eat touch events, but should still respect the touch event dispatch
+        // contract. If the normal View implementation doesn't want the events, we'll just silently
+        // eat the rest of the gesture without reporting the events to the default implementation
+        // since that's what it expects.
+
+        final int action = ev.getActionMasked();
+        if (action == MotionEvent.ACTION_DOWN) {
+            mEatingTouch = false;
+        }
+
+        if (!mEatingTouch) {
+            final boolean handled = super.onTouchEvent(ev);
+            if (action == MotionEvent.ACTION_DOWN && !handled) {
+                mEatingTouch = true;
+            }
+        }
+
+        if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+            mEatingTouch = false;
+        }
+
+        return true;
+    }
+
+    /**
+     * @hide
+     */
+    @Override
+    protected void onSetLayoutParams(View child, ViewGroup.LayoutParams lp) {
+        /*
+         * Apps may set ActionBar.LayoutParams on their action bar custom views when
+         * a Toolbar is actually acting in the role of the action bar. Perform a quick
+         * switch with Toolbar.LayoutParams whenever this happens. This does leave open
+         * one potential gotcha: if an app retains the ActionBar.LayoutParams reference
+         * and attempts to keep making changes to it before layout those changes won't
+         * be reflected in the final results.
+         */
+        if (!checkLayoutParams(lp)) {
+            child.setLayoutParams(generateLayoutParams(lp));
+        }
+    }
+
+    private void measureChildConstrained(View child, int parentWidthSpec, int widthUsed,
+            int parentHeightSpec, int heightUsed, int heightConstraint) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        int childWidthSpec = getChildMeasureSpec(parentWidthSpec,
+                mPaddingLeft + mPaddingRight + lp.leftMargin + lp.rightMargin
+                        + widthUsed, lp.width);
+        int childHeightSpec = getChildMeasureSpec(parentHeightSpec,
+                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+                        + heightUsed, lp.height);
+
+        final int childHeightMode = MeasureSpec.getMode(childHeightSpec);
+        if (childHeightMode != MeasureSpec.EXACTLY && heightConstraint >= 0) {
+            final int size = childHeightMode != MeasureSpec.UNSPECIFIED ?
+                    Math.min(MeasureSpec.getSize(childHeightSpec), heightConstraint) :
+                    heightConstraint;
+            childHeightSpec = MeasureSpec.makeMeasureSpec(size, MeasureSpec.EXACTLY);
+        }
+        child.measure(childWidthSpec, childHeightSpec);
+    }
+
+    /**
+     * Returns the width + uncollapsed margins
+     */
+    private int measureChildCollapseMargins(View child,
+            int parentWidthMeasureSpec, int widthUsed,
+            int parentHeightMeasureSpec, int heightUsed, int[] collapsingMargins) {
+        final MarginLayoutParams lp = (MarginLayoutParams) child.getLayoutParams();
+
+        final int leftDiff = lp.leftMargin - collapsingMargins[0];
+        final int rightDiff = lp.rightMargin - collapsingMargins[1];
+        final int leftMargin = Math.max(0, leftDiff);
+        final int rightMargin = Math.max(0, rightDiff);
+        final int hMargins = leftMargin + rightMargin;
+        collapsingMargins[0] = Math.max(0, -leftDiff);
+        collapsingMargins[1] = Math.max(0, -rightDiff);
+
+        final int childWidthMeasureSpec = getChildMeasureSpec(parentWidthMeasureSpec,
+                mPaddingLeft + mPaddingRight + hMargins + widthUsed, lp.width);
+        final int childHeightMeasureSpec = getChildMeasureSpec(parentHeightMeasureSpec,
+                mPaddingTop + mPaddingBottom + lp.topMargin + lp.bottomMargin
+                        + heightUsed, lp.height);
+
+        child.measure(childWidthMeasureSpec, childHeightMeasureSpec);
+        return child.getMeasuredWidth() + hMargins;
+    }
+
+    /**
+     * Returns true if the Toolbar is collapsible and has no child views with a measured size > 0.
+     */
+    private boolean shouldCollapse() {
+        if (!mCollapsible) return false;
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            if (shouldLayout(child) && child.getMeasuredWidth() > 0 &&
+                    child.getMeasuredHeight() > 0) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        int width = 0;
+        int height = 0;
+        int childState = 0;
+
+        final int[] collapsingMargins = mTempMargins;
+        final int marginStartIndex;
+        final int marginEndIndex;
+        if (isLayoutRtl()) {
+            marginStartIndex = 1;
+            marginEndIndex = 0;
+        } else {
+            marginStartIndex = 0;
+            marginEndIndex = 1;
+        }
+
+        // System views measure first.
+
+        int navWidth = 0;
+        if (shouldLayout(mNavButtonView)) {
+            measureChildConstrained(mNavButtonView, widthMeasureSpec, width, heightMeasureSpec, 0,
+                    mMaxButtonHeight);
+            navWidth = mNavButtonView.getMeasuredWidth() + getHorizontalMargins(mNavButtonView);
+            height = Math.max(height, mNavButtonView.getMeasuredHeight() +
+                    getVerticalMargins(mNavButtonView));
+            childState = combineMeasuredStates(childState, mNavButtonView.getMeasuredState());
+        }
+
+        if (shouldLayout(mCollapseButtonView)) {
+            measureChildConstrained(mCollapseButtonView, widthMeasureSpec, width,
+                    heightMeasureSpec, 0, mMaxButtonHeight);
+            navWidth = mCollapseButtonView.getMeasuredWidth() +
+                    getHorizontalMargins(mCollapseButtonView);
+            height = Math.max(height, mCollapseButtonView.getMeasuredHeight() +
+                    getVerticalMargins(mCollapseButtonView));
+            childState = combineMeasuredStates(childState, mCollapseButtonView.getMeasuredState());
+        }
+
+        final int contentInsetStart = getCurrentContentInsetStart();
+        width += Math.max(contentInsetStart, navWidth);
+        collapsingMargins[marginStartIndex] = Math.max(0, contentInsetStart - navWidth);
+
+        int menuWidth = 0;
+        if (shouldLayout(mMenuView)) {
+            measureChildConstrained(mMenuView, widthMeasureSpec, width, heightMeasureSpec, 0,
+                    mMaxButtonHeight);
+            menuWidth = mMenuView.getMeasuredWidth() + getHorizontalMargins(mMenuView);
+            height = Math.max(height, mMenuView.getMeasuredHeight() +
+                    getVerticalMargins(mMenuView));
+            childState = combineMeasuredStates(childState, mMenuView.getMeasuredState());
+        }
+
+        final int contentInsetEnd = getCurrentContentInsetEnd();
+        width += Math.max(contentInsetEnd, menuWidth);
+        collapsingMargins[marginEndIndex] = Math.max(0, contentInsetEnd - menuWidth);
+
+        if (shouldLayout(mExpandedActionView)) {
+            width += measureChildCollapseMargins(mExpandedActionView, widthMeasureSpec, width,
+                    heightMeasureSpec, 0, collapsingMargins);
+            height = Math.max(height, mExpandedActionView.getMeasuredHeight() +
+                    getVerticalMargins(mExpandedActionView));
+            childState = combineMeasuredStates(childState, mExpandedActionView.getMeasuredState());
+        }
+
+        if (shouldLayout(mLogoView)) {
+            width += measureChildCollapseMargins(mLogoView, widthMeasureSpec, width,
+                    heightMeasureSpec, 0, collapsingMargins);
+            height = Math.max(height, mLogoView.getMeasuredHeight() +
+                    getVerticalMargins(mLogoView));
+            childState = combineMeasuredStates(childState, mLogoView.getMeasuredState());
+        }
+
+        final int childCount = getChildCount();
+        for (int i = 0; i < childCount; i++) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.mViewType != LayoutParams.CUSTOM || !shouldLayout(child)) {
+                // We already got all system views above. Skip them and GONE views.
+                continue;
+            }
+
+            width += measureChildCollapseMargins(child, widthMeasureSpec, width,
+                    heightMeasureSpec, 0, collapsingMargins);
+            height = Math.max(height, child.getMeasuredHeight() + getVerticalMargins(child));
+            childState = combineMeasuredStates(childState, child.getMeasuredState());
+        }
+
+        int titleWidth = 0;
+        int titleHeight = 0;
+        final int titleVertMargins = mTitleMarginTop + mTitleMarginBottom;
+        final int titleHorizMargins = mTitleMarginStart + mTitleMarginEnd;
+        if (shouldLayout(mTitleTextView)) {
+            titleWidth = measureChildCollapseMargins(mTitleTextView, widthMeasureSpec,
+                    width + titleHorizMargins, heightMeasureSpec, titleVertMargins,
+                    collapsingMargins);
+            titleWidth = mTitleTextView.getMeasuredWidth() + getHorizontalMargins(mTitleTextView);
+            titleHeight = mTitleTextView.getMeasuredHeight() + getVerticalMargins(mTitleTextView);
+            childState = combineMeasuredStates(childState, mTitleTextView.getMeasuredState());
+        }
+        if (shouldLayout(mSubtitleTextView)) {
+            titleWidth = Math.max(titleWidth, measureChildCollapseMargins(mSubtitleTextView,
+                    widthMeasureSpec, width + titleHorizMargins,
+                    heightMeasureSpec, titleHeight + titleVertMargins,
+                    collapsingMargins));
+            titleHeight += mSubtitleTextView.getMeasuredHeight() +
+                    getVerticalMargins(mSubtitleTextView);
+            childState = combineMeasuredStates(childState, mSubtitleTextView.getMeasuredState());
+        }
+
+        width += titleWidth;
+        height = Math.max(height, titleHeight);
+
+        // Measurement already took padding into account for available space for the children,
+        // add it in for the final size.
+        width += getPaddingLeft() + getPaddingRight();
+        height += getPaddingTop() + getPaddingBottom();
+
+        final int measuredWidth = resolveSizeAndState(
+                Math.max(width, getSuggestedMinimumWidth()),
+                widthMeasureSpec, childState & MEASURED_STATE_MASK);
+        final int measuredHeight = resolveSizeAndState(
+                Math.max(height, getSuggestedMinimumHeight()),
+                heightMeasureSpec, childState << MEASURED_HEIGHT_STATE_SHIFT);
+
+        setMeasuredDimension(measuredWidth, shouldCollapse() ? 0 : measuredHeight);
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int l, int t, int r, int b) {
+        final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        final int width = getWidth();
+        final int height = getHeight();
+        final int paddingLeft = getPaddingLeft();
+        final int paddingRight = getPaddingRight();
+        final int paddingTop = getPaddingTop();
+        final int paddingBottom = getPaddingBottom();
+        int left = paddingLeft;
+        int right = width - paddingRight;
+
+        final int[] collapsingMargins = mTempMargins;
+        collapsingMargins[0] = collapsingMargins[1] = 0;
+
+        // Align views within the minimum toolbar height, if set.
+        final int minHeight = getMinimumHeight();
+        final int alignmentHeight = minHeight >= 0 ? Math.min(minHeight, b - t) : 0;
+
+        if (shouldLayout(mNavButtonView)) {
+            if (isRtl) {
+                right = layoutChildRight(mNavButtonView, right, collapsingMargins,
+                        alignmentHeight);
+            } else {
+                left = layoutChildLeft(mNavButtonView, left, collapsingMargins,
+                        alignmentHeight);
+            }
+        }
+
+        if (shouldLayout(mCollapseButtonView)) {
+            if (isRtl) {
+                right = layoutChildRight(mCollapseButtonView, right, collapsingMargins,
+                        alignmentHeight);
+            } else {
+                left = layoutChildLeft(mCollapseButtonView, left, collapsingMargins,
+                        alignmentHeight);
+            }
+        }
+
+        if (shouldLayout(mMenuView)) {
+            if (isRtl) {
+                left = layoutChildLeft(mMenuView, left, collapsingMargins,
+                        alignmentHeight);
+            } else {
+                right = layoutChildRight(mMenuView, right, collapsingMargins,
+                        alignmentHeight);
+            }
+        }
+
+        final int contentInsetLeft = getCurrentContentInsetLeft();
+        final int contentInsetRight = getCurrentContentInsetRight();
+        collapsingMargins[0] = Math.max(0, contentInsetLeft - left);
+        collapsingMargins[1] = Math.max(0, contentInsetRight - (width - paddingRight - right));
+        left = Math.max(left, contentInsetLeft);
+        right = Math.min(right, width - paddingRight - contentInsetRight);
+
+        if (shouldLayout(mExpandedActionView)) {
+            if (isRtl) {
+                right = layoutChildRight(mExpandedActionView, right, collapsingMargins,
+                        alignmentHeight);
+            } else {
+                left = layoutChildLeft(mExpandedActionView, left, collapsingMargins,
+                        alignmentHeight);
+            }
+        }
+
+        if (shouldLayout(mLogoView)) {
+            if (isRtl) {
+                right = layoutChildRight(mLogoView, right, collapsingMargins,
+                        alignmentHeight);
+            } else {
+                left = layoutChildLeft(mLogoView, left, collapsingMargins,
+                        alignmentHeight);
+            }
+        }
+
+        final boolean layoutTitle = shouldLayout(mTitleTextView);
+        final boolean layoutSubtitle = shouldLayout(mSubtitleTextView);
+        int titleHeight = 0;
+        if (layoutTitle) {
+            final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+            titleHeight += lp.topMargin + mTitleTextView.getMeasuredHeight() + lp.bottomMargin;
+        }
+        if (layoutSubtitle) {
+            final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+            titleHeight += lp.topMargin + mSubtitleTextView.getMeasuredHeight() + lp.bottomMargin;
+        }
+
+        if (layoutTitle || layoutSubtitle) {
+            int titleTop;
+            final View topChild = layoutTitle ? mTitleTextView : mSubtitleTextView;
+            final View bottomChild = layoutSubtitle ? mSubtitleTextView : mTitleTextView;
+            final LayoutParams toplp = (LayoutParams) topChild.getLayoutParams();
+            final LayoutParams bottomlp = (LayoutParams) bottomChild.getLayoutParams();
+            final boolean titleHasWidth = layoutTitle && mTitleTextView.getMeasuredWidth() > 0
+                    || layoutSubtitle && mSubtitleTextView.getMeasuredWidth() > 0;
+
+            switch (mGravity & Gravity.VERTICAL_GRAVITY_MASK) {
+                case Gravity.TOP:
+                    titleTop = getPaddingTop() + toplp.topMargin + mTitleMarginTop;
+                    break;
+                default:
+                case Gravity.CENTER_VERTICAL:
+                    final int space = height - paddingTop - paddingBottom;
+                    int spaceAbove = (space - titleHeight) / 2;
+                    if (spaceAbove < toplp.topMargin + mTitleMarginTop) {
+                        spaceAbove = toplp.topMargin + mTitleMarginTop;
+                    } else {
+                        final int spaceBelow = height - paddingBottom - titleHeight -
+                                spaceAbove - paddingTop;
+                        if (spaceBelow < toplp.bottomMargin + mTitleMarginBottom) {
+                            spaceAbove = Math.max(0, spaceAbove -
+                                    (bottomlp.bottomMargin + mTitleMarginBottom - spaceBelow));
+                        }
+                    }
+                    titleTop = paddingTop + spaceAbove;
+                    break;
+                case Gravity.BOTTOM:
+                    titleTop = height - paddingBottom - bottomlp.bottomMargin - mTitleMarginBottom -
+                            titleHeight;
+                    break;
+            }
+            if (isRtl) {
+                final int rd = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[1];
+                right -= Math.max(0, rd);
+                collapsingMargins[1] = Math.max(0, -rd);
+                int titleRight = right;
+                int subtitleRight = right;
+
+                if (layoutTitle) {
+                    final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+                    final int titleLeft = titleRight - mTitleTextView.getMeasuredWidth();
+                    final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
+                    mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
+                    titleRight = titleLeft - mTitleMarginEnd;
+                    titleTop = titleBottom + lp.bottomMargin;
+                }
+                if (layoutSubtitle) {
+                    final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+                    titleTop += lp.topMargin;
+                    final int subtitleLeft = subtitleRight - mSubtitleTextView.getMeasuredWidth();
+                    final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
+                    mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
+                    subtitleRight = subtitleRight - mTitleMarginEnd;
+                    titleTop = subtitleBottom + lp.bottomMargin;
+                }
+                if (titleHasWidth) {
+                    right = Math.min(titleRight, subtitleRight);
+                }
+            } else {
+                final int ld = (titleHasWidth ? mTitleMarginStart : 0) - collapsingMargins[0];
+                left += Math.max(0, ld);
+                collapsingMargins[0] = Math.max(0, -ld);
+                int titleLeft = left;
+                int subtitleLeft = left;
+
+                if (layoutTitle) {
+                    final LayoutParams lp = (LayoutParams) mTitleTextView.getLayoutParams();
+                    final int titleRight = titleLeft + mTitleTextView.getMeasuredWidth();
+                    final int titleBottom = titleTop + mTitleTextView.getMeasuredHeight();
+                    mTitleTextView.layout(titleLeft, titleTop, titleRight, titleBottom);
+                    titleLeft = titleRight + mTitleMarginEnd;
+                    titleTop = titleBottom + lp.bottomMargin;
+                }
+                if (layoutSubtitle) {
+                    final LayoutParams lp = (LayoutParams) mSubtitleTextView.getLayoutParams();
+                    titleTop += lp.topMargin;
+                    final int subtitleRight = subtitleLeft + mSubtitleTextView.getMeasuredWidth();
+                    final int subtitleBottom = titleTop + mSubtitleTextView.getMeasuredHeight();
+                    mSubtitleTextView.layout(subtitleLeft, titleTop, subtitleRight, subtitleBottom);
+                    subtitleLeft = subtitleRight + mTitleMarginEnd;
+                    titleTop = subtitleBottom + lp.bottomMargin;
+                }
+                if (titleHasWidth) {
+                    left = Math.max(titleLeft, subtitleLeft);
+                }
+            }
+        }
+
+        // Get all remaining children sorted for layout. This is all prepared
+        // such that absolute layout direction can be used below.
+
+        addCustomViewsWithGravity(mTempViews, Gravity.LEFT);
+        final int leftViewsCount = mTempViews.size();
+        for (int i = 0; i < leftViewsCount; i++) {
+            left = layoutChildLeft(mTempViews.get(i), left, collapsingMargins,
+                    alignmentHeight);
+        }
+
+        addCustomViewsWithGravity(mTempViews, Gravity.RIGHT);
+        final int rightViewsCount = mTempViews.size();
+        for (int i = 0; i < rightViewsCount; i++) {
+            right = layoutChildRight(mTempViews.get(i), right, collapsingMargins,
+                    alignmentHeight);
+        }
+
+        // Centered views try to center with respect to the whole bar, but views pinned
+        // to the left or right can push the mass of centered views to one side or the other.
+        addCustomViewsWithGravity(mTempViews, Gravity.CENTER_HORIZONTAL);
+        final int centerViewsWidth = getViewListMeasuredWidth(mTempViews, collapsingMargins);
+        final int parentCenter = paddingLeft + (width - paddingLeft - paddingRight) / 2;
+        final int halfCenterViewsWidth = centerViewsWidth / 2;
+        int centerLeft = parentCenter - halfCenterViewsWidth;
+        final int centerRight = centerLeft + centerViewsWidth;
+        if (centerLeft < left) {
+            centerLeft = left;
+        } else if (centerRight > right) {
+            centerLeft -= centerRight - right;
+        }
+
+        final int centerViewsCount = mTempViews.size();
+        for (int i = 0; i < centerViewsCount; i++) {
+            centerLeft = layoutChildLeft(mTempViews.get(i), centerLeft, collapsingMargins,
+                    alignmentHeight);
+        }
+
+        mTempViews.clear();
+    }
+
+    private int getViewListMeasuredWidth(List<View> views, int[] collapsingMargins) {
+        int collapseLeft = collapsingMargins[0];
+        int collapseRight = collapsingMargins[1];
+        int width = 0;
+        final int count = views.size();
+        for (int i = 0; i < count; i++) {
+            final View v = views.get(i);
+            final LayoutParams lp = (LayoutParams) v.getLayoutParams();
+            final int l = lp.leftMargin - collapseLeft;
+            final int r = lp.rightMargin - collapseRight;
+            final int leftMargin = Math.max(0, l);
+            final int rightMargin = Math.max(0, r);
+            collapseLeft = Math.max(0, -l);
+            collapseRight = Math.max(0, -r);
+            width += leftMargin + v.getMeasuredWidth() + rightMargin;
+        }
+        return width;
+    }
+
+    private int layoutChildLeft(View child, int left, int[] collapsingMargins,
+            int alignmentHeight) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        final int l = lp.leftMargin - collapsingMargins[0];
+        left += Math.max(0, l);
+        collapsingMargins[0] = Math.max(0, -l);
+        final int top = getChildTop(child, alignmentHeight);
+        final int childWidth = child.getMeasuredWidth();
+        child.layout(left, top, left + childWidth, top + child.getMeasuredHeight());
+        left += childWidth + lp.rightMargin;
+        return left;
+    }
+
+    private int layoutChildRight(View child, int right, int[] collapsingMargins,
+            int alignmentHeight) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        final int r = lp.rightMargin - collapsingMargins[1];
+        right -= Math.max(0, r);
+        collapsingMargins[1] = Math.max(0, -r);
+        final int top = getChildTop(child, alignmentHeight);
+        final int childWidth = child.getMeasuredWidth();
+        child.layout(right - childWidth, top, right, top + child.getMeasuredHeight());
+        right -= childWidth + lp.leftMargin;
+        return right;
+    }
+
+    private int getChildTop(View child, int alignmentHeight) {
+        final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        final int childHeight = child.getMeasuredHeight();
+        final int alignmentOffset = alignmentHeight > 0 ? (childHeight - alignmentHeight) / 2 : 0;
+        switch (getChildVerticalGravity(lp.gravity)) {
+            case Gravity.TOP:
+                return getPaddingTop() - alignmentOffset;
+
+            case Gravity.BOTTOM:
+                return getHeight() - getPaddingBottom() - childHeight
+                        - lp.bottomMargin - alignmentOffset;
+
+            default:
+            case Gravity.CENTER_VERTICAL:
+                final int paddingTop = getPaddingTop();
+                final int paddingBottom = getPaddingBottom();
+                final int height = getHeight();
+                final int space = height - paddingTop - paddingBottom;
+                int spaceAbove = (space - childHeight) / 2;
+                if (spaceAbove < lp.topMargin) {
+                    spaceAbove = lp.topMargin;
+                } else {
+                    final int spaceBelow = height - paddingBottom - childHeight -
+                            spaceAbove - paddingTop;
+                    if (spaceBelow < lp.bottomMargin) {
+                        spaceAbove = Math.max(0, spaceAbove - (lp.bottomMargin - spaceBelow));
+                    }
+                }
+                return paddingTop + spaceAbove;
+        }
+    }
+
+    private int getChildVerticalGravity(int gravity) {
+        final int vgrav = gravity & Gravity.VERTICAL_GRAVITY_MASK;
+        switch (vgrav) {
+            case Gravity.TOP:
+            case Gravity.BOTTOM:
+            case Gravity.CENTER_VERTICAL:
+                return vgrav;
+            default:
+                return mGravity & Gravity.VERTICAL_GRAVITY_MASK;
+        }
+    }
+
+    /**
+     * Prepare a list of non-SYSTEM child views. If the layout direction is RTL
+     * this will be in reverse child order.
+     *
+     * @param views List to populate. It will be cleared before use.
+     * @param gravity Horizontal gravity to match against
+     */
+    private void addCustomViewsWithGravity(List<View> views, int gravity) {
+        final boolean isRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL;
+        final int childCount = getChildCount();
+        final int absGrav = Gravity.getAbsoluteGravity(gravity, getLayoutDirection());
+
+        views.clear();
+
+        if (isRtl) {
+            for (int i = childCount - 1; i >= 0; i--) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.mViewType == LayoutParams.CUSTOM && shouldLayout(child) &&
+                        getChildHorizontalGravity(lp.gravity) == absGrav) {
+                    views.add(child);
+                }
+            }
+        } else {
+            for (int i = 0; i < childCount; i++) {
+                final View child = getChildAt(i);
+                final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+                if (lp.mViewType == LayoutParams.CUSTOM && shouldLayout(child) &&
+                        getChildHorizontalGravity(lp.gravity) == absGrav) {
+                    views.add(child);
+                }
+            }
+        }
+    }
+
+    private int getChildHorizontalGravity(int gravity) {
+        final int ld = getLayoutDirection();
+        final int absGrav = Gravity.getAbsoluteGravity(gravity, ld);
+        final int hGrav = absGrav & Gravity.HORIZONTAL_GRAVITY_MASK;
+        switch (hGrav) {
+            case Gravity.LEFT:
+            case Gravity.RIGHT:
+            case Gravity.CENTER_HORIZONTAL:
+                return hGrav;
+            default:
+                return ld == LAYOUT_DIRECTION_RTL ? Gravity.RIGHT : Gravity.LEFT;
+        }
+    }
+
+    private boolean shouldLayout(View view) {
+        return view != null && view.getParent() == this && view.getVisibility() != GONE;
+    }
+
+    private int getHorizontalMargins(View v) {
+        final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+        return mlp.getMarginStart() + mlp.getMarginEnd();
+    }
+
+    private int getVerticalMargins(View v) {
+        final MarginLayoutParams mlp = (MarginLayoutParams) v.getLayoutParams();
+        return mlp.topMargin + mlp.bottomMargin;
+    }
+
+    @Override
+    public LayoutParams generateLayoutParams(AttributeSet attrs) {
+        return new LayoutParams(getContext(), attrs);
+    }
+
+    @Override
+    protected LayoutParams generateLayoutParams(ViewGroup.LayoutParams p) {
+        if (p instanceof LayoutParams) {
+            return new LayoutParams((LayoutParams) p);
+        } else if (p instanceof ActionBar.LayoutParams) {
+            return new LayoutParams((ActionBar.LayoutParams) p);
+        } else if (p instanceof MarginLayoutParams) {
+            return new LayoutParams((MarginLayoutParams) p);
+        } else {
+            return new LayoutParams(p);
+        }
+    }
+
+    @Override
+    protected LayoutParams generateDefaultLayoutParams() {
+        return new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+    }
+
+    @Override
+    protected boolean checkLayoutParams(ViewGroup.LayoutParams p) {
+        return super.checkLayoutParams(p) && p instanceof LayoutParams;
+    }
+
+    private static boolean isCustomView(View child) {
+        return ((LayoutParams) child.getLayoutParams()).mViewType == LayoutParams.CUSTOM;
+    }
+
+    /** @hide */
+    public DecorToolbar getWrapper() {
+        if (mWrapper == null) {
+            mWrapper = new ToolbarWidgetWrapper(this, true);
+        }
+        return mWrapper;
+    }
+
+    void removeChildrenForExpandedActionView() {
+        final int childCount = getChildCount();
+        // Go backwards since we're removing from the list
+        for (int i = childCount - 1; i >= 0; i--) {
+            final View child = getChildAt(i);
+            final LayoutParams lp = (LayoutParams) child.getLayoutParams();
+            if (lp.mViewType != LayoutParams.EXPANDED && child != mMenuView) {
+                removeViewAt(i);
+                mHiddenViews.add(child);
+            }
+        }
+    }
+
+    void addChildrenForExpandedActionView() {
+        final int count = mHiddenViews.size();
+        // Re-add in reverse order since we removed in reverse order
+        for (int i = count - 1; i >= 0; i--) {
+            addView(mHiddenViews.get(i));
+        }
+        mHiddenViews.clear();
+    }
+
+    private boolean isChildOrHidden(View child) {
+        return child.getParent() == this || mHiddenViews.contains(child);
+    }
+
+    /**
+     * Force the toolbar to collapse to zero-height during measurement if
+     * it could be considered "empty" (no visible elements with nonzero measured size)
+     * @hide
+     */
+    public void setCollapsible(boolean collapsible) {
+        mCollapsible = collapsible;
+        requestLayout();
+    }
+
+    /**
+     * Must be called before the menu is accessed
+     * @hide
+     */
+    public void setMenuCallbacks(MenuPresenter.Callback pcb, MenuBuilder.Callback mcb) {
+        mActionMenuPresenterCallback = pcb;
+        mMenuBuilderCallback = mcb;
+        if (mMenuView != null) {
+            mMenuView.setMenuCallbacks(pcb, mcb);
+        }
+    }
+
+    private void ensureContentInsets() {
+        if (mContentInsets == null) {
+            mContentInsets = new RtlSpacingHelper();
+        }
+    }
+
+    /**
+     * Accessor to enable LayoutLib to get ActionMenuPresenter directly.
+     */
+    ActionMenuPresenter getOuterActionMenuPresenter() {
+        return mOuterActionMenuPresenter;
+    }
+
+    Context getPopupContext() {
+        return mPopupContext;
+    }
+
+    /**
+     * Interface responsible for receiving menu item click events if the items themselves
+     * do not have individual item click listeners.
+     */
+    public interface OnMenuItemClickListener {
+        /**
+         * This method will be invoked when a menu item is clicked if the item itself did
+         * not already handle the event.
+         *
+         * @param item {@link MenuItem} that was clicked
+         * @return <code>true</code> if the event was handled, <code>false</code> otherwise.
+         */
+        public boolean onMenuItemClick(MenuItem item);
+    }
+
+    /**
+     * Layout information for child views of Toolbars.
+     *
+     * <p>Toolbar.LayoutParams extends ActionBar.LayoutParams for compatibility with existing
+     * ActionBar API. See {@link android.app.Activity#setActionBar(Toolbar) Activity.setActionBar}
+     * for more info on how to use a Toolbar as your Activity's ActionBar.</p>
+     *
+     * @attr ref android.R.styleable#Toolbar_LayoutParams_layout_gravity
+     */
+    public static class LayoutParams extends ActionBar.LayoutParams {
+        static final int CUSTOM = 0;
+        static final int SYSTEM = 1;
+        static final int EXPANDED = 2;
+
+        int mViewType = CUSTOM;
+
+        public LayoutParams(@NonNull Context c, AttributeSet attrs) {
+            super(c, attrs);
+        }
+
+        public LayoutParams(int width, int height) {
+            super(width, height);
+            this.gravity = Gravity.CENTER_VERTICAL | Gravity.START;
+        }
+
+        public LayoutParams(int width, int height, int gravity) {
+            super(width, height);
+            this.gravity = gravity;
+        }
+
+        public LayoutParams(int gravity) {
+            this(WRAP_CONTENT, MATCH_PARENT, gravity);
+        }
+
+        public LayoutParams(LayoutParams source) {
+            super(source);
+
+            mViewType = source.mViewType;
+        }
+
+        public LayoutParams(ActionBar.LayoutParams source) {
+            super(source);
+        }
+
+        public LayoutParams(MarginLayoutParams source) {
+            super(source);
+            // ActionBar.LayoutParams doesn't have a MarginLayoutParams constructor.
+            // Fake it here and copy over the relevant data.
+            copyMarginsFrom(source);
+        }
+
+        public LayoutParams(ViewGroup.LayoutParams source) {
+            super(source);
+        }
+    }
+
+    static class SavedState extends BaseSavedState {
+        public int expandedMenuItemId;
+        public boolean isOverflowOpen;
+
+        public SavedState(Parcel source) {
+            super(source);
+            expandedMenuItemId = source.readInt();
+            isOverflowOpen = source.readInt() != 0;
+        }
+
+        public SavedState(Parcelable superState) {
+            super(superState);
+        }
+
+        @Override
+        public void writeToParcel(Parcel out, int flags) {
+            super.writeToParcel(out, flags);
+            out.writeInt(expandedMenuItemId);
+            out.writeInt(isOverflowOpen ? 1 : 0);
+        }
+
+        public static final Creator<SavedState> CREATOR = new Creator<SavedState>() {
+
+            @Override
+            public SavedState createFromParcel(Parcel source) {
+                return new SavedState(source);
+            }
+
+            @Override
+            public SavedState[] newArray(int size) {
+                return new SavedState[size];
+            }
+        };
+    }
+
+    private class ExpandedActionViewMenuPresenter implements MenuPresenter {
+        MenuBuilder mMenu;
+        MenuItemImpl mCurrentExpandedItem;
+
+        @Override
+        public void initForMenu(@NonNull Context context, @Nullable MenuBuilder menu) {
+            // Clear the expanded action view when menus change.
+            if (mMenu != null && mCurrentExpandedItem != null) {
+                mMenu.collapseItemActionView(mCurrentExpandedItem);
+            }
+            mMenu = menu;
+        }
+
+        @Override
+        public MenuView getMenuView(ViewGroup root) {
+            return null;
+        }
+
+        @Override
+        public void updateMenuView(boolean cleared) {
+            // Make sure the expanded item we have is still there.
+            if (mCurrentExpandedItem != null) {
+                boolean found = false;
+
+                if (mMenu != null) {
+                    final int count = mMenu.size();
+                    for (int i = 0; i < count; i++) {
+                        final MenuItem item = mMenu.getItem(i);
+                        if (item == mCurrentExpandedItem) {
+                            found = true;
+                            break;
+                        }
+                    }
+                }
+
+                if (!found) {
+                    // The item we had expanded disappeared. Collapse.
+                    collapseItemActionView(mMenu, mCurrentExpandedItem);
+                }
+            }
+        }
+
+        @Override
+        public void setCallback(Callback cb) {
+        }
+
+        @Override
+        public boolean onSubMenuSelected(SubMenuBuilder subMenu) {
+            return false;
+        }
+
+        @Override
+        public void onCloseMenu(MenuBuilder menu, boolean allMenusAreClosing) {
+        }
+
+        @Override
+        public boolean flagActionItems() {
+            return false;
+        }
+
+        @Override
+        public boolean expandItemActionView(MenuBuilder menu, MenuItemImpl item) {
+            ensureCollapseButtonView();
+            if (mCollapseButtonView.getParent() != Toolbar.this) {
+                addView(mCollapseButtonView);
+            }
+            mExpandedActionView = item.getActionView();
+            mCurrentExpandedItem = item;
+            if (mExpandedActionView.getParent() != Toolbar.this) {
+                final LayoutParams lp = generateDefaultLayoutParams();
+                lp.gravity = Gravity.START | (mButtonGravity & Gravity.VERTICAL_GRAVITY_MASK);
+                lp.mViewType = LayoutParams.EXPANDED;
+                mExpandedActionView.setLayoutParams(lp);
+                addView(mExpandedActionView);
+            }
+
+            removeChildrenForExpandedActionView();
+            requestLayout();
+            item.setActionViewExpanded(true);
+
+            if (mExpandedActionView instanceof CollapsibleActionView) {
+                ((CollapsibleActionView) mExpandedActionView).onActionViewExpanded();
+            }
+
+            return true;
+        }
+
+        @Override
+        public boolean collapseItemActionView(MenuBuilder menu, MenuItemImpl item) {
+            // Do this before detaching the actionview from the hierarchy, in case
+            // it needs to dismiss the soft keyboard, etc.
+            if (mExpandedActionView instanceof CollapsibleActionView) {
+                ((CollapsibleActionView) mExpandedActionView).onActionViewCollapsed();
+            }
+
+            removeView(mExpandedActionView);
+            removeView(mCollapseButtonView);
+            mExpandedActionView = null;
+
+            addChildrenForExpandedActionView();
+            mCurrentExpandedItem = null;
+            requestLayout();
+            item.setActionViewExpanded(false);
+
+            return true;
+        }
+
+        @Override
+        public int getId() {
+            return 0;
+        }
+
+        @Override
+        public Parcelable onSaveInstanceState() {
+            return null;
+        }
+
+        @Override
+        public void onRestoreInstanceState(Parcelable state) {
+        }
+    }
+}
diff --git a/android/widget/Toolbar_Accessor.java b/android/widget/Toolbar_Accessor.java
new file mode 100644
index 0000000..fdd1779
--- /dev/null
+++ b/android/widget/Toolbar_Accessor.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.content.Context;
+
+/**
+ * To access non public members of classes in {@link Toolbar}
+ */
+public class Toolbar_Accessor {
+    public static ActionMenuPresenter getActionMenuPresenter(Toolbar toolbar) {
+        return toolbar.getOuterActionMenuPresenter();
+    }
+
+    public static Context getPopupContext(Toolbar toolbar) {
+        return toolbar.getPopupContext();
+    }
+}
diff --git a/android/widget/TwoLineListItem.java b/android/widget/TwoLineListItem.java
new file mode 100644
index 0000000..553b86e
--- /dev/null
+++ b/android/widget/TwoLineListItem.java
@@ -0,0 +1,97 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+
+/**
+ * <p>A view group with two children, intended for use in ListViews. This item has two
+ * {@link android.widget.TextView TextViews} elements (or subclasses) with the ID values
+ * {@link android.R.id#text1 text1}
+ * and {@link android.R.id#text2 text2}. There is an optional third View element with the
+ * ID {@link android.R.id#selectedIcon selectedIcon}, which can be any View subclass
+ * (though it is typically a graphic View, such as {@link android.widget.ImageView ImageView})
+ * that can be displayed when a TwoLineListItem has focus. Android supplies a
+ * {@link android.R.layout#two_line_list_item standard layout resource for TwoLineListView}
+ * (which does not include a selected item icon), but you can design your own custom XML
+ * layout for this object.
+ *
+ * @attr ref android.R.styleable#TwoLineListItem_mode
+ *
+ * @deprecated This class can be implemented easily by apps using a {@link RelativeLayout}
+ * or a {@link LinearLayout}.
+ */
+@Deprecated
+@Widget
+public class TwoLineListItem extends RelativeLayout {
+
+    private TextView mText1;
+    private TextView mText2;
+
+    public TwoLineListItem(Context context) {
+        this(context, null, 0);
+    }
+
+    public TwoLineListItem(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public TwoLineListItem(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final TypedArray a = context.obtainStyledAttributes(
+                attrs, com.android.internal.R.styleable.TwoLineListItem, defStyleAttr, defStyleRes);
+
+        a.recycle();
+    }
+
+    @Override
+    protected void onFinishInflate() {
+        super.onFinishInflate();
+
+        mText1 = findViewById(com.android.internal.R.id.text1);
+        mText2 = findViewById(com.android.internal.R.id.text2);
+    }
+
+    /**
+     * Returns a handle to the item with ID text1.
+     * @return A handle to the item with ID text1.
+     */
+    public TextView getText1() {
+        return mText1;
+    }
+
+    /**
+     * Returns a handle to the item with ID text2.
+     * @return A handle to the item with ID text2.
+     */
+    public TextView getText2() {
+        return mText2;
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return TwoLineListItem.class.getName();
+    }
+}
diff --git a/android/widget/VideoView.java b/android/widget/VideoView.java
new file mode 100644
index 0000000..58a2b0f
--- /dev/null
+++ b/android/widget/VideoView.java
@@ -0,0 +1,975 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.annotation.NonNull;
+import android.app.AlertDialog;
+import android.content.Context;
+import android.content.DialogInterface;
+import android.content.res.Resources;
+import android.graphics.Canvas;
+import android.media.AudioAttributes;
+import android.media.AudioManager;
+import android.media.Cea708CaptionRenderer;
+import android.media.ClosedCaptionRenderer;
+import android.media.MediaFormat;
+import android.media.MediaPlayer;
+import android.media.MediaPlayer.OnCompletionListener;
+import android.media.MediaPlayer.OnErrorListener;
+import android.media.MediaPlayer.OnInfoListener;
+import android.media.Metadata;
+import android.media.SubtitleController;
+import android.media.SubtitleTrack.RenderingWidget;
+import android.media.TtmlRenderer;
+import android.media.WebVttRenderer;
+import android.net.Uri;
+import android.os.Looper;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.util.Pair;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.SurfaceHolder;
+import android.view.SurfaceView;
+import android.view.View;
+import android.widget.MediaController.MediaPlayerControl;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Map;
+import java.util.Vector;
+
+/**
+ * Displays a video file.  The VideoView class
+ * can load images from various sources (such as resources or content
+ * providers), takes care of computing its measurement from the video so that
+ * it can be used in any layout manager, and provides various display options
+ * such as scaling and tinting.<p>
+ *
+ * <em>Note: VideoView does not retain its full state when going into the
+ * background.</em>  In particular, it does not restore the current play state,
+ * play position, selected tracks, or any subtitle tracks added via
+ * {@link #addSubtitleSource addSubtitleSource()}.  Applications should
+ * save and restore these on their own in
+ * {@link android.app.Activity#onSaveInstanceState} and
+ * {@link android.app.Activity#onRestoreInstanceState}.<p>
+ * Also note that the audio session id (from {@link #getAudioSessionId}) may
+ * change from its previously returned value when the VideoView is restored.
+ * <p>
+ * By default, VideoView requests audio focus with {@link AudioManager#AUDIOFOCUS_GAIN}. Use
+ * {@link #setAudioFocusRequest(int)} to change this behavior.
+ * <p>
+ * The default {@link AudioAttributes} used during playback have a usage of
+ * {@link AudioAttributes#USAGE_MEDIA} and a content type of
+ * {@link AudioAttributes#CONTENT_TYPE_MOVIE}, use {@link #setAudioAttributes(AudioAttributes)} to
+ * modify them.
+ */
+public class VideoView extends SurfaceView
+        implements MediaPlayerControl, SubtitleController.Anchor {
+    private static final String TAG = "VideoView";
+
+    // all possible internal states
+    private static final int STATE_ERROR = -1;
+    private static final int STATE_IDLE = 0;
+    private static final int STATE_PREPARING = 1;
+    private static final int STATE_PREPARED = 2;
+    private static final int STATE_PLAYING = 3;
+    private static final int STATE_PAUSED = 4;
+    private static final int STATE_PLAYBACK_COMPLETED = 5;
+
+    private final Vector<Pair<InputStream, MediaFormat>> mPendingSubtitleTracks = new Vector<>();
+
+    // settable by the client
+    private Uri mUri;
+    private Map<String, String> mHeaders;
+
+    // mCurrentState is a VideoView object's current state.
+    // mTargetState is the state that a method caller intends to reach.
+    // For instance, regardless the VideoView object's current state,
+    // calling pause() intends to bring the object to a target state
+    // of STATE_PAUSED.
+    private int mCurrentState = STATE_IDLE;
+    private int mTargetState = STATE_IDLE;
+
+    // All the stuff we need for playing and showing a video
+    private SurfaceHolder mSurfaceHolder = null;
+    private MediaPlayer mMediaPlayer = null;
+    private int mAudioSession;
+    private int mVideoWidth;
+    private int mVideoHeight;
+    private int mSurfaceWidth;
+    private int mSurfaceHeight;
+    private MediaController mMediaController;
+    private OnCompletionListener mOnCompletionListener;
+    private MediaPlayer.OnPreparedListener mOnPreparedListener;
+    private int mCurrentBufferPercentage;
+    private OnErrorListener mOnErrorListener;
+    private OnInfoListener mOnInfoListener;
+    private int mSeekWhenPrepared;  // recording the seek position while preparing
+    private boolean mCanPause;
+    private boolean mCanSeekBack;
+    private boolean mCanSeekForward;
+    private AudioManager mAudioManager;
+    private int mAudioFocusType = AudioManager.AUDIOFOCUS_GAIN; // legacy focus gain
+    private AudioAttributes mAudioAttributes;
+
+    /** Subtitle rendering widget overlaid on top of the video. */
+    private RenderingWidget mSubtitleWidget;
+
+    /** Listener for changes to subtitle data, used to redraw when needed. */
+    private RenderingWidget.OnChangedListener mSubtitlesChangedListener;
+
+    public VideoView(Context context) {
+        this(context, null);
+    }
+
+    public VideoView(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+
+    public VideoView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public VideoView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        mVideoWidth = 0;
+        mVideoHeight = 0;
+
+        mAudioManager = (AudioManager) mContext.getSystemService(Context.AUDIO_SERVICE);
+        mAudioAttributes = new AudioAttributes.Builder().setUsage(AudioAttributes.USAGE_MEDIA)
+                .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE).build();
+
+        getHolder().addCallback(mSHCallback);
+        getHolder().setType(SurfaceHolder.SURFACE_TYPE_PUSH_BUFFERS);
+
+        setFocusable(true);
+        setFocusableInTouchMode(true);
+        requestFocus();
+
+        mCurrentState = STATE_IDLE;
+        mTargetState = STATE_IDLE;
+    }
+
+    @Override
+    protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+        //Log.i("@@@@", "onMeasure(" + MeasureSpec.toString(widthMeasureSpec) + ", "
+        //        + MeasureSpec.toString(heightMeasureSpec) + ")");
+
+        int width = getDefaultSize(mVideoWidth, widthMeasureSpec);
+        int height = getDefaultSize(mVideoHeight, heightMeasureSpec);
+        if (mVideoWidth > 0 && mVideoHeight > 0) {
+
+            int widthSpecMode = MeasureSpec.getMode(widthMeasureSpec);
+            int widthSpecSize = MeasureSpec.getSize(widthMeasureSpec);
+            int heightSpecMode = MeasureSpec.getMode(heightMeasureSpec);
+            int heightSpecSize = MeasureSpec.getSize(heightMeasureSpec);
+
+            if (widthSpecMode == MeasureSpec.EXACTLY && heightSpecMode == MeasureSpec.EXACTLY) {
+                // the size is fixed
+                width = widthSpecSize;
+                height = heightSpecSize;
+
+                // for compatibility, we adjust size based on aspect ratio
+                if ( mVideoWidth * height  < width * mVideoHeight ) {
+                    //Log.i("@@@", "image too wide, correcting");
+                    width = height * mVideoWidth / mVideoHeight;
+                } else if ( mVideoWidth * height  > width * mVideoHeight ) {
+                    //Log.i("@@@", "image too tall, correcting");
+                    height = width * mVideoHeight / mVideoWidth;
+                }
+            } else if (widthSpecMode == MeasureSpec.EXACTLY) {
+                // only the width is fixed, adjust the height to match aspect ratio if possible
+                width = widthSpecSize;
+                height = width * mVideoHeight / mVideoWidth;
+                if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+                    // couldn't match aspect ratio within the constraints
+                    height = heightSpecSize;
+                }
+            } else if (heightSpecMode == MeasureSpec.EXACTLY) {
+                // only the height is fixed, adjust the width to match aspect ratio if possible
+                height = heightSpecSize;
+                width = height * mVideoWidth / mVideoHeight;
+                if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+                    // couldn't match aspect ratio within the constraints
+                    width = widthSpecSize;
+                }
+            } else {
+                // neither the width nor the height are fixed, try to use actual video size
+                width = mVideoWidth;
+                height = mVideoHeight;
+                if (heightSpecMode == MeasureSpec.AT_MOST && height > heightSpecSize) {
+                    // too tall, decrease both width and height
+                    height = heightSpecSize;
+                    width = height * mVideoWidth / mVideoHeight;
+                }
+                if (widthSpecMode == MeasureSpec.AT_MOST && width > widthSpecSize) {
+                    // too wide, decrease both width and height
+                    width = widthSpecSize;
+                    height = width * mVideoHeight / mVideoWidth;
+                }
+            }
+        } else {
+            // no size yet, just adopt the given spec sizes
+        }
+        setMeasuredDimension(width, height);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return VideoView.class.getName();
+    }
+
+    public int resolveAdjustedSize(int desiredSize, int measureSpec) {
+        return getDefaultSize(desiredSize, measureSpec);
+    }
+
+    /**
+     * Sets video path.
+     *
+     * @param path the path of the video.
+     */
+    public void setVideoPath(String path) {
+        setVideoURI(Uri.parse(path));
+    }
+
+    /**
+     * Sets video URI.
+     *
+     * @param uri the URI of the video.
+     */
+    public void setVideoURI(Uri uri) {
+        setVideoURI(uri, null);
+    }
+
+    /**
+     * Sets video URI using specific headers.
+     *
+     * @param uri     the URI of the video.
+     * @param headers the headers for the URI request.
+     *                Note that the cross domain redirection is allowed by default, but that can be
+     *                changed with key/value pairs through the headers parameter with
+     *                "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
+     *                to disallow or allow cross domain redirection.
+     */
+    public void setVideoURI(Uri uri, Map<String, String> headers) {
+        mUri = uri;
+        mHeaders = headers;
+        mSeekWhenPrepared = 0;
+        openVideo();
+        requestLayout();
+        invalidate();
+    }
+
+    /**
+     * Sets which type of audio focus will be requested during the playback, or configures playback
+     * to not request audio focus. Valid values for focus requests are
+     * {@link AudioManager#AUDIOFOCUS_GAIN}, {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT},
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK}, and
+     * {@link AudioManager#AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE}. Or use
+     * {@link AudioManager#AUDIOFOCUS_NONE} to express that audio focus should not be
+     * requested when playback starts. You can for instance use this when playing a silent animation
+     * through this class, and you don't want to affect other audio applications playing in the
+     * background.
+     * @param focusGain the type of audio focus gain that will be requested, or
+     *    {@link AudioManager#AUDIOFOCUS_NONE} to disable the use audio focus during playback.
+     */
+    public void setAudioFocusRequest(int focusGain) {
+        if (focusGain != AudioManager.AUDIOFOCUS_NONE
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_MAY_DUCK
+                && focusGain != AudioManager.AUDIOFOCUS_GAIN_TRANSIENT_EXCLUSIVE) {
+            throw new IllegalArgumentException("Illegal audio focus type " + focusGain);
+        }
+        mAudioFocusType = focusGain;
+    }
+
+    /**
+     * Sets the {@link AudioAttributes} to be used during the playback of the video.
+     * @param attributes non-null <code>AudioAttributes</code>.
+     */
+    public void setAudioAttributes(@NonNull AudioAttributes attributes) {
+        if (attributes == null) {
+            throw new IllegalArgumentException("Illegal null AudioAttributes");
+        }
+        mAudioAttributes = attributes;
+    }
+
+    /**
+     * Adds an external subtitle source file (from the provided input stream.)
+     *
+     * Note that a single external subtitle source may contain multiple or no
+     * supported tracks in it. If the source contained at least one track in
+     * it, one will receive an {@link MediaPlayer#MEDIA_INFO_METADATA_UPDATE}
+     * info message. Otherwise, if reading the source takes excessive time,
+     * one will receive a {@link MediaPlayer#MEDIA_INFO_SUBTITLE_TIMED_OUT}
+     * message. If the source contained no supported track (including an empty
+     * source file or null input stream), one will receive a {@link
+     * MediaPlayer#MEDIA_INFO_UNSUPPORTED_SUBTITLE} message. One can find the
+     * total number of available tracks using {@link MediaPlayer#getTrackInfo()}
+     * to see what additional tracks become available after this method call.
+     *
+     * @param is     input stream containing the subtitle data.  It will be
+     *               closed by the media framework.
+     * @param format the format of the subtitle track(s).  Must contain at least
+     *               the mime type ({@link MediaFormat#KEY_MIME}) and the
+     *               language ({@link MediaFormat#KEY_LANGUAGE}) of the file.
+     *               If the file itself contains the language information,
+     *               specify "und" for the language.
+     */
+    public void addSubtitleSource(InputStream is, MediaFormat format) {
+        if (mMediaPlayer == null) {
+            mPendingSubtitleTracks.add(Pair.create(is, format));
+        } else {
+            try {
+                mMediaPlayer.addSubtitleSource(is, format);
+            } catch (IllegalStateException e) {
+                mInfoListener.onInfo(
+                        mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+            }
+        }
+    }
+
+    public void stopPlayback() {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.stop();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+            mCurrentState = STATE_IDLE;
+            mTargetState  = STATE_IDLE;
+            mAudioManager.abandonAudioFocus(null);
+        }
+    }
+
+    private void openVideo() {
+        if (mUri == null || mSurfaceHolder == null) {
+            // not ready for playback just yet, will try again later
+            return;
+        }
+        // we shouldn't clear the target state, because somebody might have
+        // called start() previously
+        release(false);
+
+        if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+            // TODO this should have a focus listener
+            mAudioManager.requestAudioFocus(null, mAudioAttributes, mAudioFocusType, 0 /*flags*/);
+        }
+
+        try {
+            mMediaPlayer = new MediaPlayer();
+            // TODO: create SubtitleController in MediaPlayer, but we need
+            // a context for the subtitle renderers
+            final Context context = getContext();
+            final SubtitleController controller = new SubtitleController(
+                    context, mMediaPlayer.getMediaTimeProvider(), mMediaPlayer);
+            controller.registerRenderer(new WebVttRenderer(context));
+            controller.registerRenderer(new TtmlRenderer(context));
+            controller.registerRenderer(new Cea708CaptionRenderer(context));
+            controller.registerRenderer(new ClosedCaptionRenderer(context));
+            mMediaPlayer.setSubtitleAnchor(controller, this);
+
+            if (mAudioSession != 0) {
+                mMediaPlayer.setAudioSessionId(mAudioSession);
+            } else {
+                mAudioSession = mMediaPlayer.getAudioSessionId();
+            }
+            mMediaPlayer.setOnPreparedListener(mPreparedListener);
+            mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
+            mMediaPlayer.setOnCompletionListener(mCompletionListener);
+            mMediaPlayer.setOnErrorListener(mErrorListener);
+            mMediaPlayer.setOnInfoListener(mInfoListener);
+            mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
+            mCurrentBufferPercentage = 0;
+            mMediaPlayer.setDataSource(mContext, mUri, mHeaders);
+            mMediaPlayer.setDisplay(mSurfaceHolder);
+            mMediaPlayer.setAudioAttributes(mAudioAttributes);
+            mMediaPlayer.setScreenOnWhilePlaying(true);
+            mMediaPlayer.prepareAsync();
+
+            for (Pair<InputStream, MediaFormat> pending: mPendingSubtitleTracks) {
+                try {
+                    mMediaPlayer.addSubtitleSource(pending.first, pending.second);
+                } catch (IllegalStateException e) {
+                    mInfoListener.onInfo(
+                            mMediaPlayer, MediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE, 0);
+                }
+            }
+
+            // we don't set the target state here either, but preserve the
+            // target state that was there before.
+            mCurrentState = STATE_PREPARING;
+            attachMediaController();
+        } catch (IOException ex) {
+            Log.w(TAG, "Unable to open content: " + mUri, ex);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+            return;
+        } catch (IllegalArgumentException ex) {
+            Log.w(TAG, "Unable to open content: " + mUri, ex);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
+            return;
+        } finally {
+            mPendingSubtitleTracks.clear();
+        }
+    }
+
+    public void setMediaController(MediaController controller) {
+        if (mMediaController != null) {
+            mMediaController.hide();
+        }
+        mMediaController = controller;
+        attachMediaController();
+    }
+
+    private void attachMediaController() {
+        if (mMediaPlayer != null && mMediaController != null) {
+            mMediaController.setMediaPlayer(this);
+            View anchorView = this.getParent() instanceof View ?
+                    (View)this.getParent() : this;
+            mMediaController.setAnchorView(anchorView);
+            mMediaController.setEnabled(isInPlaybackState());
+        }
+    }
+
+    MediaPlayer.OnVideoSizeChangedListener mSizeChangedListener =
+        new MediaPlayer.OnVideoSizeChangedListener() {
+            public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
+                mVideoWidth = mp.getVideoWidth();
+                mVideoHeight = mp.getVideoHeight();
+                if (mVideoWidth != 0 && mVideoHeight != 0) {
+                    getHolder().setFixedSize(mVideoWidth, mVideoHeight);
+                    requestLayout();
+                }
+            }
+    };
+
+    MediaPlayer.OnPreparedListener mPreparedListener = new MediaPlayer.OnPreparedListener() {
+        public void onPrepared(MediaPlayer mp) {
+            mCurrentState = STATE_PREPARED;
+
+            // Get the capabilities of the player for this stream
+            Metadata data = mp.getMetadata(MediaPlayer.METADATA_ALL,
+                                      MediaPlayer.BYPASS_METADATA_FILTER);
+
+            if (data != null) {
+                mCanPause = !data.has(Metadata.PAUSE_AVAILABLE)
+                        || data.getBoolean(Metadata.PAUSE_AVAILABLE);
+                mCanSeekBack = !data.has(Metadata.SEEK_BACKWARD_AVAILABLE)
+                        || data.getBoolean(Metadata.SEEK_BACKWARD_AVAILABLE);
+                mCanSeekForward = !data.has(Metadata.SEEK_FORWARD_AVAILABLE)
+                        || data.getBoolean(Metadata.SEEK_FORWARD_AVAILABLE);
+            } else {
+                mCanPause = mCanSeekBack = mCanSeekForward = true;
+            }
+
+            if (mOnPreparedListener != null) {
+                mOnPreparedListener.onPrepared(mMediaPlayer);
+            }
+            if (mMediaController != null) {
+                mMediaController.setEnabled(true);
+            }
+            mVideoWidth = mp.getVideoWidth();
+            mVideoHeight = mp.getVideoHeight();
+
+            int seekToPosition = mSeekWhenPrepared;  // mSeekWhenPrepared may be changed after seekTo() call
+            if (seekToPosition != 0) {
+                seekTo(seekToPosition);
+            }
+            if (mVideoWidth != 0 && mVideoHeight != 0) {
+                //Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
+                getHolder().setFixedSize(mVideoWidth, mVideoHeight);
+                if (mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
+                    // We didn't actually change the size (it was already at the size
+                    // we need), so we won't get a "surface changed" callback, so
+                    // start the video here instead of in the callback.
+                    if (mTargetState == STATE_PLAYING) {
+                        start();
+                        if (mMediaController != null) {
+                            mMediaController.show();
+                        }
+                    } else if (!isPlaying() &&
+                               (seekToPosition != 0 || getCurrentPosition() > 0)) {
+                       if (mMediaController != null) {
+                           // Show the media controls when we're paused into a video and make 'em stick.
+                           mMediaController.show(0);
+                       }
+                   }
+                }
+            } else {
+                // We don't know the video size yet, but should start anyway.
+                // The video size might be reported to us later.
+                if (mTargetState == STATE_PLAYING) {
+                    start();
+                }
+            }
+        }
+    };
+
+    private MediaPlayer.OnCompletionListener mCompletionListener =
+        new MediaPlayer.OnCompletionListener() {
+        public void onCompletion(MediaPlayer mp) {
+            mCurrentState = STATE_PLAYBACK_COMPLETED;
+            mTargetState = STATE_PLAYBACK_COMPLETED;
+            if (mMediaController != null) {
+                mMediaController.hide();
+            }
+            if (mOnCompletionListener != null) {
+                mOnCompletionListener.onCompletion(mMediaPlayer);
+            }
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+    };
+
+    private MediaPlayer.OnInfoListener mInfoListener =
+        new MediaPlayer.OnInfoListener() {
+        public  boolean onInfo(MediaPlayer mp, int arg1, int arg2) {
+            if (mOnInfoListener != null) {
+                mOnInfoListener.onInfo(mp, arg1, arg2);
+            }
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnErrorListener mErrorListener =
+        new MediaPlayer.OnErrorListener() {
+        public boolean onError(MediaPlayer mp, int framework_err, int impl_err) {
+            Log.d(TAG, "Error: " + framework_err + "," + impl_err);
+            mCurrentState = STATE_ERROR;
+            mTargetState = STATE_ERROR;
+            if (mMediaController != null) {
+                mMediaController.hide();
+            }
+
+            /* If an error handler has been supplied, use it and finish. */
+            if (mOnErrorListener != null) {
+                if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
+                    return true;
+                }
+            }
+
+            /* Otherwise, pop up an error dialog so the user knows that
+             * something bad has happened. Only try and pop up the dialog
+             * if we're attached to a window. When we're going away and no
+             * longer have a window, don't bother showing the user an error.
+             */
+            if (getWindowToken() != null) {
+                Resources r = mContext.getResources();
+                int messageId;
+
+                if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
+                    messageId = com.android.internal.R.string.VideoView_error_text_invalid_progressive_playback;
+                } else {
+                    messageId = com.android.internal.R.string.VideoView_error_text_unknown;
+                }
+
+                new AlertDialog.Builder(mContext)
+                        .setMessage(messageId)
+                        .setPositiveButton(com.android.internal.R.string.VideoView_error_button,
+                                new DialogInterface.OnClickListener() {
+                                    public void onClick(DialogInterface dialog, int whichButton) {
+                                        /* If we get here, there is no onError listener, so
+                                         * at least inform them that the video is over.
+                                         */
+                                        if (mOnCompletionListener != null) {
+                                            mOnCompletionListener.onCompletion(mMediaPlayer);
+                                        }
+                                    }
+                                })
+                        .setCancelable(false)
+                        .show();
+            }
+            return true;
+        }
+    };
+
+    private MediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
+        new MediaPlayer.OnBufferingUpdateListener() {
+        public void onBufferingUpdate(MediaPlayer mp, int percent) {
+            mCurrentBufferPercentage = percent;
+        }
+    };
+
+    /**
+     * Register a callback to be invoked when the media file
+     * is loaded and ready to go.
+     *
+     * @param l The callback that will be run
+     */
+    public void setOnPreparedListener(MediaPlayer.OnPreparedListener l)
+    {
+        mOnPreparedListener = l;
+    }
+
+    /**
+     * Register a callback to be invoked when the end of a media file
+     * has been reached during playback.
+     *
+     * @param l The callback that will be run
+     */
+    public void setOnCompletionListener(OnCompletionListener l)
+    {
+        mOnCompletionListener = l;
+    }
+
+    /**
+     * Register a callback to be invoked when an error occurs
+     * during playback or setup.  If no listener is specified,
+     * or if the listener returned false, VideoView will inform
+     * the user of any errors.
+     *
+     * @param l The callback that will be run
+     */
+    public void setOnErrorListener(OnErrorListener l)
+    {
+        mOnErrorListener = l;
+    }
+
+    /**
+     * Register a callback to be invoked when an informational event
+     * occurs during playback or setup.
+     *
+     * @param l The callback that will be run
+     */
+    public void setOnInfoListener(OnInfoListener l) {
+        mOnInfoListener = l;
+    }
+
+    SurfaceHolder.Callback mSHCallback = new SurfaceHolder.Callback()
+    {
+        public void surfaceChanged(SurfaceHolder holder, int format,
+                                    int w, int h)
+        {
+            mSurfaceWidth = w;
+            mSurfaceHeight = h;
+            boolean isValidState =  (mTargetState == STATE_PLAYING);
+            boolean hasValidSize = (mVideoWidth == w && mVideoHeight == h);
+            if (mMediaPlayer != null && isValidState && hasValidSize) {
+                if (mSeekWhenPrepared != 0) {
+                    seekTo(mSeekWhenPrepared);
+                }
+                start();
+            }
+        }
+
+        public void surfaceCreated(SurfaceHolder holder)
+        {
+            mSurfaceHolder = holder;
+            openVideo();
+        }
+
+        public void surfaceDestroyed(SurfaceHolder holder)
+        {
+            // after we return from this we can't use the surface any more
+            mSurfaceHolder = null;
+            if (mMediaController != null) mMediaController.hide();
+            release(true);
+        }
+    };
+
+    /*
+     * release the media player in any state
+     */
+    private void release(boolean cleartargetstate) {
+        if (mMediaPlayer != null) {
+            mMediaPlayer.reset();
+            mMediaPlayer.release();
+            mMediaPlayer = null;
+            mPendingSubtitleTracks.clear();
+            mCurrentState = STATE_IDLE;
+            if (cleartargetstate) {
+                mTargetState  = STATE_IDLE;
+            }
+            if (mAudioFocusType != AudioManager.AUDIOFOCUS_NONE) {
+                mAudioManager.abandonAudioFocus(null);
+            }
+        }
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN
+                && isInPlaybackState() && mMediaController != null) {
+            toggleMediaControlsVisiblity();
+        }
+        return super.onTouchEvent(ev);
+    }
+
+    @Override
+    public boolean onTrackballEvent(MotionEvent ev) {
+        if (ev.getAction() == MotionEvent.ACTION_DOWN
+                && isInPlaybackState() && mMediaController != null) {
+            toggleMediaControlsVisiblity();
+        }
+        return super.onTrackballEvent(ev);
+    }
+
+    @Override
+    public boolean onKeyDown(int keyCode, KeyEvent event)
+    {
+        boolean isKeyCodeSupported = keyCode != KeyEvent.KEYCODE_BACK &&
+                                     keyCode != KeyEvent.KEYCODE_VOLUME_UP &&
+                                     keyCode != KeyEvent.KEYCODE_VOLUME_DOWN &&
+                                     keyCode != KeyEvent.KEYCODE_VOLUME_MUTE &&
+                                     keyCode != KeyEvent.KEYCODE_MENU &&
+                                     keyCode != KeyEvent.KEYCODE_CALL &&
+                                     keyCode != KeyEvent.KEYCODE_ENDCALL;
+        if (isInPlaybackState() && isKeyCodeSupported && mMediaController != null) {
+            if (keyCode == KeyEvent.KEYCODE_HEADSETHOOK ||
+                    keyCode == KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE) {
+                if (mMediaPlayer.isPlaying()) {
+                    pause();
+                    mMediaController.show();
+                } else {
+                    start();
+                    mMediaController.hide();
+                }
+                return true;
+            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_PLAY) {
+                if (!mMediaPlayer.isPlaying()) {
+                    start();
+                    mMediaController.hide();
+                }
+                return true;
+            } else if (keyCode == KeyEvent.KEYCODE_MEDIA_STOP
+                    || keyCode == KeyEvent.KEYCODE_MEDIA_PAUSE) {
+                if (mMediaPlayer.isPlaying()) {
+                    pause();
+                    mMediaController.show();
+                }
+                return true;
+            } else {
+                toggleMediaControlsVisiblity();
+            }
+        }
+
+        return super.onKeyDown(keyCode, event);
+    }
+
+    private void toggleMediaControlsVisiblity() {
+        if (mMediaController.isShowing()) {
+            mMediaController.hide();
+        } else {
+            mMediaController.show();
+        }
+    }
+
+    @Override
+    public void start() {
+        if (isInPlaybackState()) {
+            mMediaPlayer.start();
+            mCurrentState = STATE_PLAYING;
+        }
+        mTargetState = STATE_PLAYING;
+    }
+
+    @Override
+    public void pause() {
+        if (isInPlaybackState()) {
+            if (mMediaPlayer.isPlaying()) {
+                mMediaPlayer.pause();
+                mCurrentState = STATE_PAUSED;
+            }
+        }
+        mTargetState = STATE_PAUSED;
+    }
+
+    public void suspend() {
+        release(false);
+    }
+
+    public void resume() {
+        openVideo();
+    }
+
+    @Override
+    public int getDuration() {
+        if (isInPlaybackState()) {
+            return mMediaPlayer.getDuration();
+        }
+
+        return -1;
+    }
+
+    @Override
+    public int getCurrentPosition() {
+        if (isInPlaybackState()) {
+            return mMediaPlayer.getCurrentPosition();
+        }
+        return 0;
+    }
+
+    @Override
+    public void seekTo(int msec) {
+        if (isInPlaybackState()) {
+            mMediaPlayer.seekTo(msec);
+            mSeekWhenPrepared = 0;
+        } else {
+            mSeekWhenPrepared = msec;
+        }
+    }
+
+    @Override
+    public boolean isPlaying() {
+        return isInPlaybackState() && mMediaPlayer.isPlaying();
+    }
+
+    @Override
+    public int getBufferPercentage() {
+        if (mMediaPlayer != null) {
+            return mCurrentBufferPercentage;
+        }
+        return 0;
+    }
+
+    private boolean isInPlaybackState() {
+        return (mMediaPlayer != null &&
+                mCurrentState != STATE_ERROR &&
+                mCurrentState != STATE_IDLE &&
+                mCurrentState != STATE_PREPARING);
+    }
+
+    @Override
+    public boolean canPause() {
+        return mCanPause;
+    }
+
+    @Override
+    public boolean canSeekBackward() {
+        return mCanSeekBack;
+    }
+
+    @Override
+    public boolean canSeekForward() {
+        return mCanSeekForward;
+    }
+
+    @Override
+    public int getAudioSessionId() {
+        if (mAudioSession == 0) {
+            MediaPlayer foo = new MediaPlayer();
+            mAudioSession = foo.getAudioSessionId();
+            foo.release();
+        }
+        return mAudioSession;
+    }
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onAttachedToWindow();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+
+        if (mSubtitleWidget != null) {
+            mSubtitleWidget.onDetachedFromWindow();
+        }
+    }
+
+    @Override
+    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
+        super.onLayout(changed, left, top, right, bottom);
+
+        if (mSubtitleWidget != null) {
+            measureAndLayoutSubtitleWidget();
+        }
+    }
+
+    @Override
+    public void draw(Canvas canvas) {
+        super.draw(canvas);
+
+        if (mSubtitleWidget != null) {
+            final int saveCount = canvas.save();
+            canvas.translate(getPaddingLeft(), getPaddingTop());
+            mSubtitleWidget.draw(canvas);
+            canvas.restoreToCount(saveCount);
+        }
+    }
+
+    /**
+     * Forces a measurement and layout pass for all overlaid views.
+     *
+     * @see #setSubtitleWidget(RenderingWidget)
+     */
+    private void measureAndLayoutSubtitleWidget() {
+        final int width = getWidth() - getPaddingLeft() - getPaddingRight();
+        final int height = getHeight() - getPaddingTop() - getPaddingBottom();
+
+        mSubtitleWidget.setSize(width, height);
+    }
+
+    /** @hide */
+    @Override
+    public void setSubtitleWidget(RenderingWidget subtitleWidget) {
+        if (mSubtitleWidget == subtitleWidget) {
+            return;
+        }
+
+        final boolean attachedToWindow = isAttachedToWindow();
+        if (mSubtitleWidget != null) {
+            if (attachedToWindow) {
+                mSubtitleWidget.onDetachedFromWindow();
+            }
+
+            mSubtitleWidget.setOnChangedListener(null);
+        }
+
+        mSubtitleWidget = subtitleWidget;
+
+        if (subtitleWidget != null) {
+            if (mSubtitlesChangedListener == null) {
+                mSubtitlesChangedListener = new RenderingWidget.OnChangedListener() {
+                    @Override
+                    public void onChanged(RenderingWidget renderingWidget) {
+                        invalidate();
+                    }
+                };
+            }
+
+            setWillNotDraw(false);
+            subtitleWidget.setOnChangedListener(mSubtitlesChangedListener);
+
+            if (attachedToWindow) {
+                subtitleWidget.onAttachedToWindow();
+                requestLayout();
+            }
+        } else {
+            setWillNotDraw(true);
+        }
+
+        invalidate();
+    }
+
+    /** @hide */
+    @Override
+    public Looper getSubtitleLooper() {
+        return Looper.getMainLooper();
+    }
+}
diff --git a/android/widget/ViewAnimator.java b/android/widget/ViewAnimator.java
new file mode 100644
index 0000000..1580f51
--- /dev/null
+++ b/android/widget/ViewAnimator.java
@@ -0,0 +1,363 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+
+import android.annotation.AnimRes;
+import android.content.Context;
+import android.content.res.TypedArray;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.animation.Animation;
+import android.view.animation.AnimationUtils;
+
+/**
+ * Base class for a {@link FrameLayout} container that will perform animations
+ * when switching between its views.
+ *
+ * @attr ref android.R.styleable#ViewAnimator_inAnimation
+ * @attr ref android.R.styleable#ViewAnimator_outAnimation
+ * @attr ref android.R.styleable#ViewAnimator_animateFirstView
+ */
+public class ViewAnimator extends FrameLayout {
+
+    int mWhichChild = 0;
+    boolean mFirstTime = true;
+
+    boolean mAnimateFirstTime = true;
+
+    Animation mInAnimation;
+    Animation mOutAnimation;
+
+    public ViewAnimator(Context context) {
+        super(context);
+        initViewAnimator(context, null);
+    }
+
+    public ViewAnimator(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs, com.android.internal.R.styleable.ViewAnimator);
+        int resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_inAnimation, 0);
+        if (resource > 0) {
+            setInAnimation(context, resource);
+        }
+
+        resource = a.getResourceId(com.android.internal.R.styleable.ViewAnimator_outAnimation, 0);
+        if (resource > 0) {
+            setOutAnimation(context, resource);
+        }
+
+        boolean flag = a.getBoolean(com.android.internal.R.styleable.ViewAnimator_animateFirstView, true);
+        setAnimateFirstView(flag);
+
+        a.recycle();
+
+        initViewAnimator(context, attrs);
+    }
+
+    /**
+     * Initialize this {@link ViewAnimator}, possibly setting
+     * {@link #setMeasureAllChildren(boolean)} based on {@link FrameLayout} flags.
+     */
+    private void initViewAnimator(Context context, AttributeSet attrs) {
+        if (attrs == null) {
+            // For compatibility, always measure children when undefined.
+            mMeasureAllChildren = true;
+            return;
+        }
+
+        // For compatibility, default to measure children, but allow XML
+        // attribute to override.
+        final TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.FrameLayout);
+        final boolean measureAllChildren = a.getBoolean(
+                com.android.internal.R.styleable.FrameLayout_measureAllChildren, true);
+        setMeasureAllChildren(measureAllChildren);
+        a.recycle();
+    }
+
+    /**
+     * Sets which child view will be displayed.
+     *
+     * @param whichChild the index of the child view to display
+     */
+    @android.view.RemotableViewMethod
+    public void setDisplayedChild(int whichChild) {
+        mWhichChild = whichChild;
+        if (whichChild >= getChildCount()) {
+            mWhichChild = 0;
+        } else if (whichChild < 0) {
+            mWhichChild = getChildCount() - 1;
+        }
+        boolean hasFocus = getFocusedChild() != null;
+        // This will clear old focus if we had it
+        showOnly(mWhichChild);
+        if (hasFocus) {
+            // Try to retake focus if we had it
+            requestFocus(FOCUS_FORWARD);
+        }
+    }
+
+    /**
+     * Returns the index of the currently displayed child view.
+     */
+    public int getDisplayedChild() {
+        return mWhichChild;
+    }
+
+    /**
+     * Manually shows the next child.
+     */
+    @android.view.RemotableViewMethod
+    public void showNext() {
+        setDisplayedChild(mWhichChild + 1);
+    }
+
+    /**
+     * Manually shows the previous child.
+     */
+    @android.view.RemotableViewMethod
+    public void showPrevious() {
+        setDisplayedChild(mWhichChild - 1);
+    }
+
+    /**
+     * Shows only the specified child. The other displays Views exit the screen,
+     * optionally with the with the {@link #getOutAnimation() out animation} and
+     * the specified child enters the screen, optionally with the
+     * {@link #getInAnimation() in animation}.
+     *
+     * @param childIndex The index of the child to be shown.
+     * @param animate Whether or not to use the in and out animations, defaults
+     *            to true.
+     */
+    void showOnly(int childIndex, boolean animate) {
+        final int count = getChildCount();
+        for (int i = 0; i < count; i++) {
+            final View child = getChildAt(i);
+            if (i == childIndex) {
+                if (animate && mInAnimation != null) {
+                    child.startAnimation(mInAnimation);
+                }
+                child.setVisibility(View.VISIBLE);
+                mFirstTime = false;
+            } else {
+                if (animate && mOutAnimation != null && child.getVisibility() == View.VISIBLE) {
+                    child.startAnimation(mOutAnimation);
+                } else if (child.getAnimation() == mInAnimation)
+                    child.clearAnimation();
+                child.setVisibility(View.GONE);
+            }
+        }
+    }
+    /**
+     * Shows only the specified child. The other displays Views exit the screen
+     * with the {@link #getOutAnimation() out animation} and the specified child
+     * enters the screen with the {@link #getInAnimation() in animation}.
+     *
+     * @param childIndex The index of the child to be shown.
+     */
+    void showOnly(int childIndex) {
+        final boolean animate = (!mFirstTime || mAnimateFirstTime);
+        showOnly(childIndex, animate);
+    }
+
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        super.addView(child, index, params);
+        if (getChildCount() == 1) {
+            child.setVisibility(View.VISIBLE);
+        } else {
+            child.setVisibility(View.GONE);
+        }
+        if (index >= 0 && mWhichChild >= index) {
+            // Added item above current one, increment the index of the displayed child
+            setDisplayedChild(mWhichChild + 1);
+        }
+    }
+
+    @Override
+    public void removeAllViews() {
+        super.removeAllViews();
+        mWhichChild = 0;
+        mFirstTime = true;
+    }
+
+    @Override
+    public void removeView(View view) {
+        final int index = indexOfChild(view);
+        if (index >= 0) {
+            removeViewAt(index);
+        }
+    }
+
+    @Override
+    public void removeViewAt(int index) {
+        super.removeViewAt(index);
+        final int childCount = getChildCount();
+        if (childCount == 0) {
+            mWhichChild = 0;
+            mFirstTime = true;
+        } else if (mWhichChild >= childCount) {
+            // Displayed is above child count, so float down to top of stack
+            setDisplayedChild(childCount - 1);
+        } else if (mWhichChild == index) {
+            // Displayed was removed, so show the new child living in its place
+            setDisplayedChild(mWhichChild);
+        }
+    }
+
+    public void removeViewInLayout(View view) {
+        removeView(view);
+    }
+
+    public void removeViews(int start, int count) {
+        super.removeViews(start, count);
+        if (getChildCount() == 0) {
+            mWhichChild = 0;
+            mFirstTime = true;
+        } else if (mWhichChild >= start && mWhichChild < start + count) {
+            // Try showing new displayed child, wrapping if needed
+            setDisplayedChild(mWhichChild);
+        }
+    }
+
+    public void removeViewsInLayout(int start, int count) {
+        removeViews(start, count);
+    }
+
+    /**
+     * Returns the View corresponding to the currently displayed child.
+     *
+     * @return The View currently displayed.
+     *
+     * @see #getDisplayedChild()
+     */
+    public View getCurrentView() {
+        return getChildAt(mWhichChild);
+    }
+
+    /**
+     * Returns the current animation used to animate a View that enters the screen.
+     *
+     * @return An Animation or null if none is set.
+     *
+     * @see #setInAnimation(android.view.animation.Animation)
+     * @see #setInAnimation(android.content.Context, int)
+     */
+    public Animation getInAnimation() {
+        return mInAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that enters the screen.
+     *
+     * @param inAnimation The animation started when a View enters the screen.
+     *
+     * @see #getInAnimation()
+     * @see #setInAnimation(android.content.Context, int)
+     */
+    public void setInAnimation(Animation inAnimation) {
+        mInAnimation = inAnimation;
+    }
+
+    /**
+     * Returns the current animation used to animate a View that exits the screen.
+     *
+     * @return An Animation or null if none is set.
+     *
+     * @see #setOutAnimation(android.view.animation.Animation)
+     * @see #setOutAnimation(android.content.Context, int)
+     */
+    public Animation getOutAnimation() {
+        return mOutAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that exit the screen.
+     *
+     * @param outAnimation The animation started when a View exit the screen.
+     *
+     * @see #getOutAnimation()
+     * @see #setOutAnimation(android.content.Context, int)
+     */
+    public void setOutAnimation(Animation outAnimation) {
+        mOutAnimation = outAnimation;
+    }
+
+    /**
+     * Specifies the animation used to animate a View that enters the screen.
+     *
+     * @param context The application's environment.
+     * @param resourceID The resource id of the animation.
+     *
+     * @see #getInAnimation()
+     * @see #setInAnimation(android.view.animation.Animation)
+     */
+    public void setInAnimation(Context context, @AnimRes int resourceID) {
+        setInAnimation(AnimationUtils.loadAnimation(context, resourceID));
+    }
+
+    /**
+     * Specifies the animation used to animate a View that exit the screen.
+     *
+     * @param context The application's environment.
+     * @param resourceID The resource id of the animation.
+     *
+     * @see #getOutAnimation()
+     * @see #setOutAnimation(android.view.animation.Animation)
+     */
+    public void setOutAnimation(Context context, @AnimRes int resourceID) {
+        setOutAnimation(AnimationUtils.loadAnimation(context, resourceID));
+    }
+
+    /**
+     * Returns whether the current View should be animated the first time the ViewAnimator
+     * is displayed.
+     *
+     * @return true if the current View will be animated the first time it is displayed,
+     * false otherwise.
+     *
+     * @see #setAnimateFirstView(boolean)
+     */
+    public boolean getAnimateFirstView() {
+        return mAnimateFirstTime;
+    }
+
+    /**
+     * Indicates whether the current View should be animated the first time
+     * the ViewAnimator is displayed.
+     *
+     * @param animate True to animate the current View the first time it is displayed,
+     *                false otherwise.
+     */
+    public void setAnimateFirstView(boolean animate) {
+        mAnimateFirstTime = animate;
+    }
+
+    @Override
+    public int getBaseline() {
+        return (getCurrentView() != null) ? getCurrentView().getBaseline() : super.getBaseline();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ViewAnimator.class.getName();
+    }
+}
diff --git a/android/widget/ViewFlipper.java b/android/widget/ViewFlipper.java
new file mode 100644
index 0000000..e769d71
--- /dev/null
+++ b/android/widget/ViewFlipper.java
@@ -0,0 +1,220 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.content.res.TypedArray;
+import android.os.Message;
+import android.util.AttributeSet;
+import android.util.Log;
+import android.widget.RemoteViews.RemoteView;
+
+/**
+ * Simple {@link ViewAnimator} that will animate between two or more views
+ * that have been added to it.  Only one child is shown at a time.  If
+ * requested, can automatically flip between each child at a regular interval.
+ *
+ * @attr ref android.R.styleable#ViewFlipper_flipInterval
+ * @attr ref android.R.styleable#ViewFlipper_autoStart
+ */
+@RemoteView
+public class ViewFlipper extends ViewAnimator {
+    private static final String TAG = "ViewFlipper";
+    private static final boolean LOGD = false;
+
+    private static final int DEFAULT_INTERVAL = 3000;
+
+    private int mFlipInterval = DEFAULT_INTERVAL;
+    private boolean mAutoStart = false;
+
+    private boolean mRunning = false;
+    private boolean mStarted = false;
+    private boolean mVisible = false;
+    private boolean mUserPresent = true;
+
+    public ViewFlipper(Context context) {
+        super(context);
+    }
+
+    public ViewFlipper(Context context, AttributeSet attrs) {
+        super(context, attrs);
+
+        TypedArray a = context.obtainStyledAttributes(attrs,
+                com.android.internal.R.styleable.ViewFlipper);
+        mFlipInterval = a.getInt(
+                com.android.internal.R.styleable.ViewFlipper_flipInterval, DEFAULT_INTERVAL);
+        mAutoStart = a.getBoolean(
+                com.android.internal.R.styleable.ViewFlipper_autoStart, false);
+        a.recycle();
+    }
+
+    private final BroadcastReceiver mReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            final String action = intent.getAction();
+            if (Intent.ACTION_SCREEN_OFF.equals(action)) {
+                mUserPresent = false;
+                updateRunning();
+            } else if (Intent.ACTION_USER_PRESENT.equals(action)) {
+                mUserPresent = true;
+                updateRunning(false);
+            }
+        }
+    };
+
+    @Override
+    protected void onAttachedToWindow() {
+        super.onAttachedToWindow();
+
+        // Listen for broadcasts related to user-presence
+        final IntentFilter filter = new IntentFilter();
+        filter.addAction(Intent.ACTION_SCREEN_OFF);
+        filter.addAction(Intent.ACTION_USER_PRESENT);
+
+        // OK, this is gross but needed. This class is supported by the
+        // remote views machanism and as a part of that the remote views
+        // can be inflated by a context for another user without the app
+        // having interact users permission - just for loading resources.
+        // For exmaple, when adding widgets from a user profile to the
+        // home screen. Therefore, we register the receiver as the current
+        // user not the one the context is for.
+        getContext().registerReceiverAsUser(mReceiver, android.os.Process.myUserHandle(),
+                filter, null, getHandler());
+
+        if (mAutoStart) {
+            // Automatically start when requested
+            startFlipping();
+        }
+    }
+
+    @Override
+    protected void onDetachedFromWindow() {
+        super.onDetachedFromWindow();
+        mVisible = false;
+
+        getContext().unregisterReceiver(mReceiver);
+        updateRunning();
+    }
+
+    @Override
+    protected void onWindowVisibilityChanged(int visibility) {
+        super.onWindowVisibilityChanged(visibility);
+        mVisible = visibility == VISIBLE;
+        updateRunning(false);
+    }
+
+    /**
+     * How long to wait before flipping to the next view
+     *
+     * @param milliseconds
+     *            time in milliseconds
+     */
+    @android.view.RemotableViewMethod
+    public void setFlipInterval(int milliseconds) {
+        mFlipInterval = milliseconds;
+    }
+
+    /**
+     * Start a timer to cycle through child views
+     */
+    public void startFlipping() {
+        mStarted = true;
+        updateRunning();
+    }
+
+    /**
+     * No more flips
+     */
+    public void stopFlipping() {
+        mStarted = false;
+        updateRunning();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ViewFlipper.class.getName();
+    }
+
+    /**
+     * Internal method to start or stop dispatching flip {@link Message} based
+     * on {@link #mRunning} and {@link #mVisible} state.
+     */
+    private void updateRunning() {
+        updateRunning(true);
+    }
+
+    /**
+     * Internal method to start or stop dispatching flip {@link Message} based
+     * on {@link #mRunning} and {@link #mVisible} state.
+     *
+     * @param flipNow Determines whether or not to execute the animation now, in
+     *            addition to queuing future flips. If omitted, defaults to
+     *            true.
+     */
+    private void updateRunning(boolean flipNow) {
+        boolean running = mVisible && mStarted && mUserPresent;
+        if (running != mRunning) {
+            if (running) {
+                showOnly(mWhichChild, flipNow);
+                postDelayed(mFlipRunnable, mFlipInterval);
+            } else {
+                removeCallbacks(mFlipRunnable);
+            }
+            mRunning = running;
+        }
+        if (LOGD) {
+            Log.d(TAG, "updateRunning() mVisible=" + mVisible + ", mStarted=" + mStarted
+                    + ", mUserPresent=" + mUserPresent + ", mRunning=" + mRunning);
+        }
+    }
+
+    /**
+     * Returns true if the child views are flipping.
+     */
+    public boolean isFlipping() {
+        return mStarted;
+    }
+
+    /**
+     * Set if this view automatically calls {@link #startFlipping()} when it
+     * becomes attached to a window.
+     */
+    public void setAutoStart(boolean autoStart) {
+        mAutoStart = autoStart;
+    }
+
+    /**
+     * Returns true if this view automatically calls {@link #startFlipping()}
+     * when it becomes attached to a window.
+     */
+    public boolean isAutoStart() {
+        return mAutoStart;
+    }
+
+    private final Runnable mFlipRunnable = new Runnable() {
+        @Override
+        public void run() {
+            if (mRunning) {
+                showNext();
+                postDelayed(mFlipRunnable, mFlipInterval);
+            }
+        }
+    };
+}
diff --git a/android/widget/ViewSwitcher.java b/android/widget/ViewSwitcher.java
new file mode 100644
index 0000000..0d5627e
--- /dev/null
+++ b/android/widget/ViewSwitcher.java
@@ -0,0 +1,138 @@
+/*
+ * Copyright (C) 2006 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.View;
+import android.view.ViewGroup;
+
+/**
+ * {@link ViewAnimator} that switches between two views, and has a factory
+ * from which these views are created.  You can either use the factory to
+ * create the views, or add them yourself.  A ViewSwitcher can only have two
+ * child views, of which only one is shown at a time.
+ */
+public class ViewSwitcher extends ViewAnimator {
+    /**
+     * The factory used to create the two children.
+     */
+    ViewFactory mFactory;
+
+    /**
+     * Creates a new empty ViewSwitcher.
+     *
+     * @param context the application's environment
+     */
+    public ViewSwitcher(Context context) {
+        super(context);
+    }
+
+    /**
+     * Creates a new empty ViewSwitcher for the given context and with the
+     * specified set attributes.
+     *
+     * @param context the application environment
+     * @param attrs a collection of attributes
+     */
+    public ViewSwitcher(Context context, AttributeSet attrs) {
+        super(context, attrs);
+    }
+
+    /**
+     * {@inheritDoc}
+     *
+     * @throws IllegalStateException if this switcher already contains two children
+     */
+    @Override
+    public void addView(View child, int index, ViewGroup.LayoutParams params) {
+        if (getChildCount() >= 2) {
+            throw new IllegalStateException("Can't add more than 2 views to a ViewSwitcher");
+        }
+        super.addView(child, index, params);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ViewSwitcher.class.getName();
+    }
+
+    /**
+     * Returns the next view to be displayed.
+     *
+     * @return the view that will be displayed after the next views flip.
+     */
+    public View getNextView() {
+        int which = mWhichChild == 0 ? 1 : 0;
+        return getChildAt(which);
+    }
+
+    private View obtainView() {
+        View child = mFactory.makeView();
+        LayoutParams lp = (LayoutParams) child.getLayoutParams();
+        if (lp == null) {
+            lp = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        }
+        addView(child, lp);
+        return child;
+    }
+
+    /**
+     * Sets the factory used to create the two views between which the
+     * ViewSwitcher will flip. Instead of using a factory, you can call
+     * {@link #addView(android.view.View, int, android.view.ViewGroup.LayoutParams)}
+     * twice.
+     *
+     * @param factory the view factory used to generate the switcher's content
+     */
+    public void setFactory(ViewFactory factory) {
+        mFactory = factory;
+        obtainView();
+        obtainView();
+    }
+
+    /**
+     * Reset the ViewSwitcher to hide all of the existing views and to make it
+     * think that the first time animation has not yet played.
+     */
+    public void reset() {
+        mFirstTime = true;
+        View v;
+        v = getChildAt(0);
+        if (v != null) {
+            v.setVisibility(View.GONE);
+        }
+        v = getChildAt(1);
+        if (v != null) {
+            v.setVisibility(View.GONE);
+        }
+    }
+
+    /**
+     * Creates views in a ViewSwitcher.
+     */
+    public interface ViewFactory {
+        /**
+         * Creates a new {@link android.view.View} to be added in a
+         * {@link android.widget.ViewSwitcher}.
+         *
+         * @return a {@link android.view.View}
+         */
+        View makeView();
+    }
+}
+
diff --git a/android/widget/WrapperListAdapter.java b/android/widget/WrapperListAdapter.java
new file mode 100644
index 0000000..7fe12ae
--- /dev/null
+++ b/android/widget/WrapperListAdapter.java
@@ -0,0 +1,32 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+/**
+ * List adapter that wraps another list adapter. The wrapped adapter can be retrieved
+ * by calling {@link #getWrappedAdapter()}.
+ *
+ * @see ListView
+ */
+public interface WrapperListAdapter extends ListAdapter {
+    /**
+     * Returns the adapter wrapped by this list adapter.
+     *
+     * @return The {@link android.widget.ListAdapter} wrapped by this adapter.
+     */
+    public ListAdapter getWrappedAdapter();
+}
diff --git a/android/widget/YearPickerView.java b/android/widget/YearPickerView.java
new file mode 100644
index 0000000..824fec8
--- /dev/null
+++ b/android/widget/YearPickerView.java
@@ -0,0 +1,258 @@
+/*
+ * Copyright (C) 2014 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 android.widget;
+
+import android.content.Context;
+import android.content.res.Resources;
+import android.icu.util.Calendar;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityEvent;
+
+import com.android.internal.R;
+
+/**
+ * Displays a selectable list of years.
+ */
+class YearPickerView extends ListView {
+    private final YearAdapter mAdapter;
+    private final int mViewSize;
+    private final int mChildSize;
+
+    private OnYearSelectedListener mOnYearSelectedListener;
+
+    public YearPickerView(Context context, AttributeSet attrs) {
+        this(context, attrs, R.attr.listViewStyle);
+    }
+
+    public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public YearPickerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+
+        final LayoutParams frame = new LayoutParams(
+                LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT);
+        setLayoutParams(frame);
+
+        final Resources res = context.getResources();
+        mViewSize = res.getDimensionPixelOffset(R.dimen.datepicker_view_animator_height);
+        mChildSize = res.getDimensionPixelOffset(R.dimen.datepicker_year_label_height);
+
+        setOnItemClickListener(new OnItemClickListener() {
+            @Override
+            public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+                final int year = mAdapter.getYearForPosition(position);
+                mAdapter.setSelection(year);
+
+                if (mOnYearSelectedListener != null) {
+                    mOnYearSelectedListener.onYearChanged(YearPickerView.this, year);
+                }
+            }
+        });
+
+        mAdapter = new YearAdapter(getContext());
+        setAdapter(mAdapter);
+    }
+
+    public void setOnYearSelectedListener(OnYearSelectedListener listener) {
+        mOnYearSelectedListener = listener;
+    }
+
+    /**
+     * Sets the currently selected year. Jumps immediately to the new year.
+     *
+     * @param year the target year
+     */
+    public void setYear(final int year) {
+        mAdapter.setSelection(year);
+
+        post(new Runnable() {
+            @Override
+            public void run() {
+                final int position = mAdapter.getPositionForYear(year);
+                if (position >= 0 && position < getCount()) {
+                    setSelectionCentered(position);
+                }
+            }
+        });
+    }
+
+    public void setSelectionCentered(int position) {
+        final int offset = mViewSize / 2 - mChildSize / 2;
+        setSelectionFromTop(position, offset);
+    }
+
+    public void setRange(Calendar min, Calendar max) {
+        mAdapter.setRange(min, max);
+    }
+
+    private static class YearAdapter extends BaseAdapter {
+        private static final int ITEM_LAYOUT = R.layout.year_label_text_view;
+        private static final int ITEM_TEXT_APPEARANCE =
+                R.style.TextAppearance_Material_DatePicker_List_YearLabel;
+        private static final int ITEM_TEXT_ACTIVATED_APPEARANCE =
+                R.style.TextAppearance_Material_DatePicker_List_YearLabel_Activated;
+
+        private final LayoutInflater mInflater;
+
+        private int mActivatedYear;
+        private int mMinYear;
+        private int mCount;
+
+        public YearAdapter(Context context) {
+            mInflater = LayoutInflater.from(context);
+        }
+
+        public void setRange(Calendar minDate, Calendar maxDate) {
+            final int minYear = minDate.get(Calendar.YEAR);
+            final int count = maxDate.get(Calendar.YEAR) - minYear + 1;
+
+            if (mMinYear != minYear || mCount != count) {
+                mMinYear = minYear;
+                mCount = count;
+                notifyDataSetInvalidated();
+            }
+        }
+
+        public boolean setSelection(int year) {
+            if (mActivatedYear != year) {
+                mActivatedYear = year;
+                notifyDataSetChanged();
+                return true;
+            }
+            return false;
+        }
+
+        @Override
+        public int getCount() {
+            return mCount;
+        }
+
+        @Override
+        public Integer getItem(int position) {
+            return getYearForPosition(position);
+        }
+
+        @Override
+        public long getItemId(int position) {
+            return getYearForPosition(position);
+        }
+
+        public int getPositionForYear(int year) {
+            return year - mMinYear;
+        }
+
+        public int getYearForPosition(int position) {
+            return mMinYear + position;
+        }
+
+        @Override
+        public boolean hasStableIds() {
+            return true;
+        }
+
+        @Override
+        public View getView(int position, View convertView, ViewGroup parent) {
+            final TextView v;
+            final boolean hasNewView = convertView == null;
+            if (hasNewView) {
+                v = (TextView) mInflater.inflate(ITEM_LAYOUT, parent, false);
+            } else {
+                v = (TextView) convertView;
+            }
+
+            final int year = getYearForPosition(position);
+            final boolean activated = mActivatedYear == year;
+
+            if (hasNewView || v.isActivated() != activated) {
+                final int textAppearanceResId;
+                if (activated && ITEM_TEXT_ACTIVATED_APPEARANCE != 0) {
+                    textAppearanceResId = ITEM_TEXT_ACTIVATED_APPEARANCE;
+                } else {
+                    textAppearanceResId = ITEM_TEXT_APPEARANCE;
+                }
+                v.setTextAppearance(textAppearanceResId);
+                v.setActivated(activated);
+            }
+
+            v.setText(Integer.toString(year));
+            return v;
+        }
+
+        @Override
+        public int getItemViewType(int position) {
+            return 0;
+        }
+
+        @Override
+        public int getViewTypeCount() {
+            return 1;
+        }
+
+        @Override
+        public boolean isEmpty() {
+            return false;
+        }
+
+        @Override
+        public boolean areAllItemsEnabled() {
+            return true;
+        }
+
+        @Override
+        public boolean isEnabled(int position) {
+            return true;
+        }
+    }
+
+    public int getFirstPositionOffset() {
+        final View firstChild = getChildAt(0);
+        if (firstChild == null) {
+            return 0;
+        }
+        return firstChild.getTop();
+    }
+
+    /** @hide */
+    @Override
+    public void onInitializeAccessibilityEventInternal(AccessibilityEvent event) {
+        super.onInitializeAccessibilityEventInternal(event);
+
+        // There are a bunch of years, so don't bother.
+        if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_SCROLLED) {
+            event.setFromIndex(0);
+            event.setToIndex(0);
+        }
+    }
+
+    /**
+     * The callback used to indicate the user changed the year.
+     */
+    public interface OnYearSelectedListener {
+        /**
+         * Called upon a year change.
+         *
+         * @param view The view associated with this listener.
+         * @param year The year that was set.
+         */
+        void onYearChanged(YearPickerView view, int year);
+    }
+}
\ No newline at end of file
diff --git a/android/widget/ZoomButton.java b/android/widget/ZoomButton.java
new file mode 100644
index 0000000..5cff335
--- /dev/null
+++ b/android/widget/ZoomButton.java
@@ -0,0 +1,125 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.KeyEvent;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnLongClickListener;
+
+/**
+ * This widget provides a simple utility for turning a continued long-press event
+ * into a series of clicks at some set frequency. There is no actual 'zoom' functionality
+ * handled by this widget directly. Instead, clients of this API should set up an
+ * {@link View#setOnClickListener(OnClickListener) onClickListener} to handle
+ * zoom functionality. That click listener is called on a frequency
+ * determined by {@link #setZoomSpeed(long)} whenever the user long-presses
+ * on the ZoomButton.
+ *
+ * @deprecated Use other means to handle this functionality. This widget is merely a
+ * simple wrapper around a long-press handler.
+ */
+@Deprecated
+public class ZoomButton extends ImageButton implements OnLongClickListener {
+
+    private final Runnable mRunnable = new Runnable() {
+        public void run() {
+            if (hasOnClickListeners() && mIsInLongpress && isEnabled()) {
+                callOnClick();
+                postDelayed(this, mZoomSpeed);
+            }
+        }
+    };
+    
+    private long mZoomSpeed = 1000;
+    private boolean mIsInLongpress;
+    
+    public ZoomButton(Context context) {
+        this(context, null);
+    }
+
+    public ZoomButton(Context context, AttributeSet attrs) {
+        this(context, attrs, 0);
+    }
+    
+    public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr) {
+        this(context, attrs, defStyleAttr, 0);
+    }
+
+    public ZoomButton(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
+        super(context, attrs, defStyleAttr, defStyleRes);
+        setOnLongClickListener(this);
+    }
+
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        if ((event.getAction() == MotionEvent.ACTION_CANCEL)
+                || (event.getAction() == MotionEvent.ACTION_UP)) {
+            mIsInLongpress = false;
+        }
+        return super.onTouchEvent(event);
+    }
+
+    /**
+     * Sets the delay between calls to the widget's {@link View#setOnClickListener(OnClickListener)
+     * onClickListener}.
+     *
+     * @param speed The delay between calls to the click listener, in milliseconds
+     */
+    public void setZoomSpeed(long speed) {
+        mZoomSpeed = speed;
+    }
+
+    @Override
+    public boolean onLongClick(View v) {
+        mIsInLongpress = true;
+        post(mRunnable);
+        return true;
+    }
+        
+    @Override
+    public boolean onKeyUp(int keyCode, KeyEvent event) {
+        mIsInLongpress = false;
+        return super.onKeyUp(keyCode, event);
+    }
+    
+    @Override
+    public void setEnabled(boolean enabled) {
+        if (!enabled) {
+            
+            /* If we're being disabled reset the state back to unpressed
+             * as disabled views don't get events and therefore we won't
+             * get the up event to reset the state.
+             */
+            setPressed(false);
+        }
+        super.setEnabled(enabled);
+    }
+    
+    @Override
+    public boolean dispatchUnhandledMove(View focused, int direction) {
+        clearFocus();
+        return super.dispatchUnhandledMove(focused, direction);
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ZoomButton.class.getName();
+    }
+}
diff --git a/android/widget/ZoomButtonsController.java b/android/widget/ZoomButtonsController.java
new file mode 100644
index 0000000..1a3ca86
--- /dev/null
+++ b/android/widget/ZoomButtonsController.java
@@ -0,0 +1,704 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.os.Handler;
+import android.os.Message;
+import android.util.Log;
+import android.view.Gravity;
+import android.view.KeyEvent;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.View.OnClickListener;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.ViewRootImpl;
+import android.view.WindowManager;
+import android.view.WindowManager.LayoutParams;
+
+/*
+ * Implementation notes:
+ * - The zoom controls are displayed in their own window.
+ *   (Easier for the client and better performance)
+ * - This window is never touchable, and by default is not focusable.
+ *   Its rect is quite big (fills horizontally) but has empty space between the
+ *   edges and center.  Touches there should be given to the owner.  Instead of
+ *   having the window touchable and dispatching these empty touch events to the
+ *   owner, we set the window to not touchable and steal events from owner
+ *   via onTouchListener.
+ * - To make the buttons clickable, it attaches an OnTouchListener to the owner
+ *   view and does the hit detection locally (attaches when visible, detaches when invisible).
+ * - When it is focusable, it forwards uninteresting events to the owner view's
+ *   view hierarchy.
+ */
+/**
+ * The {@link ZoomButtonsController} handles showing and hiding the zoom
+ * controls and positioning it relative to an owner view. It also gives the
+ * client access to the zoom controls container, allowing for additional
+ * accessory buttons to be shown in the zoom controls window.
+ * <p>
+ * Typically, clients should call {@link #setVisible(boolean) setVisible(true)}
+ * on a touch down or move (no need to call {@link #setVisible(boolean)
+ * setVisible(false)} since it will time out on its own). Also, whenever the
+ * owner cannot be zoomed further, the client should update
+ * {@link #setZoomInEnabled(boolean)} and {@link #setZoomOutEnabled(boolean)}.
+ * <p>
+ * If you are using this with a custom View, please call
+ * {@link #setVisible(boolean) setVisible(false)} from
+ * {@link View#onDetachedFromWindow} and from {@link View#onVisibilityChanged}
+ * when <code>visibility != View.VISIBLE</code>.
+ *
+ * @deprecated This functionality and UI is better handled with custom views and layouts
+ * rather than a dedicated zoom-control widget
+ */
+@Deprecated
+public class ZoomButtonsController implements View.OnTouchListener {
+
+    private static final String TAG = "ZoomButtonsController";
+
+    private static final int ZOOM_CONTROLS_TIMEOUT =
+            (int) ViewConfiguration.getZoomControlsTimeout();
+
+    private static final int ZOOM_CONTROLS_TOUCH_PADDING = 20;
+    private int mTouchPaddingScaledSq;
+
+    private final Context mContext;
+    private final WindowManager mWindowManager;
+    private boolean mAutoDismissControls = true;
+
+    /**
+     * The view that is being zoomed by this zoom controller.
+     */
+    private final View mOwnerView;
+
+    /**
+     * The location of the owner view on the screen. This is recalculated
+     * each time the zoom controller is shown.
+     */
+    private final int[] mOwnerViewRawLocation = new int[2];
+
+    /**
+     * The container that is added as a window.
+     */
+    private final FrameLayout mContainer;
+    private LayoutParams mContainerLayoutParams;
+    private final int[] mContainerRawLocation = new int[2];
+
+    private ZoomControls mControls;
+
+    /**
+     * The view (or null) that should receive touch events. This will get set if
+     * the touch down hits the container. It will be reset on the touch up.
+     */
+    private View mTouchTargetView;
+    /**
+     * The {@link #mTouchTargetView}'s location in window, set on touch down.
+     */
+    private final int[] mTouchTargetWindowLocation = new int[2];
+
+    /**
+     * If the zoom controller is dismissed but the user is still in a touch
+     * interaction, we set this to true. This will ignore all touch events until
+     * up/cancel, and then set the owner's touch listener to null.
+     * <p>
+     * Otherwise, the owner view would get mismatched events (i.e., touch move
+     * even though it never got the touch down.)
+     */
+    private boolean mReleaseTouchListenerOnUp;
+
+    /** Whether the container has been added to the window manager. */
+    private boolean mIsVisible;
+
+    private final Rect mTempRect = new Rect();
+    private final int[] mTempIntArray = new int[2];
+
+    private OnZoomListener mCallback;
+
+    /**
+     * When showing the zoom, we add the view as a new window. However, there is
+     * logic that needs to know the size of the zoom which is determined after
+     * it's laid out. Therefore, we must post this logic onto the UI thread so
+     * it will be exceuted AFTER the layout. This is the logic.
+     */
+    private Runnable mPostedVisibleInitializer;
+
+    private final IntentFilter mConfigurationChangedFilter =
+            new IntentFilter(Intent.ACTION_CONFIGURATION_CHANGED);
+
+    /**
+     * Needed to reposition the zoom controls after configuration changes.
+     */
+    private final BroadcastReceiver mConfigurationChangedReceiver = new BroadcastReceiver() {
+        @Override
+        public void onReceive(Context context, Intent intent) {
+            if (!mIsVisible) return;
+
+            mHandler.removeMessages(MSG_POST_CONFIGURATION_CHANGED);
+            mHandler.sendEmptyMessage(MSG_POST_CONFIGURATION_CHANGED);
+        }
+    };
+
+    /** When configuration changes, this is called after the UI thread is idle. */
+    private static final int MSG_POST_CONFIGURATION_CHANGED = 2;
+    /** Used to delay the zoom controller dismissal. */
+    private static final int MSG_DISMISS_ZOOM_CONTROLS = 3;
+    /**
+     * If setVisible(true) is called and the owner view's window token is null,
+     * we delay the setVisible(true) call until it is not null.
+     */
+    private static final int MSG_POST_SET_VISIBLE = 4;
+
+    private final Handler mHandler = new Handler() {
+        @Override
+        public void handleMessage(Message msg) {
+            switch (msg.what) {
+                case MSG_POST_CONFIGURATION_CHANGED:
+                    onPostConfigurationChanged();
+                    break;
+
+                case MSG_DISMISS_ZOOM_CONTROLS:
+                    setVisible(false);
+                    break;
+
+                case MSG_POST_SET_VISIBLE:
+                    if (mOwnerView.getWindowToken() == null) {
+                        // Doh, it is still null, just ignore the set visible call
+                        Log.e(TAG,
+                                "Cannot make the zoom controller visible if the owner view is " +
+                                "not attached to a window.");
+                    } else {
+                        setVisible(true);
+                    }
+                    break;
+            }
+
+        }
+    };
+
+    /**
+     * Constructor for the {@link ZoomButtonsController}.
+     *
+     * @param ownerView The view that is being zoomed by the zoom controls. The
+     *            zoom controls will be displayed aligned with this view.
+     */
+    public ZoomButtonsController(View ownerView) {
+        mContext = ownerView.getContext();
+        mWindowManager = (WindowManager) mContext.getSystemService(Context.WINDOW_SERVICE);
+        mOwnerView = ownerView;
+
+        mTouchPaddingScaledSq = (int)
+                (ZOOM_CONTROLS_TOUCH_PADDING * mContext.getResources().getDisplayMetrics().density);
+        mTouchPaddingScaledSq *= mTouchPaddingScaledSq;
+
+        mContainer = createContainer();
+    }
+
+    /**
+     * Whether to enable the zoom in control.
+     *
+     * @param enabled Whether to enable the zoom in control.
+     */
+    public void setZoomInEnabled(boolean enabled) {
+        mControls.setIsZoomInEnabled(enabled);
+    }
+
+    /**
+     * Whether to enable the zoom out control.
+     *
+     * @param enabled Whether to enable the zoom out control.
+     */
+    public void setZoomOutEnabled(boolean enabled) {
+        mControls.setIsZoomOutEnabled(enabled);
+    }
+
+    /**
+     * Sets the delay between zoom callbacks as the user holds a zoom button.
+     *
+     * @param speed The delay in milliseconds between zoom callbacks.
+     */
+    public void setZoomSpeed(long speed) {
+        mControls.setZoomSpeed(speed);
+    }
+
+    private FrameLayout createContainer() {
+        LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
+        // Controls are positioned BOTTOM | CENTER with respect to the owner view.
+        lp.gravity = Gravity.TOP | Gravity.START;
+        lp.flags = LayoutParams.FLAG_NOT_TOUCHABLE |
+                LayoutParams.FLAG_NOT_FOCUSABLE |
+                LayoutParams.FLAG_LAYOUT_NO_LIMITS |
+                LayoutParams.FLAG_ALT_FOCUSABLE_IM;
+        lp.height = LayoutParams.WRAP_CONTENT;
+        lp.width = LayoutParams.MATCH_PARENT;
+        lp.type = LayoutParams.TYPE_APPLICATION_PANEL;
+        lp.format = PixelFormat.TRANSLUCENT;
+        lp.windowAnimations = com.android.internal.R.style.Animation_ZoomButtons;
+        mContainerLayoutParams = lp;
+
+        FrameLayout container = new Container(mContext);
+        container.setLayoutParams(lp);
+        container.setMeasureAllChildren(true);
+
+        LayoutInflater inflater = (LayoutInflater) mContext
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(com.android.internal.R.layout.zoom_container, container);
+
+        mControls = container.findViewById(com.android.internal.R.id.zoomControls);
+        mControls.setOnZoomInClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+                if (mCallback != null) mCallback.onZoom(true);
+            }
+        });
+        mControls.setOnZoomOutClickListener(new OnClickListener() {
+            public void onClick(View v) {
+                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+                if (mCallback != null) mCallback.onZoom(false);
+            }
+        });
+
+        return container;
+    }
+
+    /**
+     * Sets the {@link OnZoomListener} listener that receives callbacks to zoom.
+     *
+     * @param listener The listener that will be told to zoom.
+     */
+    public void setOnZoomListener(OnZoomListener listener) {
+        mCallback = listener;
+    }
+
+    /**
+     * Sets whether the zoom controls should be focusable. If the controls are
+     * focusable, then trackball and arrow key interactions are possible.
+     * Otherwise, only touch interactions are possible.
+     *
+     * @param focusable Whether the zoom controls should be focusable.
+     */
+    public void setFocusable(boolean focusable) {
+        int oldFlags = mContainerLayoutParams.flags;
+        if (focusable) {
+            mContainerLayoutParams.flags &= ~LayoutParams.FLAG_NOT_FOCUSABLE;
+        } else {
+            mContainerLayoutParams.flags |= LayoutParams.FLAG_NOT_FOCUSABLE;
+        }
+
+        if ((mContainerLayoutParams.flags != oldFlags) && mIsVisible) {
+            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
+        }
+    }
+
+    /**
+     * Whether the zoom controls will be automatically dismissed after showing.
+     *
+     * @return Whether the zoom controls will be auto dismissed after showing.
+     */
+    public boolean isAutoDismissed() {
+        return mAutoDismissControls;
+    }
+
+    /**
+     * Sets whether the zoom controls will be automatically dismissed after
+     * showing.
+     */
+    public void setAutoDismissed(boolean autoDismiss) {
+        if (mAutoDismissControls == autoDismiss) return;
+        mAutoDismissControls = autoDismiss;
+    }
+
+    /**
+     * Whether the zoom controls are visible to the user.
+     *
+     * @return Whether the zoom controls are visible to the user.
+     */
+    public boolean isVisible() {
+        return mIsVisible;
+    }
+
+    /**
+     * Sets whether the zoom controls should be visible to the user.
+     *
+     * @param visible Whether the zoom controls should be visible to the user.
+     */
+    public void setVisible(boolean visible) {
+
+        if (visible) {
+            if (mOwnerView.getWindowToken() == null) {
+                /*
+                 * We need a window token to show ourselves, maybe the owner's
+                 * window hasn't been created yet but it will have been by the
+                 * time the looper is idle, so post the setVisible(true) call.
+                 */
+                if (!mHandler.hasMessages(MSG_POST_SET_VISIBLE)) {
+                    mHandler.sendEmptyMessage(MSG_POST_SET_VISIBLE);
+                }
+                return;
+            }
+
+            dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+        }
+
+        if (mIsVisible == visible) {
+            return;
+        }
+        mIsVisible = visible;
+
+        if (visible) {
+            if (mContainerLayoutParams.token == null) {
+                mContainerLayoutParams.token = mOwnerView.getWindowToken();
+            }
+
+            mWindowManager.addView(mContainer, mContainerLayoutParams);
+
+            if (mPostedVisibleInitializer == null) {
+                mPostedVisibleInitializer = new Runnable() {
+                    public void run() {
+                        refreshPositioningVariables();
+
+                        if (mCallback != null) {
+                            mCallback.onVisibilityChanged(true);
+                        }
+                    }
+                };
+            }
+
+            mHandler.post(mPostedVisibleInitializer);
+
+            // Handle configuration changes when visible
+            mContext.registerReceiver(mConfigurationChangedReceiver, mConfigurationChangedFilter);
+
+            // Steal touches events from the owner
+            mOwnerView.setOnTouchListener(this);
+            mReleaseTouchListenerOnUp = false;
+
+        } else {
+            // Don't want to steal any more touches
+            if (mTouchTargetView != null) {
+                // We are still stealing the touch events for this touch
+                // sequence, so release the touch listener later
+                mReleaseTouchListenerOnUp = true;
+            } else {
+                mOwnerView.setOnTouchListener(null);
+            }
+
+            // No longer care about configuration changes
+            mContext.unregisterReceiver(mConfigurationChangedReceiver);
+
+            mWindowManager.removeViewImmediate(mContainer);
+            mHandler.removeCallbacks(mPostedVisibleInitializer);
+
+            if (mCallback != null) {
+                mCallback.onVisibilityChanged(false);
+            }
+        }
+
+    }
+
+    /**
+     * Gets the container that is the parent of the zoom controls.
+     * <p>
+     * The client can add other views to this container to link them with the
+     * zoom controls.
+     *
+     * @return The container of the zoom controls. It will be a layout that
+     *         respects the gravity of a child's layout parameters.
+     */
+    public ViewGroup getContainer() {
+        return mContainer;
+    }
+
+    /**
+     * Gets the view for the zoom controls.
+     *
+     * @return The zoom controls view.
+     */
+    public View getZoomControls() {
+        return mControls;
+    }
+
+    private void dismissControlsDelayed(int delay) {
+        if (mAutoDismissControls) {
+            mHandler.removeMessages(MSG_DISMISS_ZOOM_CONTROLS);
+            mHandler.sendEmptyMessageDelayed(MSG_DISMISS_ZOOM_CONTROLS, delay);
+        }
+    }
+
+    private void refreshPositioningVariables() {
+        // if the mOwnerView is detached from window then skip.
+        if (mOwnerView.getWindowToken() == null) return;
+
+        // Position the zoom controls on the bottom of the owner view.
+        int ownerHeight = mOwnerView.getHeight();
+        int ownerWidth = mOwnerView.getWidth();
+        // The gap between the top of the owner and the top of the container
+        int containerOwnerYOffset = ownerHeight - mContainer.getHeight();
+
+        // Calculate the owner view's bounds
+        mOwnerView.getLocationOnScreen(mOwnerViewRawLocation);
+        mContainerRawLocation[0] = mOwnerViewRawLocation[0];
+        mContainerRawLocation[1] = mOwnerViewRawLocation[1] + containerOwnerYOffset;
+
+        int[] ownerViewWindowLoc = mTempIntArray;
+        mOwnerView.getLocationInWindow(ownerViewWindowLoc);
+
+        // lp.x and lp.y should be relative to the owner's window top-left
+        mContainerLayoutParams.x = ownerViewWindowLoc[0];
+        mContainerLayoutParams.width = ownerWidth;
+        mContainerLayoutParams.y = ownerViewWindowLoc[1] + containerOwnerYOffset;
+        if (mIsVisible) {
+            mWindowManager.updateViewLayout(mContainer, mContainerLayoutParams);
+        }
+
+    }
+
+    /* This will only be called when the container has focus. */
+    private boolean onContainerKey(KeyEvent event) {
+        int keyCode = event.getKeyCode();
+        if (isInterestingKey(keyCode)) {
+
+            if (keyCode == KeyEvent.KEYCODE_BACK) {
+                if (event.getAction() == KeyEvent.ACTION_DOWN
+                        && event.getRepeatCount() == 0) {
+                    if (mOwnerView != null) {
+                        KeyEvent.DispatcherState ds = mOwnerView.getKeyDispatcherState();
+                        if (ds != null) {
+                            ds.startTracking(event, this);
+                        }
+                    }
+                    return true;
+                } else if (event.getAction() == KeyEvent.ACTION_UP
+                        && event.isTracking() && !event.isCanceled()) {
+                    setVisible(false);
+                    return true;
+                }
+
+            } else {
+                dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+            }
+
+            // Let the container handle the key
+            return false;
+
+        } else {
+
+            ViewRootImpl viewRoot = mOwnerView.getViewRootImpl();
+            if (viewRoot != null) {
+                viewRoot.dispatchInputEvent(event);
+            }
+
+            // We gave the key to the owner, don't let the container handle this key
+            return true;
+        }
+    }
+
+    private boolean isInterestingKey(int keyCode) {
+        switch (keyCode) {
+            case KeyEvent.KEYCODE_DPAD_CENTER:
+            case KeyEvent.KEYCODE_DPAD_UP:
+            case KeyEvent.KEYCODE_DPAD_DOWN:
+            case KeyEvent.KEYCODE_DPAD_LEFT:
+            case KeyEvent.KEYCODE_DPAD_RIGHT:
+            case KeyEvent.KEYCODE_ENTER:
+            case KeyEvent.KEYCODE_BACK:
+                return true;
+            default:
+                return false;
+        }
+    }
+
+    /**
+     * @hide The ZoomButtonsController implements the OnTouchListener, but this
+     *       does not need to be shown in its public API.
+     */
+    public boolean onTouch(View v, MotionEvent event) {
+        int action = event.getAction();
+
+        if (event.getPointerCount() > 1) {
+            // ZoomButtonsController doesn't handle mutitouch. Give up control.
+            return false;
+        }
+
+        if (mReleaseTouchListenerOnUp) {
+            // The controls were dismissed but we need to throw away all events until the up
+            if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) {
+                mOwnerView.setOnTouchListener(null);
+                setTouchTargetView(null);
+                mReleaseTouchListenerOnUp = false;
+            }
+
+            // Eat this event
+            return true;
+        }
+
+        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+
+        View targetView = mTouchTargetView;
+
+        switch (action) {
+            case MotionEvent.ACTION_DOWN:
+                targetView = findViewForTouch((int) event.getRawX(), (int) event.getRawY());
+                setTouchTargetView(targetView);
+                break;
+
+            case MotionEvent.ACTION_UP:
+            case MotionEvent.ACTION_CANCEL:
+                setTouchTargetView(null);
+                break;
+        }
+
+        if (targetView != null) {
+            // The upperleft corner of the target view in raw coordinates
+            int targetViewRawX = mContainerRawLocation[0] + mTouchTargetWindowLocation[0];
+            int targetViewRawY = mContainerRawLocation[1] + mTouchTargetWindowLocation[1];
+
+            MotionEvent containerEvent = MotionEvent.obtain(event);
+            // Convert the motion event into the target view's coordinates (from
+            // owner view's coordinates)
+            containerEvent.offsetLocation(mOwnerViewRawLocation[0] - targetViewRawX,
+                    mOwnerViewRawLocation[1] - targetViewRawY);
+            /* Disallow negative coordinates (which can occur due to
+             * ZOOM_CONTROLS_TOUCH_PADDING) */
+            // These are floats because we need to potentially offset away this exact amount
+            float containerX = containerEvent.getX();
+            float containerY = containerEvent.getY();
+            if (containerX < 0 && containerX > -ZOOM_CONTROLS_TOUCH_PADDING) {
+                containerEvent.offsetLocation(-containerX, 0);
+            }
+            if (containerY < 0 && containerY > -ZOOM_CONTROLS_TOUCH_PADDING) {
+                containerEvent.offsetLocation(0, -containerY);
+            }
+            boolean retValue = targetView.dispatchTouchEvent(containerEvent);
+            containerEvent.recycle();
+            return retValue;
+
+        } else {
+            return false;
+        }
+    }
+
+    private void setTouchTargetView(View view) {
+        mTouchTargetView = view;
+        if (view != null) {
+            view.getLocationInWindow(mTouchTargetWindowLocation);
+        }
+    }
+
+    /**
+     * Returns the View that should receive a touch at the given coordinates.
+     *
+     * @param rawX The raw X.
+     * @param rawY The raw Y.
+     * @return The view that should receive the touches, or null if there is not one.
+     */
+    private View findViewForTouch(int rawX, int rawY) {
+        // Reverse order so the child drawn on top gets first dibs.
+        int containerCoordsX = rawX - mContainerRawLocation[0];
+        int containerCoordsY = rawY - mContainerRawLocation[1];
+        Rect frame = mTempRect;
+
+        View closestChild = null;
+        int closestChildDistanceSq = Integer.MAX_VALUE;
+
+        for (int i = mContainer.getChildCount() - 1; i >= 0; i--) {
+            View child = mContainer.getChildAt(i);
+            if (child.getVisibility() != View.VISIBLE) {
+                continue;
+            }
+
+            child.getHitRect(frame);
+            if (frame.contains(containerCoordsX, containerCoordsY)) {
+                return child;
+            }
+
+            int distanceX;
+            if (containerCoordsX >= frame.left && containerCoordsX <= frame.right) {
+                distanceX = 0;
+            } else {
+                distanceX = Math.min(Math.abs(frame.left - containerCoordsX),
+                    Math.abs(containerCoordsX - frame.right));
+            }
+            int distanceY;
+            if (containerCoordsY >= frame.top && containerCoordsY <= frame.bottom) {
+                distanceY = 0;
+            } else {
+                distanceY = Math.min(Math.abs(frame.top - containerCoordsY),
+                        Math.abs(containerCoordsY - frame.bottom));
+            }
+            int distanceSq = distanceX * distanceX + distanceY * distanceY;
+
+            if ((distanceSq < mTouchPaddingScaledSq) &&
+                    (distanceSq < closestChildDistanceSq)) {
+                closestChild = child;
+                closestChildDistanceSq = distanceSq;
+            }
+        }
+
+        return closestChild;
+    }
+
+    private void onPostConfigurationChanged() {
+        dismissControlsDelayed(ZOOM_CONTROLS_TIMEOUT);
+        refreshPositioningVariables();
+    }
+
+    /**
+     * Interface that will be called when the user performs an interaction that
+     * triggers some action, for example zooming.
+     */
+    public interface OnZoomListener {
+
+        /**
+         * Called when the zoom controls' visibility changes.
+         *
+         * @param visible Whether the zoom controls are visible.
+         */
+        void onVisibilityChanged(boolean visible);
+
+        /**
+         * Called when the owner view needs to be zoomed.
+         *
+         * @param zoomIn The direction of the zoom: true to zoom in, false to zoom out.
+         */
+        void onZoom(boolean zoomIn);
+    }
+
+    private class Container extends FrameLayout {
+        public Container(Context context) {
+            super(context);
+        }
+
+        /*
+         * Need to override this to intercept the key events. Otherwise, we
+         * would attach a key listener to the container but its superclass
+         * ViewGroup gives it to the focused View instead of calling the key
+         * listener, and so we wouldn't get the events.
+         */
+        @Override
+        public boolean dispatchKeyEvent(KeyEvent event) {
+            return onContainerKey(event) ? true : super.dispatchKeyEvent(event);
+        }
+    }
+
+}
diff --git a/android/widget/ZoomControls.java b/android/widget/ZoomControls.java
new file mode 100644
index 0000000..66c052b
--- /dev/null
+++ b/android/widget/ZoomControls.java
@@ -0,0 +1,114 @@
+/*
+ * Copyright (C) 2008 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 android.widget;
+
+import android.annotation.Widget;
+import android.content.Context;
+import android.util.AttributeSet;
+import android.view.LayoutInflater;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.animation.AlphaAnimation;
+
+import com.android.internal.R;
+
+
+/**
+ * The {@code ZoomControls} class displays a simple set of controls used for zooming and
+ * provides callbacks to register for events. */
+@Widget
+public class ZoomControls extends LinearLayout {
+
+    private final ZoomButton mZoomIn;
+    private final ZoomButton mZoomOut;
+        
+    public ZoomControls(Context context) {
+        this(context, null);
+    }
+
+    public ZoomControls(Context context, AttributeSet attrs) {
+        super(context, attrs);
+        setFocusable(false);
+        
+        LayoutInflater inflater = (LayoutInflater) context
+                .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        inflater.inflate(R.layout.zoom_controls, this, // we are the parent
+                true);
+        
+        mZoomIn = (ZoomButton) findViewById(R.id.zoomIn);
+        mZoomOut = (ZoomButton) findViewById(R.id.zoomOut);
+    }
+
+    public void setOnZoomInClickListener(OnClickListener listener) {
+        mZoomIn.setOnClickListener(listener);
+    }
+    
+    public void setOnZoomOutClickListener(OnClickListener listener) {
+        mZoomOut.setOnClickListener(listener);
+    }
+    
+    /*
+     * Sets how fast you get zoom events when the user holds down the
+     * zoom in/out buttons.
+     */
+    public void setZoomSpeed(long speed) {
+        mZoomIn.setZoomSpeed(speed);
+        mZoomOut.setZoomSpeed(speed);
+    }
+    
+    @Override
+    public boolean onTouchEvent(MotionEvent event) {
+        
+        /* Consume all touch events so they don't get dispatched to the view
+         * beneath this view.
+         */
+        return true;
+    }
+    
+    public void show() {
+        fade(View.VISIBLE, 0.0f, 1.0f);
+    }
+    
+    public void hide() {
+        fade(View.GONE, 1.0f, 0.0f);
+    }
+    
+    private void fade(int visibility, float startAlpha, float endAlpha) {
+        AlphaAnimation anim = new AlphaAnimation(startAlpha, endAlpha);
+        anim.setDuration(500);
+        startAnimation(anim);
+        setVisibility(visibility);
+    }
+    
+    public void setIsZoomInEnabled(boolean isEnabled) {
+        mZoomIn.setEnabled(isEnabled);
+    }
+    
+    public void setIsZoomOutEnabled(boolean isEnabled) {
+        mZoomOut.setEnabled(isEnabled);
+    }
+    
+    @Override
+    public boolean hasFocus() {
+        return mZoomIn.hasFocus() || mZoomOut.hasFocus();
+    }
+
+    @Override
+    public CharSequence getAccessibilityClassName() {
+        return ZoomControls.class.getName();
+    }
+}