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 android.R;
     20 import android.annotation.IntDef;
     21 import android.annotation.NonNull;
     22 import android.annotation.Nullable;
     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.UndoManager;
     30 import android.content.UndoOperation;
     31 import android.content.UndoOwner;
     32 import android.content.pm.PackageManager;
     33 import android.content.pm.ResolveInfo;
     34 import android.content.res.TypedArray;
     35 import android.graphics.Canvas;
     36 import android.graphics.Color;
     37 import android.graphics.Matrix;
     38 import android.graphics.Paint;
     39 import android.graphics.Path;
     40 import android.graphics.Rect;
     41 import android.graphics.RectF;
     42 import android.graphics.drawable.ColorDrawable;
     43 import android.graphics.drawable.Drawable;
     44 import android.os.Bundle;
     45 import android.os.LocaleList;
     46 import android.os.Parcel;
     47 import android.os.Parcelable;
     48 import android.os.ParcelableParcel;
     49 import android.os.SystemClock;
     50 import android.provider.Settings;
     51 import android.text.DynamicLayout;
     52 import android.text.Editable;
     53 import android.text.InputFilter;
     54 import android.text.InputType;
     55 import android.text.Layout;
     56 import android.text.ParcelableSpan;
     57 import android.text.Selection;
     58 import android.text.SpanWatcher;
     59 import android.text.Spannable;
     60 import android.text.SpannableStringBuilder;
     61 import android.text.Spanned;
     62 import android.text.StaticLayout;
     63 import android.text.TextUtils;
     64 import android.text.method.KeyListener;
     65 import android.text.method.MetaKeyKeyListener;
     66 import android.text.method.MovementMethod;
     67 import android.text.method.WordIterator;
     68 import android.text.style.EasyEditSpan;
     69 import android.text.style.SuggestionRangeSpan;
     70 import android.text.style.SuggestionSpan;
     71 import android.text.style.TextAppearanceSpan;
     72 import android.text.style.URLSpan;
     73 import android.util.DisplayMetrics;
     74 import android.util.Log;
     75 import android.util.SparseArray;
     76 import android.view.ActionMode;
     77 import android.view.ActionMode.Callback;
     78 import android.view.ContextMenu;
     79 import android.view.ContextThemeWrapper;
     80 import android.view.DisplayListCanvas;
     81 import android.view.DragAndDropPermissions;
     82 import android.view.DragEvent;
     83 import android.view.Gravity;
     84 import android.view.InputDevice;
     85 import android.view.LayoutInflater;
     86 import android.view.Menu;
     87 import android.view.MenuItem;
     88 import android.view.MotionEvent;
     89 import android.view.RenderNode;
     90 import android.view.SubMenu;
     91 import android.view.View;
     92 import android.view.View.DragShadowBuilder;
     93 import android.view.View.OnClickListener;
     94 import android.view.ViewConfiguration;
     95 import android.view.ViewGroup;
     96 import android.view.ViewGroup.LayoutParams;
     97 import android.view.ViewParent;
     98 import android.view.ViewTreeObserver;
     99 import android.view.WindowManager;
    100 import android.view.accessibility.AccessibilityNodeInfo;
    101 import android.view.inputmethod.CorrectionInfo;
    102 import android.view.inputmethod.CursorAnchorInfo;
    103 import android.view.inputmethod.EditorInfo;
    104 import android.view.inputmethod.ExtractedText;
    105 import android.view.inputmethod.ExtractedTextRequest;
    106 import android.view.inputmethod.InputConnection;
    107 import android.view.inputmethod.InputMethodManager;
    108 import android.widget.AdapterView.OnItemClickListener;
    109 import android.widget.TextView.Drawables;
    110 import android.widget.TextView.OnEditorActionListener;
    111 
    112 import com.android.internal.annotations.VisibleForTesting;
    113 import com.android.internal.util.ArrayUtils;
    114 import com.android.internal.util.GrowingArrayUtils;
    115 import com.android.internal.util.Preconditions;
    116 import com.android.internal.widget.EditableInputConnection;
    117 
    118 import java.lang.annotation.Retention;
    119 import java.lang.annotation.RetentionPolicy;
    120 import java.text.BreakIterator;
    121 import java.util.Arrays;
    122 import java.util.Comparator;
    123 import java.util.HashMap;
    124 import java.util.List;
    125 
    126 
    127 /**
    128  * Helper class used by TextView to handle editable text views.
    129  *
    130  * @hide
    131  */
    132 public class Editor {
    133     private static final String TAG = "Editor";
    134     private static final boolean DEBUG_UNDO = false;
    135 
    136     static final int BLINK = 500;
    137     private static final float[] TEMP_POSITION = new float[2];
    138     private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20;
    139     private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f;
    140     private static final int UNSET_X_VALUE = -1;
    141     private static final int UNSET_LINE = -1;
    142     // Tag used when the Editor maintains its own separate UndoManager.
    143     private static final String UNDO_OWNER_TAG = "Editor";
    144 
    145     // Ordering constants used to place the Action Mode or context menu items in their menu.
    146     private static final int MENU_ITEM_ORDER_UNDO = 1;
    147     private static final int MENU_ITEM_ORDER_REDO = 2;
    148     private static final int MENU_ITEM_ORDER_CUT = 3;
    149     private static final int MENU_ITEM_ORDER_COPY = 4;
    150     private static final int MENU_ITEM_ORDER_PASTE = 5;
    151     private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 6;
    152     private static final int MENU_ITEM_ORDER_SHARE = 7;
    153     private static final int MENU_ITEM_ORDER_SELECT_ALL = 8;
    154     private static final int MENU_ITEM_ORDER_REPLACE = 9;
    155     private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 10;
    156 
    157     // Each Editor manages its own undo stack.
    158     private final UndoManager mUndoManager = new UndoManager();
    159     private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
    160     final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this);
    161     boolean mAllowUndo = true;
    162 
    163     // Cursor Controllers.
    164     InsertionPointCursorController mInsertionPointCursorController;
    165     SelectionModifierCursorController mSelectionModifierCursorController;
    166     // Action mode used when text is selected or when actions on an insertion cursor are triggered.
    167     ActionMode mTextActionMode;
    168     boolean mInsertionControllerEnabled;
    169     boolean mSelectionControllerEnabled;
    170 
    171     // Used to highlight a word when it is corrected by the IME
    172     CorrectionHighlighter mCorrectionHighlighter;
    173 
    174     InputContentType mInputContentType;
    175     InputMethodState mInputMethodState;
    176 
    177     private static class TextRenderNode {
    178         RenderNode renderNode;
    179         boolean isDirty;
    180         public TextRenderNode(String name) {
    181             isDirty = true;
    182             renderNode = RenderNode.create(name, null);
    183         }
    184         boolean needsRecord() { return isDirty || !renderNode.isValid(); }
    185     }
    186     TextRenderNode[] mTextRenderNodes;
    187 
    188     boolean mFrozenWithFocus;
    189     boolean mSelectionMoved;
    190     boolean mTouchFocusSelected;
    191 
    192     KeyListener mKeyListener;
    193     int mInputType = EditorInfo.TYPE_NULL;
    194 
    195     boolean mDiscardNextActionUp;
    196     boolean mIgnoreActionUpEvent;
    197     private boolean mIgnoreNextMouseActionUpOrDown;
    198 
    199     long mShowCursor;
    200     Blink mBlink;
    201 
    202     boolean mCursorVisible = true;
    203     boolean mSelectAllOnFocus;
    204     boolean mTextIsSelectable;
    205 
    206     CharSequence mError;
    207     boolean mErrorWasChanged;
    208     ErrorPopup mErrorPopup;
    209 
    210     /**
    211      * This flag is set if the TextView tries to display an error before it
    212      * is attached to the window (so its position is still unknown).
    213      * It causes the error to be shown later, when onAttachedToWindow()
    214      * is called.
    215      */
    216     boolean mShowErrorAfterAttach;
    217 
    218     boolean mInBatchEditControllers;
    219     boolean mShowSoftInputOnFocus = true;
    220     private boolean mPreserveSelection;
    221     private boolean mRestartActionModeOnNextRefresh;
    222 
    223     boolean mIsBeingLongClicked;
    224 
    225     SuggestionsPopupWindow mSuggestionsPopupWindow;
    226     SuggestionRangeSpan mSuggestionRangeSpan;
    227     Runnable mShowSuggestionRunnable;
    228 
    229     final Drawable[] mCursorDrawable = new Drawable[2];
    230     int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split)
    231 
    232     private Drawable mSelectHandleLeft;
    233     private Drawable mSelectHandleRight;
    234     private Drawable mSelectHandleCenter;
    235 
    236     // Global listener that detects changes in the global position of the TextView
    237     private PositionListener mPositionListener;
    238 
    239     float mLastDownPositionX, mLastDownPositionY;
    240     private float mContextMenuAnchorX, mContextMenuAnchorY;
    241     Callback mCustomSelectionActionModeCallback;
    242     Callback mCustomInsertionActionModeCallback;
    243 
    244     // Set when this TextView gained focus with some text selected. Will start selection mode.
    245     boolean mCreatedWithASelection;
    246 
    247     // Indicates the current tap state (first tap, double tap, or triple click).
    248     private int mTapState = TAP_STATE_INITIAL;
    249     private long mLastTouchUpTime = 0;
    250     private static final int TAP_STATE_INITIAL = 0;
    251     private static final int TAP_STATE_FIRST_TAP = 1;
    252     private static final int TAP_STATE_DOUBLE_TAP = 2;
    253     // Only for mouse input.
    254     private static final int TAP_STATE_TRIPLE_CLICK = 3;
    255 
    256     // The button state as of the last time #onTouchEvent is called.
    257     private int mLastButtonState;
    258 
    259     private Runnable mInsertionActionModeRunnable;
    260 
    261     // The span controller helps monitoring the changes to which the Editor needs to react:
    262     // - EasyEditSpans, for which we have some UI to display on attach and on hide
    263     // - SelectionSpans, for which we need to call updateSelection if an IME is attached
    264     private SpanController mSpanController;
    265 
    266     WordIterator mWordIterator;
    267     SpellChecker mSpellChecker;
    268 
    269     // This word iterator is set with text and used to determine word boundaries
    270     // when a user is selecting text.
    271     private WordIterator mWordIteratorWithText;
    272     // Indicate that the text in the word iterator needs to be updated.
    273     private boolean mUpdateWordIteratorText;
    274 
    275     private Rect mTempRect;
    276 
    277     private TextView mTextView;
    278 
    279     final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler;
    280 
    281     final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier();
    282 
    283     private final Runnable mShowFloatingToolbar = new Runnable() {
    284         @Override
    285         public void run() {
    286             if (mTextActionMode != null) {
    287                 mTextActionMode.hide(0);  // hide off.
    288             }
    289         }
    290     };
    291 
    292     boolean mIsInsertionActionModeStartPending = false;
    293 
    294     private final SuggestionHelper mSuggestionHelper = new SuggestionHelper();
    295 
    296     Editor(TextView textView) {
    297         mTextView = textView;
    298         // Synchronize the filter list, which places the undo input filter at the end.
    299         mTextView.setFilters(mTextView.getFilters());
    300         mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
    301     }
    302 
    303     ParcelableParcel saveInstanceState() {
    304         ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
    305         Parcel parcel = state.getParcel();
    306         mUndoManager.saveInstanceState(parcel);
    307         mUndoInputFilter.saveInstanceState(parcel);
    308         return state;
    309     }
    310 
    311     void restoreInstanceState(ParcelableParcel state) {
    312         Parcel parcel = state.getParcel();
    313         mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
    314         mUndoInputFilter.restoreInstanceState(parcel);
    315         // Re-associate this object as the owner of undo state.
    316         mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
    317     }
    318 
    319     /**
    320      * Forgets all undo and redo operations for this Editor.
    321      */
    322     void forgetUndoRedo() {
    323         UndoOwner[] owners = { mUndoOwner };
    324         mUndoManager.forgetUndos(owners, -1 /* all */);
    325         mUndoManager.forgetRedos(owners, -1 /* all */);
    326     }
    327 
    328     boolean canUndo() {
    329         UndoOwner[] owners = { mUndoOwner };
    330         return mAllowUndo && mUndoManager.countUndos(owners) > 0;
    331     }
    332 
    333     boolean canRedo() {
    334         UndoOwner[] owners = { mUndoOwner };
    335         return mAllowUndo && mUndoManager.countRedos(owners) > 0;
    336     }
    337 
    338     void undo() {
    339         if (!mAllowUndo) {
    340             return;
    341         }
    342         UndoOwner[] owners = { mUndoOwner };
    343         mUndoManager.undo(owners, 1);  // Undo 1 action.
    344     }
    345 
    346     void redo() {
    347         if (!mAllowUndo) {
    348             return;
    349         }
    350         UndoOwner[] owners = { mUndoOwner };
    351         mUndoManager.redo(owners, 1);  // Redo 1 action.
    352     }
    353 
    354     void replace() {
    355         if (mSuggestionsPopupWindow == null) {
    356             mSuggestionsPopupWindow = new SuggestionsPopupWindow();
    357         }
    358         hideCursorAndSpanControllers();
    359         mSuggestionsPopupWindow.show();
    360 
    361         int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
    362         Selection.setSelection((Spannable) mTextView.getText(), middle);
    363     }
    364 
    365     void onAttachedToWindow() {
    366         if (mShowErrorAfterAttach) {
    367             showError();
    368             mShowErrorAfterAttach = false;
    369         }
    370 
    371         final ViewTreeObserver observer = mTextView.getViewTreeObserver();
    372         // No need to create the controller.
    373         // The get method will add the listener on controller creation.
    374         if (mInsertionPointCursorController != null) {
    375             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
    376         }
    377         if (mSelectionModifierCursorController != null) {
    378             mSelectionModifierCursorController.resetTouchOffsets();
    379             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
    380         }
    381         updateSpellCheckSpans(0, mTextView.getText().length(),
    382                 true /* create the spell checker if needed */);
    383 
    384         if (mTextView.hasSelection()) {
    385             refreshTextActionMode();
    386         }
    387 
    388         getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true);
    389         resumeBlink();
    390     }
    391 
    392     void onDetachedFromWindow() {
    393         getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier);
    394 
    395         if (mError != null) {
    396             hideError();
    397         }
    398 
    399         suspendBlink();
    400 
    401         if (mInsertionPointCursorController != null) {
    402             mInsertionPointCursorController.onDetached();
    403         }
    404 
    405         if (mSelectionModifierCursorController != null) {
    406             mSelectionModifierCursorController.onDetached();
    407         }
    408 
    409         if (mShowSuggestionRunnable != null) {
    410             mTextView.removeCallbacks(mShowSuggestionRunnable);
    411         }
    412 
    413         // Cancel the single tap delayed runnable.
    414         if (mInsertionActionModeRunnable != null) {
    415             mTextView.removeCallbacks(mInsertionActionModeRunnable);
    416         }
    417 
    418         mTextView.removeCallbacks(mShowFloatingToolbar);
    419 
    420         discardTextDisplayLists();
    421 
    422         if (mSpellChecker != null) {
    423             mSpellChecker.closeSession();
    424             // Forces the creation of a new SpellChecker next time this window is created.
    425             // Will handle the cases where the settings has been changed in the meantime.
    426             mSpellChecker = null;
    427         }
    428 
    429         hideCursorAndSpanControllers();
    430         stopTextActionModeWithPreservingSelection();
    431     }
    432 
    433     private void discardTextDisplayLists() {
    434         if (mTextRenderNodes != null) {
    435             for (int i = 0; i < mTextRenderNodes.length; i++) {
    436                 RenderNode displayList = mTextRenderNodes[i] != null
    437                         ? mTextRenderNodes[i].renderNode : null;
    438                 if (displayList != null && displayList.isValid()) {
    439                     displayList.discardDisplayList();
    440                 }
    441             }
    442         }
    443     }
    444 
    445     private void showError() {
    446         if (mTextView.getWindowToken() == null) {
    447             mShowErrorAfterAttach = true;
    448             return;
    449         }
    450 
    451         if (mErrorPopup == null) {
    452             LayoutInflater inflater = LayoutInflater.from(mTextView.getContext());
    453             final TextView err = (TextView) inflater.inflate(
    454                     com.android.internal.R.layout.textview_hint, null);
    455 
    456             final float scale = mTextView.getResources().getDisplayMetrics().density;
    457             mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f));
    458             mErrorPopup.setFocusable(false);
    459             // The user is entering text, so the input method is needed.  We
    460             // don't want the popup to be displayed on top of it.
    461             mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED);
    462         }
    463 
    464         TextView tv = (TextView) mErrorPopup.getContentView();
    465         chooseSize(mErrorPopup, mError, tv);
    466         tv.setText(mError);
    467 
    468         mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY());
    469         mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor());
    470     }
    471 
    472     public void setError(CharSequence error, Drawable icon) {
    473         mError = TextUtils.stringOrSpannedString(error);
    474         mErrorWasChanged = true;
    475 
    476         if (mError == null) {
    477             setErrorIcon(null);
    478             if (mErrorPopup != null) {
    479                 if (mErrorPopup.isShowing()) {
    480                     mErrorPopup.dismiss();
    481                 }
    482 
    483                 mErrorPopup = null;
    484             }
    485             mShowErrorAfterAttach = false;
    486         } else {
    487             setErrorIcon(icon);
    488             if (mTextView.isFocused()) {
    489                 showError();
    490             }
    491         }
    492     }
    493 
    494     private void setErrorIcon(Drawable icon) {
    495         Drawables dr = mTextView.mDrawables;
    496         if (dr == null) {
    497             mTextView.mDrawables = dr = new Drawables(mTextView.getContext());
    498         }
    499         dr.setErrorDrawable(icon, mTextView);
    500 
    501         mTextView.resetResolvedDrawables();
    502         mTextView.invalidate();
    503         mTextView.requestLayout();
    504     }
    505 
    506     private void hideError() {
    507         if (mErrorPopup != null) {
    508             if (mErrorPopup.isShowing()) {
    509                 mErrorPopup.dismiss();
    510             }
    511         }
    512 
    513         mShowErrorAfterAttach = false;
    514     }
    515 
    516     /**
    517      * Returns the X offset to make the pointy top of the error point
    518      * at the middle of the error icon.
    519      */
    520     private int getErrorX() {
    521         /*
    522          * The "25" is the distance between the point and the right edge
    523          * of the background
    524          */
    525         final float scale = mTextView.getResources().getDisplayMetrics().density;
    526 
    527         final Drawables dr = mTextView.mDrawables;
    528 
    529         final int layoutDirection = mTextView.getLayoutDirection();
    530         int errorX;
    531         int offset;
    532         switch (layoutDirection) {
    533             default:
    534             case View.LAYOUT_DIRECTION_LTR:
    535                 offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f);
    536                 errorX = mTextView.getWidth() - mErrorPopup.getWidth() -
    537                         mTextView.getPaddingRight() + offset;
    538                 break;
    539             case View.LAYOUT_DIRECTION_RTL:
    540                 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f);
    541                 errorX = mTextView.getPaddingLeft() + offset;
    542                 break;
    543         }
    544         return errorX;
    545     }
    546 
    547     /**
    548      * Returns the Y offset to make the pointy top of the error point
    549      * at the bottom of the error icon.
    550      */
    551     private int getErrorY() {
    552         /*
    553          * Compound, not extended, because the icon is not clipped
    554          * if the text height is smaller.
    555          */
    556         final int compoundPaddingTop = mTextView.getCompoundPaddingTop();
    557         int vspace = mTextView.getBottom() - mTextView.getTop() -
    558                 mTextView.getCompoundPaddingBottom() - compoundPaddingTop;
    559 
    560         final Drawables dr = mTextView.mDrawables;
    561 
    562         final int layoutDirection = mTextView.getLayoutDirection();
    563         int height;
    564         switch (layoutDirection) {
    565             default:
    566             case View.LAYOUT_DIRECTION_LTR:
    567                 height = (dr != null ? dr.mDrawableHeightRight : 0);
    568                 break;
    569             case View.LAYOUT_DIRECTION_RTL:
    570                 height = (dr != null ? dr.mDrawableHeightLeft : 0);
    571                 break;
    572         }
    573 
    574         int icontop = compoundPaddingTop + (vspace - height) / 2;
    575 
    576         /*
    577          * The "2" is the distance between the point and the top edge
    578          * of the background.
    579          */
    580         final float scale = mTextView.getResources().getDisplayMetrics().density;
    581         return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f);
    582     }
    583 
    584     void createInputContentTypeIfNeeded() {
    585         if (mInputContentType == null) {
    586             mInputContentType = new InputContentType();
    587         }
    588     }
    589 
    590     void createInputMethodStateIfNeeded() {
    591         if (mInputMethodState == null) {
    592             mInputMethodState = new InputMethodState();
    593         }
    594     }
    595 
    596     boolean isCursorVisible() {
    597         // The default value is true, even when there is no associated Editor
    598         return mCursorVisible && mTextView.isTextEditable();
    599     }
    600 
    601     void prepareCursorControllers() {
    602         boolean windowSupportsHandles = false;
    603 
    604         ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams();
    605         if (params instanceof WindowManager.LayoutParams) {
    606             WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params;
    607             windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW
    608                     || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW;
    609         }
    610 
    611         boolean enabled = windowSupportsHandles && mTextView.getLayout() != null;
    612         mInsertionControllerEnabled = enabled && isCursorVisible();
    613         mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected();
    614 
    615         if (!mInsertionControllerEnabled) {
    616             hideInsertionPointCursorController();
    617             if (mInsertionPointCursorController != null) {
    618                 mInsertionPointCursorController.onDetached();
    619                 mInsertionPointCursorController = null;
    620             }
    621         }
    622 
    623         if (!mSelectionControllerEnabled) {
    624             stopTextActionMode();
    625             if (mSelectionModifierCursorController != null) {
    626                 mSelectionModifierCursorController.onDetached();
    627                 mSelectionModifierCursorController = null;
    628             }
    629         }
    630     }
    631 
    632     void hideInsertionPointCursorController() {
    633         if (mInsertionPointCursorController != null) {
    634             mInsertionPointCursorController.hide();
    635         }
    636     }
    637 
    638     /**
    639      * Hides the insertion and span controllers.
    640      */
    641     void hideCursorAndSpanControllers() {
    642         hideCursorControllers();
    643         hideSpanControllers();
    644     }
    645 
    646     private void hideSpanControllers() {
    647         if (mSpanController != null) {
    648             mSpanController.hide();
    649         }
    650     }
    651 
    652     private void hideCursorControllers() {
    653         // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost.
    654         // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the
    655         // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp()
    656         // to distinguish one from the other.
    657         if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode()) ||
    658                 !mSuggestionsPopupWindow.isShowingUp())) {
    659             // Should be done before hide insertion point controller since it triggers a show of it
    660             mSuggestionsPopupWindow.hide();
    661         }
    662         hideInsertionPointCursorController();
    663     }
    664 
    665     /**
    666      * Create new SpellCheckSpans on the modified region.
    667      */
    668     private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) {
    669         // Remove spans whose adjacent characters are text not punctuation
    670         mTextView.removeAdjacentSuggestionSpans(start);
    671         mTextView.removeAdjacentSuggestionSpans(end);
    672 
    673         if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() &&
    674                 !(mTextView.isInExtractedMode())) {
    675             if (mSpellChecker == null && createSpellChecker) {
    676                 mSpellChecker = new SpellChecker(mTextView);
    677             }
    678             if (mSpellChecker != null) {
    679                 mSpellChecker.spellCheck(start, end);
    680             }
    681         }
    682     }
    683 
    684     void onScreenStateChanged(int screenState) {
    685         switch (screenState) {
    686             case View.SCREEN_STATE_ON:
    687                 resumeBlink();
    688                 break;
    689             case View.SCREEN_STATE_OFF:
    690                 suspendBlink();
    691                 break;
    692         }
    693     }
    694 
    695     private void suspendBlink() {
    696         if (mBlink != null) {
    697             mBlink.cancel();
    698         }
    699     }
    700 
    701     private void resumeBlink() {
    702         if (mBlink != null) {
    703             mBlink.uncancel();
    704             makeBlink();
    705         }
    706     }
    707 
    708     void adjustInputType(boolean password, boolean passwordInputType,
    709             boolean webPasswordInputType, boolean numberPasswordInputType) {
    710         // mInputType has been set from inputType, possibly modified by mInputMethod.
    711         // Specialize mInputType to [web]password if we have a text class and the original input
    712         // type was a password.
    713         if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) {
    714             if (password || passwordInputType) {
    715                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    716                         | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD;
    717             }
    718             if (webPasswordInputType) {
    719                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    720                         | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD;
    721             }
    722         } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) {
    723             if (numberPasswordInputType) {
    724                 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION))
    725                         | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD;
    726             }
    727         }
    728     }
    729 
    730     private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) {
    731         int wid = tv.getPaddingLeft() + tv.getPaddingRight();
    732         int ht = tv.getPaddingTop() + tv.getPaddingBottom();
    733 
    734         int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize(
    735                 com.android.internal.R.dimen.textview_error_popup_default_width);
    736         Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels,
    737                                     Layout.Alignment.ALIGN_NORMAL, 1, 0, true);
    738         float max = 0;
    739         for (int i = 0; i < l.getLineCount(); i++) {
    740             max = Math.max(max, l.getLineWidth(i));
    741         }
    742 
    743         /*
    744          * Now set the popup size to be big enough for the text plus the border capped
    745          * to DEFAULT_MAX_POPUP_WIDTH
    746          */
    747         pop.setWidth(wid + (int) Math.ceil(max));
    748         pop.setHeight(ht + l.getHeight());
    749     }
    750 
    751     void setFrame() {
    752         if (mErrorPopup != null) {
    753             TextView tv = (TextView) mErrorPopup.getContentView();
    754             chooseSize(mErrorPopup, mError, tv);
    755             mErrorPopup.update(mTextView, getErrorX(), getErrorY(),
    756                     mErrorPopup.getWidth(), mErrorPopup.getHeight());
    757         }
    758     }
    759 
    760     private int getWordStart(int offset) {
    761         // FIXME - For this and similar methods we're not doing anything to check if there's
    762         // a LocaleSpan in the text, this may be something we should try handling or checking for.
    763         int retOffset = getWordIteratorWithText().prevBoundary(offset);
    764         if (getWordIteratorWithText().isOnPunctuation(retOffset)) {
    765             // On punctuation boundary or within group of punctuation, find punctuation start.
    766             retOffset = getWordIteratorWithText().getPunctuationBeginning(offset);
    767         } else {
    768             // Not on a punctuation boundary, find the word start.
    769             retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset);
    770         }
    771         if (retOffset == BreakIterator.DONE) {
    772             return offset;
    773         }
    774         return retOffset;
    775     }
    776 
    777     private int getWordEnd(int offset) {
    778         int retOffset = getWordIteratorWithText().nextBoundary(offset);
    779         if (getWordIteratorWithText().isAfterPunctuation(retOffset)) {
    780             // On punctuation boundary or within group of punctuation, find punctuation end.
    781             retOffset = getWordIteratorWithText().getPunctuationEnd(offset);
    782         } else {
    783             // Not on a punctuation boundary, find the word end.
    784             retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset);
    785         }
    786         if (retOffset == BreakIterator.DONE) {
    787             return offset;
    788         }
    789         return retOffset;
    790     }
    791 
    792     private boolean needsToSelectAllToSelectWordOrParagraph() {
    793         if (mTextView.hasPasswordTransformationMethod()) {
    794             // Always select all on a password field.
    795             // Cut/copy menu entries are not available for passwords, but being able to select all
    796             // is however useful to delete or paste to replace the entire content.
    797             return true;
    798         }
    799 
    800         int inputType = mTextView.getInputType();
    801         int klass = inputType & InputType.TYPE_MASK_CLASS;
    802         int variation = inputType & InputType.TYPE_MASK_VARIATION;
    803 
    804         // Specific text field types: select the entire text for these
    805         if (klass == InputType.TYPE_CLASS_NUMBER ||
    806                 klass == InputType.TYPE_CLASS_PHONE ||
    807                 klass == InputType.TYPE_CLASS_DATETIME ||
    808                 variation == InputType.TYPE_TEXT_VARIATION_URI ||
    809                 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS ||
    810                 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS ||
    811                 variation == InputType.TYPE_TEXT_VARIATION_FILTER) {
    812             return true;
    813         }
    814         return false;
    815     }
    816 
    817     /**
    818      * Adjusts selection to the word under last touch offset. Return true if the operation was
    819      * successfully performed.
    820      */
    821     private boolean selectCurrentWord() {
    822         if (!mTextView.canSelectText()) {
    823             return false;
    824         }
    825 
    826         if (needsToSelectAllToSelectWordOrParagraph()) {
    827             return mTextView.selectAllText();
    828         }
    829 
    830         long lastTouchOffsets = getLastTouchOffsets();
    831         final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
    832         final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
    833 
    834         // Safety check in case standard touch event handling has been bypassed
    835         if (minOffset < 0 || minOffset > mTextView.getText().length()) return false;
    836         if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false;
    837 
    838         int selectionStart, selectionEnd;
    839 
    840         // If a URLSpan (web address, email, phone...) is found at that position, select it.
    841         URLSpan[] urlSpans = ((Spanned) mTextView.getText()).
    842                 getSpans(minOffset, maxOffset, URLSpan.class);
    843         if (urlSpans.length >= 1) {
    844             URLSpan urlSpan = urlSpans[0];
    845             selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan);
    846             selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan);
    847         } else {
    848             // FIXME - We should check if there's a LocaleSpan in the text, this may be
    849             // something we should try handling or checking for.
    850             final WordIterator wordIterator = getWordIterator();
    851             wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset);
    852 
    853             selectionStart = wordIterator.getBeginning(minOffset);
    854             selectionEnd = wordIterator.getEnd(maxOffset);
    855 
    856             if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE ||
    857                     selectionStart == selectionEnd) {
    858                 // Possible when the word iterator does not properly handle the text's language
    859                 long range = getCharClusterRange(minOffset);
    860                 selectionStart = TextUtils.unpackRangeStartFromLong(range);
    861                 selectionEnd = TextUtils.unpackRangeEndFromLong(range);
    862             }
    863         }
    864 
    865         Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
    866         return selectionEnd > selectionStart;
    867     }
    868 
    869     /**
    870      * Adjusts selection to the paragraph under last touch offset. Return true if the operation was
    871      * successfully performed.
    872      */
    873     private boolean selectCurrentParagraph() {
    874         if (!mTextView.canSelectText()) {
    875             return false;
    876         }
    877 
    878         if (needsToSelectAllToSelectWordOrParagraph()) {
    879             return mTextView.selectAllText();
    880         }
    881 
    882         long lastTouchOffsets = getLastTouchOffsets();
    883         final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets);
    884         final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets);
    885 
    886         final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset);
    887         final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange);
    888         final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange);
    889         if (start < end) {
    890             Selection.setSelection((Spannable) mTextView.getText(), start, end);
    891             return true;
    892         }
    893         return false;
    894     }
    895 
    896     /**
    897      * Get the minimum range of paragraphs that contains startOffset and endOffset.
    898      */
    899     private long getParagraphsRange(int startOffset, int endOffset) {
    900         final Layout layout = mTextView.getLayout();
    901         if (layout == null) {
    902             return TextUtils.packRangeInLong(-1, -1);
    903         }
    904         final CharSequence text = mTextView.getText();
    905         int minLine = layout.getLineForOffset(startOffset);
    906         // Search paragraph start.
    907         while (minLine > 0) {
    908             final int prevLineEndOffset = layout.getLineEnd(minLine - 1);
    909             if (text.charAt(prevLineEndOffset - 1) == '\n') {
    910                 break;
    911             }
    912             minLine--;
    913         }
    914         int maxLine = layout.getLineForOffset(endOffset);
    915         // Search paragraph end.
    916         while (maxLine < layout.getLineCount() - 1) {
    917             final int lineEndOffset = layout.getLineEnd(maxLine);
    918             if (text.charAt(lineEndOffset - 1) == '\n') {
    919                 break;
    920             }
    921             maxLine++;
    922         }
    923         return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine));
    924     }
    925 
    926     void onLocaleChanged() {
    927         // Will be re-created on demand in getWordIterator and getWordIteratorWithText with the
    928         // proper new locale
    929         mWordIterator = null;
    930         mWordIteratorWithText = null;
    931     }
    932 
    933     /**
    934      * @hide
    935      */
    936     public WordIterator getWordIterator() {
    937         if (mWordIterator == null) {
    938             mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
    939         }
    940         return mWordIterator;
    941     }
    942 
    943     private WordIterator getWordIteratorWithText() {
    944         if (mWordIteratorWithText == null) {
    945             mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale());
    946             mUpdateWordIteratorText = true;
    947         }
    948         if (mUpdateWordIteratorText) {
    949             // FIXME - Shouldn't copy all of the text as only the area of the text relevant
    950             // to the user's selection is needed. A possible solution would be to
    951             // copy some number N of characters near the selection and then when the
    952             // user approaches N then we'd do another copy of the next N characters.
    953             CharSequence text = mTextView.getText();
    954             mWordIteratorWithText.setCharSequence(text, 0, text.length());
    955             mUpdateWordIteratorText = false;
    956         }
    957         return mWordIteratorWithText;
    958     }
    959 
    960     private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) {
    961         final Layout layout = mTextView.getLayout();
    962         if (layout == null) return offset;
    963         return findAfterGivenOffset == layout.isRtlCharAt(offset) ?
    964                 layout.getOffsetToLeftOf(offset) : layout.getOffsetToRightOf(offset);
    965     }
    966 
    967     private long getCharClusterRange(int offset) {
    968         final int textLength = mTextView.getText().length();
    969         if (offset < textLength) {
    970             final int clusterEndOffset = getNextCursorOffset(offset, true);
    971             return TextUtils.packRangeInLong(
    972                     getNextCursorOffset(clusterEndOffset, false), clusterEndOffset);
    973         }
    974         if (offset - 1 >= 0) {
    975             final int clusterStartOffset = getNextCursorOffset(offset, false);
    976             return TextUtils.packRangeInLong(clusterStartOffset,
    977                     getNextCursorOffset(clusterStartOffset, true));
    978         }
    979         return TextUtils.packRangeInLong(offset, offset);
    980     }
    981 
    982     private boolean touchPositionIsInSelection() {
    983         int selectionStart = mTextView.getSelectionStart();
    984         int selectionEnd = mTextView.getSelectionEnd();
    985 
    986         if (selectionStart == selectionEnd) {
    987             return false;
    988         }
    989 
    990         if (selectionStart > selectionEnd) {
    991             int tmp = selectionStart;
    992             selectionStart = selectionEnd;
    993             selectionEnd = tmp;
    994             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
    995         }
    996 
    997         SelectionModifierCursorController selectionController = getSelectionController();
    998         int minOffset = selectionController.getMinTouchOffset();
    999         int maxOffset = selectionController.getMaxTouchOffset();
   1000 
   1001         return ((minOffset >= selectionStart) && (maxOffset < selectionEnd));
   1002     }
   1003 
   1004     private PositionListener getPositionListener() {
   1005         if (mPositionListener == null) {
   1006             mPositionListener = new PositionListener();
   1007         }
   1008         return mPositionListener;
   1009     }
   1010 
   1011     private interface TextViewPositionListener {
   1012         public void updatePosition(int parentPositionX, int parentPositionY,
   1013                 boolean parentPositionChanged, boolean parentScrolled);
   1014     }
   1015 
   1016     private boolean isPositionVisible(final float positionX, final float positionY) {
   1017         synchronized (TEMP_POSITION) {
   1018             final float[] position = TEMP_POSITION;
   1019             position[0] = positionX;
   1020             position[1] = positionY;
   1021             View view = mTextView;
   1022 
   1023             while (view != null) {
   1024                 if (view != mTextView) {
   1025                     // Local scroll is already taken into account in positionX/Y
   1026                     position[0] -= view.getScrollX();
   1027                     position[1] -= view.getScrollY();
   1028                 }
   1029 
   1030                 if (position[0] < 0 || position[1] < 0 ||
   1031                         position[0] > view.getWidth() || position[1] > view.getHeight()) {
   1032                     return false;
   1033                 }
   1034 
   1035                 if (!view.getMatrix().isIdentity()) {
   1036                     view.getMatrix().mapPoints(position);
   1037                 }
   1038 
   1039                 position[0] += view.getLeft();
   1040                 position[1] += view.getTop();
   1041 
   1042                 final ViewParent parent = view.getParent();
   1043                 if (parent instanceof View) {
   1044                     view = (View) parent;
   1045                 } else {
   1046                     // We've reached the ViewRoot, stop iterating
   1047                     view = null;
   1048                 }
   1049             }
   1050         }
   1051 
   1052         // We've been able to walk up the view hierarchy and the position was never clipped
   1053         return true;
   1054     }
   1055 
   1056     private boolean isOffsetVisible(int offset) {
   1057         Layout layout = mTextView.getLayout();
   1058         if (layout == null) return false;
   1059 
   1060         final int line = layout.getLineForOffset(offset);
   1061         final int lineBottom = layout.getLineBottom(line);
   1062         final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset);
   1063         return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(),
   1064                 lineBottom + mTextView.viewportToContentVerticalOffset());
   1065     }
   1066 
   1067     /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed
   1068      * in the view. Returns false when the position is in the empty space of left/right of text.
   1069      */
   1070     private boolean isPositionOnText(float x, float y) {
   1071         Layout layout = mTextView.getLayout();
   1072         if (layout == null) return false;
   1073 
   1074         final int line = mTextView.getLineAtCoordinate(y);
   1075         x = mTextView.convertToLocalHorizontalCoordinate(x);
   1076 
   1077         if (x < layout.getLineLeft(line)) return false;
   1078         if (x > layout.getLineRight(line)) return false;
   1079         return true;
   1080     }
   1081 
   1082     private void startDragAndDrop() {
   1083         // TODO: Fix drag and drop in full screen extracted mode.
   1084         if (mTextView.isInExtractedMode()) {
   1085             return;
   1086         }
   1087         final int start = mTextView.getSelectionStart();
   1088         final int end = mTextView.getSelectionEnd();
   1089         CharSequence selectedText = mTextView.getTransformedText(start, end);
   1090         ClipData data = ClipData.newPlainText(null, selectedText);
   1091         DragLocalState localState = new DragLocalState(mTextView, start, end);
   1092         mTextView.startDragAndDrop(data, getTextThumbnailBuilder(start, end), localState,
   1093                 View.DRAG_FLAG_GLOBAL);
   1094         stopTextActionMode();
   1095         if (hasSelectionController()) {
   1096             getSelectionController().resetTouchOffsets();
   1097         }
   1098     }
   1099 
   1100     public boolean performLongClick(boolean handled) {
   1101         // Long press in empty space moves cursor and starts the insertion action mode.
   1102         if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) &&
   1103                 mInsertionControllerEnabled) {
   1104             final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
   1105                     mLastDownPositionY);
   1106             Selection.setSelection((Spannable) mTextView.getText(), offset);
   1107             getInsertionController().show();
   1108             mIsInsertionActionModeStartPending = true;
   1109             handled = true;
   1110         }
   1111 
   1112         if (!handled && mTextActionMode != null) {
   1113             if (touchPositionIsInSelection()) {
   1114                 startDragAndDrop();
   1115             } else {
   1116                 stopTextActionMode();
   1117                 selectCurrentWordAndStartDrag();
   1118             }
   1119             handled = true;
   1120         }
   1121 
   1122         // Start a new selection
   1123         if (!handled) {
   1124             handled = selectCurrentWordAndStartDrag();
   1125         }
   1126 
   1127         return handled;
   1128     }
   1129 
   1130     private long getLastTouchOffsets() {
   1131         SelectionModifierCursorController selectionController = getSelectionController();
   1132         final int minOffset = selectionController.getMinTouchOffset();
   1133         final int maxOffset = selectionController.getMaxTouchOffset();
   1134         return TextUtils.packRangeInLong(minOffset, maxOffset);
   1135     }
   1136 
   1137     void onFocusChanged(boolean focused, int direction) {
   1138         mShowCursor = SystemClock.uptimeMillis();
   1139         ensureEndedBatchEdit();
   1140 
   1141         if (focused) {
   1142             int selStart = mTextView.getSelectionStart();
   1143             int selEnd = mTextView.getSelectionEnd();
   1144 
   1145             // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection
   1146             // mode for these, unless there was a specific selection already started.
   1147             final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 &&
   1148                     selEnd == mTextView.getText().length();
   1149 
   1150             mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() &&
   1151                     !isFocusHighlighted;
   1152 
   1153             if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) {
   1154                 // If a tap was used to give focus to that view, move cursor at tap position.
   1155                 // Has to be done before onTakeFocus, which can be overloaded.
   1156                 final int lastTapPosition = getLastTapPosition();
   1157                 if (lastTapPosition >= 0) {
   1158                     Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition);
   1159                 }
   1160 
   1161                 // Note this may have to be moved out of the Editor class
   1162                 MovementMethod mMovement = mTextView.getMovementMethod();
   1163                 if (mMovement != null) {
   1164                     mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction);
   1165                 }
   1166 
   1167                 // The DecorView does not have focus when the 'Done' ExtractEditText button is
   1168                 // pressed. Since it is the ViewAncestor's mView, it requests focus before
   1169                 // ExtractEditText clears focus, which gives focus to the ExtractEditText.
   1170                 // This special case ensure that we keep current selection in that case.
   1171                 // It would be better to know why the DecorView does not have focus at that time.
   1172                 if (((mTextView.isInExtractedMode()) || mSelectionMoved) &&
   1173                         selStart >= 0 && selEnd >= 0) {
   1174                     /*
   1175                      * Someone intentionally set the selection, so let them
   1176                      * do whatever it is that they wanted to do instead of
   1177                      * the default on-focus behavior.  We reset the selection
   1178                      * here instead of just skipping the onTakeFocus() call
   1179                      * because some movement methods do something other than
   1180                      * just setting the selection in theirs and we still
   1181                      * need to go through that path.
   1182                      */
   1183                     Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
   1184                 }
   1185 
   1186                 if (mSelectAllOnFocus) {
   1187                     mTextView.selectAllText();
   1188                 }
   1189 
   1190                 mTouchFocusSelected = true;
   1191             }
   1192 
   1193             mFrozenWithFocus = false;
   1194             mSelectionMoved = false;
   1195 
   1196             if (mError != null) {
   1197                 showError();
   1198             }
   1199 
   1200             makeBlink();
   1201         } else {
   1202             if (mError != null) {
   1203                 hideError();
   1204             }
   1205             // Don't leave us in the middle of a batch edit.
   1206             mTextView.onEndBatchEdit();
   1207 
   1208             if (mTextView.isInExtractedMode()) {
   1209                 hideCursorAndSpanControllers();
   1210                 stopTextActionModeWithPreservingSelection();
   1211             } else {
   1212                 hideCursorAndSpanControllers();
   1213                 if (mTextView.isTemporarilyDetached()) {
   1214                     stopTextActionModeWithPreservingSelection();
   1215                 } else {
   1216                     stopTextActionMode();
   1217                 }
   1218                 downgradeEasyCorrectionSpans();
   1219             }
   1220             // No need to create the controller
   1221             if (mSelectionModifierCursorController != null) {
   1222                 mSelectionModifierCursorController.resetTouchOffsets();
   1223             }
   1224         }
   1225     }
   1226 
   1227     /**
   1228      * Downgrades to simple suggestions all the easy correction spans that are not a spell check
   1229      * span.
   1230      */
   1231     private void downgradeEasyCorrectionSpans() {
   1232         CharSequence text = mTextView.getText();
   1233         if (text instanceof Spannable) {
   1234             Spannable spannable = (Spannable) text;
   1235             SuggestionSpan[] suggestionSpans = spannable.getSpans(0,
   1236                     spannable.length(), SuggestionSpan.class);
   1237             for (int i = 0; i < suggestionSpans.length; i++) {
   1238                 int flags = suggestionSpans[i].getFlags();
   1239                 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0
   1240                         && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) {
   1241                     flags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
   1242                     suggestionSpans[i].setFlags(flags);
   1243                 }
   1244             }
   1245         }
   1246     }
   1247 
   1248     void sendOnTextChanged(int start, int after) {
   1249         updateSpellCheckSpans(start, start + after, false);
   1250 
   1251         // Flip flag to indicate the word iterator needs to have the text reset.
   1252         mUpdateWordIteratorText = true;
   1253 
   1254         // Hide the controllers as soon as text is modified (typing, procedural...)
   1255         // We do not hide the span controllers, since they can be added when a new text is
   1256         // inserted into the text view (voice IME).
   1257         hideCursorControllers();
   1258         // Reset drag accelerator.
   1259         if (mSelectionModifierCursorController != null) {
   1260             mSelectionModifierCursorController.resetTouchOffsets();
   1261         }
   1262         stopTextActionMode();
   1263     }
   1264 
   1265     private int getLastTapPosition() {
   1266         // No need to create the controller at that point, no last tap position saved
   1267         if (mSelectionModifierCursorController != null) {
   1268             int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset();
   1269             if (lastTapPosition >= 0) {
   1270                 // Safety check, should not be possible.
   1271                 if (lastTapPosition > mTextView.getText().length()) {
   1272                     lastTapPosition = mTextView.getText().length();
   1273                 }
   1274                 return lastTapPosition;
   1275             }
   1276         }
   1277 
   1278         return -1;
   1279     }
   1280 
   1281     void onWindowFocusChanged(boolean hasWindowFocus) {
   1282         if (hasWindowFocus) {
   1283             if (mBlink != null) {
   1284                 mBlink.uncancel();
   1285                 makeBlink();
   1286             }
   1287             final InputMethodManager imm = InputMethodManager.peekInstance();
   1288             if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
   1289                 refreshTextActionMode();
   1290             }
   1291         } else {
   1292             if (mBlink != null) {
   1293                 mBlink.cancel();
   1294             }
   1295             if (mInputContentType != null) {
   1296                 mInputContentType.enterDown = false;
   1297             }
   1298             // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
   1299             hideCursorAndSpanControllers();
   1300             stopTextActionModeWithPreservingSelection();
   1301             if (mSuggestionsPopupWindow != null) {
   1302                 mSuggestionsPopupWindow.onParentLostFocus();
   1303             }
   1304 
   1305             // Don't leave us in the middle of a batch edit. Same as in onFocusChanged
   1306             ensureEndedBatchEdit();
   1307         }
   1308     }
   1309 
   1310     private void updateTapState(MotionEvent event) {
   1311         final int action = event.getActionMasked();
   1312         if (action == MotionEvent.ACTION_DOWN) {
   1313             final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
   1314             // Detect double tap and triple click.
   1315             if (((mTapState == TAP_STATE_FIRST_TAP)
   1316                     || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse))
   1317                         && (SystemClock.uptimeMillis() - mLastTouchUpTime) <=
   1318                                 ViewConfiguration.getDoubleTapTimeout()) {
   1319                 if (mTapState == TAP_STATE_FIRST_TAP) {
   1320                     mTapState = TAP_STATE_DOUBLE_TAP;
   1321                 } else {
   1322                     mTapState = TAP_STATE_TRIPLE_CLICK;
   1323                 }
   1324             } else {
   1325                 mTapState = TAP_STATE_FIRST_TAP;
   1326             }
   1327         }
   1328         if (action == MotionEvent.ACTION_UP) {
   1329             mLastTouchUpTime = SystemClock.uptimeMillis();
   1330         }
   1331     }
   1332 
   1333     private boolean shouldFilterOutTouchEvent(MotionEvent event) {
   1334         if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) {
   1335             return false;
   1336         }
   1337         final boolean primaryButtonStateChanged =
   1338                 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0;
   1339         final int action = event.getActionMasked();
   1340         if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP)
   1341                 && !primaryButtonStateChanged) {
   1342             return true;
   1343         }
   1344         if (action == MotionEvent.ACTION_MOVE
   1345                 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) {
   1346             return true;
   1347         }
   1348         return false;
   1349     }
   1350 
   1351     void onTouchEvent(MotionEvent event) {
   1352         final boolean filterOutEvent = shouldFilterOutTouchEvent(event);
   1353         mLastButtonState = event.getButtonState();
   1354         if (filterOutEvent) {
   1355             if (event.getActionMasked() == MotionEvent.ACTION_UP) {
   1356                 mDiscardNextActionUp = true;
   1357             }
   1358             return;
   1359         }
   1360         updateTapState(event);
   1361         updateFloatingToolbarVisibility(event);
   1362 
   1363         if (hasSelectionController()) {
   1364             getSelectionController().onTouchEvent(event);
   1365         }
   1366 
   1367         if (mShowSuggestionRunnable != null) {
   1368             mTextView.removeCallbacks(mShowSuggestionRunnable);
   1369             mShowSuggestionRunnable = null;
   1370         }
   1371 
   1372         if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
   1373             mLastDownPositionX = event.getX();
   1374             mLastDownPositionY = event.getY();
   1375 
   1376             // Reset this state; it will be re-set if super.onTouchEvent
   1377             // causes focus to move to the view.
   1378             mTouchFocusSelected = false;
   1379             mIgnoreActionUpEvent = false;
   1380         }
   1381     }
   1382 
   1383     private void updateFloatingToolbarVisibility(MotionEvent event) {
   1384         if (mTextActionMode != null) {
   1385             switch (event.getActionMasked()) {
   1386                 case MotionEvent.ACTION_MOVE:
   1387                     hideFloatingToolbar();
   1388                     break;
   1389                 case MotionEvent.ACTION_UP:  // fall through
   1390                 case MotionEvent.ACTION_CANCEL:
   1391                     showFloatingToolbar();
   1392             }
   1393         }
   1394     }
   1395 
   1396     private void hideFloatingToolbar() {
   1397         if (mTextActionMode != null) {
   1398             mTextView.removeCallbacks(mShowFloatingToolbar);
   1399             mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION);
   1400         }
   1401     }
   1402 
   1403     private void showFloatingToolbar() {
   1404         if (mTextActionMode != null) {
   1405             // Delay "show" so it doesn't interfere with click confirmations
   1406             // or double-clicks that could "dismiss" the floating toolbar.
   1407             int delay = ViewConfiguration.getDoubleTapTimeout();
   1408             mTextView.postDelayed(mShowFloatingToolbar, delay);
   1409         }
   1410     }
   1411 
   1412     public void beginBatchEdit() {
   1413         mInBatchEditControllers = true;
   1414         final InputMethodState ims = mInputMethodState;
   1415         if (ims != null) {
   1416             int nesting = ++ims.mBatchEditNesting;
   1417             if (nesting == 1) {
   1418                 ims.mCursorChanged = false;
   1419                 ims.mChangedDelta = 0;
   1420                 if (ims.mContentChanged) {
   1421                     // We already have a pending change from somewhere else,
   1422                     // so turn this into a full update.
   1423                     ims.mChangedStart = 0;
   1424                     ims.mChangedEnd = mTextView.getText().length();
   1425                 } else {
   1426                     ims.mChangedStart = EXTRACT_UNKNOWN;
   1427                     ims.mChangedEnd = EXTRACT_UNKNOWN;
   1428                     ims.mContentChanged = false;
   1429                 }
   1430                 mUndoInputFilter.beginBatchEdit();
   1431                 mTextView.onBeginBatchEdit();
   1432             }
   1433         }
   1434     }
   1435 
   1436     public void endBatchEdit() {
   1437         mInBatchEditControllers = false;
   1438         final InputMethodState ims = mInputMethodState;
   1439         if (ims != null) {
   1440             int nesting = --ims.mBatchEditNesting;
   1441             if (nesting == 0) {
   1442                 finishBatchEdit(ims);
   1443             }
   1444         }
   1445     }
   1446 
   1447     void ensureEndedBatchEdit() {
   1448         final InputMethodState ims = mInputMethodState;
   1449         if (ims != null && ims.mBatchEditNesting != 0) {
   1450             ims.mBatchEditNesting = 0;
   1451             finishBatchEdit(ims);
   1452         }
   1453     }
   1454 
   1455     void finishBatchEdit(final InputMethodState ims) {
   1456         mTextView.onEndBatchEdit();
   1457         mUndoInputFilter.endBatchEdit();
   1458 
   1459         if (ims.mContentChanged || ims.mSelectionModeChanged) {
   1460             mTextView.updateAfterEdit();
   1461             reportExtractedText();
   1462         } else if (ims.mCursorChanged) {
   1463             // Cheesy way to get us to report the current cursor location.
   1464             mTextView.invalidateCursor();
   1465         }
   1466         // sendUpdateSelection knows to avoid sending if the selection did
   1467         // not actually change.
   1468         sendUpdateSelection();
   1469 
   1470         // Show drag handles if they were blocked by batch edit mode.
   1471         if (mTextActionMode != null) {
   1472             final CursorController cursorController = mTextView.hasSelection() ?
   1473                     getSelectionController() : getInsertionController();
   1474             if (cursorController != null && !cursorController.isActive()
   1475                     && !cursorController.isCursorBeingModified()) {
   1476                 cursorController.show();
   1477             }
   1478         }
   1479     }
   1480 
   1481     static final int EXTRACT_NOTHING = -2;
   1482     static final int EXTRACT_UNKNOWN = -1;
   1483 
   1484     boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
   1485         return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
   1486                 EXTRACT_UNKNOWN, outText);
   1487     }
   1488 
   1489     private boolean extractTextInternal(@Nullable ExtractedTextRequest request,
   1490             int partialStartOffset, int partialEndOffset, int delta,
   1491             @Nullable ExtractedText outText) {
   1492         if (request == null || outText == null) {
   1493             return false;
   1494         }
   1495 
   1496         final CharSequence content = mTextView.getText();
   1497         if (content == null) {
   1498             return false;
   1499         }
   1500 
   1501         if (partialStartOffset != EXTRACT_NOTHING) {
   1502             final int N = content.length();
   1503             if (partialStartOffset < 0) {
   1504                 outText.partialStartOffset = outText.partialEndOffset = -1;
   1505                 partialStartOffset = 0;
   1506                 partialEndOffset = N;
   1507             } else {
   1508                 // Now use the delta to determine the actual amount of text
   1509                 // we need.
   1510                 partialEndOffset += delta;
   1511                 // Adjust offsets to ensure we contain full spans.
   1512                 if (content instanceof Spanned) {
   1513                     Spanned spanned = (Spanned)content;
   1514                     Object[] spans = spanned.getSpans(partialStartOffset,
   1515                             partialEndOffset, ParcelableSpan.class);
   1516                     int i = spans.length;
   1517                     while (i > 0) {
   1518                         i--;
   1519                         int j = spanned.getSpanStart(spans[i]);
   1520                         if (j < partialStartOffset) partialStartOffset = j;
   1521                         j = spanned.getSpanEnd(spans[i]);
   1522                         if (j > partialEndOffset) partialEndOffset = j;
   1523                     }
   1524                 }
   1525                 outText.partialStartOffset = partialStartOffset;
   1526                 outText.partialEndOffset = partialEndOffset - delta;
   1527 
   1528                 if (partialStartOffset > N) {
   1529                     partialStartOffset = N;
   1530                 } else if (partialStartOffset < 0) {
   1531                     partialStartOffset = 0;
   1532                 }
   1533                 if (partialEndOffset > N) {
   1534                     partialEndOffset = N;
   1535                 } else if (partialEndOffset < 0) {
   1536                     partialEndOffset = 0;
   1537                 }
   1538             }
   1539             if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) {
   1540                 outText.text = content.subSequence(partialStartOffset,
   1541                         partialEndOffset);
   1542             } else {
   1543                 outText.text = TextUtils.substring(content, partialStartOffset,
   1544                         partialEndOffset);
   1545             }
   1546         } else {
   1547             outText.partialStartOffset = 0;
   1548             outText.partialEndOffset = 0;
   1549             outText.text = "";
   1550         }
   1551         outText.flags = 0;
   1552         if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) {
   1553             outText.flags |= ExtractedText.FLAG_SELECTING;
   1554         }
   1555         if (mTextView.isSingleLine()) {
   1556             outText.flags |= ExtractedText.FLAG_SINGLE_LINE;
   1557         }
   1558         outText.startOffset = 0;
   1559         outText.selectionStart = mTextView.getSelectionStart();
   1560         outText.selectionEnd = mTextView.getSelectionEnd();
   1561         return true;
   1562     }
   1563 
   1564     boolean reportExtractedText() {
   1565         final Editor.InputMethodState ims = mInputMethodState;
   1566         if (ims != null) {
   1567             final boolean contentChanged = ims.mContentChanged;
   1568             if (contentChanged || ims.mSelectionModeChanged) {
   1569                 ims.mContentChanged = false;
   1570                 ims.mSelectionModeChanged = false;
   1571                 final ExtractedTextRequest req = ims.mExtractedTextRequest;
   1572                 if (req != null) {
   1573                     InputMethodManager imm = InputMethodManager.peekInstance();
   1574                     if (imm != null) {
   1575                         if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
   1576                                 "Retrieving extracted start=" + ims.mChangedStart +
   1577                                 " end=" + ims.mChangedEnd +
   1578                                 " delta=" + ims.mChangedDelta);
   1579                         if (ims.mChangedStart < 0 && !contentChanged) {
   1580                             ims.mChangedStart = EXTRACT_NOTHING;
   1581                         }
   1582                         if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd,
   1583                                 ims.mChangedDelta, ims.mExtractedText)) {
   1584                             if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG,
   1585                                     "Reporting extracted start=" +
   1586                                     ims.mExtractedText.partialStartOffset +
   1587                                     " end=" + ims.mExtractedText.partialEndOffset +
   1588                                     ": " + ims.mExtractedText.text);
   1589 
   1590                             imm.updateExtractedText(mTextView, req.token, ims.mExtractedText);
   1591                             ims.mChangedStart = EXTRACT_UNKNOWN;
   1592                             ims.mChangedEnd = EXTRACT_UNKNOWN;
   1593                             ims.mChangedDelta = 0;
   1594                             ims.mContentChanged = false;
   1595                             return true;
   1596                         }
   1597                     }
   1598                 }
   1599             }
   1600         }
   1601         return false;
   1602     }
   1603 
   1604     private void sendUpdateSelection() {
   1605         if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) {
   1606             final InputMethodManager imm = InputMethodManager.peekInstance();
   1607             if (null != imm) {
   1608                 final int selectionStart = mTextView.getSelectionStart();
   1609                 final int selectionEnd = mTextView.getSelectionEnd();
   1610                 int candStart = -1;
   1611                 int candEnd = -1;
   1612                 if (mTextView.getText() instanceof Spannable) {
   1613                     final Spannable sp = (Spannable) mTextView.getText();
   1614                     candStart = EditableInputConnection.getComposingSpanStart(sp);
   1615                     candEnd = EditableInputConnection.getComposingSpanEnd(sp);
   1616                 }
   1617                 // InputMethodManager#updateSelection skips sending the message if
   1618                 // none of the parameters have changed since the last time we called it.
   1619                 imm.updateSelection(mTextView,
   1620                         selectionStart, selectionEnd, candStart, candEnd);
   1621             }
   1622         }
   1623     }
   1624 
   1625     void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint,
   1626             int cursorOffsetVertical) {
   1627         final int selectionStart = mTextView.getSelectionStart();
   1628         final int selectionEnd = mTextView.getSelectionEnd();
   1629 
   1630         final InputMethodState ims = mInputMethodState;
   1631         if (ims != null && ims.mBatchEditNesting == 0) {
   1632             InputMethodManager imm = InputMethodManager.peekInstance();
   1633             if (imm != null) {
   1634                 if (imm.isActive(mTextView)) {
   1635                     if (ims.mContentChanged || ims.mSelectionModeChanged) {
   1636                         // We are in extract mode and the content has changed
   1637                         // in some way... just report complete new text to the
   1638                         // input method.
   1639                         reportExtractedText();
   1640                     }
   1641                 }
   1642             }
   1643         }
   1644 
   1645         if (mCorrectionHighlighter != null) {
   1646             mCorrectionHighlighter.draw(canvas, cursorOffsetVertical);
   1647         }
   1648 
   1649         if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) {
   1650             drawCursor(canvas, cursorOffsetVertical);
   1651             // Rely on the drawable entirely, do not draw the cursor line.
   1652             // Has to be done after the IMM related code above which relies on the highlight.
   1653             highlight = null;
   1654         }
   1655 
   1656         if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) {
   1657             drawHardwareAccelerated(canvas, layout, highlight, highlightPaint,
   1658                     cursorOffsetVertical);
   1659         } else {
   1660             layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical);
   1661         }
   1662     }
   1663 
   1664     private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight,
   1665             Paint highlightPaint, int cursorOffsetVertical) {
   1666         final long lineRange = layout.getLineRangeForDraw(canvas);
   1667         int firstLine = TextUtils.unpackRangeStartFromLong(lineRange);
   1668         int lastLine = TextUtils.unpackRangeEndFromLong(lineRange);
   1669         if (lastLine < 0) return;
   1670 
   1671         layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical,
   1672                 firstLine, lastLine);
   1673 
   1674         if (layout instanceof DynamicLayout) {
   1675             if (mTextRenderNodes == null) {
   1676                 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class);
   1677             }
   1678 
   1679             DynamicLayout dynamicLayout = (DynamicLayout) layout;
   1680             int[] blockEndLines = dynamicLayout.getBlockEndLines();
   1681             int[] blockIndices = dynamicLayout.getBlockIndices();
   1682             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
   1683             final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock();
   1684 
   1685             int endOfPreviousBlock = -1;
   1686             int searchStartIndex = 0;
   1687             for (int i = 0; i < numberOfBlocks; i++) {
   1688                 int blockEndLine = blockEndLines[i];
   1689                 int blockIndex = blockIndices[i];
   1690 
   1691                 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX;
   1692                 if (blockIsInvalid) {
   1693                     blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks,
   1694                             searchStartIndex);
   1695                     // Note how dynamic layout's internal block indices get updated from Editor
   1696                     blockIndices[i] = blockIndex;
   1697                     if (mTextRenderNodes[blockIndex] != null) {
   1698                         mTextRenderNodes[blockIndex].isDirty = true;
   1699                     }
   1700                     searchStartIndex = blockIndex + 1;
   1701                 }
   1702 
   1703                 if (mTextRenderNodes[blockIndex] == null) {
   1704                     mTextRenderNodes[blockIndex] =
   1705                             new TextRenderNode("Text " + blockIndex);
   1706                 }
   1707 
   1708                 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord();
   1709                 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode;
   1710                 if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) {
   1711                     final int blockBeginLine = endOfPreviousBlock + 1;
   1712                     final int top = layout.getLineTop(blockBeginLine);
   1713                     final int bottom = layout.getLineBottom(blockEndLine);
   1714                     int left = 0;
   1715                     int right = mTextView.getWidth();
   1716                     if (mTextView.getHorizontallyScrolling()) {
   1717                         float min = Float.MAX_VALUE;
   1718                         float max = Float.MIN_VALUE;
   1719                         for (int line = blockBeginLine; line <= blockEndLine; line++) {
   1720                             min = Math.min(min, layout.getLineLeft(line));
   1721                             max = Math.max(max, layout.getLineRight(line));
   1722                         }
   1723                         left = (int) min;
   1724                         right = (int) (max + 0.5f);
   1725                     }
   1726 
   1727                     // Rebuild display list if it is invalid
   1728                     if (blockDisplayListIsInvalid) {
   1729                         final DisplayListCanvas displayListCanvas = blockDisplayList.start(
   1730                                 right - left, bottom - top);
   1731                         try {
   1732                             // drawText is always relative to TextView's origin, this translation
   1733                             // brings this range of text back to the top left corner of the viewport
   1734                             displayListCanvas.translate(-left, -top);
   1735                             layout.drawText(displayListCanvas, blockBeginLine, blockEndLine);
   1736                             mTextRenderNodes[blockIndex].isDirty = false;
   1737                             // No need to untranslate, previous context is popped after
   1738                             // drawDisplayList
   1739                         } finally {
   1740                             blockDisplayList.end(displayListCanvas);
   1741                             // Same as drawDisplayList below, handled by our TextView's parent
   1742                             blockDisplayList.setClipToBounds(false);
   1743                         }
   1744                     }
   1745 
   1746                     // Valid disply list whose index is >= indexFirstChangedBlock
   1747                     // only needs to update its drawing location.
   1748                     blockDisplayList.setLeftTopRightBottom(left, top, right, bottom);
   1749                 }
   1750 
   1751                 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList);
   1752 
   1753                 endOfPreviousBlock = blockEndLine;
   1754             }
   1755 
   1756             dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks);
   1757         } else {
   1758             // Boring layout is used for empty and hint text
   1759             layout.drawText(canvas, firstLine, lastLine);
   1760         }
   1761     }
   1762 
   1763     private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks,
   1764             int searchStartIndex) {
   1765         int length = mTextRenderNodes.length;
   1766         for (int i = searchStartIndex; i < length; i++) {
   1767             boolean blockIndexFound = false;
   1768             for (int j = 0; j < numberOfBlocks; j++) {
   1769                 if (blockIndices[j] == i) {
   1770                     blockIndexFound = true;
   1771                     break;
   1772                 }
   1773             }
   1774             if (blockIndexFound) continue;
   1775             return i;
   1776         }
   1777 
   1778         // No available index found, the pool has to grow
   1779         mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null);
   1780         return length;
   1781     }
   1782 
   1783     private void drawCursor(Canvas canvas, int cursorOffsetVertical) {
   1784         final boolean translate = cursorOffsetVertical != 0;
   1785         if (translate) canvas.translate(0, cursorOffsetVertical);
   1786         for (int i = 0; i < mCursorCount; i++) {
   1787             mCursorDrawable[i].draw(canvas);
   1788         }
   1789         if (translate) canvas.translate(0, -cursorOffsetVertical);
   1790     }
   1791 
   1792     void invalidateHandlesAndActionMode() {
   1793         if (mSelectionModifierCursorController != null) {
   1794             mSelectionModifierCursorController.invalidateHandles();
   1795         }
   1796         if (mInsertionPointCursorController != null) {
   1797             mInsertionPointCursorController.invalidateHandle();
   1798         }
   1799         if (mTextActionMode != null) {
   1800             mTextActionMode.invalidate();
   1801         }
   1802     }
   1803 
   1804     /**
   1805      * Invalidates all the sub-display lists that overlap the specified character range
   1806      */
   1807     void invalidateTextDisplayList(Layout layout, int start, int end) {
   1808         if (mTextRenderNodes != null && layout instanceof DynamicLayout) {
   1809             final int firstLine = layout.getLineForOffset(start);
   1810             final int lastLine = layout.getLineForOffset(end);
   1811 
   1812             DynamicLayout dynamicLayout = (DynamicLayout) layout;
   1813             int[] blockEndLines = dynamicLayout.getBlockEndLines();
   1814             int[] blockIndices = dynamicLayout.getBlockIndices();
   1815             final int numberOfBlocks = dynamicLayout.getNumberOfBlocks();
   1816 
   1817             int i = 0;
   1818             // Skip the blocks before firstLine
   1819             while (i < numberOfBlocks) {
   1820                 if (blockEndLines[i] >= firstLine) break;
   1821                 i++;
   1822             }
   1823 
   1824             // Invalidate all subsequent blocks until lastLine is passed
   1825             while (i < numberOfBlocks) {
   1826                 final int blockIndex = blockIndices[i];
   1827                 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) {
   1828                     mTextRenderNodes[blockIndex].isDirty = true;
   1829                 }
   1830                 if (blockEndLines[i] >= lastLine) break;
   1831                 i++;
   1832             }
   1833         }
   1834     }
   1835 
   1836     void invalidateTextDisplayList() {
   1837         if (mTextRenderNodes != null) {
   1838             for (int i = 0; i < mTextRenderNodes.length; i++) {
   1839                 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true;
   1840             }
   1841         }
   1842     }
   1843 
   1844     void updateCursorsPositions() {
   1845         if (mTextView.mCursorDrawableRes == 0) {
   1846             mCursorCount = 0;
   1847             return;
   1848         }
   1849 
   1850         Layout layout = mTextView.getLayout();
   1851         final int offset = mTextView.getSelectionStart();
   1852         final int line = layout.getLineForOffset(offset);
   1853         final int top = layout.getLineTop(line);
   1854         final int bottom = layout.getLineTop(line + 1);
   1855 
   1856         mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1;
   1857 
   1858         int middle = bottom;
   1859         if (mCursorCount == 2) {
   1860             // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)}
   1861             middle = (top + bottom) >> 1;
   1862         }
   1863 
   1864         boolean clamped = layout.shouldClampCursor(line);
   1865         updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped));
   1866 
   1867         if (mCursorCount == 2) {
   1868             updateCursorPosition(1, middle, bottom, layout.getSecondaryHorizontal(offset, clamped));
   1869         }
   1870     }
   1871 
   1872     void refreshTextActionMode() {
   1873         if (extractedTextModeWillBeStarted()) {
   1874             mRestartActionModeOnNextRefresh = false;
   1875             return;
   1876         }
   1877         final boolean hasSelection = mTextView.hasSelection();
   1878         final SelectionModifierCursorController selectionController = getSelectionController();
   1879         final InsertionPointCursorController insertionController = getInsertionController();
   1880         if ((selectionController != null && selectionController.isCursorBeingModified())
   1881                 || (insertionController != null && insertionController.isCursorBeingModified())) {
   1882             // ActionMode should be managed by the currently active cursor controller.
   1883             mRestartActionModeOnNextRefresh = false;
   1884             return;
   1885         }
   1886         if (hasSelection) {
   1887             hideInsertionPointCursorController();
   1888             if (mTextActionMode == null) {
   1889                 if (mRestartActionModeOnNextRefresh) {
   1890                     // To avoid distraction, newly start action mode only when selection action
   1891                     // mode is being restarted.
   1892                     startSelectionActionMode();
   1893                 }
   1894             } else if (selectionController == null || !selectionController.isActive()) {
   1895                 // Insertion action mode is active. Avoid dismissing the selection.
   1896                 stopTextActionModeWithPreservingSelection();
   1897                 startSelectionActionMode();
   1898             } else {
   1899                 mTextActionMode.invalidateContentRect();
   1900             }
   1901         } else {
   1902             // Insertion action mode is started only when insertion controller is explicitly
   1903             // activated.
   1904             if (insertionController == null || !insertionController.isActive()) {
   1905                 stopTextActionMode();
   1906             } else if (mTextActionMode != null) {
   1907                 mTextActionMode.invalidateContentRect();
   1908             }
   1909         }
   1910         mRestartActionModeOnNextRefresh = false;
   1911     }
   1912 
   1913     /**
   1914      * Start an Insertion action mode.
   1915      */
   1916     void startInsertionActionMode() {
   1917         if (mInsertionActionModeRunnable != null) {
   1918             mTextView.removeCallbacks(mInsertionActionModeRunnable);
   1919         }
   1920         if (extractedTextModeWillBeStarted()) {
   1921             return;
   1922         }
   1923         stopTextActionMode();
   1924 
   1925         ActionMode.Callback actionModeCallback =
   1926                 new TextActionModeCallback(false /* hasSelection */);
   1927         mTextActionMode = mTextView.startActionMode(
   1928                 actionModeCallback, ActionMode.TYPE_FLOATING);
   1929         if (mTextActionMode != null && getInsertionController() != null) {
   1930             getInsertionController().show();
   1931         }
   1932     }
   1933 
   1934     /**
   1935      * Starts a Selection Action Mode with the current selection and ensures the selection handles
   1936      * are shown if there is a selection. This should be used when the mode is started from a
   1937      * non-touch event.
   1938      *
   1939      * @return true if the selection mode was actually started.
   1940      */
   1941     boolean startSelectionActionMode() {
   1942         boolean selectionStarted = startSelectionActionModeInternal();
   1943         if (selectionStarted) {
   1944             getSelectionController().show();
   1945         }
   1946         mRestartActionModeOnNextRefresh = false;
   1947         return selectionStarted;
   1948     }
   1949 
   1950     /**
   1951      * If the TextView allows text selection, selects the current word when no existing selection
   1952      * was available and starts a drag.
   1953      *
   1954      * @return true if the drag was started.
   1955      */
   1956     private boolean selectCurrentWordAndStartDrag() {
   1957         if (mInsertionActionModeRunnable != null) {
   1958             mTextView.removeCallbacks(mInsertionActionModeRunnable);
   1959         }
   1960         if (extractedTextModeWillBeStarted()) {
   1961             return false;
   1962         }
   1963         if (!checkField()) {
   1964             return false;
   1965         }
   1966         if (!mTextView.hasSelection() && !selectCurrentWord()) {
   1967             // No selection and cannot select a word.
   1968             return false;
   1969         }
   1970         stopTextActionModeWithPreservingSelection();
   1971         getSelectionController().enterDrag(
   1972                 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
   1973         return true;
   1974     }
   1975 
   1976     /**
   1977      * Checks whether a selection can be performed on the current TextView.
   1978      *
   1979      * @return true if a selection can be performed
   1980      */
   1981     boolean checkField() {
   1982         if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
   1983             Log.w(TextView.LOG_TAG,
   1984                     "TextView does not support text selection. Selection cancelled.");
   1985             return false;
   1986         }
   1987         return true;
   1988     }
   1989 
   1990     private boolean startSelectionActionModeInternal() {
   1991         if (extractedTextModeWillBeStarted()) {
   1992             return false;
   1993         }
   1994         if (mTextActionMode != null) {
   1995             // Text action mode is already started
   1996             mTextActionMode.invalidate();
   1997             return false;
   1998         }
   1999 
   2000         if (!checkField() || !mTextView.hasSelection()) {
   2001             return false;
   2002         }
   2003 
   2004         ActionMode.Callback actionModeCallback =
   2005                 new TextActionModeCallback(true /* hasSelection */);
   2006         mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);
   2007 
   2008         final boolean selectionStarted = mTextActionMode != null;
   2009         if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
   2010             // Show the IME to be able to replace text, except when selecting non editable text.
   2011             final InputMethodManager imm = InputMethodManager.peekInstance();
   2012             if (imm != null) {
   2013                 imm.showSoftInput(mTextView, 0, null);
   2014             }
   2015         }
   2016         return selectionStarted;
   2017     }
   2018 
   2019     boolean extractedTextModeWillBeStarted() {
   2020         if (!(mTextView.isInExtractedMode())) {
   2021             final InputMethodManager imm = InputMethodManager.peekInstance();
   2022             return  imm != null && imm.isFullscreenMode();
   2023         }
   2024         return false;
   2025     }
   2026 
   2027     /**
   2028      * @return <code>true</code> if it's reasonable to offer to show suggestions depending on
   2029      * the current cursor position or selection range. This method is consistent with the
   2030      * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}.
   2031      */
   2032     private boolean shouldOfferToShowSuggestions() {
   2033         CharSequence text = mTextView.getText();
   2034         if (!(text instanceof Spannable)) return false;
   2035 
   2036         final Spannable spannable = (Spannable) text;
   2037         final int selectionStart = mTextView.getSelectionStart();
   2038         final int selectionEnd = mTextView.getSelectionEnd();
   2039         final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd,
   2040                 SuggestionSpan.class);
   2041         if (suggestionSpans.length == 0) {
   2042             return false;
   2043         }
   2044         if (selectionStart == selectionEnd) {
   2045             // Spans overlap the cursor.
   2046             for (int i = 0; i < suggestionSpans.length; i++) {
   2047                 if (suggestionSpans[i].getSuggestions().length > 0) {
   2048                     return true;
   2049                 }
   2050             }
   2051             return false;
   2052         }
   2053         int minSpanStart = mTextView.getText().length();
   2054         int maxSpanEnd = 0;
   2055         int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length();
   2056         int unionOfSpansCoveringSelectionStartEnd = 0;
   2057         boolean hasValidSuggestions = false;
   2058         for (int i = 0; i < suggestionSpans.length; i++) {
   2059             final int spanStart = spannable.getSpanStart(suggestionSpans[i]);
   2060             final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]);
   2061             minSpanStart = Math.min(minSpanStart, spanStart);
   2062             maxSpanEnd = Math.max(maxSpanEnd, spanEnd);
   2063             if (selectionStart < spanStart || selectionStart > spanEnd) {
   2064                 // The span doesn't cover the current selection start point.
   2065                 continue;
   2066             }
   2067             hasValidSuggestions =
   2068                     hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0;
   2069             unionOfSpansCoveringSelectionStartStart =
   2070                     Math.min(unionOfSpansCoveringSelectionStartStart, spanStart);
   2071             unionOfSpansCoveringSelectionStartEnd =
   2072                     Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd);
   2073         }
   2074         if (!hasValidSuggestions) {
   2075             return false;
   2076         }
   2077         if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) {
   2078             // No spans cover the selection start point.
   2079             return false;
   2080         }
   2081         if (minSpanStart < unionOfSpansCoveringSelectionStartStart
   2082                 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) {
   2083             // There is a span that is not covered by the union. In this case, we soouldn't offer
   2084             // to show suggestions as it's confusing.
   2085             return false;
   2086         }
   2087         return true;
   2088     }
   2089 
   2090     /**
   2091      * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with
   2092      * {@link SuggestionSpan#FLAG_EASY_CORRECT} set.
   2093      */
   2094     private boolean isCursorInsideEasyCorrectionSpan() {
   2095         Spannable spannable = (Spannable) mTextView.getText();
   2096         SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(),
   2097                 mTextView.getSelectionEnd(), SuggestionSpan.class);
   2098         for (int i = 0; i < suggestionSpans.length; i++) {
   2099             if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) {
   2100                 return true;
   2101             }
   2102         }
   2103         return false;
   2104     }
   2105 
   2106     void onTouchUpEvent(MotionEvent event) {
   2107         boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect();
   2108         hideCursorAndSpanControllers();
   2109         stopTextActionMode();
   2110         CharSequence text = mTextView.getText();
   2111         if (!selectAllGotFocus && text.length() > 0) {
   2112             // Move cursor
   2113             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   2114             Selection.setSelection((Spannable) text, offset);
   2115             if (mSpellChecker != null) {
   2116                 // When the cursor moves, the word that was typed may need spell check
   2117                 mSpellChecker.onSelectionChanged();
   2118             }
   2119 
   2120             if (!extractedTextModeWillBeStarted()) {
   2121                 if (isCursorInsideEasyCorrectionSpan()) {
   2122                     // Cancel the single tap delayed runnable.
   2123                     if (mInsertionActionModeRunnable != null) {
   2124                         mTextView.removeCallbacks(mInsertionActionModeRunnable);
   2125                     }
   2126 
   2127                     mShowSuggestionRunnable = new Runnable() {
   2128                         public void run() {
   2129                             replace();
   2130                         }
   2131                     };
   2132                     // removeCallbacks is performed on every touch
   2133                     mTextView.postDelayed(mShowSuggestionRunnable,
   2134                             ViewConfiguration.getDoubleTapTimeout());
   2135                 } else if (hasInsertionController()) {
   2136                     getInsertionController().show();
   2137                 }
   2138             }
   2139         }
   2140     }
   2141 
   2142     protected void stopTextActionMode() {
   2143         if (mTextActionMode != null) {
   2144             // This will hide the mSelectionModifierCursorController
   2145             mTextActionMode.finish();
   2146         }
   2147     }
   2148 
   2149     private void stopTextActionModeWithPreservingSelection() {
   2150         if (mTextActionMode != null) {
   2151             mRestartActionModeOnNextRefresh = true;
   2152         }
   2153         mPreserveSelection = true;
   2154         stopTextActionMode();
   2155         mPreserveSelection = false;
   2156     }
   2157 
   2158     /**
   2159      * @return True if this view supports insertion handles.
   2160      */
   2161     boolean hasInsertionController() {
   2162         return mInsertionControllerEnabled;
   2163     }
   2164 
   2165     /**
   2166      * @return True if this view supports selection handles.
   2167      */
   2168     boolean hasSelectionController() {
   2169         return mSelectionControllerEnabled;
   2170     }
   2171 
   2172     InsertionPointCursorController getInsertionController() {
   2173         if (!mInsertionControllerEnabled) {
   2174             return null;
   2175         }
   2176 
   2177         if (mInsertionPointCursorController == null) {
   2178             mInsertionPointCursorController = new InsertionPointCursorController();
   2179 
   2180             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   2181             observer.addOnTouchModeChangeListener(mInsertionPointCursorController);
   2182         }
   2183 
   2184         return mInsertionPointCursorController;
   2185     }
   2186 
   2187     SelectionModifierCursorController getSelectionController() {
   2188         if (!mSelectionControllerEnabled) {
   2189             return null;
   2190         }
   2191 
   2192         if (mSelectionModifierCursorController == null) {
   2193             mSelectionModifierCursorController = new SelectionModifierCursorController();
   2194 
   2195             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   2196             observer.addOnTouchModeChangeListener(mSelectionModifierCursorController);
   2197         }
   2198 
   2199         return mSelectionModifierCursorController;
   2200     }
   2201 
   2202     /**
   2203      * @hide
   2204      */
   2205     @VisibleForTesting
   2206     public Drawable[] getCursorDrawable() {
   2207         return mCursorDrawable;
   2208     }
   2209 
   2210     private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) {
   2211         if (mCursorDrawable[cursorIndex] == null)
   2212             mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable(
   2213                     mTextView.mCursorDrawableRes);
   2214         final Drawable drawable = mCursorDrawable[cursorIndex];
   2215         final int left = clampHorizontalPosition(drawable, horizontal);
   2216         final int width = drawable.getIntrinsicWidth();
   2217         drawable.setBounds(left, top - mTempRect.top, left + width,
   2218                 bottom + mTempRect.bottom);
   2219     }
   2220 
   2221     /**
   2222      * Return clamped position for the drawable. If the drawable is within the boundaries of the
   2223      * view, then it is offset with the left padding of the cursor drawable. If the drawable is at
   2224      * the beginning or the end of the text then its drawable edge is aligned with left or right of
   2225      * the view boundary. If the drawable is null, horizontal parameter is aligned to left or right
   2226      * of the view.
   2227      *
   2228      * @param drawable Drawable. Can be null.
   2229      * @param horizontal Horizontal position for the drawable.
   2230      * @return The clamped horizontal position for the drawable.
   2231      */
   2232     private int clampHorizontalPosition(@Nullable final Drawable drawable, float horizontal) {
   2233         horizontal = Math.max(0.5f, horizontal - 0.5f);
   2234         if (mTempRect == null) mTempRect = new Rect();
   2235 
   2236         int drawableWidth = 0;
   2237         if (drawable != null) {
   2238             drawable.getPadding(mTempRect);
   2239             drawableWidth = drawable.getIntrinsicWidth();
   2240         } else {
   2241             mTempRect.setEmpty();
   2242         }
   2243 
   2244         int scrollX = mTextView.getScrollX();
   2245         float horizontalDiff = horizontal - scrollX;
   2246         int viewClippedWidth = mTextView.getWidth() - mTextView.getCompoundPaddingLeft()
   2247                 - mTextView.getCompoundPaddingRight();
   2248 
   2249         final int left;
   2250         if (horizontalDiff >= (viewClippedWidth - 1f)) {
   2251             // at the rightmost position
   2252             left = viewClippedWidth + scrollX - (drawableWidth - mTempRect.right);
   2253         } else if (Math.abs(horizontalDiff) <= 1f ||
   2254                 (TextUtils.isEmpty(mTextView.getText())
   2255                         && (TextView.VERY_WIDE - scrollX) <= (viewClippedWidth + 1f)
   2256                         && horizontal <= 1f)) {
   2257             // at the leftmost position
   2258             left = scrollX - mTempRect.left;
   2259         } else {
   2260             left = (int) horizontal - mTempRect.left;
   2261         }
   2262         return left;
   2263     }
   2264 
   2265     /**
   2266      * Called by the framework in response to a text auto-correction (such as fixing a typo using a
   2267      * a dictionary) from the current input method, provided by it calling
   2268      * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default
   2269      * implementation flashes the background of the corrected word to provide feedback to the user.
   2270      *
   2271      * @param info The auto correct info about the text that was corrected.
   2272      */
   2273     public void onCommitCorrection(CorrectionInfo info) {
   2274         if (mCorrectionHighlighter == null) {
   2275             mCorrectionHighlighter = new CorrectionHighlighter();
   2276         } else {
   2277             mCorrectionHighlighter.invalidate(false);
   2278         }
   2279 
   2280         mCorrectionHighlighter.highlight(info);
   2281     }
   2282 
   2283     void onScrollChanged() {
   2284         if (mPositionListener != null) {
   2285             mPositionListener.onScrollChanged();
   2286         }
   2287         if (mTextActionMode != null) {
   2288             mTextActionMode.invalidateContentRect();
   2289         }
   2290     }
   2291 
   2292     /**
   2293      * @return True when the TextView isFocused and has a valid zero-length selection (cursor).
   2294      */
   2295     private boolean shouldBlink() {
   2296         if (!isCursorVisible() || !mTextView.isFocused()) return false;
   2297 
   2298         final int start = mTextView.getSelectionStart();
   2299         if (start < 0) return false;
   2300 
   2301         final int end = mTextView.getSelectionEnd();
   2302         if (end < 0) return false;
   2303 
   2304         return start == end;
   2305     }
   2306 
   2307     void makeBlink() {
   2308         if (shouldBlink()) {
   2309             mShowCursor = SystemClock.uptimeMillis();
   2310             if (mBlink == null) mBlink = new Blink();
   2311             mTextView.removeCallbacks(mBlink);
   2312             mTextView.postDelayed(mBlink, BLINK);
   2313         } else {
   2314             if (mBlink != null) mTextView.removeCallbacks(mBlink);
   2315         }
   2316     }
   2317 
   2318     private class Blink implements Runnable {
   2319         private boolean mCancelled;
   2320 
   2321         public void run() {
   2322             if (mCancelled) {
   2323                 return;
   2324             }
   2325 
   2326             mTextView.removeCallbacks(this);
   2327 
   2328             if (shouldBlink()) {
   2329                 if (mTextView.getLayout() != null) {
   2330                     mTextView.invalidateCursorPath();
   2331                 }
   2332 
   2333                 mTextView.postDelayed(this, BLINK);
   2334             }
   2335         }
   2336 
   2337         void cancel() {
   2338             if (!mCancelled) {
   2339                 mTextView.removeCallbacks(this);
   2340                 mCancelled = true;
   2341             }
   2342         }
   2343 
   2344         void uncancel() {
   2345             mCancelled = false;
   2346         }
   2347     }
   2348 
   2349     private DragShadowBuilder getTextThumbnailBuilder(int start, int end) {
   2350         TextView shadowView = (TextView) View.inflate(mTextView.getContext(),
   2351                 com.android.internal.R.layout.text_drag_thumbnail, null);
   2352 
   2353         if (shadowView == null) {
   2354             throw new IllegalArgumentException("Unable to inflate text drag thumbnail");
   2355         }
   2356 
   2357         if (end - start > DRAG_SHADOW_MAX_TEXT_LENGTH) {
   2358             final long range = getCharClusterRange(start + DRAG_SHADOW_MAX_TEXT_LENGTH);
   2359             end = TextUtils.unpackRangeEndFromLong(range);
   2360         }
   2361         final CharSequence text = mTextView.getTransformedText(start, end);
   2362         shadowView.setText(text);
   2363         shadowView.setTextColor(mTextView.getTextColors());
   2364 
   2365         shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge);
   2366         shadowView.setGravity(Gravity.CENTER);
   2367 
   2368         shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   2369                 ViewGroup.LayoutParams.WRAP_CONTENT));
   2370 
   2371         final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED);
   2372         shadowView.measure(size, size);
   2373 
   2374         shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight());
   2375         shadowView.invalidate();
   2376         return new DragShadowBuilder(shadowView);
   2377     }
   2378 
   2379     private static class DragLocalState {
   2380         public TextView sourceTextView;
   2381         public int start, end;
   2382 
   2383         public DragLocalState(TextView sourceTextView, int start, int end) {
   2384             this.sourceTextView = sourceTextView;
   2385             this.start = start;
   2386             this.end = end;
   2387         }
   2388     }
   2389 
   2390     void onDrop(DragEvent event) {
   2391         StringBuilder content = new StringBuilder("");
   2392 
   2393         final DragAndDropPermissions permissions = DragAndDropPermissions.obtain(event);
   2394         if (permissions != null) {
   2395             permissions.takeTransient();
   2396         }
   2397 
   2398         try {
   2399             ClipData clipData = event.getClipData();
   2400             final int itemCount = clipData.getItemCount();
   2401             for (int i=0; i < itemCount; i++) {
   2402                 Item item = clipData.getItemAt(i);
   2403                 content.append(item.coerceToStyledText(mTextView.getContext()));
   2404             }
   2405         }
   2406         finally {
   2407             if (permissions != null) {
   2408                 permissions.release();
   2409             }
   2410         }
   2411 
   2412         final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   2413 
   2414         Object localState = event.getLocalState();
   2415         DragLocalState dragLocalState = null;
   2416         if (localState instanceof DragLocalState) {
   2417             dragLocalState = (DragLocalState) localState;
   2418         }
   2419         boolean dragDropIntoItself = dragLocalState != null &&
   2420                 dragLocalState.sourceTextView == mTextView;
   2421 
   2422         if (dragDropIntoItself) {
   2423             if (offset >= dragLocalState.start && offset < dragLocalState.end) {
   2424                 // A drop inside the original selection discards the drop.
   2425                 return;
   2426             }
   2427         }
   2428 
   2429         final int originalLength = mTextView.getText().length();
   2430         int min = offset;
   2431         int max = offset;
   2432 
   2433         Selection.setSelection((Spannable) mTextView.getText(), max);
   2434         mTextView.replaceText_internal(min, max, content);
   2435 
   2436         if (dragDropIntoItself) {
   2437             int dragSourceStart = dragLocalState.start;
   2438             int dragSourceEnd = dragLocalState.end;
   2439             if (max <= dragSourceStart) {
   2440                 // Inserting text before selection has shifted positions
   2441                 final int shift = mTextView.getText().length() - originalLength;
   2442                 dragSourceStart += shift;
   2443                 dragSourceEnd += shift;
   2444             }
   2445 
   2446             mUndoInputFilter.setForceMerge(true);
   2447             try {
   2448                 // Delete original selection
   2449                 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
   2450 
   2451                 // Make sure we do not leave two adjacent spaces.
   2452                 final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
   2453                 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
   2454                 if (nextCharIdx > prevCharIdx + 1) {
   2455                     CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
   2456                     if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
   2457                         mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
   2458                     }
   2459                 }
   2460             } finally {
   2461                 mUndoInputFilter.setForceMerge(false);
   2462             }
   2463         }
   2464     }
   2465 
   2466     public void addSpanWatchers(Spannable text) {
   2467         final int textLength = text.length();
   2468 
   2469         if (mKeyListener != null) {
   2470             text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
   2471         }
   2472 
   2473         if (mSpanController == null) {
   2474             mSpanController = new SpanController();
   2475         }
   2476         text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
   2477     }
   2478 
   2479     void setContextMenuAnchor(float x, float y) {
   2480         mContextMenuAnchorX = x;
   2481         mContextMenuAnchorY = y;
   2482     }
   2483 
   2484     void onCreateContextMenu(ContextMenu menu) {
   2485         if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
   2486                 || Float.isNaN(mContextMenuAnchorY)) {
   2487             return;
   2488         }
   2489         final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
   2490         if (offset == -1) {
   2491             return;
   2492         }
   2493         stopTextActionModeWithPreservingSelection();
   2494         final boolean isOnSelection = mTextView.hasSelection()
   2495                 && offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd();
   2496         if (!isOnSelection) {
   2497             // Right clicked position is not on the selection. Remove the selection and move the
   2498             // cursor to the right clicked position.
   2499             Selection.setSelection((Spannable) mTextView.getText(), offset);
   2500             stopTextActionMode();
   2501         }
   2502 
   2503         if (shouldOfferToShowSuggestions()) {
   2504             final SuggestionInfo[] suggestionInfoArray =
   2505                     new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
   2506             for (int i = 0; i < suggestionInfoArray.length; i++) {
   2507                 suggestionInfoArray[i] = new SuggestionInfo();
   2508             }
   2509             final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
   2510                     com.android.internal.R.string.replace);
   2511             final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
   2512             for (int i = 0; i < numItems; i++) {
   2513                 final SuggestionInfo info = suggestionInfoArray[i];
   2514                 subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
   2515                         .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
   2516                             @Override
   2517                             public boolean onMenuItemClick(MenuItem item) {
   2518                                 replaceWithSuggestion(info);
   2519                                 return true;
   2520                             }
   2521                         });
   2522             }
   2523         }
   2524 
   2525         menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
   2526                 com.android.internal.R.string.undo)
   2527                 .setAlphabeticShortcut('z')
   2528                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
   2529                 .setEnabled(mTextView.canUndo());
   2530         menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
   2531                 com.android.internal.R.string.redo)
   2532                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
   2533                 .setEnabled(mTextView.canRedo());
   2534 
   2535         menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
   2536                 com.android.internal.R.string.cut)
   2537                 .setAlphabeticShortcut('x')
   2538                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
   2539                 .setEnabled(mTextView.canCut());
   2540         menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
   2541                 com.android.internal.R.string.copy)
   2542                 .setAlphabeticShortcut('c')
   2543                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
   2544                 .setEnabled(mTextView.canCopy());
   2545         menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
   2546                 com.android.internal.R.string.paste)
   2547                 .setAlphabeticShortcut('v')
   2548                 .setEnabled(mTextView.canPaste())
   2549                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
   2550         menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
   2551                 com.android.internal.R.string.paste_as_plain_text)
   2552                 .setEnabled(mTextView.canPaste())
   2553                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
   2554         menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
   2555                 com.android.internal.R.string.share)
   2556                 .setEnabled(mTextView.canShare())
   2557                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
   2558         menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
   2559                 com.android.internal.R.string.selectAll)
   2560                 .setAlphabeticShortcut('a')
   2561                 .setEnabled(mTextView.canSelectAllText())
   2562                 .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
   2563 
   2564         mPreserveSelection = true;
   2565     }
   2566 
   2567     @Nullable
   2568     private SuggestionSpan findEquivalentSuggestionSpan(
   2569             @NonNull SuggestionSpanInfo suggestionSpanInfo) {
   2570         final Editable editable = (Editable) mTextView.getText();
   2571         if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
   2572             // Exactly same span is found.
   2573             return suggestionSpanInfo.mSuggestionSpan;
   2574         }
   2575         // Suggestion span couldn't be found. Try to find a suggestion span that has the same
   2576         // contents.
   2577         final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
   2578                 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
   2579         for (final SuggestionSpan suggestionSpan : suggestionSpans) {
   2580             final int start = editable.getSpanStart(suggestionSpan);
   2581             if (start != suggestionSpanInfo.mSpanStart) {
   2582                 continue;
   2583             }
   2584             final int end = editable.getSpanEnd(suggestionSpan);
   2585             if (end != suggestionSpanInfo.mSpanEnd) {
   2586                 continue;
   2587             }
   2588             if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
   2589                 return suggestionSpan;
   2590             }
   2591         }
   2592         return null;
   2593     }
   2594 
   2595     private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
   2596         final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
   2597                 suggestionInfo.mSuggestionSpanInfo);
   2598         if (targetSuggestionSpan == null) {
   2599             // Span has been removed
   2600             return;
   2601         }
   2602         final Editable editable = (Editable) mTextView.getText();
   2603         final int spanStart = editable.getSpanStart(targetSuggestionSpan);
   2604         final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
   2605         if (spanStart < 0 || spanEnd <= spanStart) {
   2606             // Span has been removed
   2607             return;
   2608         }
   2609 
   2610         final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
   2611         // SuggestionSpans are removed by replace: save them before
   2612         SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
   2613                 SuggestionSpan.class);
   2614         final int length = suggestionSpans.length;
   2615         int[] suggestionSpansStarts = new int[length];
   2616         int[] suggestionSpansEnds = new int[length];
   2617         int[] suggestionSpansFlags = new int[length];
   2618         for (int i = 0; i < length; i++) {
   2619             final SuggestionSpan suggestionSpan = suggestionSpans[i];
   2620             suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
   2621             suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
   2622             suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
   2623 
   2624             // Remove potential misspelled flags
   2625             int suggestionSpanFlags = suggestionSpan.getFlags();
   2626             if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
   2627                 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
   2628                 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
   2629                 suggestionSpan.setFlags(suggestionSpanFlags);
   2630             }
   2631         }
   2632 
   2633         // Notify source IME of the suggestion pick. Do this before swapping texts.
   2634         targetSuggestionSpan.notifySelection(
   2635                 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
   2636 
   2637         // Swap text content between actual text and Suggestion span
   2638         final int suggestionStart = suggestionInfo.mSuggestionStart;
   2639         final int suggestionEnd = suggestionInfo.mSuggestionEnd;
   2640         final String suggestion = suggestionInfo.mText.subSequence(
   2641                 suggestionStart, suggestionEnd).toString();
   2642         mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
   2643 
   2644         String[] suggestions = targetSuggestionSpan.getSuggestions();
   2645         suggestions[suggestionInfo.mSuggestionIndex] = originalText;
   2646 
   2647         // Restore previous SuggestionSpans
   2648         final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
   2649         for (int i = 0; i < length; i++) {
   2650             // Only spans that include the modified region make sense after replacement
   2651             // Spans partially included in the replaced region are removed, there is no
   2652             // way to assign them a valid range after replacement
   2653             if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
   2654                 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
   2655                         suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
   2656             }
   2657         }
   2658         // Move cursor at the end of the replaced word
   2659         final int newCursorPosition = spanEnd + lengthDelta;
   2660         mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
   2661     }
   2662 
   2663     private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
   2664             new MenuItem.OnMenuItemClickListener() {
   2665         @Override
   2666         public boolean onMenuItemClick(MenuItem item) {
   2667             if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
   2668                 return true;
   2669             }
   2670             return mTextView.onTextContextMenuItem(item.getItemId());
   2671         }
   2672     };
   2673 
   2674     /**
   2675      * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
   2676      * pop-up should be displayed.
   2677      * Also monitors {@link Selection} to call back to the attached input method.
   2678      */
   2679     class SpanController implements SpanWatcher {
   2680 
   2681         private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
   2682 
   2683         private EasyEditPopupWindow mPopupWindow;
   2684 
   2685         private Runnable mHidePopup;
   2686 
   2687         // This function is pure but inner classes can't have static functions
   2688         private boolean isNonIntermediateSelectionSpan(final Spannable text,
   2689                 final Object span) {
   2690             return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
   2691                     && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
   2692         }
   2693 
   2694         @Override
   2695         public void onSpanAdded(Spannable text, Object span, int start, int end) {
   2696             if (isNonIntermediateSelectionSpan(text, span)) {
   2697                 sendUpdateSelection();
   2698             } else if (span instanceof EasyEditSpan) {
   2699                 if (mPopupWindow == null) {
   2700                     mPopupWindow = new EasyEditPopupWindow();
   2701                     mHidePopup = new Runnable() {
   2702                         @Override
   2703                         public void run() {
   2704                             hide();
   2705                         }
   2706                     };
   2707                 }
   2708 
   2709                 // Make sure there is only at most one EasyEditSpan in the text
   2710                 if (mPopupWindow.mEasyEditSpan != null) {
   2711                     mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
   2712                 }
   2713 
   2714                 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
   2715                 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
   2716                     @Override
   2717                     public void onDeleteClick(EasyEditSpan span) {
   2718                         Editable editable = (Editable) mTextView.getText();
   2719                         int start = editable.getSpanStart(span);
   2720                         int end = editable.getSpanEnd(span);
   2721                         if (start >= 0 && end >= 0) {
   2722                             sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
   2723                             mTextView.deleteText_internal(start, end);
   2724                         }
   2725                         editable.removeSpan(span);
   2726                     }
   2727                 });
   2728 
   2729                 if (mTextView.getWindowVisibility() != View.VISIBLE) {
   2730                     // The window is not visible yet, ignore the text change.
   2731                     return;
   2732                 }
   2733 
   2734                 if (mTextView.getLayout() == null) {
   2735                     // The view has not been laid out yet, ignore the text change
   2736                     return;
   2737                 }
   2738 
   2739                 if (extractedTextModeWillBeStarted()) {
   2740                     // The input is in extract mode. Do not handle the easy edit in
   2741                     // the original TextView, as the ExtractEditText will do
   2742                     return;
   2743                 }
   2744 
   2745                 mPopupWindow.show();
   2746                 mTextView.removeCallbacks(mHidePopup);
   2747                 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
   2748             }
   2749         }
   2750 
   2751         @Override
   2752         public void onSpanRemoved(Spannable text, Object span, int start, int end) {
   2753             if (isNonIntermediateSelectionSpan(text, span)) {
   2754                 sendUpdateSelection();
   2755             } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
   2756                 hide();
   2757             }
   2758         }
   2759 
   2760         @Override
   2761         public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
   2762                 int newStart, int newEnd) {
   2763             if (isNonIntermediateSelectionSpan(text, span)) {
   2764                 sendUpdateSelection();
   2765             } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
   2766                 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
   2767                 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
   2768                 text.removeSpan(easyEditSpan);
   2769             }
   2770         }
   2771 
   2772         public void hide() {
   2773             if (mPopupWindow != null) {
   2774                 mPopupWindow.hide();
   2775                 mTextView.removeCallbacks(mHidePopup);
   2776             }
   2777         }
   2778 
   2779         private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
   2780             try {
   2781                 PendingIntent pendingIntent = span.getPendingIntent();
   2782                 if (pendingIntent != null) {
   2783                     Intent intent = new Intent();
   2784                     intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
   2785                     pendingIntent.send(mTextView.getContext(), 0, intent);
   2786                 }
   2787             } catch (CanceledException e) {
   2788                 // This should not happen, as we should try to send the intent only once.
   2789                 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
   2790             }
   2791         }
   2792     }
   2793 
   2794     /**
   2795      * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
   2796      */
   2797     private interface EasyEditDeleteListener {
   2798 
   2799         /**
   2800          * Clicks the delete pop-up.
   2801          */
   2802         void onDeleteClick(EasyEditSpan span);
   2803     }
   2804 
   2805     /**
   2806      * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
   2807      * by {@link SpanController}.
   2808      */
   2809     private class EasyEditPopupWindow extends PinnedPopupWindow
   2810             implements OnClickListener {
   2811         private static final int POPUP_TEXT_LAYOUT =
   2812                 com.android.internal.R.layout.text_edit_action_popup_text;
   2813         private TextView mDeleteTextView;
   2814         private EasyEditSpan mEasyEditSpan;
   2815         private EasyEditDeleteListener mOnDeleteListener;
   2816 
   2817         @Override
   2818         protected void createPopupWindow() {
   2819             mPopupWindow = new PopupWindow(mTextView.getContext(), null,
   2820                     com.android.internal.R.attr.textSelectHandleWindowStyle);
   2821             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
   2822             mPopupWindow.setClippingEnabled(true);
   2823         }
   2824 
   2825         @Override
   2826         protected void initContentView() {
   2827             LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
   2828             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
   2829             mContentView = linearLayout;
   2830             mContentView.setBackgroundResource(
   2831                     com.android.internal.R.drawable.text_edit_side_paste_window);
   2832 
   2833             LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
   2834                     getSystemService(Context.LAYOUT_INFLATER_SERVICE);
   2835 
   2836             LayoutParams wrapContent = new LayoutParams(
   2837                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
   2838 
   2839             mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
   2840             mDeleteTextView.setLayoutParams(wrapContent);
   2841             mDeleteTextView.setText(com.android.internal.R.string.delete);
   2842             mDeleteTextView.setOnClickListener(this);
   2843             mContentView.addView(mDeleteTextView);
   2844         }
   2845 
   2846         public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
   2847             mEasyEditSpan = easyEditSpan;
   2848         }
   2849 
   2850         private void setOnDeleteListener(EasyEditDeleteListener listener) {
   2851             mOnDeleteListener = listener;
   2852         }
   2853 
   2854         @Override
   2855         public void onClick(View view) {
   2856             if (view == mDeleteTextView
   2857                     && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
   2858                     && mOnDeleteListener != null) {
   2859                 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
   2860             }
   2861         }
   2862 
   2863         @Override
   2864         public void hide() {
   2865             if (mEasyEditSpan != null) {
   2866                 mEasyEditSpan.setDeleteEnabled(false);
   2867             }
   2868             mOnDeleteListener = null;
   2869             super.hide();
   2870         }
   2871 
   2872         @Override
   2873         protected int getTextOffset() {
   2874             // Place the pop-up at the end of the span
   2875             Editable editable = (Editable) mTextView.getText();
   2876             return editable.getSpanEnd(mEasyEditSpan);
   2877         }
   2878 
   2879         @Override
   2880         protected int getVerticalLocalPosition(int line) {
   2881             return mTextView.getLayout().getLineBottom(line);
   2882         }
   2883 
   2884         @Override
   2885         protected int clipVertically(int positionY) {
   2886             // As we display the pop-up below the span, no vertical clipping is required.
   2887             return positionY;
   2888         }
   2889     }
   2890 
   2891     private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
   2892         // 3 handles
   2893         // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
   2894         // 1 CursorAnchorInfoNotifier
   2895         private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
   2896         private TextViewPositionListener[] mPositionListeners =
   2897                 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
   2898         private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
   2899         private boolean mPositionHasChanged = true;
   2900         // Absolute position of the TextView with respect to its parent window
   2901         private int mPositionX, mPositionY;
   2902         private int mPositionXOnScreen, mPositionYOnScreen;
   2903         private int mNumberOfListeners;
   2904         private boolean mScrollHasChanged;
   2905         final int[] mTempCoords = new int[2];
   2906 
   2907         public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
   2908             if (mNumberOfListeners == 0) {
   2909                 updatePosition();
   2910                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
   2911                 vto.addOnPreDrawListener(this);
   2912             }
   2913 
   2914             int emptySlotIndex = -1;
   2915             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2916                 TextViewPositionListener listener = mPositionListeners[i];
   2917                 if (listener == positionListener) {
   2918                     return;
   2919                 } else if (emptySlotIndex < 0 && listener == null) {
   2920                     emptySlotIndex = i;
   2921                 }
   2922             }
   2923 
   2924             mPositionListeners[emptySlotIndex] = positionListener;
   2925             mCanMove[emptySlotIndex] = canMove;
   2926             mNumberOfListeners++;
   2927         }
   2928 
   2929         public void removeSubscriber(TextViewPositionListener positionListener) {
   2930             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2931                 if (mPositionListeners[i] == positionListener) {
   2932                     mPositionListeners[i] = null;
   2933                     mNumberOfListeners--;
   2934                     break;
   2935                 }
   2936             }
   2937 
   2938             if (mNumberOfListeners == 0) {
   2939                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
   2940                 vto.removeOnPreDrawListener(this);
   2941             }
   2942         }
   2943 
   2944         public int getPositionX() {
   2945             return mPositionX;
   2946         }
   2947 
   2948         public int getPositionY() {
   2949             return mPositionY;
   2950         }
   2951 
   2952         public int getPositionXOnScreen() {
   2953             return mPositionXOnScreen;
   2954         }
   2955 
   2956         public int getPositionYOnScreen() {
   2957             return mPositionYOnScreen;
   2958         }
   2959 
   2960         @Override
   2961         public boolean onPreDraw() {
   2962             updatePosition();
   2963 
   2964             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
   2965                 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
   2966                     TextViewPositionListener positionListener = mPositionListeners[i];
   2967                     if (positionListener != null) {
   2968                         positionListener.updatePosition(mPositionX, mPositionY,
   2969                                 mPositionHasChanged, mScrollHasChanged);
   2970                     }
   2971                 }
   2972             }
   2973 
   2974             mScrollHasChanged = false;
   2975             return true;
   2976         }
   2977 
   2978         private void updatePosition() {
   2979             mTextView.getLocationInWindow(mTempCoords);
   2980 
   2981             mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
   2982 
   2983             mPositionX = mTempCoords[0];
   2984             mPositionY = mTempCoords[1];
   2985 
   2986             mTextView.getLocationOnScreen(mTempCoords);
   2987 
   2988             mPositionXOnScreen = mTempCoords[0];
   2989             mPositionYOnScreen = mTempCoords[1];
   2990         }
   2991 
   2992         public void onScrollChanged() {
   2993             mScrollHasChanged = true;
   2994         }
   2995     }
   2996 
   2997     private abstract class PinnedPopupWindow implements TextViewPositionListener {
   2998         protected PopupWindow mPopupWindow;
   2999         protected ViewGroup mContentView;
   3000         int mPositionX, mPositionY;
   3001         int mClippingLimitLeft, mClippingLimitRight;
   3002 
   3003         protected abstract void createPopupWindow();
   3004         protected abstract void initContentView();
   3005         protected abstract int getTextOffset();
   3006         protected abstract int getVerticalLocalPosition(int line);
   3007         protected abstract int clipVertically(int positionY);
   3008         protected void setUp() {
   3009         }
   3010 
   3011         public PinnedPopupWindow() {
   3012             // Due to calling subclass methods in base constructor, subclass constructor is not
   3013             // called before subclass methods, e.g. createPopupWindow or initContentView. To give
   3014             // a chance to initialize subclasses, call setUp() method here.
   3015             // TODO: It is good to extract non trivial initialization code from constructor.
   3016             setUp();
   3017 
   3018             createPopupWindow();
   3019 
   3020             mPopupWindow.setWindowLayoutType(
   3021                     WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
   3022             mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
   3023             mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
   3024 
   3025             initContentView();
   3026 
   3027             LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
   3028                     ViewGroup.LayoutParams.WRAP_CONTENT);
   3029             mContentView.setLayoutParams(wrapContent);
   3030 
   3031             mPopupWindow.setContentView(mContentView);
   3032         }
   3033 
   3034         public void show() {
   3035             getPositionListener().addSubscriber(this, false /* offset is fixed */);
   3036 
   3037             computeLocalPosition();
   3038 
   3039             final PositionListener positionListener = getPositionListener();
   3040             updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
   3041         }
   3042 
   3043         protected void measureContent() {
   3044             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   3045             mContentView.measure(
   3046                     View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
   3047                             View.MeasureSpec.AT_MOST),
   3048                     View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
   3049                             View.MeasureSpec.AT_MOST));
   3050         }
   3051 
   3052         /* The popup window will be horizontally centered on the getTextOffset() and vertically
   3053          * positioned according to viewportToContentHorizontalOffset.
   3054          *
   3055          * This method assumes that mContentView has properly been measured from its content. */
   3056         private void computeLocalPosition() {
   3057             measureContent();
   3058             final int width = mContentView.getMeasuredWidth();
   3059             final int offset = getTextOffset();
   3060             mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
   3061             mPositionX += mTextView.viewportToContentHorizontalOffset();
   3062 
   3063             final int line = mTextView.getLayout().getLineForOffset(offset);
   3064             mPositionY = getVerticalLocalPosition(line);
   3065             mPositionY += mTextView.viewportToContentVerticalOffset();
   3066         }
   3067 
   3068         private void updatePosition(int parentPositionX, int parentPositionY) {
   3069             int positionX = parentPositionX + mPositionX;
   3070             int positionY = parentPositionY + mPositionY;
   3071 
   3072             positionY = clipVertically(positionY);
   3073 
   3074             // Horizontal clipping
   3075             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   3076             final int width = mContentView.getMeasuredWidth();
   3077             positionX = Math.min(
   3078                     displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
   3079             positionX = Math.max(-mClippingLimitLeft, positionX);
   3080 
   3081             if (isShowing()) {
   3082                 mPopupWindow.update(positionX, positionY, -1, -1);
   3083             } else {
   3084                 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
   3085                         positionX, positionY);
   3086             }
   3087         }
   3088 
   3089         public void hide() {
   3090             if (!isShowing()) {
   3091                 return;
   3092             }
   3093             mPopupWindow.dismiss();
   3094             getPositionListener().removeSubscriber(this);
   3095         }
   3096 
   3097         @Override
   3098         public void updatePosition(int parentPositionX, int parentPositionY,
   3099                 boolean parentPositionChanged, boolean parentScrolled) {
   3100             // Either parentPositionChanged or parentScrolled is true, check if still visible
   3101             if (isShowing() && isOffsetVisible(getTextOffset())) {
   3102                 if (parentScrolled) computeLocalPosition();
   3103                 updatePosition(parentPositionX, parentPositionY);
   3104             } else {
   3105                 hide();
   3106             }
   3107         }
   3108 
   3109         public boolean isShowing() {
   3110             return mPopupWindow.isShowing();
   3111         }
   3112     }
   3113 
   3114     private static final class SuggestionInfo {
   3115         // Range of actual suggestion within mText
   3116         int mSuggestionStart, mSuggestionEnd;
   3117 
   3118         // The SuggestionSpan that this TextView represents
   3119         final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
   3120 
   3121         // The index of this suggestion inside suggestionSpan
   3122         int mSuggestionIndex;
   3123 
   3124         final SpannableStringBuilder mText = new SpannableStringBuilder();
   3125 
   3126         void clear() {
   3127             mSuggestionSpanInfo.clear();
   3128             mText.clear();
   3129         }
   3130 
   3131         // Utility method to set attributes about a SuggestionSpan.
   3132         void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
   3133             mSuggestionSpanInfo.mSuggestionSpan = span;
   3134             mSuggestionSpanInfo.mSpanStart = spanStart;
   3135             mSuggestionSpanInfo.mSpanEnd = spanEnd;
   3136         }
   3137     }
   3138 
   3139     private static final class SuggestionSpanInfo {
   3140         // The SuggestionSpan;
   3141         @Nullable
   3142         SuggestionSpan mSuggestionSpan;
   3143 
   3144         // The SuggestionSpan start position
   3145         int mSpanStart;
   3146 
   3147         // The SuggestionSpan end position
   3148         int mSpanEnd;
   3149 
   3150         void clear() {
   3151             mSuggestionSpan = null;
   3152         }
   3153     }
   3154 
   3155     private class SuggestionHelper {
   3156         private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
   3157                 new SuggestionSpanComparator();
   3158         private final HashMap<SuggestionSpan, Integer> mSpansLengths =
   3159                 new HashMap<SuggestionSpan, Integer>();
   3160 
   3161         private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
   3162             public int compare(SuggestionSpan span1, SuggestionSpan span2) {
   3163                 final int flag1 = span1.getFlags();
   3164                 final int flag2 = span2.getFlags();
   3165                 if (flag1 != flag2) {
   3166                     // The order here should match what is used in updateDrawState
   3167                     final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
   3168                     final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
   3169                     final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
   3170                     final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
   3171                     if (easy1 && !misspelled1) return -1;
   3172                     if (easy2 && !misspelled2) return 1;
   3173                     if (misspelled1) return -1;
   3174                     if (misspelled2) return 1;
   3175                 }
   3176 
   3177                 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
   3178             }
   3179         }
   3180 
   3181         /**
   3182          * Returns the suggestion spans that cover the current cursor position. The suggestion
   3183          * spans are sorted according to the length of text that they are attached to.
   3184          */
   3185         private SuggestionSpan[] getSortedSuggestionSpans() {
   3186             int pos = mTextView.getSelectionStart();
   3187             Spannable spannable = (Spannable) mTextView.getText();
   3188             SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
   3189 
   3190             mSpansLengths.clear();
   3191             for (SuggestionSpan suggestionSpan : suggestionSpans) {
   3192                 int start = spannable.getSpanStart(suggestionSpan);
   3193                 int end = spannable.getSpanEnd(suggestionSpan);
   3194                 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
   3195             }
   3196 
   3197             // The suggestions are sorted according to their types (easy correction first, then
   3198             // misspelled) and to the length of the text that they cover (shorter first).
   3199             Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
   3200             mSpansLengths.clear();
   3201 
   3202             return suggestionSpans;
   3203         }
   3204 
   3205         /**
   3206          * Gets the SuggestionInfo list that contains suggestion information at the current cursor
   3207          * position.
   3208          *
   3209          * @param suggestionInfos SuggestionInfo array the results will be set.
   3210          * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
   3211          * @return the number of suggestions actually fetched.
   3212          */
   3213         public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
   3214                 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
   3215             final Spannable spannable = (Spannable) mTextView.getText();
   3216             final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
   3217             final int nbSpans = suggestionSpans.length;
   3218             if (nbSpans == 0) return 0;
   3219 
   3220             int numberOfSuggestions = 0;
   3221             for (final SuggestionSpan suggestionSpan : suggestionSpans) {
   3222                 final int spanStart = spannable.getSpanStart(suggestionSpan);
   3223                 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
   3224 
   3225                 if (misspelledSpanInfo != null
   3226                         && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
   3227                     misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
   3228                     misspelledSpanInfo.mSpanStart = spanStart;
   3229                     misspelledSpanInfo.mSpanEnd = spanEnd;
   3230                 }
   3231 
   3232                 final String[] suggestions = suggestionSpan.getSuggestions();
   3233                 final int nbSuggestions = suggestions.length;
   3234                 suggestionLoop:
   3235                 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
   3236                     final String suggestion = suggestions[suggestionIndex];
   3237                     for (int i = 0; i < numberOfSuggestions; i++) {
   3238                         final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
   3239                         if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
   3240                             final int otherSpanStart =
   3241                                     otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
   3242                             final int otherSpanEnd =
   3243                                     otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
   3244                             if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
   3245                                 continue suggestionLoop;
   3246                             }
   3247                         }
   3248                     }
   3249 
   3250                     SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
   3251                     suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
   3252                     suggestionInfo.mSuggestionIndex = suggestionIndex;
   3253                     suggestionInfo.mSuggestionStart = 0;
   3254                     suggestionInfo.mSuggestionEnd = suggestion.length();
   3255                     suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
   3256                     numberOfSuggestions++;
   3257                     if (numberOfSuggestions >= suggestionInfos.length) {
   3258                         return numberOfSuggestions;
   3259                     }
   3260                 }
   3261             }
   3262             return numberOfSuggestions;
   3263         }
   3264     }
   3265 
   3266     @VisibleForTesting
   3267     public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
   3268         private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
   3269 
   3270         // Key of intent extras for inserting new word into user dictionary.
   3271         private static final String USER_DICTIONARY_EXTRA_WORD = "word";
   3272         private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
   3273 
   3274         private SuggestionInfo[] mSuggestionInfos;
   3275         private int mNumberOfSuggestions;
   3276         private boolean mCursorWasVisibleBeforeSuggestions;
   3277         private boolean mIsShowingUp = false;
   3278         private SuggestionAdapter mSuggestionsAdapter;
   3279         private TextAppearanceSpan mHighlightSpan;  // TODO: Make mHighlightSpan final.
   3280         private TextView mAddToDictionaryButton;
   3281         private TextView mDeleteButton;
   3282         private ListView mSuggestionListView;
   3283         private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
   3284         private int mContainerMarginWidth;
   3285         private int mContainerMarginTop;
   3286         private LinearLayout mContainerView;
   3287         private Context mContext;  // TODO: Make mContext final.
   3288 
   3289         private class CustomPopupWindow extends PopupWindow {
   3290 
   3291             @Override
   3292             public void dismiss() {
   3293                 if (!isShowing()) {
   3294                     return;
   3295                 }
   3296                 super.dismiss();
   3297                 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
   3298 
   3299                 // Safe cast since show() checks that mTextView.getText() is an Editable
   3300                 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
   3301 
   3302                 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
   3303                 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
   3304                     getInsertionController().show();
   3305                 }
   3306             }
   3307         }
   3308 
   3309         public SuggestionsPopupWindow() {
   3310             mCursorWasVisibleBeforeSuggestions = mCursorVisible;
   3311         }
   3312 
   3313         @Override
   3314         protected void setUp() {
   3315             mContext = applyDefaultTheme(mTextView.getContext());
   3316             mHighlightSpan = new TextAppearanceSpan(mContext,
   3317                     mTextView.mTextEditSuggestionHighlightStyle);
   3318         }
   3319 
   3320         private Context applyDefaultTheme(Context originalContext) {
   3321             TypedArray a = originalContext.obtainStyledAttributes(
   3322                     new int[]{com.android.internal.R.attr.isLightTheme});
   3323             boolean isLightTheme = a.getBoolean(0, true);
   3324             int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
   3325                     : R.style.ThemeOverlay_Material_Dark;
   3326             a.recycle();
   3327             return new ContextThemeWrapper(originalContext, themeId);
   3328         }
   3329 
   3330         @Override
   3331         protected void createPopupWindow() {
   3332             mPopupWindow = new CustomPopupWindow();
   3333             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
   3334             mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
   3335             mPopupWindow.setFocusable(true);
   3336             mPopupWindow.setClippingEnabled(false);
   3337         }
   3338 
   3339         @Override
   3340         protected void initContentView() {
   3341             final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
   3342                     Context.LAYOUT_INFLATER_SERVICE);
   3343             mContentView = (ViewGroup) inflater.inflate(
   3344                     mTextView.mTextEditSuggestionContainerLayout, null);
   3345 
   3346             mContainerView = (LinearLayout) mContentView.findViewById(
   3347                     com.android.internal.R.id.suggestionWindowContainer);
   3348             ViewGroup.MarginLayoutParams lp =
   3349                     (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
   3350             mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
   3351             mContainerMarginTop = lp.topMargin;
   3352             mClippingLimitLeft = lp.leftMargin;
   3353             mClippingLimitRight = lp.rightMargin;
   3354 
   3355             mSuggestionListView = (ListView) mContentView.findViewById(
   3356                     com.android.internal.R.id.suggestionContainer);
   3357 
   3358             mSuggestionsAdapter = new SuggestionAdapter();
   3359             mSuggestionListView.setAdapter(mSuggestionsAdapter);
   3360             mSuggestionListView.setOnItemClickListener(this);
   3361 
   3362             // Inflate the suggestion items once and for all.
   3363             mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
   3364             for (int i = 0; i < mSuggestionInfos.length; i++) {
   3365                 mSuggestionInfos[i] = new SuggestionInfo();
   3366             }
   3367 
   3368             mAddToDictionaryButton = (TextView) mContentView.findViewById(
   3369                     com.android.internal.R.id.addToDictionaryButton);
   3370             mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
   3371                 public void onClick(View v) {
   3372                     final SuggestionSpan misspelledSpan =
   3373                             findEquivalentSuggestionSpan(mMisspelledSpanInfo);
   3374                     if (misspelledSpan == null) {
   3375                         // Span has been removed.
   3376                         return;
   3377                     }
   3378                     final Editable editable = (Editable) mTextView.getText();
   3379                     final int spanStart = editable.getSpanStart(misspelledSpan);
   3380                     final int spanEnd = editable.getSpanEnd(misspelledSpan);
   3381                     if (spanStart < 0 || spanEnd <= spanStart) {
   3382                         return;
   3383                     }
   3384                     final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
   3385 
   3386                     final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
   3387                     intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
   3388                     intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
   3389                             mTextView.getTextServicesLocale().toString());
   3390                     intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
   3391                     mTextView.getContext().startActivity(intent);
   3392                     // There is no way to know if the word was indeed added. Re-check.
   3393                     // TODO The ExtractEditText should remove the span in the original text instead
   3394                     editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
   3395                     Selection.setSelection(editable, spanEnd);
   3396                     updateSpellCheckSpans(spanStart, spanEnd, false);
   3397                     hideWithCleanUp();
   3398                 }
   3399             });
   3400 
   3401             mDeleteButton = (TextView) mContentView.findViewById(
   3402                     com.android.internal.R.id.deleteButton);
   3403             mDeleteButton.setOnClickListener(new View.OnClickListener() {
   3404                 public void onClick(View v) {
   3405                     final Editable editable = (Editable) mTextView.getText();
   3406 
   3407                     final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
   3408                     int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
   3409                     if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
   3410                         // Do not leave two adjacent spaces after deletion, or one at beginning of
   3411                         // text
   3412                         if (spanUnionEnd < editable.length() &&
   3413                                 Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
   3414                                 (spanUnionStart == 0 ||
   3415                                 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
   3416                             spanUnionEnd = spanUnionEnd + 1;
   3417                         }
   3418                         mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
   3419                     }
   3420                     hideWithCleanUp();
   3421                 }
   3422             });
   3423 
   3424         }
   3425 
   3426         public boolean isShowingUp() {
   3427             return mIsShowingUp;
   3428         }
   3429 
   3430         public void onParentLostFocus() {
   3431             mIsShowingUp = false;
   3432         }
   3433 
   3434         private class SuggestionAdapter extends BaseAdapter {
   3435             private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
   3436                     Context.LAYOUT_INFLATER_SERVICE);
   3437 
   3438             @Override
   3439             public int getCount() {
   3440                 return mNumberOfSuggestions;
   3441             }
   3442 
   3443             @Override
   3444             public Object getItem(int position) {
   3445                 return mSuggestionInfos[position];
   3446             }
   3447 
   3448             @Override
   3449             public long getItemId(int position) {
   3450                 return position;
   3451             }
   3452 
   3453             @Override
   3454             public View getView(int position, View convertView, ViewGroup parent) {
   3455                 TextView textView = (TextView) convertView;
   3456 
   3457                 if (textView == null) {
   3458                     textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
   3459                             parent, false);
   3460                 }
   3461 
   3462                 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
   3463                 textView.setText(suggestionInfo.mText);
   3464                 return textView;
   3465             }
   3466         }
   3467 
   3468         @VisibleForTesting
   3469         public ViewGroup getContentViewForTesting() {
   3470             return mContentView;
   3471         }
   3472 
   3473         @Override
   3474         public void show() {
   3475             if (!(mTextView.getText() instanceof Editable)) return;
   3476             if (extractedTextModeWillBeStarted()) {
   3477                 return;
   3478             }
   3479 
   3480             if (updateSuggestions()) {
   3481                 mCursorWasVisibleBeforeSuggestions = mCursorVisible;
   3482                 mTextView.setCursorVisible(false);
   3483                 mIsShowingUp = true;
   3484                 super.show();
   3485             }
   3486         }
   3487 
   3488         @Override
   3489         protected void measureContent() {
   3490             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   3491             final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
   3492                     displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
   3493             final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
   3494                     displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
   3495 
   3496             int width = 0;
   3497             View view = null;
   3498             for (int i = 0; i < mNumberOfSuggestions; i++) {
   3499                 view = mSuggestionsAdapter.getView(i, view, mContentView);
   3500                 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
   3501                 view.measure(horizontalMeasure, verticalMeasure);
   3502                 width = Math.max(width, view.getMeasuredWidth());
   3503             }
   3504 
   3505             if (mAddToDictionaryButton.getVisibility() != View.GONE) {
   3506                 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
   3507                 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
   3508             }
   3509 
   3510             mDeleteButton.measure(horizontalMeasure, verticalMeasure);
   3511             width = Math.max(width, mDeleteButton.getMeasuredWidth());
   3512 
   3513             width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
   3514                     + mContainerMarginWidth;
   3515 
   3516             // Enforce the width based on actual text widths
   3517             mContentView.measure(
   3518                     View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
   3519                     verticalMeasure);
   3520 
   3521             Drawable popupBackground = mPopupWindow.getBackground();
   3522             if (popupBackground != null) {
   3523                 if (mTempRect == null) mTempRect = new Rect();
   3524                 popupBackground.getPadding(mTempRect);
   3525                 width += mTempRect.left + mTempRect.right;
   3526             }
   3527             mPopupWindow.setWidth(width);
   3528         }
   3529 
   3530         @Override
   3531         protected int getTextOffset() {
   3532             return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
   3533         }
   3534 
   3535         @Override
   3536         protected int getVerticalLocalPosition(int line) {
   3537             return mTextView.getLayout().getLineBottom(line) - mContainerMarginTop;
   3538         }
   3539 
   3540         @Override
   3541         protected int clipVertically(int positionY) {
   3542             final int height = mContentView.getMeasuredHeight();
   3543             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
   3544             return Math.min(positionY, displayMetrics.heightPixels - height);
   3545         }
   3546 
   3547         private void hideWithCleanUp() {
   3548             for (final SuggestionInfo info : mSuggestionInfos) {
   3549                 info.clear();
   3550             }
   3551             mMisspelledSpanInfo.clear();
   3552             hide();
   3553         }
   3554 
   3555         private boolean updateSuggestions() {
   3556             Spannable spannable = (Spannable) mTextView.getText();
   3557             mNumberOfSuggestions =
   3558                     mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
   3559             if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
   3560                 return false;
   3561             }
   3562 
   3563             int spanUnionStart = mTextView.getText().length();
   3564             int spanUnionEnd = 0;
   3565 
   3566             for (int i = 0; i < mNumberOfSuggestions; i++) {
   3567                 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
   3568                 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
   3569                 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
   3570             }
   3571             if (mMisspelledSpanInfo.mSuggestionSpan != null) {
   3572                 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
   3573                 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
   3574             }
   3575 
   3576             for (int i = 0; i < mNumberOfSuggestions; i++) {
   3577                 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
   3578             }
   3579 
   3580             // Make "Add to dictionary" item visible if there is a span with the misspelled flag
   3581             int addToDictionaryButtonVisibility = View.GONE;
   3582             if (mMisspelledSpanInfo.mSuggestionSpan != null) {
   3583                 if (mMisspelledSpanInfo.mSpanStart >= 0
   3584                         && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
   3585                     addToDictionaryButtonVisibility = View.VISIBLE;
   3586                 }
   3587             }
   3588             mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
   3589 
   3590             if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
   3591             final int underlineColor;
   3592             if (mNumberOfSuggestions != 0) {
   3593                 underlineColor =
   3594                         mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
   3595             } else {
   3596                 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
   3597             }
   3598 
   3599             if (underlineColor == 0) {
   3600                 // Fallback on the default highlight color when the first span does not provide one
   3601                 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
   3602             } else {
   3603                 final float BACKGROUND_TRANSPARENCY = 0.4f;
   3604                 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
   3605                 mSuggestionRangeSpan.setBackgroundColor(
   3606                         (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
   3607             }
   3608             spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
   3609                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   3610 
   3611             mSuggestionsAdapter.notifyDataSetChanged();
   3612             return true;
   3613         }
   3614 
   3615         private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
   3616                 int unionEnd) {
   3617             final Spannable text = (Spannable) mTextView.getText();
   3618             final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
   3619             final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
   3620 
   3621             // Adjust the start/end of the suggestion span
   3622             suggestionInfo.mSuggestionStart = spanStart - unionStart;
   3623             suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
   3624                     + suggestionInfo.mText.length();
   3625 
   3626             suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
   3627                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
   3628 
   3629             // Add the text before and after the span.
   3630             final String textAsString = text.toString();
   3631             suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
   3632             suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
   3633         }
   3634 
   3635         @Override
   3636         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
   3637             SuggestionInfo suggestionInfo = mSuggestionInfos[position];
   3638             replaceWithSuggestion(suggestionInfo);
   3639             hideWithCleanUp();
   3640         }
   3641     }
   3642 
   3643     /**
   3644      * An ActionMode Callback class that is used to provide actions while in text insertion or
   3645      * selection mode.
   3646      *
   3647      * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
   3648      * actions, depending on which of these this TextView supports and the current selection.
   3649      */
   3650     private class TextActionModeCallback extends ActionMode.Callback2 {
   3651         private final Path mSelectionPath = new Path();
   3652         private final RectF mSelectionBounds = new RectF();
   3653         private final boolean mHasSelection;
   3654 
   3655         private int mHandleHeight;
   3656 
   3657         public TextActionModeCallback(boolean hasSelection) {
   3658             mHasSelection = hasSelection;
   3659             if (mHasSelection) {
   3660                 SelectionModifierCursorController selectionController = getSelectionController();
   3661                 if (selectionController.mStartHandle == null) {
   3662                     // As these are for initializing selectionController, hide() must be called.
   3663                     selectionController.initDrawables();
   3664                     selectionController.initHandles();
   3665                     selectionController.hide();
   3666                 }
   3667                 mHandleHeight = Math.max(
   3668                         mSelectHandleLeft.getMinimumHeight(),
   3669                         mSelectHandleRight.getMinimumHeight());
   3670             } else {
   3671                 InsertionPointCursorController insertionController = getInsertionController();
   3672                 if (insertionController != null) {
   3673                     insertionController.getHandle();
   3674                     mHandleHeight = mSelectHandleCenter.getMinimumHeight();
   3675                 }
   3676             }
   3677         }
   3678 
   3679         @Override
   3680         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
   3681             mode.setTitle(null);
   3682             mode.setSubtitle(null);
   3683             mode.setTitleOptionalHint(true);
   3684             populateMenuWithItems(menu);
   3685 
   3686             Callback customCallback = getCustomCallback();
   3687             if (customCallback != null) {
   3688                 if (!customCallback.onCreateActionMode(mode, menu)) {
   3689                     // The custom mode can choose to cancel the action mode, dismiss selection.
   3690                     Selection.setSelection((Spannable) mTextView.getText(),
   3691                             mTextView.getSelectionEnd());
   3692                     return false;
   3693                 }
   3694             }
   3695 
   3696             if (mTextView.canProcessText()) {
   3697                 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
   3698             }
   3699 
   3700             if (menu.hasVisibleItems() || mode.getCustomView() != null) {
   3701                 if (mHasSelection && !mTextView.hasTransientState()) {
   3702                     mTextView.setHasTransientState(true);
   3703                 }
   3704                 return true;
   3705             } else {
   3706                 return false;
   3707             }
   3708         }
   3709 
   3710         private Callback getCustomCallback() {
   3711             return mHasSelection
   3712                     ? mCustomSelectionActionModeCallback
   3713                     : mCustomInsertionActionModeCallback;
   3714         }
   3715 
   3716         private void populateMenuWithItems(Menu menu) {
   3717             if (mTextView.canCut()) {
   3718                 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
   3719                         com.android.internal.R.string.cut).
   3720                     setAlphabeticShortcut('x').
   3721                     setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
   3722             }
   3723 
   3724             if (mTextView.canCopy()) {
   3725                 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
   3726                         com.android.internal.R.string.copy).
   3727                     setAlphabeticShortcut('c').
   3728                     setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
   3729             }
   3730 
   3731             if (mTextView.canPaste()) {
   3732                 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
   3733                         com.android.internal.R.string.paste).
   3734                     setAlphabeticShortcut('v').
   3735                     setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
   3736             }
   3737 
   3738             if (mTextView.canShare()) {
   3739                 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
   3740                         com.android.internal.R.string.share).
   3741                     setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
   3742             }
   3743 
   3744             updateSelectAllItem(menu);
   3745             updateReplaceItem(menu);
   3746         }
   3747 
   3748         @Override
   3749         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
   3750             updateSelectAllItem(menu);
   3751             updateReplaceItem(menu);
   3752 
   3753             Callback customCallback = getCustomCallback();
   3754             if (customCallback != null) {
   3755                 return customCallback.onPrepareActionMode(mode, menu);
   3756             }
   3757             return true;
   3758         }
   3759 
   3760         private void updateSelectAllItem(Menu menu) {
   3761             boolean canSelectAll = mTextView.canSelectAllText();
   3762             boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
   3763             if (canSelectAll && !selectAllItemExists) {
   3764                 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
   3765                         com.android.internal.R.string.selectAll)
   3766                     .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
   3767             } else if (!canSelectAll && selectAllItemExists) {
   3768                 menu.removeItem(TextView.ID_SELECT_ALL);
   3769             }
   3770         }
   3771 
   3772         private void updateReplaceItem(Menu menu) {
   3773             boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
   3774             boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
   3775             if (canReplace && !replaceItemExists) {
   3776                 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
   3777                         com.android.internal.R.string.replace)
   3778                     .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
   3779             } else if (!canReplace && replaceItemExists) {
   3780                 menu.removeItem(TextView.ID_REPLACE);
   3781             }
   3782         }
   3783 
   3784         @Override
   3785         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
   3786             if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
   3787                 return true;
   3788             }
   3789             Callback customCallback = getCustomCallback();
   3790             if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
   3791                 return true;
   3792             }
   3793             return mTextView.onTextContextMenuItem(item.getItemId());
   3794         }
   3795 
   3796         @Override
   3797         public void onDestroyActionMode(ActionMode mode) {
   3798             // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
   3799             mTextActionMode = null;
   3800             Callback customCallback = getCustomCallback();
   3801             if (customCallback != null) {
   3802                 customCallback.onDestroyActionMode(mode);
   3803             }
   3804 
   3805             if (!mPreserveSelection) {
   3806                 /*
   3807                  * Leave current selection when we tentatively destroy action mode for the
   3808                  * selection. If we're detaching from a window, we'll bring back the selection
   3809                  * mode when (if) we get reattached.
   3810                  */
   3811                 Selection.setSelection((Spannable) mTextView.getText(),
   3812                         mTextView.getSelectionEnd());
   3813             }
   3814 
   3815             if (mSelectionModifierCursorController != null) {
   3816                 mSelectionModifierCursorController.hide();
   3817             }
   3818         }
   3819 
   3820         @Override
   3821         public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
   3822             if (!view.equals(mTextView) || mTextView.getLayout() == null) {
   3823                 super.onGetContentRect(mode, view, outRect);
   3824                 return;
   3825             }
   3826             if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
   3827                 // We have a selection.
   3828                 mSelectionPath.reset();
   3829                 mTextView.getLayout().getSelectionPath(
   3830                         mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
   3831                 mSelectionPath.computeBounds(mSelectionBounds, true);
   3832                 mSelectionBounds.bottom += mHandleHeight;
   3833             } else if (mCursorCount == 2) {
   3834                 // We have a split cursor. In this case, we take the rectangle that includes both
   3835                 // parts of the cursor to ensure we don't obscure either of them.
   3836                 Rect firstCursorBounds = mCursorDrawable[0].getBounds();
   3837                 Rect secondCursorBounds = mCursorDrawable[1].getBounds();
   3838                 mSelectionBounds.set(
   3839                         Math.min(firstCursorBounds.left, secondCursorBounds.left),
   3840                         Math.min(firstCursorBounds.top, secondCursorBounds.top),
   3841                         Math.max(firstCursorBounds.right, secondCursorBounds.right),
   3842                         Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
   3843                                 + mHandleHeight);
   3844             } else {
   3845                 // We have a single cursor.
   3846                 Layout layout = mTextView.getLayout();
   3847                 int line = layout.getLineForOffset(mTextView.getSelectionStart());
   3848                 float primaryHorizontal = clampHorizontalPosition(null,
   3849                         layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
   3850                 mSelectionBounds.set(
   3851                         primaryHorizontal,
   3852                         layout.getLineTop(line),
   3853                         primaryHorizontal,
   3854                         layout.getLineTop(line + 1) + mHandleHeight);
   3855             }
   3856             // Take TextView's padding and scroll into account.
   3857             int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
   3858             int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
   3859             outRect.set(
   3860                     (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
   3861                     (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
   3862                     (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
   3863                     (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
   3864         }
   3865     }
   3866 
   3867     /**
   3868      * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
   3869      * while the input method is requesting the cursor/anchor position. Does nothing as long as
   3870      * {@link InputMethodManager#isWatchingCursor(View)} returns false.
   3871      */
   3872     private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
   3873         final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
   3874         final int[] mTmpIntOffset = new int[2];
   3875         final Matrix mViewToScreenMatrix = new Matrix();
   3876 
   3877         @Override
   3878         public void updatePosition(int parentPositionX, int parentPositionY,
   3879                 boolean parentPositionChanged, boolean parentScrolled) {
   3880             final InputMethodState ims = mInputMethodState;
   3881             if (ims == null || ims.mBatchEditNesting > 0) {
   3882                 return;
   3883             }
   3884             final InputMethodManager imm = InputMethodManager.peekInstance();
   3885             if (null == imm) {
   3886                 return;
   3887             }
   3888             if (!imm.isActive(mTextView)) {
   3889                 return;
   3890             }
   3891             // Skip if the IME has not requested the cursor/anchor position.
   3892             if (!imm.isCursorAnchorInfoEnabled()) {
   3893                 return;
   3894             }
   3895             Layout layout = mTextView.getLayout();
   3896             if (layout == null) {
   3897                 return;
   3898             }
   3899 
   3900             final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
   3901             builder.reset();
   3902 
   3903             final int selectionStart = mTextView.getSelectionStart();
   3904             builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
   3905 
   3906             // Construct transformation matrix from view local coordinates to screen coordinates.
   3907             mViewToScreenMatrix.set(mTextView.getMatrix());
   3908             mTextView.getLocationOnScreen(mTmpIntOffset);
   3909             mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
   3910             builder.setMatrix(mViewToScreenMatrix);
   3911 
   3912             final float viewportToContentHorizontalOffset =
   3913                     mTextView.viewportToContentHorizontalOffset();
   3914             final float viewportToContentVerticalOffset =
   3915                     mTextView.viewportToContentVerticalOffset();
   3916 
   3917             final CharSequence text = mTextView.getText();
   3918             if (text instanceof Spannable) {
   3919                 final Spannable sp = (Spannable) text;
   3920                 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
   3921                 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
   3922                 if (composingTextEnd < composingTextStart) {
   3923                     final int temp = composingTextEnd;
   3924                     composingTextEnd = composingTextStart;
   3925                     composingTextStart = temp;
   3926                 }
   3927                 final boolean hasComposingText =
   3928                         (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
   3929                 if (hasComposingText) {
   3930                     final CharSequence composingText = text.subSequence(composingTextStart,
   3931                             composingTextEnd);
   3932                     builder.setComposingText(composingTextStart, composingText);
   3933 
   3934                     final int minLine = layout.getLineForOffset(composingTextStart);
   3935                     final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
   3936                     for (int line = minLine; line <= maxLine; ++line) {
   3937                         final int lineStart = layout.getLineStart(line);
   3938                         final int lineEnd = layout.getLineEnd(line);
   3939                         final int offsetStart = Math.max(lineStart, composingTextStart);
   3940                         final int offsetEnd = Math.min(lineEnd, composingTextEnd);
   3941                         final boolean ltrLine =
   3942                                 layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
   3943                         final float[] widths = new float[offsetEnd - offsetStart];
   3944                         layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
   3945                         final float top = layout.getLineTop(line);
   3946                         final float bottom = layout.getLineBottom(line);
   3947                         for (int offset = offsetStart; offset < offsetEnd; ++offset) {
   3948                             final float charWidth = widths[offset - offsetStart];
   3949                             final boolean isRtl = layout.isRtlCharAt(offset);
   3950                             final float primary = layout.getPrimaryHorizontal(offset);
   3951                             final float secondary = layout.getSecondaryHorizontal(offset);
   3952                             // TODO: This doesn't work perfectly for text with custom styles and
   3953                             // TAB chars.
   3954                             final float left;
   3955                             final float right;
   3956                             if (ltrLine) {
   3957                                 if (isRtl) {
   3958                                     left = secondary - charWidth;
   3959                                     right = secondary;
   3960                                 } else {
   3961                                     left = primary;
   3962                                     right = primary + charWidth;
   3963                                 }
   3964                             } else {
   3965                                 if (!isRtl) {
   3966                                     left = secondary;
   3967                                     right = secondary + charWidth;
   3968                                 } else {
   3969                                     left = primary - charWidth;
   3970                                     right = primary;
   3971                                 }
   3972                             }
   3973                             // TODO: Check top-right and bottom-left as well.
   3974                             final float localLeft = left + viewportToContentHorizontalOffset;
   3975                             final float localRight = right + viewportToContentHorizontalOffset;
   3976                             final float localTop = top + viewportToContentVerticalOffset;
   3977                             final float localBottom = bottom + viewportToContentVerticalOffset;
   3978                             final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
   3979                             final boolean isBottomRightVisible =
   3980                                     isPositionVisible(localRight, localBottom);
   3981                             int characterBoundsFlags = 0;
   3982                             if (isTopLeftVisible || isBottomRightVisible) {
   3983                                 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
   3984                             }
   3985                             if (!isTopLeftVisible || !isBottomRightVisible) {
   3986                                 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
   3987                             }
   3988                             if (isRtl) {
   3989                                 characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
   3990                             }
   3991                             // Here offset is the index in Java chars.
   3992                             builder.addCharacterBounds(offset, localLeft, localTop, localRight,
   3993                                     localBottom, characterBoundsFlags);
   3994                         }
   3995                     }
   3996                 }
   3997             }
   3998 
   3999             // Treat selectionStart as the insertion point.
   4000             if (0 <= selectionStart) {
   4001                 final int offset = selectionStart;
   4002                 final int line = layout.getLineForOffset(offset);
   4003                 final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
   4004                         + viewportToContentHorizontalOffset;
   4005                 final float insertionMarkerTop = layout.getLineTop(line)
   4006                         + viewportToContentVerticalOffset;
   4007                 final float insertionMarkerBaseline = layout.getLineBaseline(line)
   4008                         + viewportToContentVerticalOffset;
   4009                 final float insertionMarkerBottom = layout.getLineBottom(line)
   4010                         + viewportToContentVerticalOffset;
   4011                 final boolean isTopVisible =
   4012                         isPositionVisible(insertionMarkerX, insertionMarkerTop);
   4013                 final boolean isBottomVisible =
   4014                         isPositionVisible(insertionMarkerX, insertionMarkerBottom);
   4015                 int insertionMarkerFlags = 0;
   4016                 if (isTopVisible || isBottomVisible) {
   4017                     insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
   4018                 }
   4019                 if (!isTopVisible || !isBottomVisible) {
   4020                     insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
   4021                 }
   4022                 if (layout.isRtlCharAt(offset)) {
   4023                     insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
   4024                 }
   4025                 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
   4026                         insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
   4027             }
   4028 
   4029             imm.updateCursorAnchorInfo(mTextView, builder.build());
   4030         }
   4031     }
   4032 
   4033     @VisibleForTesting
   4034     public abstract class HandleView extends View implements TextViewPositionListener {
   4035         protected Drawable mDrawable;
   4036         protected Drawable mDrawableLtr;
   4037         protected Drawable mDrawableRtl;
   4038         private final PopupWindow mContainer;
   4039         // Position with respect to the parent TextView
   4040         private int mPositionX, mPositionY;
   4041         private boolean mIsDragging;
   4042         // Offset from touch position to mPosition
   4043         private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
   4044         protected int mHotspotX;
   4045         protected int mHorizontalGravity;
   4046         // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
   4047         private float mTouchOffsetY;
   4048         // Where the touch position should be on the handle to ensure a maximum cursor visibility
   4049         private float mIdealVerticalOffset;
   4050         // Parent's (TextView) previous position in window
   4051         private int mLastParentX, mLastParentY;
   4052         // Parent's (TextView) previous position on screen
   4053         private int mLastParentXOnScreen, mLastParentYOnScreen;
   4054         // Previous text character offset
   4055         protected int mPreviousOffset = -1;
   4056         // Previous text character offset
   4057         private boolean mPositionHasChanged = true;
   4058         // Minimum touch target size for handles
   4059         private int mMinSize;
   4060         // Indicates the line of text that the handle is on.
   4061         protected int mPrevLine = UNSET_LINE;
   4062         // Indicates the line of text that the user was touching. This can differ from mPrevLine
   4063         // when selecting text when the handles jump to the end / start of words which may be on
   4064         // a different line.
   4065         protected int mPreviousLineTouched = UNSET_LINE;
   4066 
   4067         private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
   4068             super(mTextView.getContext());
   4069             setId(id);
   4070             mContainer = new PopupWindow(mTextView.getContext(), null,
   4071                     com.android.internal.R.attr.textSelectHandleWindowStyle);
   4072             mContainer.setSplitTouchEnabled(true);
   4073             mContainer.setClippingEnabled(false);
   4074             mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
   4075             mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
   4076             mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
   4077             mContainer.setContentView(this);
   4078 
   4079             mDrawableLtr = drawableLtr;
   4080             mDrawableRtl = drawableRtl;
   4081             mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
   4082                     com.android.internal.R.dimen.text_handle_min_size);
   4083 
   4084             updateDrawable();
   4085 
   4086             final int handleHeight = getPreferredHeight();
   4087             mTouchOffsetY = -0.3f * handleHeight;
   4088             mIdealVerticalOffset = 0.7f * handleHeight;
   4089         }
   4090 
   4091         public float getIdealVerticalOffset() {
   4092             return mIdealVerticalOffset;
   4093         }
   4094 
   4095         protected void updateDrawable() {
   4096             if (mIsDragging) {
   4097                 // Don't update drawable during dragging.
   4098                 return;
   4099             }
   4100             final Layout layout = mTextView.getLayout();
   4101             if (layout == null) {
   4102                 return;
   4103             }
   4104             final int offset = getCurrentCursorOffset();
   4105             final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
   4106             final Drawable oldDrawable = mDrawable;
   4107             mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
   4108             mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
   4109             mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
   4110             if (oldDrawable != mDrawable && isShowing()) {
   4111                 // Update popup window position.
   4112                 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
   4113                         getHorizontalOffset() + getCursorOffset();
   4114                 mPositionX += mTextView.viewportToContentHorizontalOffset();
   4115                 mPositionHasChanged = true;
   4116                 updatePosition(mLastParentX, mLastParentY, false, false);
   4117                 postInvalidate();
   4118             }
   4119         }
   4120 
   4121         protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
   4122         protected abstract int getHorizontalGravity(boolean isRtlRun);
   4123 
   4124         // Touch-up filter: number of previous positions remembered
   4125         private static final int HISTORY_SIZE = 5;
   4126         private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
   4127         private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
   4128         private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
   4129         private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
   4130         private int mPreviousOffsetIndex = 0;
   4131         private int mNumberPreviousOffsets = 0;
   4132 
   4133         private void startTouchUpFilter(int offset) {
   4134             mNumberPreviousOffsets = 0;
   4135             addPositionToTouchUpFilter(offset);
   4136         }
   4137 
   4138         private void addPositionToTouchUpFilter(int offset) {
   4139             mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
   4140             mPreviousOffsets[mPreviousOffsetIndex] = offset;
   4141             mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
   4142             mNumberPreviousOffsets++;
   4143         }
   4144 
   4145         private void filterOnTouchUp() {
   4146             final long now = SystemClock.uptimeMillis();
   4147             int i = 0;
   4148             int index = mPreviousOffsetIndex;
   4149             final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
   4150             while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
   4151                 i++;
   4152                 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
   4153             }
   4154 
   4155             if (i > 0 && i < iMax &&
   4156                     (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
   4157                 positionAtCursorOffset(mPreviousOffsets[index], false);
   4158             }
   4159         }
   4160 
   4161         public boolean offsetHasBeenChanged() {
   4162             return mNumberPreviousOffsets > 1;
   4163         }
   4164 
   4165         @Override
   4166         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
   4167             setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
   4168         }
   4169 
   4170         @Override
   4171         public void invalidate() {
   4172             super.invalidate();
   4173             if (isShowing()) {
   4174                 positionAtCursorOffset(getCurrentCursorOffset(), true);
   4175             }
   4176         };
   4177 
   4178         private int getPreferredWidth() {
   4179             return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
   4180         }
   4181 
   4182         private int getPreferredHeight() {
   4183             return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
   4184         }
   4185 
   4186         public void show() {
   4187             if (isShowing()) return;
   4188 
   4189             getPositionListener().addSubscriber(this, true /* local position may change */);
   4190 
   4191             // Make sure the offset is always considered new, even when focusing at same position
   4192             mPreviousOffset = -1;
   4193             positionAtCursorOffset(getCurrentCursorOffset(), false);
   4194         }
   4195 
   4196         protected void dismiss() {
   4197             mIsDragging = false;
   4198             mContainer.dismiss();
   4199             onDetached();
   4200         }
   4201 
   4202         public void hide() {
   4203             dismiss();
   4204 
   4205             getPositionListener().removeSubscriber(this);
   4206         }
   4207 
   4208         public boolean isShowing() {
   4209             return mContainer.isShowing();
   4210         }
   4211 
   4212         private boolean isVisible() {
   4213             // Always show a dragging handle.
   4214             if (mIsDragging) {
   4215                 return true;
   4216             }
   4217 
   4218             if (mTextView.isInBatchEditMode()) {
   4219                 return false;
   4220             }
   4221 
   4222             return isPositionVisible(mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
   4223         }
   4224 
   4225         public abstract int getCurrentCursorOffset();
   4226 
   4227         protected abstract void updateSelection(int offset);
   4228 
   4229         public abstract void updatePosition(float x, float y);
   4230 
   4231         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
   4232             return layout.isRtlCharAt(offset);
   4233         }
   4234 
   4235         @VisibleForTesting
   4236         public float getHorizontal(@NonNull Layout layout, int offset) {
   4237             return layout.getPrimaryHorizontal(offset);
   4238         }
   4239 
   4240         protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
   4241             return mTextView.getOffsetAtCoordinate(line, x);
   4242         }
   4243 
   4244         /**
   4245          * @param offset Cursor offset. Must be in [-1, length].
   4246          * @param forceUpdatePosition whether to force update the position.  This should be true
   4247          * when If the parent has been scrolled, for example.
   4248          */
   4249         protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
   4250             // A HandleView relies on the layout, which may be nulled by external methods
   4251             Layout layout = mTextView.getLayout();
   4252             if (layout == null) {
   4253                 // Will update controllers' state, hiding them and stopping selection mode if needed
   4254                 prepareCursorControllers();
   4255                 return;
   4256             }
   4257             layout = mTextView.getLayout();
   4258 
   4259             boolean offsetChanged = offset != mPreviousOffset;
   4260             if (offsetChanged || forceUpdatePosition) {
   4261                 if (offsetChanged) {
   4262                     updateSelection(offset);
   4263                     addPositionToTouchUpFilter(offset);
   4264                 }
   4265                 final int line = layout.getLineForOffset(offset);
   4266                 mPrevLine = line;
   4267 
   4268                 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
   4269                         getHorizontalOffset() + getCursorOffset();
   4270                 mPositionY = layout.getLineBottom(line);
   4271 
   4272                 // Take TextView's padding and scroll into account.
   4273                 mPositionX += mTextView.viewportToContentHorizontalOffset();
   4274                 mPositionY += mTextView.viewportToContentVerticalOffset();
   4275 
   4276                 mPreviousOffset = offset;
   4277                 mPositionHasChanged = true;
   4278             }
   4279         }
   4280 
   4281         /**
   4282          * Return the clamped horizontal position for the first cursor.
   4283          *
   4284          * @param layout Text layout.
   4285          * @param offset Character offset for the cursor.
   4286          * @return The clamped horizontal position for the cursor.
   4287          */
   4288         int getCursorHorizontalPosition(Layout layout, int offset) {
   4289             return (int) (getHorizontal(layout, offset) - 0.5f);
   4290         }
   4291 
   4292         @Override
   4293         public void updatePosition(int parentPositionX, int parentPositionY,
   4294                 boolean parentPositionChanged, boolean parentScrolled) {
   4295             positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
   4296             if (parentPositionChanged || mPositionHasChanged) {
   4297                 if (mIsDragging) {
   4298                     // Update touchToWindow offset in case of parent scrolling while dragging
   4299                     if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
   4300                         mTouchToWindowOffsetX += parentPositionX - mLastParentX;
   4301                         mTouchToWindowOffsetY += parentPositionY - mLastParentY;
   4302                         mLastParentX = parentPositionX;
   4303                         mLastParentY = parentPositionY;
   4304                     }
   4305 
   4306                     onHandleMoved();
   4307                 }
   4308 
   4309                 if (isVisible()) {
   4310                     // Transform to the window coordinates to follow the view tranformation.
   4311                     final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
   4312                     mTextView.transformFromViewToWindowSpace(pts);
   4313                     pts[0] -= mHotspotX + getHorizontalOffset();
   4314 
   4315                     if (isShowing()) {
   4316                         mContainer.update(pts[0], pts[1], -1, -1);
   4317                     } else {
   4318                         mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
   4319                     }
   4320                 } else {
   4321                     if (isShowing()) {
   4322                         dismiss();
   4323                     }
   4324                 }
   4325 
   4326                 mPositionHasChanged = false;
   4327             }
   4328         }
   4329 
   4330         @Override
   4331         protected void onDraw(Canvas c) {
   4332             final int drawWidth = mDrawable.getIntrinsicWidth();
   4333             final int left = getHorizontalOffset();
   4334 
   4335             mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
   4336             mDrawable.draw(c);
   4337         }
   4338 
   4339         private int getHorizontalOffset() {
   4340             final int width = getPreferredWidth();
   4341             final int drawWidth = mDrawable.getIntrinsicWidth();
   4342             final int left;
   4343             switch (mHorizontalGravity) {
   4344                 case Gravity.LEFT:
   4345                     left = 0;
   4346                     break;
   4347                 default:
   4348                 case Gravity.CENTER:
   4349                     left = (width - drawWidth) / 2;
   4350                     break;
   4351                 case Gravity.RIGHT:
   4352                     left = width - drawWidth;
   4353                     break;
   4354             }
   4355             return left;
   4356         }
   4357 
   4358         protected int getCursorOffset() {
   4359             return 0;
   4360         }
   4361 
   4362         @Override
   4363         public boolean onTouchEvent(MotionEvent ev) {
   4364             updateFloatingToolbarVisibility(ev);
   4365 
   4366             switch (ev.getActionMasked()) {
   4367                 case MotionEvent.ACTION_DOWN: {
   4368                     startTouchUpFilter(getCurrentCursorOffset());
   4369 
   4370                     final PositionListener positionListener = getPositionListener();
   4371                     mLastParentX = positionListener.getPositionX();
   4372                     mLastParentY = positionListener.getPositionY();
   4373                     mLastParentXOnScreen = positionListener.getPositionXOnScreen();
   4374                     mLastParentYOnScreen = positionListener.getPositionYOnScreen();
   4375 
   4376                     final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
   4377                     final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
   4378                     mTouchToWindowOffsetX = xInWindow - mPositionX;
   4379                     mTouchToWindowOffsetY = yInWindow - mPositionY;
   4380 
   4381                     mIsDragging = true;
   4382                     mPreviousLineTouched = UNSET_LINE;
   4383                     break;
   4384                 }
   4385 
   4386                 case MotionEvent.ACTION_MOVE: {
   4387                     final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
   4388                     final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
   4389 
   4390                     // Vertical hysteresis: vertical down movement tends to snap to ideal offset
   4391                     final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
   4392                     final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
   4393                     float newVerticalOffset;
   4394                     if (previousVerticalOffset < mIdealVerticalOffset) {
   4395                         newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
   4396                         newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
   4397                     } else {
   4398                         newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
   4399                         newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
   4400                     }
   4401                     mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
   4402 
   4403                     final float newPosX =
   4404                             xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
   4405                     final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
   4406 
   4407                     updatePosition(newPosX, newPosY);
   4408                     break;
   4409                 }
   4410 
   4411                 case MotionEvent.ACTION_UP:
   4412                     filterOnTouchUp();
   4413                     mIsDragging = false;
   4414                     updateDrawable();
   4415                     break;
   4416 
   4417                 case MotionEvent.ACTION_CANCEL:
   4418                     mIsDragging = false;
   4419                     updateDrawable();
   4420                     break;
   4421             }
   4422             return true;
   4423         }
   4424 
   4425         public boolean isDragging() {
   4426             return mIsDragging;
   4427         }
   4428 
   4429         void onHandleMoved() {}
   4430 
   4431         public void onDetached() {}
   4432     }
   4433 
   4434     private class InsertionHandleView extends HandleView {
   4435         private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
   4436         private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
   4437 
   4438         // Used to detect taps on the insertion handle, which will affect the insertion action mode
   4439         private float mDownPositionX, mDownPositionY;
   4440         private Runnable mHider;
   4441 
   4442         public InsertionHandleView(Drawable drawable) {
   4443             super(drawable, drawable, com.android.internal.R.id.insertion_handle);
   4444         }
   4445 
   4446         @Override
   4447         public void show() {
   4448             super.show();
   4449 
   4450             final long durationSinceCutOrCopy =
   4451                     SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
   4452 
   4453             // Cancel the single tap delayed runnable.
   4454             if (mInsertionActionModeRunnable != null
   4455                     && ((mTapState == TAP_STATE_DOUBLE_TAP)
   4456                             || (mTapState == TAP_STATE_TRIPLE_CLICK)
   4457                             || isCursorInsideEasyCorrectionSpan())) {
   4458                 mTextView.removeCallbacks(mInsertionActionModeRunnable);
   4459             }
   4460 
   4461             // Prepare and schedule the single tap runnable to run exactly after the double tap
   4462             // timeout has passed.
   4463             if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
   4464                     && !isCursorInsideEasyCorrectionSpan()
   4465                     && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
   4466                 if (mTextActionMode == null) {
   4467                     if (mInsertionActionModeRunnable == null) {
   4468                         mInsertionActionModeRunnable = new Runnable() {
   4469                             @Override
   4470                             public void run() {
   4471                                 startInsertionActionMode();
   4472                             }
   4473                         };
   4474                     }
   4475                     mTextView.postDelayed(
   4476                             mInsertionActionModeRunnable,
   4477                             ViewConfiguration.getDoubleTapTimeout() + 1);
   4478                 }
   4479 
   4480             }
   4481 
   4482             hideAfterDelay();
   4483         }
   4484 
   4485         private void hideAfterDelay() {
   4486             if (mHider == null) {
   4487                 mHider = new Runnable() {
   4488                     public void run() {
   4489                         hide();
   4490                     }
   4491                 };
   4492             } else {
   4493                 removeHiderCallback();
   4494             }
   4495             mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
   4496         }
   4497 
   4498         private void removeHiderCallback() {
   4499             if (mHider != null) {
   4500                 mTextView.removeCallbacks(mHider);
   4501             }
   4502         }
   4503 
   4504         @Override
   4505         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
   4506             return drawable.getIntrinsicWidth() / 2;
   4507         }
   4508 
   4509         @Override
   4510         protected int getHorizontalGravity(boolean isRtlRun) {
   4511             return Gravity.CENTER_HORIZONTAL;
   4512         }
   4513 
   4514         @Override
   4515         protected int getCursorOffset() {
   4516             int offset = super.getCursorOffset();
   4517             final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
   4518             if (cursor != null) {
   4519                 cursor.getPadding(mTempRect);
   4520                 offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
   4521             }
   4522             return offset;
   4523         }
   4524 
   4525         @Override
   4526         int getCursorHorizontalPosition(Layout layout, int offset) {
   4527             final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
   4528             if (drawable != null) {
   4529                 final float horizontal = getHorizontal(layout, offset);
   4530                 return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
   4531             }
   4532             return super.getCursorHorizontalPosition(layout, offset);
   4533         }
   4534 
   4535         @Override
   4536         public boolean onTouchEvent(MotionEvent ev) {
   4537             final boolean result = super.onTouchEvent(ev);
   4538 
   4539             switch (ev.getActionMasked()) {
   4540                 case MotionEvent.ACTION_DOWN:
   4541                     mDownPositionX = ev.getRawX();
   4542                     mDownPositionY = ev.getRawY();
   4543                     break;
   4544 
   4545                 case MotionEvent.ACTION_UP:
   4546                     if (!offsetHasBeenChanged()) {
   4547                         final float deltaX = mDownPositionX - ev.getRawX();
   4548                         final float deltaY = mDownPositionY - ev.getRawY();
   4549                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   4550 
   4551                         final ViewConfiguration viewConfiguration = ViewConfiguration.get(
   4552                                 mTextView.getContext());
   4553                         final int touchSlop = viewConfiguration.getScaledTouchSlop();
   4554 
   4555                         if (distanceSquared < touchSlop * touchSlop) {
   4556                             // Tapping on the handle toggles the insertion action mode.
   4557                             if (mTextActionMode != null) {
   4558                                 stopTextActionMode();
   4559                             } else {
   4560                                 startInsertionActionMode();
   4561                             }
   4562                         }
   4563                     } else {
   4564                         if (mTextActionMode != null) {
   4565                             mTextActionMode.invalidateContentRect();
   4566                         }
   4567                     }
   4568                     hideAfterDelay();
   4569                     break;
   4570 
   4571                 case MotionEvent.ACTION_CANCEL:
   4572                     hideAfterDelay();
   4573                     break;
   4574 
   4575                 default:
   4576                     break;
   4577             }
   4578 
   4579             return result;
   4580         }
   4581 
   4582         @Override
   4583         public int getCurrentCursorOffset() {
   4584             return mTextView.getSelectionStart();
   4585         }
   4586 
   4587         @Override
   4588         public void updateSelection(int offset) {
   4589             Selection.setSelection((Spannable) mTextView.getText(), offset);
   4590         }
   4591 
   4592         @Override
   4593         public void updatePosition(float x, float y) {
   4594             Layout layout = mTextView.getLayout();
   4595             int offset;
   4596             if (layout != null) {
   4597                 if (mPreviousLineTouched == UNSET_LINE) {
   4598                     mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
   4599                 }
   4600                 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
   4601                 offset = getOffsetAtCoordinate(layout, currLine, x);
   4602                 mPreviousLineTouched = currLine;
   4603             } else {
   4604                 offset = -1;
   4605             }
   4606             positionAtCursorOffset(offset, false);
   4607             if (mTextActionMode != null) {
   4608                 mTextActionMode.invalidate();
   4609             }
   4610         }
   4611 
   4612         @Override
   4613         void onHandleMoved() {
   4614             super.onHandleMoved();
   4615             removeHiderCallback();
   4616         }
   4617 
   4618         @Override
   4619         public void onDetached() {
   4620             super.onDetached();
   4621             removeHiderCallback();
   4622         }
   4623     }
   4624 
   4625     @Retention(RetentionPolicy.SOURCE)
   4626     @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
   4627     public @interface HandleType {}
   4628     public static final int HANDLE_TYPE_SELECTION_START = 0;
   4629     public static final int HANDLE_TYPE_SELECTION_END = 1;
   4630 
   4631     private class SelectionHandleView extends HandleView {
   4632         // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
   4633         // end (HANDLE_TYPE_SELECTION_END).
   4634         @HandleType
   4635         private final int mHandleType;
   4636         // Indicates whether the cursor is making adjustments within a word.
   4637         private boolean mInWord = false;
   4638         // Difference between touch position and word boundary position.
   4639         private float mTouchWordDelta;
   4640         // X value of the previous updatePosition call.
   4641         private float mPrevX;
   4642         // Indicates if the handle has moved a boundary between LTR and RTL text.
   4643         private boolean mLanguageDirectionChanged = false;
   4644         // Distance from edge of horizontally scrolling text view
   4645         // to use to switch to character mode.
   4646         private final float mTextViewEdgeSlop;
   4647         // Used to save text view location.
   4648         private final int[] mTextViewLocation = new int[2];
   4649 
   4650         public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
   4651                 @HandleType int handleType) {
   4652             super(drawableLtr, drawableRtl, id);
   4653             mHandleType = handleType;
   4654             ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
   4655             mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
   4656         }
   4657 
   4658         private boolean isStartHandle() {
   4659             return mHandleType == HANDLE_TYPE_SELECTION_START;
   4660         }
   4661 
   4662         @Override
   4663         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
   4664             if (isRtlRun == isStartHandle()) {
   4665                 return drawable.getIntrinsicWidth() / 4;
   4666             } else {
   4667                 return (drawable.getIntrinsicWidth() * 3) / 4;
   4668             }
   4669         }
   4670 
   4671         @Override
   4672         protected int getHorizontalGravity(boolean isRtlRun) {
   4673             return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
   4674         }
   4675 
   4676         @Override
   4677         public int getCurrentCursorOffset() {
   4678             return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
   4679         }
   4680 
   4681         @Override
   4682         protected void updateSelection(int offset) {
   4683             if (isStartHandle()) {
   4684                 Selection.setSelection((Spannable) mTextView.getText(), offset,
   4685                         mTextView.getSelectionEnd());
   4686             } else {
   4687                 Selection.setSelection((Spannable) mTextView.getText(),
   4688                         mTextView.getSelectionStart(), offset);
   4689             }
   4690             updateDrawable();
   4691             if (mTextActionMode != null) {
   4692                 mTextActionMode.invalidate();
   4693             }
   4694         }
   4695 
   4696         @Override
   4697         public void updatePosition(float x, float y) {
   4698             final Layout layout = mTextView.getLayout();
   4699             if (layout == null) {
   4700                 // HandleView will deal appropriately in positionAtCursorOffset when
   4701                 // layout is null.
   4702                 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y));
   4703                 return;
   4704             }
   4705 
   4706             if (mPreviousLineTouched == UNSET_LINE) {
   4707                 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
   4708             }
   4709 
   4710             boolean positionCursor = false;
   4711             final int anotherHandleOffset =
   4712                     isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
   4713             int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
   4714             int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
   4715 
   4716             if (isStartHandle() && initialOffset >= anotherHandleOffset
   4717                     || !isStartHandle() && initialOffset <= anotherHandleOffset) {
   4718                 // Handles have crossed, bound it to the first selected line and
   4719                 // adjust by word / char as normal.
   4720                 currLine = layout.getLineForOffset(anotherHandleOffset);
   4721                 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
   4722             }
   4723 
   4724             int offset = initialOffset;
   4725             final int wordEnd = getWordEnd(offset);
   4726             final int wordStart = getWordStart(offset);
   4727 
   4728             if (mPrevX == UNSET_X_VALUE) {
   4729                 mPrevX = x;
   4730             }
   4731 
   4732             final int currentOffset = getCurrentCursorOffset();
   4733             final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
   4734             final boolean atRtl = isAtRtlRun(layout, offset);
   4735             final boolean isLvlBoundary = layout.isLevelBoundary(offset);
   4736 
   4737             // We can't determine if the user is expanding or shrinking the selection if they're
   4738             // on a bi-di boundary, so until they've moved past the boundary we'll just place
   4739             // the cursor at the current position.
   4740             if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
   4741                 // We're on a boundary or this is the first direction change -- just update
   4742                 // to the current position.
   4743                 mLanguageDirectionChanged = true;
   4744                 mTouchWordDelta = 0.0f;
   4745                 positionAndAdjustForCrossingHandles(offset);
   4746                 return;
   4747             } else if (mLanguageDirectionChanged && !isLvlBoundary) {
   4748                 // We've just moved past the boundary so update the position. After this we can
   4749                 // figure out if the user is expanding or shrinking to go by word or character.
   4750                 positionAndAdjustForCrossingHandles(offset);
   4751                 mTouchWordDelta = 0.0f;
   4752                 mLanguageDirectionChanged = false;
   4753                 return;
   4754             }
   4755 
   4756             boolean isExpanding;
   4757             final float xDiff = x - mPrevX;
   4758             if (isStartHandle()) {
   4759                 isExpanding = currLine < mPreviousLineTouched;
   4760             } else {
   4761                 isExpanding = currLine > mPreviousLineTouched;
   4762             }
   4763             if (atRtl == isStartHandle()) {
   4764                 isExpanding |= xDiff > 0;
   4765             } else {
   4766                 isExpanding |= xDiff < 0;
   4767             }
   4768 
   4769             if (mTextView.getHorizontallyScrolling()) {
   4770                 if (positionNearEdgeOfScrollingView(x, atRtl)
   4771                         && ((isStartHandle() && mTextView.getScrollX() != 0)
   4772                                 || (!isStartHandle()
   4773                                         && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
   4774                         && ((isExpanding && ((isStartHandle() && offset < currentOffset)
   4775                                 || (!isStartHandle() && offset > currentOffset)))
   4776                                         || !isExpanding)) {
   4777                     // If we're expanding ensure that the offset is actually expanding compared to
   4778                     // the current offset, if the handle snapped to the word, the finger position
   4779                     // may be out of sync and we don't want the selection to jump back.
   4780                     mTouchWordDelta = 0.0f;
   4781                     final int nextOffset = (atRtl == isStartHandle())
   4782                             ? layout.getOffsetToRightOf(mPreviousOffset)
   4783                             : layout.getOffsetToLeftOf(mPreviousOffset);
   4784                     positionAndAdjustForCrossingHandles(nextOffset);
   4785                     return;
   4786                 }
   4787             }
   4788 
   4789             if (isExpanding) {
   4790                 // User is increasing the selection.
   4791                 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
   4792                 final boolean snapToWord = (!mInWord
   4793                         || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
   4794                                 && atRtl == isAtRtlRun(layout, wordBoundary);
   4795                 if (snapToWord) {
   4796                     // Sometimes words can be broken across lines (Chinese, hyphenation).
   4797                     // We still snap to the word boundary but we only use the letters on the
   4798                     // current line to determine if the user is far enough into the word to snap.
   4799                     if (layout.getLineForOffset(wordBoundary) != currLine) {
   4800                         wordBoundary = isStartHandle() ?
   4801                                 layout.getLineStart(currLine) : layout.getLineEnd(currLine);
   4802                     }
   4803                     final int offsetThresholdToSnap = isStartHandle()
   4804                             ? wordEnd - ((wordEnd - wordBoundary) / 2)
   4805                             : wordStart + ((wordBoundary - wordStart) / 2);
   4806                     if (isStartHandle()
   4807                             && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
   4808                         // User is far enough into the word or on a different line so we expand by
   4809                         // word.
   4810                         offset = wordStart;
   4811                     } else if (!isStartHandle()
   4812                             && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
   4813                         // User is far enough into the word or on a different line so we expand by
   4814                         // word.
   4815                         offset = wordEnd;
   4816                     } else {
   4817                         offset = mPreviousOffset;
   4818                     }
   4819                 }
   4820                 if ((isStartHandle() && offset < initialOffset)
   4821                         || (!isStartHandle() && offset > initialOffset)) {
   4822                     final float adjustedX = getHorizontal(layout, offset);
   4823                     mTouchWordDelta =
   4824                             mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
   4825                 } else {
   4826                     mTouchWordDelta = 0.0f;
   4827                 }
   4828                 positionCursor = true;
   4829             } else {
   4830                 final int adjustedOffset =
   4831                         getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
   4832                 final boolean shrinking = isStartHandle()
   4833                         ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
   4834                         : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
   4835                 if (shrinking) {
   4836                     // User is shrinking the selection.
   4837                     if (currLine != mPrevLine) {
   4838                         // We're on a different line, so we'll snap to word boundaries.
   4839                         offset = isStartHandle() ? wordStart : wordEnd;
   4840                         if ((isStartHandle() && offset < initialOffset)
   4841                                 || (!isStartHandle() && offset > initialOffset)) {
   4842                             final float adjustedX = getHorizontal(layout, offset);
   4843                             mTouchWordDelta =
   4844                                     mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
   4845                         } else {
   4846                             mTouchWordDelta = 0.0f;
   4847                         }
   4848                     } else {
   4849                         offset = adjustedOffset;
   4850                     }
   4851                     positionCursor = true;
   4852                 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
   4853                         || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
   4854                     // Handle has jumped to the word boundary, and the user is moving
   4855                     // their finger towards the handle, the delta should be updated.
   4856                     mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) -
   4857                             getHorizontal(layout, mPreviousOffset);
   4858                 }
   4859             }
   4860 
   4861             if (positionCursor) {
   4862                 mPreviousLineTouched = currLine;
   4863                 positionAndAdjustForCrossingHandles(offset);
   4864             }
   4865             mPrevX = x;
   4866         }
   4867 
   4868         @Override
   4869         protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
   4870             super.positionAtCursorOffset(offset, forceUpdatePosition);
   4871             mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
   4872         }
   4873 
   4874         @Override
   4875         public boolean onTouchEvent(MotionEvent event) {
   4876             boolean superResult = super.onTouchEvent(event);
   4877             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
   4878                 // Reset the touch word offset and x value when the user
   4879                 // re-engages the handle.
   4880                 mTouchWordDelta = 0.0f;
   4881                 mPrevX = UNSET_X_VALUE;
   4882             }
   4883             return superResult;
   4884         }
   4885 
   4886         private void positionAndAdjustForCrossingHandles(int offset) {
   4887             final int anotherHandleOffset =
   4888                     isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
   4889             if ((isStartHandle() && offset >= anotherHandleOffset)
   4890                     || (!isStartHandle() && offset <= anotherHandleOffset)) {
   4891                 mTouchWordDelta = 0.0f;
   4892                 final Layout layout = mTextView.getLayout();
   4893                 if (layout != null && offset != anotherHandleOffset) {
   4894                     final float horiz = getHorizontal(layout, offset);
   4895                     final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
   4896                             !isStartHandle());
   4897                     final float currentHoriz = getHorizontal(layout, mPreviousOffset);
   4898                     if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
   4899                             || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
   4900                         // This handle passes another one as it crossed a direction boundary.
   4901                         // Don't minimize the selection, but keep the handle at the run boundary.
   4902                         final int currentOffset = getCurrentCursorOffset();
   4903                         final int offsetToGetRunRange = isStartHandle() ?
   4904                                 currentOffset : Math.max(currentOffset - 1, 0);
   4905                         final long range = layout.getRunRange(offsetToGetRunRange);
   4906                         if (isStartHandle()) {
   4907                             offset = TextUtils.unpackRangeStartFromLong(range);
   4908                         } else {
   4909                             offset = TextUtils.unpackRangeEndFromLong(range);
   4910                         }
   4911                         positionAtCursorOffset(offset, false);
   4912                         return;
   4913                     }
   4914                 }
   4915                 // Handles can not cross and selection is at least one character.
   4916                 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
   4917             }
   4918             positionAtCursorOffset(offset, false);
   4919         }
   4920 
   4921         private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
   4922             mTextView.getLocationOnScreen(mTextViewLocation);
   4923             boolean nearEdge;
   4924             if (atRtl == isStartHandle()) {
   4925                 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
   4926                         - mTextView.getPaddingRight();
   4927                 nearEdge = x > rightEdge - mTextViewEdgeSlop;
   4928             } else {
   4929                 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
   4930                 nearEdge = x < leftEdge + mTextViewEdgeSlop;
   4931             }
   4932             return nearEdge;
   4933         }
   4934 
   4935         @Override
   4936         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
   4937             final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
   4938             return layout.isRtlCharAt(offsetToCheck);
   4939         }
   4940 
   4941         @Override
   4942         public float getHorizontal(@NonNull Layout layout, int offset) {
   4943             return getHorizontal(layout, offset, isStartHandle());
   4944         }
   4945 
   4946         private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
   4947             final int line = layout.getLineForOffset(offset);
   4948             final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
   4949             final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
   4950             final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
   4951             return (isRtlChar == isRtlParagraph) ?
   4952                     layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
   4953         }
   4954 
   4955         @Override
   4956         protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
   4957             final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
   4958             final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
   4959             if (!layout.isLevelBoundary(primaryOffset)) {
   4960                 return primaryOffset;
   4961             }
   4962             final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
   4963             final int currentOffset = getCurrentCursorOffset();
   4964             final int primaryDiff = Math.abs(primaryOffset - currentOffset);
   4965             final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
   4966             if (primaryDiff < secondaryDiff) {
   4967                 return primaryOffset;
   4968             } else if (primaryDiff > secondaryDiff) {
   4969                 return secondaryOffset;
   4970             } else {
   4971                 final int offsetToCheck = isStartHandle() ?
   4972                         currentOffset : Math.max(currentOffset - 1, 0);
   4973                 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
   4974                 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
   4975                 return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
   4976             }
   4977         }
   4978     }
   4979 
   4980     private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
   4981         final int trueLine = mTextView.getLineAtCoordinate(y);
   4982         if (layout == null || prevLine > layout.getLineCount()
   4983                 || layout.getLineCount() <= 0 || prevLine < 0) {
   4984             // Invalid parameters, just return whatever line is at y.
   4985             return trueLine;
   4986         }
   4987 
   4988         if (Math.abs(trueLine - prevLine) >= 2) {
   4989             // Only stick to lines if we're within a line of the previous selection.
   4990             return trueLine;
   4991         }
   4992 
   4993         final float verticalOffset = mTextView.viewportToContentVerticalOffset();
   4994         final int lineCount = layout.getLineCount();
   4995         final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
   4996 
   4997         final float firstLineTop = layout.getLineTop(0) + verticalOffset;
   4998         final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
   4999         final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
   5000 
   5001         final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
   5002         final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
   5003         final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
   5004 
   5005         // Determine if we've moved lines based on y position and previous line.
   5006         int currLine;
   5007         if (y <= yTopBound) {
   5008             currLine = Math.max(prevLine - 1, 0);
   5009         } else if (y >= yBottomBound) {
   5010             currLine = Math.min(prevLine + 1, lineCount - 1);
   5011         } else {
   5012             currLine = prevLine;
   5013         }
   5014         return currLine;
   5015     }
   5016 
   5017     /**
   5018      * A CursorController instance can be used to control a cursor in the text.
   5019      */
   5020     private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
   5021         /**
   5022          * Makes the cursor controller visible on screen.
   5023          * See also {@link #hide()}.
   5024          */
   5025         public void show();
   5026 
   5027         /**
   5028          * Hide the cursor controller from screen.
   5029          * See also {@link #show()}.
   5030          */
   5031         public void hide();
   5032 
   5033         /**
   5034          * Called when the view is detached from window. Perform house keeping task, such as
   5035          * stopping Runnable thread that would otherwise keep a reference on the context, thus
   5036          * preventing the activity from being recycled.
   5037          */
   5038         public void onDetached();
   5039 
   5040         public boolean isCursorBeingModified();
   5041 
   5042         public boolean isActive();
   5043     }
   5044 
   5045     private class InsertionPointCursorController implements CursorController {
   5046         private InsertionHandleView mHandle;
   5047 
   5048         public void show() {
   5049             getHandle().show();
   5050 
   5051             if (mSelectionModifierCursorController != null) {
   5052                 mSelectionModifierCursorController.hide();
   5053             }
   5054         }
   5055 
   5056         public void hide() {
   5057             if (mHandle != null) {
   5058                 mHandle.hide();
   5059             }
   5060         }
   5061 
   5062         public void onTouchModeChanged(boolean isInTouchMode) {
   5063             if (!isInTouchMode) {
   5064                 hide();
   5065             }
   5066         }
   5067 
   5068         private InsertionHandleView getHandle() {
   5069             if (mSelectHandleCenter == null) {
   5070                 mSelectHandleCenter = mTextView.getContext().getDrawable(
   5071                         mTextView.mTextSelectHandleRes);
   5072             }
   5073             if (mHandle == null) {
   5074                 mHandle = new InsertionHandleView(mSelectHandleCenter);
   5075             }
   5076             return mHandle;
   5077         }
   5078 
   5079         @Override
   5080         public void onDetached() {
   5081             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   5082             observer.removeOnTouchModeChangeListener(this);
   5083 
   5084             if (mHandle != null) mHandle.onDetached();
   5085         }
   5086 
   5087         @Override
   5088         public boolean isCursorBeingModified() {
   5089             return mHandle != null && mHandle.isDragging();
   5090         }
   5091 
   5092         @Override
   5093         public boolean isActive() {
   5094             return mHandle != null && mHandle.isShowing();
   5095         }
   5096 
   5097         public void invalidateHandle() {
   5098             if (mHandle != null) {
   5099                 mHandle.invalidate();
   5100             }
   5101         }
   5102     }
   5103 
   5104     class SelectionModifierCursorController implements CursorController {
   5105         // The cursor controller handles, lazily created when shown.
   5106         private SelectionHandleView mStartHandle;
   5107         private SelectionHandleView mEndHandle;
   5108         // The offsets of that last touch down event. Remembered to start selection there.
   5109         private int mMinTouchOffset, mMaxTouchOffset;
   5110 
   5111         private float mDownPositionX, mDownPositionY;
   5112         private boolean mGestureStayedInTapRegion;
   5113 
   5114         // Where the user first starts the drag motion.
   5115         private int mStartOffset = -1;
   5116 
   5117         private boolean mHaventMovedEnoughToStartDrag;
   5118         // The line that a selection happened most recently with the drag accelerator.
   5119         private int mLineSelectionIsOn = -1;
   5120         // Whether the drag accelerator has selected past the initial line.
   5121         private boolean mSwitchedLines = false;
   5122 
   5123         // Indicates the drag accelerator mode that the user is currently using.
   5124         private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
   5125         // Drag accelerator is inactive.
   5126         private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
   5127         // Character based selection by dragging. Only for mouse.
   5128         private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
   5129         // Word based selection by dragging. Enabled after long pressing or double tapping.
   5130         private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
   5131         // Paragraph based selection by dragging. Enabled after mouse triple click.
   5132         private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
   5133 
   5134         SelectionModifierCursorController() {
   5135             resetTouchOffsets();
   5136         }
   5137 
   5138         public void show() {
   5139             if (mTextView.isInBatchEditMode()) {
   5140                 return;
   5141             }
   5142             initDrawables();
   5143             initHandles();
   5144         }
   5145 
   5146         private void initDrawables() {
   5147             if (mSelectHandleLeft == null) {
   5148                 mSelectHandleLeft = mTextView.getContext().getDrawable(
   5149                         mTextView.mTextSelectHandleLeftRes);
   5150             }
   5151             if (mSelectHandleRight == null) {
   5152                 mSelectHandleRight = mTextView.getContext().getDrawable(
   5153                         mTextView.mTextSelectHandleRightRes);
   5154             }
   5155         }
   5156 
   5157         private void initHandles() {
   5158             // Lazy object creation has to be done before updatePosition() is called.
   5159             if (mStartHandle == null) {
   5160                 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
   5161                         com.android.internal.R.id.selection_start_handle,
   5162                         HANDLE_TYPE_SELECTION_START);
   5163             }
   5164             if (mEndHandle == null) {
   5165                 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
   5166                         com.android.internal.R.id.selection_end_handle,
   5167                         HANDLE_TYPE_SELECTION_END);
   5168             }
   5169 
   5170             mStartHandle.show();
   5171             mEndHandle.show();
   5172 
   5173             hideInsertionPointCursorController();
   5174         }
   5175 
   5176         public void hide() {
   5177             if (mStartHandle != null) mStartHandle.hide();
   5178             if (mEndHandle != null) mEndHandle.hide();
   5179         }
   5180 
   5181         public void enterDrag(int dragAcceleratorMode) {
   5182             // Just need to init the handles / hide insertion cursor.
   5183             show();
   5184             mDragAcceleratorMode = dragAcceleratorMode;
   5185             // Start location of selection.
   5186             mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
   5187                     mLastDownPositionY);
   5188             mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
   5189             // Don't show the handles until user has lifted finger.
   5190             hide();
   5191 
   5192             // This stops scrolling parents from intercepting the touch event, allowing
   5193             // the user to continue dragging across the screen to select text; TextView will
   5194             // scroll as necessary.
   5195             mTextView.getParent().requestDisallowInterceptTouchEvent(true);
   5196             mTextView.cancelLongPress();
   5197         }
   5198 
   5199         public void onTouchEvent(MotionEvent event) {
   5200             // This is done even when the View does not have focus, so that long presses can start
   5201             // selection and tap can move cursor from this tap position.
   5202             final float eventX = event.getX();
   5203             final float eventY = event.getY();
   5204             final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
   5205             switch (event.getActionMasked()) {
   5206                 case MotionEvent.ACTION_DOWN:
   5207                     if (extractedTextModeWillBeStarted()) {
   5208                         // Prevent duplicating the selection handles until the mode starts.
   5209                         hide();
   5210                     } else {
   5211                         // Remember finger down position, to be able to start selection from there.
   5212                         mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
   5213                                 eventX, eventY);
   5214 
   5215                         // Double tap detection
   5216                         if (mGestureStayedInTapRegion) {
   5217                             if (mTapState == TAP_STATE_DOUBLE_TAP
   5218                                     || mTapState == TAP_STATE_TRIPLE_CLICK) {
   5219                                 final float deltaX = eventX - mDownPositionX;
   5220                                 final float deltaY = eventY - mDownPositionY;
   5221                                 final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   5222 
   5223                                 ViewConfiguration viewConfiguration = ViewConfiguration.get(
   5224                                         mTextView.getContext());
   5225                                 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
   5226                                 boolean stayedInArea =
   5227                                         distanceSquared < doubleTapSlop * doubleTapSlop;
   5228 
   5229                                 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
   5230                                     if (mTapState == TAP_STATE_DOUBLE_TAP) {
   5231                                         selectCurrentWordAndStartDrag();
   5232                                     } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
   5233                                         selectCurrentParagraphAndStartDrag();
   5234                                     }
   5235                                     mDiscardNextActionUp = true;
   5236                                 }
   5237                             }
   5238                         }
   5239 
   5240                         mDownPositionX = eventX;
   5241                         mDownPositionY = eventY;
   5242                         mGestureStayedInTapRegion = true;
   5243                         mHaventMovedEnoughToStartDrag = true;
   5244                     }
   5245                     break;
   5246 
   5247                 case MotionEvent.ACTION_POINTER_DOWN:
   5248                 case MotionEvent.ACTION_POINTER_UP:
   5249                     // Handle multi-point gestures. Keep min and max offset positions.
   5250                     // Only activated for devices that correctly handle multi-touch.
   5251                     if (mTextView.getContext().getPackageManager().hasSystemFeature(
   5252                             PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
   5253                         updateMinAndMaxOffsets(event);
   5254                     }
   5255                     break;
   5256 
   5257                 case MotionEvent.ACTION_MOVE:
   5258                     final ViewConfiguration viewConfig = ViewConfiguration.get(
   5259                             mTextView.getContext());
   5260                     final int touchSlop = viewConfig.getScaledTouchSlop();
   5261 
   5262                     if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
   5263                         final float deltaX = eventX - mDownPositionX;
   5264                         final float deltaY = eventY - mDownPositionY;
   5265                         final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
   5266 
   5267                         if (mGestureStayedInTapRegion) {
   5268                             int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
   5269                             mGestureStayedInTapRegion =
   5270                                     distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
   5271                         }
   5272                         if (mHaventMovedEnoughToStartDrag) {
   5273                             // We don't start dragging until the user has moved enough.
   5274                             mHaventMovedEnoughToStartDrag =
   5275                                     distanceSquared <= touchSlop * touchSlop;
   5276                         }
   5277                     }
   5278 
   5279                     if (isMouse && !isDragAcceleratorActive()) {
   5280                         final int offset = mTextView.getOffsetForPosition(eventX, eventY);
   5281                         if (mTextView.hasSelection()
   5282                                 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
   5283                                 && offset >= mTextView.getSelectionStart()
   5284                                 && offset <= mTextView.getSelectionEnd()) {
   5285                             startDragAndDrop();
   5286                             break;
   5287                         }
   5288 
   5289                         if (mStartOffset != offset) {
   5290                             // Start character based drag accelerator.
   5291                             stopTextActionMode();
   5292                             enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
   5293                             mDiscardNextActionUp = true;
   5294                             mHaventMovedEnoughToStartDrag = false;
   5295                         }
   5296                     }
   5297 
   5298                     if (mStartHandle != null && mStartHandle.isShowing()) {
   5299                         // Don't do the drag if the handles are showing already.
   5300                         break;
   5301                     }
   5302 
   5303                     updateSelection(event);
   5304                     break;
   5305 
   5306                 case MotionEvent.ACTION_UP:
   5307                     if (!isDragAcceleratorActive()) {
   5308                         break;
   5309                     }
   5310                     updateSelection(event);
   5311 
   5312                     // No longer dragging to select text, let the parent intercept events.
   5313                     mTextView.getParent().requestDisallowInterceptTouchEvent(false);
   5314 
   5315                     // No longer the first dragging motion, reset.
   5316                     resetDragAcceleratorState();
   5317 
   5318                     if (mTextView.hasSelection()) {
   5319                         startSelectionActionMode();
   5320                     }
   5321                     break;
   5322             }
   5323         }
   5324 
   5325         private void updateSelection(MotionEvent event) {
   5326             if (mTextView.getLayout() != null) {
   5327                 switch (mDragAcceleratorMode) {
   5328                     case DRAG_ACCELERATOR_MODE_CHARACTER:
   5329                         updateCharacterBasedSelection(event);
   5330                         break;
   5331                     case DRAG_ACCELERATOR_MODE_WORD:
   5332                         updateWordBasedSelection(event);
   5333                         break;
   5334                     case DRAG_ACCELERATOR_MODE_PARAGRAPH:
   5335                         updateParagraphBasedSelection(event);
   5336                         break;
   5337                 }
   5338             }
   5339         }
   5340 
   5341         /**
   5342          * If the TextView allows text selection, selects the current paragraph and starts a drag.
   5343          *
   5344          * @return true if the drag was started.
   5345          */
   5346         private boolean selectCurrentParagraphAndStartDrag() {
   5347             if (mInsertionActionModeRunnable != null) {
   5348                 mTextView.removeCallbacks(mInsertionActionModeRunnable);
   5349             }
   5350             stopTextActionMode();
   5351             if (!selectCurrentParagraph()) {
   5352                 return false;
   5353             }
   5354             enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
   5355             return true;
   5356         }
   5357 
   5358         private void updateCharacterBasedSelection(MotionEvent event) {
   5359             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   5360             Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
   5361         }
   5362 
   5363         private void updateWordBasedSelection(MotionEvent event) {
   5364             if (mHaventMovedEnoughToStartDrag) {
   5365                 return;
   5366             }
   5367             final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
   5368             final ViewConfiguration viewConfig = ViewConfiguration.get(
   5369                     mTextView.getContext());
   5370             final float eventX = event.getX();
   5371             final float eventY = event.getY();
   5372             final int currLine;
   5373             if (isMouse) {
   5374                 // No need to offset the y coordinate for mouse input.
   5375                 currLine = mTextView.getLineAtCoordinate(eventY);
   5376             } else {
   5377                 float y = eventY;
   5378                 if (mSwitchedLines) {
   5379                     // Offset the finger by the same vertical offset as the handles.
   5380                     // This improves visibility of the content being selected by
   5381                     // shifting the finger below the content, this is applied once
   5382                     // the user has switched lines.
   5383                     final int touchSlop = viewConfig.getScaledTouchSlop();
   5384                     final float fingerOffset = (mStartHandle != null)
   5385                             ? mStartHandle.getIdealVerticalOffset()
   5386                             : touchSlop;
   5387                     y = eventY - fingerOffset;
   5388                 }
   5389 
   5390                 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
   5391                         y);
   5392                 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
   5393                     // Break early here, we want to offset the finger position from
   5394                     // the selection highlight, once the user moved their finger
   5395                     // to a different line we should apply the offset and *not* switch
   5396                     // lines until recomputing the position with the finger offset.
   5397                     mSwitchedLines = true;
   5398                     return;
   5399                 }
   5400             }
   5401 
   5402             int startOffset;
   5403             int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
   5404             // Snap to word boundaries.
   5405             if (mStartOffset < offset) {
   5406                 // Expanding with end handle.
   5407                 offset = getWordEnd(offset);
   5408                 startOffset = getWordStart(mStartOffset);
   5409             } else {
   5410                 // Expanding with start handle.
   5411                 offset = getWordStart(offset);
   5412                 startOffset = getWordEnd(mStartOffset);
   5413             }
   5414             mLineSelectionIsOn = currLine;
   5415             Selection.setSelection((Spannable) mTextView.getText(),
   5416                     startOffset, offset);
   5417         }
   5418 
   5419         private void updateParagraphBasedSelection(MotionEvent event) {
   5420             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
   5421 
   5422             final int start = Math.min(offset, mStartOffset);
   5423             final int end = Math.max(offset, mStartOffset);
   5424             final long paragraphsRange = getParagraphsRange(start, end);
   5425             final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
   5426             final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
   5427             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
   5428         }
   5429 
   5430         /**
   5431          * @param event
   5432          */
   5433         private void updateMinAndMaxOffsets(MotionEvent event) {
   5434             int pointerCount = event.getPointerCount();
   5435             for (int index = 0; index < pointerCount; index++) {
   5436                 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
   5437                 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
   5438                 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
   5439             }
   5440         }
   5441 
   5442         public int getMinTouchOffset() {
   5443             return mMinTouchOffset;
   5444         }
   5445 
   5446         public int getMaxTouchOffset() {
   5447             return mMaxTouchOffset;
   5448         }
   5449 
   5450         public void resetTouchOffsets() {
   5451             mMinTouchOffset = mMaxTouchOffset = -1;
   5452             resetDragAcceleratorState();
   5453         }
   5454 
   5455         private void resetDragAcceleratorState() {
   5456             mStartOffset = -1;
   5457             mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
   5458             mSwitchedLines = false;
   5459             final int selectionStart = mTextView.getSelectionStart();
   5460             final int selectionEnd = mTextView.getSelectionEnd();
   5461             if (selectionStart > selectionEnd) {
   5462                 Selection.setSelection((Spannable) mTextView.getText(),
   5463                         selectionEnd, selectionStart);
   5464             }
   5465         }
   5466 
   5467         /**
   5468          * @return true iff this controller is currently used to move the selection start.
   5469          */
   5470         public boolean isSelectionStartDragged() {
   5471             return mStartHandle != null && mStartHandle.isDragging();
   5472         }
   5473 
   5474         @Override
   5475         public boolean isCursorBeingModified() {
   5476             return isDragAcceleratorActive() || isSelectionStartDragged()
   5477                     || (mEndHandle != null && mEndHandle.isDragging());
   5478         }
   5479 
   5480         /**
   5481          * @return true if the user is selecting text using the drag accelerator.
   5482          */
   5483         public boolean isDragAcceleratorActive() {
   5484             return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
   5485         }
   5486 
   5487         public void onTouchModeChanged(boolean isInTouchMode) {
   5488             if (!isInTouchMode) {
   5489                 hide();
   5490             }
   5491         }
   5492 
   5493         @Override
   5494         public void onDetached() {
   5495             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
   5496             observer.removeOnTouchModeChangeListener(this);
   5497 
   5498             if (mStartHandle != null) mStartHandle.onDetached();
   5499             if (mEndHandle != null) mEndHandle.onDetached();
   5500         }
   5501 
   5502         @Override
   5503         public boolean isActive() {
   5504             return mStartHandle != null && mStartHandle.isShowing();
   5505         }
   5506 
   5507         public void invalidateHandles() {
   5508             if (mStartHandle != null) {
   5509                 mStartHandle.invalidate();
   5510             }
   5511             if (mEndHandle != null) {
   5512                 mEndHandle.invalidate();
   5513             }
   5514         }
   5515     }
   5516 
   5517     private class CorrectionHighlighter {
   5518         private final Path mPath = new Path();
   5519         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
   5520         private int mStart, mEnd;
   5521         private long mFadingStartTime;
   5522         private RectF mTempRectF;
   5523         private final static int FADE_OUT_DURATION = 400;
   5524 
   5525         public CorrectionHighlighter() {
   5526             mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
   5527                     applicationScale);
   5528             mPaint.setStyle(Paint.Style.FILL);
   5529         }
   5530 
   5531         public void highlight(CorrectionInfo info) {
   5532             mStart = info.getOffset();
   5533             mEnd = mStart + info.getNewText().length();
   5534             mFadingStartTime = SystemClock.uptimeMillis();
   5535 
   5536             if (mStart < 0 || mEnd < 0) {
   5537                 stopAnimation();
   5538             }
   5539         }
   5540 
   5541         public void draw(Canvas canvas, int cursorOffsetVertical) {
   5542             if (updatePath() && updatePaint()) {
   5543                 if (cursorOffsetVertical != 0) {
   5544                     canvas.translate(0, cursorOffsetVertical);
   5545                 }
   5546 
   5547                 canvas.drawPath(mPath, mPaint);
   5548 
   5549                 if (cursorOffsetVertical != 0) {
   5550                     canvas.translate(0, -cursorOffsetVertical);
   5551                 }
   5552                 invalidate(true); // TODO invalidate cursor region only
   5553             } else {
   5554                 stopAnimation();
   5555                 invalidate(false); // TODO invalidate cursor region only
   5556             }
   5557         }
   5558 
   5559         private boolean updatePaint() {
   5560             final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
   5561             if (duration > FADE_OUT_DURATION) return false;
   5562 
   5563             final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
   5564             final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
   5565             final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
   5566                     ((int) (highlightColorAlpha * coef) << 24);
   5567             mPaint.setColor(color);
   5568             return true;
   5569         }
   5570 
   5571         private boolean updatePath() {
   5572             final Layout layout = mTextView.getLayout();
   5573             if (layout == null) return false;
   5574 
   5575             // Update in case text is edited while the animation is run
   5576             final int length = mTextView.getText().length();
   5577             int start = Math.min(length, mStart);
   5578             int end = Math.min(length, mEnd);
   5579 
   5580             mPath.reset();
   5581             layout.getSelectionPath(start, end, mPath);
   5582             return true;
   5583         }
   5584 
   5585         private void invalidate(boolean delayed) {
   5586             if (mTextView.getLayout() == null) return;
   5587 
   5588             if (mTempRectF == null) mTempRectF = new RectF();
   5589             mPath.computeBounds(mTempRectF, false);
   5590 
   5591             int left = mTextView.getCompoundPaddingLeft();
   5592             int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
   5593 
   5594             if (delayed) {
   5595                 mTextView.postInvalidateOnAnimation(
   5596                         left + (int) mTempRectF.left, top + (int) mTempRectF.top,
   5597                         left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
   5598             } else {
   5599                 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
   5600                         (int) mTempRectF.right, (int) mTempRectF.bottom);
   5601             }
   5602         }
   5603 
   5604         private void stopAnimation() {
   5605             Editor.this.mCorrectionHighlighter = null;
   5606         }
   5607     }
   5608 
   5609     private static class ErrorPopup extends PopupWindow {
   5610         private boolean mAbove = false;
   5611         private final TextView mView;
   5612         private int mPopupInlineErrorBackgroundId = 0;
   5613         private int mPopupInlineErrorAboveBackgroundId = 0;
   5614 
   5615         ErrorPopup(TextView v, int width, int height) {
   5616             super(v, width, height);
   5617             mView = v;
   5618             // Make sure the TextView has a background set as it will be used the first time it is
   5619             // shown and positioned. Initialized with below background, which should have
   5620             // dimensions identical to the above version for this to work (and is more likely).
   5621             mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
   5622                     com.android.internal.R.styleable.Theme_errorMessageBackground);
   5623             mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
   5624         }
   5625 
   5626         void fixDirection(boolean above) {
   5627             mAbove = above;
   5628 
   5629             if (above) {
   5630                 mPopupInlineErrorAboveBackgroundId =
   5631                     getResourceId(mPopupInlineErrorAboveBackgroundId,
   5632                             com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
   5633             } else {
   5634                 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
   5635                         com.android.internal.R.styleable.Theme_errorMessageBackground);
   5636             }
   5637 
   5638             mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
   5639                 mPopupInlineErrorBackgroundId);
   5640         }
   5641 
   5642         private int getResourceId(int currentId, int index) {
   5643             if (currentId == 0) {
   5644                 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
   5645                         R.styleable.Theme);
   5646                 currentId = styledAttributes.getResourceId(index, 0);
   5647                 styledAttributes.recycle();
   5648             }
   5649             return currentId;
   5650         }
   5651 
   5652         @Override
   5653         public void update(int x, int y, int w, int h, boolean force) {
   5654             super.update(x, y, w, h, force);
   5655 
   5656             boolean above = isAboveAnchor();
   5657             if (above != mAbove) {
   5658                 fixDirection(above);
   5659             }
   5660         }
   5661     }
   5662 
   5663     static class InputContentType {
   5664         int imeOptions = EditorInfo.IME_NULL;
   5665         String privateImeOptions;
   5666         CharSequence imeActionLabel;
   5667         int imeActionId;
   5668         Bundle extras;
   5669         OnEditorActionListener onEditorActionListener;
   5670         boolean enterDown;
   5671         LocaleList imeHintLocales;
   5672     }
   5673 
   5674     static class InputMethodState {
   5675         ExtractedTextRequest mExtractedTextRequest;
   5676         final ExtractedText mExtractedText = new ExtractedText();
   5677         int mBatchEditNesting;
   5678         boolean mCursorChanged;
   5679         boolean mSelectionModeChanged;
   5680         boolean mContentChanged;
   5681         int mChangedStart, mChangedEnd, mChangedDelta;
   5682     }
   5683 
   5684     /**
   5685      * @return True iff (start, end) is a valid range within the text.
   5686      */
   5687     private static boolean isValidRange(CharSequence text, int start, int end) {
   5688         return 0 <= start && start <= end && end <= text.length();
   5689     }
   5690 
   5691     @VisibleForTesting
   5692     public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
   5693         return mSuggestionsPopupWindow;
   5694     }
   5695 
   5696     /**
   5697      * An InputFilter that monitors text input to maintain undo history. It does not modify the
   5698      * text being typed (and hence always returns null from the filter() method).
   5699      */
   5700     public static class UndoInputFilter implements InputFilter {
   5701         private final Editor mEditor;
   5702 
   5703         // Whether the current filter pass is directly caused by an end-user text edit.
   5704         private boolean mIsUserEdit;
   5705 
   5706         // Whether the text field is handling an IME composition. Must be parceled in case the user
   5707         // rotates the screen during composition.
   5708         private boolean mHasComposition;
   5709 
   5710         // Whether to merge events into one operation.
   5711         private boolean mForceMerge;
   5712 
   5713         public UndoInputFilter(Editor editor) {
   5714             mEditor = editor;
   5715         }
   5716 
   5717         public void saveInstanceState(Parcel parcel) {
   5718             parcel.writeInt(mIsUserEdit ? 1 : 0);
   5719             parcel.writeInt(mHasComposition ? 1 : 0);
   5720         }
   5721 
   5722         public void restoreInstanceState(Parcel parcel) {
   5723             mIsUserEdit = parcel.readInt() != 0;
   5724             mHasComposition = parcel.readInt() != 0;
   5725         }
   5726 
   5727         public void setForceMerge(boolean forceMerge) {
   5728             mForceMerge = forceMerge;
   5729         }
   5730 
   5731         /**
   5732          * Signals that a user-triggered edit is starting.
   5733          */
   5734         public void beginBatchEdit() {
   5735             if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
   5736             mIsUserEdit = true;
   5737         }
   5738 
   5739         public void endBatchEdit() {
   5740             if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
   5741             mIsUserEdit = false;
   5742         }
   5743 
   5744         @Override
   5745         public CharSequence filter(CharSequence source, int start, int end,
   5746                 Spanned dest, int dstart, int dend) {
   5747             if (DEBUG_UNDO) {
   5748                 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " +
   5749                         "dest=" + dest + " (" + dstart + "-" + dend + ")");
   5750             }
   5751 
   5752             // Check to see if this edit should be tracked for undo.
   5753             if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
   5754                 return null;
   5755             }
   5756 
   5757             // Check for and handle IME composition edits.
   5758             if (handleCompositionEdit(source, start, end, dstart)) {
   5759                 return null;
   5760             }
   5761 
   5762             // Handle keyboard edits.
   5763             handleKeyboardEdit(source, start, end, dest, dstart, dend);
   5764             return null;
   5765         }
   5766 
   5767         /**
   5768          * Returns true iff the edit was handled, either because it should be ignored or because
   5769          * this function created an undo operation for it.
   5770          */
   5771         private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) {
   5772             // Ignore edits while the user is composing.
   5773             if (isComposition(source)) {
   5774                 mHasComposition = true;
   5775                 return true;
   5776             }
   5777             final boolean hadComposition = mHasComposition;
   5778             mHasComposition = false;
   5779 
   5780             // Check for the transition out of the composing state.
   5781             if (hadComposition) {
   5782                 // If there was no text the user canceled composition. Ignore the edit.
   5783                 if (start == end) {
   5784                     return true;
   5785                 }
   5786 
   5787                 // Otherwise the user inserted the composition.
   5788                 String newText = TextUtils.substring(source, start, end);
   5789                 EditOperation edit = new EditOperation(mEditor, "", dstart, newText);
   5790                 recordEdit(edit, mForceMerge);
   5791                 return true;
   5792             }
   5793 
   5794             // This was neither a composition event nor a transition out of composing.
   5795             return false;
   5796         }
   5797 
   5798         private void handleKeyboardEdit(CharSequence source, int start, int end,
   5799                 Spanned dest, int dstart, int dend) {
   5800             // An application may install a TextWatcher to provide additional modifications after
   5801             // the initial input filters run (e.g. a credit card formatter that adds spaces to a
   5802             // string). This results in multiple filter() calls for what the user considers to be
   5803             // a single operation. Always undo the whole set of changes in one step.
   5804             final boolean forceMerge = mForceMerge || isInTextWatcher();
   5805 
   5806             // Build a new operation with all the information from this edit.
   5807             String newText = TextUtils.substring(source, start, end);
   5808             String oldText = TextUtils.substring(dest, dstart, dend);
   5809             EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText);
   5810             recordEdit(edit, forceMerge);
   5811         }
   5812 
   5813         /**
   5814          * Fetches the last undo operation and checks to see if a new edit should be merged into it.
   5815          * If forceMerge is true then the new edit is always merged.
   5816          */
   5817         private void recordEdit(EditOperation edit, boolean forceMerge) {
   5818             // Fetch the last edit operation and attempt to merge in the new edit.
   5819             final UndoManager um = mEditor.mUndoManager;
   5820             um.beginUpdate("Edit text");
   5821             EditOperation lastEdit = um.getLastOperation(
   5822                   EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
   5823             if (lastEdit == null) {
   5824                 // Add this as the first edit.
   5825                 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
   5826                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
   5827             } else if (forceMerge) {
   5828                 // Forced merges take priority because they could be the result of a non-user-edit
   5829                 // change and this case should not create a new undo operation.
   5830                 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
   5831                 lastEdit.forceMergeWith(edit);
   5832             } else if (!mIsUserEdit) {
   5833                 // An application directly modified the Editable outside of a text edit. Treat this
   5834                 // as a new change and don't attempt to merge.
   5835                 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
   5836                 um.commitState(mEditor.mUndoOwner);
   5837                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
   5838             } else if (lastEdit.mergeWith(edit)) {
   5839                 // Merge succeeded, nothing else to do.
   5840                 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
   5841             } else {
   5842                 // Could not merge with the last edit, so commit the last edit and add this edit.
   5843                 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
   5844                 um.commitState(mEditor.mUndoOwner);
   5845                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
   5846             }
   5847             um.endUpdate();
   5848         }
   5849 
   5850         private boolean canUndoEdit(CharSequence source, int start, int end,
   5851                 Spanned dest, int dstart, int dend) {
   5852             if (!mEditor.mAllowUndo) {
   5853                 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
   5854                 return false;
   5855             }
   5856 
   5857             if (mEditor.mUndoManager.isInUndo()) {
   5858                 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
   5859                 return false;
   5860             }
   5861 
   5862             // Text filters run before input operations are applied. However, some input operations
   5863             // are invalid and will throw exceptions when applied. This is common in tests. Don't
   5864             // attempt to undo invalid operations.
   5865             if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
   5866                 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
   5867                 return false;
   5868             }
   5869 
   5870             // Earlier filters can rewrite input to be a no-op, for example due to a length limit
   5871             // on an input field. Skip no-op changes.
   5872             if (start == end && dstart == dend) {
   5873                 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
   5874                 return false;
   5875             }
   5876 
   5877             return true;
   5878         }
   5879 
   5880         private boolean isComposition(CharSequence source) {
   5881             if (!(source instanceof Spannable)) {
   5882                 return false;
   5883             }
   5884             // This is a composition edit if the source has a non-zero-length composing span.
   5885             Spannable text = (Spannable) source;
   5886             int composeBegin = EditableInputConnection.getComposingSpanStart(text);
   5887             int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
   5888             return composeBegin < composeEnd;
   5889         }
   5890 
   5891         private boolean isInTextWatcher() {
   5892             CharSequence text = mEditor.mTextView.getText();
   5893             return (text instanceof SpannableStringBuilder)
   5894                     && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
   5895         }
   5896     }
   5897 
   5898     /**
   5899      * An operation to undo a single "edit" to a text view.
   5900      */
   5901     public static class EditOperation extends UndoOperation<Editor> {
   5902         private static final int TYPE_INSERT = 0;
   5903         private static final int TYPE_DELETE = 1;
   5904         private static final int TYPE_REPLACE = 2;
   5905 
   5906         private int mType;
   5907         private String mOldText;
   5908         private int mOldTextStart;
   5909         private String mNewText;
   5910         private int mNewTextStart;
   5911 
   5912         private int mOldCursorPos;
   5913         private int mNewCursorPos;
   5914 
   5915         /**
   5916          * Constructs an edit operation from a text input operation on editor that replaces the
   5917          * oldText starting at dstart with newText.
   5918          */
   5919         public EditOperation(Editor editor, String oldText, int dstart, String newText) {
   5920             super(editor.mUndoOwner);
   5921             mOldText = oldText;
   5922             mNewText = newText;
   5923 
   5924             // Determine the type of the edit and store where it occurred. Avoid storing
   5925             // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
   5926             // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being
   5927             // outside the bounds of the final text).
   5928             if (mNewText.length() > 0 && mOldText.length() == 0) {
   5929                 mType = TYPE_INSERT;
   5930                 mNewTextStart = dstart;
   5931             } else if (mNewText.length() == 0 && mOldText.length() > 0) {
   5932                 mType = TYPE_DELETE;
   5933                 mOldTextStart = dstart;
   5934             } else {
   5935                 mType = TYPE_REPLACE;
   5936                 mOldTextStart = mNewTextStart = dstart;
   5937             }
   5938 
   5939             // Store cursor data.
   5940             mOldCursorPos = editor.mTextView.getSelectionStart();
   5941             mNewCursorPos = dstart + mNewText.length();
   5942         }
   5943 
   5944         public EditOperation(Parcel src, ClassLoader loader) {
   5945             super(src, loader);
   5946             mType = src.readInt();
   5947             mOldText = src.readString();
   5948             mOldTextStart = src.readInt();
   5949             mNewText = src.readString();
   5950             mNewTextStart = src.readInt();
   5951             mOldCursorPos = src.readInt();
   5952             mNewCursorPos = src.readInt();
   5953         }
   5954 
   5955         @Override
   5956         public void writeToParcel(Parcel dest, int flags) {
   5957             dest.writeInt(mType);
   5958             dest.writeString(mOldText);
   5959             dest.writeInt(mOldTextStart);
   5960             dest.writeString(mNewText);
   5961             dest.writeInt(mNewTextStart);
   5962             dest.writeInt(mOldCursorPos);
   5963             dest.writeInt(mNewCursorPos);
   5964         }
   5965 
   5966         private int getNewTextEnd() {
   5967             return mNewTextStart + mNewText.length();
   5968         }
   5969 
   5970         private int getOldTextEnd() {
   5971             return mOldTextStart + mOldText.length();
   5972         }
   5973 
   5974         @Override
   5975         public void commit() {
   5976         }
   5977 
   5978         @Override
   5979         public void undo() {
   5980             if (DEBUG_UNDO) Log.d(TAG, "undo");
   5981             // Remove the new text and insert the old.
   5982             Editor editor = getOwnerData();
   5983             Editable text = (Editable) editor.mTextView.getText();
   5984             modifyText(text, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
   5985                     mOldCursorPos);
   5986         }
   5987 
   5988         @Override
   5989         public void redo() {
   5990             if (DEBUG_UNDO) Log.d(TAG, "redo");
   5991             // Remove the old text and insert the new.
   5992             Editor editor = getOwnerData();
   5993             Editable text = (Editable) editor.mTextView.getText();
   5994             modifyText(text, mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart,
   5995                     mNewCursorPos);
   5996         }
   5997 
   5998         /**
   5999          * Attempts to merge this existing operation with a new edit.
   6000          * @param edit The new edit operation.
   6001          * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
   6002          * object unchanged.
   6003          */
   6004         private boolean mergeWith(EditOperation edit) {
   6005             if (DEBUG_UNDO) {
   6006                 Log.d(TAG, "mergeWith old " + this);
   6007                 Log.d(TAG, "mergeWith new " + edit);
   6008             }
   6009             switch (mType) {
   6010                 case TYPE_INSERT:
   6011                     return mergeInsertWith(edit);
   6012                 case TYPE_DELETE:
   6013                     return mergeDeleteWith(edit);
   6014                 case TYPE_REPLACE:
   6015                     return mergeReplaceWith(edit);
   6016                 default:
   6017                     return false;
   6018             }
   6019         }
   6020 
   6021         private boolean mergeInsertWith(EditOperation edit) {
   6022             // Only merge continuous insertions.
   6023             if (edit.mType != TYPE_INSERT) {
   6024                 return false;
   6025             }
   6026             // Only merge insertions that are contiguous.
   6027             if (getNewTextEnd() != edit.mNewTextStart) {
   6028                 return false;
   6029             }
   6030             mNewText += edit.mNewText;
   6031             mNewCursorPos = edit.mNewCursorPos;
   6032             return true;
   6033         }
   6034 
   6035         // TODO: Support forward delete.
   6036         private boolean mergeDeleteWith(EditOperation edit) {
   6037             // Only merge continuous deletes.
   6038             if (edit.mType != TYPE_DELETE) {
   6039                 return false;
   6040             }
   6041             // Only merge deletions that are contiguous.
   6042             if (mOldTextStart != edit.getOldTextEnd()) {
   6043                 return false;
   6044             }
   6045             mOldTextStart = edit.mOldTextStart;
   6046             mOldText = edit.mOldText + mOldText;
   6047             mNewCursorPos = edit.mNewCursorPos;
   6048             return true;
   6049         }
   6050 
   6051         private boolean mergeReplaceWith(EditOperation edit) {
   6052             // Replacements can merge only with adjacent inserts.
   6053             if (edit.mType != TYPE_INSERT || getNewTextEnd() != edit.mNewTextStart) {
   6054                 return false;
   6055             }
   6056             mOldText += edit.mOldText;
   6057             mNewText += edit.mNewText;
   6058             mNewCursorPos = edit.mNewCursorPos;
   6059             return true;
   6060         }
   6061 
   6062         /**
   6063          * Forcibly creates a single merged edit operation by simulating the entire text
   6064          * contents being replaced.
   6065          */
   6066         public void forceMergeWith(EditOperation edit) {
   6067             if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
   6068             Editor editor = getOwnerData();
   6069 
   6070             // Copy the text of the current field.
   6071             // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
   6072             // but would require two parallel implementations of modifyText() because Editable and
   6073             // StringBuilder do not share an interface for replace/delete/insert.
   6074             Editable editable = (Editable) editor.mTextView.getText();
   6075             Editable originalText = new SpannableStringBuilder(editable.toString());
   6076 
   6077             // Roll back the last operation.
   6078             modifyText(originalText, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart,
   6079                     mOldCursorPos);
   6080 
   6081             // Clone the text again and apply the new operation.
   6082             Editable finalText = new SpannableStringBuilder(editable.toString());
   6083             modifyText(finalText, edit.mOldTextStart, edit.getOldTextEnd(), edit.mNewText,
   6084                     edit.mNewTextStart, edit.mNewCursorPos);
   6085 
   6086             // Convert this operation into a non-mergeable replacement of the entire string.
   6087             mType = TYPE_REPLACE;
   6088             mNewText = finalText.toString();
   6089             mNewTextStart = 0;
   6090             mOldText = originalText.toString();
   6091             mOldTextStart = 0;
   6092             mNewCursorPos = edit.mNewCursorPos;
   6093             // mOldCursorPos is unchanged.
   6094         }
   6095 
   6096         private static void modifyText(Editable text, int deleteFrom, int deleteTo,
   6097                 CharSequence newText, int newTextInsertAt, int newCursorPos) {
   6098             // Apply the edit if it is still valid.
   6099             if (isValidRange(text, deleteFrom, deleteTo) &&
   6100                     newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
   6101                 if (deleteFrom != deleteTo) {
   6102                     text.delete(deleteFrom, deleteTo);
   6103                 }
   6104                 if (newText.length() != 0) {
   6105                     text.insert(newTextInsertAt, newText);
   6106                 }
   6107             }
   6108             // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
   6109             // don't explicitly set it and rely on SpannableStringBuilder to position it.
   6110             // TODO: Select all the text that was undone.
   6111             if (0 <= newCursorPos && newCursorPos <= text.length()) {
   6112                 Selection.setSelection(text, newCursorPos);
   6113             }
   6114         }
   6115 
   6116         private String getTypeString() {
   6117             switch (mType) {
   6118                 case TYPE_INSERT:
   6119                     return "insert";
   6120                 case TYPE_DELETE:
   6121                     return "delete";
   6122                 case TYPE_REPLACE:
   6123                     return "replace";
   6124                 default:
   6125                     return "";
   6126             }
   6127         }
   6128 
   6129         @Override
   6130         public String toString() {
   6131             return "[mType=" + getTypeString() + ", " +
   6132                     "mOldText=" + mOldText + ", " +
   6133                     "mOldTextStart=" + mOldTextStart + ", " +
   6134                     "mNewText=" + mNewText + ", " +
   6135                     "mNewTextStart=" + mNewTextStart + ", " +
   6136                     "mOldCursorPos=" + mOldCursorPos + ", " +
   6137                     "mNewCursorPos=" + mNewCursorPos + "]";
   6138         }
   6139 
   6140         public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR
   6141                 = new Parcelable.ClassLoaderCreator<EditOperation>() {
   6142             @Override
   6143             public EditOperation createFromParcel(Parcel in) {
   6144                 return new EditOperation(in, null);
   6145             }
   6146 
   6147             @Override
   6148             public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
   6149                 return new EditOperation(in, loader);
   6150             }
   6151 
   6152             @Override
   6153             public EditOperation[] newArray(int size) {
   6154                 return new EditOperation[size];
   6155             }
   6156         };
   6157     }
   6158 
   6159     /**
   6160      * A helper for enabling and handling "PROCESS_TEXT" menu actions.
   6161      * These allow external applications to plug into currently selected text.
   6162      */
   6163     static final class ProcessTextIntentActionsHandler {
   6164 
   6165         private final Editor mEditor;
   6166         private final TextView mTextView;
   6167         private final PackageManager mPackageManager;
   6168         private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<Intent>();
   6169         private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions
   6170                 = new SparseArray<AccessibilityNodeInfo.AccessibilityAction>();
   6171 
   6172         private ProcessTextIntentActionsHandler(Editor editor) {
   6173             mEditor = Preconditions.checkNotNull(editor);
   6174             mTextView = Preconditions.checkNotNull(mEditor.mTextView);
   6175             mPackageManager = Preconditions.checkNotNull(
   6176                     mTextView.getContext().getPackageManager());
   6177         }
   6178 
   6179         /**
   6180          * Adds "PROCESS_TEXT" menu items to the specified menu.
   6181          */
   6182         public void onInitializeMenu(Menu menu) {
   6183             int i = 0;
   6184             for (ResolveInfo resolveInfo : getSupportedActivities()) {
   6185                 menu.add(Menu.NONE, Menu.NONE,
   6186                         Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
   6187                         getLabel(resolveInfo))
   6188                         .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
   6189                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
   6190             }
   6191         }
   6192 
   6193         /**
   6194          * Performs a "PROCESS_TEXT" action if there is one associated with the specified
   6195          * menu item.
   6196          *
   6197          * @return True if the action was performed, false otherwise.
   6198          */
   6199         public boolean performMenuItemAction(MenuItem item) {
   6200             return fireIntent(item.getIntent());
   6201         }
   6202 
   6203         /**
   6204          * Initializes and caches "PROCESS_TEXT" accessibility actions.
   6205          */
   6206         public void initializeAccessibilityActions() {
   6207             mAccessibilityIntents.clear();
   6208             mAccessibilityActions.clear();
   6209             int i = 0;
   6210             for (ResolveInfo resolveInfo : getSupportedActivities()) {
   6211                 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
   6212                 mAccessibilityActions.put(
   6213                         actionId,
   6214                         new AccessibilityNodeInfo.AccessibilityAction(
   6215                                 actionId, getLabel(resolveInfo)));
   6216                 mAccessibilityIntents.put(
   6217                         actionId, createProcessTextIntentForResolveInfo(resolveInfo));
   6218             }
   6219         }
   6220 
   6221         /**
   6222          * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
   6223          * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
   6224          * latest accessibility actions available for this call.
   6225          */
   6226         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
   6227             for (int i = 0; i < mAccessibilityActions.size(); i++) {
   6228                 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
   6229             }
   6230         }
   6231 
   6232         /**
   6233          * Performs a "PROCESS_TEXT" action if there is one associated with the specified
   6234          * accessibility action id.
   6235          *
   6236          * @return True if the action was performed, false otherwise.
   6237          */
   6238         public boolean performAccessibilityAction(int actionId) {
   6239             return fireIntent(mAccessibilityIntents.get(actionId));
   6240         }
   6241 
   6242         private boolean fireIntent(Intent intent) {
   6243             if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
   6244                 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
   6245                 mEditor.mPreserveSelection = true;
   6246                 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
   6247                 return true;
   6248             }
   6249             return false;
   6250         }
   6251 
   6252         private List<ResolveInfo> getSupportedActivities() {
   6253             PackageManager packageManager = mTextView.getContext().getPackageManager();
   6254             return packageManager.queryIntentActivities(createProcessTextIntent(), 0);
   6255         }
   6256 
   6257         private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
   6258             return createProcessTextIntent()
   6259                     .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
   6260                     .setClassName(info.activityInfo.packageName, info.activityInfo.name);
   6261         }
   6262 
   6263         private Intent createProcessTextIntent() {
   6264             return new Intent()
   6265                     .setAction(Intent.ACTION_PROCESS_TEXT)
   6266                     .setType("text/plain");
   6267         }
   6268 
   6269         private CharSequence getLabel(ResolveInfo resolveInfo) {
   6270             return resolveInfo.loadLabel(mPackageManager);
   6271         }
   6272     }
   6273 }
   6274