Merge "java-side enable/disable switch for link prefetch via WebSettings"
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java
index b708750..d0c87c6 100644
--- a/core/java/android/text/SpannableStringBuilder.java
+++ b/core/java/android/text/SpannableStringBuilder.java
@@ -338,7 +338,7 @@
                     en = tbend;
 
                 if (getSpanStart(spans[i]) < 0) {
-                    setSpan(false, spans[i],
+                    setSpan(true, spans[i],
                             st - tbstart + start,
                             en - tbstart + start,
                             sp.getSpanFlags(spans[i]));
@@ -579,8 +579,7 @@
                 mSpanEnds[i] = end;
                 mSpanFlags[i] = flags;
 
-                if (send) 
-                    sendSpanChanged(what, ostart, oend, nstart, nend);
+                if (send) sendSpanChanged(what, ostart, oend, nstart, nend);
 
                 return;
             }
@@ -610,8 +609,7 @@
         mSpanFlags[mSpanCount] = flags;
         mSpanCount++;
 
-        if (send)
-            sendSpanAdded(what, nstart, nend);
+        if (send) sendSpanAdded(what, nstart, nend);
     }
 
     /**
diff --git a/core/java/android/widget/Editor.java b/core/java/android/widget/Editor.java
new file mode 100644
index 0000000..880dc34
--- /dev/null
+++ b/core/java/android/widget/Editor.java
@@ -0,0 +1,3750 @@
+/*
+ * 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.content.ClipData;
+import android.content.ClipData.Item;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+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.RectF;
+import android.graphics.drawable.Drawable;
+import android.inputmethodservice.ExtractEditText;
+import android.os.Bundle;
+import android.os.Handler;
+import android.os.SystemClock;
+import android.provider.Settings;
+import android.text.DynamicLayout;
+import android.text.Editable;
+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.TextWatcher;
+import android.text.method.KeyListener;
+import android.text.method.MetaKeyKeyListener;
+import android.text.method.MovementMethod;
+import android.text.method.PasswordTransformationMethod;
+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.DisplayMetrics;
+import android.util.Log;
+import android.view.ActionMode;
+import android.view.ActionMode.Callback;
+import android.view.DisplayList;
+import android.view.DragEvent;
+import android.view.Gravity;
+import android.view.HardwareCanvas;
+import android.view.LayoutInflater;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.MotionEvent;
+import android.view.View;
+import android.view.ViewConfiguration;
+import android.view.ViewGroup;
+import android.view.View.DragShadowBuilder;
+import android.view.View.OnClickListener;
+import android.view.ViewGroup.LayoutParams;
+import android.view.ViewParent;
+import android.view.ViewTreeObserver;
+import android.view.WindowManager;
+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.InputMethodManager;
+import android.widget.AdapterView.OnItemClickListener;
+import android.widget.TextView.Drawables;
+import android.widget.TextView.OnEditorActionListener;
+
+import com.android.internal.util.ArrayUtils;
+import com.android.internal.widget.EditableInputConnection;
+
+import java.text.BreakIterator;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.HashMap;
+
+/**
+ * Helper class used by TextView to handle editable text views.
+ *
+ * @hide
+ */
+public class Editor {
+    static final int BLINK = 500;
+    private static final float[] TEMP_POSITION = new float[2];
+    private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
+
+    // Cursor Controllers.
+    InsertionPointCursorController mInsertionPointCursorController;
+    SelectionModifierCursorController mSelectionModifierCursorController;
+    ActionMode mSelectionActionMode;
+    boolean mInsertionControllerEnabled;
+    boolean mSelectionControllerEnabled;
+
+    // Used to highlight a word when it is corrected by the IME
+    CorrectionHighlighter mCorrectionHighlighter;
+
+    InputContentType mInputContentType;
+    InputMethodState mInputMethodState;
+
+    DisplayList[] mTextDisplayLists;
+
+    boolean mFrozenWithFocus;
+    boolean mSelectionMoved;
+    boolean mTouchFocusSelected;
+
+    KeyListener mKeyListener;
+    int mInputType = EditorInfo.TYPE_NULL;
+
+    boolean mDiscardNextActionUp;
+    boolean mIgnoreActionUpEvent;
+
+    long mShowCursor;
+    Blink mBlink;
+
+    boolean mCursorVisible = true;
+    boolean mSelectAllOnFocus;
+    boolean mTextIsSelectable;
+
+    CharSequence mError;
+    boolean mErrorWasChanged;
+    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.
+     */
+    boolean mShowErrorAfterAttach;
+
+    boolean mInBatchEditControllers;
+
+    SuggestionsPopupWindow mSuggestionsPopupWindow;
+    SuggestionRangeSpan mSuggestionRangeSpan;
+    Runnable mShowSuggestionRunnable;
+
+    final Drawable[] mCursorDrawable = new Drawable[2];
+    int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
+
+    private Drawable mSelectHandleLeft;
+    private Drawable mSelectHandleRight;
+    private Drawable mSelectHandleCenter;
+
+    // Global listener that detects changes in the global position of the TextView
+    private PositionListener mPositionListener;
+
+    float mLastDownPositionX, mLastDownPositionY;
+    Callback mCustomSelectionActionModeCallback;
+
+    // Set when this TextView gained focus with some text selected. Will start selection mode.
+    boolean mCreatedWithASelection;
+
+    private EasyEditSpanController mEasyEditSpanController;
+
+    WordIterator mWordIterator;
+    SpellChecker mSpellChecker;
+
+    private Rect mTempRect;
+
+    private TextView mTextView;
+
+    Editor(TextView textView) {
+        mTextView = textView;
+        mEasyEditSpanController = new EasyEditSpanController();
+        mTextView.addTextChangedListener(mEasyEditSpanController);
+    }
+
+    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) {
+            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+        }
+        updateSpellCheckSpans(0, mTextView.getText().length(),
+                true /* create the spell checker if needed */);
+    }
+
+    void onDetachedFromWindow() {
+        if (mError != null) {
+            hideError();
+        }
+
+        if (mBlink != null) {
+            mBlink.removeCallbacks(mBlink);
+        }
+
+        if (mInsertionPointCursorController != null) {
+            mInsertionPointCursorController.onDetached();
+        }
+
+        if (mSelectionModifierCursorController != null) {
+            mSelectionModifierCursorController.onDetached();
+        }
+
+        if (mShowSuggestionRunnable != null) {
+            mTextView.removeCallbacks(mShowSuggestionRunnable);
+        }
+
+        invalidateTextDisplayList();
+
+        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;
+        }
+
+        hideControllers();
+    }
+
+    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;
+        final Drawables dr = mTextView.mDrawables;
+        if (dr != null) {
+            switch (mTextView.getResolvedLayoutDirection()) {
+                default:
+                case View.LAYOUT_DIRECTION_LTR:
+                    mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
+                            dr.mDrawableBottom);
+                    break;
+                case View.LAYOUT_DIRECTION_RTL:
+                    mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
+                            dr.mDrawableBottom);
+                    break;
+            }
+        } else {
+            mTextView.setCompoundDrawables(null, null, icon, null);
+        }
+
+        if (mError == null) {
+            if (mErrorPopup != null) {
+                if (mErrorPopup.isShowing()) {
+                    mErrorPopup.dismiss();
+                }
+
+                mErrorPopup = null;
+            }
+        } else {
+            if (mTextView.isFocused()) {
+                showError();
+            }
+        }
+    }
+
+    private void hideError() {
+        if (mErrorPopup != null) {
+            if (mErrorPopup.isShowing()) {
+                mErrorPopup.dismiss();
+            }
+        }
+
+        mShowErrorAfterAttach = false;
+    }
+
+    /**
+     * Returns the Y 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;
+        return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() -
+                (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
+    }
+
+    /**
+     * 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;
+        int icontop = compoundPaddingTop +
+                (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 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 + (dr != null ? dr.mDrawableHeightRight : 0) - 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) {
+            stopSelectionActionMode();
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.onDetached();
+                mSelectionModifierCursorController = null;
+            }
+        }
+    }
+
+    private void hideInsertionPointCursorController() {
+        if (mInsertionPointCursorController != null) {
+            mInsertionPointCursorController.hide();
+        }
+    }
+
+    /**
+     * Hides the insertion controller and stops text selection mode, hiding the selection controller
+     */
+    void hideControllers() {
+        hideCursorControllers();
+        hideSpanControllers();
+    }
+
+    private void hideSpanControllers() {
+        if (mEasyEditSpanController != null) {
+            mEasyEditSpanController.hide();
+        }
+    }
+
+    private void hideCursorControllers() {
+        if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
+            // Should be done before hide insertion point controller since it triggers a show of it
+            mSuggestionsPopupWindow.hide();
+        }
+        hideInsertionPointCursorController();
+        stopSelectionActionMode();
+    }
+
+    /**
+     * Create new SpellCheckSpans on the modified region.
+     */
+    private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
+        if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
+                !(mTextView instanceof ExtractEditText)) {
+            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(PopupWindow pop, CharSequence text, TextView tv) {
+        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
+        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
+
+        int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
+                com.android.internal.R.dimen.textview_error_popup_default_width);
+        Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
+                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
+        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());
+        }
+    }
+
+    /**
+     * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
+     * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
+     * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
+     */
+    private boolean canSelectText() {
+        return hasSelectionController() && mTextView.getText().length() != 0;
+    }
+
+    /**
+     * 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.
+     */
+    private boolean hasPasswordTransformationMethod() {
+        return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
+    }
+
+    /**
+     * Adjusts selection to the word under last touch offset.
+     * Return true if the operation was successfully performed.
+     */
+    private boolean selectCurrentWord() {
+        if (!canSelectText()) {
+            return false;
+        }
+
+        if (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 mTextView.selectAllText();
+        }
+
+        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 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 {
+            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 = getCharRange(minOffset);
+                selectionStart = TextUtils.unpackRangeStartFromLong(range);
+                selectionEnd = TextUtils.unpackRangeEndFromLong(range);
+            }
+        }
+
+        Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
+        return selectionEnd > selectionStart;
+    }
+
+    void onLocaleChanged() {
+        // Will be re-created on demand in getWordIterator with the proper new locale
+        mWordIterator = null;
+    }
+
+    /**
+     * @hide
+     */
+    public WordIterator getWordIterator() {
+        if (mWordIterator == null) {
+            mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
+        }
+        return mWordIterator;
+    }
+
+    private long getCharRange(int offset) {
+        final int textLength = mTextView.getText().length();
+        if (offset + 1 < textLength) {
+            final char currentChar = mTextView.getText().charAt(offset);
+            final char nextChar = mTextView.getText().charAt(offset + 1);
+            if (Character.isSurrogatePair(currentChar, nextChar)) {
+                return TextUtils.packRangeInLong(offset,  offset + 2);
+            }
+        }
+        if (offset < textLength) {
+            return TextUtils.packRangeInLong(offset,  offset + 1);
+        }
+        if (offset - 2 >= 0) {
+            final char previousChar = mTextView.getText().charAt(offset - 1);
+            final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
+            if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
+                return TextUtils.packRangeInLong(offset - 2,  offset);
+            }
+        }
+        if (offset - 1 >= 0) {
+            return TextUtils.packRangeInLong(offset - 1,  offset);
+        }
+        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 isPositionVisible(int positionX, int positionY) {
+        synchronized (TEMP_POSITION) {
+            final float[] position = TEMP_POSITION;
+            position[0] = positionX;
+            position[1] = positionY;
+            View view = mTextView;
+
+            while (view != null) {
+                if (view != mTextView) {
+                    // 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;
+    }
+
+    private boolean isOffsetVisible(int offset) {
+        Layout layout = mTextView.getLayout();
+        final int line = layout.getLineForOffset(offset);
+        final int lineBottom = layout.getLineBottom(line);
+        final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
+        return 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;
+    }
+
+    public boolean performLongClick(boolean handled) {
+        // Long press in empty space moves cursor and shows the Paste affordance if available.
+        if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
+                mInsertionControllerEnabled) {
+            final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
+                    mLastDownPositionY);
+            stopSelectionActionMode();
+            Selection.setSelection((Spannable) mTextView.getText(), offset);
+            getInsertionController().showWithActionPopup();
+            handled = true;
+        }
+
+        if (!handled && mSelectionActionMode != null) {
+            if (touchPositionIsInSelection()) {
+                // Start a drag
+                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.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
+                stopSelectionActionMode();
+            } else {
+                getSelectionController().hide();
+                selectCurrentWord();
+                getSelectionController().show();
+            }
+            handled = true;
+        }
+
+        // Start a new selection
+        if (!handled) {
+            handled = startSelectionActionMode();
+        }
+
+        return handled;
+    }
+
+    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 instanceof ExtractEditText) || 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 instanceof ExtractEditText) {
+                // terminateTextSelectionMode removes selection, which we want to keep when
+                // ExtractEditText goes out of focus.
+                final int selStart = mTextView.getSelectionStart();
+                final int selEnd = mTextView.getSelectionEnd();
+                hideControllers();
+                Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
+            } else {
+                hideControllers();
+                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 after) {
+        updateSpellCheckSpans(start, start + after, false);
+
+        // 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();
+    }
+
+    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();
+            }
+        } else {
+            if (mBlink != null) {
+                mBlink.cancel();
+            }
+            if (mInputContentType != null) {
+                mInputContentType.enterDown = false;
+            }
+            // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
+            hideControllers();
+            if (mSuggestionsPopupWindow != null) {
+                mSuggestionsPopupWindow.onParentLostFocus();
+            }
+
+            // Don't leave us in the middle of a batch edit.
+            mTextView.onEndBatchEdit();
+        }
+    }
+
+    void onTouchEvent(MotionEvent event) {
+        if (hasSelectionController()) {
+            getSelectionController().onTouchEvent(event);
+        }
+
+        if (mShowSuggestionRunnable != null) {
+            mTextView.removeCallbacks(mShowSuggestionRunnable);
+            mShowSuggestionRunnable = null;
+        }
+
+        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;
+        }
+    }
+
+    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;
+                }
+                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();
+
+        if (ims.mContentChanged || ims.mSelectionModeChanged) {
+            mTextView.updateAfterEdit();
+            reportExtractedText();
+        } else if (ims.mCursorChanged) {
+            // Cheezy way to get us to report the current cursor location.
+            mTextView.invalidateCursor();
+        }
+    }
+
+    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(ExtractedTextRequest request,
+            int partialStartOffset, int partialEndOffset, int delta,
+            ExtractedText outText) {
+        final CharSequence content = mTextView.getText();
+        if (content != null) {
+            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;
+        }
+        return false;
+    }
+
+    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.mExtracting;
+                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.mTmpExtracted)) {
+                            if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
+                                    "Reporting extracted start=" +
+                                    ims.mTmpExtracted.partialStartOffset +
+                                    " end=" + ims.mTmpExtracted.partialEndOffset +
+                                    ": " + ims.mTmpExtracted.text);
+                            imm.updateExtractedText(mTextView, req.token, ims.mTmpExtracted);
+                            ims.mChangedStart = EXTRACT_UNKNOWN;
+                            ims.mChangedEnd = EXTRACT_UNKNOWN;
+                            ims.mChangedDelta = 0;
+                            ims.mContentChanged = false;
+                            return true;
+                        }
+                    }
+                }
+            }
+        }
+        return false;
+    }
+
+    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)) {
+                    boolean reported = false;
+                    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.
+                        reported = reportExtractedText();
+                    }
+                    if (!reported && highlight != null) {
+                        int candStart = -1;
+                        int candEnd = -1;
+                        if (mTextView.getText() instanceof Spannable) {
+                            Spannable sp = (Spannable) mTextView.getText();
+                            candStart = EditableInputConnection.getComposingSpanStart(sp);
+                            candEnd = EditableInputConnection.getComposingSpanEnd(sp);
+                        }
+                        imm.updateSelection(mTextView,
+                                selectionStart, selectionEnd, candStart, candEnd);
+                    }
+                }
+
+                if (imm.isWatchingCursor(mTextView) && highlight != null) {
+                    highlight.computeBounds(ims.mTmpRectF, true);
+                    ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
+
+                    canvas.getMatrix().mapPoints(ims.mTmpOffset);
+                    ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
+
+                    ims.mTmpRectF.offset(0, cursorOffsetVertical);
+
+                    ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
+                            (int)(ims.mTmpRectF.top + 0.5),
+                            (int)(ims.mTmpRectF.right + 0.5),
+                            (int)(ims.mTmpRectF.bottom + 0.5));
+
+                    imm.updateCursor(mTextView,
+                            ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
+                            ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
+                }
+            }
+        }
+
+        if (mCorrectionHighlighter != null) {
+            mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
+        }
+
+        if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
+            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);
+        }
+    }
+
+    private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
+            Paint highlightPaint, int cursorOffsetVertical) {
+        final int width = mTextView.getWidth();
+        final int height = mTextView.getHeight();
+
+        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 (mTextDisplayLists == null) {
+                mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
+            }
+
+            DynamicLayout dynamicLayout = (DynamicLayout) layout;
+            int[] blockEnds = dynamicLayout.getBlockEnds();
+            int[] blockIndices = dynamicLayout.getBlockIndices();
+            final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
+
+            final int mScrollX = mTextView.getScrollX();
+            final int mScrollY = mTextView.getScrollY();
+            canvas.translate(mScrollX, mScrollY);
+            int endOfPreviousBlock = -1;
+            int searchStartIndex = 0;
+            for (int i = 0; i < numberOfBlocks; i++) {
+                int blockEnd = blockEnds[i];
+                int blockIndex = blockIndices[i];
+
+                final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
+                if (blockIsInvalid) {
+                    blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
+                            searchStartIndex);
+                    // Dynamic layout internal block indices structure is updated from Editor
+                    blockIndices[i] = blockIndex;
+                    searchStartIndex = blockIndex + 1;
+                }
+
+                DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
+                if (blockDisplayList == null) {
+                    blockDisplayList = mTextDisplayLists[blockIndex] =
+                            mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex);
+                } else {
+                    if (blockIsInvalid) blockDisplayList.invalidate();
+                }
+
+                if (!blockDisplayList.isValid()) {
+                    final HardwareCanvas hardwareCanvas = blockDisplayList.start();
+                    try {
+                        hardwareCanvas.setViewport(width, height);
+                        // The dirty rect should always be null for a display list
+                        hardwareCanvas.onPreDraw(null);
+                        hardwareCanvas.translate(-mScrollX, -mScrollY);
+                        layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
+                        hardwareCanvas.translate(mScrollX, mScrollY);
+                    } finally {
+                        hardwareCanvas.onPostDraw();
+                        blockDisplayList.end();
+                        if (View.USE_DISPLAY_LIST_PROPERTIES) {
+                            blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
+                        }
+                    }
+                }
+
+                ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
+                        DisplayList.FLAG_CLIP_CHILDREN);
+                endOfPreviousBlock = blockEnd;
+            }
+            canvas.translate(-mScrollX, -mScrollY);
+        } else {
+            // Boring layout is used for empty and hint text
+            layout.drawText(canvas, firstLine, lastLine);
+        }
+    }
+
+    private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
+            int searchStartIndex) {
+        int length = mTextDisplayLists.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
+        int newSize = ArrayUtils.idealIntArraySize(length + 1);
+        DisplayList[] displayLists = new DisplayList[newSize];
+        System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
+        mTextDisplayLists = displayLists;
+        return length;
+    }
+
+    private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
+        final boolean translate = cursorOffsetVertical != 0;
+        if (translate) canvas.translate(0, cursorOffsetVertical);
+        for (int i = 0; i < mCursorCount; i++) {
+            mCursorDrawable[i].draw(canvas);
+        }
+        if (translate) canvas.translate(0, -cursorOffsetVertical);
+    }
+
+    void invalidateTextDisplayList() {
+        if (mTextDisplayLists != null) {
+            for (int i = 0; i < mTextDisplayLists.length; i++) {
+                if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
+            }
+        }
+    }
+
+    void updateCursorsPositions() {
+        if (mTextView.mCursorDrawableRes == 0) {
+            mCursorCount = 0;
+            return;
+        }
+
+        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.getLineTop(line + 1);
+
+        mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
+
+        int middle = bottom;
+        if (mCursorCount == 2) {
+            // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
+            middle = (top + bottom) >> 1;
+        }
+
+        updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset));
+
+        if (mCursorCount == 2) {
+            updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset));
+        }
+    }
+
+    /**
+     * @return true if the selection mode was actually started.
+     */
+    boolean startSelectionActionMode() {
+        if (mSelectionActionMode != null) {
+            // Selection action mode is already started
+            return false;
+        }
+
+        if (!canSelectText() || !mTextView.requestFocus()) {
+            Log.w(TextView.LOG_TAG,
+                    "TextView does not support text selection. Action mode cancelled.");
+            return false;
+        }
+
+        if (!mTextView.hasSelection()) {
+            // There may already be a selection on device rotation
+            if (!selectCurrentWord()) {
+                // No word found under cursor or text selection not permitted.
+                return false;
+            }
+        }
+
+        boolean willExtract = extractedTextModeWillBeStarted();
+
+        // Do not start the action mode when extracted text will show up full screen, which would
+        // immediately hide the newly created action bar and would be visually distracting.
+        if (!willExtract) {
+            ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
+            mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
+        }
+
+        final boolean selectionStarted = mSelectionActionMode != null || willExtract;
+        if (selectionStarted && !mTextView.isTextSelectable()) {
+            // 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 instanceof ExtractEditText)) {
+            final InputMethodManager imm = InputMethodManager.peekInstance();
+            return  imm != null && imm.isFullscreenMode();
+        }
+        return false;
+    }
+
+    /**
+     * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
+     */
+    private boolean isCursorInsideSuggestionSpan() {
+        CharSequence text = mTextView.getText();
+        if (!(text instanceof Spannable)) return false;
+
+        SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
+                mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
+        return (suggestionSpans.length > 0);
+    }
+
+    /**
+     * @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) {
+        boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
+        hideControllers();
+        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()) {
+                    mShowSuggestionRunnable = new Runnable() {
+                        public void run() {
+                            showSuggestions();
+                        }
+                    };
+                    // removeCallbacks is performed on every touch
+                    mTextView.postDelayed(mShowSuggestionRunnable,
+                            ViewConfiguration.getDoubleTapTimeout());
+                } else if (hasInsertionController()) {
+                    getInsertionController().show();
+                }
+            }
+        }
+    }
+
+    protected void stopSelectionActionMode() {
+        if (mSelectionActionMode != null) {
+            // This will hide the mSelectionModifierCursorController
+            mSelectionActionMode.finish();
+        }
+    }
+
+    /**
+     * @return True if this view supports insertion handles.
+     */
+    boolean hasInsertionController() {
+        return mInsertionControllerEnabled;
+    }
+
+    /**
+     * @return True if this view supports selection handles.
+     */
+    boolean hasSelectionController() {
+        return mSelectionControllerEnabled;
+    }
+
+    InsertionPointCursorController getInsertionController() {
+        if (!mInsertionControllerEnabled) {
+            return null;
+        }
+
+        if (mInsertionPointCursorController == null) {
+            mInsertionPointCursorController = new InsertionPointCursorController();
+
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
+        }
+
+        return mInsertionPointCursorController;
+    }
+
+    SelectionModifierCursorController getSelectionController() {
+        if (!mSelectionControllerEnabled) {
+            return null;
+        }
+
+        if (mSelectionModifierCursorController == null) {
+            mSelectionModifierCursorController = new SelectionModifierCursorController();
+
+            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
+            observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
+        }
+
+        return mSelectionModifierCursorController;
+    }
+
+    private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
+        if (mCursorDrawable[cursorIndex] == null)
+            mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable(
+                    mTextView.mCursorDrawableRes);
+
+        if (mTempRect == null) mTempRect = new Rect();
+        mCursorDrawable[cursorIndex].getPadding(mTempRect);
+        final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
+        horizontal = Math.max(0.5f, horizontal - 0.5f);
+        final int left = (int) (horizontal) - mTempRect.left;
+        mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
+                bottom + mTempRect.bottom);
+    }
+
+    /**
+     * Called by the framework in response to a text auto-correction (such as fixing a typo using a
+     * a dictionnary) 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);
+    }
+
+    void showSuggestions() {
+        if (mSuggestionsPopupWindow == null) {
+            mSuggestionsPopupWindow = new SuggestionsPopupWindow();
+        }
+        hideControllers();
+        mSuggestionsPopupWindow.show();
+    }
+
+    boolean areSuggestionsShown() {
+        return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
+    }
+
+    void onScrollChanged() {
+            if (mPositionListener != null) {
+                mPositionListener.onScrollChanged();
+            }
+            // Internal scroll affects the clip boundaries
+            invalidateTextDisplayList();
+    }
+
+    /**
+     * @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();
+            mBlink.removeCallbacks(mBlink);
+            mBlink.postAtTime(mBlink, mShowCursor + BLINK);
+        } else {
+            if (mBlink != null) mBlink.removeCallbacks(mBlink);
+        }
+    }
+
+    private class Blink extends Handler implements Runnable {
+        private boolean mCancelled;
+
+        public void run() {
+            Log.d("GILLES", "blinking !!!");
+            if (mCancelled) {
+                return;
+            }
+
+            removeCallbacks(Blink.this);
+
+            if (shouldBlink()) {
+                if (mTextView.getLayout() != null) {
+                    mTextView.invalidateCursorPath();
+                }
+
+                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
+            }
+        }
+
+        void cancel() {
+            if (!mCancelled) {
+                removeCallbacks(Blink.this);
+                mCancelled = true;
+            }
+        }
+
+        void uncancel() {
+            mCancelled = false;
+        }
+    }
+
+    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
+        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 (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
+            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
+        }
+        shadowView.setText(text);
+        shadowView.setTextColor(mTextView.getTextColors());
+
+        shadowView.setTextAppearance(mTextView.getContext(), 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) {
+        StringBuilder content = new StringBuilder("");
+        ClipData clipData = event.getClipData();
+        final int itemCount = clipData.getItemCount();
+        for (int i=0; i < itemCount; i++) {
+            Item item = clipData.getItemAt(i);
+            content.append(item.coerceToText(mTextView.getContext()));
+        }
+
+        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();
+        long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
+        int min = TextUtils.unpackRangeStartFromLong(minMax);
+        int max = TextUtils.unpackRangeEndFromLong(minMax);
+
+        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.
+            CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1);
+            if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) &&
+                    (dragSourceStart == mTextView.getText().length() ||
+                    Character.isSpaceChar(t.charAt(1))) ) {
+                final int pos = dragSourceStart == mTextView.getText().length() ?
+                        dragSourceStart - 1 : dragSourceStart;
+                mTextView.deleteText_internal(pos, pos + 1);
+            }
+        }
+    }
+
+    /**
+     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
+     * pop-up should be displayed.
+     */
+    class EasyEditSpanController implements TextWatcher {
+
+        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
+
+        private EasyEditPopupWindow mPopupWindow;
+
+        private EasyEditSpan mEasyEditSpan;
+
+        private Runnable mHidePopup;
+
+        public void hide() {
+            if (mPopupWindow != null) {
+                mPopupWindow.hide();
+                mTextView.removeCallbacks(mHidePopup);
+            }
+            removeSpans(mTextView.getText());
+            mEasyEditSpan = null;
+        }
+
+        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
+            // Intentionally empty
+        }
+
+        public void afterTextChanged(Editable s) {
+            // Intentionally empty
+        }
+
+        /**
+         * Monitors the changes in the text.
+         *
+         * <p>{@link SpanWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
+         * as the notifications are not sent when a spannable (with spans) is inserted.
+         */
+        public void onTextChanged(CharSequence buffer, int start, int before, int after) {
+            adjustSpans(buffer, start, after);
+
+            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 layout yet, ignore the text change
+                return;
+            }
+
+            InputMethodManager imm = InputMethodManager.peekInstance();
+            if (!(mTextView instanceof ExtractEditText) && imm != null && imm.isFullscreenMode()) {
+                // The input is in extract mode. We do not have to handle the easy edit in the
+                // original TextView, as the ExtractEditText will do
+                return;
+            }
+
+            // Remove the current easy edit span, as the text changed, and remove the pop-up
+            // (if any)
+            if (mEasyEditSpan != null) {
+                if (buffer instanceof Spannable) {
+                    ((Spannable) buffer).removeSpan(mEasyEditSpan);
+                }
+                mEasyEditSpan = null;
+            }
+            if (mPopupWindow != null && mPopupWindow.isShowing()) {
+                mPopupWindow.hide();
+            }
+
+            // Display the new easy edit span (if any).
+            if (buffer instanceof Spanned) {
+                mEasyEditSpan = getSpan((Spanned) buffer);
+                if (mEasyEditSpan != null) {
+                    if (mPopupWindow == null) {
+                        mPopupWindow = new EasyEditPopupWindow();
+                        mHidePopup = new Runnable() {
+                            @Override
+                            public void run() {
+                                hide();
+                            }
+                        };
+                    }
+                    mPopupWindow.show(mEasyEditSpan);
+                    mTextView.removeCallbacks(mHidePopup);
+                    mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
+                }
+            }
+        }
+
+        /**
+         * Adjusts the spans by removing all of them except the last one.
+         */
+        private void adjustSpans(CharSequence buffer, int start, int after) {
+            // This method enforces that only one easy edit span is attached to the text.
+            // A better way to enforce this would be to listen for onSpanAdded, but this method
+            // cannot be used in this scenario as no notification is triggered when a text with
+            // spans is inserted into a text.
+            if (buffer instanceof Spannable) {
+                Spannable spannable = (Spannable) buffer;
+                EasyEditSpan[] spans = spannable.getSpans(start, start + after, EasyEditSpan.class);
+                if (spans.length > 0) {
+                    // Assuming there was only one EasyEditSpan before, we only need check to
+                    // check for a duplicate if a new one is found in the modified interval
+                    spans = spannable.getSpans(0, spannable.length(),  EasyEditSpan.class);
+                    for (int i = 1; i < spans.length; i++) {
+                        spannable.removeSpan(spans[i]);
+                    }
+                }
+            }
+        }
+
+        /**
+         * Removes all the {@link EasyEditSpan} currently attached.
+         */
+        private void removeSpans(CharSequence buffer) {
+            if (buffer instanceof Spannable) {
+                Spannable spannable = (Spannable) buffer;
+                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
+                        EasyEditSpan.class);
+                for (int i = 0; i < spans.length; i++) {
+                    spannable.removeSpan(spans[i]);
+                }
+            }
+        }
+
+        private EasyEditSpan getSpan(Spanned spanned) {
+            EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
+                    EasyEditSpan.class);
+            if (easyEditSpans.length == 0) {
+                return null;
+            } else {
+                return easyEditSpans[0];
+            }
+        }
+    }
+
+    /**
+     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
+     * by {@link EasyEditSpanController}.
+     */
+    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;
+
+        @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 show(EasyEditSpan easyEditSpan) {
+            mEasyEditSpan = easyEditSpan;
+            super.show();
+        }
+
+        @Override
+        public void onClick(View view) {
+            if (view == mDeleteTextView) {
+                Editable editable = (Editable) mTextView.getText();
+                int start = editable.getSpanStart(mEasyEditSpan);
+                int end = editable.getSpanEnd(mEasyEditSpan);
+                if (start >= 0 && end >= 0) {
+                    mTextView.deleteText_internal(start, end);
+                }
+            }
+        }
+
+        @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) {
+            return mTextView.getLayout().getLineBottom(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)
+        private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
+        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 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;
+        }
+
+        @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];
+        }
+
+        public void onScrollChanged() {
+            mScrollHasChanged = true;
+        }
+    }
+
+    private abstract class PinnedPopupWindow implements TextViewPositionListener {
+        protected PopupWindow mPopupWindow;
+        protected ViewGroup mContentView;
+        int mPositionX, mPositionY;
+
+        protected abstract void createPopupWindow();
+        protected abstract void initContentView();
+        protected abstract int getTextOffset();
+        protected abstract int getVerticalLocalPosition(int line);
+        protected abstract int clipVertically(int positionY);
+
+        public PinnedPopupWindow() {
+            createPopupWindow();
+
+            mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_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, positionX);
+            positionX = Math.max(0, positionX);
+
+            if (isShowing()) {
+                mPopupWindow.update(positionX, positionY, -1, -1);
+            } else {
+                mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+                        positionX, positionY);
+            }
+        }
+
+        public void hide() {
+            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 class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
+        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
+        private static final int ADD_TO_DICTIONARY = -1;
+        private static final int DELETE_TEXT = -2;
+        private SuggestionInfo[] mSuggestionInfos;
+        private int mNumberOfSuggestions;
+        private boolean mCursorWasVisibleBeforeSuggestions;
+        private boolean mIsShowingUp = false;
+        private SuggestionAdapter mSuggestionsAdapter;
+        private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
+        private final HashMap<SuggestionSpan, Integer> mSpansLengths;
+
+        private class CustomPopupWindow extends PopupWindow {
+            public CustomPopupWindow(Context context, int defStyle) {
+                super(context, null, defStyle);
+            }
+
+            @Override
+            public void dismiss() {
+                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()) {
+                    getInsertionController().show();
+                }
+            }
+        }
+
+        public SuggestionsPopupWindow() {
+            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
+            mSuggestionSpanComparator = new SuggestionSpanComparator();
+            mSpansLengths = new HashMap<SuggestionSpan, Integer>();
+        }
+
+        @Override
+        protected void createPopupWindow() {
+            mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
+                com.android.internal.R.attr.textSuggestionsWindowStyle);
+            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
+            mPopupWindow.setFocusable(true);
+            mPopupWindow.setClippingEnabled(false);
+        }
+
+        @Override
+        protected void initContentView() {
+            ListView listView = new ListView(mTextView.getContext());
+            mSuggestionsAdapter = new SuggestionAdapter();
+            listView.setAdapter(mSuggestionsAdapter);
+            listView.setOnItemClickListener(this);
+            mContentView = listView;
+
+            // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
+            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
+            for (int i = 0; i < mSuggestionInfos.length; i++) {
+                mSuggestionInfos[i] = new SuggestionInfo();
+            }
+        }
+
+        public boolean isShowingUp() {
+            return mIsShowingUp;
+        }
+
+        public void onParentLostFocus() {
+            mIsShowingUp = false;
+        }
+
+        private class SuggestionInfo {
+            int suggestionStart, suggestionEnd; // range of actual suggestion within text
+            SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
+            int suggestionIndex; // the index of this suggestion inside suggestionSpan
+            SpannableStringBuilder text = new SpannableStringBuilder();
+            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
+                    android.R.style.TextAppearance_SuggestionHighlight);
+        }
+
+        private class SuggestionAdapter extends BaseAdapter {
+            private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
+                    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.text);
+
+                if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
+                    textView.setCompoundDrawablesWithIntrinsicBounds(
+                            com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
+                } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
+                    textView.setCompoundDrawablesWithIntrinsicBounds(
+                            com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
+                } else {
+                    textView.setCompoundDrawables(null, null, null, null);
+                }
+
+                return textView;
+            }
+        }
+
+        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[] getSuggestionSpans() {
+            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);
+            return suggestionSpans;
+        }
+
+        @Override
+        public void show() {
+            if (!(mTextView.getText() instanceof Editable)) 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());
+            }
+
+            // 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();
+        }
+
+        @Override
+        protected int getVerticalLocalPosition(int line) {
+            return mTextView.getLayout().getLineBottom(line);
+        }
+
+        @Override
+        protected int clipVertically(int positionY) {
+            final int height = mContentView.getMeasuredHeight();
+            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
+            return Math.min(positionY, displayMetrics.heightPixels - height);
+        }
+
+        @Override
+        public void hide() {
+            super.hide();
+        }
+
+        private boolean updateSuggestions() {
+            Spannable spannable = (Spannable) mTextView.getText();
+            SuggestionSpan[] suggestionSpans = getSuggestionSpans();
+
+            final int nbSpans = suggestionSpans.length;
+            // Suggestions are shown after a delay: the underlying spans may have been removed
+            if (nbSpans == 0) return false;
+
+            mNumberOfSuggestions = 0;
+            int spanUnionStart = mTextView.getText().length();
+            int spanUnionEnd = 0;
+
+            SuggestionSpan misspelledSpan = null;
+            int underlineColor = 0;
+
+            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
+                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
+                final int spanStart = spannable.getSpanStart(suggestionSpan);
+                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
+                spanUnionStart = Math.min(spanStart, spanUnionStart);
+                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
+
+                if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
+                    misspelledSpan = suggestionSpan;
+                }
+
+                // The first span dictates the background color of the highlighted text
+                if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
+
+                String[] suggestions = suggestionSpan.getSuggestions();
+                int nbSuggestions = suggestions.length;
+                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
+                    String suggestion = suggestions[suggestionIndex];
+
+                    boolean suggestionIsDuplicate = false;
+                    for (int i = 0; i < mNumberOfSuggestions; i++) {
+                        if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
+                            SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
+                            final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
+                            final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
+                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
+                                suggestionIsDuplicate = true;
+                                break;
+                            }
+                        }
+                    }
+
+                    if (!suggestionIsDuplicate) {
+                        SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+                        suggestionInfo.suggestionSpan = suggestionSpan;
+                        suggestionInfo.suggestionIndex = suggestionIndex;
+                        suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
+
+                        mNumberOfSuggestions++;
+
+                        if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
+                            // Also end outer for loop
+                            spanIndex = nbSpans;
+                            break;
+                        }
+                    }
+                }
+            }
+
+            for (int i = 0; i < mNumberOfSuggestions; i++) {
+                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
+            }
+
+            // Add "Add to dictionary" item if there is a span with the misspelled flag
+            if (misspelledSpan != null) {
+                final int misspelledStart = spannable.getSpanStart(misspelledSpan);
+                final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
+                if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
+                    SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+                    suggestionInfo.suggestionSpan = misspelledSpan;
+                    suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
+                    suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
+                            getContext().getString(com.android.internal.R.string.addToDictionary));
+                    suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
+                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+                    mNumberOfSuggestions++;
+                }
+            }
+
+            // Delete item
+            SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
+            suggestionInfo.suggestionSpan = null;
+            suggestionInfo.suggestionIndex = DELETE_TEXT;
+            suggestionInfo.text.replace(0, suggestionInfo.text.length(),
+                    mTextView.getContext().getString(com.android.internal.R.string.deleteText));
+            suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+            mNumberOfSuggestions++;
+
+            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
+            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 = text.getSpanStart(suggestionInfo.suggestionSpan);
+            final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
+
+            // Adjust the start/end of the suggestion span
+            suggestionInfo.suggestionStart = spanStart - unionStart;
+            suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
+                    + suggestionInfo.text.length();
+
+            suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
+                    suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+
+            // Add the text before and after the span.
+            final String textAsString = text.toString();
+            suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
+            suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
+        }
+
+        @Override
+        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
+            Editable editable = (Editable) mTextView.getText();
+            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
+
+            if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
+                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);
+                }
+                hide();
+                return;
+            }
+
+            final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
+            final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
+            if (spanStart < 0 || spanEnd <= spanStart) {
+                // Span has been removed
+                hide();
+                return;
+            }
+
+            final String originalText = editable.toString().substring(spanStart, spanEnd);
+
+            if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
+                Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
+                intent.putExtra("word", originalText);
+                intent.putExtra("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(suggestionInfo.suggestionSpan);
+                updateSpellCheckSpans(spanStart, spanEnd, false);
+            } else {
+                // 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);
+                    }
+                }
+
+                final int suggestionStart = suggestionInfo.suggestionStart;
+                final int suggestionEnd = suggestionInfo.suggestionEnd;
+                final String suggestion = suggestionInfo.text.subSequence(
+                        suggestionStart, suggestionEnd).toString();
+                mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
+
+                // Notify source IME of the suggestion pick. Do this before swaping texts.
+                if (!TextUtils.isEmpty(
+                        suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
+                    InputMethodManager imm = InputMethodManager.peekInstance();
+                    if (imm != null) {
+                        imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
+                                suggestionInfo.suggestionIndex);
+                    }
+                }
+
+                // Swap text content between actual text and Suggestion span
+                String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
+                suggestions[suggestionInfo.suggestionIndex] = originalText;
+
+                // Restore previous SuggestionSpans
+                final int lengthDifference = 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] + lengthDifference, suggestionSpansFlags[i]);
+                    }
+                }
+
+                // Move cursor at the end of the replaced word
+                final int newCursorPosition = spanEnd + lengthDifference;
+                mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
+            }
+
+            hide();
+        }
+    }
+
+    /**
+     * An ActionMode Callback class that is used to provide actions while in text selection mode.
+     *
+     * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
+     * on which of these this TextView supports.
+     */
+    private class SelectionActionModeCallback implements ActionMode.Callback {
+
+        @Override
+        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
+            TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
+                    com.android.internal.R.styleable.SelectionModeDrawables);
+
+            boolean allowText = mTextView.getContext().getResources().getBoolean(
+                    com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
+
+            mode.setTitle(mTextView.getContext().getString(
+                    com.android.internal.R.string.textSelectionCABTitle));
+            mode.setSubtitle(null);
+            mode.setTitleOptionalHint(true);
+
+            int selectAllIconId = 0; // No icon by default
+            if (!allowText) {
+                // Provide an icon, text will not be displayed on smaller screens.
+                selectAllIconId = styledAttributes.getResourceId(
+                        R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
+            }
+
+            menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
+                    setIcon(selectAllIconId).
+                    setAlphabeticShortcut('a').
+                    setShowAsAction(
+                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+
+            if (mTextView.canCut()) {
+                menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
+                    setIcon(styledAttributes.getResourceId(
+                            R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
+                    setAlphabeticShortcut('x').
+                    setShowAsAction(
+                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+            }
+
+            if (mTextView.canCopy()) {
+                menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
+                    setIcon(styledAttributes.getResourceId(
+                            R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
+                    setAlphabeticShortcut('c').
+                    setShowAsAction(
+                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+            }
+
+            if (mTextView.canPaste()) {
+                menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
+                        setIcon(styledAttributes.getResourceId(
+                                R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
+                        setAlphabeticShortcut('v').
+                        setShowAsAction(
+                                MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
+            }
+
+            styledAttributes.recycle();
+
+            if (mCustomSelectionActionModeCallback != null) {
+                if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
+                    // The custom mode can choose to cancel the action mode
+                    return false;
+                }
+            }
+
+            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
+                getSelectionController().show();
+                return true;
+            } else {
+                return false;
+            }
+        }
+
+        @Override
+        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
+            if (mCustomSelectionActionModeCallback != null) {
+                return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
+            }
+            return true;
+        }
+
+        @Override
+        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
+            if (mCustomSelectionActionModeCallback != null &&
+                 mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
+                return true;
+            }
+            return mTextView.onTextContextMenuItem(item.getItemId());
+        }
+
+        @Override
+        public void onDestroyActionMode(ActionMode mode) {
+            if (mCustomSelectionActionModeCallback != null) {
+                mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
+            }
+            Selection.setSelection((Spannable) mTextView.getText(), mTextView.getSelectionEnd());
+
+            if (mSelectionModifierCursorController != null) {
+                mSelectionModifierCursorController.hide();
+            }
+
+            mSelectionActionMode = null;
+        }
+    }
+
+    private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
+        private static final int POPUP_TEXT_LAYOUT =
+                com.android.internal.R.layout.text_edit_action_popup_text;
+        private TextView mPasteTextView;
+        private TextView mReplaceTextView;
+
+        @Override
+        protected void createPopupWindow() {
+            mPopupWindow = new PopupWindow(mTextView.getContext(), null,
+                    com.android.internal.R.attr.textSelectHandleWindowStyle);
+            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_paste_window);
+
+            LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
+                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+
+            LayoutParams wrapContent = new LayoutParams(
+                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+
+            mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+            mPasteTextView.setLayoutParams(wrapContent);
+            mContentView.addView(mPasteTextView);
+            mPasteTextView.setText(com.android.internal.R.string.paste);
+            mPasteTextView.setOnClickListener(this);
+
+            mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
+            mReplaceTextView.setLayoutParams(wrapContent);
+            mContentView.addView(mReplaceTextView);
+            mReplaceTextView.setText(com.android.internal.R.string.replace);
+            mReplaceTextView.setOnClickListener(this);
+        }
+
+        @Override
+        public void show() {
+            boolean canPaste = mTextView.canPaste();
+            boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
+            mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
+            mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
+
+            if (!canPaste && !canSuggest) return;
+
+            super.show();
+        }
+
+        @Override
+        public void onClick(View view) {
+            if (view == mPasteTextView && mTextView.canPaste()) {
+                mTextView.onTextContextMenuItem(TextView.ID_PASTE);
+                hide();
+            } else if (view == mReplaceTextView) {
+                int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+                stopSelectionActionMode();
+                Selection.setSelection((Spannable) mTextView.getText(), middle);
+                showSuggestions();
+            }
+        }
+
+        @Override
+        protected int getTextOffset() {
+            return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
+        }
+
+        @Override
+        protected int getVerticalLocalPosition(int line) {
+            return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
+        }
+
+        @Override
+        protected int clipVertically(int positionY) {
+            if (positionY < 0) {
+                final int offset = getTextOffset();
+                final Layout layout = mTextView.getLayout();
+                final int line = layout.getLineForOffset(offset);
+                positionY += layout.getLineBottom(line) - layout.getLineTop(line);
+                positionY += mContentView.getMeasuredHeight();
+
+                // Assumes insertion and selection handles share the same height
+                final Drawable handle = mTextView.getResources().getDrawable(
+                        mTextView.mTextSelectHandleRes);
+                positionY += handle.getIntrinsicHeight();
+            }
+
+            return positionY;
+        }
+    }
+
+    private 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;
+        // 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;
+        // Transient action popup window for Paste and Replace actions
+        protected ActionPopupWindow mActionPopupWindow;
+        // Previous text character offset
+        private int mPreviousOffset = -1;
+        // Previous text character offset
+        private boolean mPositionHasChanged = true;
+        // Used to delay the appearance of the action popup window
+        private Runnable mActionPopupShower;
+
+        public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
+            super(mTextView.getContext());
+            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.setContentView(this);
+
+            mDrawableLtr = drawableLtr;
+            mDrawableRtl = drawableRtl;
+
+            updateDrawable();
+
+            final int handleHeight = mDrawable.getIntrinsicHeight();
+            mTouchOffsetY = -0.3f * handleHeight;
+            mIdealVerticalOffset = 0.7f * handleHeight;
+        }
+
+        protected void updateDrawable() {
+            final int offset = getCurrentCursorOffset();
+            final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
+            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
+            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
+        }
+
+        protected abstract int getHotspotX(Drawable drawable, 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() {
+            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);
+            }
+        }
+
+        public boolean offsetHasBeenChanged() {
+            return mNumberPreviousOffsets > 1;
+        }
+
+        @Override
+        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+            setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
+        }
+
+        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);
+
+            hideActionPopupWindow();
+        }
+
+        protected void dismiss() {
+            mIsDragging = false;
+            mContainer.dismiss();
+            onDetached();
+        }
+
+        public void hide() {
+            dismiss();
+
+            getPositionListener().removeSubscriber(this);
+        }
+
+        void showActionPopupWindow(int delay) {
+            if (mActionPopupWindow == null) {
+                mActionPopupWindow = new ActionPopupWindow();
+            }
+            if (mActionPopupShower == null) {
+                mActionPopupShower = new Runnable() {
+                    public void run() {
+                        mActionPopupWindow.show();
+                    }
+                };
+            } else {
+                mTextView.removeCallbacks(mActionPopupShower);
+            }
+            mTextView.postDelayed(mActionPopupShower, delay);
+        }
+
+        protected void hideActionPopupWindow() {
+            if (mActionPopupShower != null) {
+                mTextView.removeCallbacks(mActionPopupShower);
+            }
+            if (mActionPopupWindow != null) {
+                mActionPopupWindow.hide();
+            }
+        }
+
+        public boolean isShowing() {
+            return mContainer.isShowing();
+        }
+
+        private boolean isVisible() {
+            // Always show a dragging handle.
+            if (mIsDragging) {
+                return true;
+            }
+
+            if (mTextView.isInBatchEditMode()) {
+                return false;
+            }
+
+            return isPositionVisible(mPositionX + mHotspotX, mPositionY);
+        }
+
+        public abstract int getCurrentCursorOffset();
+
+        protected abstract void updateSelection(int offset);
+
+        public abstract void updatePosition(float x, float y);
+
+        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
+            // 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;
+            }
+
+            boolean offsetChanged = offset != mPreviousOffset;
+            if (offsetChanged || parentScrolled) {
+                if (offsetChanged) {
+                    updateSelection(offset);
+                    addPositionToTouchUpFilter(offset);
+                }
+                final int line = layout.getLineForOffset(offset);
+
+                mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
+                mPositionY = layout.getLineBottom(line);
+
+                // Take TextView's padding and scroll into account.
+                mPositionX += mTextView.viewportToContentHorizontalOffset();
+                mPositionY += mTextView.viewportToContentVerticalOffset();
+
+                mPreviousOffset = offset;
+                mPositionHasChanged = true;
+            }
+        }
+
+        public void updatePosition(int parentPositionX, int parentPositionY,
+                boolean parentPositionChanged, boolean parentScrolled) {
+            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
+            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()) {
+                    final int positionX = parentPositionX + mPositionX;
+                    final int positionY = parentPositionY + mPositionY;
+                    if (isShowing()) {
+                        mContainer.update(positionX, positionY, -1, -1);
+                    } else {
+                        mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
+                                positionX, positionY);
+                    }
+                } else {
+                    if (isShowing()) {
+                        dismiss();
+                    }
+                }
+
+                mPositionHasChanged = false;
+            }
+        }
+
+        @Override
+        protected void onDraw(Canvas c) {
+            mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
+            mDrawable.draw(c);
+        }
+
+        @Override
+        public boolean onTouchEvent(MotionEvent ev) {
+            switch (ev.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN: {
+                    startTouchUpFilter(getCurrentCursorOffset());
+                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
+                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
+
+                    final PositionListener positionListener = getPositionListener();
+                    mLastParentX = positionListener.getPositionX();
+                    mLastParentY = positionListener.getPositionY();
+                    mIsDragging = true;
+                    break;
+                }
+
+                case MotionEvent.ACTION_MOVE: {
+                    final float rawX = ev.getRawX();
+                    final float rawY = ev.getRawY();
+
+                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
+                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
+                    final float currentVerticalOffset = rawY - 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 = rawX - mTouchToWindowOffsetX + mHotspotX;
+                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
+
+                    updatePosition(newPosX, newPosY);
+                    break;
+                }
+
+                case MotionEvent.ACTION_UP:
+                    filterOnTouchUp();
+                    mIsDragging = false;
+                    break;
+
+                case MotionEvent.ACTION_CANCEL:
+                    mIsDragging = false;
+                    break;
+            }
+            return true;
+        }
+
+        public boolean isDragging() {
+            return mIsDragging;
+        }
+
+        void onHandleMoved() {
+            hideActionPopupWindow();
+        }
+
+        public void onDetached() {
+            hideActionPopupWindow();
+        }
+    }
+
+    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 ActionPopupWindow
+        private float mDownPositionX, mDownPositionY;
+        private Runnable mHider;
+
+        public InsertionHandleView(Drawable drawable) {
+            super(drawable, drawable);
+        }
+
+        @Override
+        public void show() {
+            super.show();
+
+            final long durationSinceCutOrCopy =
+                    SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
+            if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
+                showActionPopupWindow(0);
+            }
+
+            hideAfterDelay();
+        }
+
+        public void showWithActionPopup() {
+            show();
+            showActionPopupWindow(0);
+        }
+
+        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
+        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) {
+                            if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
+                                // Tapping on the handle dismisses the displayed action popup
+                                mActionPopupWindow.hide();
+                            } else {
+                                showWithActionPopup();
+                            }
+                        }
+                    }
+                    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
+        public void updatePosition(float x, float y) {
+            positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
+        }
+
+        @Override
+        void onHandleMoved() {
+            super.onHandleMoved();
+            removeHiderCallback();
+        }
+
+        @Override
+        public void onDetached() {
+            super.onDetached();
+            removeHiderCallback();
+        }
+    }
+
+    private class SelectionStartHandleView extends HandleView {
+
+        public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
+            super(drawableLtr, drawableRtl);
+        }
+
+        @Override
+        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+            if (isRtlRun) {
+                return drawable.getIntrinsicWidth() / 4;
+            } else {
+                return (drawable.getIntrinsicWidth() * 3) / 4;
+            }
+        }
+
+        @Override
+        public int getCurrentCursorOffset() {
+            return mTextView.getSelectionStart();
+        }
+
+        @Override
+        public void updateSelection(int offset) {
+            Selection.setSelection((Spannable) mTextView.getText(), offset,
+                    mTextView.getSelectionEnd());
+            updateDrawable();
+        }
+
+        @Override
+        public void updatePosition(float x, float y) {
+            int offset = mTextView.getOffsetForPosition(x, y);
+
+            // Handles can not cross and selection is at least one character
+            final int selectionEnd = mTextView.getSelectionEnd();
+            if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
+
+            positionAtCursorOffset(offset, false);
+        }
+
+        public ActionPopupWindow getActionPopupWindow() {
+            return mActionPopupWindow;
+        }
+    }
+
+    private class SelectionEndHandleView extends HandleView {
+
+        public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
+            super(drawableLtr, drawableRtl);
+        }
+
+        @Override
+        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
+            if (isRtlRun) {
+                return (drawable.getIntrinsicWidth() * 3) / 4;
+            } else {
+                return drawable.getIntrinsicWidth() / 4;
+            }
+        }
+
+        @Override
+        public int getCurrentCursorOffset() {
+            return mTextView.getSelectionEnd();
+        }
+
+        @Override
+        public void updateSelection(int offset) {
+            Selection.setSelection((Spannable) mTextView.getText(),
+                    mTextView.getSelectionStart(), offset);
+            updateDrawable();
+        }
+
+        @Override
+        public void updatePosition(float x, float y) {
+            int offset = mTextView.getOffsetForPosition(x, y);
+
+            // Handles can not cross and selection is at least one character
+            final int selectionStart = mTextView.getSelectionStart();
+            if (offset <= selectionStart) {
+                offset = Math.min(selectionStart + 1, mTextView.getText().length());
+            }
+
+            positionAtCursorOffset(offset, false);
+        }
+
+        public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
+            mActionPopupWindow = actionPopupWindow;
+        }
+    }
+
+    /**
+     * 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();
+    }
+
+    private class InsertionPointCursorController implements CursorController {
+        private InsertionHandleView mHandle;
+
+        public void show() {
+            getHandle().show();
+        }
+
+        public void showWithActionPopup() {
+            getHandle().showWithActionPopup();
+        }
+
+        public void hide() {
+            if (mHandle != null) {
+                mHandle.hide();
+            }
+        }
+
+        public void onTouchModeChanged(boolean isInTouchMode) {
+            if (!isInTouchMode) {
+                hide();
+            }
+        }
+
+        private InsertionHandleView getHandle() {
+            if (mSelectHandleCenter == null) {
+                mSelectHandleCenter = mTextView.getResources().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();
+        }
+    }
+
+    class SelectionModifierCursorController implements CursorController {
+        private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
+        // The cursor controller handles, lazily created when shown.
+        private SelectionStartHandleView mStartHandle;
+        private SelectionEndHandleView mEndHandle;
+        // The offsets of that last touch down event. Remembered to start selection there.
+        private int mMinTouchOffset, mMaxTouchOffset;
+
+        // Double tap detection
+        private long mPreviousTapUpTime = 0;
+        private float mDownPositionX, mDownPositionY;
+        private boolean mGestureStayedInTapRegion;
+
+        SelectionModifierCursorController() {
+            resetTouchOffsets();
+        }
+
+        public void show() {
+            if (mTextView.isInBatchEditMode()) {
+                return;
+            }
+            initDrawables();
+            initHandles();
+            hideInsertionPointCursorController();
+        }
+
+        private void initDrawables() {
+            if (mSelectHandleLeft == null) {
+                mSelectHandleLeft = mTextView.getContext().getResources().getDrawable(
+                        mTextView.mTextSelectHandleLeftRes);
+            }
+            if (mSelectHandleRight == null) {
+                mSelectHandleRight = mTextView.getContext().getResources().getDrawable(
+                        mTextView.mTextSelectHandleRightRes);
+            }
+        }
+
+        private void initHandles() {
+            // Lazy object creation has to be done before updatePosition() is called.
+            if (mStartHandle == null) {
+                mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
+            }
+            if (mEndHandle == null) {
+                mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
+            }
+
+            mStartHandle.show();
+            mEndHandle.show();
+
+            // Make sure both left and right handles share the same ActionPopupWindow (so that
+            // moving any of the handles hides the action popup).
+            mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
+            mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
+
+            hideInsertionPointCursorController();
+        }
+
+        public void hide() {
+            if (mStartHandle != null) mStartHandle.hide();
+            if (mEndHandle != null) mEndHandle.hide();
+        }
+
+        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.
+            switch (event.getActionMasked()) {
+                case MotionEvent.ACTION_DOWN:
+                    final float x = event.getX();
+                    final float y = event.getY();
+
+                    // Remember finger down position, to be able to start selection from there
+                    mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
+
+                    // Double tap detection
+                    if (mGestureStayedInTapRegion) {
+                        long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
+                        if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
+                            final float deltaX = x - mDownPositionX;
+                            final float deltaY = y - 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 && isPositionOnText(x, y)) {
+                                startSelectionActionMode();
+                                mDiscardNextActionUp = true;
+                            }
+                        }
+                    }
+
+                    mDownPositionX = x;
+                    mDownPositionY = y;
+                    mGestureStayedInTapRegion = 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:
+                    if (mGestureStayedInTapRegion) {
+                        final float deltaX = event.getX() - mDownPositionX;
+                        final float deltaY = event.getY() - mDownPositionY;
+                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
+
+                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
+                                mTextView.getContext());
+                        int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
+
+                        if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
+                            mGestureStayedInTapRegion = false;
+                        }
+                    }
+                    break;
+
+                case MotionEvent.ACTION_UP:
+                    mPreviousTapUpTime = SystemClock.uptimeMillis();
+                    break;
+            }
+        }
+
+        /**
+         * @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;
+        }
+
+        /**
+         * @return true iff this controller is currently used to move the selection start.
+         */
+        public boolean isSelectionStartDragged() {
+            return mStartHandle != null && mStartHandle.isDragging();
+        }
+
+        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();
+        }
+    }
+
+    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 final static 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 positionned. 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;
+    }
+
+    static class InputMethodState {
+        Rect mCursorRectInWindow = new Rect();
+        RectF mTmpRectF = new RectF();
+        float[] mTmpOffset = new float[2];
+        ExtractedTextRequest mExtracting;
+        final ExtractedText mTmpExtracted = new ExtractedText();
+        int mBatchEditNesting;
+        boolean mCursorChanged;
+        boolean mSelectionModeChanged;
+        boolean mContentChanged;
+        int mChangedStart, mChangedEnd, mChangedDelta;
+    }
+}
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 4bdb3e2..2a81f08 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -18,11 +18,8 @@
 
 import android.R;
 import android.content.ClipData;
