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