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