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