-import android.content.ClipData.Item;
 import android.content.ClipboardManager;
 import android.content.Context;
-import android.content.Intent;
-import android.content.pm.PackageManager;
 import android.content.res.ColorStateList;
 import android.content.res.CompatibilityInfo;
 import android.content.res.Resources;
@@ -43,7 +40,6 @@
 import android.os.Parcel;
 import android.os.Parcelable;
 import android.os.SystemClock;
-import android.provider.Settings;
 import android.text.BoringLayout;
 import android.text.DynamicLayout;
 import android.text.Editable;
@@ -57,7 +53,6 @@
 import android.text.SpanWatcher;
 import android.text.Spannable;
 import android.text.SpannableString;
-import android.text.SpannableStringBuilder;
 import android.text.Spanned;
 import android.text.SpannedString;
 import android.text.StaticLayout;
@@ -86,42 +81,31 @@
 import android.text.method.WordIterator;
 import android.text.style.CharacterStyle;
 import android.text.style.ClickableSpan;
-import android.text.style.EasyEditSpan;
 import android.text.style.ParagraphStyle;
 import android.text.style.SpellCheckSpan;
-import android.text.style.SuggestionRangeSpan;
 import android.text.style.SuggestionSpan;
-import android.text.style.TextAppearanceSpan;
 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.FloatMath;
 import android.util.Log;
 import android.util.TypedValue;
 import android.view.ActionMode;
-import android.view.ActionMode.Callback;
-import android.view.DisplayList;
 import android.view.DragEvent;
 import android.view.Gravity;
 import android.view.HapticFeedbackConstants;
-import android.view.HardwareCanvas;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
-import android.view.LayoutInflater;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewConfiguration;
 import android.view.ViewDebug;
-import android.view.ViewGroup;
 import android.view.ViewGroup.LayoutParams;
-import android.view.ViewParent;
 import android.view.ViewRootImpl;
 import android.view.ViewTreeObserver;
-import android.view.WindowManager;
 import android.view.accessibility.AccessibilityEvent;
 import android.view.accessibility.AccessibilityManager;
 import android.view.accessibility.AccessibilityNodeInfo;
@@ -136,10 +120,8 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.textservice.SpellCheckerSubtype;
 import android.view.textservice.TextServicesManager;
-import android.widget.AdapterView.OnItemClickListener;
 import android.widget.RemoteViews.RemoteView;
 
-import com.android.internal.util.ArrayUtils;
 import com.android.internal.util.FastMath;
 import com.android.internal.widget.EditableInputConnection;
 
@@ -147,11 +129,7 @@
 
 import java.io.IOException;
 import java.lang.ref.WeakReference;
-import java.text.BreakIterator;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashMap;
 import java.util.Locale;
 
 /**
@@ -267,24 +245,21 @@
     private static final int PIXELS = 2;
 
     private static final RectF TEMP_RECTF = new RectF();
-    private static final float[] TEMP_POSITION = new float[2];
 
     // XXX should be much larger
     private static final int VERY_WIDE = 1024*1024;
-    private static final int BLINK = 500;
     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 int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
     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 };
 
     // System wide time for last cut or copy action.
-    private static long LAST_CUT_OR_COPY_TIME;
+    static long LAST_CUT_OR_COPY_TIME;
 
     private int mCurrentAlpha = 255;
 
@@ -316,7 +291,7 @@
                 mDrawableHeightStart, mDrawableHeightEnd;
         int mDrawablePadding;
     }
-    private Drawables mDrawables;
+    Drawables mDrawables;
 
     private CharWrapper mCharWrapper;
 
@@ -404,23 +379,23 @@
 
     // 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.
-    private int mHighlightColor = 0x6633B5E5;
+    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.
-    private int mCursorDrawableRes;
+    int mCursorDrawableRes;
     // These four 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.
-    private int mTextSelectHandleLeftRes;
-    private int mTextSelectHandleRightRes;
-    private int mTextSelectHandleRes;
-    private int mTextEditSuggestionItemLayout;
+    int mTextSelectHandleLeftRes;
+    int mTextSelectHandleRightRes;
+    int mTextSelectHandleRes;
+    int mTextEditSuggestionItemLayout;
 
     /**
      * EditText specific data, created on demand when one of the Editor fields is used.
@@ -826,26 +801,20 @@
 
             case com.android.internal.R.styleable.TextView_imeOptions:
                 createEditorIfNeeded("IME options specified in constructor");
-                if (getEditor().mInputContentType == null) {
-                    getEditor().mInputContentType = new InputContentType();
-                }
+                getEditor().createInputContentTypeIfNeeded();
                 getEditor().mInputContentType.imeOptions = a.getInt(attr,
                         getEditor().mInputContentType.imeOptions);
                 break;
 
             case com.android.internal.R.styleable.TextView_imeActionLabel:
                 createEditorIfNeeded("IME action label specified in constructor");
-                if (getEditor().mInputContentType == null) {
-                    getEditor().mInputContentType = new InputContentType();
-                }
+                getEditor().createInputContentTypeIfNeeded();
                 getEditor().mInputContentType.imeActionLabel = a.getText(attr);
                 break;
 
             case com.android.internal.R.styleable.TextView_imeActionId:
                 createEditorIfNeeded("IME action id specified in constructor");
-                if (getEditor().mInputContentType == null) {
-                    getEditor().mInputContentType = new InputContentType();
-                }
+                getEditor().createInputContentTypeIfNeeded();
                 getEditor().mInputContentType.imeActionId = a.getInt(attr,
                         getEditor().mInputContentType.imeActionId);
                 break;
@@ -1135,7 +1104,7 @@
         setClickable(clickable);
         setLongClickable(longClickable);
 
-        prepareCursorControllers();
+        if (mEditor != null) mEditor.prepareCursorControllers();
     }
 
     private void setTypefaceByIndex(int typefaceIndex, int styleIndex) {
@@ -1216,11 +1185,13 @@
         }
 
         // Will change text color
-        if (mEditor != null) getEditor().invalidateTextDisplayList();
-        prepareCursorControllers();
+        if (mEditor != null) {
+            getEditor().invalidateTextDisplayList();
+            getEditor().prepareCursorControllers();
 
-        // start or stop the cursor blinking as appropriate
-        makeBlink();
+            // start or stop the cursor blinking as appropriate
+            getEditor().makeBlink();
+        }
     }
 
     /**
@@ -1412,7 +1383,7 @@
             fixFocusableAndClickableSettings();
 
             // SelectionModifierCursorController depends on textCanBeSelected, which depends on mMovement
-            prepareCursorControllers();
+            if (mEditor != null) getEditor().prepareCursorControllers();
         }
     }
 
@@ -3250,7 +3221,7 @@
         }
 
         // SelectionModifierCursorController depends on textCanBeSelected, which depends on text
-        prepareCursorControllers();
+        if (mEditor != null) getEditor().prepareCursorControllers();
     }
 
     /**
@@ -3366,12 +3337,37 @@
         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 = new SpannableString(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}
@@ -3543,9 +3539,7 @@
      */
     public void setImeOptions(int imeOptions) {
         createEditorIfNeeded("IME options specified");
-        if (getEditor().mInputContentType == null) {
-            getEditor().mInputContentType = new InputContentType();
-        }
+        getEditor().createInputContentTypeIfNeeded();
         getEditor().mInputContentType.imeOptions = imeOptions;
     }
 
