Home | History | Annotate | Download | only in widget
      1 /*
      2  * Copyright (C) 2012 The Android Open Source Project
      3  *
      4  * Licensed under the Apache License, Version 2.0 (the "License");
      5  * you may not use this file except in compliance with the License.
      6  * You may obtain a copy of the License at
      7  *
      8  *      http://www.apache.org/licenses/LICENSE-2.0
      9  *
     10  * Unless required by applicable law or agreed to in writing, software
     11  * distributed under the License is distributed on an "AS IS" BASIS,
     12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     13  * See the License for the specific language governing permissions and
     14  * limitations under the License.
     15  */
     16 
     17 package android.widget;
     18 
     19 import com.android.internal.util.ArrayUtils;
     20 import com.android.internal.widget.EditableInputConnection;
     21 
     22 import android.R;
     23 import android.content.ClipData;
     24 import android.content.ClipData.Item;
     25 import android.content.Context;
     26 import android.content.Intent;
     27 import android.content.pm.PackageManager;
     28 import android.content.res.TypedArray;
     29 import android.graphics.Canvas;
     30 import android.graphics.Color;
     31 import android.graphics.Paint;
     32 import android.graphics.Path;
     33 import android.graphics.Rect;
     34 import android.graphics.RectF;
     35 import android.graphics.drawable.Drawable;
     36 import android.inputmethodservice.ExtractEditText;
     37 import android.os.Bundle;
     38 import android.os.Handler;
     39 import android.os.SystemClock;
     40 import android.provider.Settings;
     41 import android.text.DynamicLayout;
     42 import android.text.Editable;
     43 import android.text.InputType;
     44 import android.text.Layout;
     45 import android.text.ParcelableSpan;
     46 import android.text.Selection;
     47 import android.text.SpanWatcher;
     48 import android.text.Spannable;
     49 import android.text.SpannableStringBuilder;
     50 import android.text.Spanned;
     51 import android.text.StaticLayout;
     52 import android.text.TextUtils;
     53 import android.text.method.KeyListener;
     54 import android.text.method.MetaKeyKeyListener;
     55 import android.text.method.MovementMethod;
     56 import android.text.method.PasswordTransformationMethod;
     57 import android.text.method.WordIterator;
     58 import android.text.style.EasyEditSpan;
     59 import android.text.style.SuggestionRangeSpan;
     60 import android.text.style.SuggestionSpan;
     61 import android.text.style.TextAppearanceSpan;
     62 import android.text.style.URLSpan;
     63 import android.util.DisplayMetrics;
     64 import android.util.Log;
     65 import android.view.ActionMode;
     66 import android.view.ActionMode.Callback;
     67 import android.view.DisplayList;
     68 import android.view.DragEvent;
     69 import android.view.Gravity;
     70 import android.view.HardwareCanvas;
     71 import android.view.LayoutInflater;
     72 import android.view.Menu;
     73 import android.view.MenuItem;
     74 import android.view.MotionEvent;
     75 import android.view.View;
     76 import android.view.View.DragShadowBuilder;
     77 import android.view.View.OnClickListener;
     78 import android.view.ViewConfiguration;
     79 import android.view.ViewGroup;
     80 import android.view.ViewGroup.LayoutParams;
     81 import android.view.ViewParent;
     82 import android.view.ViewTreeObserver;
     83 import android.view.WindowManager;
     84 import android.view.inputmethod.CorrectionInfo;
     85 import android.view.inputmethod.EditorInfo;
     86 import android.view.inputmethod.ExtractedText;
     87 import android.view.inputmethod.ExtractedTextRequest;
     88 import android.view.inputmethod.InputConnection;
     89 import android.view.inputmethod.InputMethodManager;
     90 import android.widget.AdapterView.OnItemClickListener;
     91 import android.widget.Editor.InputContentType;
     92 import android.widget.Editor.InputMethodState;
     93 import android.widget.Editor.SelectionModifierCursorController;
     94 import android.widget.TextView.Drawables;
     95 import android.widget.TextView.OnEditorActionListener;
     96 
     97 import java.text.BreakIterator;
     98 import java.util.Arrays;
     99 import java.util.Comparator;
    100 import java.util.HashMap;
    101 
    102 /**
    103  * Helper class used by TextView to handle editable text views.
    104  *
    105  * @hide
    106  */
    107 public class Editor {
    108     private static final String TAG = "Editor";
    109 
    110     static final int BLINK = 500;
    111     private static final float[] TEMP_POSITION = new float[2];
    112     private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
    113 
    114     // Cursor Controllers.
    115     InsertionPointCursorController mInsertionPointCursorController;
    116     SelectionModifierCursorController mSelectionModifierCursorController;
    117     ActionMode mSelectionActionMode;
    118     boolean mInsertionControllerEnabled;
    119     boolean mSelectionControllerEnabled;
    120 
    121     // Used to highlight a word when it is corrected by the IME
    122     CorrectionHighlighter mCorrectionHighlighter;
    123 
    124     InputContentType mInputContentType;
    125     InputMethodState mInputMethodState;
    126 
    127     DisplayList[] mTextDisplayLists;
    128 
    129     boolean mFrozenWithFocus;
    130     boolean mSelectionMoved;
    131     boolean mTouchFocusSelected;
    132 
    133     KeyListener mKeyListener;
    134     int mInputType = EditorInfo.TYPE_NULL;
    135 
    136     boolean mDiscardNextActionUp;
    137     boolean mIgnoreActionUpEvent;
    138 
    139     long mShowCursor;
    140     Blink mBlink;
    141 
    142     boolean mCursorVisible = true;
    143     boolean mSelectAllOnFocus;
    144     boolean mTextIsSelectable;
    145 
    146     CharSequence mError;
    147     boolean mErrorWasChanged;
    148     ErrorPopup mErrorPopup;
    149     /**
    150      * This flag is set if the TextView tries to display an error before it
    151      * is attached to the window (so its position is still unknown).
    152      * It causes the error to be shown later, when onAttachedToWindow()
    153      * is called.
    154      */
    155     boolean mShowErrorAfterAttach;
    156 
    157     boolean mInBatchEditControllers;
    158     boolean mShowSoftInputOnFocus = true;
    159     boolean mPreserveDetachedSelection;
    160     boolean mTemporaryDetach;
    161 
    162     SuggestionsPopupWindow mSuggestionsPopupWindow;
    163     SuggestionRangeSpan mSuggestionRangeSpan;
    164     Runnable mShowSuggestionRunnable;
    165 
    166     final Drawable[] mCursorDrawable = new Drawable[2];
    167     int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
    168 
    169     private Drawable mSelectHandleLeft;
    170     private Drawable mSelectHandleRight;
    171     private Drawable mSelectHandleCenter;
    172 
    173     // Global listener that detects changes in the global position of the TextView
    174     private PositionListener mPositionListener;
    175 
    176     float mLastDownPositionX, mLastDownPositionY;
    177     Callback mCustomSelectionActionModeCallback;
    178 
    179     // Set when this TextView gained focus with some text selected. Will start selection mode.
    180     boolean mCreatedWithASelection;
    181 
    182     private EasyEditSpanController mEasyEditSpanController;
    183 
    184     WordIterator mWordIterator;
    185     SpellChecker mSpellChecker;
    186 
    187     private Rect mTempRect;
    188 
    189     private TextView mTextView;
    190 
    191     Editor(TextView textView) {
    192         mTextView = textView;
    193     }
    194 
    195     void onAttachedToWindow() {
    196         if (mShowErrorAfterAttach) {
    197             showError();
    198             mShowErrorAfterAttach = false;
    199         }
    200         mTemporaryDetach = false;
    201 
    202         final ViewTreeObserver observer = mTextView.getViewTreeObserver();
    203         // No need to create the controller.
    204         // The get method will add the listener on controller creation.
    205         if (mInsertionPointCursorController != null) {
    206             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
    207         }
    208         if (mSelectionModifierCursorController != null) {
    209             mSelectionModifierCursorController.resetTouchOffsets();
    210             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
    211         }
    212         updateSpellCheckSpans(0, mTextView.getText().length(),
    213                 true /* create the spell checker if needed */);
    214 
    215         if (mTextView.hasTransientState() &&
    216                 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
    217             // Since transient state is reference counted make sure it stays matched
    218             // with our own calls to it for managing selection.
    219             // The action mode callback will set this back again when/if the action mode starts.
    220             mTextView.setHasTransientState(false);
    221 
    222             // We had an active selection from before, start the selection mode.
    223             startSelectionActionMode();
    224         }
    225     }
    226 
    227     void onDetachedFromWindow() {
    228         if (mError != null) {
    229             hideError();
    230         }
    231 
    232         if (mBlink != null) {
    233             mBlink.removeCallbacks(mBlink);
    234         }
    235 
    236         if (mInsertionPointCursorController != null) {
    237             mInsertionPointCursorController.onDetached();
    238         }
    239 
    240         if (mSelectionModifierCursorController != null) {
    241             mSelectionModifierCursorController.onDetached();
    242         }
    243 
    244         if (mShowSuggestionRunnable != null) {
    245             mTextView.removeCallbacks(mShowSuggestionRunnable);
    246         }
    247 
    248         invalidateTextDisplayList();
    249 
    250         if (mSpellChecker != null) {
    251             mSpellChecker.closeSession();
    252             // Forces the creation of a new SpellChecker next time this window is created.
    253             // Will handle the cases where the settings has been changed in the meantime.
    254             mSpellChecker = null;
    255         }
    256 
    257         mPreserveDetachedSelection = true;
    258         hideControllers();
    259         mPreserveDetachedSelection = false;
    260         mTemporaryDetach = false;
    261     }
    262 
    263     private void showError() {
    264         if (mTextView.getWindowToken() == null) {
    265             mShowErrorAfterAttach = true;
    266             return;
    267         }
    268 
    269         if (mErrorPopup == null) {
    270             LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
    271             final TextView err = (TextView) inflater.inflate(
    272                     com.android.internal.R.layout.textview_hint, null);
    273 
    274             final float scale = mTextView.getResources().getDisplayMetrics().density;
    275             mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
    276             mErrorPopup.setFocusable(false);
    277             // The user is entering text, so the input method is needed.  We
    278             // don't want the popup to be displayed on top of it.
    279             mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
    280         }
    281 
    282         TextView tv = (TextView) mErrorPopup.getContentView();
    283         chooseSize(mErrorPopup, mError, tv);
    284         tv.setText(mError);
    285 
    286         mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
    287         mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
    288     }
    289 
    290     public void setError(CharSequence error, Drawable icon) {
    291         mError = TextUtils.stringOrSpannedString(error);
    292         mErrorWasChanged = true;
    293         final Drawables dr = mTextView.mDrawables;
    294         if (dr != null) {
    295             switch (mTextView.getResolvedLayoutDirection()) {
    296                 default:
    297                 case View.LAYOUT_DIRECTION_LTR:
    298                     mTextView.setCompoundDrawables(dr.mDrawableLeft, dr.mDrawableTop, icon,
    299                             dr.mDrawableBottom);
    300                     break;
    301                 case View.LAYOUT_DIRECTION_RTL:
    302                     mTextView.setCompoundDrawables(icon, dr.mDrawableTop, dr.mDrawableRight,
    303                             dr.mDrawableBottom);
    304                     break;
    305             }
    306         } else {
    307             mTextView.setCompoundDrawables(null, null, icon, null);
    308         }
    309 
    310         if (mError == null) {
    311             if (mErrorPopup != null) {
    312                 if (mErrorPopup.isShowing()) {
    313                     mErrorPopup.dismiss();
    314                 }
    315 
    316                 mErrorPopup = null;
    317             }
    318         } else {
    319             if (mTextView.isFocused()) {
    320                 showError();
    321             }
    322         }
    323     }
    324 
    325     private void hideError() {
    326         if (mErrorPopup != null) {
    327             if (mErrorPopup.isShowing()) {
    328                 mErrorPopup.dismiss();
    329             }
    330         }
    331 
    332         mShowErrorAfterAttach = false;
    333     }
    334 
    335     /**
    336      * Returns the Y offset to make the pointy top of the error point
    337      * at the middle of the error icon.
    338      */
    339     private int getErrorX() {
    340         /*
    341          * The "25" is the distance between the point and the right edge
    342          * of the background
    343          */
    344         final float scale = mTextView.getResources().getDisplayMetrics().density;
    345 
    346         final Drawables dr = mTextView.mDrawables;
    347         return mTextView.getWidth() - mErrorPopup.getWidth() - mTextView.getPaddingRight() -
    348                 (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
    349     }
    350 
    351     /**
    352      * Returns the Y offset to make the pointy top of the error point
    353      * at the bottom of the error icon.
    354      */
    355     private int getErrorY() {
    356         /*
    357          * Compound, not extended, because the icon is not clipped
    358          * if the text height is smaller.
    359          */
    360         final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
    361         int vspace = mTextView.getBottom() - mTextView.getTop() -
    362                 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
    363 
    364         final Drawables dr = mTextView.mDrawables;
    365         int icontop = compoundPaddingTop +
    366                 (vspace - (dr != null ? dr.mDrawableHeightRight : 0)) / 2;
    367 
    368         /*
    369          * The "2" is the distance between the point and the top edge
    370          * of the background.
    371          */
    372         final float scale = mTextView.getResources().getDisplayMetrics().density;
    373         return icontop + (dr != null ? dr.mDrawableHeightRight : 0) - mTextView.getHeight() -
    374                 (int) (2 * scale + 0.5f);
    375     }
    376 
    377     void createInputContentTypeIfNeeded() {
    378         if (mInputContentType == null) {
    379             mInputContentType = new InputContentType();
    380         }
    381     }
    382 
    383     void createInputMethodStateIfNeeded() {
    384         if (mInputMethodState == null) {
    385             mInputMethodState = new InputMethodState();
    386         }
    387     }
    388 
    389     boolean isCursorVisible() {
    390         // The default value is true, even when there is no associated Editor
    391         return mCursorVisible && mTextView.isTextEditable();
    392     }
    393 
    394     void prepareCursorControllers() {
    395         boolean windowSupportsHandles = false;
    396 
    397         ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
    398         if (params instanceof WindowManager.LayoutParams) {
    399             WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
    400             windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
    401                     || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
    402         }
    403 
    404         boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
    405         mInsertionControllerEnabled = enabled && isCursorVisible();
    406         mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
    407 
    408         if (!mInsertionControllerEnabled) {
    409             hideInsertionPointCursorController();
    410             if (mInsertionPointCursorController != null) {
    411                 mInsertionPointCursorController.onDetached();
    412                 mInsertionPointCursorController = null;
    413             }
    414         }
    415 
    416         if (!mSelectionControllerEnabled) {
    417             stopSelectionActionMode();
    418             if (mSelectionModifierCursorController != null) {
    419                 mSelectionModifierCursorController.onDetached();
    420                 mSelectionModifierCursorController = null;
    421             }
    422         }
    423     }
    424 
    425     private void hideInsertionPointCursorController() {
    426         if (mInsertionPointCursorController != null) {
    427             mInsertionPointCursorController.hide();
    428         }
    429     }
    430 
    431     /**
    432      * Hides the insertion controller and stops text selection mode, hiding the selection controller
    433      */
    434     void hideControllers() {
    435         hideCursorControllers();
    436         hideSpanControllers();
    437     }
    438 
    439     private void hideSpanControllers() {
    440         if (mEasyEditSpanController != null) {
    441             mEasyEditSpanController.hide();
    442         }
    443     }
    444 
    445     private void hideCursorControllers() {
    446         if (mSuggestionsPopupWindow != null && !mSuggestionsPopupWindow.isShowingUp()) {
    447             // Should be done before hide insertion point controller since it triggers a show of it
    448             mSuggestionsPopupWindow.hide();
    449         }
    450         hideInsertionPointCursorController();
    451         stopSelectionActionMode();
    452     }
    453 
    454     /**
    455      * Create new SpellCheckSpans on the modified region.
    456      */
    457     private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
    458         if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
    459                 !(mTextView instanceof ExtractEditText)) {
    460             if (mSpellChecker == null && createSpellChecker) {
    461                 mSpellChecker = new SpellChecker(mTextView);
    462             }
    463             if (mSpellChecker != null) {
    464                 mSpellChecker.spellCheck(start, end);
    465             }
    466         }
    467     }
    468 
    469     void onScreenStateChanged(int screenState) {
    470         switch (screenState) {
    471             case View.SCREEN_STATE_ON:
    472                 resumeBlink();
    473                 break;
    474             case View.SCREEN_STATE_OFF:
    475                 suspendBlink();
    476                 break;
    477         }
    478     }
    479 
    480     private void suspendBlink() {
    481         if (mBlink != null) {
    482             mBlink.cancel();
    483         }
    484     }
    485 
    486     private void resumeBlink() {
    487         if (mBlink != null) {
    488             mBlink.uncancel();
    489             makeBlink();
    490         }
    491     }
    492 
    493     void adjustInputType(boolean password, boolean passwordInputType,
    494             boolean webPasswordInputType, boolean numberPasswordInputType) {
    495         // mInputType has been set from inputType, possibly modified by mInputMethod.
    496         // Specialize mInputType to [web]password if we have a text class and the original input
    497         // type was a password.
    498         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
    499             if (password || passwordInputType) {
    500                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    501                         | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
    502             }
    503             if (webPasswordInputType) {
    504                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    505                         | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
    506             }
    507         } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
    508             if (numberPasswordInputType) {
    509                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    510                         | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
    511             }
    512         }
    513     }
    514 
    515     private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
    516         int wid = tv.getPaddingLeft() + tv.getPaddingRight();
    517         int ht = tv.getPaddingTop() + tv.getPaddingBottom();
    518 
    519         int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
    520                 com.android.internal.R.dimen.textview_error_popup_default_width);
    521         Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
    522                                     Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    523         float max = 0;
    524         for (int i = 0; i < l.getLineCount(); i++) {
    525             max = Math.max(max, l.getLineWidth(i));
    526         }
    527 
    528         /*
    529          * Now set the popup size to be big enough for the text plus the border capped
    530          * to DEFAULT_MAX_POPUP_WIDTH
    531          */
    532         pop.setWidth(wid + (int) Math.ceil(max));
    533         pop.setHeight(ht + l.getHeight());
    534     }
    535 
    536     void setFrame() {
    537         if (mErrorPopup != null) {
    538             TextView tv = (TextView) mErrorPopup.getContentView();
    539             chooseSize(mErrorPopup, mError, tv);
    540             mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
    541                     mErrorPopup.getWidth(), mErrorPopup.getHeight());
    542         }
    543     }
    544 
    545     /**
    546      * Unlike {@link TextView#textCanBeSelected()}, this method is based on the <i>current</i> state
    547      * of the TextView. textCanBeSelected() has to be true (this is one of the conditions to have
    548      * a selection controller (see {@link #prepareCursorControllers()}), but this is not sufficient.
    549      */
    550     private boolean canSelectText() {
    551         return hasSelectionController() && mTextView.getText().length() != 0;
    552     }
    553 
    554     /**
    555      * It would be better to rely on the input type for everything. A password inputType should have
    556      * a password transformation. We should hence use isPasswordInputType instead of this method.
    557      *
    558      * We should:
    559      * - Call setInputType in setKeyListener instead of changing the input type directly (which
    560      * would install the correct transformation).
    561      * - Refuse the installation of a non-password transformation in setTransformation if the input
    562      * type is password.
    563      *
    564      * However, this is like this for legacy reasons and we cannot break existing apps. This method
    565      * is useful since it matches what the user can see (obfuscated text or not).
    566      *
    567      * @return true if the current transformation method is of the password type.
    568      */
    569     private boolean hasPasswordTransformationMethod() {
    570         return mTextView.getTransformationMethod() instanceof PasswordTransformationMethod;
    571     }
    572 
    573     /**
    574      * Adjusts selection to the word under last touch offset.
    575      * Return true if the operation was successfully performed.
    576      */
    577     private boolean selectCurrentWord() {
    578         if (!canSelectText()) {
    579             return false;
    580         }
    581 
    582         if (hasPasswordTransformationMethod()) {
    583             // Always select all on a password field.
    584             // Cut/copy menu entries are not available for passwords, but being able to select all
    585             // is however useful to delete or paste to replace the entire content.
    586             return mTextView.selectAllText();
    587         }
    588 
    589         int inputType = mTextView.getInputType();
    590         int klass = inputType & InputType.TYPE_MASK_CLASS;
    591         int variation = inputType & InputType.TYPE_MASK_VARIATION;
    592 
    593         // Specific text field types: select the entire text for these
    594         if (klass == InputType.TYPE_CLASS_NUMBER ||
    595                 klass == InputType.TYPE_CLASS_PHONE ||
    596                 klass == InputType.TYPE_CLASS_DATETIME ||
    597                 variation == InputType.TYPE_TEXT_VARIATION_URI ||
    598                 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
    599                 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
    600                 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    601             return mTextView.selectAllText();
    602         }
    603 
    604         long lastTouchOffsets = getLastTouchOffsets();
    605         final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
    606         final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
    607 
    608         // Safety check in case standard touch event handling has been bypassed
    609         if (minOffset < 0 || minOffset >= mTextView.getText().length()) return false;
    610         if (maxOffset < 0 || maxOffset >= mTextView.getText().length()) return false;
    611 
    612         int selectionStart, selectionEnd;
    613 
    614         // If a URLSpan (web address, email, phone...) is found at that position, select it.
    615         URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
    616                 getSpans(minOffset, maxOffset, URLSpan.class);
    617         if (urlSpans.length >= 1) {
    618             URLSpan urlSpan = urlSpans[0];
    619             selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
    620             selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
    621         } else {
    622             final WordIterator wordIterator = getWordIterator();
    623             wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
    624 
    625             selectionStart = wordIterator.getBeginning(minOffset);
    626             selectionEnd = wordIterator.getEnd(maxOffset);
    627 
    628             if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
    629                     selectionStart == selectionEnd) {
    630                 // Possible when the word iterator does not properly handle the text's language
    631                 long range = getCharRange(minOffset);
    632                 selectionStart = TextUtils.unpackRangeStartFromLong(range);
    633                 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
    634             }
    635         }
    636 
    637         Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
    638         return selectionEnd > selectionStart;
    639     }
    640 
    641     void onLocaleChanged() {
    642         // Will be re-created on demand in getWordIterator with the proper new locale
    643         mWordIterator = null;
    644     }
    645 
    646     /**
    647      * @hide
    648      */
    649     public WordIterator getWordIterator() {
    650         if (mWordIterator == null) {
    651             mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
    652         }
    653         return mWordIterator;
    654     }
    655 
    656     private long getCharRange(int offset) {
    657         final int textLength = mTextView.getText().length();
    658         if (offset + 1 < textLength) {
    659             final char currentChar = mTextView.getText().charAt(offset);
    660             final char nextChar = mTextView.getText().charAt(offset + 1);
    661             if (Character.isSurrogatePair(currentChar, nextChar)) {
    662                 return TextUtils.packRangeInLong(offset,  offset + 2);
    663             }
    664         }
    665         if (offset < textLength) {
    666             return TextUtils.packRangeInLong(offset,  offset + 1);
    667         }
    668         if (offset - 2 >= 0) {
    669             final char previousChar = mTextView.getText().charAt(offset - 1);
    670             final char previousPreviousChar = mTextView.getText().charAt(offset - 2);
    671             if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
    672                 return TextUtils.packRangeInLong(offset - 2,  offset);
    673             }
    674         }
    675         if (offset - 1 >= 0) {
    676             return TextUtils.packRangeInLong(offset - 1,  offset);
    677         }
    678         return TextUtils.packRangeInLong(offset,  offset);
    679     }
    680 
    681     private boolean touchPositionIsInSelection() {
    682         int selectionStart = mTextView.getSelectionStart();
    683         int selectionEnd = mTextView.getSelectionEnd();
    684 
    685         if (selectionStart == selectionEnd) {
    686             return false;
    687         }
    688 
    689         if (selectionStart > selectionEnd) {
    690             int tmp = selectionStart;
    691             selectionStart = selectionEnd;
    692             selectionEnd = tmp;
    693             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
    694         }
    695 
    696         SelectionModifierCursorController selectionController = getSelectionController();
    697         int minOffset = selectionController.getMinTouchOffset();
    698         int maxOffset = selectionController.getMaxTouchOffset();
    699 
    700         return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
    701     }
    702 
    703     private PositionListener getPositionListener() {
    704         if (mPositionListener == null) {
    705             mPositionListener = new PositionListener();
    706         }
    707         return mPositionListener;
    708     }
    709 
    710     private interface TextViewPositionListener {
    711         public void updatePosition(int parentPositionX, int parentPositionY,
    712                 boolean parentPositionChanged, boolean parentScrolled);
    713     }
    714 
    715     private boolean isPositionVisible(int positionX, int positionY) {
    716         synchronized (TEMP_POSITION) {
    717             final float[] position = TEMP_POSITION;
    718             position[0] = positionX;
    719             position[1] = positionY;
    720             View view = mTextView;
    721 
    722             while (view != null) {
    723                 if (view != mTextView) {
    724                     // Local scroll is already taken into account in positionX/Y
    725                     position[0] -= view.getScrollX();
    726                     position[1] -= view.getScrollY();
    727                 }
    728 
    729                 if (position[0] < 0 || position[1] < 0 ||
    730                         position[0] > view.getWidth() || position[1] > view.getHeight()) {
    731                     return false;
    732                 }
    733 
    734                 if (!view.getMatrix().isIdentity()) {
    735                     view.getMatrix().mapPoints(position);
    736                 }
    737 
    738                 position[0] += view.getLeft();
    739                 position[1] += view.getTop();
    740 
    741                 final ViewParent parent = view.getParent();
    742                 if (parent instanceof View) {
    743                     view = (View) parent;
    744                 } else {
    745                     // We've reached the ViewRoot, stop iterating
    746                     view = null;
    747                 }
    748             }
    749         }
    750 
    751         // We've been able to walk up the view hierarchy and the position was never clipped
    752         return true;
    753     }
    754 
    755     private boolean isOffsetVisible(int offset) {
    756         Layout layout = mTextView.getLayout();
    757         final int line = layout.getLineForOffset(offset);
    758         final int lineBottom = layout.getLineBottom(line);
    759         final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
    760         return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
    761                 lineBottom + mTextView.viewportToContentVerticalOffset());
    762     }
    763 
    764     /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
    765      * in the view. Returns false when the position is in the empty space of left/right of text.
    766      */
    767     private boolean isPositionOnText(float x, float y) {
    768         Layout layout = mTextView.getLayout();
    769         if (layout == null) return false;
    770 
    771         final int line = mTextView.getLineAtCoordinate(y);
    772         x = mTextView.convertToLocalHorizontalCoordinate(x);
    773 
    774         if (x < layout.getLineLeft(line)) return false;
    775         if (x > layout.getLineRight(line)) return false;
    776         return true;
    777     }
    778 
    779     public boolean performLongClick(boolean handled) {
    780         // Long press in empty space moves cursor and shows the Paste affordance if available.
    781         if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
    782                 mInsertionControllerEnabled) {
    783             final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
    784                     mLastDownPositionY);
    785             stopSelectionActionMode();
    786             Selection.setSelection((Spannable) mTextView.getText(), offset);
    787             getInsertionController().showWithActionPopup();
    788             handled = true;
    789         }
    790 
    791         if (!handled && mSelectionActionMode != null) {
    792             if (touchPositionIsInSelection()) {
    793                 // Start a drag
    794                 final int start = mTextView.getSelectionStart();
    795                 final int end = mTextView.getSelectionEnd();
    796                 CharSequence selectedText = mTextView.getTransformedText(start, end);
    797                 ClipData data = ClipData.newPlainText(null, selectedText);
    798                 DragLocalState localState = new DragLocalState(mTextView, start, end);
    799                 mTextView.startDrag(data, getTextThumbnailBuilder(selectedText), localState, 0);
    800                 stopSelectionActionMode();
    801             } else {
    802                 getSelectionController().hide();
    803                 selectCurrentWord();
    804                 getSelectionController().show();
    805             }
    806             handled = true;
    807         }
    808 
    809         // Start a new selection
    810         if (!handled) {
    811             handled = startSelectionActionMode();
    812         }
    813 
    814         return handled;
    815     }
    816 
    817     private long getLastTouchOffsets() {
    818         SelectionModifierCursorController selectionController = getSelectionController();
    819         final int minOffset = selectionController.getMinTouchOffset();
    820         final int maxOffset = selectionController.getMaxTouchOffset();
    821         return TextUtils.packRangeInLong(minOffset, maxOffset);
    822     }
    823 
    824     void onFocusChanged(boolean focused, int direction) {
    825         mShowCursor = SystemClock.uptimeMillis();
    826         ensureEndedBatchEdit();
    827 
    828         if (focused) {
    829             int selStart = mTextView.getSelectionStart();
    830             int selEnd = mTextView.getSelectionEnd();
    831 
    832             // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
    833             // mode for these, unless there was a specific selection already started.
    834             final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
    835                     selEnd == mTextView.getText().length();
    836 
    837             mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
    838                     !isFocusHighlighted;
    839 
    840             if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
    841                 // If a tap was used to give focus to that view, move cursor at tap position.
    842                 // Has to be done before onTakeFocus, which can be overloaded.
    843                 final int lastTapPosition = getLastTapPosition();
    844                 if (lastTapPosition >= 0) {
    845                     Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
    846                 }
    847 
    848                 // Note this may have to be moved out of the Editor class
    849                 MovementMethod mMovement = mTextView.getMovementMethod();
    850                 if (mMovement != null) {
    851                     mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
    852                 }
    853 
    854                 // The DecorView does not have focus when the 'Done' ExtractEditText button is
    855                 // pressed. Since it is the ViewAncestor's mView, it requests focus before
    856                 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
    857                 // This special case ensure that we keep current selection in that case.
    858                 // It would be better to know why the DecorView does not have focus at that time.
    859                 if (((mTextView instanceof ExtractEditText) || mSelectionMoved) &&
    860                         selStart >= 0 && selEnd >= 0) {
    861                     /*
    862                      * Someone intentionally set the selection, so let them
    863                      * do whatever it is that they wanted to do instead of
    864                      * the default on-focus behavior.  We reset the selection
    865                      * here instead of just skipping the onTakeFocus() call
    866                      * because some movement methods do something other than
    867                      * just setting the selection in theirs and we still
    868                      * need to go through that path.
    869                      */
    870                     Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
    871                 }
    872 
    873                 if (mSelectAllOnFocus) {
    874                     mTextView.selectAllText();
    875                 }
    876 
    877                 mTouchFocusSelected = true;
    878             }
    879 
    880             mFrozenWithFocus = false;
    881             mSelectionMoved = false;
    882 
    883             if (mError != null) {
    884                 showError();
    885             }
    886 
    887             makeBlink();
    888         } else {
    889             if (mError != null) {
    890                 hideError();
    891             }
    892             // Don't leave us in the middle of a batch edit.
    893             mTextView.onEndBatchEdit();
    894 
    895             if (mTextView instanceof ExtractEditText) {
    896                 // terminateTextSelectionMode removes selection, which we want to keep when
    897                 // ExtractEditText goes out of focus.
    898                 final int selStart = mTextView.getSelectionStart();
    899                 final int selEnd = mTextView.getSelectionEnd();
    900                 hideControllers();
    901                 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
    902             } else {
    903                 if (mTemporaryDetach) mPreserveDetachedSelection = true;
    904                 hideControllers();
    905                 if (mTemporaryDetach) mPreserveDetachedSelection = false;
    906                 downgradeEasyCorrectionSpans();
    907             }
    908 
    909             // No need to create the controller
    910             if (mSelectionModifierCursorController != null) {
    911                 mSelectionModifierCursorController.resetTouchOffsets();
    912             }
    913         }
    914     }
    915 
    916     /**
    917      * Downgrades to simple suggestions all the easy correction spans that are not a spell check
    918      * span.
    919      */
    920     private void downgradeEasyCorrectionSpans() {
    921         CharSequence text = mTextView.getText();
    922         if (text instanceof Spannable) {
    923             Spannable spannable = (Spannable) text;
    924             SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
    925                     spannable.length(), SuggestionSpan.class);
    926             for (int i = 0; i < suggestionSpans.length; i++) {
    927                 int flags = suggestionSpans[i].getFlags();
    928                 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
    929                         && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
    930                     flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
    931                     suggestionSpans[i].setFlags(flags);
    932                 }
    933             }
    934         }
    935     }
    936 
    937     void sendOnTextChanged(int start, int after) {
    938         updateSpellCheckSpans(start, start + after, false);
    939 
    940         // Hide the controllers as soon as text is modified (typing, procedural...)
    941         // We do not hide the span controllers, since they can be added when a new text is
    942         // inserted into the text view (voice IME).
    943         hideCursorControllers();
    944     }
    945 
    946     private int getLastTapPosition() {
    947         // No need to create the controller at that point, no last tap position saved
    948         if (mSelectionModifierCursorController != null) {
    949             int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
    950             if (lastTapPosition >= 0) {
    951                 // Safety check, should not be possible.
    952                 if (lastTapPosition > mTextView.getText().length()) {
    953                     lastTapPosition = mTextView.getText().length();
    954                 }
    955                 return lastTapPosition;
    956             }
    957         }
    958 
    959         return -1;
    960     }
    961 
    962     void onWindowFocusChanged(boolean hasWindowFocus) {
    963         if (hasWindowFocus) {
    964             if (mBlink != null) {
    965                 mBlink.uncancel();
    966                 makeBlink();
    967             }
    968         } else {
    969             if (mBlink != null) {
    970                 mBlink.cancel();
    971             }
    972             if (mInputContentType != null) {
    973                 mInputContentType.enterDown = false;
    974             }
    975             // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
    976             hideControllers();
    977             if (mSuggestionsPopupWindow != null) {
    978                 mSuggestionsPopupWindow.onParentLostFocus();
    979             }
    980 
    981             // Don't leave us in the middle of a batch edit.
    982             mTextView.onEndBatchEdit();
    983         }
    984     }
    985 
    986     void onTouchEvent(MotionEvent event) {
    987         if (hasSelectionController()) {
    988             getSelectionController().onTouchEvent(event);
    989         }
    990 
    991         if (mShowSuggestionRunnable != null) {
    992             mTextView.removeCallbacks(mShowSuggestionRunnable);
    993             mShowSuggestionRunnable = null;
    994         }
    995 
    996         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
    997             mLastDownPositionX = event.getX();
    998             mLastDownPositionY = event.getY();
    999 
   1000             // Reset this state; it will be re-set if super.onTouchEvent
   1001             // causes focus to move to the view.
   1002             mTouchFocusSelected = false;
   1003             mIgnoreActionUpEvent = false;
   1004         }
   1005     }
   1006 
   1007     public void beginBatchEdit() {
   1008         mInBatchEditControllers = true;
   1009         final InputMethodState ims = mInputMethodState;
   1010         if (ims != null) {
   1011             int nesting = ++ims.mBatchEditNesting;
   1012             if (nesting == 1) {
   1013                 ims.mCursorChanged = false;
   1014                 ims.mChangedDelta = 0;
   1015                 if (ims.mContentChanged) {
   1016                     // We already have a pending change from somewhere else,
   1017                     // so turn this into a full update.
   1018                     ims.mChangedStart = 0;
   1019                     ims.mChangedEnd = mTextView.getText().length();
   1020                 } else {
   1021                     ims.mChangedStart = EXTRACT_UNKNOWN;
   1022                     ims.mChangedEnd = EXTRACT_UNKNOWN;
   1023                     ims.mContentChanged = false;
   1024                 }
   1025                 mTextView.onBeginBatchEdit();
   1026             }
   1027         }
   1028     }
   1029 
   1030     public void endBatchEdit() {
   1031         mInBatchEditControllers = false;
   1032         final InputMethodState ims = mInputMethodState;
   1033         if (ims != null) {
   1034             int nesting = --ims.mBatchEditNesting;
   1035             if (nesting == 0) {
   1036                 finishBatchEdit(ims);
   1037             }
   1038         }
   1039     }
   1040 
   1041     void ensureEndedBatchEdit() {
   1042         final InputMethodState ims = mInputMethodState;
   1043         if (ims != null && ims.mBatchEditNesting != 0) {
   1044             ims.mBatchEditNesting = 0;
   1045             finishBatchEdit(ims);
   1046         }
   1047     }
   1048 
   1049     void finishBatchEdit(final InputMethodState ims) {
   1050         mTextView.onEndBatchEdit();
   1051 
   1052         if (ims.mContentChanged || ims.mSelectionModeChanged) {
   1053             mTextView.updateAfterEdit();
   1054             reportExtractedText();
   1055         } else if (ims.mCursorChanged) {
   1056             // Cheezy way to get us to report the current cursor location.
   1057             mTextView.invalidateCursor();
   1058         }
   1059     }
   1060 
   1061     static final int EXTRACT_NOTHING = -2;
   1062     static final int EXTRACT_UNKNOWN = -1;
   1063 
   1064     boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
   1065         return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
   1066                 EXTRACT_UNKNOWN, outText);
   1067     }
   1068 
   1069     private boolean extractTextInternal(ExtractedTextRequest request,
   1070             int partialStartOffset, int partialEndOffset, int delta,
   1071             ExtractedText outText) {
   1072         final CharSequence content = mTextView.getText();
   1073         if (content != null) {
   1074             if (partialStartOffset != EXTRACT_NOTHING) {
   1075                 final int N = content.length();
   1076                 if (partialStartOffset < 0) {
   1077                     outText.partialStartOffset = outText.partialEndOffset = -1;
   1078                     partialStartOffset = 0;
   1079                     partialEndOffset = N;
   1080                 } else {
   1081                     // Now use the delta to determine the actual amount of text
   1082                     // we need.
   1083                     partialEndOffset += delta;
   1084                     // Adjust offsets to ensure we contain full spans.
   1085                     if (content instanceof Spanned) {
   1086                         Spanned spanned = (Spanned)content;
   1087                         Object[] spans = spanned.getSpans(partialStartOffset,
   1088                                 partialEndOffset, ParcelableSpan.class);
   1089                         int i = spans.length;
   1090                         while (i > 0) {
   1091                             i--;
   1092                             int j = spanned.getSpanStart(spans[i]);
   1093                             if (j < partialStartOffset) partialStartOffset = j;
   1094                             j = spanned.getSpanEnd(spans[i]);
   1095                             if (j > partialEndOffset) partialEndOffset = j;
   1096                         }
   1097                     }
   1098                     outText.partialStartOffset = partialStartOffset;
   1099                     outText.partialEndOffset = partialEndOffset - delta;
   1100 
   1101                     if (partialStartOffset > N) {
   1102                         partialStartOffset = N;
   1103                     } else if (partialStartOffset < 0) {
   1104                         partialStartOffset = 0;
   1105                     }
   1106                     if (partialEndOffset > N) {
   1107                         partialEndOffset = N;
   1108                     } else if (partialEndOffset < 0) {
   1109                         partialEndOffset = 0;
   1110                     }
   1111                 }
   1112                 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
   1113                     outText.text = content.subSequence(partialStartOffset,
   1114                             partialEndOffset);
   1115                 } else {
   1116                     outText.text = TextUtils.substring(content, partialStartOffset,
   1117                             partialEndOffset);
   1118                 }
   1119             } else {
   1120                 outText.partialStartOffset = 0;
   1121                 outText.partialEndOffset = 0;
   1122                 outText.text = "";
   1123             }
   1124             outText.flags = 0;
   1125             if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
   1126                 outText.flags |= ExtractedText.FLAG_SELECTING;
   1127             }
   1128             if (mTextView.isSingleLine()) {
   1129                 outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
   1130             }
   1131             outText.startOffset = 0;
   1132             outText.selectionStart = mTextView.getSelectionStart();
   1133             outText.selectionEnd = mTextView.getSelectionEnd();
   1134             return true;
   1135         }
   1136         return false;
   1137     }
   1138 
   1139     boolean reportExtractedText() {
   1140         final Editor.InputMethodState ims = mInputMethodState;
   1141         if (ims != null) {
   1142             final boolean contentChanged = ims.mContentChanged;
   1143             if (contentChanged || ims.mSelectionModeChanged) {
   1144                 ims.mContentChanged = false;
   1145                 ims.mSelectionModeChanged = false;
   1146                 final ExtractedTextRequest req = ims.mExtractedTextRequest;
   1147                 if (req != null) {
   1148                     InputMethodManager imm = InputMethodManager.peekInstance();
   1149                     if (imm != null) {
   1150                         if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
   1151                                 "Retrieving extracted start=" + ims.mChangedStart +
   1152                                 " end=" + ims.mChangedEnd +
   1153                                 " delta=" + ims.mChangedDelta);
   1154                         if (ims.mChangedStart < 0 && !contentChanged) {
   1155                             ims.mChangedStart = EXTRACT_NOTHING;
   1156                         }
   1157                         if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
   1158                                 ims.mChangedDelta, ims.mExtractedText)) {
   1159                             if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
   1160                                     "Reporting extracted start=" +
   1161                                     ims.mExtractedText.partialStartOffset +
   1162                                     " end=" + ims.mExtractedText.partialEndOffset +
   1163                                     ": " + ims.mExtractedText.text);
   1164 
   1165                             imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
   1166                             ims.mChangedStart = EXTRACT_UNKNOWN;
   1167                             ims.mChangedEnd = EXTRACT_UNKNOWN;
   1168                             ims.mChangedDelta = 0;
   1169                             ims.mContentChanged = false;
   1170                             return true;
   1171                         }
   1172                     }
   1173                 }
   1174             }
   1175         }
   1176         return false;
   1177     }
   1178 
   1179     void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
   1180             int cursorOffsetVertical) {
   1181         final int selectionStart = mTextView.getSelectionStart();
   1182         final int selectionEnd = mTextView.getSelectionEnd();
   1183 
   1184         final InputMethodState ims = mInputMethodState;
   1185         if (ims != null && ims.mBatchEditNesting == 0) {
   1186             InputMethodManager imm = InputMethodManager.peekInstance();
   1187             if (imm != null) {
   1188                 if (imm.isActive(mTextView)) {
   1189                     boolean reported = false;
   1190                     if (ims.mContentChanged || ims.mSelectionModeChanged) {
   1191                         // We are in extract mode and the content has changed
   1192                         // in some way... just report complete new text to the
   1193                         // input method.
   1194                         reported = reportExtractedText();
   1195                     }
   1196                     if (!reported && highlight != null) {
   1197                         int candStart = -1;
   1198                         int candEnd = -1;
   1199                         if (mTextView.getText() instanceof Spannable) {
   1200                             Spannable sp = (Spannable) mTextView.getText();
   1201                             candStart = EditableInputConnection.getComposingSpanStart(sp);
   1202                             candEnd = EditableInputConnection.getComposingSpanEnd(sp);
   1203                         }
   1204                         imm.updateSelection(mTextView,
   1205                                 selectionStart, selectionEnd, candStart, candEnd);
   1206                     }
   1207                 }
   1208 
   1209                 if (imm.isWatchingCursor(mTextView) && highlight != null) {
   1210                     highlight.computeBounds(ims.mTmpRectF, true);
   1211                     ims.mTmpOffset[0] = ims.mTmpOffset[1] = 0;
   1212 
   1213                     canvas.getMatrix().mapPoints(ims.mTmpOffset);
   1214                     ims.mTmpRectF.offset(ims.mTmpOffset[0], ims.mTmpOffset[1]);
   1215 
   1216                     ims.mTmpRectF.offset(0, cursorOffsetVertical);
   1217 
   1218                     ims.mCursorRectInWindow.set((int)(ims.mTmpRectF.left + 0.5),
   1219                             (int)(ims.mTmpRectF.top + 0.5),
   1220                             (int)(ims.mTmpRectF.right + 0.5),
   1221                             (int)(ims.mTmpRectF.bottom + 0.5));
   1222 
   1223                     imm.updateCursor(mTextView,
   1224                             ims.mCursorRectInWindow.left, ims.mCursorRectInWindow.top,
   1225                             ims.mCursorRectInWindow.right, ims.mCursorRectInWindow.bottom);
   1226                 }
   1227             }
   1228         }
   1229 
   1230         if (mCorrectionHighlighter != null) {
   1231             mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
   1232         }
   1233 
   1234         if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
   1235             drawCursor(canvas, cursorOffsetVertical);
   1236             // Rely on the drawable entirely, do not draw the cursor line.
   1237             // Has to be done after the IMM related code above which relies on the highlight.
   1238             highlight = null;
   1239         }
   1240 
   1241         if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
   1242             drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
   1243                     cursorOffsetVertical);
   1244         } else {
   1245             layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
   1246         }
   1247     }
   1248 
   1249     private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
   1250             Paint highlightPaint, int cursorOffsetVertical) {
   1251         final long lineRange = layout.getLineRangeForDraw(canvas);
   1252         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
   1253         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
   1254         if (lastLine < 0) return;
   1255 
   1256         layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
   1257                 firstLine, lastLine);
   1258 
   1259         if (layout instanceof DynamicLayout) {
   1260             if (mTextDisplayLists == null) {
   1261                 mTextDisplayLists = new DisplayList[ArrayUtils.idealObjectArraySize(0)];
   1262             }
   1263 
   1264             DynamicLayout dynamicLayout = (DynamicLayout) layout;
   1265             int[] blockEndLines = dynamicLayout.getBlockEndLines();
   1266             int[] blockIndices = dynamicLayout.getBlockIndices();
   1267             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
   1268 
   1269             int endOfPreviousBlock = -1;
   1270             int searchStartIndex = 0;
   1271             for (int i = 0; i < numberOfBlocks; i++) {
   1272                 int blockEndLine = blockEndLines[i];
   1273                 int blockIndex = blockIndices[i];
   1274 
   1275                 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
   1276                 if (blockIsInvalid) {
   1277                     blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
   1278                             searchStartIndex);
   1279                     // Note how dynamic layout's internal block indices get updated from Editor
   1280                     blockIndices[i] = blockIndex;
   1281                     searchStartIndex = blockIndex + 1;
   1282                 }
   1283 
   1284                 DisplayList blockDisplayList = mTextDisplayLists[blockIndex];
   1285                 if (blockDisplayList == null) {
   1286                     blockDisplayList = mTextDisplayLists[blockIndex] =
   1287                             mTextView.getHardwareRenderer().createDisplayList("Text " + blockIndex);
   1288                 } else {
   1289                     if (blockIsInvalid) blockDisplayList.invalidate();
   1290                 }
   1291 
   1292                 if (!blockDisplayList.isValid()) {
   1293                     final int blockBeginLine = endOfPreviousBlock + 1;
   1294                     final int top = layout.getLineTop(blockBeginLine);
   1295                     final int bottom = layout.getLineBottom(blockEndLine);
   1296                     int left = 0;
   1297                     int right = mTextView.getWidth();
   1298                     if (mTextView.getHorizontallyScrolling()) {
   1299                         float min = Float.MAX_VALUE;
   1300                         float max = Float.MIN_VALUE;
   1301                         for (int line = blockBeginLine; line <= blockEndLine; line++) {
   1302                             min = Math.min(min, layout.getLineLeft(line));
   1303                             max = Math.max(max, layout.getLineRight(line));
   1304                         }
   1305                         left = (int) min;
   1306                         right = (int) (max + 0.5f);
   1307                     }
   1308 
   1309                     final HardwareCanvas hardwareCanvas = blockDisplayList.start();
   1310                     try {
   1311                         // Tighten the bounds of the viewport to the actual text size
   1312                         hardwareCanvas.setViewport(right - left, bottom - top);
   1313                         // The dirty rect should always be null for a display list
   1314                         hardwareCanvas.onPreDraw(null);
   1315                         // drawText is always relative to TextView's origin, this translation brings
   1316                         // this range of text back to the top left corner of the viewport
   1317                         hardwareCanvas.translate(-left, -top);
   1318                         layout.drawText(hardwareCanvas, blockBeginLine, blockEndLine);
   1319                         // No need to untranslate, previous context is popped after drawDisplayList
   1320                     } finally {
   1321                         hardwareCanvas.onPostDraw();
   1322                         blockDisplayList.end();
   1323                         blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
   1324                         // Same as drawDisplayList below, handled by our TextView's parent
   1325                         blockDisplayList.setClipChildren(false);
   1326                     }
   1327                 }
   1328 
   1329                 ((HardwareCanvas) canvas).drawDisplayList(blockDisplayList, null,
   1330                         0 /* no child clipping, our TextView parent enforces it */);
   1331 
   1332                 endOfPreviousBlock = blockEndLine;
   1333             }
   1334         } else {
   1335             // Boring layout is used for empty and hint text
   1336             layout.drawText(canvas, firstLine, lastLine);
   1337         }
   1338     }
   1339 
   1340     private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
   1341             int searchStartIndex) {
   1342         int length = mTextDisplayLists.length;
   1343         for (int i = searchStartIndex; i < length; i++) {
   1344             boolean blockIndexFound = false;
   1345             for (int j = 0; j < numberOfBlocks; j++) {
   1346                 if (blockIndices[j] == i) {
   1347                     blockIndexFound = true;
   1348                     break;
   1349                 }
   1350             }
   1351             if (blockIndexFound) continue;
   1352             return i;
   1353         }
   1354 
   1355         // No available index found, the pool has to grow
   1356         int newSize = ArrayUtils.idealIntArraySize(length + 1);
   1357         DisplayList[] displayLists = new DisplayList[newSize];
   1358         System.arraycopy(mTextDisplayLists, 0, displayLists, 0, length);
   1359         mTextDisplayLists = displayLists;
   1360         return length;
   1361     }
   1362 
   1363     private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
   1364         final boolean translate = cursorOffsetVertical != 0;
   1365         if (translate) canvas.translate(0, cursorOffsetVertical);
   1366         for (int i = 0; i < mCursorCount; i++) {
   1367             mCursorDrawable[i].draw(canvas);
   1368         }
   1369         if (translate) canvas.translate(0, -cursorOffsetVertical);
   1370     }
   1371 
   1372     /**
   1373      * Invalidates all the sub-display lists that overlap the specified character range
   1374      */
   1375     void invalidateTextDisplayList(Layout layout, int start, int end) {
   1376         if (mTextDisplayLists != null && layout instanceof DynamicLayout) {
   1377             final int firstLine = layout.getLineForOffset(start);
   1378             final int lastLine = layout.getLineForOffset(end);
   1379 
   1380             DynamicLayout dynamicLayout = (DynamicLayout) layout;
   1381             int[] blockEndLines = dynamicLayout.getBlockEndLines();
   1382             int[] blockIndices = dynamicLayout.getBlockIndices();
   1383             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
   1384 
   1385             int i = 0;
   1386             // Skip the blocks before firstLine
   1387             while (i < numberOfBlocks) {
   1388                 if (blockEndLines[i] >= firstLine) break;
   1389                 i++;
   1390             }
   1391 
   1392             // Invalidate all subsequent blocks until lastLine is passed
   1393             while (i < numberOfBlocks) {
   1394                 final int blockIndex = blockIndices[i];
   1395                 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
   1396                     mTextDisplayLists[blockIndex].invalidate();
   1397                 }
   1398                 if (blockEndLines[i] >= lastLine) break;
   1399                 i++;
   1400             }
   1401         }
   1402     }
   1403 
   1404     void invalidateTextDisplayList() {
   1405         if (mTextDisplayLists != null) {
   1406             for (int i = 0; i < mTextDisplayLists.length; i++) {
   1407                 if (mTextDisplayLists[i] != null) mTextDisplayLists[i].invalidate();
   1408             }
   1409         }
   1410     }
   1411 
   1412     void updateCursorsPositions() {
   1413         if (mTextView.mCursorDrawableRes == 0) {
   1414             mCursorCount = 0;
   1415             return;
   1416         }
   1417 
   1418         Layout layout = mTextView.getLayout();
   1419         Layout hintLayout = mTextView.getHintLayout();
   1420         final int offset = mTextView.getSelectionStart();
   1421         final int line = layout.getLineForOffset(offset);
   1422         final int top = layout.getLineTop(line);
   1423         final int bottom = layout.getLineTop(line + 1);
   1424 
   1425         mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
   1426 
   1427         int middle = bottom;
   1428         if (mCursorCount == 2) {
   1429             // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
   1430             middle = (top + bottom) >> 1;
   1431         }
   1432 
   1433         updateCursorPosition(0, top, middle, getPrimaryHorizontal(layout, hintLayout, offset));
   1434 
   1435         if (mCursorCount == 2) {
   1436             updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset));
   1437         }
   1438     }
   1439 
   1440     private float getPrimaryHorizontal(Layout layout, Layout hintLayout, int offset) {
   1441         if (TextUtils.isEmpty(layout.getText()) &&
   1442                 hintLayout != null &&
   1443                 !TextUtils.isEmpty(hintLayout.getText())) {
   1444             return hintLayout.getPrimaryHorizontal(offset);
   1445         } else {
   1446             return layout.getPrimaryHorizontal(offset);
   1447         }
   1448     }
   1449 
   1450     /**
   1451      * @return true if the selection mode was actually started.
   1452      */
   1453     boolean startSelectionActionMode() {
   1454         if (mSelectionActionMode != null) {
   1455             // Selection action mode is already started
   1456             return false;
   1457         }
   1458 
   1459         if (!canSelectText() || !mTextView.requestFocus()) {
   1460             Log.w(TextView.LOG_TAG,
   1461                     "TextView does not support text selection. Action mode cancelled.");
   1462             return false;
   1463         }
   1464 
   1465         if (!mTextView.hasSelection()) {
   1466             // There may already be a selection on device rotation
   1467             if (!selectCurrentWord()) {
   1468                 // No word found under cursor or text selection not permitted.
   1469                 return false;
   1470             }
   1471         }
   1472 
   1473         boolean willExtract = extractedTextModeWillBeStarted();
   1474 
   1475         // Do not start the action mode when extracted text will show up full screen, which would
   1476         // immediately hide the newly created action bar and would be visually distracting.
   1477         if (!willExtract) {
   1478             ActionMode.Callback actionModeCallback = new SelectionActionModeCallback();
   1479             mSelectionActionMode = mTextView.startActionMode(actionModeCallback);
   1480         }
   1481 
   1482         final boolean selectionStarted = mSelectionActionMode != null || willExtract;
   1483         if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
   1484             // Show the IME to be able to replace text, except when selecting non editable text.
   1485             final InputMethodManager imm = InputMethodManager.peekInstance();
   1486             if (imm != null) {
   1487                 imm.showSoftInput(mTextView, 0, null);
   1488             }
   1489         }
   1490 
   1491         return selectionStarted;
   1492     }
   1493 
   1494     private boolean extractedTextModeWillBeStarted() {
   1495         if (!(mTextView instanceof ExtractEditText)) {
   1496             final InputMethodManager imm = InputMethodManager.peekInstance();
   1497             return  imm != null && imm.isFullscreenMode();
   1498         }
   1499         return false;
   1500     }
   1501 
   1502     /**
   1503      * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}.
   1504      */
   1505     private boolean isCursorInsideSuggestionSpan() {
   1506         CharSequence text = mTextView.getText();
   1507         if (!(text instanceof Spannable)) return false;
   1508 
   1509         SuggestionSpan[] suggestionSpans = ((Spannable) text).getSpans(
   1510                 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), SuggestionSpan.class);
   1511         return (suggestionSpans.length > 0);
   1512     }
   1513 
   1514     /**
   1515      * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
   1516      * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
   1517      */
   1518     private boolean isCursorInsideEasyCorrectionSpan() {
   1519         Spannable spannable = (Spannable) mTextView.getText();
   1520         SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
   1521                 mTextView.getSelectionEnd(), SuggestionSpan.class);
   1522         for (int i = 0; i < suggestionSpans.length; i++) {
   1523             if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
   1524                 return true;
   1525             }
   1526         }
   1527         return false;
   1528     }
   1529 
   1530     void onTouchUpEvent(MotionEvent event) {
   1531         boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
   1532         hideControllers();
   1533         CharSequence text = mTextView.getText();
   1534         if (!selectAllGotFocus && text.length() > 0) {
   1535             // Move cursor
   1536             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   1537             Selection.setSelection((Spannable) text, offset);
   1538             if (mSpellChecker != null) {
   1539                 // When the cursor moves, the word that was typed may need spell check
   1540                 mSpellChecker.onSelectionChanged();
   1541             }
   1542             if (!extractedTextModeWillBeStarted()) {
   1543                 if (isCursorInsideEasyCorrectionSpan()) {
   1544                     mShowSuggestionRunnable = new Runnable() {
   1545                         public void run() {
   1546                             showSuggestions();
   1547                         }
   1548                     };
   1549                     // removeCallbacks is performed on every touch
   1550                     mTextView.postDelayed(mShowSuggestionRunnable,
   1551                             ViewConfiguration.getDoubleTapTimeout());
   1552                 } else if (hasInsertionController()) {
   1553                     getInsertionController().show();
   1554                 }
   1555             }
   1556         }
   1557     }
   1558 
   1559     protected void stopSelectionActionMode() {
   1560         if (mSelectionActionMode != null) {
   1561             // This will hide the mSelectionModifierCursorController
   1562             mSelectionActionMode.finish();
   1563         }
   1564     }
   1565 
   1566     /**
   1567      * @return True if this view supports insertion handles.
   1568      */
   1569     boolean hasInsertionController() {
   1570         return mInsertionControllerEnabled;
   1571     }
   1572 
   1573     /**
   1574      * @return True if this view supports selection handles.
   1575      */
   1576     boolean hasSelectionController() {
   1577         return mSelectionControllerEnabled;
   1578     }
   1579 
   1580     InsertionPointCursorController getInsertionController() {
   1581         if (!mInsertionControllerEnabled) {
   1582             return null;
   1583         }
   1584 
   1585         if (mInsertionPointCursorController == null) {
   1586             mInsertionPointCursorController = new InsertionPointCursorController();
   1587 
   1588             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   1589             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
   1590         }
   1591 
   1592         return mInsertionPointCursorController;
   1593     }
   1594 
   1595     SelectionModifierCursorController getSelectionController() {
   1596         if (!mSelectionControllerEnabled) {
   1597             return null;
   1598         }
   1599 
   1600         if (mSelectionModifierCursorController == null) {
   1601             mSelectionModifierCursorController = new SelectionModifierCursorController();
   1602 
   1603             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   1604             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
   1605         }
   1606 
   1607         return mSelectionModifierCursorController;
   1608     }
   1609 
   1610     private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
   1611         if (mCursorDrawable[cursorIndex] == null)
   1612             mCursorDrawable[cursorIndex] = mTextView.getResources().getDrawable(
   1613                     mTextView.mCursorDrawableRes);
   1614 
   1615         if (mTempRect == null) mTempRect = new Rect();
   1616         mCursorDrawable[cursorIndex].getPadding(mTempRect);
   1617         final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth();
   1618         horizontal = Math.max(0.5f, horizontal - 0.5f);
   1619         final int left = (int) (horizontal) - mTempRect.left;
   1620         mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width,
   1621                 bottom + mTempRect.bottom);
   1622     }
   1623 
   1624     /**
   1625      * Called by the framework in response to a text auto-correction (such as fixing a typo using a
   1626      * a dictionnary) from the current input method, provided by it calling
   1627      * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
   1628      * implementation flashes the background of the corrected word to provide feedback to the user.
   1629      *
   1630      * @param info The auto correct info about the text that was corrected.
   1631      */
   1632     public void onCommitCorrection(CorrectionInfo info) {
   1633         if (mCorrectionHighlighter == null) {
   1634             mCorrectionHighlighter = new CorrectionHighlighter();
   1635         } else {
   1636             mCorrectionHighlighter.invalidate(false);
   1637         }
   1638 
   1639         mCorrectionHighlighter.highlight(info);
   1640     }
   1641 
   1642     void showSuggestions() {
   1643         if (mSuggestionsPopupWindow == null) {
   1644             mSuggestionsPopupWindow = new SuggestionsPopupWindow();
   1645         }
   1646         hideControllers();
   1647         mSuggestionsPopupWindow.show();
   1648     }
   1649 
   1650     boolean areSuggestionsShown() {
   1651         return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing();
   1652     }
   1653 
   1654     void onScrollChanged() {
   1655         if (mPositionListener != null) {
   1656             mPositionListener.onScrollChanged();
   1657         }
   1658     }
   1659 
   1660     /**
   1661      * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
   1662      */
   1663     private boolean shouldBlink() {
   1664         if (!isCursorVisible() || !mTextView.isFocused()) return false;
   1665 
   1666         final int start = mTextView.getSelectionStart();
   1667         if (start < 0) return false;
   1668 
   1669         final int end = mTextView.getSelectionEnd();
   1670         if (end < 0) return false;
   1671 
   1672         return start == end;
   1673     }
   1674 
   1675     void makeBlink() {
   1676         if (shouldBlink()) {
   1677             mShowCursor = SystemClock.uptimeMillis();
   1678             if (mBlink == null) mBlink = new Blink();
   1679             mBlink.removeCallbacks(mBlink);
   1680             mBlink.postAtTime(mBlink, mShowCursor + BLINK);
   1681         } else {
   1682             if (mBlink != null) mBlink.removeCallbacks(mBlink);
   1683         }
   1684     }
   1685 
   1686     private class Blink extends Handler implements Runnable {
   1687         private boolean mCancelled;
   1688 
   1689         public void run() {
   1690             if (mCancelled) {
   1691                 return;
   1692             }
   1693 
   1694             removeCallbacks(Blink.this);
   1695 
   1696             if (shouldBlink()) {
   1697                 if (mTextView.getLayout() != null) {
   1698                     mTextView.invalidateCursorPath();
   1699                 }
   1700 
   1701                 postAtTime(this, SystemClock.uptimeMillis() + BLINK);
   1702             }
   1703         }
   1704 
   1705         void cancel() {
   1706             if (!mCancelled) {
   1707                 removeCallbacks(Blink.this);
   1708                 mCancelled = true;
   1709             }
   1710         }
   1711 
   1712         void uncancel() {
   1713             mCancelled = false;
   1714         }
   1715     }
   1716 
   1717     private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) {
   1718         TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
   1719                 com.android.internal.R.layout.text_drag_thumbnail, null);
   1720 
   1721         if (shadowView == null) {
   1722             throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
   1723         }
   1724 
   1725         if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) {
   1726             text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH);
   1727         }
   1728         shadowView.setText(text);
   1729         shadowView.setTextColor(mTextView.getTextColors());
   1730 
   1731         shadowView.setTextAppearance(mTextView.getContext(), R.styleable.Theme_textAppearanceLarge);
   1732         shadowView.setGravity(Gravity.CENTER);
   1733 
   1734         shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   1735                 ViewGroup.LayoutParams.WRAP_CONTENT));
   1736 
   1737         final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
   1738         shadowView.measure(size, size);
   1739 
   1740         shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
   1741         shadowView.invalidate();
   1742         return new DragShadowBuilder(shadowView);
   1743     }
   1744 
   1745     private static class DragLocalState {
   1746         public TextView sourceTextView;
   1747         public int start, end;
   1748 
   1749         public DragLocalState(TextView sourceTextView, int start, int end) {
   1750             this.sourceTextView = sourceTextView;
   1751             this.start = start;
   1752             this.end = end;
   1753         }
   1754     }
   1755 
   1756     void onDrop(DragEvent event) {
   1757         StringBuilder content = new StringBuilder("");
   1758         ClipData clipData = event.getClipData();
   1759         final int itemCount = clipData.getItemCount();
   1760         for (int i=0; i < itemCount; i++) {
   1761             Item item = clipData.getItemAt(i);
   1762             content.append(item.coerceToStyledText(mTextView.getContext()));
   1763         }
   1764 
   1765         final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   1766 
   1767         Object localState = event.getLocalState();
   1768         DragLocalState dragLocalState = null;
   1769         if (localState instanceof DragLocalState) {
   1770             dragLocalState = (DragLocalState) localState;
   1771         }
   1772         boolean dragDropIntoItself = dragLocalState != null &&
   1773                 dragLocalState.sourceTextView == mTextView;
   1774 
   1775         if (dragDropIntoItself) {
   1776             if (offset >= dragLocalState.start && offset < dragLocalState.end) {
   1777                 // A drop inside the original selection discards the drop.
   1778                 return;
   1779             }
   1780         }
   1781 
   1782         final int originalLength = mTextView.getText().length();
   1783         long minMax = mTextView.prepareSpacesAroundPaste(offset, offset, content);
   1784         int min = TextUtils.unpackRangeStartFromLong(minMax);
   1785         int max = TextUtils.unpackRangeEndFromLong(minMax);
   1786 
   1787         Selection.setSelection((Spannable) mTextView.getText(), max);
   1788         mTextView.replaceText_internal(min, max, content);
   1789 
   1790         if (dragDropIntoItself) {
   1791             int dragSourceStart = dragLocalState.start;
   1792             int dragSourceEnd = dragLocalState.end;
   1793             if (max <= dragSourceStart) {
   1794                 // Inserting text before selection has shifted positions
   1795                 final int shift = mTextView.getText().length() - originalLength;
   1796                 dragSourceStart += shift;
   1797                 dragSourceEnd += shift;
   1798             }
   1799 
   1800             // Delete original selection
   1801             mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
   1802 
   1803             // Make sure we do not leave two adjacent spaces.
   1804             CharSequence t = mTextView.getTransformedText(dragSourceStart - 1, dragSourceStart + 1);
   1805             if ( (dragSourceStart == 0 || Character.isSpaceChar(t.charAt(0))) &&
   1806                     (dragSourceStart == mTextView.getText().length() ||
   1807                     Character.isSpaceChar(t.charAt(1))) ) {
   1808                 final int pos = dragSourceStart == mTextView.getText().length() ?
   1809                         dragSourceStart - 1 : dragSourceStart;
   1810                 mTextView.deleteText_internal(pos, pos + 1);
   1811             }
   1812         }
   1813     }
   1814 
   1815     public void addSpanWatchers(Spannable text) {
   1816         final int textLength = text.length();
   1817 
   1818         if (mKeyListener != null) {
   1819             text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
   1820         }
   1821 
   1822         if (mEasyEditSpanController == null) {
   1823             mEasyEditSpanController = new EasyEditSpanController();
   1824         }
   1825         text.setSpan(mEasyEditSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
   1826     }
   1827 
   1828     /**
   1829      * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
   1830      * pop-up should be displayed.
   1831      */
   1832     class EasyEditSpanController implements SpanWatcher {
   1833 
   1834         private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
   1835 
   1836         private EasyEditPopupWindow mPopupWindow;
   1837 
   1838         private Runnable mHidePopup;
   1839 
   1840         @Override
   1841         public void onSpanAdded(Spannable text, Object span, int start, int end) {
   1842             if (span instanceof EasyEditSpan) {
   1843                 if (mPopupWindow == null) {
   1844                     mPopupWindow = new EasyEditPopupWindow();
   1845                     mHidePopup = new Runnable() {
   1846                         @Override
   1847                         public void run() {
   1848                             hide();
   1849                         }
   1850                     };
   1851                 }
   1852 
   1853                 // Make sure there is only at most one EasyEditSpan in the text
   1854                 if (mPopupWindow.mEasyEditSpan != null) {
   1855                     text.removeSpan(mPopupWindow.mEasyEditSpan);
   1856                 }
   1857 
   1858                 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
   1859 
   1860                 if (mTextView.getWindowVisibility() != View.VISIBLE) {
   1861                     // The window is not visible yet, ignore the text change.
   1862                     return;
   1863                 }
   1864 
   1865                 if (mTextView.getLayout() == null) {
   1866                     // The view has not been laid out yet, ignore the text change
   1867                     return;
   1868                 }
   1869 
   1870                 if (extractedTextModeWillBeStarted()) {
   1871                     // The input is in extract mode. Do not handle the easy edit in
   1872                     // the original TextView, as the ExtractEditText will do
   1873                     return;
   1874                 }
   1875 
   1876                 mPopupWindow.show();
   1877                 mTextView.removeCallbacks(mHidePopup);
   1878                 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
   1879             }
   1880         }
   1881 
   1882         @Override
   1883         public void onSpanRemoved(Spannable text, Object span, int start, int end) {
   1884             if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
   1885                 hide();
   1886             }
   1887         }
   1888 
   1889         @Override
   1890         public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
   1891                 int newStart, int newEnd) {
   1892             if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
   1893                 text.removeSpan(mPopupWindow.mEasyEditSpan);
   1894             }
   1895         }
   1896 
   1897         public void hide() {
   1898             if (mPopupWindow != null) {
   1899                 mPopupWindow.hide();
   1900                 mTextView.removeCallbacks(mHidePopup);
   1901             }
   1902         }
   1903     }
   1904 
   1905     /**
   1906      * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
   1907      * by {@link EasyEditSpanController}.
   1908      */
   1909     private class EasyEditPopupWindow extends PinnedPopupWindow
   1910             implements OnClickListener {
   1911         private static final int POPUP_TEXT_LAYOUT =
   1912                 com.android.internal.R.layout.text_edit_action_popup_text;
   1913         private TextView mDeleteTextView;
   1914         private EasyEditSpan mEasyEditSpan;
   1915 
   1916         @Override
   1917         protected void createPopupWindow() {
   1918             mPopupWindow = new PopupWindow(mTextView.getContext(), null,
   1919                     com.android.internal.R.attr.textSelectHandleWindowStyle);
   1920             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
   1921             mPopupWindow.setClippingEnabled(true);
   1922         }
   1923 
   1924         @Override
   1925         protected void initContentView() {
   1926             LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
   1927             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
   1928             mContentView = linearLayout;
   1929             mContentView.setBackgroundResource(
   1930                     com.android.internal.R.drawable.text_edit_side_paste_window);
   1931 
   1932             LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
   1933                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   1934 
   1935             LayoutParams wrapContent = new LayoutParams(
   1936                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   1937 
   1938             mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
   1939             mDeleteTextView.setLayoutParams(wrapContent);
   1940             mDeleteTextView.setText(com.android.internal.R.string.delete);
   1941             mDeleteTextView.setOnClickListener(this);
   1942             mContentView.addView(mDeleteTextView);
   1943         }
   1944 
   1945         public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
   1946             mEasyEditSpan = easyEditSpan;
   1947         }
   1948 
   1949         @Override
   1950         public void onClick(View view) {
   1951             if (view == mDeleteTextView) {
   1952                 Editable editable = (Editable) mTextView.getText();
   1953                 int start = editable.getSpanStart(mEasyEditSpan);
   1954                 int end = editable.getSpanEnd(mEasyEditSpan);
   1955                 if (start >= 0 && end >= 0) {
   1956                     mTextView.deleteText_internal(start, end);
   1957                 }
   1958             }
   1959         }
   1960 
   1961         @Override
   1962         protected int getTextOffset() {
   1963             // Place the pop-up at the end of the span
   1964             Editable editable = (Editable) mTextView.getText();
   1965             return editable.getSpanEnd(mEasyEditSpan);
   1966         }
   1967 
   1968         @Override
   1969         protected int getVerticalLocalPosition(int line) {
   1970             return mTextView.getLayout().getLineBottom(line);
   1971         }
   1972 
   1973         @Override
   1974         protected int clipVertically(int positionY) {
   1975             // As we display the pop-up below the span, no vertical clipping is required.
   1976             return positionY;
   1977         }
   1978     }
   1979 
   1980     private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
   1981         // 3 handles
   1982         // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
   1983         private final int MAXIMUM_NUMBER_OF_LISTENERS = 6;
   1984         private TextViewPositionListener[] mPositionListeners =
   1985                 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
   1986         private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
   1987         private boolean mPositionHasChanged = true;
   1988         // Absolute position of the TextView with respect to its parent window
   1989         private int mPositionX, mPositionY;
   1990         private int mNumberOfListeners;
   1991         private boolean mScrollHasChanged;
   1992         final int[] mTempCoords = new int[2];
   1993 
   1994         public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
   1995             if (mNumberOfListeners == 0) {
   1996                 updatePosition();
   1997                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
   1998                 vto.addOnPreDrawListener(this);
   1999             }
   2000 
   2001             int emptySlotIndex = -1;
   2002             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2003                 TextViewPositionListener listener = mPositionListeners[i];
   2004                 if (listener == positionListener) {
   2005                     return;
   2006                 } else if (emptySlotIndex < 0 && listener == null) {
   2007                     emptySlotIndex = i;
   2008                 }
   2009             }
   2010 
   2011             mPositionListeners[emptySlotIndex] = positionListener;
   2012             mCanMove[emptySlotIndex] = canMove;
   2013             mNumberOfListeners++;
   2014         }
   2015 
   2016         public void removeSubscriber(TextViewPositionListener positionListener) {
   2017             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2018                 if (mPositionListeners[i] == positionListener) {
   2019                     mPositionListeners[i] = null;
   2020                     mNumberOfListeners--;
   2021                     break;
   2022                 }
   2023             }
   2024 
   2025             if (mNumberOfListeners == 0) {
   2026                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
   2027                 vto.removeOnPreDrawListener(this);
   2028             }
   2029         }
   2030 
   2031         public int getPositionX() {
   2032             return mPositionX;
   2033         }
   2034 
   2035         public int getPositionY() {
   2036             return mPositionY;
   2037         }
   2038 
   2039         @Override
   2040         public boolean onPreDraw() {
   2041             updatePosition();
   2042 
   2043             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2044                 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
   2045                     TextViewPositionListener positionListener = mPositionListeners[i];
   2046                     if (positionListener != null) {
   2047                         positionListener.updatePosition(mPositionX, mPositionY,
   2048                                 mPositionHasChanged, mScrollHasChanged);
   2049                     }
   2050                 }
   2051             }
   2052 
   2053             mScrollHasChanged = false;
   2054             return true;
   2055         }
   2056 
   2057         private void updatePosition() {
   2058             mTextView.getLocationInWindow(mTempCoords);
   2059 
   2060             mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
   2061 
   2062             mPositionX = mTempCoords[0];
   2063             mPositionY = mTempCoords[1];
   2064         }
   2065 
   2066         public void onScrollChanged() {
   2067             mScrollHasChanged = true;
   2068         }
   2069     }
   2070 
   2071     private abstract class PinnedPopupWindow implements TextViewPositionListener {
   2072         protected PopupWindow mPopupWindow;
   2073         protected ViewGroup mContentView;
   2074         int mPositionX, mPositionY;
   2075 
   2076         protected abstract void createPopupWindow();
   2077         protected abstract void initContentView();
   2078         protected abstract int getTextOffset();
   2079         protected abstract int getVerticalLocalPosition(int line);
   2080         protected abstract int clipVertically(int positionY);
   2081 
   2082         public PinnedPopupWindow() {
   2083             createPopupWindow();
   2084 
   2085             mPopupWindow.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
   2086             mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
   2087             mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
   2088 
   2089             initContentView();
   2090 
   2091             LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   2092                     ViewGroup.LayoutParams.WRAP_CONTENT);
   2093             mContentView.setLayoutParams(wrapContent);
   2094 
   2095             mPopupWindow.setContentView(mContentView);
   2096         }
   2097 
   2098         public void show() {
   2099             getPositionListener().addSubscriber(this, false /* offset is fixed */);
   2100 
   2101             computeLocalPosition();
   2102 
   2103             final PositionListener positionListener = getPositionListener();
   2104             updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
   2105         }
   2106 
   2107         protected void measureContent() {
   2108             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   2109             mContentView.measure(
   2110                     View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
   2111                             View.MeasureSpec.AT_MOST),
   2112                     View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
   2113                             View.MeasureSpec.AT_MOST));
   2114         }
   2115 
   2116         /* The popup window will be horizontally centered on the getTextOffset() and vertically
   2117          * positioned according to viewportToContentHorizontalOffset.
   2118          *
   2119          * This method assumes that mContentView has properly been measured from its content. */
   2120         private void computeLocalPosition() {
   2121             measureContent();
   2122             final int width = mContentView.getMeasuredWidth();
   2123             final int offset = getTextOffset();
   2124             mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
   2125             mPositionX += mTextView.viewportToContentHorizontalOffset();
   2126 
   2127             final int line = mTextView.getLayout().getLineForOffset(offset);
   2128             mPositionY = getVerticalLocalPosition(line);
   2129             mPositionY += mTextView.viewportToContentVerticalOffset();
   2130         }
   2131 
   2132         private void updatePosition(int parentPositionX, int parentPositionY) {
   2133             int positionX = parentPositionX + mPositionX;
   2134             int positionY = parentPositionY + mPositionY;
   2135 
   2136             positionY = clipVertically(positionY);
   2137 
   2138             // Horizontal clipping
   2139             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   2140             final int width = mContentView.getMeasuredWidth();
   2141             positionX = Math.min(displayMetrics.widthPixels - width, positionX);
   2142             positionX = Math.max(0, positionX);
   2143 
   2144             if (isShowing()) {
   2145                 mPopupWindow.update(positionX, positionY, -1, -1);
   2146             } else {
   2147                 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
   2148                         positionX, positionY);
   2149             }
   2150         }
   2151 
   2152         public void hide() {
   2153             mPopupWindow.dismiss();
   2154             getPositionListener().removeSubscriber(this);
   2155         }
   2156 
   2157         @Override
   2158         public void updatePosition(int parentPositionX, int parentPositionY,
   2159                 boolean parentPositionChanged, boolean parentScrolled) {
   2160             // Either parentPositionChanged or parentScrolled is true, check if still visible
   2161             if (isShowing() && isOffsetVisible(getTextOffset())) {
   2162                 if (parentScrolled) computeLocalPosition();
   2163                 updatePosition(parentPositionX, parentPositionY);
   2164             } else {
   2165                 hide();
   2166             }
   2167         }
   2168 
   2169         public boolean isShowing() {
   2170             return mPopupWindow.isShowing();
   2171         }
   2172     }
   2173 
   2174     private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
   2175         private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
   2176         private static final int ADD_TO_DICTIONARY = -1;
   2177         private static final int DELETE_TEXT = -2;
   2178         private SuggestionInfo[] mSuggestionInfos;
   2179         private int mNumberOfSuggestions;
   2180         private boolean mCursorWasVisibleBeforeSuggestions;
   2181         private boolean mIsShowingUp = false;
   2182         private SuggestionAdapter mSuggestionsAdapter;
   2183         private final Comparator<SuggestionSpan> mSuggestionSpanComparator;
   2184         private final HashMap<SuggestionSpan, Integer> mSpansLengths;
   2185 
   2186         private class CustomPopupWindow extends PopupWindow {
   2187             public CustomPopupWindow(Context context, int defStyle) {
   2188                 super(context, null, defStyle);
   2189             }
   2190 
   2191             @Override
   2192             public void dismiss() {
   2193                 super.dismiss();
   2194 
   2195                 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
   2196 
   2197                 // Safe cast since show() checks that mTextView.getText() is an Editable
   2198                 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
   2199 
   2200                 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
   2201                 if (hasInsertionController()) {
   2202                     getInsertionController().show();
   2203                 }
   2204             }
   2205         }
   2206 
   2207         public SuggestionsPopupWindow() {
   2208             mCursorWasVisibleBeforeSuggestions = mCursorVisible;
   2209             mSuggestionSpanComparator = new SuggestionSpanComparator();
   2210             mSpansLengths = new HashMap<SuggestionSpan, Integer>();
   2211         }
   2212 
   2213         @Override
   2214         protected void createPopupWindow() {
   2215             mPopupWindow = new CustomPopupWindow(mTextView.getContext(),
   2216                 com.android.internal.R.attr.textSuggestionsWindowStyle);
   2217             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
   2218             mPopupWindow.setFocusable(true);
   2219             mPopupWindow.setClippingEnabled(false);
   2220         }
   2221 
   2222         @Override
   2223         protected void initContentView() {
   2224             ListView listView = new ListView(mTextView.getContext());
   2225             mSuggestionsAdapter = new SuggestionAdapter();
   2226             listView.setAdapter(mSuggestionsAdapter);
   2227             listView.setOnItemClickListener(this);
   2228             mContentView = listView;
   2229 
   2230             // Inflate the suggestion items once and for all. + 2 for add to dictionary and delete
   2231             mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS + 2];
   2232             for (int i = 0; i < mSuggestionInfos.length; i++) {
   2233                 mSuggestionInfos[i] = new SuggestionInfo();
   2234             }
   2235         }
   2236 
   2237         public boolean isShowingUp() {
   2238             return mIsShowingUp;
   2239         }
   2240 
   2241         public void onParentLostFocus() {
   2242             mIsShowingUp = false;
   2243         }
   2244 
   2245         private class SuggestionInfo {
   2246             int suggestionStart, suggestionEnd; // range of actual suggestion within text
   2247             SuggestionSpan suggestionSpan; // the SuggestionSpan that this TextView represents
   2248             int suggestionIndex; // the index of this suggestion inside suggestionSpan
   2249             SpannableStringBuilder text = new SpannableStringBuilder();
   2250             TextAppearanceSpan highlightSpan = new TextAppearanceSpan(mTextView.getContext(),
   2251                     android.R.style.TextAppearance_SuggestionHighlight);
   2252         }
   2253 
   2254         private class SuggestionAdapter extends BaseAdapter {
   2255             private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext().
   2256                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   2257 
   2258             @Override
   2259             public int getCount() {
   2260                 return mNumberOfSuggestions;
   2261             }
   2262 
   2263             @Override
   2264             public Object getItem(int position) {
   2265                 return mSuggestionInfos[position];
   2266             }
   2267 
   2268             @Override
   2269             public long getItemId(int position) {
   2270                 return position;
   2271             }
   2272 
   2273             @Override
   2274             public View getView(int position, View convertView, ViewGroup parent) {
   2275                 TextView textView = (TextView) convertView;
   2276 
   2277                 if (textView == null) {
   2278                     textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
   2279                             parent, false);
   2280                 }
   2281 
   2282                 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
   2283                 textView.setText(suggestionInfo.text);
   2284 
   2285                 if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
   2286                     textView.setCompoundDrawablesWithIntrinsicBounds(
   2287                             com.android.internal.R.drawable.ic_suggestions_add, 0, 0, 0);
   2288                 } else if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
   2289                     textView.setCompoundDrawablesWithIntrinsicBounds(
   2290                             com.android.internal.R.drawable.ic_suggestions_delete, 0, 0, 0);
   2291                 } else {
   2292                     textView.setCompoundDrawables(null, null, null, null);
   2293                 }
   2294 
   2295                 return textView;
   2296             }
   2297         }
   2298 
   2299         private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
   2300             public int compare(SuggestionSpan span1, SuggestionSpan span2) {
   2301                 final int flag1 = span1.getFlags();
   2302                 final int flag2 = span2.getFlags();
   2303                 if (flag1 != flag2) {
   2304                     // The order here should match what is used in updateDrawState
   2305                     final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
   2306                     final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
   2307                     final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
   2308                     final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
   2309                     if (easy1 && !misspelled1) return -1;
   2310                     if (easy2 && !misspelled2) return 1;
   2311                     if (misspelled1) return -1;
   2312                     if (misspelled2) return 1;
   2313                 }
   2314 
   2315                 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
   2316             }
   2317         }
   2318 
   2319         /**
   2320          * Returns the suggestion spans that cover the current cursor position. The suggestion
   2321          * spans are sorted according to the length of text that they are attached to.
   2322          */
   2323         private SuggestionSpan[] getSuggestionSpans() {
   2324             int pos = mTextView.getSelectionStart();
   2325             Spannable spannable = (Spannable) mTextView.getText();
   2326             SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
   2327 
   2328             mSpansLengths.clear();
   2329             for (SuggestionSpan suggestionSpan : suggestionSpans) {
   2330                 int start = spannable.getSpanStart(suggestionSpan);
   2331                 int end = spannable.getSpanEnd(suggestionSpan);
   2332                 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
   2333             }
   2334 
   2335             // The suggestions are sorted according to their types (easy correction first, then
   2336             // misspelled) and to the length of the text that they cover (shorter first).
   2337             Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
   2338             return suggestionSpans;
   2339         }
   2340 
   2341         @Override
   2342         public void show() {
   2343             if (!(mTextView.getText() instanceof Editable)) return;
   2344 
   2345             if (updateSuggestions()) {
   2346                 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
   2347                 mTextView.setCursorVisible(false);
   2348                 mIsShowingUp = true;
   2349                 super.show();
   2350             }
   2351         }
   2352 
   2353         @Override
   2354         protected void measureContent() {
   2355             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   2356             final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
   2357                     displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
   2358             final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
   2359                     displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
   2360 
   2361             int width = 0;
   2362             View view = null;
   2363             for (int i = 0; i < mNumberOfSuggestions; i++) {
   2364                 view = mSuggestionsAdapter.getView(i, view, mContentView);
   2365                 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
   2366                 view.measure(horizontalMeasure, verticalMeasure);
   2367                 width = Math.max(width, view.getMeasuredWidth());
   2368             }
   2369 
   2370             // Enforce the width based on actual text widths
   2371             mContentView.measure(
   2372                     View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
   2373                     verticalMeasure);
   2374 
   2375             Drawable popupBackground = mPopupWindow.getBackground();
   2376             if (popupBackground != null) {
   2377                 if (mTempRect == null) mTempRect = new Rect();
   2378                 popupBackground.getPadding(mTempRect);
   2379                 width += mTempRect.left + mTempRect.right;
   2380             }
   2381             mPopupWindow.setWidth(width);
   2382         }
   2383 
   2384         @Override
   2385         protected int getTextOffset() {
   2386             return mTextView.getSelectionStart();
   2387         }
   2388 
   2389         @Override
   2390         protected int getVerticalLocalPosition(int line) {
   2391             return mTextView.getLayout().getLineBottom(line);
   2392         }
   2393 
   2394         @Override
   2395         protected int clipVertically(int positionY) {
   2396             final int height = mContentView.getMeasuredHeight();
   2397             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   2398             return Math.min(positionY, displayMetrics.heightPixels - height);
   2399         }
   2400 
   2401         @Override
   2402         public void hide() {
   2403             super.hide();
   2404         }
   2405 
   2406         private boolean updateSuggestions() {
   2407             Spannable spannable = (Spannable) mTextView.getText();
   2408             SuggestionSpan[] suggestionSpans = getSuggestionSpans();
   2409 
   2410             final int nbSpans = suggestionSpans.length;
   2411             // Suggestions are shown after a delay: the underlying spans may have been removed
   2412             if (nbSpans == 0) return false;
   2413 
   2414             mNumberOfSuggestions = 0;
   2415             int spanUnionStart = mTextView.getText().length();
   2416             int spanUnionEnd = 0;
   2417 
   2418             SuggestionSpan misspelledSpan = null;
   2419             int underlineColor = 0;
   2420 
   2421             for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) {
   2422                 SuggestionSpan suggestionSpan = suggestionSpans[spanIndex];
   2423                 final int spanStart = spannable.getSpanStart(suggestionSpan);
   2424                 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
   2425                 spanUnionStart = Math.min(spanStart, spanUnionStart);
   2426                 spanUnionEnd = Math.max(spanEnd, spanUnionEnd);
   2427 
   2428                 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
   2429                     misspelledSpan = suggestionSpan;
   2430                 }
   2431 
   2432                 // The first span dictates the background color of the highlighted text
   2433                 if (spanIndex == 0) underlineColor = suggestionSpan.getUnderlineColor();
   2434 
   2435                 String[] suggestions = suggestionSpan.getSuggestions();
   2436                 int nbSuggestions = suggestions.length;
   2437                 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
   2438                     String suggestion = suggestions[suggestionIndex];
   2439 
   2440                     boolean suggestionIsDuplicate = false;
   2441                     for (int i = 0; i < mNumberOfSuggestions; i++) {
   2442                         if (mSuggestionInfos[i].text.toString().equals(suggestion)) {
   2443                             SuggestionSpan otherSuggestionSpan = mSuggestionInfos[i].suggestionSpan;
   2444                             final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan);
   2445                             final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan);
   2446                             if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
   2447                                 suggestionIsDuplicate = true;
   2448                                 break;
   2449                             }
   2450                         }
   2451                     }
   2452 
   2453                     if (!suggestionIsDuplicate) {
   2454                         SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
   2455                         suggestionInfo.suggestionSpan = suggestionSpan;
   2456                         suggestionInfo.suggestionIndex = suggestionIndex;
   2457                         suggestionInfo.text.replace(0, suggestionInfo.text.length(), suggestion);
   2458 
   2459                         mNumberOfSuggestions++;
   2460 
   2461                         if (mNumberOfSuggestions == MAX_NUMBER_SUGGESTIONS) {
   2462                             // Also end outer for loop
   2463                             spanIndex = nbSpans;
   2464                             break;
   2465                         }
   2466                     }
   2467                 }
   2468             }
   2469 
   2470             for (int i = 0; i < mNumberOfSuggestions; i++) {
   2471                 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
   2472             }
   2473 
   2474             // Add "Add to dictionary" item if there is a span with the misspelled flag
   2475             if (misspelledSpan != null) {
   2476                 final int misspelledStart = spannable.getSpanStart(misspelledSpan);
   2477                 final int misspelledEnd = spannable.getSpanEnd(misspelledSpan);
   2478                 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) {
   2479                     SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
   2480                     suggestionInfo.suggestionSpan = misspelledSpan;
   2481                     suggestionInfo.suggestionIndex = ADD_TO_DICTIONARY;
   2482                     suggestionInfo.text.replace(0, suggestionInfo.text.length(), mTextView.
   2483                             getContext().getString(com.android.internal.R.string.addToDictionary));
   2484                     suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
   2485                             Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2486 
   2487                     mNumberOfSuggestions++;
   2488                 }
   2489             }
   2490 
   2491             // Delete item
   2492             SuggestionInfo suggestionInfo = mSuggestionInfos[mNumberOfSuggestions];
   2493             suggestionInfo.suggestionSpan = null;
   2494             suggestionInfo.suggestionIndex = DELETE_TEXT;
   2495             suggestionInfo.text.replace(0, suggestionInfo.text.length(),
   2496                     mTextView.getContext().getString(com.android.internal.R.string.deleteText));
   2497             suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0, 0,
   2498                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2499             mNumberOfSuggestions++;
   2500 
   2501             if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
   2502             if (underlineColor == 0) {
   2503                 // Fallback on the default highlight color when the first span does not provide one
   2504                 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
   2505             } else {
   2506                 final float BACKGROUND_TRANSPARENCY = 0.4f;
   2507                 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
   2508                 mSuggestionRangeSpan.setBackgroundColor(
   2509                         (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
   2510             }
   2511             spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
   2512                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2513 
   2514             mSuggestionsAdapter.notifyDataSetChanged();
   2515             return true;
   2516         }
   2517 
   2518         private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
   2519                 int unionEnd) {
   2520             final Spannable text = (Spannable) mTextView.getText();
   2521             final int spanStart = text.getSpanStart(suggestionInfo.suggestionSpan);
   2522             final int spanEnd = text.getSpanEnd(suggestionInfo.suggestionSpan);
   2523 
   2524             // Adjust the start/end of the suggestion span
   2525             suggestionInfo.suggestionStart = spanStart - unionStart;
   2526             suggestionInfo.suggestionEnd = suggestionInfo.suggestionStart
   2527                     + suggestionInfo.text.length();
   2528 
   2529             suggestionInfo.text.setSpan(suggestionInfo.highlightSpan, 0,
   2530                     suggestionInfo.text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   2531 
   2532             // Add the text before and after the span.
   2533             final String textAsString = text.toString();
   2534             suggestionInfo.text.insert(0, textAsString.substring(unionStart, spanStart));
   2535             suggestionInfo.text.append(textAsString.substring(spanEnd, unionEnd));
   2536         }
   2537 
   2538         @Override
   2539         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   2540             Editable editable = (Editable) mTextView.getText();
   2541             SuggestionInfo suggestionInfo = mSuggestionInfos[position];
   2542 
   2543             if (suggestionInfo.suggestionIndex == DELETE_TEXT) {
   2544                 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
   2545                 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
   2546                 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
   2547                     // Do not leave two adjacent spaces after deletion, or one at beginning of text
   2548                     if (spanUnionEnd < editable.length() &&
   2549                             Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
   2550                             (spanUnionStart == 0 ||
   2551                             Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
   2552                         spanUnionEnd = spanUnionEnd + 1;
   2553                     }
   2554                     mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
   2555                 }
   2556                 hide();
   2557                 return;
   2558             }
   2559 
   2560             final int spanStart = editable.getSpanStart(suggestionInfo.suggestionSpan);
   2561             final int spanEnd = editable.getSpanEnd(suggestionInfo.suggestionSpan);
   2562             if (spanStart < 0 || spanEnd <= spanStart) {
   2563                 // Span has been removed
   2564                 hide();
   2565                 return;
   2566             }
   2567 
   2568             final String originalText = editable.toString().substring(spanStart, spanEnd);
   2569 
   2570             if (suggestionInfo.suggestionIndex == ADD_TO_DICTIONARY) {
   2571                 Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
   2572                 intent.putExtra("word", originalText);
   2573                 intent.putExtra("locale", mTextView.getTextServicesLocale().toString());
   2574                 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
   2575                 mTextView.getContext().startActivity(intent);
   2576                 // There is no way to know if the word was indeed added. Re-check.
   2577                 // TODO The ExtractEditText should remove the span in the original text instead
   2578                 editable.removeSpan(suggestionInfo.suggestionSpan);
   2579                 Selection.setSelection(editable, spanEnd);
   2580                 updateSpellCheckSpans(spanStart, spanEnd, false);
   2581             } else {
   2582                 // SuggestionSpans are removed by replace: save them before
   2583                 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
   2584                         SuggestionSpan.class);
   2585                 final int length = suggestionSpans.length;
   2586                 int[] suggestionSpansStarts = new int[length];
   2587                 int[] suggestionSpansEnds = new int[length];
   2588                 int[] suggestionSpansFlags = new int[length];
   2589                 for (int i = 0; i < length; i++) {
   2590                     final SuggestionSpan suggestionSpan = suggestionSpans[i];
   2591                     suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
   2592                     suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
   2593                     suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
   2594 
   2595                     // Remove potential misspelled flags
   2596                     int suggestionSpanFlags = suggestionSpan.getFlags();
   2597                     if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) {
   2598                         suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
   2599                         suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
   2600                         suggestionSpan.setFlags(suggestionSpanFlags);
   2601                     }
   2602                 }
   2603 
   2604                 final int suggestionStart = suggestionInfo.suggestionStart;
   2605                 final int suggestionEnd = suggestionInfo.suggestionEnd;
   2606                 final String suggestion = suggestionInfo.text.subSequence(
   2607                         suggestionStart, suggestionEnd).toString();
   2608                 mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
   2609 
   2610                 // Notify source IME of the suggestion pick. Do this before swaping texts.
   2611                 if (!TextUtils.isEmpty(
   2612                         suggestionInfo.suggestionSpan.getNotificationTargetClassName())) {
   2613                     InputMethodManager imm = InputMethodManager.peekInstance();
   2614                     if (imm != null) {
   2615                         imm.notifySuggestionPicked(suggestionInfo.suggestionSpan, originalText,
   2616                                 suggestionInfo.suggestionIndex);
   2617                     }
   2618                 }
   2619 
   2620                 // Swap text content between actual text and Suggestion span
   2621                 String[] suggestions = suggestionInfo.suggestionSpan.getSuggestions();
   2622                 suggestions[suggestionInfo.suggestionIndex] = originalText;
   2623 
   2624                 // Restore previous SuggestionSpans
   2625                 final int lengthDifference = suggestion.length() - (spanEnd - spanStart);
   2626                 for (int i = 0; i < length; i++) {
   2627                     // Only spans that include the modified region make sense after replacement
   2628                     // Spans partially included in the replaced region are removed, there is no
   2629                     // way to assign them a valid range after replacement
   2630                     if (suggestionSpansStarts[i] <= spanStart &&
   2631                             suggestionSpansEnds[i] >= spanEnd) {
   2632                         mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
   2633                                 suggestionSpansEnds[i] + lengthDifference, suggestionSpansFlags[i]);
   2634                     }
   2635                 }
   2636 
   2637                 // Move cursor at the end of the replaced word
   2638                 final int newCursorPosition = spanEnd + lengthDifference;
   2639                 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
   2640             }
   2641 
   2642             hide();
   2643         }
   2644     }
   2645 
   2646     /**
   2647      * An ActionMode Callback class that is used to provide actions while in text selection mode.
   2648      *
   2649      * The default callback provides a subset of Select All, Cut, Copy and Paste actions, depending
   2650      * on which of these this TextView supports.
   2651      */
   2652     private class SelectionActionModeCallback implements ActionMode.Callback {
   2653 
   2654         @Override
   2655         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
   2656             TypedArray styledAttributes = mTextView.getContext().obtainStyledAttributes(
   2657                     com.android.internal.R.styleable.SelectionModeDrawables);
   2658 
   2659             boolean allowText = mTextView.getContext().getResources().getBoolean(
   2660                     com.android.internal.R.bool.config_allowActionMenuItemTextWithIcon);
   2661 
   2662             mode.setTitle(mTextView.getContext().getString(
   2663                     com.android.internal.R.string.textSelectionCABTitle));
   2664             mode.setSubtitle(null);
   2665             mode.setTitleOptionalHint(true);
   2666 
   2667             int selectAllIconId = 0; // No icon by default
   2668             if (!allowText) {
   2669                 // Provide an icon, text will not be displayed on smaller screens.
   2670                 selectAllIconId = styledAttributes.getResourceId(
   2671                         R.styleable.SelectionModeDrawables_actionModeSelectAllDrawable, 0);
   2672             }
   2673 
   2674             menu.add(0, TextView.ID_SELECT_ALL, 0, com.android.internal.R.string.selectAll).
   2675                     setIcon(selectAllIconId).
   2676                     setAlphabeticShortcut('a').
   2677                     setShowAsAction(
   2678                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
   2679 
   2680             if (mTextView.canCut()) {
   2681                 menu.add(0, TextView.ID_CUT, 0, com.android.internal.R.string.cut).
   2682                     setIcon(styledAttributes.getResourceId(
   2683                             R.styleable.SelectionModeDrawables_actionModeCutDrawable, 0)).
   2684                     setAlphabeticShortcut('x').
   2685                     setShowAsAction(
   2686                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
   2687             }
   2688 
   2689             if (mTextView.canCopy()) {
   2690                 menu.add(0, TextView.ID_COPY, 0, com.android.internal.R.string.copy).
   2691                     setIcon(styledAttributes.getResourceId(
   2692                             R.styleable.SelectionModeDrawables_actionModeCopyDrawable, 0)).
   2693                     setAlphabeticShortcut('c').
   2694                     setShowAsAction(
   2695                             MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
   2696             }
   2697 
   2698             if (mTextView.canPaste()) {
   2699                 menu.add(0, TextView.ID_PASTE, 0, com.android.internal.R.string.paste).
   2700                         setIcon(styledAttributes.getResourceId(
   2701                                 R.styleable.SelectionModeDrawables_actionModePasteDrawable, 0)).
   2702                         setAlphabeticShortcut('v').
   2703                         setShowAsAction(
   2704                                 MenuItem.SHOW_AS_ACTION_ALWAYS | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
   2705             }
   2706 
   2707             styledAttributes.recycle();
   2708 
   2709             if (mCustomSelectionActionModeCallback != null) {
   2710                 if (!mCustomSelectionActionModeCallback.onCreateActionMode(mode, menu)) {
   2711                     // The custom mode can choose to cancel the action mode
   2712                     return false;
   2713                 }
   2714             }
   2715 
   2716             if (menu.hasVisibleItems() || mode.getCustomView() != null) {
   2717                 getSelectionController().show();
   2718                 mTextView.setHasTransientState(true);
   2719                 return true;
   2720             } else {
   2721                 return false;
   2722             }
   2723         }
   2724 
   2725         @Override
   2726         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
   2727             if (mCustomSelectionActionModeCallback != null) {
   2728                 return mCustomSelectionActionModeCallback.onPrepareActionMode(mode, menu);
   2729             }
   2730             return true;
   2731         }
   2732 
   2733         @Override
   2734         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
   2735             if (mCustomSelectionActionModeCallback != null &&
   2736                  mCustomSelectionActionModeCallback.onActionItemClicked(mode, item)) {
   2737                 return true;
   2738             }
   2739             return mTextView.onTextContextMenuItem(item.getItemId());
   2740         }
   2741 
   2742         @Override
   2743         public void onDestroyActionMode(ActionMode mode) {
   2744             if (mCustomSelectionActionModeCallback != null) {
   2745                 mCustomSelectionActionModeCallback.onDestroyActionMode(mode);
   2746             }
   2747 
   2748             /*
   2749              * If we're ending this mode because we're detaching from a window,
   2750              * we still have selection state to preserve. Don't clear it, we'll
   2751              * bring back the selection mode when (if) we get reattached.
   2752              */
   2753             if (!mPreserveDetachedSelection) {
   2754                 Selection.setSelection((Spannable) mTextView.getText(),
   2755                         mTextView.getSelectionEnd());
   2756                 mTextView.setHasTransientState(false);
   2757             }
   2758 
   2759             if (mSelectionModifierCursorController != null) {
   2760                 mSelectionModifierCursorController.hide();
   2761             }
   2762 
   2763             mSelectionActionMode = null;
   2764         }
   2765     }
   2766 
   2767     private class ActionPopupWindow extends PinnedPopupWindow implements OnClickListener {
   2768         private static final int POPUP_TEXT_LAYOUT =
   2769                 com.android.internal.R.layout.text_edit_action_popup_text;
   2770         private TextView mPasteTextView;
   2771         private TextView mReplaceTextView;
   2772 
   2773         @Override
   2774         protected void createPopupWindow() {
   2775             mPopupWindow = new PopupWindow(mTextView.getContext(), null,
   2776                     com.android.internal.R.attr.textSelectHandleWindowStyle);
   2777             mPopupWindow.setClippingEnabled(true);
   2778         }
   2779 
   2780         @Override
   2781         protected void initContentView() {
   2782             LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
   2783             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
   2784             mContentView = linearLayout;
   2785             mContentView.setBackgroundResource(
   2786                     com.android.internal.R.drawable.text_edit_paste_window);
   2787 
   2788             LayoutInflater inflater = (LayoutInflater) mTextView.getContext().
   2789                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   2790 
   2791             LayoutParams wrapContent = new LayoutParams(
   2792                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   2793 
   2794             mPasteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
   2795             mPasteTextView.setLayoutParams(wrapContent);
   2796             mContentView.addView(mPasteTextView);
   2797             mPasteTextView.setText(com.android.internal.R.string.paste);
   2798             mPasteTextView.setOnClickListener(this);
   2799 
   2800             mReplaceTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
   2801             mReplaceTextView.setLayoutParams(wrapContent);
   2802             mContentView.addView(mReplaceTextView);
   2803             mReplaceTextView.setText(com.android.internal.R.string.replace);
   2804             mReplaceTextView.setOnClickListener(this);
   2805         }
   2806 
   2807         @Override
   2808         public void show() {
   2809             boolean canPaste = mTextView.canPaste();
   2810             boolean canSuggest = mTextView.isSuggestionsEnabled() && isCursorInsideSuggestionSpan();
   2811             mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE);
   2812             mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE);
   2813 
   2814             if (!canPaste && !canSuggest) return;
   2815 
   2816             super.show();
   2817         }
   2818 
   2819         @Override
   2820         public void onClick(View view) {
   2821             if (view == mPasteTextView && mTextView.canPaste()) {
   2822                 mTextView.onTextContextMenuItem(TextView.ID_PASTE);
   2823                 hide();
   2824             } else if (view == mReplaceTextView) {
   2825                 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
   2826                 stopSelectionActionMode();
   2827                 Selection.setSelection((Spannable) mTextView.getText(), middle);
   2828                 showSuggestions();
   2829             }
   2830         }
   2831 
   2832         @Override
   2833         protected int getTextOffset() {
   2834             return (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
   2835         }
   2836 
   2837         @Override
   2838         protected int getVerticalLocalPosition(int line) {
   2839             return mTextView.getLayout().getLineTop(line) - mContentView.getMeasuredHeight();
   2840         }
   2841 
   2842         @Override
   2843         protected int clipVertically(int positionY) {
   2844             if (positionY < 0) {
   2845                 final int offset = getTextOffset();
   2846                 final Layout layout = mTextView.getLayout();
   2847                 final int line = layout.getLineForOffset(offset);
   2848                 positionY += layout.getLineBottom(line) - layout.getLineTop(line);
   2849                 positionY += mContentView.getMeasuredHeight();
   2850 
   2851                 // Assumes insertion and selection handles share the same height
   2852                 final Drawable handle = mTextView.getResources().getDrawable(
   2853                         mTextView.mTextSelectHandleRes);
   2854                 positionY += handle.getIntrinsicHeight();
   2855             }
   2856 
   2857             return positionY;
   2858         }
   2859     }
   2860 
   2861     private abstract class HandleView extends View implements TextViewPositionListener {
   2862         protected Drawable mDrawable;
   2863         protected Drawable mDrawableLtr;
   2864         protected Drawable mDrawableRtl;
   2865         private final PopupWindow mContainer;
   2866         // Position with respect to the parent TextView
   2867         private int mPositionX, mPositionY;
   2868         private boolean mIsDragging;
   2869         // Offset from touch position to mPosition
   2870         private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
   2871         protected int mHotspotX;
   2872         // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
   2873         private float mTouchOffsetY;
   2874         // Where the touch position should be on the handle to ensure a maximum cursor visibility
   2875         private float mIdealVerticalOffset;
   2876         // Parent's (TextView) previous position in window
   2877         private int mLastParentX, mLastParentY;
   2878         // Transient action popup window for Paste and Replace actions
   2879         protected ActionPopupWindow mActionPopupWindow;
   2880         // Previous text character offset
   2881         private int mPreviousOffset = -1;
   2882         // Previous text character offset
   2883         private boolean mPositionHasChanged = true;
   2884         // Used to delay the appearance of the action popup window
   2885         private Runnable mActionPopupShower;
   2886 
   2887         public HandleView(Drawable drawableLtr, Drawable drawableRtl) {
   2888             super(mTextView.getContext());
   2889             mContainer = new PopupWindow(mTextView.getContext(), null,
   2890                     com.android.internal.R.attr.textSelectHandleWindowStyle);
   2891             mContainer.setSplitTouchEnabled(true);
   2892             mContainer.setClippingEnabled(false);
   2893             mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
   2894             mContainer.setContentView(this);
   2895 
   2896             mDrawableLtr = drawableLtr;
   2897             mDrawableRtl = drawableRtl;
   2898 
   2899             updateDrawable();
   2900 
   2901             final int handleHeight = mDrawable.getIntrinsicHeight();
   2902             mTouchOffsetY = -0.3f * handleHeight;
   2903             mIdealVerticalOffset = 0.7f * handleHeight;
   2904         }
   2905 
   2906         protected void updateDrawable() {
   2907             final int offset = getCurrentCursorOffset();
   2908             final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset);
   2909             mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
   2910             mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
   2911         }
   2912 
   2913         protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
   2914 
   2915         // Touch-up filter: number of previous positions remembered
   2916         private static final int HISTORY_SIZE = 5;
   2917         private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
   2918         private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
   2919         private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
   2920         private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
   2921         private int mPreviousOffsetIndex = 0;
   2922         private int mNumberPreviousOffsets = 0;
   2923 
   2924         private void startTouchUpFilter(int offset) {
   2925             mNumberPreviousOffsets = 0;
   2926             addPositionToTouchUpFilter(offset);
   2927         }
   2928 
   2929         private void addPositionToTouchUpFilter(int offset) {
   2930             mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
   2931             mPreviousOffsets[mPreviousOffsetIndex] = offset;
   2932             mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
   2933             mNumberPreviousOffsets++;
   2934         }
   2935 
   2936         private void filterOnTouchUp() {
   2937             final long now = SystemClock.uptimeMillis();
   2938             int i = 0;
   2939             int index = mPreviousOffsetIndex;
   2940             final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
   2941             while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
   2942                 i++;
   2943                 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
   2944             }
   2945 
   2946             if (i > 0 && i < iMax &&
   2947                     (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
   2948                 positionAtCursorOffset(mPreviousOffsets[index], false);
   2949             }
   2950         }
   2951 
   2952         public boolean offsetHasBeenChanged() {
   2953             return mNumberPreviousOffsets > 1;
   2954         }
   2955 
   2956         @Override
   2957         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   2958             setMeasuredDimension(mDrawable.getIntrinsicWidth(), mDrawable.getIntrinsicHeight());
   2959         }
   2960 
   2961         public void show() {
   2962             if (isShowing()) return;
   2963 
   2964             getPositionListener().addSubscriber(this, true /* local position may change */);
   2965 
   2966             // Make sure the offset is always considered new, even when focusing at same position
   2967             mPreviousOffset = -1;
   2968             positionAtCursorOffset(getCurrentCursorOffset(), false);
   2969 
   2970             hideActionPopupWindow();
   2971         }
   2972 
   2973         protected void dismiss() {
   2974             mIsDragging = false;
   2975             mContainer.dismiss();
   2976             onDetached();
   2977         }
   2978 
   2979         public void hide() {
   2980             dismiss();
   2981 
   2982             getPositionListener().removeSubscriber(this);
   2983         }
   2984 
   2985         void showActionPopupWindow(int delay) {
   2986             if (mActionPopupWindow == null) {
   2987                 mActionPopupWindow = new ActionPopupWindow();
   2988             }
   2989             if (mActionPopupShower == null) {
   2990                 mActionPopupShower = new Runnable() {
   2991                     public void run() {
   2992                         mActionPopupWindow.show();
   2993                     }
   2994                 };
   2995             } else {
   2996                 mTextView.removeCallbacks(mActionPopupShower);
   2997             }
   2998             mTextView.postDelayed(mActionPopupShower, delay);
   2999         }
   3000 
   3001         protected void hideActionPopupWindow() {
   3002             if (mActionPopupShower != null) {
   3003                 mTextView.removeCallbacks(mActionPopupShower);
   3004             }
   3005             if (mActionPopupWindow != null) {
   3006                 mActionPopupWindow.hide();
   3007             }
   3008         }
   3009 
   3010         public boolean isShowing() {
   3011             return mContainer.isShowing();
   3012         }
   3013 
   3014         private boolean isVisible() {
   3015             // Always show a dragging handle.
   3016             if (mIsDragging) {
   3017                 return true;
   3018             }
   3019 
   3020             if (mTextView.isInBatchEditMode()) {
   3021                 return false;
   3022             }
   3023 
   3024             return isPositionVisible(mPositionX + mHotspotX, mPositionY);
   3025         }
   3026 
   3027         public abstract int getCurrentCursorOffset();
   3028 
   3029         protected abstract void updateSelection(int offset);
   3030 
   3031         public abstract void updatePosition(float x, float y);
   3032 
   3033         protected void positionAtCursorOffset(int offset, boolean parentScrolled) {
   3034             // A HandleView relies on the layout, which may be nulled by external methods
   3035             Layout layout = mTextView.getLayout();
   3036             if (layout == null) {
   3037                 // Will update controllers' state, hiding them and stopping selection mode if needed
   3038                 prepareCursorControllers();
   3039                 return;
   3040             }
   3041 
   3042             boolean offsetChanged = offset != mPreviousOffset;
   3043             if (offsetChanged || parentScrolled) {
   3044                 if (offsetChanged) {
   3045                     updateSelection(offset);
   3046                     addPositionToTouchUpFilter(offset);
   3047                 }
   3048                 final int line = layout.getLineForOffset(offset);
   3049 
   3050                 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX);
   3051                 mPositionY = layout.getLineBottom(line);
   3052 
   3053                 // Take TextView's padding and scroll into account.
   3054                 mPositionX += mTextView.viewportToContentHorizontalOffset();
   3055                 mPositionY += mTextView.viewportToContentVerticalOffset();
   3056 
   3057                 mPreviousOffset = offset;
   3058                 mPositionHasChanged = true;
   3059             }
   3060         }
   3061 
   3062         public void updatePosition(int parentPositionX, int parentPositionY,
   3063                 boolean parentPositionChanged, boolean parentScrolled) {
   3064             positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
   3065             if (parentPositionChanged || mPositionHasChanged) {
   3066                 if (mIsDragging) {
   3067                     // Update touchToWindow offset in case of parent scrolling while dragging
   3068                     if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
   3069                         mTouchToWindowOffsetX += parentPositionX - mLastParentX;
   3070                         mTouchToWindowOffsetY += parentPositionY - mLastParentY;
   3071                         mLastParentX = parentPositionX;
   3072                         mLastParentY = parentPositionY;
   3073                     }
   3074 
   3075                     onHandleMoved();
   3076                 }
   3077 
   3078                 if (isVisible()) {
   3079                     final int positionX = parentPositionX + mPositionX;
   3080                     final int positionY = parentPositionY + mPositionY;
   3081                     if (isShowing()) {
   3082                         mContainer.update(positionX, positionY, -1, -1);
   3083                     } else {
   3084                         mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY,
   3085                                 positionX, positionY);
   3086                     }
   3087                 } else {
   3088                     if (isShowing()) {
   3089                         dismiss();
   3090                     }
   3091                 }
   3092 
   3093                 mPositionHasChanged = false;
   3094             }
   3095         }
   3096 
   3097         @Override
   3098         protected void onDraw(Canvas c) {
   3099             mDrawable.setBounds(0, 0, mRight - mLeft, mBottom - mTop);
   3100             mDrawable.draw(c);
   3101         }
   3102 
   3103         @Override
   3104         public boolean onTouchEvent(MotionEvent ev) {
   3105             switch (ev.getActionMasked()) {
   3106                 case MotionEvent.ACTION_DOWN: {
   3107                     startTouchUpFilter(getCurrentCursorOffset());
   3108                     mTouchToWindowOffsetX = ev.getRawX() - mPositionX;
   3109                     mTouchToWindowOffsetY = ev.getRawY() - mPositionY;
   3110 
   3111                     final PositionListener positionListener = getPositionListener();
   3112                     mLastParentX = positionListener.getPositionX();
   3113                     mLastParentY = positionListener.getPositionY();
   3114                     mIsDragging = true;
   3115                     break;
   3116                 }
   3117 
   3118                 case MotionEvent.ACTION_MOVE: {
   3119                     final float rawX = ev.getRawX();
   3120                     final float rawY = ev.getRawY();
   3121 
   3122                     // Vertical hysteresis: vertical down movement tends to snap to ideal offset
   3123                     final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
   3124                     final float currentVerticalOffset = rawY - mPositionY - mLastParentY;
   3125                     float newVerticalOffset;
   3126                     if (previousVerticalOffset < mIdealVerticalOffset) {
   3127                         newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
   3128                         newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
   3129                     } else {
   3130                         newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
   3131                         newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
   3132                     }
   3133                     mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
   3134 
   3135                     final float newPosX = rawX - mTouchToWindowOffsetX + mHotspotX;
   3136                     final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY;
   3137 
   3138                     updatePosition(newPosX, newPosY);
   3139                     break;
   3140                 }
   3141 
   3142                 case MotionEvent.ACTION_UP:
   3143                     filterOnTouchUp();
   3144                     mIsDragging = false;
   3145                     break;
   3146 
   3147                 case MotionEvent.ACTION_CANCEL:
   3148                     mIsDragging = false;
   3149                     break;
   3150             }
   3151             return true;
   3152         }
   3153 
   3154         public boolean isDragging() {
   3155             return mIsDragging;
   3156         }
   3157 
   3158         void onHandleMoved() {
   3159             hideActionPopupWindow();
   3160         }
   3161 
   3162         public void onDetached() {
   3163             hideActionPopupWindow();
   3164         }
   3165     }
   3166 
   3167     private class InsertionHandleView extends HandleView {
   3168         private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
   3169         private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
   3170 
   3171         // Used to detect taps on the insertion handle, which will affect the ActionPopupWindow
   3172         private float mDownPositionX, mDownPositionY;
   3173         private Runnable mHider;
   3174 
   3175         public InsertionHandleView(Drawable drawable) {
   3176             super(drawable, drawable);
   3177         }
   3178 
   3179         @Override
   3180         public void show() {
   3181             super.show();
   3182 
   3183             final long durationSinceCutOrCopy =
   3184                     SystemClock.uptimeMillis() - TextView.LAST_CUT_OR_COPY_TIME;
   3185             if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) {
   3186                 showActionPopupWindow(0);
   3187             }
   3188 
   3189             hideAfterDelay();
   3190         }
   3191 
   3192         public void showWithActionPopup() {
   3193             show();
   3194             showActionPopupWindow(0);
   3195         }
   3196 
   3197         private void hideAfterDelay() {
   3198             if (mHider == null) {
   3199                 mHider = new Runnable() {
   3200                     public void run() {
   3201                         hide();
   3202                     }
   3203                 };
   3204             } else {
   3205                 removeHiderCallback();
   3206             }
   3207             mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
   3208         }
   3209 
   3210         private void removeHiderCallback() {
   3211             if (mHider != null) {
   3212                 mTextView.removeCallbacks(mHider);
   3213             }
   3214         }
   3215 
   3216         @Override
   3217         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
   3218             return drawable.getIntrinsicWidth() / 2;
   3219         }
   3220 
   3221         @Override
   3222         public boolean onTouchEvent(MotionEvent ev) {
   3223             final boolean result = super.onTouchEvent(ev);
   3224 
   3225             switch (ev.getActionMasked()) {
   3226                 case MotionEvent.ACTION_DOWN:
   3227                     mDownPositionX = ev.getRawX();
   3228                     mDownPositionY = ev.getRawY();
   3229                     break;
   3230 
   3231                 case MotionEvent.ACTION_UP:
   3232                     if (!offsetHasBeenChanged()) {
   3233                         final float deltaX = mDownPositionX - ev.getRawX();
   3234                         final float deltaY = mDownPositionY - ev.getRawY();
   3235                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   3236 
   3237                         final ViewConfiguration viewConfiguration = ViewConfiguration.get(
   3238                                 mTextView.getContext());
   3239                         final int touchSlop = viewConfiguration.getScaledTouchSlop();
   3240 
   3241                         if (distanceSquared < touchSlop * touchSlop) {
   3242                             if (mActionPopupWindow != null && mActionPopupWindow.isShowing()) {
   3243                                 // Tapping on the handle dismisses the displayed action popup
   3244                                 mActionPopupWindow.hide();
   3245                             } else {
   3246                                 showWithActionPopup();
   3247                             }
   3248                         }
   3249                     }
   3250                     hideAfterDelay();
   3251                     break;
   3252 
   3253                 case MotionEvent.ACTION_CANCEL:
   3254                     hideAfterDelay();
   3255                     break;
   3256 
   3257                 default:
   3258                     break;
   3259             }
   3260 
   3261             return result;
   3262         }
   3263 
   3264         @Override
   3265         public int getCurrentCursorOffset() {
   3266             return mTextView.getSelectionStart();
   3267         }
   3268 
   3269         @Override
   3270         public void updateSelection(int offset) {
   3271             Selection.setSelection((Spannable) mTextView.getText(), offset);
   3272         }
   3273 
   3274         @Override
   3275         public void updatePosition(float x, float y) {
   3276             positionAtCursorOffset(mTextView.getOffsetForPosition(x, y), false);
   3277         }
   3278 
   3279         @Override
   3280         void onHandleMoved() {
   3281             super.onHandleMoved();
   3282             removeHiderCallback();
   3283         }
   3284 
   3285         @Override
   3286         public void onDetached() {
   3287             super.onDetached();
   3288             removeHiderCallback();
   3289         }
   3290     }
   3291 
   3292     private class SelectionStartHandleView extends HandleView {
   3293 
   3294         public SelectionStartHandleView(Drawable drawableLtr, Drawable drawableRtl) {
   3295             super(drawableLtr, drawableRtl);
   3296         }
   3297 
   3298         @Override
   3299         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
   3300             if (isRtlRun) {
   3301                 return drawable.getIntrinsicWidth() / 4;
   3302             } else {
   3303                 return (drawable.getIntrinsicWidth() * 3) / 4;
   3304             }
   3305         }
   3306 
   3307         @Override
   3308         public int getCurrentCursorOffset() {
   3309             return mTextView.getSelectionStart();
   3310         }
   3311 
   3312         @Override
   3313         public void updateSelection(int offset) {
   3314             Selection.setSelection((Spannable) mTextView.getText(), offset,
   3315                     mTextView.getSelectionEnd());
   3316             updateDrawable();
   3317         }
   3318 
   3319         @Override
   3320         public void updatePosition(float x, float y) {
   3321             int offset = mTextView.getOffsetForPosition(x, y);
   3322 
   3323             // Handles can not cross and selection is at least one character
   3324             final int selectionEnd = mTextView.getSelectionEnd();
   3325             if (offset >= selectionEnd) offset = Math.max(0, selectionEnd - 1);
   3326 
   3327             positionAtCursorOffset(offset, false);
   3328         }
   3329 
   3330         public ActionPopupWindow getActionPopupWindow() {
   3331             return mActionPopupWindow;
   3332         }
   3333     }
   3334 
   3335     private class SelectionEndHandleView extends HandleView {
   3336 
   3337         public SelectionEndHandleView(Drawable drawableLtr, Drawable drawableRtl) {
   3338             super(drawableLtr, drawableRtl);
   3339         }
   3340 
   3341         @Override
   3342         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
   3343             if (isRtlRun) {
   3344                 return (drawable.getIntrinsicWidth() * 3) / 4;
   3345             } else {
   3346                 return drawable.getIntrinsicWidth() / 4;
   3347             }
   3348         }
   3349 
   3350         @Override
   3351         public int getCurrentCursorOffset() {
   3352             return mTextView.getSelectionEnd();
   3353         }
   3354 
   3355         @Override
   3356         public void updateSelection(int offset) {
   3357             Selection.setSelection((Spannable) mTextView.getText(),
   3358                     mTextView.getSelectionStart(), offset);
   3359             updateDrawable();
   3360         }
   3361 
   3362         @Override
   3363         public void updatePosition(float x, float y) {
   3364             int offset = mTextView.getOffsetForPosition(x, y);
   3365 
   3366             // Handles can not cross and selection is at least one character
   3367             final int selectionStart = mTextView.getSelectionStart();
   3368             if (offset <= selectionStart) {
   3369                 offset = Math.min(selectionStart + 1, mTextView.getText().length());
   3370             }
   3371 
   3372             positionAtCursorOffset(offset, false);
   3373         }
   3374 
   3375         public void setActionPopupWindow(ActionPopupWindow actionPopupWindow) {
   3376             mActionPopupWindow = actionPopupWindow;
   3377         }
   3378     }
   3379 
   3380     /**
   3381      * A CursorController instance can be used to control a cursor in the text.
   3382      */
   3383     private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
   3384         /**
   3385          * Makes the cursor controller visible on screen.
   3386          * See also {@link #hide()}.
   3387          */
   3388         public void show();
   3389 
   3390         /**
   3391          * Hide the cursor controller from screen.
   3392          * See also {@link #show()}.
   3393          */
   3394         public void hide();
   3395 
   3396         /**
   3397          * Called when the view is detached from window. Perform house keeping task, such as
   3398          * stopping Runnable thread that would otherwise keep a reference on the context, thus
   3399          * preventing the activity from being recycled.
   3400          */
   3401         public void onDetached();
   3402     }
   3403 
   3404     private class InsertionPointCursorController implements CursorController {
   3405         private InsertionHandleView mHandle;
   3406 
   3407         public void show() {
   3408             getHandle().show();
   3409         }
   3410 
   3411         public void showWithActionPopup() {
   3412             getHandle().showWithActionPopup();
   3413         }
   3414 
   3415         public void hide() {
   3416             if (mHandle != null) {
   3417                 mHandle.hide();
   3418             }
   3419         }
   3420 
   3421         public void onTouchModeChanged(boolean isInTouchMode) {
   3422             if (!isInTouchMode) {
   3423                 hide();
   3424             }
   3425         }
   3426 
   3427         private InsertionHandleView getHandle() {
   3428             if (mSelectHandleCenter == null) {
   3429                 mSelectHandleCenter = mTextView.getResources().getDrawable(
   3430                         mTextView.mTextSelectHandleRes);
   3431             }
   3432             if (mHandle == null) {
   3433                 mHandle = new InsertionHandleView(mSelectHandleCenter);
   3434             }
   3435             return mHandle;
   3436         }
   3437 
   3438         @Override
   3439         public void onDetached() {
   3440             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   3441             observer.removeOnTouchModeChangeListener(this);
   3442 
   3443             if (mHandle != null) mHandle.onDetached();
   3444         }
   3445     }
   3446 
   3447     class SelectionModifierCursorController implements CursorController {
   3448         private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds
   3449         // The cursor controller handles, lazily created when shown.
   3450         private SelectionStartHandleView mStartHandle;
   3451         private SelectionEndHandleView mEndHandle;
   3452         // The offsets of that last touch down event. Remembered to start selection there.
   3453         private int mMinTouchOffset, mMaxTouchOffset;
   3454 
   3455         // Double tap detection
   3456         private long mPreviousTapUpTime = 0;
   3457         private float mDownPositionX, mDownPositionY;
   3458         private boolean mGestureStayedInTapRegion;
   3459 
   3460         SelectionModifierCursorController() {
   3461             resetTouchOffsets();
   3462         }
   3463 
   3464         public void show() {
   3465             if (mTextView.isInBatchEditMode()) {
   3466                 return;
   3467             }
   3468             initDrawables();
   3469             initHandles();
   3470             hideInsertionPointCursorController();
   3471         }
   3472 
   3473         private void initDrawables() {
   3474             if (mSelectHandleLeft == null) {
   3475                 mSelectHandleLeft = mTextView.getContext().getResources().getDrawable(
   3476                         mTextView.mTextSelectHandleLeftRes);
   3477             }
   3478             if (mSelectHandleRight == null) {
   3479                 mSelectHandleRight = mTextView.getContext().getResources().getDrawable(
   3480                         mTextView.mTextSelectHandleRightRes);
   3481             }
   3482         }
   3483 
   3484         private void initHandles() {
   3485             // Lazy object creation has to be done before updatePosition() is called.
   3486             if (mStartHandle == null) {
   3487                 mStartHandle = new SelectionStartHandleView(mSelectHandleLeft, mSelectHandleRight);
   3488             }
   3489             if (mEndHandle == null) {
   3490                 mEndHandle = new SelectionEndHandleView(mSelectHandleRight, mSelectHandleLeft);
   3491             }
   3492 
   3493             mStartHandle.show();
   3494             mEndHandle.show();
   3495 
   3496             // Make sure both left and right handles share the same ActionPopupWindow (so that
   3497             // moving any of the handles hides the action popup).
   3498             mStartHandle.showActionPopupWindow(DELAY_BEFORE_REPLACE_ACTION);
   3499             mEndHandle.setActionPopupWindow(mStartHandle.getActionPopupWindow());
   3500 
   3501             hideInsertionPointCursorController();
   3502         }
   3503 
   3504         public void hide() {
   3505             if (mStartHandle != null) mStartHandle.hide();
   3506             if (mEndHandle != null) mEndHandle.hide();
   3507         }
   3508 
   3509         public void onTouchEvent(MotionEvent event) {
   3510             // This is done even when the View does not have focus, so that long presses can start
   3511             // selection and tap can move cursor from this tap position.
   3512             switch (event.getActionMasked()) {
   3513                 case MotionEvent.ACTION_DOWN:
   3514                     final float x = event.getX();
   3515                     final float y = event.getY();
   3516 
   3517                     // Remember finger down position, to be able to start selection from there
   3518                     mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(x, y);
   3519 
   3520                     // Double tap detection
   3521                     if (mGestureStayedInTapRegion) {
   3522                         long duration = SystemClock.uptimeMillis() - mPreviousTapUpTime;
   3523                         if (duration <= ViewConfiguration.getDoubleTapTimeout()) {
   3524                             final float deltaX = x - mDownPositionX;
   3525                             final float deltaY = y - mDownPositionY;
   3526                             final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   3527 
   3528                             ViewConfiguration viewConfiguration = ViewConfiguration.get(
   3529                                     mTextView.getContext());
   3530                             int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
   3531                             boolean stayedInArea = distanceSquared < doubleTapSlop * doubleTapSlop;
   3532 
   3533                             if (stayedInArea && isPositionOnText(x, y)) {
   3534                                 startSelectionActionMode();
   3535                                 mDiscardNextActionUp = true;
   3536                             }
   3537                         }
   3538                     }
   3539 
   3540                     mDownPositionX = x;
   3541                     mDownPositionY = y;
   3542                     mGestureStayedInTapRegion = true;
   3543                     break;
   3544 
   3545                 case MotionEvent.ACTION_POINTER_DOWN:
   3546                 case MotionEvent.ACTION_POINTER_UP:
   3547                     // Handle multi-point gestures. Keep min and max offset positions.
   3548                     // Only activated for devices that correctly handle multi-touch.
   3549                     if (mTextView.getContext().getPackageManager().hasSystemFeature(
   3550                             PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
   3551                         updateMinAndMaxOffsets(event);
   3552                     }
   3553                     break;
   3554 
   3555                 case MotionEvent.ACTION_MOVE:
   3556                     if (mGestureStayedInTapRegion) {
   3557                         final float deltaX = event.getX() - mDownPositionX;
   3558                         final float deltaY = event.getY() - mDownPositionY;
   3559                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   3560 
   3561                         final ViewConfiguration viewConfiguration = ViewConfiguration.get(
   3562                                 mTextView.getContext());
   3563                         int doubleTapTouchSlop = viewConfiguration.getScaledDoubleTapTouchSlop();
   3564 
   3565                         if (distanceSquared > doubleTapTouchSlop * doubleTapTouchSlop) {
   3566                             mGestureStayedInTapRegion = false;
   3567                         }
   3568                     }
   3569                     break;
   3570 
   3571                 case MotionEvent.ACTION_UP:
   3572                     mPreviousTapUpTime = SystemClock.uptimeMillis();
   3573                     break;
   3574             }
   3575         }
   3576 
   3577         /**
   3578          * @param event
   3579          */
   3580         private void updateMinAndMaxOffsets(MotionEvent event) {
   3581             int pointerCount = event.getPointerCount();
   3582             for (int index = 0; index < pointerCount; index++) {
   3583                 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
   3584                 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
   3585                 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
   3586             }
   3587         }
   3588 
   3589         public int getMinTouchOffset() {
   3590             return mMinTouchOffset;
   3591         }
   3592 
   3593         public int getMaxTouchOffset() {
   3594             return mMaxTouchOffset;
   3595         }
   3596 
   3597         public void resetTouchOffsets() {
   3598             mMinTouchOffset = mMaxTouchOffset = -1;
   3599         }
   3600 
   3601         /**
   3602          * @return true iff this controller is currently used to move the selection start.
   3603          */
   3604         public boolean isSelectionStartDragged() {
   3605             return mStartHandle != null && mStartHandle.isDragging();
   3606         }
   3607 
   3608         public void onTouchModeChanged(boolean isInTouchMode) {
   3609             if (!isInTouchMode) {
   3610                 hide();
   3611             }
   3612         }
   3613 
   3614         @Override
   3615         public void onDetached() {
   3616             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   3617             observer.removeOnTouchModeChangeListener(this);
   3618 
   3619             if (mStartHandle != null) mStartHandle.onDetached();
   3620             if (mEndHandle != null) mEndHandle.onDetached();
   3621         }
   3622     }
   3623 
   3624     private class CorrectionHighlighter {
   3625         private final Path mPath = new Path();
   3626         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   3627         private int mStart, mEnd;
   3628         private long mFadingStartTime;
   3629         private RectF mTempRectF;
   3630         private final static int FADE_OUT_DURATION = 400;
   3631 
   3632         public CorrectionHighlighter() {
   3633             mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
   3634                     applicationScale);
   3635             mPaint.setStyle(Paint.Style.FILL);
   3636         }
   3637 
   3638         public void highlight(CorrectionInfo info) {
   3639             mStart = info.getOffset();
   3640             mEnd = mStart + info.getNewText().length();
   3641             mFadingStartTime = SystemClock.uptimeMillis();
   3642 
   3643             if (mStart < 0 || mEnd < 0) {
   3644                 stopAnimation();
   3645             }
   3646         }
   3647 
   3648         public void draw(Canvas canvas, int cursorOffsetVertical) {
   3649             if (updatePath() && updatePaint()) {
   3650                 if (cursorOffsetVertical != 0) {
   3651                     canvas.translate(0, cursorOffsetVertical);
   3652                 }
   3653 
   3654                 canvas.drawPath(mPath, mPaint);
   3655 
   3656                 if (cursorOffsetVertical != 0) {
   3657                     canvas.translate(0, -cursorOffsetVertical);
   3658                 }
   3659                 invalidate(true); // TODO invalidate cursor region only
   3660             } else {
   3661                 stopAnimation();
   3662                 invalidate(false); // TODO invalidate cursor region only
   3663             }
   3664         }
   3665 
   3666         private boolean updatePaint() {
   3667             final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
   3668             if (duration > FADE_OUT_DURATION) return false;
   3669 
   3670             final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
   3671             final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
   3672             final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
   3673                     ((int) (highlightColorAlpha * coef) << 24);
   3674             mPaint.setColor(color);
   3675             return true;
   3676         }
   3677 
   3678         private boolean updatePath() {
   3679             final Layout layout = mTextView.getLayout();
   3680             if (layout == null) return false;
   3681 
   3682             // Update in case text is edited while the animation is run
   3683             final int length = mTextView.getText().length();
   3684             int start = Math.min(length, mStart);
   3685             int end = Math.min(length, mEnd);
   3686 
   3687             mPath.reset();
   3688             layout.getSelectionPath(start, end, mPath);
   3689             return true;
   3690         }
   3691 
   3692         private void invalidate(boolean delayed) {
   3693             if (mTextView.getLayout() == null) return;
   3694 
   3695             if (mTempRectF == null) mTempRectF = new RectF();
   3696             mPath.computeBounds(mTempRectF, false);
   3697 
   3698             int left = mTextView.getCompoundPaddingLeft();
   3699             int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
   3700 
   3701             if (delayed) {
   3702                 mTextView.postInvalidateOnAnimation(
   3703                         left + (int) mTempRectF.left, top + (int) mTempRectF.top,
   3704                         left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
   3705             } else {
   3706                 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
   3707                         (int) mTempRectF.right, (int) mTempRectF.bottom);
   3708             }
   3709         }
   3710 
   3711         private void stopAnimation() {
   3712             Editor.this.mCorrectionHighlighter = null;
   3713         }
   3714     }
   3715 
   3716     private static class ErrorPopup extends PopupWindow {
   3717         private boolean mAbove = false;
   3718         private final TextView mView;
   3719         private int mPopupInlineErrorBackgroundId = 0;
   3720         private int mPopupInlineErrorAboveBackgroundId = 0;
   3721 
   3722         ErrorPopup(TextView v, int width, int height) {
   3723             super(v, width, height);
   3724             mView = v;
   3725             // Make sure the TextView has a background set as it will be used the first time it is
   3726             // shown and positionned. Initialized with below background, which should have
   3727             // dimensions identical to the above version for this to work (and is more likely).
   3728             mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
   3729                     com.android.internal.R.styleable.Theme_errorMessageBackground);
   3730             mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
   3731         }
   3732 
   3733         void fixDirection(boolean above) {
   3734             mAbove = above;
   3735 
   3736             if (above) {
   3737                 mPopupInlineErrorAboveBackgroundId =
   3738                     getResourceId(mPopupInlineErrorAboveBackgroundId,
   3739                             com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
   3740             } else {
   3741                 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
   3742                         com.android.internal.R.styleable.Theme_errorMessageBackground);
   3743             }
   3744 
   3745             mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
   3746                 mPopupInlineErrorBackgroundId);
   3747         }
   3748 
   3749         private int getResourceId(int currentId, int index) {
   3750             if (currentId == 0) {
   3751                 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
   3752                         R.styleable.Theme);
   3753                 currentId = styledAttributes.getResourceId(index, 0);
   3754                 styledAttributes.recycle();
   3755             }
   3756             return currentId;
   3757         }
   3758 
   3759         @Override
   3760         public void update(int x, int y, int w, int h, boolean force) {
   3761             super.update(x, y, w, h, force);
   3762 
   3763             boolean above = isAboveAnchor();
   3764             if (above != mAbove) {
   3765                 fixDirection(above);
   3766             }
   3767         }
   3768     }
   3769 
   3770     static class InputContentType {
   3771         int imeOptions = EditorInfo.IME_NULL;
   3772         String privateImeOptions;
   3773         CharSequence imeActionLabel;
   3774         int imeActionId;
   3775         Bundle extras;
   3776         OnEditorActionListener onEditorActionListener;
   3777         boolean enterDown;
   3778     }
   3779 
   3780     static class InputMethodState {
   3781         Rect mCursorRectInWindow = new Rect();
   3782         RectF mTmpRectF = new RectF();
   3783         float[] mTmpOffset = new float[2];
   3784         ExtractedTextRequest mExtractedTextRequest;
   3785         final ExtractedText mExtractedText = new ExtractedText();
   3786         int mBatchEditNesting;
   3787         boolean mCursorChanged;
   3788         boolean mSelectionModeChanged;
   3789         boolean mContentChanged;
   3790         int mChangedStart, mChangedEnd, mChangedDelta;
   3791     }
   3792 }
   3793