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