@@ -3572,9 +3566,7 @@
      */
     public void setImeActionLabel(CharSequence label, int actionId) {
         createEditorIfNeeded("IME action label specified");
-        if (getEditor().mInputContentType == null) {
-            getEditor().mInputContentType = new InputContentType();
-        }
+        getEditor().createInputContentTypeIfNeeded();
         getEditor().mInputContentType.imeActionLabel = label;
         getEditor().mInputContentType.imeActionId = actionId;
     }
@@ -3611,9 +3603,7 @@
      */
     public void setOnEditorActionListener(OnEditorActionListener l) {
         createEditorIfNeeded("Editor action listener set");
-        if (getEditor().mInputContentType == null) {
-            getEditor().mInputContentType = new InputContentType();
-        }
+        getEditor().createInputContentTypeIfNeeded();
         getEditor().mInputContentType.onEditorActionListener = l;
     }
 
@@ -3638,7 +3628,7 @@
      * @see #setOnEditorActionListener
      */
     public void onEditorAction(int actionCode) {
-        final InputContentType ict = mEditor == null ? null : getEditor().mInputContentType;
+        final Editor.InputContentType ict = mEditor == null ? null : getEditor().mInputContentType;
         if (ict != null) {
             if (ict.onEditorActionListener != null) {
                 if (ict.onEditorActionListener.onEditorAction(this,
@@ -3710,8 +3700,7 @@
      */
     public void setPrivateImeOptions(String type) {
         createEditorIfNeeded("Private IME option set");
-        if (getEditor().mInputContentType == null)
-            getEditor().mInputContentType = new InputContentType();
+        getEditor().createInputContentTypeIfNeeded();
         getEditor().mInputContentType.privateImeOptions = type;
     }
 
@@ -3740,8 +3729,7 @@
     public void setInputExtras(int xmlResId) throws XmlPullParserException, IOException {
         createEditorIfNeeded("Input extra set");
         XmlResourceParser parser = getResources().getXml(xmlResId);
-        if (getEditor().mInputContentType == null)
-            getEditor().mInputContentType = new InputContentType();
+        getEditor().createInputContentTypeIfNeeded();
         getEditor().mInputContentType.extras = new Bundle();
         getResources().parseBundleExtras(parser, getEditor().mInputContentType.extras);
     }
@@ -3761,7 +3749,7 @@
         createEditorIfNeeded("get Input extra");
         if (getEditor().mInputContentType == null) {
             if (!create) return null;
-            getEditor().mInputContentType = new InputContentType();
+            getEditor().createInputContentTypeIfNeeded();
         }
         if (getEditor().mInputContentType.extras == null) {
             if (!create) return null;
@@ -3811,142 +3799,9 @@
      */
     public void setError(CharSequence error, Drawable icon) {
         createEditorIfNeeded("setError");
-        error = TextUtils.stringOrSpannedString(error);
-
-        getEditor().mError = error;
-        getEditor().mErrorWasChanged = true;
-        final Drawables dr = mDrawables;
-        if (dr != null) {
-            switch (getResolvedLayoutDirection()) {
-                default:
-                case LAYOUT_DIRECTION_LTR:
-                    setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
-                            dr.mDrawableBottom);
-                    break;
-                case LAYOUT_DIRECTION_RTL:
-                    setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
-                            dr.mDrawableBottom);
-                    break;
-            }
-        } else {
-            setCompoundDrawables(null, null, icon, null);
-        }
-
-        if (error == null) {
-            if (getEditor().mErrorPopup != null) {
-                if (getEditor().mErrorPopup.isShowing()) {
-                    getEditor().mErrorPopup.dismiss();
-                }
-
-                getEditor().mErrorPopup = null;
-            }
-        } else {
-            if (isFocused()) {
-                showError();
-            }
-        }
+        getEditor().setError(error, icon);
     }
 
-    private void showError() {
-        if (getWindowToken() == null) {
-            getEditor().mShowErrorAfterAttach = true;
-            return;
-        }
-
-        if (getEditor().mErrorPopup == null) {
-            LayoutInflater inflater = LayoutInflater.from(getContext());
-            final TextView err = (TextView) inflater.inflate(
-                    com.android.internal.R.layout.textview_hint, null);
-
-            final float scale = getResources().getDisplayMetrics().density;
-            getEditor().mErrorPopup = new ErrorPopup(err, (int) (200 * scale + 0.5f), (int) (50 * scale + 0.5f));
-            getEditor().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.
-            getEditor().mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
-        }
-
-        TextView tv = (TextView) getEditor().mErrorPopup.getContentView();
-        chooseSize(getEditor().mErrorPopup, getEditor().mError, tv);
-        tv.setText(getEditor().mError);
-
-        getEditor().mErrorPopup.showAsDropDown(this, getErrorX(), getErrorY());
-        getEditor().mErrorPopup.fixDirection(getEditor().mErrorPopup.isAboveAnchor());
-    }
-
-    /**
-     * Returns the Y 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 = getResources().getDisplayMetrics().density;
-
-        final Drawables dr = mDrawables;
-        return getWidth() - getEditor().mErrorPopup.getWidth() - getPaddingRight() -
-                (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
-    }
-
-    /**
-     * 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 = getCompoundPaddingTop();
-        int vspace = mBottom - mTop - getCompoundPaddingBottom() - compoundPaddingTop;
-
-        final Drawables dr = mDrawables;
-        int icontop = compoundPaddingTop +
-                (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
-
-        /*
-         * The "2" is the distance between the point and the top edge
-         * of the background.
-         */
-        final float scale = getResources().getDisplayMetrics().density;
-        return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - getHeight() -
-                (int) (2 * scale + 0.5f);
-    }
-
-    private void hideError() {
-        if (getEditor().mErrorPopup != null) {
-            if (getEditor().mErrorPopup.isShowing()) {
-                getEditor().mErrorPopup.dismiss();
-            }
-        }
-
-        getEditor().mShowErrorAfterAttach = false;
-    }
-
-    private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
-        int wid = tv.getPaddingLeft() + tv.getPaddingRight();
-        int ht = tv.getPaddingTop() + tv.getPaddingBottom();
-
-        int defaultWidthInPixels = getResources().getDimensionPixelSize(
-                com.android.internal.R.dimen.textview_error_popup_default_width);
-        Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
-                                    Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
-        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());
-    }
-
-
     @Override
     protected boolean setFrame(int l, int t, int r, int b) {
         boolean result = super.setFrame(l, t, r, b);
@@ -4009,7 +3864,7 @@
 
     /////////////////////////////////////////////////////////////////////////
 
-    private int getVerticalOffset(boolean forceNormal) {
+    int getVerticalOffset(boolean forceNormal) {
         int voffset = 0;
         final int gravity = mGravity & Gravity.VERTICAL_GRAVITY_MASK;
 
@@ -4071,7 +3926,7 @@
         return voffset;
     }
 
-    private void invalidateCursorPath() {
+    void invalidateCursorPath() {
         if (mHighlightPathBogus) {
             invalidateCursor();
         } else {
@@ -4114,7 +3969,7 @@
         }
     }
 
-    private void invalidateCursor() {
+    void invalidateCursor() {
         int where = getSelectionEnd();
 
         invalidateCursor(where, where, where);
@@ -4130,8 +3985,6 @@
 
     /**
      * Invalidates the region of text enclosed between the start and end text offsets.
-     *
-     * @hide
      */
     void invalidateRegion(int start, int end, boolean invalidateCursor) {
         if (mLayout == null) {
@@ -4237,15 +4090,15 @@
         // - 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 && getEditor().mCreatedWithASelection) {
-            startSelectionActionMode();
+            getEditor().startSelectionActionMode();
             getEditor().mCreatedWithASelection = false;
         }
 
         // Phone specific code (there is no ExtractEditText on tablets).
         // ExtractEditText does not call onFocus when it is displayed, and mHasSelectionOnFocus can
         // not be set. Do the test here instead.
-        if (this instanceof ExtractEditText && hasSelection()) {
-            startSelectionActionMode();
+        if (this instanceof ExtractEditText && hasSelection() && mEditor != null) {
+            getEditor().startSelectionActionMode();
         }
 
         getViewTreeObserver().removeOnPreDrawListener(this);
@@ -4260,11 +4113,6 @@
 
         mTemporaryDetach = false;
         
-        if (mEditor != null && getEditor().mShowErrorAfterAttach) {
-            showError();
-            getEditor().mShowErrorAfterAttach = false;
-        }
-
         // Resolve drawables as the layout direction has been resolved
         resolveDrawables();
 
@@ -4495,7 +4343,7 @@
         setText(getText(), selectable ? BufferType.SPANNABLE : BufferType.NORMAL);
 
         // Called by setText above, but safer in case of future code changes
-        prepareCursorControllers();
+        getEditor().prepareCursorControllers();
     }
 
     @Override
@@ -4536,8 +4384,9 @@
         final int selEnd = getSelectionEnd();
         if (mMovement != null && (isFocused() || isPressed()) && selStart >= 0) {
             if (selStart == selEnd) {
-                if (mEditor != null && isCursorVisible() &&
-                        (SystemClock.uptimeMillis() - getEditor().mShowCursor) % (2 * BLINK) < BLINK) {
+                if (mEditor != null && getEditor().isCursorVisible() &&
+                        (SystemClock.uptimeMillis() - getEditor().mShowCursor) %
+                        (2 * Editor.BLINK) < Editor.BLINK) {
                     if (mHighlightPathBogus) {
                         if (mHighlightPath == null) mHighlightPath = new Path();
                         mHighlightPath.reset();
@@ -4730,14 +4579,14 @@
 
         Path highlight = getUpdatedHighlightPath();
         if (mEditor != null) {
-            getEditor().onDraw(canvas, layout, highlight, cursorOffsetVertical);
+            getEditor().onDraw(canvas, layout, highlight, mHighlightPaint, cursorOffsetVertical);
         } else {
             layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
+        }
 
-            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
-                canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
-                layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
-            }
+        if (mMarquee != null && mMarquee.shouldDrawGhost()) {
+            canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
+            layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
         }
 
         canvas.restore();
@@ -4853,7 +4702,6 @@
 
     /**
      * @hide
-     * @param offsetRequired
      */
     @Override
     protected int getFadeTop(boolean offsetRequired) {
@@ -4871,7 +4719,6 @@
 
     /**
      * @hide
-     * @param offsetRequired
      */
     @Override
     protected int getFadeHeight(boolean offsetRequired) {
@@ -5252,9 +5099,7 @@
     @Override
     public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
         if (onCheckIsTextEditor() && isEnabled()) {
-            if (getEditor().mInputMethodState == null) {
-                getEditor().mInputMethodState = new InputMethodState();
-            }
+            getEditor().createInputMethodStateIfNeeded();
             outAttrs.inputType = getInputType();
             if (getEditor().mInputContentType != null) {
                 outAttrs.imeOptions = getEditor().mInputContentType.imeOptions;
@@ -5307,122 +5152,11 @@
      * 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) {
-        return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
-                EXTRACT_UNKNOWN, outText);
+    public boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
+        createEditorIfNeeded("extractText");
+        return getEditor().extractText(request, outText);
     }
-    
-    static final int EXTRACT_NOTHING = -2;
-    static final int EXTRACT_UNKNOWN = -1;
-    
-    boolean extractTextInternal(ExtractedTextRequest request,
-            int partialStartOffset, int partialEndOffset, int delta,
-            ExtractedText outText) {
-        final CharSequence content = mText;
-        if (content != null) {
-            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(mText, MetaKeyKeyListener.META_SELECTING) != 0) {
-                outText.flags |= ExtractedText.FLAG_SELECTING;
-            }
-            if (mSingleLine) {
-                outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
-            }
-            outText.startOffset = 0;
-            outText.selectionStart = getSelectionStart();
-            outText.selectionEnd = getSelectionEnd();
-            return true;
-        }
-        return false;
-    }
-    
-    boolean reportExtractedText() {
-        final InputMethodState ims = getEditor().mInputMethodState;
-        if (ims != null) {
-            final boolean contentChanged = ims.mContentChanged;
-            if (contentChanged || ims.mSelectionModeChanged) {
-                ims.mContentChanged = false;
-                ims.mSelectionModeChanged = false;
-                final ExtractedTextRequest req = ims.mExtracting;
-                if (req != null) {
-                    InputMethodManager imm = InputMethodManager.peekInstance();
-                    if (imm != null) {
-                        if (DEBUG_EXTRACT) Log.v(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.mTmpExtracted)) {
-                            if (DEBUG_EXTRACT) Log.v(LOG_TAG, "Reporting extracted start="
-                                    + ims.mTmpExtracted.partialStartOffset
-                                    + " end=" + ims.mTmpExtracted.partialEndOffset
-                                    + ": " + ims.mTmpExtracted.text);
-                            imm.updateExtractedText(this, req.token, ims.mTmpExtracted);
-                            ims.mChangedStart = EXTRACT_UNKNOWN;
-                            ims.mChangedEnd = EXTRACT_UNKNOWN;
-                            ims.mChangedDelta = 0;
-                            ims.mContentChanged = false;
-                            return true;
-                        }
-                    }
-                }
-            }
-        }
-        return false;
-    }
-    
     /**
      * 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
@@ -5436,7 +5170,7 @@
             spannable.removeSpan(spans[i]);
         }
     }
-    
+
     /**
      * Apply to this text view the given extracted text, as previously
      * returned by {@link #extractText(ExtractedTextRequest, ExtractedText)}.
@@ -5492,7 +5226,7 @@
         // 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.
-        hideControllers();
+        getEditor().hideControllers();
     }
 
     /**
@@ -5518,87 +5252,15 @@
      * @param info The auto correct info about the text that was corrected.
      */
     public void onCommitCorrection(CorrectionInfo info) {
-        if (mEditor == null) return;
-        if (getEditor().mCorrectionHighlighter == null) {
-            getEditor().mCorrectionHighlighter = new CorrectionHighlighter();
-        } else {
-            getEditor().mCorrectionHighlighter.invalidate(false);
-        }
-
-        getEditor().mCorrectionHighlighter.highlight(info);
+        if (mEditor != null) getEditor().onCommitCorrection(info);
     }
 
     public void beginBatchEdit() {
-        if (mEditor == null) return;
-        getEditor().mInBatchEditControllers = true;
-        final InputMethodState ims = getEditor().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 = mText.length();
-                } else {
-                    ims.mChangedStart = EXTRACT_UNKNOWN;
-                    ims.mChangedEnd = EXTRACT_UNKNOWN;
-                    ims.mContentChanged = false;
-                }
-                onBeginBatchEdit();
-            }
-        }
+        if (mEditor != null) getEditor().beginBatchEdit();
     }
     
     public void endBatchEdit() {
-        if (mEditor == null) return;
-        getEditor().mInBatchEditControllers = false;
-        final InputMethodState ims = getEditor().mInputMethodState;
-        if (ims != null) {
-            int nesting = --ims.mBatchEditNesting;
-            if (nesting == 0) {
-                finishBatchEdit(ims);
-            }
-        }
-    }
-    
-    void ensureEndedBatchEdit() {
-        final InputMethodState ims = getEditor().mInputMethodState;
-        if (ims != null && ims.mBatchEditNesting != 0) {
-            ims.mBatchEditNesting = 0;
-            finishBatchEdit(ims);
-        }
-    }
-    
-    void finishBatchEdit(final InputMethodState ims) {
-        onEndBatchEdit();
-        
-        if (ims.mContentChanged || ims.mSelectionModeChanged) {
-            updateAfterEdit();
-            reportExtractedText();
-        } else if (ims.mCursorChanged) {
-            // Cheezy way to get us to report the current cursor location.
-            invalidateCursor();
-        }
-    }
-    
-    void updateAfterEdit() {
-        invalidate();
-        int curs = getSelectionStart();
-
-        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
-            registerForPreDraw();
-        }
-
-        if (curs >= 0) {
-            mHighlightPathBogus = true;
-            makeBlink();
-            bringPointIntoView(curs);
-        }
-
-        checkForResize();
+        if (mEditor != null) getEditor().endBatchEdit();
     }
     
     /**
@@ -5644,7 +5306,7 @@
         mBoring = mHintBoring = null;
 
         // Since it depends on the value of mLayout
-        prepareCursorControllers();
+        if (mEditor != null) getEditor().prepareCursorControllers();
     }
 
     /**
@@ -5826,7 +5488,7 @@
         }
 
         // CursorControllers need a non-null mLayout
-        prepareCursorControllers();
+        if (mEditor != null) getEditor().prepareCursorControllers();
     }
 
     private Layout makeSingleLayout(int wantWidth, BoringLayout.Metrics boring, int ellipsisWidth,
@@ -6638,11 +6300,11 @@
         r.bottom += verticalOffset;
     }
 
-    private int viewportToContentHorizontalOffset() {
+    int viewportToContentHorizontalOffset() {
         return getCompoundPaddingLeft() - mScrollX;
     }
 
-    private int viewportToContentVerticalOffset() {
+    int viewportToContentVerticalOffset() {
         int offset = getExtendedPaddingTop() - mScrollY;
         if ((mGravity & Gravity.VERTICAL_GRAVITY_MASK) != Gravity.TOP) {
             offset += getVerticalOffset(false);
@@ -6855,18 +6517,13 @@
             getEditor().mCursorVisible = visible;
             invalidate();
 
-            makeBlink();
+            getEditor().makeBlink();
 
             // InsertionPointCursorController depends on mCursorVisible
-            prepareCursorControllers();
+            getEditor().prepareCursorControllers();
         }
     }
 
-    private boolean isCursorVisible() {
-        // The default value is true, even when there is no associated Editor
-        return mEditor == null ? true : (getEditor().mCursorVisible && isTextEditable());
-    }
-
     private boolean canMarquee() {
         int width = (mRight - mLeft - getCompoundPaddingLeft() - getCompoundPaddingRight());
         return width > 0 && (mLayout.getLineWidth(0) > width ||
@@ -7049,12 +6706,29 @@
         }
     }
 
+    void updateAfterEdit() {
+        invalidate();
+        int curs = getSelectionStart();
+
+        if (curs >= 0 || (mGravity & Gravity.VERTICAL_GRAVITY_MASK) == Gravity.BOTTOM) {
+            registerForPreDraw();
+        }
+
+        if (curs >= 0) {
+            mHighlightPathBogus = true;
+            if (mEditor != null) getEditor().makeBlink();
+            bringPointIntoView(curs);
+        }
+
+        checkForResize();
+    }
+
     /**
      * 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) {
-        final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
+        final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
         if (ims == null || ims.mBatchEditNesting == 0) {
             updateAfterEdit();
         }
@@ -7085,7 +6759,7 @@
         boolean selChanged = false;
         int newSelStart=-1, newSelEnd=-1;
 
-        final InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
+        final Editor.InputMethodState ims = mEditor == null ? null : getEditor().mInputMethodState;
 
         if (what == Selection.SELECTION_END) {
             selChanged = true;
@@ -7094,7 +6768,7 @@
             if (oldStart >= 0 || newStart >= 0) {
                 invalidateCursor(Selection.getSelectionStart(buf), oldStart, newStart);
                 registerForPreDraw();
-                makeBlink();
+                if (mEditor != null) getEditor().makeBlink();
             }
         }
 
@@ -7186,20 +6860,6 @@
     }
 
     /**
-     * Create new SpellCheckSpans on the modified region.
-     */
-    private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
-        if (isTextEditable() && isSuggestionsEnabled() && !(this instanceof ExtractEditText)) {
-            if (getEditor().mSpellChecker == null && createSpellChecker) {
-                getEditor().mSpellChecker = new SpellChecker(this);
-            }
-            if (getEditor().mSpellChecker != null) {
-                getEditor().mSpellChecker.spellCheck(start, end);
-            }
-        }
-    }
-
-    /**
      * @hide
      */
     @Override
@@ -7219,7 +6879,7 @@
         // Because of View recycling in ListView, there is no easy way to know when a TextView with
         // selection becomes visible again. Until a better solution is found, stop text selection
         // mode (if any) as soon as this TextView is recycled.
-        if (mEditor != null) hideControllers();
+        if (mEditor != null) getEditor().hideControllers();
     }
 
     @Override
@@ -7269,7 +6929,7 @@
     protected void onVisibilityChanged(View changedView, int visibility) {
         super.onVisibilityChanged(changedView, visibility);
         if (mEditor != null && visibility != VISIBLE) {
-            hideControllers();
+            getEditor().hideControllers();
         }
     }
 
@@ -7350,31 +7010,8 @@
                     handled |= imm != null && imm.showSoftInput(this, 0);
                 }
 
-                boolean selectAllGotFocus = getEditor().mSelectAllOnFocus && didTouchFocusSelect();
-                hideControllers();
-                if (!selectAllGotFocus && mText.length() > 0) {
-                    // Move cursor
-                    final int offset = getOffsetForPosition(event.getX(), event.getY());
-                    Selection.setSelection((Spannable) mText, offset);
-                    if (getEditor().mSpellChecker != null) {
-                        // When the cursor moves, the word that was typed may need spell check
-                        getEditor().mSpellChecker.onSelectionChanged();
-                    }
-                    if (!extractedTextModeWillBeStarted()) {
-                        if (isCursorInsideEasyCorrectionSpan()) {
-                            getEditor().mShowSuggestionRunnable = new Runnable() {
-                                public void run() {
-                                    showSuggestions();
-                                }
-                            };
-                            // removeCallbacks is performed on every touch
-                            postDelayed(getEditor().mShowSuggestionRunnable,
-                                    ViewConfiguration.getDoubleTapTimeout());
-                        } else if (hasInsertionController()) {
-                            getInsertionController().show();
-                        }
-                    }
-                }
+                // The above condition ensures that the mEditor is not null
+                getEditor().onTouchUpEvent(event);
 
                 handled = true;
             }
@@ -7387,53 +7024,6 @@
         return superResult;
     }
 
-    /**
-     * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
-     */
-    private boolean isCursorInsideSuggestionSpan() {
-        if (!(mText instanceof Spannable)) return false;
-
-        SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(),
-                getSelectionEnd(), SuggestionSpan.class);
-        return (suggestionSpans.length > 0);
-    }
-
-    /**
-     * @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) mText;
-        SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(),
-                getSelectionEnd(), SuggestionSpan.class);
-        for (int i = 0; i < suggestionSpans.length; i++) {
-            if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
-                return true;
-            }
-        }
-        return false;
-    }
-
-    /**
-     * Downgrades to simple suggestions all the easy correction spans that are not a spell check
-     * span.
-     */
-    private void downgradeEasyCorrectionSpans() {
-        if (mText instanceof Spannable) {
-            Spannable spannable = (Spannable) mText;
-            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);
-                }
-            }
-        }
-    }
-
     @Override
     public boolean onGenericMotionEvent(MotionEvent event) {
         if (mMovement != null && mText instanceof Spannable && mLayout != null) {
@@ -7450,44 +7040,11 @@
         return super.onGenericMotionEvent(event);
     }
 
-    private void prepareCursorControllers() {
-        if (mEditor == null) return;
-
-        boolean windowSupportsHandles = false;
-
-        ViewGroup.LayoutParams params = 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;
-        }
-
-        getEditor().mInsertionControllerEnabled = windowSupportsHandles && isCursorVisible() && mLayout != null;
-        getEditor().mSelectionControllerEnabled = windowSupportsHandles && textCanBeSelected() &&
-                mLayout != null;
-
-        if (!getEditor().mInsertionControllerEnabled) {
-            hideInsertionPointCursorController();
-            if (getEditor().mInsertionPointCursorController != null) {
-                getEditor().mInsertionPointCursorController.onDetached();
-                getEditor().mInsertionPointCursorController = null;
-            }
-        }
-
-        if (!getEditor().mSelectionControllerEnabled) {
-            stopSelectionActionMode();
-            if (getEditor().mSelectionModifierCursorController != null) {
-                getEditor().mSelectionModifierCursorController.onDetached();
-                getEditor().mSelectionModifierCursorController = null;
-            }
-        }
-    }
-
     /**
      * @return True iff this TextView contains a text that can be edited, or if this is
      * a selectable TextView.
      */
-    private boolean isTextEditable() {
+    boolean isTextEditable() {
         return mText instanceof Editable && onCheckIsTextEditor() && isEnabled();
     }
 
@@ -7522,32 +7079,6 @@
         mScroller = s;
     }
 
-    /**
-     * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
-     */
-    private boolean shouldBlink() {
-        if (mEditor == null || !isCursorVisible() || !isFocused()) return false;
-
-        final int start = getSelectionStart();
-        if (start < 0) return false;
-
-        final int end = getSelectionEnd();
-        if (end < 0) return false;
-
-        return start == end;
-    }
-
-    private void makeBlink() {
-        if (shouldBlink()) {
-            getEditor().mShowCursor = SystemClock.uptimeMillis();
-            if (getEditor().mBlink == null) getEditor().mBlink = new Blink(this);
-            getEditor().mBlink.removeCallbacks(getEditor().mBlink);
-            getEditor().mBlink.postAtTime(getEditor().mBlink, getEditor().mShowCursor + BLINK);
-        } else {
-            if (mEditor != null && getEditor().mBlink != null) getEditor().mBlink.removeCallbacks(getEditor().mBlink);
-        }
-    }
-
     @Override
     protected float getLeftFadingEdgeStrength() {
         if (mCurrentAlpha <= ViewConfiguration.ALPHA_THRESHOLD_INT) return 0.0f;
@@ -7726,10 +7257,10 @@
     /**
      * 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 #prepareCursorControllers()}), but this is not sufficient.
+     * a selection controller (see {@link Editor#prepareCursorControllers()}), but this is not sufficient.
      */
     private boolean canSelectText() {
-        return hasSelectionController() && mText.length() != 0;
+        return mText.length() != 0 && mEditor != null && getEditor().hasSelectionController();
     }
 
     /**
@@ -7738,7 +7269,7 @@
      * 
      * See also {@link #canSelectText()}.
      */
-    private boolean textCanBeSelected() {
+    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.
@@ -7746,112 +7277,6 @@
         return isTextEditable() || (isTextSelectable() && mText instanceof Spannable && isEnabled());
     }
 
-    private boolean canCut() {
-        if (hasPasswordTransformationMethod()) {
-            return false;
-        }
-
-        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
-            return true;
-        }
-
-        return false;
-    }
-
-    private boolean canCopy() {
-        if (hasPasswordTransformationMethod()) {
-            return false;
-        }
-
-        if (mText.length() > 0 && hasSelection()) {
-            return true;
-        }
-
-        return false;
-    }
-
-    private boolean canPaste() {
-        return (mText instanceof Editable &&
-                mEditor != null && getEditor().mKeyListener != null &&
-                getSelectionStart() >= 0 &&
-                getSelectionEnd() >= 0 &&
-                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
-                hasPrimaryClip());
-    }
-
-    private boolean selectAll() {
-        final int length = mText.length();
-        Selection.setSelection((Spannable) mText, 0, length);
-        return length > 0;
-    }
-
-    /**
-     * Adjusts selection to the word under last touch offset.
-     * Return true if the operation was successfully performed.
-     */
-    private boolean selectCurrentWord() {
-        if (!canSelectText()) {
-            return false;
-        }
-
-        if (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 selectAll();
-        }
-
-        int inputType = 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 selectAll();
-        }
-
-        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 >= mText.length()) return false;
-        if (maxOffset < 0 || maxOffset >= mText.length()) return false;
-
-        int selectionStart, selectionEnd;
-
-        // If a URLSpan (web address, email, phone...) is found at that position, select it.
-        URLSpan[] urlSpans = ((Spanned) mText).getSpans(minOffset, maxOffset, URLSpan.class);
-        if (urlSpans.length >= 1) {
-            URLSpan urlSpan = urlSpans[0];
-            selectionStart = ((Spanned) mText).getSpanStart(urlSpan);
-            selectionEnd = ((Spanned) mText).getSpanEnd(urlSpan);
-        } else {
-            final WordIterator wordIterator = getWordIterator();
-            wordIterator.setCharSequence(mText, 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 = getCharRange(minOffset);
-                selectionStart = TextUtils.unpackRangeStartFromLong(range);
-                selectionEnd = TextUtils.unpackRangeEndFromLong(range);
-            }
-        }
-
-        Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
-        return selectionEnd > selectionStart;
-    }
-
     /**
      * This is a temporary method. Future versions may support multi-locale text.
      *
@@ -7877,45 +7302,16 @@
     }
 
     /**
+     * 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 (getEditor().mWordIterator == null) {
-            getEditor().mWordIterator = new WordIterator(getTextServicesLocale());
+        if (getEditor() != null) {
+            return mEditor.getWordIterator(); 
+        } else {
+            return null;
         }
-        return getEditor().mWordIterator;
-    }
-
-    private long getCharRange(int offset) {
-        final int textLength = mText.length();
-        if (offset + 1 < textLength) {
-            final char currentChar = mText.charAt(offset);
-            final char nextChar = mText.charAt(offset + 1);
-            if (Character.isSurrogatePair(currentChar, nextChar)) {
-                return TextUtils.packRangeInLong(offset,  offset + 2);
-            }
-        }
-        if (offset < textLength) {
-            return TextUtils.packRangeInLong(offset,  offset + 1);
-        }
-        if (offset - 2 >= 0) {
-            final char previousChar = mText.charAt(offset - 1);
-            final char previousPreviousChar = mText.charAt(offset - 2);
-            if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
-                return TextUtils.packRangeInLong(offset - 2,  offset);
-            }
-        }
-        if (offset - 1 >= 0) {
-            return TextUtils.packRangeInLong(offset - 1,  offset);
-        }
-        return TextUtils.packRangeInLong(offset,  offset);
-    }
-
-    private long getLastTouchOffsets() {
-        SelectionModifierCursorController selectionController = getSelectionController();
-        final int minOffset = selectionController.getMinTouchOffset();
-        final int maxOffset = selectionController.getMaxTouchOffset();
-        return TextUtils.packRangeInLong(minOffset, maxOffset);
     }
 
     @Override
@@ -8004,11 +7400,10 @@
         return imm != null && imm.isActive(this);
     }
     
-    // Selection context mode
-    private static final int ID_SELECT_ALL = android.R.id.selectAll;
-    private static final int ID_CUT = android.R.id.cut;
-    private static final int ID_COPY = android.R.id.copy;
-    private static final int ID_PASTE = android.R.id.paste;
+    static final int ID_SELECT_ALL = android.R.id.selectAll;
+    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;
 
     /**
      * Called when a context menu option for the text view is selected.  Currently
@@ -8033,7 +7428,7 @@
             case ID_SELECT_ALL:
                 // This does not enter text selection mode. Text is highlighted, so that it can be
                 // bulk edited, like selectAllOnFocus does. Returns true even if text is empty.
-                selectAll();
+                selectAllText();
                 return true;
 
             case ID_PASTE:
@@ -8054,89 +7449,10 @@
         return false;
     }
 
-    private CharSequence getTransformedText(int start, int end) {
+    CharSequence getTransformedText(int start, int end) {
         return removeSuggestionSpans(mTransformed.subSequence(start, end));
     }
 
-    /**
-     * Prepare text so that there are not zero or two spaces at beginning and end of region defined
-     * by [min, max] when replacing this region by paste.
-     * Note that if there were two spaces (or more) at that position before, they are kept. We just
-     * make sure we do not add an extra one from the paste content.
-     */
-    private long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
-        if (paste.length() > 0) {
-            if (min > 0) {
-                final char charBefore = mTransformed.charAt(min - 1);
-                final char charAfter = paste.charAt(0);
-
-                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
-                    // Two spaces at beginning of paste: remove one
-                    final int originalLength = mText.length();
-                    deleteText_internal(min - 1, min);
-                    // Due to filters, there is no guarantee that exactly one character was
-                    // removed: count instead.
-                    final int delta = mText.length() - originalLength;
-                    min += delta;
-                    max += delta;
-                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
-                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
-                    // No space at beginning of paste: add one
-                    final int originalLength = mText.length();
-                    replaceText_internal(min, min, " ");
-                    // Taking possible filters into account as above.
-                    final int delta = mText.length() - originalLength;
-                    min += delta;
-                    max += delta;
-                }
-            }
-
-            if (max < mText.length()) {
-                final char charBefore = paste.charAt(paste.length() - 1);
-                final char charAfter = mTransformed.charAt(max);
-
-                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
-                    // Two spaces at end of paste: remove one
-                    deleteText_internal(max, max + 1);
-                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
-                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
-                    // No space at end of paste: add one
-                    replaceText_internal(max, max, " ");
-                }
-            }
-        }
-
-        return TextUtils.packRangeInLong(min, max);
-    }
-
-    private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
-        TextView shadowView = (TextView) inflate(mContext,
-                com.android.internal.R.layout.text_drag_thumbnail, null);
-
-        if (shadowView == null) {
-            throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
-        }
-
-        if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
-            text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
-        }
-        shadowView.setText(text);
-        shadowView.setTextColor(getTextColors());
-
-        shadowView.setTextAppearance(mContext, 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);
-    }
-
     @Override
     public boolean performLongClick() {
         boolean handled = false;
@@ -8145,179 +7461,27 @@
             handled = true;
         }
 
-        if (mEditor == null) {
-            return handled;
-        }
-
-        // Long press in empty space moves cursor and shows the Paste affordance if available.
-        if (!handled && !isPositionOnText(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY) &&
-                getEditor().mInsertionControllerEnabled) {
-            final int offset = getOffsetForPosition(getEditor().mLastDownPositionX, getEditor().mLastDownPositionY);
-            stopSelectionActionMode();
-            Selection.setSelection((Spannable) mText, offset);
-            getInsertionController().showWithActionPopup();
-            handled = true;
-        }
-
-        if (!handled && getEditor().mSelectionActionMode != null) {
-            if (touchPositionIsInSelection()) {
-                // Start a drag
-                final int start = getSelectionStart();
-                final int end = getSelectionEnd();
-                CharSequence selectedText = getTransformedText(start, end);
-                ClipData data = ClipData.newPlainText(null, selectedText);
-                DragLocalState localState = new DragLocalState(this, start, end);
-                startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
-                stopSelectionActionMode();
-            } else {
-                getSelectionController().hide();
-                selectCurrentWord();
-                getSelectionController().show();
-            }
-            handled = true;
-        }
-
-        // Start a new selection
-        if (!handled) {
-            handled = startSelectionActionMode();
+        if (mEditor != null) {
+            handled |= getEditor().performLongClick(handled);
         }
 
         if (handled) {
             performHapticFeedback(HapticFeedbackConstants.LONG_PRESS);
-            getEditor().mDiscardNextActionUp = true;
+            if (mEditor != null) getEditor().mDiscardNextActionUp = true;
         }
 
         return handled;
     }
 
-    private boolean touchPositionIsInSelection() {
-        int selectionStart = getSelectionStart();
-        int selectionEnd = getSelectionEnd();
-
-        if (selectionStart == selectionEnd) {
-            return false;
-        }
-
-        if (selectionStart > selectionEnd) {
-            int tmp = selectionStart;
-            selectionStart = selectionEnd;
-            selectionEnd = tmp;
-            Selection.setSelection((Spannable) mText, selectionStart, selectionEnd);
-        }
-
-        SelectionModifierCursorController selectionController = getSelectionController();
-        int minOffset = selectionController.getMinTouchOffset();
-        int maxOffset = selectionController.getMaxTouchOffset();
-
-        return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
-    }
-
-    private PositionListener getPositionListener() {
-        if (getEditor().mPositionListener == null) {
-            getEditor().mPositionListener = new PositionListener();
-        }
-        return getEditor().mPositionListener;
-    }
-
-    private interface TextViewPositionListener {
-        public void updatePosition(int parentPositionX, int parentPositionY,
-                boolean parentPositionChanged, boolean parentScrolled);
-    }
-
-    private boolean isPositionVisible(int positionX, int 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;
-    }
-
-    private boolean isOffsetVisible(int offset) {
-        final int line = mLayout.getLineForOffset(offset);
-        final int lineBottom = mLayout.getLineBottom(line);
-        final int primaryHorizontal = (int) mLayout.getPrimaryHorizontal(offset);
-        return isPositionVisible(primaryHorizontal + viewportToContentHorizontalOffset(),
-                lineBottom + viewportToContentVerticalOffset());
-    }
-
     @Override
     protected void onScrollChanged(int horiz, int vert, int oldHoriz, int oldVert) {
         super.onScrollChanged(horiz, vert, oldHoriz, oldVert);
         if (mEditor != null) {
-            if (getEditor().mPositionListener != null) {
-                getEditor().mPositionListener.onScrollChanged();
-            }
-            // Internal scroll affects the clip boundaries
-            getEditor().invalidateTextDisplayList();
+            getEditor().onScrollChanged();
         }
     }
 
     /**
-     * Removes the suggestion spans.
-     */
-    CharSequence removeSuggestionSpans(CharSequence text) {
-       if (text instanceof Spanned) {
-           Spannable spannable;
-           if (text instanceof Spannable) {
-               spannable = (Spannable) text;
-           } else {
-               spannable = new SpannableString(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;
-    }
-
-    void showSuggestions() {
-        if (getEditor().mSuggestionsPopupWindow == null) {
-            getEditor().mSuggestionsPopupWindow = new SuggestionsPopupWindow();
-        }
-        hideControllers();
-        getEditor().mSuggestionsPopupWindow.show();
-    }
-
-    boolean areSuggestionsShown() {
-        return getEditor().mSuggestionsPopupWindow != null && getEditor().mSuggestionsPopupWindow.isShowing();
-    }
-
-    /**
      * 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.
@@ -8391,65 +7555,100 @@
     }
 
     /**
-     *
-     * @return true if the selection mode was actually started.
-     */
-    private boolean startSelectionActionMode() {
-        if (getEditor().mSelectionActionMode != null) {
-            // Selection action mode is already started
-            return false;
-        }
-
-        if (!canSelectText() || !requestFocus()) {
-            Log.w(LOG_TAG, "TextView does not support text selection. Action mode cancelled.");
-            return false;
-        }
-
-        if (!hasSelection()) {
-            // There may already be a selection on device rotation
-            if (!selectCurrentWord()) {
-                // No word found under cursor or text selection not permitted.
-                return false;
-            }
-        }
-
-        boolean willExtract = extractedTextModeWillBeStarted();
-
-        // Do not start the action mode when extracted text will show up full screen, which would
-        // immediately hide the newly created action bar and would be visually distracting.
-        if (!willExtract) {
-            ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
-            getEditor().mSelectionActionMode = startActionMode(actionModeCallback);
-        }
-
-        final boolean selectionStarted = getEditor().mSelectionActionMode != null || willExtract;
-        if (selectionStarted && !isTextSelectable()) {
-            // 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(this, 0, null);
-            }
-        }
-
-        return selectionStarted;
-    }
-
-    private boolean extractedTextModeWillBeStarted() {
-        if (!(this instanceof ExtractEditText)) {
-            final InputMethodManager imm = InputMethodManager.peekInstance();
-            return  imm != null && imm.isFullscreenMode();
-        }
-        return false;
-    }
-
-    /**
      * @hide
      */
     protected void stopSelectionActionMode() {
-        if (getEditor().mSelectionActionMode != null) {
-            // This will hide the mSelectionModifierCursorController
-            getEditor().mSelectionActionMode.finish();
+        getEditor().stopSelectionActionMode();
+    }
+
+    boolean canCut() {
+        if (hasPasswordTransformationMethod()) {
+            return false;
         }
+
+        if (mText.length() > 0 && hasSelection() && mText instanceof Editable && mEditor != null && getEditor().mKeyListener != null) {
+            return true;
+        }
+
+        return false;
+    }
+
+    boolean canCopy() {
+        if (hasPasswordTransformationMethod()) {
+            return false;
+        }
+
+        if (mText.length() > 0 && hasSelection()) {
+            return true;
+        }
+
+        return false;
+    }
+
+    boolean canPaste() {
+        return (mText instanceof Editable &&
+                mEditor != null && getEditor().mKeyListener != null &&
+                getSelectionStart() >= 0 &&
+                getSelectionEnd() >= 0 &&
+                ((ClipboardManager)getContext().getSystemService(Context.CLIPBOARD_SERVICE)).
+                hasPrimaryClip());
+    }
+
+    boolean selectAllText() {
+        final int length = mText.length();
+        Selection.setSelection((Spannable) mText, 0, length);
+        return length > 0;
+    }
+
+    /**
+     * Prepare text so that there are not zero or two spaces at beginning and end of region defined
+     * by [min, max] when replacing this region by paste.
+     * Note that if there were two spaces (or more) at that position before, they are kept. We just
+     * make sure we do not add an extra one from the paste content.
+     */
+    long prepareSpacesAroundPaste(int min, int max, CharSequence paste) {
+        if (paste.length() > 0) {
+            if (min > 0) {
+                final char charBefore = mTransformed.charAt(min - 1);
+                final char charAfter = paste.charAt(0);
+
+                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+                    // Two spaces at beginning of paste: remove one
+                    final int originalLength = mText.length();
+                    deleteText_internal(min - 1, min);
+                    // Due to filters, there is no guarantee that exactly one character was
+                    // removed: count instead.
+                    final int delta = mText.length() - originalLength;
+                    min += delta;
+                    max += delta;
+                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+                    // No space at beginning of paste: add one
+                    final int originalLength = mText.length();
+                    replaceText_internal(min, min, " ");
+                    // Taking possible filters into account as above.
+                    final int delta = mText.length() - originalLength;
+                    min += delta;
+                    max += delta;
+                }
+            }
+
+            if (max < mText.length()) {
+                final char charBefore = paste.charAt(paste.length() - 1);
+                final char charAfter = mTransformed.charAt(max);
+
+                if (Character.isSpaceChar(charBefore) && Character.isSpaceChar(charAfter)) {
+                    // Two spaces at end of paste: remove one
+                    deleteText_internal(max, max + 1);
+                } else if (!Character.isSpaceChar(charBefore) && charBefore != '\n' &&
+                        !Character.isSpaceChar(charAfter) && charAfter != '\n') {
+                    // No space at end of paste: add one
+                    replaceText_internal(max, max, " ");
+                }
+            }
+        }
+
+        return TextUtils.packRangeInLong(min, max);
     }
 
     /**
@@ -8489,36 +7688,6 @@
         LAST_CUT_OR_COPY_TIME = SystemClock.uptimeMillis();
     }
 
-    private void hideInsertionPointCursorController() {
-        // No need to create the controller to hide it.
-        if (getEditor().mInsertionPointCursorController != null) {
-            getEditor().mInsertionPointCursorController.hide();
-        }
-    }
-
-    /**
-     * Hides the insertion controller and stops text selection mode, hiding the selection controller
-     */
-    private void hideControllers() {
-        hideCursorControllers();
-        hideSpanControllers();
-    }
-
-    private void hideSpanControllers() {
-        if (mChangeWatcher != null) {
-            mChangeWatcher.hideControllers();
-        }
-    }
-
-    private void hideCursorControllers() {
-        if (getEditor().mSuggestionsPopupWindow != null && !getEditor().mSuggestionsPopupWindow.isShowingUp()) {
-            // Should be done before hide insertion point controller since it triggers a show of it
-            getEditor().mSuggestionsPopupWindow.hide();
-        }
-        hideInsertionPointCursorController();
-        stopSelectionActionMode();
-    }
-
     /**
      * 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.
@@ -8535,7 +7704,7 @@
         return offset;
     }
 
-    private float convertToLocalHorizontalCoordinate(float x) {
+    float convertToLocalHorizontalCoordinate(float x) {
         x -= getTotalPaddingLeft();
         // Clamp the position to inside of the view.
         x = Math.max(0.0f, x);
@@ -8544,7 +7713,7 @@
         return x;
     }
 
-    private int getLineAtCoordinate(float y) {
+    int getLineAtCoordinate(float y) {
         y -= getTotalPaddingTop();
         // Clamp the position to inside of the view.
         y = Math.max(0.0f, y);
@@ -8558,25 +7727,11 @@
         return getLayout().getOffsetForHorizontal(line, x);
     }
 
-    /** 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) {
-        if (getLayout() == null) return false;
-
-        final int line = getLineAtCoordinate(y);
-        x = convertToLocalHorizontalCoordinate(x);
-
-        if (x < getLayout().getLineLeft(line)) return false;
-        if (x > getLayout().getLineRight(line)) return false;
-        return true;
-    }
-
     @Override
     public boolean onDragEvent(DragEvent event) {
         switch (event.getAction()) {
             case DragEvent.ACTION_DRAG_STARTED:
-                return mEditor != null && hasInsertionController();
+                return mEditor != null && getEditor().hasInsertionController();
 
             case DragEvent.ACTION_DRAG_ENTERED:
                 TextView.this.requestFocus();
@@ -8588,7 +7743,7 @@
                 return true;
 
             case DragEvent.ACTION_DROP:
-                onDrop(event);
+                if (mEditor != null) getEditor().onDrop(event);
                 return true;
 
             case DragEvent.ACTION_DRAG_ENDED:
@@ -8598,112 +7753,9 @@
         }
     }
 
-    private void onDrop(DragEvent event) {
-        StringBuilder content = new StringBuilder("");
-        ClipData clipData = event.getClipData();
-        final int itemCount = clipData.getItemCount();
-        for (int i=0; i < itemCount; i++) {
-            Item item = clipData.getItemAt(i);
-            content.append(item.coerceToText(TextView.this.mContext));
-        }
-
-        final int offset = 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 == this;
-
-        if (dragDropIntoItself) {
-            if (offset >= dragLocalState.start && offset < dragLocalState.end) {
-                // A drop inside the original selection discards the drop.
-                return;
-            }
-        }
-
-        final int originalLength = mText.length();
-        long minMax = prepareSpacesAroundPaste(offset, offset, content);
-        int min = TextUtils.unpackRangeStartFromLong(minMax);
-        int max = TextUtils.unpackRangeEndFromLong(minMax);
-
-        Selection.setSelection((Spannable) mText, max);
-        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 = mText.length() - originalLength;
-                dragSourceStart += shift;
-                dragSourceEnd += shift;
-            }
-
-            // Delete original selection
-            deleteText_internal(dragSourceStart, dragSourceEnd);
-
-            // Make sure we do not leave two adjacent spaces.
-            if ((dragSourceStart == 0 ||
-                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart - 1))) &&
-                    (dragSourceStart == mText.length() ||
-                    Character.isSpaceChar(mTransformed.charAt(dragSourceStart)))) {
-                final int pos = dragSourceStart == mText.length() ?
-                        dragSourceStart - 1 : dragSourceStart;
-                deleteText_internal(pos, pos + 1);
-            }
-        }
-    }
-
-    /**
-     * @return True if this view supports insertion handles.
-     */
-    boolean hasInsertionController() {
-        return getEditor().mInsertionControllerEnabled;
-    }
-
-    /**
-     * @return True if this view supports selection handles.
-     */
-    boolean hasSelectionController() {
-        return getEditor().mSelectionControllerEnabled;
-    }
-
-    InsertionPointCursorController getInsertionController() {
-        if (!getEditor().mInsertionControllerEnabled) {
-            return null;
-        }
-
-        if (getEditor().mInsertionPointCursorController == null) {
-            getEditor().mInsertionPointCursorController = new InsertionPointCursorController();
-
-            final ViewTreeObserver observer = getViewTreeObserver();
-            observer.addOnTouchModeChangeListener(getEditor().mInsertionPointCursorController);
-        }
-
-        return getEditor().mInsertionPointCursorController;
-    }
-
-    SelectionModifierCursorController getSelectionController() {
-        if (!getEditor().mSelectionControllerEnabled) {
-            return null;
-        }
-
-        if (getEditor().mSelectionModifierCursorController == null) {
-            getEditor().mSelectionModifierCursorController = new SelectionModifierCursorController();
-
-            final ViewTreeObserver observer = getViewTreeObserver();
-            observer.addOnTouchModeChangeListener(getEditor().mSelectionModifierCursorController);
-        }
-
-        return getEditor().mSelectionModifierCursorController;
-    }
-
     boolean isInBatchEditMode() {
         if (mEditor == null) return false;
-        final InputMethodState ims = getEditor().mInputMethodState;
+        final Editor.InputMethodState ims = getEditor().mInputMethodState;
         if (ims != null) {
             return ims.mBatchEditNesting > 0;
         }
@@ -8864,7 +7916,7 @@
             if (!(this instanceof EditText)) {
                 Log.e(LOG_TAG + " EDITOR", "Creating Editor on TextView. " + reason);
             }
-            mEditor = new Editor();
+            mEditor = new Editor(this);
         } else {
             if (!(this instanceof EditText)) {
                 Log.d(LOG_TAG + " EDITOR", "Redundant Editor creation. " + reason);
@@ -9041,151 +8093,6 @@
         }
     }
 
-    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 positionned. 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);
-            }
-        }
-    }
-
-    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 final static int FADE_OUT_DURATION = 400;
-
-        public CorrectionHighlighter() {
-            mPaint.setCompatibilityScaling(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(mHighlightColor);
-            final int color = (mHighlightColor & 0x00FFFFFF) +
-                    ((int) (highlightColorAlpha * coef) << 24);
-            mPaint.setColor(color);
-            return true;
-        }
-
-        private boolean updatePath() {
-            final Layout layout = TextView.this.mLayout;
-            if (layout == null) return false;
-
-            // Update in case text is edited while the animation is run
-            final int length = mText.length();
-            int start = Math.min(length, mStart);
-            int end = Math.min(length, mEnd);
-
-            mPath.reset();
-            TextView.this.mLayout.getSelectionPath(start, end, mPath);
-            return true;
-        }
-
-        private void invalidate(boolean delayed) {
-            if (TextView.this.mLayout == null) return;
-
-            synchronized (TEMP_RECTF) {
-                mPath.computeBounds(TEMP_RECTF, false);
-
-                int left = getCompoundPaddingLeft();
-                int top = getExtendedPaddingTop() + getVerticalOffset(true);
-
-                if (delayed) {
-                    TextView.this.postInvalidateOnAnimation(
-                            left + (int) TEMP_RECTF.left, top + (int) TEMP_RECTF.top,
-                            left + (int) TEMP_RECTF.right, top + (int) TEMP_RECTF.bottom);
-                } else {
-                    TextView.this.postInvalidate((int) TEMP_RECTF.left, (int) TEMP_RECTF.top,
-                            (int) TEMP_RECTF.right, (int) TEMP_RECTF.bottom);
-                }
-            }
-        }
-
-        private void stopAnimation() {
-            TextView.this.getEditor().mCorrectionHighlighter = null;
-        }
-    }
-
     private static final class Marquee extends Handler {
         // TODO: Add an option to configure this
         private static final float MARQUEE_DELTA_MAX = 0.07f;
@@ -9322,217 +8229,10 @@
         }
     }
 
-    /**
-     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
-     * pop-up should be displayed.
-     */
-    private class EasyEditSpanController {
-
-        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
-
-        private EasyEditPopupWindow mPopupWindow;
-
-        private EasyEditSpan mEasyEditSpan;
-
-        private Runnable mHidePopup;
-
-        private void hide() {
-            if (mPopupWindow != null) {
-                mPopupWindow.hide();
-                TextView.this.removeCallbacks(mHidePopup);
-            }
-            removeSpans(mText);
-            mEasyEditSpan = null;
-        }
-
-        /**
-         * Monitors the changes in the text.
-         *
-         * <p>{@link ChangeWatcher#onSpanAdded(Spannable, Object, int, int)} cannot be used,
-         * as the notifications are not sent when a spannable (with spans) is inserted.
-         */
-        public void onTextChange(CharSequence buffer) {
-            adjustSpans(mText);
-
-            if (getWindowVisibility() != View.VISIBLE) {
-                // The window is not visible yet, ignore the text change.
-                return;
-            }
-
-            if (mLayout == null) {
-                // The view has not been layout yet, ignore the text change
-                return;
-            }
-
-            InputMethodManager imm = InputMethodManager.peekInstance();
-            if (!(TextView.this instanceof ExtractEditText)
-                    && imm != null && imm.isFullscreenMode()) {
-                // The input is in extract mode. We do not have to handle the easy edit in the
-                // original TextView, as the ExtractEditText will do
-                return;
-            }
-
-            // Remove the current easy edit span, as the text changed, and remove the pop-up
-            // (if any)
-            if (mEasyEditSpan != null) {
-                if (mText instanceof Spannable) {
-                    ((Spannable) mText).removeSpan(mEasyEditSpan);
-                }
-                mEasyEditSpan = null;
-            }
-            if (mPopupWindow != null && mPopupWindow.isShowing()) {
-                mPopupWindow.hide();
-            }
-
-            // Display the new easy edit span (if any).
-            if (buffer instanceof Spanned) {
-                mEasyEditSpan = getSpan((Spanned) buffer);
-                if (mEasyEditSpan != null) {
-                    if (mPopupWindow == null) {
-                        mPopupWindow = new EasyEditPopupWindow();
-                        mHidePopup = new Runnable() {
-                            @Override
-                            public void run() {
-                                hide();
-                            }
-                        };
-                    }
-                    mPopupWindow.show(mEasyEditSpan);
-                    TextView.this.removeCallbacks(mHidePopup);
-                    TextView.this.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
-                }
-            }
-        }
-
-        /**
-         * Adjusts the spans by removing all of them except the last one.
-         */
-        private void adjustSpans(CharSequence buffer) {
-            // This method enforces that only one easy edit span is attached to the text.
-            // A better way to enforce this would be to listen for onSpanAdded, but this method
-            // cannot be used in this scenario as no notification is triggered when a text with
-            // spans is inserted into a text.
-            if (buffer instanceof Spannable) {
-                Spannable spannable = (Spannable) buffer;
-                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
-                        EasyEditSpan.class);
-                for (int i = 0; i < spans.length - 1; i++) {
-                    spannable.removeSpan(spans[i]);
-                }
-            }
-        }
-
-        /**
-         * Removes all the {@link EasyEditSpan} currently attached.
-         */
-        private void removeSpans(CharSequence buffer) {
-            if (buffer instanceof Spannable) {
-                Spannable spannable = (Spannable) buffer;
-                EasyEditSpan[] spans = spannable.getSpans(0, spannable.length(),
-                        EasyEditSpan.class);
-                for (int i = 0; i < spans.length; i++) {
-                    spannable.removeSpan(spans[i]);
-                }
-            }
-        }
-
-        private EasyEditSpan getSpan(Spanned spanned) {
-            EasyEditSpan[] easyEditSpans = spanned.getSpans(0, spanned.length(),
-                    EasyEditSpan.class);
-            if (easyEditSpans.length == 0) {
-                return null;
-            } else {
-                return easyEditSpans[0];
-            }
-        }
-    }
-
-    /**
-     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
-     * by {@link EasyEditSpanController}.
-     */
-    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;
-
-        @Override
-        protected void createPopupWindow() {
-            mPopupWindow = new PopupWindow(TextView.this.mContext, 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(TextView.this.getContext());
-            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
-            mContentView = linearLayout;
-            mContentView.setBackgroundResource(
-                    com.android.internal.R.drawable.text_edit_side_paste_window);
-
-            LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
-                    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 show(EasyEditSpan easyEditSpan) {
-            mEasyEditSpan = easyEditSpan;
-            super.show();
-        }
-
-        @Override
-        public void onClick(View view) {
-            if (view == mDeleteTextView) {
-                Editable editable = (Editable) mText;
-                int start = editable.getSpanStart(mEasyEditSpan);
-                int end = editable.getSpanEnd(mEasyEditSpan);
-                if (start >= 0 && end >= 0) {
-                    deleteText_internal(start, end);
-                }
-            }
-        }
-
-        @Override
-        protected int getTextOffset() {
-            // Place the pop-up at the end of the span
-            Editable editable = (Editable) mText;
-            return editable.getSpanEnd(mEasyEditSpan);
-        }
-
-        @Override
-        protected int getVerticalLocalPosition(int line) {
-            return mLayout.getLineBottom(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 ChangeWatcher implements TextWatcher, SpanWatcher {
 
         private CharSequence mBeforeText;
 
-        private EasyEditSpanController mEasyEditSpanController;
-
-        private ChangeWatcher() {
-            mEasyEditSpanController = new EasyEditSpanController();
-        }
-
         public void beforeTextChanged(CharSequence buffer, int start,
                                       int before, int after) {
             if (DEBUG_EXTRACT) Log.v(LOG_TAG, "beforeTextChanged start=" + start
@@ -9547,14 +8247,11 @@
             TextView.this.sendBeforeTextChanged(buffer, start, before, after);
         }
 
-        public void onTextChanged(CharSequence buffer, int start,
-                                  int before, int 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);
 
-            mEasyEditSpanController.onTextChange(buffer);
-
             if (AccessibilityManager.getInstance(mContext).isEnabled() &&
                     (isFocused() || isSelected() && isShown())) {
                 sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after);
@@ -9571,8 +8268,7 @@
             }
         }
 
-        public void onSpanChanged(Spannable buf,
-                                  Object what, int s, int e, int st, int en) {
+        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);
@@ -9589,2264 +8285,5 @@
                     + " what=" + what + ": " + buf);
             TextView.this.spanChange(buf, what, s, -1, e, -1);
         }
-
-        private void hideControllers() {
-            mEasyEditSpanController.hide();
-        }
-    }
-
-    private static class Blink extends Handler implements Runnable {
-        private final WeakReference<TextView> mView;
-        private boolean mCancelled;
-
-        public Blink(TextView v) {
-            mView = new WeakReference<TextView>(v);
-        }
-
-        public void run() {
-            if (mCancelled) {
-                return;
-            }
-
-            removeCallbacks(Blink.this);
-
-            TextView tv = mView.get();
-
-            if (tv != null && tv.shouldBlink()) {
-                if (tv.mLayout != null) {
-                    tv.invalidateCursorPath();
-                }
-
-                postAtTime(this, SystemClock.uptimeMillis() + BLINK);
-            }
-        }
-
-        void cancel() {
-            if (!mCancelled) {
-                removeCallbacks(Blink.this);
-                mCancelled = true;
-            }
-        }
-
-        void uncancel() {
-            mCancelled = false;
-        }
-    }
-
-    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;
-        }
-    }
-    
-    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
-        // 3 handles
-        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
-        private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
-        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 mNumberOfListeners;
-        private boolean mScrollHasChanged;
-        final int[] mTempCoords = new int[2];
-
-        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
-            if (mNumberOfListeners == 0) {
-                updatePosition();
-                ViewTreeObserver vto = TextView.this.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 = TextView.this.getViewTreeObserver();
-                vto.removeOnPreDrawListener(this);
-            }
-        }
-
-        public int getPositionX() {
-            return mPositionX;
-        }
-
-        public int getPositionY() {
-            return mPositionY;
-        }
-
-        @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() {
-            TextView.this.getLocationInWindow(mTempCoords);
-
-            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
-
-            mPositionX = mTempCoords[0];
-            mPositionY = mTempCoords[1];
-        }
-
-        public void onScrollChanged() {
-            mScrollHasChanged = true;
-        }
-    }
-
-    private abstract class PinnedPopupWindow implements TextViewPositionListener {
-        protected PopupWindow mPopupWindow;
-        protected ViewGroup mContentView;
-        int mPositionX, mPositionY;
-
-        protected abstract void createPopupWindow();
-        protected abstract void initContentView();
-        protected abstract int getTextOffset();
-        protected abstract int getVerticalLocalPosition(int line);
-        protected abstract int clipVertically(int positionY);
-
-        public PinnedPopupWindow() {
-            createPopupWindow();
-
-            mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_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() {
-            TextView.this.getPositionListener().addSubscriber(this, false /* offset is fixed */);
-
-            computeLocalPosition();
-
-            final PositionListener positionListener = TextView.this.getPositionListener();
-            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
-        }
-        
-        protected void measureContent() {
-            final DisplayMetrics displayMetrics = mContext.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) (mLayout.getPrimaryHorizontal(offset) - width / 2.0f);
-            mPositionX += viewportToContentHorizontalOffset();
-
-            final int line = mLayout.getLineForOffset(offset);
-            mPositionY = getVerticalLocalPosition(line);
-            mPositionY += viewportToContentVerticalOffset();
-        }
-
-        private void updatePosition(int parentPositionX, int parentPositionY) {
-            int positionX = parentPositionX + mPositionX;
-            int positionY = parentPositionY + mPositionY;
-
-            positionY = clipVertically(positionY);
-
-            // Horizontal clipping
-            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
-            final int width = mContentView.getMeasuredWidth();
-            positionX = Math.min(displayMetrics.widthPixels - width, positionX);
-            positionX = Math.max(0, positionX);
-
-            if (isShowing()) {
-                mPopupWindow.update(positionX, positionY, -1, -1);
-            } else {
-                mPopupWindow.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
-                        positionX, positionY);
-            }
-        }
-
-        public void hide() {
-            mPopupWindow.dismiss();
-            TextView.this.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 class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
-        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
-        private static final int ADD_TO_DICTIONARY = -1;
-        private static final int DELETE_TEXT = -2;
-        private SuggestionInfo[] mSuggestionInfos;
-        private int mNumberOfSuggestions;
-        private boolean mCursorWasVisibleBeforeSuggestions;
-        private boolean mIsShowingUp = false;
-        private SuggestionAdapter mSuggestionsAdapter;
-        private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
-        private final HashMap<SuggestionSpan, Integer> mSpansLengths;
-
-        private class CustomPopupWindow extends PopupWindow {
-            public CustomPopupWindow(Context context, int defStyle) {
-                super(context, null, defStyle);
-            }
-
-            @Override
-            public void dismiss() {
-                super.dismiss();
-
-                TextView.this.getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
-
-                // Safe cast since show() checks that mText is an Editable
-                ((Spannable) mText).removeSpan(getEditor().mSuggestionRangeSpan);
-
-                setCursorVisible(mCursorWasVisibleBeforeSuggestions);
-                if (hasInsertionController()) {
-                    getInsertionController().show(); 
-                }
-            }
-        }
-
-        public SuggestionsPopupWindow() {
-            mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible;
-            mSuggestionSpanComparator = new SuggestionSpanComparator();
-            mSpansLengths = new HashMap<SuggestionSpan, Integer>();
-        }
-
-        @Override
-        protected void createPopupWindow() {
-            mPopupWindow = new CustomPopupWindow(TextView.this.mContext,
-                com.android.internal.R.attr.textSuggestionsWindowStyle);
-            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
-            mPopupWindow.setFocusable(true);
-            mPopupWindow.setClippingEnabled(false);
-        }
-
-        @Override
-        protected void initContentView() {
-            ListView listView = new ListView(TextView.this.getContext());
-            mSuggestionsAdapter = new SuggestionAdapter();
-            listView.setAdapter(mSuggestionsAdapter);
-            listView.setOnItemClickListener(this);
-            mContentView = listView;
-
-            // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
-            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
-            for (int i = 0; i < mSuggestionInfos.length; i++) {
-                mSuggestionInfos[i] = new SuggestionInfo();
-            }
-        }
-
-        public boolean isShowingUp() {
-            return mIsShowingUp;
-        }
-
-        public void onParentLostFocus() {
-            mIsShowingUp = false;
-        }
-
-        private class SuggestionInfo {
-            int suggestionStart, suggestionEnd; // range of actual suggestion within text
-            SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
-            int suggestionIndex; // the index of this suggestion inside suggestionSpan
-            SpannableStringBuilder text = new SpannableStringBuilder();
-            TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mContext,
-                    android.R.style.TextAppearance_SuggestionHighlight);
-        }
-
-        private class SuggestionAdapter extends BaseAdapter {
-            private LayoutInflater mInflater = (LayoutInflater) TextView.this.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(mTextEditSuggestionItemLayout, parent,
-                            false);
-                }
-
-                final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
-                textView.setText(suggestionInfo.text);
-
-                if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
-                    textView.setCompoundDrawablesWithIntrinsicBounds(
-                            com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
-                } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
-                    textView.setCompoundDrawablesWithIntrinsicBounds(
-                            com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
-                } else {
-                    textView.setCompoundDrawables(null, null, null, null);
-                }
-
-                return textView;
-            }
-        }
-
-        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[] getSuggestionSpans() {
-            int pos = TextView.this.getSelectionStart();
-            Spannable spannable = (Spannable) TextView.this.mText;
-            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);
-            return suggestionSpans;
-        }
-
-        @Override
-        public void show() {
-            if (!(mText instanceof Editable)) return;
-
-            if (updateSuggestions()) {
-                mCursorWasVisibleBeforeSuggestions = getEditor().mCursorVisible;
-                setCursorVisible(false);
-                mIsShowingUp = true;
-                super.show();
-            }
-        }
-
-        @Override
-        protected void measureContent() {
-            final DisplayMetrics displayMetrics = mContext.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());
-            }
-
-            // 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 getSelectionStart();
-        }
-
-        @Override
-        protected int getVerticalLocalPosition(int line) {
-            return mLayout.getLineBottom(line);
-        }
-
-        @Override
-        protected int clipVertically(int positionY) {
-            final int height = mContentView.getMeasuredHeight();
-            final DisplayMetrics displayMetrics = mContext.getResources().getDisplayMetrics();
-            return Math.min(positionY, displayMetrics.heightPixels - height);
-        }
-
-        @Override
-        public void hide() {
-            super.hide();
-        }
-
-        private boolean updateSuggestions() {
-            Spannable spannable = (Spannable) TextView.this.mText;
-            SuggestionSpan[] suggestionSpans = getSuggestionSpans();
-
-            final int nbSpans = suggestionSpans.length;
-            // Suggestions are shown after a delay: the underlying spans may have been removed
-            if (nbSpans == 0) return false;
-
-            mNumberOfSuggestions = 0;
-            int spanUnionStart = mText.length();
-            int spanUnionEnd = 0;
-
-            SuggestionSpan misspelledSpan = null;
-            int underlineColor = 0;
-
-            for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
-                SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
-                final int spanStart = spannable.getSpanStart(suggestionSpan);
-                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
-                spanUnionStart = Math.min(spanStart, spanUnionStart);
-                spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
-
-                if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
-                    misspelledSpan = suggestionSpan;
-                }
-
-                // The first span dictates the background color of the highlighted text
-                if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
-
-                String[] suggestions = suggestionSpan.getSuggestions();
-                int nbSuggestions = suggestions.length;
-                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
-                    String suggestion = suggestions[suggestionIndex];
-
-                    boolean suggestionIsDuplicate = false;
-                    for (int i = 0; i < mNumberOfSuggestions; i++) {
-                        if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
-                            SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
-                            final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
-                            final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
-                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
-                                suggestionIsDuplicate = true;
-                                break;
-                            }
-                        }
-                    }
-
-                    if (!suggestionIsDuplicate) {
-                        SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
-                        suggestionInfo.suggestionSpan = suggestionSpan;
-                        suggestionInfo.suggestionIndex = suggestionIndex;
-                        suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
-
-                        mNumberOfSuggestions++;
-
-                        if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
-                            // Also end outer for loop
-                            spanIndex = nbSpans;
-                            break;
-                        }
-                    }
-                }
-            }
-
-            for (int i = 0; i < mNumberOfSuggestions; i++) {
-                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
-            }
-
-            // Add "Add to dictionary" item if there is a span with the misspelled flag
-            if (misspelledSpan != null) {
-                final int misspelledStart = spannable.getSpanStart(misspelledSpan);
-                final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
-                if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
-                    SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
-                    suggestionInfo.suggestionSpan = misspelledSpan;
-                    suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
-                    suggestionInfo.text.replace(0, suggestionInfo.text.length(),
-                            getContext().getString(com.android.internal.R.string.addToDictionary));
-                    suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
-                            Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
-                    mNumberOfSuggestions++;
-                }
-            }
-
-            // Delete item
-            SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
-            suggestionInfo.suggestionSpan = null;
-            suggestionInfo.suggestionIndex = DELETE_TEXT;
-            suggestionInfo.text.replace(0, suggestionInfo.text.length(),
-                    getContext().getString(com.android.internal.R.string.deleteText));
-            suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
-                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-            mNumberOfSuggestions++;
-
-            if (getEditor().mSuggestionRangeSpan == null) getEditor().mSuggestionRangeSpan = new SuggestionRangeSpan();
-            if (underlineColor == 0) {
-                // Fallback on the default highlight color when the first span does not provide one
-                getEditor().mSuggestionRangeSpan.setBackgroundColor(mHighlightColor);
-            } else {
-                final float BACKGROUND_TRANSPARENCY = 0.4f;
-                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
-                getEditor().mSuggestionRangeSpan.setBackgroundColor(
-                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
-            }
-            spannable.setSpan(getEditor().mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
-                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
-            mSuggestionsAdapter.notifyDataSetChanged();
-            return true;
-        }
-
-        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
-                int unionEnd) {
-            final Spannable text = (Spannable) mText;
-            final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
-            final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
-
-            // Adjust the start/end of the suggestion span
-            suggestionInfo.suggestionStart = spanStart - unionStart;
-            suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart 
-                    + suggestionInfo.text.length();
-
-            suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
-                    suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-
-            // Add the text before and after the span.
-            final String textAsString = text.toString();
-            suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
-            suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
-        }
-
-        @Override
-        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
-            Editable editable = (Editable) mText;
-            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
-
-            if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
-                final int spanUnionStart = editable.getSpanStart(getEditor().mSuggestionRangeSpan);
-                int spanUnionEnd = editable.getSpanEnd(getEditor().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;
-                    }
-                    deleteText_internal(spanUnionStart, spanUnionEnd);
-                }
-                hide();
-                return;
-            }
-
-            final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
-            final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
-            if (spanStart < 0 || spanEnd <= spanStart) {
-                // Span has been removed
-                hide();
-                return;
-            }
-            final String originalText = mText.toString().substring(spanStart, spanEnd);
-
-            if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
-                Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
-                intent.putExtra("word", originalText);
-                intent.putExtra("locale", getTextServicesLocale().toString());
-                intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
-                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(suggestionInfo.suggestionSpan);
-                updateSpellCheckSpans(spanStart, spanEnd, false);
-            } else {
-                // 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);
-                    }
-                }
-
-                final int suggestionStart = suggestionInfo.suggestionStart;
-                final int suggestionEnd = suggestionInfo.suggestionEnd;
-                final String suggestion = suggestionInfo.text.subSequence(
-                        suggestionStart, suggestionEnd).toString();
-                replaceText_internal(spanStart, spanEnd, suggestion);
-
-                // Notify source IME of the suggestion pick. Do this before swaping texts.
-                if (!TextUtils.isEmpty(
-                        suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
-                    InputMethodManager imm = InputMethodManager.peekInstance();
-                    if (imm != null) {
-                        imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
-                                suggestionInfo.suggestionIndex);
-                    }
-                }
-
-                // Swap text content between actual text and Suggestion span
-                String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
-                suggestions[suggestionInfo.suggestionIndex] = originalText;
-
-                // Restore previous SuggestionSpans
-                final int lengthDifference = 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) {
-                        setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
-                                suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
-                    }
-                }
-
-                // Move cursor at the end of the replaced word
-                final int newCursorPosition = spanEnd + lengthDifference;
-                setCursorPosition_internal(newCursorPosition, newCursorPosition);
-            }
-
-            hide();
-        }
-    }
-
-    /**
-     * An ActionMode Callback class that is used to provide actions while in text selection mode.
-     *
-     * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
-     * on which of these this TextView supports.
-     */
-    private class SelectionActionModeCallback implements ActionMode.Callback {
-
-        @Override
-        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
-            TypedArray styledAttributes = mContext.obtainStyledAttributes(
-                    com.android.internal.R.styleable.SelectionModeDrawables);
-
-            boolean allowText = getContext().getResources().getBoolean(
-                    com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
-
-            mode.setTitle(mContext.getString(com.android.internal.R.string.textSelectionCABTitle));
-            mode.setSubtitle(null);
-            mode.setTitleOptionalHint(true);
-
-            int selectAllIconId = 0; // No icon by default
-            if (!allowText) {
-                // Provide an icon, text will not be displayed on smaller screens.
-                selectAllIconId = styledAttributes.getResourceId(
-                        R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
-            }
-
-            menu.add(0, ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
-                    setIcon(selectAllIconId).
-                    setAlphabeticShortcut('a').
-                    setShowAsAction(
-                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-
-            if (canCut()) {
-                menu.add(0, ID_CUT, 0, com.android.internal.R.string.cut).
-                    setIcon(styledAttributes.getResourceId(
-                            R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
-                    setAlphabeticShortcut('x').
-                    setShowAsAction(
-                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-            }
-
-            if (canCopy()) {
-                menu.add(0, ID_COPY, 0, com.android.internal.R.string.copy).
-                    setIcon(styledAttributes.getResourceId(
-                            R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
-                    setAlphabeticShortcut('c').
-                    setShowAsAction(
-                            MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-            }
-
-            if (canPaste()) {
-                menu.add(0, ID_PASTE, 0, com.android.internal.R.string.paste).
-                        setIcon(styledAttributes.getResourceId(
-                                R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
-                        setAlphabeticShortcut('v').
-                        setShowAsAction(
-                                MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-            }
-
-            styledAttributes.recycle();
-
-            if (getEditor().mCustomSelectionActionModeCallback != null) {
-                if (!getEditor().mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
-                    // The custom mode can choose to cancel the action mode
-                    return false;
-                }
-            }
-
-            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
-                getSelectionController().show();
-                return true;
-            } else {
-                return false;
-            }
-        }
-
-        @Override
-        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
-            if (getEditor().mCustomSelectionActionModeCallback != null) {
-                return getEditor().mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
-            }
-            return true;
-        }
-
-        @Override
-        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
-            if (getEditor().mCustomSelectionActionModeCallback != null &&
-                 getEditor().mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
-                return true;
-            }
-            return onTextContextMenuItem(item.getItemId());
-        }
-
-        @Override
-        public void onDestroyActionMode(ActionMode mode) {
-            if (getEditor().mCustomSelectionActionModeCallback != null) {
-                getEditor().mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
-            }
-            Selection.setSelection((Spannable) mText, getSelectionEnd());
-
-            if (getEditor().mSelectionModifierCursorController != null) {
-                getEditor().mSelectionModifierCursorController.hide();
-            }
-
-            getEditor().mSelectionActionMode = null;
-        }
-    }
-
-    private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
-        private static final int POPUP_TEXT_LAYOUT =
-                com.android.internal.R.layout.text_edit_action_popup_text;
-        private TextView mPasteTextView;
-        private TextView mReplaceTextView;
-
-        @Override
-        protected void createPopupWindow() {
-            mPopupWindow = new PopupWindow(TextView.this.mContext, null,
-                    com.android.internal.R.attr.textSelectHandleWindowStyle);
-            mPopupWindow.setClippingEnabled(true);   
-        }
-
-        @Override
-        protected void initContentView() {
-            LinearLayout linearLayout = new LinearLayout(TextView.this.getContext());
-            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
-            mContentView = linearLayout;
-            mContentView.setBackgroundResource(
-                    com.android.internal.R.drawable.text_edit_paste_window);
-
-            LayoutInflater inflater = (LayoutInflater)TextView.this.mContext.
-                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
-
-            LayoutParams wrapContent = new LayoutParams(
-                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
-
-            mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
-            mPasteTextView.setLayoutParams(wrapContent);
-            mContentView.addView(mPasteTextView);
-            mPasteTextView.setText(com.android.internal.R.string.paste);
-            mPasteTextView.setOnClickListener(this);
-
-            mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
-            mReplaceTextView.setLayoutParams(wrapContent);
-            mContentView.addView(mReplaceTextView);
-            mReplaceTextView.setText(com.android.internal.R.string.replace);
-            mReplaceTextView.setOnClickListener(this);
-        }
-
-        @Override
-        public void show() {
-            boolean canPaste = canPaste();
-            boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
-            mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
-            mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
-
-            if (!canPaste && !canSuggest) return;
-
-            super.show();
-        }
-
-        @Override
-        public void onClick(View view) {
-            if (view == mPasteTextView && canPaste()) {
-                onTextContextMenuItem(ID_PASTE);
-                hide();
-            } else if (view == mReplaceTextView) {
-                final int middle = (getSelectionStart() + getSelectionEnd()) / 2;
-                stopSelectionActionMode();
-                Selection.setSelection((Spannable) mText, middle);
-                showSuggestions();
-            }
-        }
-
-        @Override
-        protected int getTextOffset() {
-            return (getSelectionStart() + getSelectionEnd()) / 2;
-        }
-
-        @Override
-        protected int getVerticalLocalPosition(int line) {
-            return mLayout.getLineTop(line) - mContentView.getMeasuredHeight();
-        }
-
-        @Override
-        protected int clipVertically(int positionY) {
-            if (positionY < 0) {
-                final int offset = getTextOffset();
-                final int line = mLayout.getLineForOffset(offset);
-                positionY += mLayout.getLineBottom(line) - mLayout.getLineTop(line);
-                positionY += mContentView.getMeasuredHeight();
-
-                // Assumes insertion and selection handles share the same height
-                final Drawable handle = mContext.getResources().getDrawable(mTextSelectHandleRes);
-                positionY += handle.getIntrinsicHeight();
-            }
-
-            return positionY;
-        }
-    }
-
-    private 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;
-        // 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;
-        // Transient action popup window for Paste and Replace actions
-        protected ActionPopupWindow mActionPopupWindow;
-        // Previous text character offset
-        private int mPreviousOffset = -1;
-        // Previous text character offset
-        private boolean mPositionHasChanged = true;
-        // Used to delay the appearance of the action popup window
-        private Runnable mActionPopupShower;
-
-        public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
-            super(TextView.this.mContext);
-            mContainer = new PopupWindow(TextView.this.mContext, null,
-                    com.android.internal.R.attr.textSelectHandleWindowStyle);
-            mContainer.setSplitTouchEnabled(true);
-            mContainer.setClippingEnabled(false);
-            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
-            mContainer.setContentView(this);
-
-            mDrawableLtr = drawableLtr;
-            mDrawableRtl = drawableRtl;
-
-            updateDrawable();
-
-            final int handleHeight = mDrawable.getIntrinsicHeight();
-            mTouchOffsetY = -0.3f * handleHeight;
-            mIdealVerticalOffset = 0.7f * handleHeight;
-        }
-
-        protected void updateDrawable() {
-            final int offset = getCurrentCursorOffset();
-            final boolean isRtlCharAtOffset = mLayout.isRtlCharAt(offset);
-            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
-            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
-        }
-
-        protected abstract int getHotspotX(Drawable drawable, 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() {
-            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);
-            }
-        }
-
-        public boolean offsetHasBeenChanged() {
-            return mNumberPreviousOffsets > 1;
-        }
-
-        @Override
-        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
-            setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
-        }
-
-        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);
-
-            hideActionPopupWindow();
-        }
-
-        protected void dismiss() {
-            mIsDragging = false;
-            mContainer.dismiss();
-            onDetached();
-        }
-
-        public void hide() {
-            dismiss();
-
-            TextView.this.getPositionListener().removeSubscriber(this);
-        }
-
-        void showActionPopupWindow(int delay) {
-            if (mActionPopupWindow == null) {
-                mActionPopupWindow = new ActionPopupWindow();
-            }
-            if (mActionPopupShower == null) {
-                mActionPopupShower = new Runnable() {
-                    public void run() {
-                        mActionPopupWindow.show();
-                    }
-                };
-            } else {
-                TextView.this.removeCallbacks(mActionPopupShower);
-            }
-            TextView.this.postDelayed(mActionPopupShower, delay);
-        }
-
-        protected void hideActionPopupWindow() {
-            if (mActionPopupShower != null) {
-                TextView.this.removeCallbacks(mActionPopupShower);
-            }
-            if (mActionPopupWindow != null) {
-                mActionPopupWindow.hide();
-            }
-        }
-
-        public boolean isShowing() {
-            return mContainer.isShowing();
-        }
-
-        private boolean isVisible() {
-            // Always show a dragging handle.
-            if (mIsDragging) {
-                return true;
-            }
-
-            if (isInBatchEditMode()) {
-                return false;
-            }
-
-            return TextView.this.isPositionVisible(mPositionX + mHotspotX, mPositionY);
-        }
-
-        public abstract int getCurrentCursorOffset();
-
-        protected abstract void updateSelection(int offset);
-
-        public abstract void updatePosition(float x, float y);
-
-        protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
-            // A HandleView relies on the layout, which may be nulled by external methods
-            if (mLayout == null) {
-                // Will update controllers' state, hiding them and stopping selection mode if needed
-                prepareCursorControllers();
-                return;
-            }
-
-            boolean offsetChanged = offset != mPreviousOffset;
-            if (offsetChanged || parentScrolled) {
-                if (offsetChanged) {
-                    updateSelection(offset);
-                    addPositionToTouchUpFilter(offset);
-                }
-                final int line = mLayout.getLineForOffset(offset);
-
-                mPositionX = (int) (mLayout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
-                mPositionY = mLayout.getLineBottom(line);
-
-                // Take TextView's padding and scroll into account.
-                mPositionX += viewportToContentHorizontalOffset();
-                mPositionY += viewportToContentVerticalOffset();
-
-                mPreviousOffset = offset;
-                mPositionHasChanged = true;
-            }
-        }
-
-        public void updatePosition(int parentPositionX, int parentPositionY,
-                boolean parentPositionChanged, boolean parentScrolled) {
-            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
-            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()) {
-                    final int positionX = parentPositionX + mPositionX;
-                    final int positionY = parentPositionY + mPositionY;
-                    if (isShowing()) {
-                        mContainer.update(positionX, positionY, -1, -1);
-                    } else {
-                        mContainer.showAtLocation(TextView.this, Gravity.NO_GRAVITY,
-                                positionX, positionY);
-                    }
-                } else {
-                    if (isShowing()) {
-                        dismiss();
-                    }
-                }
-
-                mPositionHasChanged = false;
-            }
-        }
-
-        @Override
-        protected void onDraw(Canvas c) {
-            mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
-            mDrawable.draw(c);
-        }
-
-        @Override
-        public boolean onTouchEvent(MotionEvent ev) {
-            switch (ev.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN: {
-                    startTouchUpFilter(getCurrentCursorOffset());
-                    mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
-                    mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
-
-                    final PositionListener positionListener = getPositionListener();
-                    mLastParentX = positionListener.getPositionX();
-                    mLastParentY = positionListener.getPositionY();
-                    mIsDragging = true;
-                    break;
-                }
-
-                case MotionEvent.ACTION_MOVE: {
-                    final float rawX = ev.getRawX();
-                    final float rawY = ev.getRawY();
-
-                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
-                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
-                    final float currentVerticalOffset = rawY - 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 = rawX - mTouchToWindowOffsetX + mHotspotX;
-                    final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
-
-                    updatePosition(newPosX, newPosY);
-                    break;
-                }
-
-                case MotionEvent.ACTION_UP:
-                    filterOnTouchUp();
-                    mIsDragging = false;
-                    break;
-
-                case MotionEvent.ACTION_CANCEL:
-                    mIsDragging = false;
-                    break;
-            }
-            return true;
-        }
-
-        public boolean isDragging() {
-            return mIsDragging;
-        }
-
-        void onHandleMoved() {
-            hideActionPopupWindow();
-        }
-
-        public void onDetached() {
-            hideActionPopupWindow();
-        }
-    }
-
-    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 ActionPopupWindow
-        private float mDownPositionX, mDownPositionY;
-        private Runnable mHider;
-
-        public InsertionHandleView(Drawable drawable) {
-            super(drawable, drawable);
-        }
-
-        @Override
-        public void show() {
-            super.show();
-
-            final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - LAST_CUT_OR_COPY_TIME;
-            if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
-                showActionPopupWindow(0);
-            }
-
-            hideAfterDelay();
-        }
-
-        public void showWithActionPopup() {
-            show();
-            showActionPopupWindow(0);
-        }
-
-        private void hideAfterDelay() {
-            if (mHider == null) {
-                mHider = new Runnable() {
-                    public void run() {
-                        hide();
-                    }
-                };
-            } else {
-                removeHiderCallback();
-            }
-            TextView.this.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
-        }
-
-        private void removeHiderCallback() {
-            if (mHider != null) {
-                TextView.this.removeCallbacks(mHider);
-            }
-        }
-
-        @Override
-        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
-            return drawable.getIntrinsicWidth() / 2;
-        }
-
-        @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(
-                                TextView.this.getContext());
-                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
-
-                        if (distanceSquared < touchSlop * touchSlop) {
-                            if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
-                                // Tapping on the handle dismisses the displayed action popup
-                                mActionPopupWindow.hide();
-                            } else {
-                                showWithActionPopup();
-                            }
-                        }
-                    }
-                    hideAfterDelay();
-                    break;
-
-                case MotionEvent.ACTION_CANCEL:
-                    hideAfterDelay();
-                    break;
-
-                default:
-                    break;
-            }
-
-            return result;
-        }
-
-        @Override
-        public int getCurrentCursorOffset() {
-            return TextView.this.getSelectionStart();
-        }
-
-        @Override
-        public void updateSelection(int offset) {
-            Selection.setSelection((Spannable) mText, offset);
-        }
-
-        @Override
-        public void updatePosition(float x, float y) {
-            positionAtCursorOffset(getOffsetForPosition(x, y), false);
-        }
-
-        @Override
-        void onHandleMoved() {
-            super.onHandleMoved();
-            removeHiderCallback();
-        }
-
-        @Override
-        public void onDetached() {
-            super.onDetached();
-            removeHiderCallback();
-        }
-    }
-
-    private class SelectionStartHandleView extends HandleView {
-
-        public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
-            super(drawableLtr, drawableRtl);
-        }
-
-        @Override
-        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
-            if (isRtlRun) {
-                return drawable.getIntrinsicWidth() / 4;
-            } else {
-                return (drawable.getIntrinsicWidth() * 3) / 4;
-            }
-        }
-
-        @Override
-        public int getCurrentCursorOffset() {
-            return TextView.this.getSelectionStart();
-        }
-
-        @Override
-        public void updateSelection(int offset) {
-            Selection.setSelection((Spannable) mText, offset, getSelectionEnd());
-            updateDrawable();
-        }
-
-        @Override
-        public void updatePosition(float x, float y) {
-            int offset = getOffsetForPosition(x, y);
-
-            // Handles can not cross and selection is at least one character
-            final int selectionEnd = getSelectionEnd();
-            if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
-
-            positionAtCursorOffset(offset, false);
-        }
-
-        public ActionPopupWindow getActionPopupWindow() {
-            return mActionPopupWindow;
-        }
-    }
-
-    private class SelectionEndHandleView extends HandleView {
-
-        public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
-            super(drawableLtr, drawableRtl);
-        }
-
-        @Override
-        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
-            if (isRtlRun) {
-                return (drawable.getIntrinsicWidth() * 3) / 4;
-            } else {
-                return drawable.getIntrinsicWidth() / 4;
-            }
-        }
-
-        @Override
-        public int getCurrentCursorOffset() {
-            return TextView.this.getSelectionEnd();
-        }
-
-        @Override
-        public void updateSelection(int offset) {
-            Selection.setSelection((Spannable) mText, getSelectionStart(), offset);
-            updateDrawable();
-        }
-
-        @Override
-        public void updatePosition(float x, float y) {
-            int offset = getOffsetForPosition(x, y);
-
-            // Handles can not cross and selection is at least one character
-            final int selectionStart = getSelectionStart();
-            if (offset <= selectionStart) offset = Math.min(selectionStart + 1, mText.length());
-
-            positionAtCursorOffset(offset, false);
-        }
-
-        public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
-            mActionPopupWindow = actionPopupWindow;
-        }
-    }
-
-    /**
-     * A CursorController instance can be used to control a cursor in the text.
-     * It is not used outside of {@link TextView}.
-     * @hide
-     */
-    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
-        /**
-         * Makes the cursor controller visible on screen. Will be drawn by {@link #draw(Canvas)}.
-         * 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();
-    }
-
-    private class InsertionPointCursorController implements CursorController {
-        private InsertionHandleView mHandle;
-
-        public void show() {
-            getHandle().show();
-        }
-
-        public void showWithActionPopup() {
-            getHandle().showWithActionPopup();
-        }
-
-        public void hide() {
-            if (mHandle != null) {
-                mHandle.hide();
-            }
-        }
-
-        public void onTouchModeChanged(boolean isInTouchMode) {
-            if (!isInTouchMode) {
-                hide();
-            }
-        }
-
-        private InsertionHandleView getHandle() {
-            if (getEditor().mSelectHandleCenter == null) {
-                getEditor().mSelectHandleCenter = mContext.getResources().getDrawable(
-                        mTextSelectHandleRes);
-            }
-            if (mHandle == null) {
-                mHandle = new InsertionHandleView(getEditor().mSelectHandleCenter);
-            }
-            return mHandle;
-        }
-
-        @Override
-        public void onDetached() {
-            final ViewTreeObserver observer = getViewTreeObserver();
-            observer.removeOnTouchModeChangeListener(this);
-
-            if (mHandle != null) mHandle.onDetached();
-        }
-    }
-
-    private class SelectionModifierCursorController implements CursorController {
-        private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
-        // The cursor controller handles, lazily created when shown.
-        private SelectionStartHandleView mStartHandle;
-        private SelectionEndHandleView mEndHandle;
-        // The offsets of that last touch down event. Remembered to start selection there.
-        private int mMinTouchOffset, mMaxTouchOffset;
-
-        // Double tap detection
-        private long mPreviousTapUpTime = 0;
-        private float mDownPositionX, mDownPositionY;
-        private boolean mGestureStayedInTapRegion;
-
-        SelectionModifierCursorController() {
-            resetTouchOffsets();
-        }
-
-        public void show() {
-            if (isInBatchEditMode()) {
-                return;
-            }
-            initDrawables();
-            initHandles();
-            hideInsertionPointCursorController();
-        }
-
-        private void initDrawables() {
-            if (getEditor().mSelectHandleLeft == null) {
-                getEditor().mSelectHandleLeft = mContext.getResources().getDrawable(
-                        mTextSelectHandleLeftRes);
-            }
-            if (getEditor().mSelectHandleRight == null) {
-                getEditor().mSelectHandleRight = mContext.getResources().getDrawable(
-                        mTextSelectHandleRightRes);
-            }
-        }
-
-        private void initHandles() {
-            // Lazy object creation has to be done before updatePosition() is called.
-            if (mStartHandle == null) {
-                mStartHandle = new SelectionStartHandleView(getEditor().mSelectHandleLeft, getEditor().mSelectHandleRight);
-            }
-            if (mEndHandle == null) {
-                mEndHandle = new SelectionEndHandleView(getEditor().mSelectHandleRight, getEditor().mSelectHandleLeft);
-            }
-
-            mStartHandle.show();
-            mEndHandle.show();
-
-            // Make sure both left and right handles share the same ActionPopupWindow (so that
-            // moving any of the handles hides the action popup).
-            mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
-            mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
-
-            hideInsertionPointCursorController();
-        }
-
-        public void hide() {
-            if (mStartHandle != null) mStartHandle.hide();
-            if (mEndHandle != null) mEndHandle.hide();
-        }
-
-        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.
-            switch (event.getActionMasked()) {
-                case MotionEvent.ACTION_DOWN:
-                    final float x = event.getX();
-                    final float y = event.getY();
-
-                    // Remember finger down position, to be able to start selection from there
-                    mMinTouchOffset = mMaxTouchOffset = getOffsetForPosition(x, y);
-
-                    // Double tap detection
-                    if (mGestureStayedInTapRegion) {
-                        long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
-                        if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
-                            final float deltaX = x - mDownPositionX;
-                            final float deltaY = y - mDownPositionY;
-                            final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
-                            ViewConfiguration viewConfiguration = ViewConfiguration.get(
-                                    TextView.this.getContext());
-                            int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
-                            boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
-
-                            if (stayedInArea && isPositionOnText(x, y)) {
-                                startSelectionActionMode();
-                                getEditor().mDiscardNextActionUp = true;
-                            }
-                        }
-                    }
-
-                    mDownPositionX = x;
-                    mDownPositionY = y;
-                    mGestureStayedInTapRegion = 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 (mContext.getPackageManager().hasSystemFeature(
-                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
-                        updateMinAndMaxOffsets(event);
-                    }
-                    break;
-
-                case MotionEvent.ACTION_MOVE:
-                    if (mGestureStayedInTapRegion) {
-                        final float deltaX = event.getX() - mDownPositionX;
-                        final float deltaY = event.getY() - mDownPositionY;
-                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
-
-                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
-                                TextView.this.getContext());
-                        int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
-
-                        if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
-                            mGestureStayedInTapRegion = false;
-                        }
-                    }
-                    break;
-
-                case MotionEvent.ACTION_UP:
-                    mPreviousTapUpTime = SystemClock.uptimeMillis();
-                    break;
-            }
-        }
-
-        /**
-         * @param event
-         */
-        private void updateMinAndMaxOffsets(MotionEvent event) {
-            int pointerCount = event.getPointerCount();
-            for (int index = 0; index < pointerCount; index++) {
-                int offset = 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;
-        }
-
-        /**
-         * @return true iff this controller is currently used to move the selection start.
-         */
-        public boolean isSelectionStartDragged() {
-            return mStartHandle != null && mStartHandle.isDragging();
-        }
-
-        public void onTouchModeChanged(boolean isInTouchMode) {
-            if (!isInTouchMode) {
-                hide();
-            }
-        }
-
-        @Override
-        public void onDetached() {
-            final ViewTreeObserver observer = getViewTreeObserver();
-            observer.removeOnTouchModeChangeListener(this);
-
-            if (mStartHandle != null) mStartHandle.onDetached();
-            if (mEndHandle != null) mEndHandle.onDetached();
-        }
-    }
-
-    static class InputContentType {
-        int imeOptions = EditorInfo.IME_NULL;
-        String privateImeOptions;
-        CharSequence imeActionLabel;
-        int imeActionId;
-        Bundle extras;
-        OnEditorActionListener onEditorActionListener;
-        boolean enterDown;
-    }
-
-    static class InputMethodState {
-        Rect mCursorRectInWindow = new Rect();
-        RectF mTmpRectF = new RectF();
-        float[] mTmpOffset = new float[2];
-        ExtractedTextRequest mExtracting;
-        final ExtractedText mTmpExtracted = new ExtractedText();
-        int mBatchEditNesting;
-        boolean mCursorChanged;
-        boolean mSelectionModeChanged;
-        boolean mContentChanged;
-        int mChangedStart, mChangedEnd, mChangedDelta;
-    }
-
-    private class Editor {
-        // Cursor Controllers.
-        InsertionPointCursorController mInsertionPointCursorController;
-        SelectionModifierCursorController mSelectionModifierCursorController;
-        ActionMode mSelectionActionMode;
-        boolean mInsertionControllerEnabled;
-        boolean mSelectionControllerEnabled;
-
-        // Used to highlight a word when it is corrected by the IME
-        CorrectionHighlighter mCorrectionHighlighter;
-
-        InputContentType mInputContentType;
-        InputMethodState mInputMethodState;
-
-        DisplayList[] mTextDisplayLists;
-
-        boolean mFrozenWithFocus;
-        boolean mSelectionMoved;
-        boolean mTouchFocusSelected;
-
-        KeyListener mKeyListener;
-        int mInputType = EditorInfo.TYPE_NULL;
-
-        boolean mDiscardNextActionUp;
-        boolean mIgnoreActionUpEvent;
-
-        long mShowCursor;
-        Blink mBlink;
-
-        boolean mCursorVisible = true;
-        boolean mSelectAllOnFocus;
-        boolean mTextIsSelectable;
-
-        CharSequence mError;
-        boolean mErrorWasChanged;
-        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.
-         */
-        boolean mShowErrorAfterAttach;
-
-        boolean mInBatchEditControllers;
-
-        SuggestionsPopupWindow mSuggestionsPopupWindow;
-        SuggestionRangeSpan mSuggestionRangeSpan;
-        Runnable mShowSuggestionRunnable;
-
-        final Drawable[] mCursorDrawable = new Drawable[2];
-        int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
-
-        Drawable mSelectHandleLeft;
-        Drawable mSelectHandleRight;
-        Drawable mSelectHandleCenter;
-
-        // Global listener that detects changes in the global position of the TextView
-        PositionListener mPositionListener;
-
-        float mLastDownPositionX, mLastDownPositionY;
-        Callback mCustomSelectionActionModeCallback;
-
-        // Set when this TextView gained focus with some text selected. Will start selection mode.
-        boolean mCreatedWithASelection;
-
-        WordIterator mWordIterator;
-        SpellChecker mSpellChecker;
-
-        void onAttachedToWindow() {
-            final ViewTreeObserver observer = 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) {
-                observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
-            }
-            updateSpellCheckSpans(0, mText.length(), true /* create the spell checker if needed */);
-        }
-
-        void onDetachedFromWindow() {
-            if (mError != null) {
-                hideError();
-            }
-
-            if (mBlink != null) {
-                mBlink.removeCallbacks(mBlink);
-            }
-
-            if (mInsertionPointCursorController != null) {
-                mInsertionPointCursorController.onDetached();
-            }
-
-            if (mSelectionModifierCursorController != null) {
-                mSelectionModifierCursorController.onDetached();
-            }
-
-            if (mShowSuggestionRunnable != null) {
-                removeCallbacks(mShowSuggestionRunnable);
-            }
-
-            invalidateTextDisplayList();
-
-            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;
-            }
-
-            hideControllers();
-        }
-
-        void onScreenStateChanged(int screenState) {
-            switch (screenState) {
-                case SCREEN_STATE_ON:
-                    resumeBlink();
-                    break;
-                case 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;
-                }
-            }
-        }
-
-        void setFrame() {
-            if (mErrorPopup != null) {
-                TextView tv = (TextView) mErrorPopup.getContentView();
-                chooseSize(mErrorPopup, mError, tv);
-                mErrorPopup.update(TextView.this, getErrorX(), getErrorY(),
-                        mErrorPopup.getWidth(), mErrorPopup.getHeight());
-            }
-        }
-
-        void onFocusChanged(boolean focused, int direction) {
-            mShowCursor = SystemClock.uptimeMillis();
-            ensureEndedBatchEdit();
-
-            if (focused) {
-                int selStart = getSelectionStart();
-                int selEnd = 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 == mText.length();
-
-                mCreatedWithASelection = mFrozenWithFocus && 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) mText, lastTapPosition);
-                    }
-
-                    // Note this may have to be moved out of the Editor class
-                    if (mMovement != null) {
-                        mMovement.onTakeFocus(TextView.this, (Spannable) mText, 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 (((TextView.this instanceof ExtractEditText) || 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) mText, selStart, selEnd);
-                    }
-
-                    if (mSelectAllOnFocus) {
-                        selectAll();
-                    }
-
-                    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.
-                onEndBatchEdit();
-
-                if (TextView.this instanceof ExtractEditText) {
-                    // terminateTextSelectionMode removes selection, which we want to keep when
-                    // ExtractEditText goes out of focus.
-                    final int selStart = getSelectionStart();
-                    final int selEnd = getSelectionEnd();
-                    hideControllers();
-                    Selection.setSelection((Spannable) mText, selStart, selEnd);
-                } else {
-                    hideControllers();
-                    downgradeEasyCorrectionSpans();
-                }
-
-                // No need to create the controller
-                if (mSelectionModifierCursorController != null) {
-                    mSelectionModifierCursorController.resetTouchOffsets();
-                }
-            }
-        }
-
-        void sendOnTextChanged(int start, int after) {
-            updateSpellCheckSpans(start, start + after, false);
-
-            // 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();
-        }
-
-        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 > mText.length()) {
-                        Log.e(LOG_TAG, "Invalid tap focus position (" + lastTapPosition + " vs "
-                                + mText.length() + ")");
-                        lastTapPosition = mText.length();
-                    }
-                    return lastTapPosition;
-                }
-            }
-
-            return -1;
-        }
-
-        void onWindowFocusChanged(boolean hasWindowFocus) {
-            if (hasWindowFocus) {
-                if (mBlink != null) {
-                    mBlink.uncancel();
-                    makeBlink();
-                }
-            } else {
-                if (mBlink != null) {
-                    mBlink.cancel();
-                }
-                if (mInputContentType != null) {
-                    mInputContentType.enterDown = false;
-                }
-                // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
-                hideControllers();
-                if (mSuggestionsPopupWindow != null) {
-                    mSuggestionsPopupWindow.onParentLostFocus();
-                }
-
-                // Don't leave us in the middle of a batch edit.
-                onEndBatchEdit();
-            }
-        }
-
-        void onTouchEvent(MotionEvent event) {
-            if (hasSelectionController()) {
-                getSelectionController().onTouchEvent(event);
-            }
-
-            if (mShowSuggestionRunnable != null) {
-                removeCallbacks(mShowSuggestionRunnable);
-                mShowSuggestionRunnable = null;
-            }
-
-            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;
-            }
-        }
-
-        void onDraw(Canvas canvas, Layout layout, Path highlight, int cursorOffsetVertical) {
-            final int selectionStart = getSelectionStart();
-            final int selectionEnd = getSelectionEnd();
-
-            final InputMethodState ims = mInputMethodState;
-            if (ims != null && ims.mBatchEditNesting == 0) {
-                InputMethodManager imm = InputMethodManager.peekInstance();
-                if (imm != null) {
-                    if (imm.isActive(TextView.this)) {
-                        boolean reported = false;
-                        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.
-                            reported = reportExtractedText();
-                        }
-                        if (!reported && highlight != null) {
-                            int candStart = -1;
-                            int candEnd = -1;
-                            if (mText instanceof Spannable) {
-                                Spannable sp = (Spannable)mText;
-                                candStart = EditableInputConnection.getComposingSpanStart(sp);
-                                candEnd = EditableInputConnection.getComposingSpanEnd(sp);
-                            }
-                            imm.updateSelection(TextView.this,
-                                    selectionStart, selectionEnd, candStart, candEnd);
-                        }
-                    }
-
-                    if (imm.isWatchingCursor(TextView.this) && highlight != null) {
-                        highlight.computeBounds(ims.mTmpRectF, true);
-                        ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
-
-                        canvas.getMatrix().mapPoints(ims.mTmpOffset);
-                        ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
-
-                        ims.mTmpRectF.offset(0, cursorOffsetVertical);
-
-                        ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
-                                (int)(ims.mTmpRectF.top + 0.5),
-                                (int)(ims.mTmpRectF.right + 0.5),
-                                (int)(ims.mTmpRectF.bottom + 0.5));
-
-                        imm.updateCursor(TextView.this,
-                                ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
-                                ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
-                    }
-                }
-            }
-
-            if (mCorrectionHighlighter != null) {
-                mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
-            }
-
-            if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
-                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 (canHaveDisplayList() && canvas.isHardwareAccelerated()) {
-                drawHardwareAccelerated(canvas, layout, highlight, cursorOffsetVertical);
-            } else {
-                layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
-            }
-
-            if (mMarquee != null && mMarquee.shouldDrawGhost()) {
-                canvas.translate((int) mMarquee.getGhostOffset(), 0.0f);
-                layout.draw(canvas, highlight, mHighlightPaint, cursorOffsetVertical);
-            }
-        }
-
-        private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
-                int cursorOffsetVertical) {
-            final int width = mRight - mLeft;
-            final int height = mBottom - mTop;
-
-            final long lineRange = layout.getLineRangeForDraw(canvas);
-            int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
-            int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
-            if (lastLine < 0) return;
-
-            layout.drawBackground(canvas, highlight, mHighlightPaint, cursorOffsetVertical,
-                    firstLine, lastLine);
-
-            if (layout instanceof DynamicLayout) {
-                if (mTextDisplayLists == null) {
-                    mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
-                }
-
-                DynamicLayout dynamicLayout = (DynamicLayout) layout;
-                int[] blockEnds = dynamicLayout.getBlockEnds();
-                int[] blockIndices = dynamicLayout.getBlockIndices();
-                final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
-
-                canvas.translate(mScrollX, mScrollY);
-                int endOfPreviousBlock = -1;
-                int searchStartIndex = 0;
-                for (int i = 0; i < numberOfBlocks; i++) {
-                    int blockEnd = blockEnds[i];
-                    int blockIndex = blockIndices[i];
-
-                    final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
-                    if (blockIsInvalid) {
-                        blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
-                                searchStartIndex);
-                        // Dynamic layout internal block indices structure is updated from Editor
-                        blockIndices[i] = blockIndex;
-                        searchStartIndex = blockIndex + 1;
-                    }
-
-                    DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
-                    if (blockDisplayList == null) {
-                        blockDisplayList = mTextDisplayLists[blockIndex] =
-                                getHardwareRenderer().createDisplayList("Text " + blockIndex);
-                    } else {
-                        if (blockIsInvalid) blockDisplayList.invalidate();
-                    }
-
-                    if (!blockDisplayList.isValid()) {
-                        final HardwareCanvas hardwareCanvas = blockDisplayList.start();
-                        try {
-                            hardwareCanvas.setViewport(width, height);
-                            // The dirty rect should always be null for a display list
-                            hardwareCanvas.onPreDraw(null);
-                            hardwareCanvas.translate(-mScrollX, -mScrollY);
-                            layout.drawText(hardwareCanvas, endOfPreviousBlock + 1, blockEnd);
-                            hardwareCanvas.translate(mScrollX, mScrollY);
-                        } finally {
-                            hardwareCanvas.onPostDraw();
-                            blockDisplayList.end();
-                            if (USE_DISPLAY_LIST_PROPERTIES) {
-                                blockDisplayList.setLeftTopRightBottom(0, 0, width, height);
-                            }
-                        }
-                    }
-
-                    ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, width, height, null,
-                            DisplayList.FLAG_CLIP_CHILDREN);
-                    endOfPreviousBlock = blockEnd;
-                }
-                canvas.translate(-mScrollX, -mScrollY);
-            } else {
-                // Fallback on the layout method (a BoringLayout is used when the text is empty)
-                layout.drawText(canvas, firstLine, lastLine);
-            }
-        }
-
-        private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
-                int searchStartIndex) {
-            int length = mTextDisplayLists.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
-            int newSize = ArrayUtils.idealIntArraySize(length + 1);
-            DisplayList[] displayLists = new DisplayList[newSize];
-            System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
-            mTextDisplayLists = displayLists;
-            return length;
-        }
-
-        private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
-            final boolean translate = cursorOffsetVertical != 0;
-            if (translate) canvas.translate(0, cursorOffsetVertical);
-            for (int i = 0; i < getEditor().mCursorCount; i++) {
-                mCursorDrawable[i].draw(canvas);
-            }
-            if (translate) canvas.translate(0, -cursorOffsetVertical);
-        }
-
-        private void invalidateTextDisplayList() {
-            if (mTextDisplayLists != null) {
-                for (int i = 0; i < mTextDisplayLists.length; i++) {
-                    if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
-                }
-            }
-        }
-
-        private void updateCursorsPositions() {
-            if (mCursorDrawableRes == 0) {
-                mCursorCount = 0;
-                return;
-            }
-
-            final int offset = getSelectionStart();
-            final int line = mLayout.getLineForOffset(offset);
-            final int top = mLayout.getLineTop(line);
-            final int bottom = mLayout.getLineTop(line + 1);
-
-            mCursorCount = mLayout.isLevelBoundary(offset) ? 2 : 1;
-
-            int middle = bottom;
-            if (mCursorCount == 2) {
-                // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
-                middle = (top + bottom) >> 1;
-            }
-
-            updateCursorPosition(0, top, middle, mLayout.getPrimaryHorizontal(offset));
-
-            if (mCursorCount == 2) {
-                updateCursorPosition(1, middle, bottom, mLayout.getSecondaryHorizontal(offset));
-            }
-        }
-
-        private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
-            if (mCursorDrawable[cursorIndex] == null)
-                mCursorDrawable[cursorIndex] = mContext.getResources().getDrawable(mCursorDrawableRes);
-
-            if (mTempRect == null) mTempRect = new Rect();
-            mCursorDrawable[cursorIndex].getPadding(mTempRect);
-            final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
-            horizontal = Math.max(0.5f, horizontal - 0.5f);
-            final int left = (int) (horizontal) - mTempRect.left;
-            mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
-                    bottom + mTempRect.bottom);
-        }
     }
 }
diff --git a/core/java/com/android/internal/widget/LockPatternUtils.java b/core/java/com/android/internal/widget/LockPatternUtils.java
index 5a7d519..93f90f6 100644
--- a/core/java/com/android/internal/widget/LockPatternUtils.java
+++ b/core/java/com/android/internal/widget/LockPatternUtils.java
@@ -438,10 +438,9 @@
      * Calls back SetupFaceLock to delete the temporary gallery file
      */
     public void deleteTempGallery() {
-        Intent intent = new Intent().setClassName("com.android.facelock",
-                "com.android.facelock.SetupFaceLock");
+        Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY");
         intent.putExtra("deleteTempGallery", true);
-        mContext.startActivity(intent);
+        mContext.sendBroadcast(intent);
     }
 
     /**
@@ -449,10 +448,9 @@
     */
     void deleteGallery() {
         if(usingBiometricWeak()) {
-            Intent intent = new Intent().setClassName("com.android.facelock",
-                    "com.android.facelock.SetupFaceLock");
+            Intent intent = new Intent().setAction("com.android.facelock.DELETE_GALLERY");
             intent.putExtra("deleteGallery", true);
-            mContext.startActivity(intent);
+            mContext.sendBroadcast(intent);
         }
     }
 
diff --git a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
index 6a471ad..b2075ae 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerBaseTest.java
@@ -16,12 +16,8 @@
 
 package android.app;
 
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
-
 import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
-import android.app.DownloadManagerBaseTest.DataType;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -39,10 +35,14 @@
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 
+import com.google.mockwebserver.MockResponse;
+import com.google.mockwebserver.MockWebServer;
+
 import java.io.DataInputStream;
 import java.io.DataOutputStream;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.net.URL;
@@ -53,13 +53,15 @@
 import java.util.Set;
 import java.util.concurrent.TimeoutException;
 
+import libcore.io.Streams;
+
 /**
  * Base class for Instrumented tests for the Download Manager.
  */
 public class DownloadManagerBaseTest extends InstrumentationTestCase {
     private static final String TAG = "DownloadManagerBaseTest";
     protected DownloadManager mDownloadManager = null;
-    protected MockWebServer mServer = null;
+    private MockWebServer mServer = null;
     protected String mFileType = "text/plain";
     protected Context mContext = null;
     protected MultipleDownloadsCompletedReceiver mReceiver = null;
@@ -237,63 +239,57 @@
         mContext = getInstrumentation().getContext();
         mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
         mServer = new MockWebServer();
+        mServer.play();
         mReceiver = registerNewMultipleDownloadsReceiver();
         // Note: callers overriding this should call mServer.play() with the desired port #
     }
 
     /**
-     * Helper to enqueue a response from the MockWebServer with no body.
+     * Helper to build a response from the MockWebServer with no body.
      *
      * @param status The HTTP status code to return for this response
      * @return Returns the mock web server response that was queued (which can be modified)
      */
-    protected MockResponse enqueueResponse(int status) {
-        return doEnqueueResponse(status);
-
+    protected MockResponse buildResponse(int status) {
+        MockResponse response = new MockResponse().setResponseCode(status);
+        response.setHeader("Content-type", mFileType);
+        return response;
     }
 
     /**
-     * Helper to enqueue a response from the MockWebServer.
+     * Helper to build a response from the MockWebServer.
      *
      * @param status The HTTP status code to return for this response
      * @param body The body to return in this response
      * @return Returns the mock web server response that was queued (which can be modified)
      */
-    protected MockResponse enqueueResponse(int status, byte[] body) {
-        return doEnqueueResponse(status).setBody(body);
-
+    protected MockResponse buildResponse(int status, byte[] body) {
+        return buildResponse(status).setBody(body);
     }
 
     /**
-     * Helper to enqueue a response from the MockWebServer.
+     * Helper to build a response from the MockWebServer.
      *
      * @param status The HTTP status code to return for this response
      * @param bodyFile The body to return in this response
      * @return Returns the mock web server response that was queued (which can be modified)
      */
-    protected MockResponse enqueueResponse(int status, File bodyFile) {
-        return doEnqueueResponse(status).setBody(bodyFile);
+    protected MockResponse buildResponse(int status, File bodyFile)
+            throws FileNotFoundException, IOException {
+        final byte[] body = Streams.readFully(new FileInputStream(bodyFile));
+        return buildResponse(status).setBody(body);
     }
 
-    /**
-     * Helper for enqueue'ing a response from the MockWebServer.
-     *
-     * @param status The HTTP status code to return for this response
-     * @return Returns the mock web server response that was queued (which can be modified)
-     */
-    protected MockResponse doEnqueueResponse(int status) {
-        MockResponse response = new MockResponse().setResponseCode(status);
-        response.addHeader("Content-type", mFileType);
-        mServer.enqueue(response);
-        return response;
+    protected void enqueueResponse(MockResponse resp) {
+        mServer.enqueue(resp);
     }
 
     /**
      * Helper to generate a random blob of bytes.
      *
      * @param size The size of the data to generate
-     * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
-     *         {@link DataType.BINARY}.
+     * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or
+     *         {@link DataType#BINARY}.
      * @return The random data that is generated.
      */
     protected byte[] generateData(int size, DataType type) {
@@ -304,8 +300,8 @@
      * Helper to generate a random blob of bytes using a given RNG.
      *
      * @param size The size of the data to generate
-     * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
-     *         {@link DataType.BINARY}.
+     * @param type The type of data to generate: currently, one of {@link DataType#TEXT} or
+     *         {@link DataType#BINARY}.
      * @param rng (optional) The RNG to use; pass null to use
      * @return The random data that is generated.
      */
@@ -492,8 +488,6 @@
             assertEquals(1, cursor.getCount());
             assertTrue(cursor.moveToFirst());
 
-            mServer.checkForExceptions();
-
             verifyFileSize(pfd, fileSize);
             verifyFileContents(pfd, fileData);
         } finally {
@@ -928,7 +922,7 @@
 
     protected long enqueueDownloadRequest(byte[] body, int location) throws Exception {
         // Prepare the mock server with a standard response
-        enqueueResponse(HTTP_OK, body);
+        mServer.enqueue(buildResponse(HTTP_OK, body));
         return doEnqueue(location);
     }
 
@@ -943,7 +937,7 @@
 
     protected long enqueueDownloadRequest(File body, int location) throws Exception {
         // Prepare the mock server with a standard response
-        enqueueResponse(HTTP_OK, body);
+        mServer.enqueue(buildResponse(HTTP_OK, body));
         return doEnqueue(location);
     }
 
@@ -1035,4 +1029,4 @@
         assertEquals(1, mReceiver.numDownloadsCompleted());
         return dlRequest;
     }
-}
\ No newline at end of file
+}
diff --git a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
index afe7f55..aa9f69d 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerFunctionalTest.java
@@ -16,8 +16,6 @@
 
 package android.app;
 
-import coretestutils.http.MockResponse;
-
 import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
 import android.database.Cursor;
@@ -26,6 +24,8 @@
 import android.os.ParcelFileDescriptor;
 import android.test.suitebuilder.annotation.LargeTest;
 
+import com.google.mockwebserver.MockResponse;
+
 import java.io.File;
 import java.util.Iterator;
 import java.util.Set;
@@ -47,7 +47,6 @@
     public void setUp() throws Exception {
         super.setUp();
         setWiFiStateOn(true);
-        mServer.play();
         removeAllCurrentDownloads();
     }
 
@@ -132,8 +131,6 @@
             assertEquals(1, cursor.getCount());
             assertTrue(cursor.moveToFirst());
 
-            mServer.checkForExceptions();
-
             verifyFileSize(pfd, fileSize);
             verifyFileContents(pfd, fileData);
             int colIndex = cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_FILENAME);
@@ -154,7 +151,7 @@
         byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
 
         // Prepare the mock server with a standard response
-        enqueueResponse(HTTP_OK, blobData);
+        enqueueResponse(buildResponse(HTTP_OK, blobData));
 
         try {
             Uri uri = getServerUri(DEFAULT_FILENAME);
@@ -193,7 +190,7 @@
             byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
 
             // Prepare the mock server with a standard response
-            enqueueResponse(HTTP_OK, blobData);
+            enqueueResponse(buildResponse(HTTP_OK, blobData));
 
             Uri uri = getServerUri(DEFAULT_FILENAME);
             Request request = new Request(uri);
@@ -224,7 +221,7 @@
             byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
 
             // Prepare the mock server with a standard response
-            enqueueResponse(HTTP_OK, blobData);
+            enqueueResponse(buildResponse(HTTP_OK, blobData));
 
             Uri uri = getServerUri(DEFAULT_FILENAME);
             Request request = new Request(uri);
@@ -251,7 +248,7 @@
     public void testGetDownloadIdOnNotification() throws Exception {
         byte[] blobData = generateData(3000, DataType.TEXT);  // file size = 3000 bytes
 
-        MockResponse response = enqueueResponse(HTTP_OK, blobData);
+        enqueueResponse(buildResponse(HTTP_OK, blobData));
         long dlRequest = doCommonStandardEnqueue();
         waitForDownloadOrTimeout(dlRequest);
 
@@ -271,8 +268,9 @@
 
         // force 6 redirects
         for (int i = 0; i < 6; ++i) {
-            MockResponse response = enqueueResponse(HTTP_REDIRECT);
-            response.addHeader("Location", uri.toString());
+            final MockResponse resp = buildResponse(HTTP_REDIRECT);
+            resp.setHeader("Location", uri.toString());
+            enqueueResponse(resp);
         }
         doErrorTest(uri, DownloadManager.ERROR_TOO_MANY_REDIRECTS);
     }
@@ -283,7 +281,7 @@
     @LargeTest
     public void testErrorUnhandledHttpCode() throws Exception {
         Uri uri = getServerUri(DEFAULT_FILENAME);
-        MockResponse response = enqueueResponse(HTTP_PARTIAL_CONTENT);
+        enqueueResponse(buildResponse(HTTP_PARTIAL_CONTENT));
 
         doErrorTest(uri, DownloadManager.ERROR_UNHANDLED_HTTP_CODE);
     }
@@ -294,8 +292,9 @@
     @LargeTest
     public void testErrorHttpDataError_invalidRedirect() throws Exception {
         Uri uri = getServerUri(DEFAULT_FILENAME);
-        MockResponse response = enqueueResponse(HTTP_REDIRECT);
-        response.addHeader("Location", "://blah.blah.blah.com");
+        final MockResponse resp = buildResponse(HTTP_REDIRECT);
+        resp.setHeader("Location", "://blah.blah.blah.com");
+        enqueueResponse(resp);
 
         doErrorTest(uri, DownloadManager.ERROR_HTTP_DATA_ERROR);
     }
@@ -327,7 +326,7 @@
     public void testSetTitle() throws Exception {
         int fileSize = 1024;
         byte[] blobData = generateData(fileSize, DataType.BINARY);
-        MockResponse response = enqueueResponse(HTTP_OK, blobData);
+        enqueueResponse(buildResponse(HTTP_OK, blobData));
 
         // An arbitrary unicode string title
         final String title = "\u00a5123;\"\u0152\u017d \u054b \u0a07 \ucce0 \u6820\u03a8\u5c34" +
@@ -359,7 +358,7 @@
         byte[] blobData = generateData(fileSize, DataType.TEXT);
 
         setWiFiStateOn(false);
-        enqueueResponse(HTTP_OK, blobData);
+        enqueueResponse(buildResponse(HTTP_OK, blobData));
 
         try {
             Uri uri = getServerUri(DEFAULT_FILENAME);
@@ -383,32 +382,16 @@
     }
 
     /**
-     * Tests when the server drops the connection after all headers (but before any data send).
-     */
-    @LargeTest
-    public void testDropConnection_headers() throws Exception {
-        byte[] blobData = generateData(DEFAULT_FILE_SIZE, DataType.TEXT);
-
-        MockResponse response = enqueueResponse(HTTP_OK, blobData);
-        response.setCloseConnectionAfterHeader("content-length");
-        long dlRequest = doCommonStandardEnqueue();
-
-        // Download will never complete when header is dropped
-        boolean success = waitForDownloadOrTimeoutNoThrow(dlRequest, DEFAULT_WAIT_POLL_TIME,
-                DEFAULT_MAX_WAIT_TIME);
-
-        assertFalse(success);
-    }
-
-    /**
      * Tests that we get an error code when the server drops the connection during a download.
      */
     @LargeTest
     public void testServerDropConnection_body() throws Exception {
         byte[] blobData = generateData(25000, DataType.TEXT);  // file size = 25000 bytes
 
-        MockResponse response = enqueueResponse(HTTP_OK, blobData);
-        response.setCloseConnectionAfterXBytes(15382);
+        final MockResponse resp = buildResponse(HTTP_OK, blobData);
+        resp.setHeader("Content-Length", "50000");
+        enqueueResponse(resp);
+
         long dlRequest = doCommonStandardEnqueue();
         waitForDownloadOrTimeout(dlRequest);
 
diff --git a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
index bdeb554..864b2d6 100644
--- a/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
+++ b/core/tests/coretests/src/android/app/DownloadManagerStressTest.java
@@ -46,7 +46,6 @@
     public void setUp() throws Exception {
         super.setUp();
         setWiFiStateOn(true);
-        mServer.play();
         removeAllCurrentDownloads();
     }
 
@@ -85,7 +84,7 @@
             request.setTitle(String.format("%s--%d", DEFAULT_FILENAME + i, i));
 
             // Prepare the mock server with a standard response
-            enqueueResponse(HTTP_OK, blobData);
+            enqueueResponse(buildResponse(HTTP_OK, blobData));
 
             long requestID = mDownloadManager.enqueue(request);
         }
@@ -127,7 +126,7 @@
         try {
             long dlRequest = doStandardEnqueue(largeFile);
 
-             // wait for the download to complete
+            // wait for the download to complete
             waitForDownloadOrTimeout(dlRequest);
 
             ParcelFileDescriptor pfd = mDownloadManager.openDownloadedFile(dlRequest);
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
index a419068..09dcac5 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/Android.mk
@@ -20,7 +20,7 @@
 
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
 
-LOCAL_STATIC_JAVA_LIBRARIES := android-common frameworks-core-util-lib
+LOCAL_STATIC_JAVA_LIBRARIES := android-common mockwebserver
 LOCAL_SDK_VERSION := current
 
 LOCAL_PACKAGE_NAME := DownloadManagerTestApp
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
similarity index 73%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
index 334661d..8e935f8 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerBaseTest.java
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerBaseTest.java
@@ -18,7 +18,6 @@
 
 import android.app.DownloadManager;
 import android.app.DownloadManager.Query;
-import android.app.DownloadManager.Request;
 import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
@@ -26,37 +25,19 @@
 import android.database.Cursor;
 import android.net.ConnectivityManager;
 import android.net.NetworkInfo;
-import android.net.Uri;
 import android.net.wifi.WifiManager;
-import android.os.Bundle;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
 import android.os.SystemClock;
-import android.os.ParcelFileDescriptor.AutoCloseInputStream;
 import android.provider.Settings;
 import android.test.InstrumentationTestCase;
 import android.util.Log;
 
-import java.io.DataInputStream;
-import java.io.DataOutputStream;
 import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.net.URL;
-import java.util.concurrent.TimeoutException;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Random;
 import java.util.Set;
-import java.util.Vector;
-
-import junit.framework.AssertionFailedError;
-
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
+import java.util.concurrent.TimeoutException;
 
 /**
  * Base class for Instrumented tests for the Download Manager.
@@ -64,7 +45,6 @@
 public class DownloadManagerBaseTest extends InstrumentationTestCase {
 
     protected DownloadManager mDownloadManager = null;
-    protected MockWebServer mServer = null;
     protected String mFileType = "text/plain";
     protected Context mContext = null;
     protected MultipleDownloadsCompletedReceiver mReceiver = null;
@@ -77,7 +57,6 @@
     protected static final int HTTP_PARTIAL_CONTENT = 206;
     protected static final int HTTP_NOT_FOUND = 404;
     protected static final int HTTP_SERVICE_UNAVAILABLE = 503;
-    protected String DEFAULT_FILENAME = "somefile.txt";
 
     protected static final int DEFAULT_MAX_WAIT_TIME = 2 * 60 * 1000;  // 2 minutes
     protected static final int DEFAULT_WAIT_POLL_TIME = 5 * 1000;  // 5 seconds
@@ -86,48 +65,6 @@
     protected static final int MAX_WAIT_FOR_DOWNLOAD_TIME = 5 * 60 * 1000; // 5 minutes
     protected static final int MAX_WAIT_FOR_LARGE_DOWNLOAD_TIME = 15 * 60 * 1000; // 15 minutes
 
-    protected static final int DOWNLOAD_TO_SYSTEM_CACHE = 1;
-    protected static final int DOWNLOAD_TO_DOWNLOAD_CACHE_DIR = 2;
-
-    // Just a few popular file types used to return from a download
-    protected enum DownloadFileType {
-        PLAINTEXT,
-        APK,
-        GIF,
-        GARBAGE,
-        UNRECOGNIZED,
-        ZIP
-    }
-
-    protected enum DataType {
-        TEXT,
-        BINARY
-    }
-
-    public static class LoggingRng extends Random {
-
-        /**
-         * Constructor
-         *
-         * Creates RNG with self-generated seed value.
-         */
-        public LoggingRng() {
-            this(SystemClock.uptimeMillis());
-        }
-
-        /**
-         * Constructor
-         *
-         * Creats RNG with given initial seed value
-
-         * @param seed The initial seed value
-         */
-        public LoggingRng(long seed) {
-            super(seed);
-            Log.i(LOG_TAG, "Seeding RNG with value: " + seed);
-        }
-    }
-
     public static class MultipleDownloadsCompletedReceiver extends BroadcastReceiver {
         private volatile int mNumDownloadsCompleted = 0;
         private Set<Long> downloadIds = Collections.synchronizedSet(new HashSet<Long>());
@@ -171,7 +108,7 @@
 
         /**
          * Gets the number of times the {@link #onReceive} callback has been called for the
-         * {@link DownloadManager.ACTION_DOWNLOAD_COMPLETED} action, indicating the number of
+         * {@link DownloadManager#ACTION_DOWNLOAD_COMPLETE} action, indicating the number of
          * downloads completed thus far.
          *
          * @return the number of downloads completed so far.
@@ -241,76 +178,7 @@
     public void setUp() throws Exception {
         mContext = getInstrumentation().getContext();
         mDownloadManager = (DownloadManager)mContext.getSystemService(Context.DOWNLOAD_SERVICE);
-        mServer = new MockWebServer();
         mReceiver = registerNewMultipleDownloadsReceiver();
-        // Note: callers overriding this should call mServer.play() with the desired port #
-    }
-
-    /**
-     * Helper to enqueue a response from the MockWebServer.
-     *
-     * @param status The HTTP status code to return for this response
-     * @param body The body to return in this response
-     * @return Returns the mock web server response that was queued (which can be modified)
-     */
-    private MockResponse enqueueResponse(int status, byte[] body) {
-        return doEnqueueResponse(status).setBody(body);
-
-    }
-
-    /**
-     * Helper to enqueue a response from the MockWebServer.
-     *
-     * @param status The HTTP status code to return for this response
-     * @param bodyFile The body to return in this response
-     * @return Returns the mock web server response that was queued (which can be modified)
-     */
-    private MockResponse enqueueResponse(int status, File bodyFile) {
-        return doEnqueueResponse(status).setBody(bodyFile);
-    }
-
-    /**
-     * Helper for enqueue'ing a response from the MockWebServer.
-     *
-     * @param status The HTTP status code to return for this response
-     * @return Returns the mock web server response that was queued (which can be modified)
-     */
-    private MockResponse doEnqueueResponse(int status) {
-        MockResponse response = new MockResponse().setResponseCode(status);
-        response.addHeader("Content-type", mFileType);
-        mServer.enqueue(response);
-        return response;
-    }
-
-    /**
-     * Helper to generate a random blob of bytes using a given RNG.
-     *
-     * @param size The size of the data to generate
-     * @param type The type of data to generate: currently, one of {@link DataType.TEXT} or
-     *         {@link DataType.BINARY}.
-     * @param rng (optional) The RNG to use; pass null to use
-     * @return The random data that is generated.
-     */
-    private byte[] generateData(int size, DataType type, Random rng) {
-        int min = Byte.MIN_VALUE;
-        int max = Byte.MAX_VALUE;
-
-        // Only use chars in the HTTP ASCII printable character range for Text
-        if (type == DataType.TEXT) {
-            min = 32;
-            max = 126;
-        }
-        byte[] result = new byte[size];
-        Log.i(LOG_TAG, "Generating data of size: " + size);
-
-        if (rng == null) {
-            rng = new LoggingRng();
-        }
-
-        for (int i = 0; i < size; ++i) {
-            result[i] = (byte) (min + rng.nextInt(max - min + 1));
-        }
-        return result;
     }
 
     /**
@@ -324,76 +192,6 @@
     }
 
     /**
-     * Helper to verify the contents of a downloaded file versus a byte[].
-     *
-     * @param actual The file of whose contents to verify
-     * @param expected The data we expect to find in the aforementioned file
-     * @throws IOException if there was a problem reading from the file
-     */
-    private void verifyFileContents(ParcelFileDescriptor actual, byte[] expected)
-            throws IOException {
-        AutoCloseInputStream input = new ParcelFileDescriptor.AutoCloseInputStream(actual);
-        long fileSize = actual.getStatSize();
-
-        assertTrue(fileSize <= Integer.MAX_VALUE);
-        assertEquals(expected.length, fileSize);
-
-        byte[] actualData = new byte[expected.length];
-        assertEquals(input.read(actualData), fileSize);
-        compareByteArrays(actualData, expected);
-    }
-
-    /**
-     * Helper to compare 2 byte arrays.
-     *
-     * @param actual The array whose data we want to verify
-     * @param expected The array of data we expect to see
-     */
-    private void compareByteArrays(byte[] actual, byte[] expected) {
-        assertEquals(actual.length, expected.length);
-        int length = actual.length;
-        for (int i = 0; i < length; ++i) {
-            // assert has a bit of overhead, so only do the assert when the values are not the same
-            if (actual[i] != expected[i]) {
-                fail("Byte arrays are not equal.");
-            }
-        }
-    }
-
-    /**
-     * Gets the MIME content string for a given type
-     *
-     * @param type The MIME type to return
-     * @return the String representation of that MIME content type
-     */
-    protected String getMimeMapping(DownloadFileType type) {
-        switch (type) {
-            case APK:
-                return "application/vnd.android.package-archive";
-            case GIF:
-                return "image/gif";
-            case ZIP:
-                return "application/x-zip-compressed";
-            case GARBAGE:
-                return "zip\\pidy/doo/da";
-            case UNRECOGNIZED:
-                return "application/new.undefined.type.of.app";
-        }
-        return "text/plain";
-    }
-
-    /**
-     * Gets the Uri that should be used to access the mock server
-     *
-     * @param filename The name of the file to try to retrieve from the mock server
-     * @return the Uri to use for access the file on the mock server
-     */
-    private Uri getServerUri(String filename) throws Exception {
-        URL url = mServer.getUrl("/" + filename);
-        return Uri.parse(url.toString());
-    }
-
-    /**
      * Helper to create and register a new MultipleDownloadCompletedReciever
      *
      * This is used to track many simultaneous downloads by keeping count of all the downloads
@@ -738,39 +536,6 @@
     }
 
     /**
-     * Helper to perform a standard enqueue of data to the mock server.
-     * download is performed to the downloads cache dir (NOT systemcache dir)
-     *
-     * @param body The body to return in the response from the server
-     */
-    private long doStandardEnqueue(byte[] body) throws Exception {
-        // Prepare the mock server with a standard response
-        enqueueResponse(HTTP_OK, body);
-        return doCommonStandardEnqueue();
-    }
-
-    /**
-     * Helper to perform a standard enqueue of data to the mock server.
-     *
-     * @param body The body to return in the response from the server, contained in the file
-     */
-    private long doStandardEnqueue(File body) throws Exception {
-        // Prepare the mock server with a standard response
-        enqueueResponse(HTTP_OK, body);
-        return doCommonStandardEnqueue();
-    }
-
-    /**
-     * Helper to do the additional steps (setting title and Uri of default filename) when
-     * doing a standard enqueue request to the server.
-     */
-    private long doCommonStandardEnqueue() throws Exception {
-        Uri uri = getServerUri(DEFAULT_FILENAME);
-        Request request = new Request(uri).setTitle(DEFAULT_FILENAME);
-        return mDownloadManager.enqueue(request);
-    }
-
-    /**
      * Helper to verify an int value in a Cursor
      *
      * @param cursor The cursor containing the query results
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
similarity index 97%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
index 654f747..9c44d61 100644
--- a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestApp.java
+++ b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestApp.java
@@ -16,16 +16,11 @@
 package com.android.frameworks.downloadmanagertests;
 
 import android.app.DownloadManager;
-import android.app.DownloadManager.Query;
 import android.app.DownloadManager.Request;
-import android.content.Context;
-import android.content.Intent;
 import android.database.Cursor;
 import android.net.Uri;
 import android.os.Environment;
 import android.os.ParcelFileDescriptor;
-import android.provider.Settings;
-import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
 import java.io.DataInputStream;
@@ -33,13 +28,8 @@
 import java.io.File;
 import java.io.FileInputStream;
 import java.io.FileOutputStream;
-import java.io.FileWriter;
 import java.util.HashSet;
 
-import coretestutils.http.MockResponse;
-import coretestutils.http.MockWebServer;
-import coretestutils.http.RecordedRequest;
-
 /**
  * Class to test downloading files from a real (not mock) external server.
  */
@@ -243,7 +233,7 @@
 
         Uri remoteUri = getExternalFileUri(filename);
         Request request = new Request(remoteUri);
-        request.setMimeType(getMimeMapping(DownloadFileType.APK));
+        request.setMimeType("application/vnd.android.package-archive");
 
         dlRequest = mDownloadManager.enqueue(request);
 
diff --git a/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java b/core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java
similarity index 100%
rename from core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/DownloadManagerTestRunner.java
rename to core/tests/hosttests/test-apps/DownloadManagerTestApp/src/com/android/frameworks/downloadmanagertests/DownloadManagerTestRunner.java
diff --git a/core/tests/utillib/src/coretestutils/http/MockResponse.java b/core/tests/utillib/src/coretestutils/http/MockResponse.java
deleted file mode 100644
index 5b03e5f..0000000
--- a/core/tests/utillib/src/coretestutils/http/MockResponse.java
+++ /dev/null
@@ -1,239 +0,0 @@
-/*
- * 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 coretestutils.http;
-
-import static coretestutils.http.MockWebServer.ASCII;
-
-import java.io.ByteArrayInputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.InputStream;
-import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-import android.util.Log;
-
-/**
- * A scripted response to be replayed by the mock web server.
- */
-public class MockResponse {
-    private static final byte[] EMPTY_BODY = new byte[0];
-    static final String LOG_TAG = "coretestutils.http.MockResponse";
-
-    private String status = "HTTP/1.1 200 OK";
-    private Map<String, String> headers = new HashMap<String, String>();
-    private byte[] body = EMPTY_BODY;
-    private boolean closeConnectionAfter = false;
-    private String closeConnectionAfterHeader = null;
-    private int closeConnectionAfterXBytes = -1;
-    private int pauseConnectionAfterXBytes = -1;
-    private File bodyExternalFile = null;
-
-    public MockResponse() {
-        addHeader("Content-Length", 0);
-    }
-
-    /**
-     * Returns the HTTP response line, such as "HTTP/1.1 200 OK".
-     */
-    public String getStatus() {
-        return status;
-    }
-
-    public MockResponse setResponseCode(int code) {
-        this.status = "HTTP/1.1 " + code + " OK";
-        return this;
-    }
-
-    /**
-     * Returns the HTTP headers, such as "Content-Length: 0".
-     */
-    public List<String> getHeaders() {
-        List<String> headerStrings = new ArrayList<String>();
-        for (String header : headers.keySet()) {
-            headerStrings.add(header + ": " + headers.get(header));
-        }
-        return headerStrings;
-    }
-
-    public MockResponse addHeader(String header, String value) {
-        headers.put(header.toLowerCase(), value);
-        return this;
-    }
-
-    public MockResponse addHeader(String header, long value) {
-        return addHeader(header, Long.toString(value));
-    }
-
-    public MockResponse removeHeader(String header) {
-        headers.remove(header.toLowerCase());
-        return this;
-    }
-
-    /**
-     * Returns true if the body should come from an external file, false otherwise.
-     */
-    private boolean bodyIsExternal() {
-        return bodyExternalFile != null;
-    }
-
-    /**
-     * Returns an input stream containing the raw HTTP payload.
-     */
-    public InputStream getBody() {
-        if (bodyIsExternal()) {
-            try {
-                return new FileInputStream(bodyExternalFile);
-            } catch (FileNotFoundException e) {
-                Log.e(LOG_TAG, "File not found: " + bodyExternalFile.getAbsolutePath());
-            }
-        }
-        return new ByteArrayInputStream(this.body);
-    }
-
-    public MockResponse setBody(File body) {
-        addHeader("Content-Length", body.length());
-        this.bodyExternalFile = body;
-        return this;
-    }
-
-    public MockResponse setBody(byte[] body) {
-        addHeader("Content-Length", body.length);
-        this.body = body;
-        return this;
-    }
-
-    public MockResponse setBody(String body) {
-        try {
-            return setBody(body.getBytes(ASCII));
-        } catch (UnsupportedEncodingException e) {
-            throw new AssertionError();
-        }
-    }
-
-    /**
-     * Sets the body as chunked.
-     *
-     * Currently chunked body is not supported for external files as bodies.
-     */
-    public MockResponse setChunkedBody(byte[] body, int maxChunkSize) throws IOException {
-        addHeader("Transfer-encoding", "chunked");
-
-        ByteArrayOutputStream bytesOut = new ByteArrayOutputStream();
-        int pos = 0;
-        while (pos < body.length) {
-            int chunkSize = Math.min(body.length - pos, maxChunkSize);
-            bytesOut.write(Integer.toHexString(chunkSize).getBytes(ASCII));
-            bytesOut.write("\r\n".getBytes(ASCII));
-            bytesOut.write(body, pos, chunkSize);
-            bytesOut.write("\r\n".getBytes(ASCII));
-            pos += chunkSize;
-        }
-        bytesOut.write("0\r\n".getBytes(ASCII));
-        this.body = bytesOut.toByteArray();
-        return this;
-    }
-
-    public MockResponse setChunkedBody(String body, int maxChunkSize) throws IOException {
-        return setChunkedBody(body.getBytes(ASCII), maxChunkSize);
-    }
-
-    @Override public String toString() {
-        return status;
-    }
-
-    public boolean shouldCloseConnectionAfter() {
-        return closeConnectionAfter;
-    }
-
-    public MockResponse setCloseConnectionAfter(boolean closeConnectionAfter) {
-        this.closeConnectionAfter = closeConnectionAfter;
-        return this;
-    }
-
-    /**
-     * Sets the header after which sending the server should close the connection.
-     */
-    public MockResponse setCloseConnectionAfterHeader(String header) {
-        closeConnectionAfterHeader = header;
-        setCloseConnectionAfter(true);
-        return this;
-    }
-
-    /**
-     * Returns the header after which sending the server should close the connection.
-     */
-    public String getCloseConnectionAfterHeader() {
-        return closeConnectionAfterHeader;
-    }
-
-    /**
-     * Sets the number of bytes in the body to send before which the server should close the
-     * connection. Set to -1 to unset and send the entire body (default).
-     */
-    public MockResponse setCloseConnectionAfterXBytes(int position) {
-        closeConnectionAfterXBytes = position;
-        setCloseConnectionAfter(true);
-        return this;
-    }
-
-    /**
-     * Returns the number of bytes in the body to send before which the server should close the
-     * connection. Returns -1 if the entire body should be sent (default).
-     */
-    public int getCloseConnectionAfterXBytes() {
-        return closeConnectionAfterXBytes;
-    }
-
-    /**
-     * Sets the number of bytes in the body to send before which the server should pause the
-     * connection (stalls in sending data). Only one pause per response is supported.
-     * Set to -1 to unset pausing (default).
-     */
-    public MockResponse setPauseConnectionAfterXBytes(int position) {
-        pauseConnectionAfterXBytes = position;
-        return this;
-    }
-
-    /**
-     * Returns the number of bytes in the body to send before which the server should pause the
-     * connection (stalls in sending data). (Returns -1 if it should not pause).
-     */
-    public int getPauseConnectionAfterXBytes() {
-        return pauseConnectionAfterXBytes;
-    }
-
-    /**
-     * Returns true if this response is flagged to pause the connection mid-stream, false otherwise
-     */
-    public boolean getShouldPause() {
-        return (pauseConnectionAfterXBytes != -1);
-    }
-
-    /**
-     * Returns true if this response is flagged to close the connection mid-stream, false otherwise
-     */
-    public boolean getShouldClose() {
-        return (closeConnectionAfterXBytes != -1);
-    }
-}
diff --git a/core/tests/utillib/src/coretestutils/http/MockWebServer.java b/core/tests/utillib/src/coretestutils/http/MockWebServer.java
deleted file mode 100644
index c329ffa..0000000
--- a/core/tests/utillib/src/coretestutils/http/MockWebServer.java
+++ /dev/null
@@ -1,426 +0,0 @@
-/*
- * 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 coretestutils.http;
-
-import java.io.BufferedInputStream;
-import java.io.BufferedOutputStream;
-import java.io.ByteArrayOutputStream;
-import java.io.File;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.OutputStream;
-import java.net.MalformedURLException;
-import java.net.ServerSocket;
-import java.net.Socket;
-import java.net.URL;
-import java.util.ArrayList;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Queue;
-import java.util.concurrent.BlockingQueue;
-import java.util.concurrent.Callable;
-import java.util.concurrent.ExecutionException;
-import java.util.concurrent.ExecutorService;
-import java.util.concurrent.Executors;
-import java.util.concurrent.Future;
-import java.util.concurrent.LinkedBlockingQueue;
-import java.util.concurrent.TimeUnit;
-import java.util.concurrent.TimeoutException;
-
-import android.util.Log;
-
-/**
- * A scriptable web server. Callers supply canned responses and the server
- * replays them upon request in sequence.
- *
- * TODO: merge with the version from libcore/support/src/tests/java once it's in.
- */
-public final class MockWebServer {
-    static final String ASCII = "US-ASCII";
-    static final String LOG_TAG = "coretestutils.http.MockWebServer";
-
-    private final BlockingQueue<RecordedRequest> requestQueue
-            = new LinkedBlockingQueue<RecordedRequest>();
-    private final BlockingQueue<MockResponse> responseQueue
-            = new LinkedBlockingQueue<MockResponse>();
-    private int bodyLimit = Integer.MAX_VALUE;
-    private final ExecutorService executor = Executors.newCachedThreadPool();
-    // keep Futures around so we can rethrow any exceptions thrown by Callables
-    private final Queue<Future<?>> futures = new LinkedList<Future<?>>();
-    private final Object downloadPauseLock = new Object();
-    // global flag to signal when downloads should resume on the server
-    private volatile boolean downloadResume = false;
-
-    private int port = -1;
-
-    public int getPort() {
-        if (port == -1) {
-            throw new IllegalStateException("Cannot retrieve port before calling play()");
-        }
-        return port;
-    }
-
-    /**
-     * Returns a URL for connecting to this server.
-     *
-     * @param path the request path, such as "/".
-     */
-    public URL getUrl(String path) throws MalformedURLException {
-        return new URL("http://localhost:" + getPort() + path);
-    }
-
-    /**
-     * Sets the number of bytes of the POST body to keep in memory to the given
-     * limit.
-     */
-    public void setBodyLimit(int maxBodyLength) {
-        this.bodyLimit = maxBodyLength;
-    }
-
-    public void enqueue(MockResponse response) {
-        responseQueue.add(response);
-    }
-
-    /**
-     * Awaits the next HTTP request, removes it, and returns it. Callers should
-     * use this to verify the request sent was as intended.
-     */
-    public RecordedRequest takeRequest() throws InterruptedException {
-        return requestQueue.take();
-    }
-
-    public RecordedRequest takeRequestWithTimeout(long timeoutMillis) throws InterruptedException {
-        return requestQueue.poll(timeoutMillis, TimeUnit.MILLISECONDS);
-    }
-
-    public List<RecordedRequest> drainRequests() {
-        List<RecordedRequest> requests = new ArrayList<RecordedRequest>();
-        requestQueue.drainTo(requests);
-        return requests;
-    }
-
-    /**
-     * Starts the server, serves all enqueued requests, and shuts the server
-     * down using the default (server-assigned) port.
-     */
-    public void play() throws IOException {
-        play(0);
-    }
-
-    /**
-     * Starts the server, serves all enqueued requests, and shuts the server
-     * down.
-     *
-     * @param port The port number to use to listen to connections on; pass in 0 to have the
-     * server automatically assign a free port
-     */
-    public void play(int portNumber) throws IOException {
-        final ServerSocket ss = new ServerSocket(portNumber);
-        ss.setReuseAddress(true);
-        port = ss.getLocalPort();
-        submitCallable(new Callable<Void>() {
-            public Void call() throws Exception {
-                int count = 0;
-                while (true) {
-                    if (count > 0 && responseQueue.isEmpty()) {
-                        ss.close();
-                        executor.shutdown();
-                        return null;
-                    }
-
-                    serveConnection(ss.accept());
-                    count++;
-                }
-            }
-        });
-    }
-
-    private void serveConnection(final Socket s) {
-        submitCallable(new Callable<Void>() {
-            public Void call() throws Exception {
-                InputStream in = new BufferedInputStream(s.getInputStream());
-                OutputStream out = new BufferedOutputStream(s.getOutputStream());
-
-                int sequenceNumber = 0;
-                while (true) {
-                    RecordedRequest request = readRequest(in, sequenceNumber);
-                    if (request == null) {
-                        if (sequenceNumber == 0) {
-                            throw new IllegalStateException("Connection without any request!");
-                        } else {
-                            break;
-                        }
-                    }
-                    requestQueue.add(request);
-                    MockResponse response = computeResponse(request);
-                    writeResponse(out, response);
-                    if (response.shouldCloseConnectionAfter()) {
-                        break;
-                    }
-                    sequenceNumber++;
-                }
-
-                in.close();
-                out.close();
-                return null;
-            }
-        });
-    }
-
-    private void submitCallable(Callable<?> callable) {
-        Future<?> future = executor.submit(callable);
-        futures.add(future);
-    }
-
-    /**
-     * Check for and raise any exceptions that have been thrown by child threads.  Will not block on
-     * children still running.
-     * @throws ExecutionException for the first child thread that threw an exception
-     */
-    public void checkForExceptions() throws ExecutionException, InterruptedException {
-        final int originalSize = futures.size();
-        for (int i = 0; i < originalSize; i++) {
-            Future<?> future = futures.remove();
-            try {
-                future.get(0, TimeUnit.SECONDS);
-            } catch (TimeoutException e) {
-                futures.add(future); // still running
-            }
-        }
-    }
-
-    /**
-     * @param sequenceNumber the index of this request on this connection.
-     */
-    private RecordedRequest readRequest(InputStream in, int sequenceNumber) throws IOException {
-        String request = readAsciiUntilCrlf(in);
-        if (request.equals("")) {
-            return null; // end of data; no more requests
-        }
-
-        List<String> headers = new ArrayList<String>();
-        int contentLength = -1;
-        boolean chunked = false;
-        String header;
-        while (!(header = readAsciiUntilCrlf(in)).equals("")) {
-            headers.add(header);
-            String lowercaseHeader = header.toLowerCase();
-            if (contentLength == -1 && lowercaseHeader.startsWith("content-length:")) {
-                contentLength = Integer.parseInt(header.substring(15).trim());
-            }
-            if (lowercaseHeader.startsWith("transfer-encoding:") &&
-                    lowercaseHeader.substring(18).trim().equals("chunked")) {
-                chunked = true;
-            }
-        }
-
-        boolean hasBody = false;
-        TruncatingOutputStream requestBody = new TruncatingOutputStream();
-        List<Integer> chunkSizes = new ArrayList<Integer>();
-        if (contentLength != -1) {
-            hasBody = true;
-            transfer(contentLength, in, requestBody);
-        } else if (chunked) {
-            hasBody = true;
-            while (true) {
-                int chunkSize = Integer.parseInt(readAsciiUntilCrlf(in).trim(), 16);
-                if (chunkSize == 0) {
-                    readEmptyLine(in);
-                    break;
-                }
-                chunkSizes.add(chunkSize);
-                transfer(chunkSize, in, requestBody);
-                readEmptyLine(in);
-            }
-        }
-
-        if (request.startsWith("GET ")) {
-            if (hasBody) {
-                throw new IllegalArgumentException("GET requests should not have a body!");
-            }
-        } else if (request.startsWith("POST ")) {
-            if (!hasBody) {
-                throw new IllegalArgumentException("POST requests must have a body!");
-            }
-        } else {
-            throw new UnsupportedOperationException("Unexpected method: " + request);
-        }
-
-        return new RecordedRequest(request, headers, chunkSizes,
-                requestBody.numBytesReceived, requestBody.toByteArray(), sequenceNumber);
-    }
-
-    /**
-     * Returns a response to satisfy {@code request}.
-     */
-    private MockResponse computeResponse(RecordedRequest request) throws InterruptedException {
-        if (responseQueue.isEmpty()) {
-            throw new IllegalStateException("Unexpected request: " + request);
-        }
-        return responseQueue.take();
-    }
-
-    private void writeResponse(OutputStream out, MockResponse response) throws IOException {
-        out.write((response.getStatus() + "\r\n").getBytes(ASCII));
-        boolean doCloseConnectionAfterHeader = (response.getCloseConnectionAfterHeader() != null);
-
-        // Send headers
-        String closeConnectionAfterHeader = response.getCloseConnectionAfterHeader();
-        for (String header : response.getHeaders()) {
-            out.write((header + "\r\n").getBytes(ASCII));
-
-            if (doCloseConnectionAfterHeader && header.startsWith(closeConnectionAfterHeader)) {
-                Log.i(LOG_TAG, "Closing connection after header" + header);
-                break;
-            }
-        }
-
-        // Send actual body data
-        if (!doCloseConnectionAfterHeader) {
-            out.write(("\r\n").getBytes(ASCII));
-
-            InputStream body = response.getBody();
-            final int READ_BLOCK_SIZE = 10000;  // process blocks this size
-            byte[] currentBlock = new byte[READ_BLOCK_SIZE];
-            int currentBlockSize = 0;
-            int writtenSoFar = 0;
-
-            boolean shouldPause = response.getShouldPause();
-            boolean shouldClose = response.getShouldClose();
-            int pause = response.getPauseConnectionAfterXBytes();
-            int close = response.getCloseConnectionAfterXBytes();
-
-            // Don't bother pausing if it's set to pause -after- the connection should be dropped
-            if (shouldPause && shouldClose && (pause > close)) {
-                shouldPause = false;
-            }
-
-            // Process each block we read in...
-            while ((currentBlockSize = body.read(currentBlock)) != -1) {
-                int startIndex = 0;
-                int writeLength = currentBlockSize;
-
-                // handle the case of pausing
-                if (shouldPause && (writtenSoFar + currentBlockSize >= pause)) {
-                    writeLength = pause - writtenSoFar;
-                    out.write(currentBlock, 0, writeLength);
-                    out.flush();
-                    writtenSoFar += writeLength;
-
-                    // now pause...
-                    try {
-                        Log.i(LOG_TAG, "Pausing connection after " + pause + " bytes");
-                        // Wait until someone tells us to resume sending...
-                        synchronized(downloadPauseLock) {
-                            while (!downloadResume) {
-                                downloadPauseLock.wait();
-                            }
-                            // reset resume back to false
-                            downloadResume = false;
-                        }
-                    } catch (InterruptedException e) {
-                        Log.e(LOG_TAG, "Server was interrupted during pause in download.");
-                    }
-
-                    startIndex = writeLength;
-                    writeLength = currentBlockSize - writeLength;
-                }
-
-                // handle the case of closing the connection
-                if (shouldClose && (writtenSoFar + writeLength > close)) {
-                    writeLength = close - writtenSoFar;
-                    out.write(currentBlock, startIndex, writeLength);
-                    writtenSoFar += writeLength;
-                    Log.i(LOG_TAG, "Closing connection after " + close + " bytes");
-                    break;
-                }
-                out.write(currentBlock, startIndex, writeLength);
-                writtenSoFar += writeLength;
-            }
-        }
-        out.flush();
-    }
-
-    /**
-     * Transfer bytes from {@code in} to {@code out} until either {@code length}
-     * bytes have been transferred or {@code in} is exhausted.
-     */
-    private void transfer(int length, InputStream in, OutputStream out) throws IOException {
-        byte[] buffer = new byte[1024];
-        while (length > 0) {
-            int count = in.read(buffer, 0, Math.min(buffer.length, length));
-            if (count == -1) {
-                return;
-            }
-            out.write(buffer, 0, count);
-            length -= count;
-        }
-    }
-
-    /**
-     * Returns the text from {@code in} until the next "\r\n", or null if
-     * {@code in} is exhausted.
-     */
-    private String readAsciiUntilCrlf(InputStream in) throws IOException {
-        StringBuilder builder = new StringBuilder();
-        while (true) {
-            int c = in.read();
-            if (c == '\n' && builder.length() > 0 && builder.charAt(builder.length() - 1) == '\r') {
-                builder.deleteCharAt(builder.length() - 1);
-                return builder.toString();
-            } else if (c == -1) {
-                return builder.toString();
-            } else {
-                builder.append((char) c);
-            }
-        }
-    }
-
-    private void readEmptyLine(InputStream in) throws IOException {
-        String line = readAsciiUntilCrlf(in);
-        if (!line.equals("")) {
-            throw new IllegalStateException("Expected empty but was: " + line);
-        }
-    }
-
-    /**
-     * An output stream that drops data after bodyLimit bytes.
-     */
-    private class TruncatingOutputStream extends ByteArrayOutputStream {
-        private int numBytesReceived = 0;
-        @Override public void write(byte[] buffer, int offset, int len) {
-            numBytesReceived += len;
-            super.write(buffer, offset, Math.min(len, bodyLimit - count));
-        }
-        @Override public void write(int oneByte) {
-            numBytesReceived++;
-            if (count < bodyLimit) {
-                super.write(oneByte);
-            }
-        }
-    }
-
-    /**
-     * Trigger the server to resume sending the download
-     */
-    public void doResumeDownload() {
-        synchronized (downloadPauseLock) {
-            downloadResume = true;
-            downloadPauseLock.notifyAll();
-        }
-    }
-}
diff --git a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java b/core/tests/utillib/src/coretestutils/http/RecordedRequest.java
deleted file mode 100644
index 293ff80..0000000
--- a/core/tests/utillib/src/coretestutils/http/RecordedRequest.java
+++ /dev/null
@@ -1,93 +0,0 @@
-/*
- * 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 coretestutils.http;
-
-import java.util.List;
-
-/**
- * An HTTP request that came into the mock web server.
- */
-public final class RecordedRequest {
-    private final String requestLine;
-    private final List<String> headers;
-    private final List<Integer> chunkSizes;
-    private final int bodySize;
-    private final byte[] body;
-    private final int sequenceNumber;
-
-    RecordedRequest(String requestLine, List<String> headers, List<Integer> chunkSizes,
-            int bodySize, byte[] body, int sequenceNumber) {
-        this.requestLine = requestLine;
-        this.headers = headers;
-        this.chunkSizes = chunkSizes;
-        this.bodySize = bodySize;
-        this.body = body;
-        this.sequenceNumber = sequenceNumber;
-    }
-
-    public String getRequestLine() {
-        return requestLine;
-    }
-
-    public List<String> getHeaders() {
-        return headers;
-    }
-
-    /**
-     * Returns the sizes of the chunks of this request's body, or an empty list
-     * if the request's body was empty or unchunked.
-     */
-    public List<Integer> getChunkSizes() {
-        return chunkSizes;
-    }
-
-    /**
-     * Returns the total size of the body of this POST request (before
-     * truncation).
-     */
-    public int getBodySize() {
-        return bodySize;
-    }
-
-    /**
-     * Returns the body of this POST request. This may be truncated.
-     */
-    public byte[] getBody() {
-        return body;
-    }
-
-    /**
-     * Returns the index of this request on its HTTP connection. Since a single
-     * HTTP connection may serve multiple requests, each request is assigned its
-     * own sequence number.
-     */
-    public int getSequenceNumber() {
-        return sequenceNumber;
-    }
-
-    @Override public String toString() {
-        return requestLine;
-    }
-
-    public String getMethod() {
-        return getRequestLine().split(" ")[0];
-    }
-
-    public String getPath() {
-        return getRequestLine().split(" ")[1];
-    }
-}
diff --git a/services/java/com/android/server/wm/AppWindowToken.java b/services/java/com/android/server/wm/AppWindowToken.java
index c24c2d9..d1f1b44 100644
--- a/services/java/com/android/server/wm/AppWindowToken.java
+++ b/services/java/com/android/server/wm/AppWindowToken.java
@@ -393,8 +393,8 @@
                 if (!win.isDrawnLw()) {
                     Slog.v(WindowManagerService.TAG, "Not displayed: s=" + win.mWinAnimator.mSurface
                             + " pv=" + win.mPolicyVisibility
-                            + " dp=" + win.mDrawPending
-                            + " cdp=" + win.mCommitDrawPending
+                            + " dp=" + win.mWinAnimator.mDrawPending
+                            + " cdp=" + win.mWinAnimator.mCommitDrawPending
                             + " ah=" + win.mAttachedHidden
                             + " th="
                             + (win.mAppToken != null
diff --git a/services/java/com/android/server/wm/WindowAnimator.java b/services/java/com/android/server/wm/WindowAnimator.java
index eddfe92..26f4d0d 100644
--- a/services/java/com/android/server/wm/WindowAnimator.java
+++ b/services/java/com/android/server/wm/WindowAnimator.java
@@ -4,6 +4,9 @@
 
 import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
 
+import static com.android.server.wm.WindowManagerService.LayoutFields.SET_UPDATE_ROTATION;
+import static com.android.server.wm.WindowManagerService.LayoutFields.SET_WALLPAPER_MAY_CHANGE;
+
 import android.content.Context;
 import android.os.SystemClock;
 import android.util.Log;
@@ -30,7 +33,6 @@
     final WindowManagerPolicy mPolicy;
 
     boolean mAnimating;
-    boolean mUpdateRotation;
     boolean mTokenMayBeDrawn;
     boolean mForceHiding;
     WindowState mWindowAnimationBackground;
@@ -61,9 +63,10 @@
     // seen.
     WindowState mWindowDetachedWallpaper = null;
     WindowState mDetachedWallpaper = null;
-    boolean mWallpaperMayChange;
     DimSurface mWindowAnimationBackgroundSurface = null;
 
+    int mBulkUpdateParams = 0;
+
     WindowAnimator(final WindowManagerService service, final Context context,
             final WindowManagerPolicy policy) {
         mService = service;
@@ -77,7 +80,7 @@
                     "Detached wallpaper changed from " + mWindowDetachedWallpaper
                     + " to " + mDetachedWallpaper);
             mWindowDetachedWallpaper = mDetachedWallpaper;
-            mWallpaperMayChange = true;
+            mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
         }
 
         if (mWindowAnimationBackgroundColor != 0) {
@@ -147,10 +150,9 @@
                 (mScreenRotationAnimation.isAnimating() ||
                         mScreenRotationAnimation.mFinishAnimReady)) {
             if (mScreenRotationAnimation.stepAnimationLocked(mCurrentTime)) {
-                mUpdateRotation = false;
                 mAnimating = true;
             } else {
-                mUpdateRotation = true;
+                mBulkUpdateParams |= SET_UPDATE_ROTATION;
                 mScreenRotationAnimation.kill();
                 mScreenRotationAnimation = null;
             }
@@ -217,7 +219,7 @@
                 }
 
                 if (wasAnimating && !winAnimator.mAnimating && mService.mWallpaperTarget == w) {
-                    mWallpaperMayChange = true;
+                    mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
                     mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
                     if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
                         mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 2");
@@ -270,7 +272,7 @@
                     }
                     if (changed && (attrs.flags
                             & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
-                        mWallpaperMayChange = true;
+                        mBulkUpdateParams |= SET_WALLPAPER_MAY_CHANGE;
                         mPendingLayoutChanges |= WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
                         if (WindowManagerService.DEBUG_LAYOUT_REPEATS) {
                             mService.debugLayoutRepeats("updateWindowsAndWallpaperLocked 4");
@@ -297,8 +299,8 @@
                         if (!w.isDrawnLw()) {
                             Slog.v(TAG, "Not displayed: s=" + winAnimator.mSurface
                                     + " pv=" + w.mPolicyVisibility
-                                    + " dp=" + w.mDrawPending
-                                    + " cdp=" + w.mCommitDrawPending
+                                    + " dp=" + winAnimator.mDrawPending
+                                    + " cdp=" + winAnimator.mCommitDrawPending
                                     + " ah=" + w.mAttachedHidden
                                     + " th=" + atoken.hiddenRequested
                                     + " a=" + winAnimator.mAnimating);
@@ -386,7 +388,6 @@
 
     private void performAnimationsLocked() {
         mTokenMayBeDrawn = false;
-        mService.mInnerFields.mWallpaperMayChange = false;
         mForceHiding = false;
         mDetachedWallpaper = null;
         mWindowAnimationBackground = null;
@@ -399,11 +400,10 @@
         }
     }
 
-
     void animate() {
         mPendingLayoutChanges = 0;
-        mWallpaperMayChange = false;
         mCurrentTime = SystemClock.uptimeMillis();
+        mBulkUpdateParams = 0;
 
         // Update animations of all applications, including those
         // associated with exiting/removed apps
@@ -445,8 +445,8 @@
             Surface.closeTransaction();
         }
 
-        if (mWallpaperMayChange) {
-            mService.notifyWallpaperMayChange();
+        if (mBulkUpdateParams != 0) {
+            mService.bulkSetParameters(mBulkUpdateParams);
         }
     }
 
diff --git a/services/java/com/android/server/wm/WindowManagerService.java b/services/java/com/android/server/wm/WindowManagerService.java
index 3d60c6b..a91e716 100644
--- a/services/java/com/android/server/wm/WindowManagerService.java
+++ b/services/java/com/android/server/wm/WindowManagerService.java
@@ -589,7 +589,10 @@
 
     /** Pulled out of performLayoutAndPlaceSurfacesLockedInner in order to refactor into multiple
      * methods. */
-    class LayoutAndSurfaceFields {
+    class LayoutFields {
+        static final int SET_UPDATE_ROTATION        = 1 << 0;
+        static final int SET_WALLPAPER_MAY_CHANGE   = 1 << 1;
+
         boolean mWallpaperForceHidingChanged = false;
         boolean mWallpaperMayChange = false;
         boolean mOrientationChangeComplete = true;
@@ -600,8 +603,9 @@
         private boolean mSyswin = false;
         private float mScreenBrightness = -1;
         private float mButtonBrightness = -1;
+        private boolean mUpdateRotation = false;
     }
-    LayoutAndSurfaceFields mInnerFields = new LayoutAndSurfaceFields();
+    LayoutFields mInnerFields = new LayoutFields();
 
     /** Only do a maximum of 6 repeated layouts. After that quit */
     private int mLayoutRepeatCount;
@@ -1547,6 +1551,7 @@
     static final int ADJUST_WALLPAPER_VISIBILITY_CHANGED = 1<<2;
 
     int adjustWallpaperWindowsLocked() {
+        mInnerFields.mWallpaperMayChange = false;
         int changed = 0;
 
         final int dw = mAppDisplayWidth;
@@ -1584,8 +1589,8 @@
                 }
             }
             if (DEBUG_WALLPAPER) Slog.v(TAG, "Win " + w + ": readyfordisplay="
-                    + w.isReadyForDisplay() + " drawpending=" + w.mDrawPending
-                    + " commitdrawpending=" + w.mCommitDrawPending);
+                    + w.isReadyForDisplay() + " drawpending=" + w.mWinAnimator.mDrawPending
+                    + " commitdrawpending=" + w.mWinAnimator.mCommitDrawPending);
             if ((w.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0 && w.isReadyForDisplay()
                     && (mWallpaperTarget == w || w.isDrawnLw())) {
                 if (DEBUG_WALLPAPER) Slog.v(TAG,
@@ -2944,7 +2949,7 @@
         final long origId = Binder.clearCallingIdentity();
         synchronized(mWindowMap) {
             WindowState win = windowForClientLocked(session, client, false);
-            if (win != null && win.finishDrawingLocked()) {
+            if (win != null && win.mWinAnimator.finishDrawingLocked()) {
                 if ((win.mAttrs.flags&FLAG_SHOW_WALLPAPER) != 0) {
                     adjustWallpaperWindowsLocked();
                 }
@@ -6651,6 +6656,7 @@
         public static final int REPORT_HARD_KEYBOARD_STATUS_CHANGE = 22;
         public static final int BOOT_TIMEOUT = 23;
         public static final int WAITING_FOR_DRAWN_TIMEOUT = 24;
+        public static final int BULK_UPDATE_PARAMETERS = 25;
 
         private Session mLastReportedHold;
 
@@ -7061,6 +7067,21 @@
                     }
                     break;
                 }
+
+                case BULK_UPDATE_PARAMETERS: {
+                    synchronized (mWindowMap) {
+                        // TODO(cmautner): As the number of bits grows, use masks of bit groups to
+                        //  eliminate unnecessary tests.
+                        if ((msg.arg1 & LayoutFields.SET_UPDATE_ROTATION) != 0) {
+                            mInnerFields.mUpdateRotation = true;
+                        }
+                        if ((msg.arg1 & LayoutFields.SET_WALLPAPER_MAY_CHANGE) != 0) {
+                            mInnerFields.mWallpaperMayChange = true;
+                        }
+
+                        requestTraversalLocked();
+                    }
+                }
             }
         }
     }
@@ -7995,7 +8016,6 @@
             }
         }
         mInnerFields.mAdjResult |= adjustWallpaperWindowsLocked();
-        mInnerFields.mWallpaperMayChange = false;
         mInnerFields.mWallpaperForceHidingChanged = false;
         if (DEBUG_WALLPAPER) Slog.v(TAG, "****** OLD: " + oldWallpaper
                 + " NEW: " + mWallpaperTarget
@@ -8026,6 +8046,7 @@
     }
 
     private void updateResizingWindows(final WindowState w) {
+        final WindowStateAnimator winAnimator = w.mWinAnimator;
         if (!w.mAppFreezing && w.mLayoutSeq == mLayoutSeq) {
             w.mContentInsetsChanged |=
                 !w.mLastContentInsets.equals(w.mContentInsets);
@@ -8045,7 +8066,7 @@
             w.mLastFrame.set(w.mFrame);
             if (w.mContentInsetsChanged
                     || w.mVisibleInsetsChanged
-                    || w.mWinAnimator.mSurfaceResized
+                    || winAnimator.mSurfaceResized
                     || configChanged) {
                 if (DEBUG_RESIZE || DEBUG_ORIENTATION) {
                     Slog.v(TAG, "Resize reasons: "
@@ -8067,8 +8088,8 @@
                     if (DEBUG_ORIENTATION) Slog.v(TAG,
                             "Orientation start waiting for draw in "
                             + w + ", surface " + w.mWinAnimator.mSurface);
-                    w.mDrawPending = true;
-                    w.mCommitDrawPending = false;
+                    winAnimator.mDrawPending = true;
+                    winAnimator.mCommitDrawPending = false;
                     w.mReadyToShow = false;
                     if (w.mAppToken != null) {
                         w.mAppToken.allDrawn = false;
@@ -8377,7 +8398,7 @@
             // Moved from updateWindowsAndWallpaperLocked().
             if (winAnimator.mSurface != null) {
                 // Take care of the window being ready to display.
-                if (w.commitFinishDrawingLocked(currentTime)) {
+                if (winAnimator.commitFinishDrawingLocked(currentTime)) {
                     if ((w.mAttrs.flags
                             & WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER) != 0) {
                         if (WindowManagerService.DEBUG_WALLPAPER) Slog.v(TAG,
@@ -8439,6 +8460,7 @@
             do {
                 i--;
                 WindowState win = mResizingWindows.get(i);
+                final WindowStateAnimator winAnimator = win.mWinAnimator; 
                 try {
                     if (DEBUG_RESIZE || DEBUG_ORIENTATION) Slog.v(TAG,
                             "Reporting new frame to " + win + ": " + win.mCompatFrame);
@@ -8450,20 +8472,20 @@
                     if ((DEBUG_RESIZE || DEBUG_ORIENTATION || DEBUG_CONFIGURATION)
                             && configChanged) {
                         Slog.i(TAG, "Sending new config to window " + win + ": "
-                                + win.mWinAnimator.mSurfaceW + "x" + win.mWinAnimator.mSurfaceH
+                                + winAnimator.mSurfaceW + "x" + winAnimator.mSurfaceH
                                 + " / " + mCurConfiguration + " / 0x"
                                 + Integer.toHexString(diff));
                     }
                     win.mConfiguration = mCurConfiguration;
-                    if (DEBUG_ORIENTATION && win.mDrawPending) Slog.i(
+                    if (DEBUG_ORIENTATION && winAnimator.mDrawPending) Slog.i(
                             TAG, "Resizing " + win + " WITH DRAW PENDING"); 
-                    win.mClient.resized((int)win.mWinAnimator.mSurfaceW,
-                            (int)win.mWinAnimator.mSurfaceH,
-                            win.mLastContentInsets, win.mLastVisibleInsets, win.mDrawPending,
-                            configChanged ? win.mConfiguration : null);
+                    win.mClient.resized((int)winAnimator.mSurfaceW,
+                            (int)winAnimator.mSurfaceH,
+                            win.mLastContentInsets, win.mLastVisibleInsets,
+                            winAnimator.mDrawPending, configChanged ? win.mConfiguration : null);
                     win.mContentInsetsChanged = false;
                     win.mVisibleInsetsChanged = false;
-                    win.mWinAnimator.mSurfaceResized = false;
+                    winAnimator.mSurfaceResized = false;
                 } catch (RemoteException e) {
                     win.mOrientationChanging = false;
                 }
@@ -8574,17 +8596,17 @@
             mTurnOnScreen = false;
         }
 
-        if (mAnimator.mUpdateRotation) {
+        if (mInnerFields.mUpdateRotation) {
             if (DEBUG_ORIENTATION) Slog.d(TAG, "Performing post-rotate rotation");
             if (updateRotationUncheckedLocked(false)) {
                 mH.sendEmptyMessage(H.SEND_NEW_CONFIGURATION);
             } else {
-                mAnimator.mUpdateRotation = false;
+                mInnerFields.mUpdateRotation = false;
             }
         }
 
         if (mInnerFields.mOrientationChangeComplete && !mLayoutNeeded &&
-                !mAnimator.mUpdateRotation) {
+                !mInnerFields.mUpdateRotation) {
             checkDrawnWindowsLocked();
         }
 
@@ -9646,15 +9668,14 @@
         requestTraversalLocked();
     }
 
-    void notifyWallpaperMayChange() {
-        mInnerFields.mWallpaperMayChange = true;
-        requestTraversalLocked();
-    }
-
     void debugLayoutRepeats(final String msg) {
         if (mLayoutRepeatCount >= LAYOUT_REPEAT_THRESHOLD) {
             Slog.v(TAG, "Layouts looping: " + msg);
             Slog.v(TAG, "mPendingLayoutChanges = 0x" + Integer.toHexString(mPendingLayoutChanges));
         }
     }
+
+    void bulkSetParameters(final int bulkUpdateParams) {
+        mH.sendMessage(mH.obtainMessage(H.BULK_UPDATE_PARAMETERS, bulkUpdateParams, 0));
+    }
 }
diff --git a/services/java/com/android/server/wm/WindowState.java b/services/java/com/android/server/wm/WindowState.java
index d22b17c..7f63429 100644
--- a/services/java/com/android/server/wm/WindowState.java
+++ b/services/java/com/android/server/wm/WindowState.java
@@ -19,7 +19,6 @@
 import static android.view.WindowManager.LayoutParams.FIRST_SUB_WINDOW;
 import static android.view.WindowManager.LayoutParams.FLAG_COMPATIBLE_WINDOW;
 import static android.view.WindowManager.LayoutParams.LAST_SUB_WINDOW;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD;
 import static android.view.WindowManager.LayoutParams.TYPE_INPUT_METHOD_DIALOG;
 import static android.view.WindowManager.LayoutParams.TYPE_WALLPAPER;
@@ -207,15 +206,6 @@
     // when in that case until the layout is done.
     boolean mLayoutNeeded;
 
-    // This is set after the Surface has been created but before the
-    // window has been drawn.  During this time the surface is hidden.
-    boolean mDrawPending;
-
-    // This is set after the window has finished drawing for the first
-    // time but before its surface is shown.  The surface will be
-    // displayed when the next layout is run.
-    boolean mCommitDrawPending;
-
     // This is set during the time after the window's drawing has been
     // committed, and before its surface is actually shown.  It is used
     // to delay showing the surface until all windows in a token are ready
@@ -595,36 +585,6 @@
         return mAppToken != null ? mAppToken.firstWindowDrawn : false;
     }
 
-    // TODO(cmautner): Move to WindowStateAnimator
-    boolean finishDrawingLocked() {
-        if (mDrawPending) {
-            if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v(
-                TAG, "finishDrawingLocked: " + this + " in "
-                        + mWinAnimator.mSurface);
-            mCommitDrawPending = true;
-            mDrawPending = false;
-            return true;
-        }
-        return false;
-    }
-
-    // TODO(cmautner): Move to WindowStateAnimator
-    // This must be called while inside a transaction.
-    boolean commitFinishDrawingLocked(long currentTime) {
-        //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface);
-        if (!mCommitDrawPending) {
-            return false;
-        }
-        mCommitDrawPending = false;
-        mReadyToShow = true;
-        final boolean starting = mAttrs.type == TYPE_APPLICATION_STARTING;
-        final AppWindowToken atoken = mAppToken;
-        if (atoken == null || atoken.allDrawn || starting) {
-            mWinAnimator.performShowLocked();
-        }
-        return true;
-    }
-
     boolean isIdentityMatrix(float dsdx, float dtdx, float dsdy, float dtdy) {
         if (dsdx < .99999f || dsdx > 1.00001f) return false;
         if (dtdy < .99999f || dtdy > 1.00001f) return false;
@@ -782,7 +742,7 @@
      */
     public boolean isDrawnLw() {
         return mWinAnimator.mSurface != null && !mDestroying
-            && !mDrawPending && !mCommitDrawPending;
+            && !mWinAnimator.mDrawPending && !mWinAnimator.mCommitDrawPending;
     }
 
     /**
@@ -1087,8 +1047,8 @@
         }
         mWinAnimator.dump(pw, prefix, dumpAll);
         if (dumpAll) {
-            pw.print(prefix); pw.print("mDrawPending="); pw.print(mDrawPending);
-                    pw.print(" mCommitDrawPending="); pw.print(mCommitDrawPending);
+            pw.print(prefix); pw.print("mDrawPending="); pw.print(mWinAnimator.mDrawPending);
+                    pw.print(" mCommitDrawPending="); pw.print(mWinAnimator.mCommitDrawPending);
                     pw.print(" mReadyToShow="); pw.print(mReadyToShow);
                     pw.print(" mHasDrawn="); pw.println(mHasDrawn);
         }
diff --git a/services/java/com/android/server/wm/WindowStateAnimator.java b/services/java/com/android/server/wm/WindowStateAnimator.java
index d1539ba..e99340c 100644
--- a/services/java/com/android/server/wm/WindowStateAnimator.java
+++ b/services/java/com/android/server/wm/WindowStateAnimator.java
@@ -98,6 +98,15 @@
     // an enter animation.
     boolean mEnterAnimationPending;
 
+    // This is set after the Surface has been created but before the
+    // window has been drawn.  During this time the surface is hidden.
+    boolean mDrawPending;
+
+    // This is set after the window has finished drawing for the first
+    // time but before its surface is shown.  The surface will be
+    // displayed when the next layout is run.
+    boolean mCommitDrawPending;
+
     public WindowStateAnimator(final WindowManagerService service, final WindowState win,
                                final WindowState attachedWindow) {
         mService = service;
@@ -347,14 +356,41 @@
         }
     }
 
+    boolean finishDrawingLocked() {
+        if (mDrawPending) {
+            if (SHOW_TRANSACTIONS || WindowManagerService.DEBUG_ORIENTATION) Slog.v(
+                TAG, "finishDrawingLocked: " + this + " in " + mSurface);
+            mCommitDrawPending = true;
+            mDrawPending = false;
+            return true;
+        }
+        return false;
+    }
+
+    // This must be called while inside a transaction.
+    boolean commitFinishDrawingLocked(long currentTime) {
+        //Slog.i(TAG, "commitFinishDrawingLocked: " + mSurface);
+        if (!mCommitDrawPending) {
+            return false;
+        }
+        mCommitDrawPending = false;
+        mWin.mReadyToShow = true;
+        final boolean starting = mWin.mAttrs.type == TYPE_APPLICATION_STARTING;
+        final AppWindowToken atoken = mWin.mAppToken;
+        if (atoken == null || atoken.allDrawn || starting) {
+            performShowLocked();
+        }
+        return true;
+    }
+
     Surface createSurfaceLocked() {
         if (mSurface == null) {
             mReportDestroySurface = false;
             mSurfacePendingDestroy = false;
             if (WindowManagerService.DEBUG_ORIENTATION) Slog.i(TAG,
                     "createSurface " + this + ": DRAW NOW PENDING");
-            mWin.mDrawPending = true;
-            mWin.mCommitDrawPending = false;
+            mDrawPending = true;
+            mCommitDrawPending = false;
             mWin.mReadyToShow = false;
             if (mWin.mAppToken != null) {
                 mWin.mAppToken.allDrawn = false;
@@ -471,8 +507,8 @@
         }
 
         if (mSurface != null) {
-            mWin.mDrawPending = false;
-            mWin.mCommitDrawPending = false;
+            mDrawPending = false;
+            mCommitDrawPending = false;
             mWin.mReadyToShow = false;
 
             int i = mWin.mChildWindows.size();
diff --git a/voip/java/android/net/rtp/AudioGroup.java b/voip/java/android/net/rtp/AudioGroup.java
index 3e7ace8..8c19062 100644
--- a/voip/java/android/net/rtp/AudioGroup.java
+++ b/voip/java/android/net/rtp/AudioGroup.java
@@ -142,34 +142,34 @@
     private native void nativeSetMode(int mode);
 
     // Package-private method used by AudioStream.join().
-    synchronized void add(AudioStream stream, AudioCodec codec, int dtmfType) {
+    synchronized void add(AudioStream stream) {
         if (!mStreams.containsKey(stream)) {
             try {
-                int socket = stream.dup();
+                AudioCodec codec = stream.getCodec();
                 String codecSpec = String.format("%d %s %s", codec.type,
                         codec.rtpmap, codec.fmtp);
-                nativeAdd(stream.getMode(), socket,
+                int id = nativeAdd(stream.getMode(), stream.getSocket(),
                         stream.getRemoteAddress().getHostAddress(),
-                        stream.getRemotePort(), codecSpec, dtmfType);
-                mStreams.put(stream, socket);
+                        stream.getRemotePort(), codecSpec, stream.getDtmfType());
+                mStreams.put(stream, id);
             } catch (NullPointerException e) {
                 throw new IllegalStateException(e);
             }
         }
     }
 
-    private native void nativeAdd(int mode, int socket, String remoteAddress,
+    private native int nativeAdd(int mode, int socket, String remoteAddress,
             int remotePort, String codecSpec, int dtmfType);
 
     // Package-private method used by AudioStream.join().
     synchronized void remove(AudioStream stream) {
-        Integer socket = mStreams.remove(stream);
-        if (socket != null) {
-            nativeRemove(socket);
+        Integer id = mStreams.remove(stream);
+        if (id != null) {
+            nativeRemove(id);
         }
     }
 
-    private native void nativeRemove(int socket);
+    private native void nativeRemove(int id);
 
     /**
      * Sends a DTMF digit to every {@link AudioStream} in this group. Currently
@@ -192,15 +192,14 @@
      * Removes every {@link AudioStream} in this group.
      */
     public void clear() {
-        synchronized (this) {
-            mStreams.clear();
-            nativeRemove(-1);
+        for (AudioStream stream : getStreams()) {
+            stream.join(null);
         }
     }
 
     @Override
     protected void finalize() throws Throwable {
-        clear();
+        nativeRemove(0);
         super.finalize();
     }
 }
diff --git a/voip/java/android/net/rtp/AudioStream.java b/voip/java/android/net/rtp/AudioStream.java
index b7874f7..5cd1abc 100644
--- a/voip/java/android/net/rtp/AudioStream.java
+++ b/voip/java/android/net/rtp/AudioStream.java
@@ -94,7 +94,7 @@
                 mGroup = null;
             }
             if (group != null) {
-                group.add(this, mCodec, mDtmfType);
+                group.add(this);
                 mGroup = group;
             }
         }
diff --git a/voip/java/android/net/rtp/RtpStream.java b/voip/java/android/net/rtp/RtpStream.java
index e94ac42..b9d75cd 100644
--- a/voip/java/android/net/rtp/RtpStream.java
+++ b/voip/java/android/net/rtp/RtpStream.java
@@ -54,7 +54,7 @@
     private int mRemotePort = -1;
     private int mMode = MODE_NORMAL;
 
-    private int mNative;
+    private int mSocket = -1;
     static {
         System.loadLibrary("rtp_jni");
     }
@@ -165,7 +165,9 @@
         mRemotePort = port;
     }
 
-    synchronized native int dup();
+    int getSocket() {
+        return mSocket;
+    }
 
     /**
      * Releases allocated resources. The stream becomes inoperable after calling
@@ -175,13 +177,15 @@
      * @see #isBusy()
      */
     public void release() {
-        if (isBusy()) {
-            throw new IllegalStateException("Busy");
+        synchronized (this) {
+            if (isBusy()) {
+                throw new IllegalStateException("Busy");
+            }
+            close();
         }
-        close();
     }
 
-    private synchronized native void close();
+    private native void close();
 
     @Override
     protected void finalize() throws Throwable {
diff --git a/voip/jni/rtp/AudioGroup.cpp b/voip/jni/rtp/AudioGroup.cpp
index b9bbd16..673a650 100644
--- a/voip/jni/rtp/AudioGroup.cpp
+++ b/voip/jni/rtp/AudioGroup.cpp
@@ -478,7 +478,7 @@
     bool setMode(int mode);
     bool sendDtmf(int event);
     bool add(AudioStream *stream);
-    bool remove(int socket);
+    bool remove(AudioStream *stream);
     bool platformHasAec() { return mPlatformHasAec; }
 
 private:
@@ -691,20 +691,19 @@
     return true;
 }
 
-bool AudioGroup::remove(int socket)
+bool AudioGroup::remove(AudioStream *stream)
 {
     mNetworkThread->requestExitAndWait();
 
-    for (AudioStream *stream = mChain; stream->mNext; stream = stream->mNext) {
-        AudioStream *target = stream->mNext;
-        if (target->mSocket == socket) {
-            if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, socket, NULL)) {
+    for (AudioStream *chain = mChain; chain->mNext; chain = chain->mNext) {
+        if (chain->mNext == stream) {
+            if (epoll_ctl(mEventQueue, EPOLL_CTL_DEL, stream->mSocket, NULL)) {
                 ALOGE("epoll_ctl: %s", strerror(errno));
                 return false;
             }
-            stream->mNext = target->mNext;
-            ALOGD("stream[%d] leaves group[%d]", socket, mDeviceSocket);
-            delete target;
+            chain->mNext = stream->mNext;
+            ALOGD("stream[%d] leaves group[%d]", stream->mSocket, mDeviceSocket);
+            delete stream;
             break;
         }
     }
@@ -931,7 +930,7 @@
 static jfieldID gNative;
 static jfieldID gMode;
 
-void add(JNIEnv *env, jobject thiz, jint mode,
+int add(JNIEnv *env, jobject thiz, jint mode,
     jint socket, jstring jRemoteAddress, jint remotePort,
     jstring jCodecSpec, jint dtmfType)
 {
@@ -943,16 +942,22 @@
     sockaddr_storage remote;
     if (parse(env, jRemoteAddress, remotePort, &remote) < 0) {
         // Exception already thrown.
-        return;
+        return 0;
     }
     if (!jCodecSpec) {
         jniThrowNullPointerException(env, "codecSpec");
-        return;
+        return 0;
     }
     const char *codecSpec = env->GetStringUTFChars(jCodecSpec, NULL);
     if (!codecSpec) {
         // Exception already thrown.
-        return;
+        return 0;
+    }
+    socket = dup(socket);
+    if (socket == -1) {
+        jniThrowException(env, "java/lang/IllegalStateException",
+            "cannot get stream socket");
+        return 0;
     }
 
     // Create audio codec.
@@ -1001,7 +1006,7 @@
 
     // Succeed.
     env->SetIntField(thiz, gNative, (int)group);
-    return;
+    return (int)stream;
 
 error:
     delete group;
@@ -1009,13 +1014,14 @@
     delete codec;
     close(socket);
     env->SetIntField(thiz, gNative, 0);
+    return 0;
 }
 
-void remove(JNIEnv *env, jobject thiz, jint socket)
+void remove(JNIEnv *env, jobject thiz, jint stream)
 {
     AudioGroup *group = (AudioGroup *)env->GetIntField(thiz, gNative);
     if (group) {
-        if (socket == -1 || !group->remove(socket)) {
+        if (!stream || !group->remove((AudioStream *)stream)) {
             delete group;
             env->SetIntField(thiz, gNative, 0);
         }
@@ -1039,7 +1045,7 @@
 }
 
 JNINativeMethod gMethods[] = {
-    {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)V", (void *)add},
+    {"nativeAdd", "(IILjava/lang/String;ILjava/lang/String;I)I", (void *)add},
     {"nativeRemove", "(I)V", (void *)remove},
     {"nativeSetMode", "(I)V", (void *)setMode},
     {"nativeSendDtmf", "(I)V", (void *)sendDtmf},
diff --git a/voip/jni/rtp/RtpStream.cpp b/voip/jni/rtp/RtpStream.cpp
index 6540099..bfe8e24 100644
--- a/voip/jni/rtp/RtpStream.cpp
+++ b/voip/jni/rtp/RtpStream.cpp
@@ -33,11 +33,11 @@
 
 namespace {
 
-jfieldID gNative;
+jfieldID gSocket;
 
 jint create(JNIEnv *env, jobject thiz, jstring jAddress)
 {
-    env->SetIntField(thiz, gNative, -1);
+    env->SetIntField(thiz, gSocket, -1);
 
     sockaddr_storage ss;
     if (parse(env, jAddress, 0, &ss) < 0) {
@@ -58,7 +58,7 @@
         &((sockaddr_in *)&ss)->sin_port : &((sockaddr_in6 *)&ss)->sin6_port;
     uint16_t port = ntohs(*p);
     if ((port & 1) == 0) {
-        env->SetIntField(thiz, gNative, socket);
+        env->SetIntField(thiz, gSocket, socket);
         return port;
     }
     ::close(socket);
@@ -75,7 +75,7 @@
             *p = htons(port);
 
             if (bind(socket, (sockaddr *)&ss, sizeof(ss)) == 0) {
-                env->SetIntField(thiz, gNative, socket);
+                env->SetIntField(thiz, gSocket, socket);
                 return port;
             }
         }
@@ -86,25 +86,15 @@
     return -1;
 }
 
-jint dup(JNIEnv *env, jobject thiz)
-{
-    int socket = ::dup(env->GetIntField(thiz, gNative));
-    if (socket == -1) {
-        jniThrowException(env, "java/lang/IllegalStateException", strerror(errno));
-    }
-    return socket;
-}
-
 void close(JNIEnv *env, jobject thiz)
 {
-    int socket = env->GetIntField(thiz, gNative);
+    int socket = env->GetIntField(thiz, gSocket);
     ::close(socket);
-    env->SetIntField(thiz, gNative, -1);
+    env->SetIntField(thiz, gSocket, -1);
 }
 
 JNINativeMethod gMethods[] = {
     {"create", "(Ljava/lang/String;)I", (void *)create},
-    {"dup", "()I", (void *)dup},
     {"close", "()V", (void *)close},
 };
 
@@ -114,7 +104,7 @@
 {
     jclass clazz;
     if ((clazz = env->FindClass("android/net/rtp/RtpStream")) == NULL ||
-        (gNative = env->GetFieldID(clazz, "mNative", "I")) == NULL ||
+        (gSocket = env->GetFieldID(clazz, "mSocket", "I")) == NULL ||
         env->RegisterNatives(clazz, gMethods, NELEM(gMethods)) < 0) {
         ALOGE("JNI registration failed");
         return -1;
diff --git a/wifi/java/android/net/wifi/WifiConfigStore.java b/wifi/java/android/net/wifi/WifiConfigStore.java
index a9dbd10..3c761c8 100644
--- a/wifi/java/android/net/wifi/WifiConfigStore.java
+++ b/wifi/java/android/net/wifi/WifiConfigStore.java
@@ -1141,7 +1141,15 @@
                 String varName = field.varName();
                 String value = field.value();
                 if (value != null) {
-                    if (field != config.eap && field != config.engine) {
+                    if (field == config.engine) {
+                        /*
+                         * If the field is declared as an integer, it must not
+                         * be null
+                         */
+                        if (value.length() == 0) {
+                            value = "0";
+                        }
+                    } else if (field != config.eap) {
                         value = (value.length() == 0) ? "NULL" : convertToQuotedString(value);
                     }
                     if (!mWifiNative.setNetworkVariable(