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;
TextRenderNode(String name)250         public TextRenderNode(String name) {
251             renderNode = RenderNode.create(name, null);
252             isDirty = true;
253             needsToBeShifted = true;
254         }
needsRecord()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 
Editor(TextView textView)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 
saveInstanceState()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 
restoreInstanceState(ParcelableParcel state)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      */
forgetUndoRedo()405     void forgetUndoRedo() {
406         UndoOwner[] owners = { mUndoOwner };
407         mUndoManager.forgetUndos(owners, -1 /* all */);
408         mUndoManager.forgetRedos(owners, -1 /* all */);
409     }
410 
canUndo()411     boolean canUndo() {
412         UndoOwner[] owners = { mUndoOwner };
413         return mAllowUndo && mUndoManager.countUndos(owners) > 0;
414     }
415 
canRedo()416     boolean canRedo() {
417         UndoOwner[] owners = { mUndoOwner };
418         return mAllowUndo && mUndoManager.countRedos(owners) > 0;
419     }
420 
undo()421     void undo() {
422         if (!mAllowUndo) {
423             return;
424         }
425         UndoOwner[] owners = { mUndoOwner };
426         mUndoManager.undo(owners, 1);  // Undo 1 action.
427     }
428 
redo()429     void redo() {
430         if (!mAllowUndo) {
431             return;
432         }
433         UndoOwner[] owners = { mUndoOwner };
434         mUndoManager.redo(owners, 1);  // Redo 1 action.
435     }
436 
replace()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 
onAttachedToWindow()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 
onDetachedFromWindow()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 
discardTextDisplayLists()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 
showError()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 
setError(CharSequence error, Drawable icon)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 
setErrorIcon(Drawable icon)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 
hideError()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      */
getErrorX()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      */
getErrorY()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 
createInputContentTypeIfNeeded()682     void createInputContentTypeIfNeeded() {
683         if (mInputContentType == null) {
684             mInputContentType = new InputContentType();
685         }
686     }
687 
createInputMethodStateIfNeeded()688     void createInputMethodStateIfNeeded() {
689         if (mInputMethodState == null) {
690             mInputMethodState = new InputMethodState();
691         }
692     }
693 
isCursorVisible()694     private boolean isCursorVisible() {
695         // The default value is true, even when there is no associated Editor
696         return mCursorVisible && mTextView.isTextEditable();
697     }
698 
shouldRenderCursor()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 
prepareCursorControllers()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 
hideInsertionPointCursorController()741     void hideInsertionPointCursorController() {
742         if (mInsertionPointCursorController != null) {
743             mInsertionPointCursorController.hide();
744         }
745     }
746 
747     /**
748      * Hides the insertion and span controllers.
749      */
hideCursorAndSpanControllers()750     void hideCursorAndSpanControllers() {
751         hideCursorControllers();
752         hideSpanControllers();
753     }
754 
hideSpanControllers()755     private void hideSpanControllers() {
756         if (mSpanController != null) {
757             mSpanController.hide();
758         }
759     }
760 
hideCursorControllers()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      */
updateSpellCheckSpans(int start, int end, boolean createSpellChecker)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 
onScreenStateChanged(int screenState)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 
suspendBlink()804     private void suspendBlink() {
805         if (mBlink != null) {
806             mBlink.cancel();
807         }
808     }
809 
resumeBlink()810     private void resumeBlink() {
811         if (mBlink != null) {
812             mBlink.uncancel();
813             makeBlink();
814         }
815     }
816 
adjustInputType(boolean password, boolean passwordInputType, boolean webPasswordInputType, boolean numberPasswordInputType)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 
chooseSize(@onNull PopupWindow pop, @NonNull CharSequence text, @NonNull TextView tv)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 
setFrame()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 
getWordStart(int offset)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 
getWordEnd(int offset)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 
needsToSelectAllToSelectWordOrParagraph()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      */
selectCurrentWord()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      */
selectCurrentParagraph()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      */
getParagraphsRange(int startOffset, int endOffset)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 
onLocaleChanged()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 
getWordIterator()1046     public WordIterator getWordIterator() {
1047         if (mWordIterator == null) {
1048             mWordIterator = new WordIterator(mTextView.getTextServicesLocale());
1049         }
1050         return mWordIterator;
1051     }
1052 
getWordIteratorWithText()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 
getNextCursorOffset(int offset, boolean findAfterGivenOffset)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 
getCharClusterRange(int offset)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 
touchPositionIsInSelection()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 
getPositionListener()1114     private PositionListener getPositionListener() {
1115         if (mPositionListener == null) {
1116             mPositionListener = new PositionListener();
1117         }
1118         return mPositionListener;
1119     }
1120 
1121     private interface TextViewPositionListener {
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)1122         public void updatePosition(int parentPositionX, int parentPositionY,
1123                 boolean parentPositionChanged, boolean parentScrolled);
1124     }
1125 
isOffsetVisible(int offset)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      */
isPositionOnText(float x, float y)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 
startDragAndDrop()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 
performLongClick(boolean handled)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 
getLastUpPositionX()1221     float getLastUpPositionX() {
1222         return mLastUpPositionX;
1223     }
1224 
getLastUpPositionY()1225     float getLastUpPositionY() {
1226         return mLastUpPositionY;
1227     }
1228 
getLastTouchOffsets()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 
onFocusChanged(boolean focused, int direction)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 
ensureNoSelectionIfNonSelectable()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      */
downgradeEasyCorrectionSpans()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 
sendOnTextChanged(int start, int before, int after)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 
getLastTapPosition()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 
onWindowFocusChanged(boolean hasWindowFocus)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 
updateTapState(MotionEvent event)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 
shouldFilterOutTouchEvent(MotionEvent event)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 
onTouchEvent(MotionEvent event)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 
updateFloatingToolbarVisibility(MotionEvent event)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 
hideFloatingToolbar(int duration)1512     void hideFloatingToolbar(int duration) {
1513         if (mTextActionMode != null) {
1514             mTextView.removeCallbacks(mShowFloatingToolbar);
1515             mTextActionMode.hide(duration);
1516         }
1517     }
1518 
showFloatingToolbar()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 
beginBatchEdit()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 
endBatchEdit()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 
ensureEndedBatchEdit()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 
finishBatchEdit(final InputMethodState ims)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 
extractText(ExtractedTextRequest request, ExtractedText outText)1605     boolean extractText(ExtractedTextRequest request, ExtractedText outText) {
1606         return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN,
1607                 EXTRACT_UNKNOWN, outText);
1608     }
1609 
extractTextInternal(@ullable ExtractedTextRequest request, int partialStartOffset, int partialEndOffset, int delta, @Nullable ExtractedText outText)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 
reportExtractedText()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 
sendUpdateSelection()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 
onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, int cursorOffsetVertical)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 
drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, int cursorOffsetVertical)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 
drawHardwareAcceleratedInner(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, int cursorOffsetVertical, int[] blockEndLines, int[] blockIndices, int blockInfoIndex, int numberOfBlocks, int startIndexToFindAvailableRenderNode)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 
getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, int searchStartIndex)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 
drawCursor(Canvas canvas, int cursorOffsetVertical)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 
invalidateHandlesAndActionMode()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      */
invalidateTextDisplayList(Layout layout, int start, int end)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 
invalidateTextDisplayList()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 
updateCursorPosition()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 
refreshTextActionMode()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      */
startInsertionActionMode()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
getTextView()2114     TextView getTextView() {
2115         return mTextView;
2116     }
2117 
2118     @Nullable
getTextActionMode()2119     ActionMode getTextActionMode() {
2120         return mTextActionMode;
2121     }
2122 
setRestartActionModeOnNextRefresh(boolean value)2123     void setRestartActionModeOnNextRefresh(boolean value) {
2124         mRestartActionModeOnNextRefresh = value;
2125     }
2126 
2127     /**
2128      * Asynchronously starts a selection action mode using the TextClassifier.
2129      */
startSelectionActionModeAsync(boolean adjustSelection)2130     void startSelectionActionModeAsync(boolean adjustSelection) {
2131         getSelectionActionModeHelper().startSelectionActionModeAsync(adjustSelection);
2132     }
2133 
startLinkActionModeAsync(int start, int end)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      */
invalidateActionModeAsync()2146     void invalidateActionModeAsync() {
2147         getSelectionActionModeHelper().invalidateActionModeAsync();
2148     }
2149 
2150     /**
2151      * Synchronously invalidates an action mode without the TextClassifier.
2152      */
invalidateActionMode()2153     private void invalidateActionMode() {
2154         if (mTextActionMode != null) {
2155             mTextActionMode.invalidate();
2156         }
2157     }
2158 
getSelectionActionModeHelper()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      */
selectCurrentWordAndStartDrag()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      */
checkField()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 
startActionModeInternal(@extActionMode int actionMode)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 
extractedTextModeWillBeStarted()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      */
shouldOfferToShowSuggestions()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      */
isCursorInsideEasyCorrectionSpan()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 
onTouchUpEvent(MotionEvent event)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 
stopTextActionMode()2379     protected void stopTextActionMode() {
2380         if (mTextActionMode != null) {
2381             // This will hide the mSelectionModifierCursorController
2382             mTextActionMode.finish();
2383         }
2384     }
2385 
stopTextActionModeWithPreservingSelection()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      */
hasInsertionController()2398     boolean hasInsertionController() {
2399         return mInsertionControllerEnabled;
2400     }
2401 
2402     /**
2403      * @return True if this view supports selection handles.
2404      */
hasSelectionController()2405     boolean hasSelectionController() {
2406         return mSelectionControllerEnabled;
2407     }
2408 
getInsertionController()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
getSelectionController()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
getCursorDrawable()2442     public Drawable getCursorDrawable() {
2443         return mDrawableForCursor;
2444     }
2445 
updateCursorPosition(int top, int bottom, float horizontal)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      */
clampHorizontalPosition(@ullable final Drawable drawable, float horizontal)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      */
onCommitCorrection(CorrectionInfo info)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 
onScrollChanged()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      */
shouldBlink()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 
makeBlink()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 
run()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 
cancel()2574         void cancel() {
2575             if (!mCancelled) {
2576                 mTextView.removeCallbacks(this);
2577                 mCancelled = true;
2578             }
2579         }
2580 
uncancel()2581         void uncancel() {
2582             mCancelled = false;
2583         }
2584     }
2585 
getTextThumbnailBuilder(int start, int end)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 
DragLocalState(TextView sourceTextView, int start, int end)2620         public DragLocalState(TextView sourceTextView, int start, int end) {
2621             this.sourceTextView = sourceTextView;
2622             this.start = start;
2623             this.end = end;
2624         }
2625     }
2626 
onDrop(DragEvent event)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 
addSpanWatchers(Spannable text)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 
setContextMenuAnchor(float x, float y)2716     void setContextMenuAnchor(float x, float y) {
2717         mContextMenuAnchorX = x;
2718         mContextMenuAnchorY = y;
2719     }
2720 
onCreateContextMenu(ContextMenu menu)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
findEquivalentSuggestionSpan( @onNull SuggestionSpanInfo suggestionSpanInfo)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 
replaceWithSuggestion(@onNull final SuggestionInfo suggestionInfo)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
isNonIntermediateSelectionSpan(final Spannable text, final Object span)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
onSpanAdded(Spannable text, Object span, int start, int end)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
onSpanRemoved(Spannable text, Object span, int start, int end)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
onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, int newStart, int newEnd)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 
hide()3017         public void hide() {
3018             if (mPopupWindow != null) {
3019                 mPopupWindow.hide();
3020                 mTextView.removeCallbacks(mHidePopup);
3021             }
3022         }
3023 
sendEasySpanNotification(int textChangedType, EasyEditSpan span)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          */
onDeleteClick(EasyEditSpan span)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
createPopupWindow()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
initContentView()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 
setEasyEditSpan(EasyEditSpan easyEditSpan)3091         public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
3092             mEasyEditSpan = easyEditSpan;
3093         }
3094 
setOnDeleteListener(EasyEditDeleteListener listener)3095         private void setOnDeleteListener(EasyEditDeleteListener listener) {
3096             mOnDeleteListener = listener;
3097         }
3098 
3099         @Override
onClick(View view)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
hide()3109         public void hide() {
3110             if (mEasyEditSpan != null) {
3111                 mEasyEditSpan.setDeleteEnabled(false);
3112             }
3113             mOnDeleteListener = null;
3114             super.hide();
3115         }
3116 
3117         @Override
getTextOffset()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
getVerticalLocalPosition(int line)3125         protected int getVerticalLocalPosition(int line) {
3126             final Layout layout = mTextView.getLayout();
3127             return layout.getLineBottomWithoutSpacing(line);
3128         }
3129 
3130         @Override
clipVertically(int positionY)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 
addSubscriber(TextViewPositionListener positionListener, boolean canMove)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 
removeSubscriber(TextViewPositionListener positionListener)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 
getPositionX()3190         public int getPositionX() {
3191             return mPositionX;
3192         }
3193 
getPositionY()3194         public int getPositionY() {
3195             return mPositionY;
3196         }
3197 
getPositionXOnScreen()3198         public int getPositionXOnScreen() {
3199             return mPositionXOnScreen;
3200         }
3201 
getPositionYOnScreen()3202         public int getPositionYOnScreen() {
3203             return mPositionYOnScreen;
3204         }
3205 
3206         @Override
onPreDraw()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 
updatePosition()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 
onScrollChanged()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 
createPopupWindow()3249         protected abstract void createPopupWindow();
initContentView()3250         protected abstract void initContentView();
getTextOffset()3251         protected abstract int getTextOffset();
getVerticalLocalPosition(int line)3252         protected abstract int getVerticalLocalPosition(int line);
clipVertically(int positionY)3253         protected abstract int clipVertically(int positionY);
setUp()3254         protected void setUp() {
3255         }
3256 
PinnedPopupWindow()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 
show()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 
measureContent()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. */
computeLocalPosition()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 
updatePosition(int parentPositionX, int parentPositionY)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 
hide()3335         public void hide() {
3336             if (!isShowing()) {
3337                 return;
3338             }
3339             mPopupWindow.dismiss();
3340             getPositionListener().removeSubscriber(this);
3341         }
3342 
3343         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)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 
isShowing()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 
clear()3372         void clear() {
3373             mSuggestionSpanInfo.clear();
3374             mText.clear();
3375         }
3376 
3377         // Utility method to set attributes about a SuggestionSpan.
setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd)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 
clear()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> {
compare(SuggestionSpan span1, SuggestionSpan span2)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          */
getSortedSuggestionSpans()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          */
getSuggestionInfo(SuggestionInfo[] suggestionInfos, @Nullable SuggestionSpanInfo misspelledSpanInfo)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
dismiss()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 
SuggestionsPopupWindow()3555         public SuggestionsPopupWindow() {
3556             mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3557         }
3558 
3559         @Override
setUp()3560         protected void setUp() {
3561             mContext = applyDefaultTheme(mTextView.getContext());
3562             mHighlightSpan = new TextAppearanceSpan(mContext,
3563                     mTextView.mTextEditSuggestionHighlightStyle);
3564         }
3565 
applyDefaultTheme(Context originalContext)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
createPopupWindow()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
initContentView()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 
isShowingUp()3673         public boolean isShowingUp() {
3674             return mIsShowingUp;
3675         }
3676 
onParentLostFocus()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
getCount()3686             public int getCount() {
3687                 return mNumberOfSuggestions;
3688             }
3689 
3690             @Override
getItem(int position)3691             public Object getItem(int position) {
3692                 return mSuggestionInfos[position];
3693             }
3694 
3695             @Override
getItemId(int position)3696             public long getItemId(int position) {
3697                 return position;
3698             }
3699 
3700             @Override
getView(int position, View convertView, ViewGroup parent)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
getContentViewForTesting()3716         public ViewGroup getContentViewForTesting() {
3717             return mContentView;
3718         }
3719 
3720         @Override
show()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
measureContent()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
getTextOffset()3780         protected int getTextOffset() {
3781             return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
3782         }
3783 
3784         @Override
getVerticalLocalPosition(int line)3785         protected int getVerticalLocalPosition(int line) {
3786             final Layout layout = mTextView.getLayout();
3787             return layout.getLineBottomWithoutSpacing(line) - mContainerMarginTop;
3788         }
3789 
3790         @Override
clipVertically(int positionY)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 
hideWithCleanUp()3797         private void hideWithCleanUp() {
3798             for (final SuggestionInfo info : mSuggestionInfos) {
3799                 info.clear();
3800             }
3801             mMisspelledSpanInfo.clear();
3802             hide();
3803         }
3804 
updateSuggestions()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 
highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, int unionEnd)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
onItemClick(AdapterView<?> parent, View view, int position, long id)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 
TextActionModeCallback(@extActionMode int mode)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
onCreateActionMode(ActionMode mode, Menu menu)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 
getCustomCallback()3961         private Callback getCustomCallback() {
3962             return mHasSelection
3963                     ? mCustomSelectionActionModeCallback
3964                     : mCustomInsertionActionModeCallback;
3965         }
3966 
populateMenuWithItems(Menu menu)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
onPrepareActionMode(ActionMode mode, Menu menu)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 
updateSelectAllItem(Menu menu)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 
updateReplaceItem(Menu menu)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 
updateAssistMenuItems(Menu menu)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 
addAssistMenuItem(Menu menu, RemoteAction action, int intemId, int order, int showAsAction)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 
clearAssistMenuItems(Menu menu)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 
hasLegacyAssistItem(TextClassification classification)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 
onAssistMenuItemClicked(MenuItem assistMenuItem)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 
createAssistMenuItemPendingIntentRequestCode()4153         private int createAssistMenuItemPendingIntentRequestCode() {
4154             return mTextView.hasSelection()
4155                     ? mTextView.getText().subSequence(
4156                             mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4157                             .hashCode()
4158                     : 0;
4159         }
4160 
shouldEnableAssistMenuItems()4161         private boolean shouldEnableAssistMenuItems() {
4162             return mTextView.isDeviceProvisioned()
4163                 && TextClassificationManager.getSettings(mTextView.getContext())
4164                         .isSmartTextShareEnabled();
4165         }
4166 
4167         @Override
onActionItemClicked(ActionMode mode, MenuItem item)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
onDestroyActionMode(ActionMode mode)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
onGetContentRect(ActionMode mode, View view, Rect outRect)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
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)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 
MagnifierMotionAnimator(final Magnifier magnifier)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          */
show(final float x, final float y)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          */
update()4422         private void update() {
4423             mMagnifier.update();
4424         }
4425 
4426         /**
4427          * Dismisses the magnifier, or does nothing if it is already dismissed.
4428          */
dismiss()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 
HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id)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 
getIdealVerticalOffset()4494         public float getIdealVerticalOffset() {
4495             return mIdealVerticalOffset;
4496         }
4497 
updateDrawable()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 
getHotspotX(Drawable drawable, boolean isRtlRun)4524         protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
getHorizontalGravity(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 
startTouchUpFilter(int offset)4536         private void startTouchUpFilter(int offset) {
4537             mNumberPreviousOffsets = 0;
4538             addPositionToTouchUpFilter(offset);
4539         }
4540 
addPositionToTouchUpFilter(int offset)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 
filterOnTouchUp(boolean fromTouchScreen)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 
offsetHasBeenChanged()4564         public boolean offsetHasBeenChanged() {
4565             return mNumberPreviousOffsets > 1;
4566         }
4567 
4568         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)4569         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
4570             setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4571         }
4572 
4573         @Override
invalidate()4574         public void invalidate() {
4575             super.invalidate();
4576             if (isShowing()) {
4577                 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
4578             }
4579         };
4580 
getPreferredWidth()4581         private int getPreferredWidth() {
4582             return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4583         }
4584 
getPreferredHeight()4585         private int getPreferredHeight() {
4586             return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
4587         }
4588 
show()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 
dismiss()4599         protected void dismiss() {
4600             mIsDragging = false;
4601             mContainer.dismiss();
4602             onDetached();
4603         }
4604 
hide()4605         public void hide() {
4606             dismiss();
4607 
4608             getPositionListener().removeSubscriber(this);
4609         }
4610 
isShowing()4611         public boolean isShowing() {
4612             return mContainer.isShowing();
4613         }
4614 
shouldShow()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 
setVisible(final boolean visible)4629         private void setVisible(final boolean visible) {
4630             mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
4631         }
4632 
getCurrentCursorOffset()4633         public abstract int getCurrentCursorOffset();
4634 
updateSelection(int offset)4635         protected abstract void updateSelection(int offset);
4636 
updatePosition(float x, float y, boolean fromTouchScreen)4637         protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
4638 
4639         @MagnifierHandleTrigger
getMagnifierHandleTrigger()4640         protected abstract int getMagnifierHandleTrigger();
4641 
isAtRtlRun(@onNull Layout layout, int offset)4642         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4643             return layout.isRtlCharAt(offset);
4644         }
4645 
4646         @VisibleForTesting
getHorizontal(@onNull Layout layout, int offset)4647         public float getHorizontal(@NonNull Layout layout, int offset) {
4648             return layout.getPrimaryHorizontal(offset);
4649         }
4650 
getOffsetAtCoordinate(@onNull Layout layout, int line, float x)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          */
positionAtCursorOffset(int offset, boolean forceUpdatePosition, boolean fromTouchScreen)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          */
getCursorHorizontalPosition(Layout layout, int offset)4705         int getCursorHorizontalPosition(Layout layout, int offset) {
4706             return (int) (getHorizontal(layout, offset) - 0.5f);
4707         }
4708 
4709         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)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
onDraw(Canvas c)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 
getHorizontalOffset()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 
getCursorOffset()4775         protected int getCursorOffset() {
4776             return 0;
4777         }
4778 
tooLargeTextForMagnifier()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          */
obtainMagnifierShowCoordinates(@onNull final MotionEvent event, final PointF showPosInView)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 
handleOverlapsMagnifier(@onNull final HandleView handle, @NonNull final Rect magnifierRect)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 
getOtherSelectionHandle()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 
updateMagnifier(@onNull final MotionEvent event)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 
dismissMagnifier()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
onTouchEvent(MotionEvent ev)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 
isDragging()5005         public boolean isDragging() {
5006             return mIsDragging;
5007         }
5008 
onHandleMoved()5009         void onHandleMoved() {}
5010 
onDetached()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 
InsertionHandleView(Drawable drawable)5022         public InsertionHandleView(Drawable drawable) {
5023             super(drawable, drawable, com.android.internal.R.id.insertion_handle);
5024         }
5025 
5026         @Override
show()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 
hideAfterDelay()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 
removeHiderCallback()5078         private void removeHiderCallback() {
5079             if (mHider != null) {
5080                 mTextView.removeCallbacks(mHider);
5081             }
5082         }
5083 
5084         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)5085         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5086             return drawable.getIntrinsicWidth() / 2;
5087         }
5088 
5089         @Override
getHorizontalGravity(boolean isRtlRun)5090         protected int getHorizontalGravity(boolean isRtlRun) {
5091             return Gravity.CENTER_HORIZONTAL;
5092         }
5093 
5094         @Override
getCursorOffset()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
getCursorHorizontalPosition(Layout layout, int offset)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
onTouchEvent(MotionEvent ev)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
getCurrentCursorOffset()5166         public int getCurrentCursorOffset() {
5167             return mTextView.getSelectionStart();
5168         }
5169 
5170         @Override
updateSelection(int offset)5171         public void updateSelection(int offset) {
5172             Selection.setSelection((Spannable) mTextView.getText(), offset);
5173         }
5174 
5175         @Override
updatePosition(float x, float y, boolean fromTouchScreen)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
onHandleMoved()5196         void onHandleMoved() {
5197             super.onHandleMoved();
5198             removeHiderCallback();
5199         }
5200 
5201         @Override
onDetached()5202         public void onDetached() {
5203             super.onDetached();
5204             removeHiderCallback();
5205         }
5206 
5207         @Override
5208         @MagnifierHandleTrigger
getMagnifierHandleTrigger()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 
SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id, @HandleType int handleType)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 
isStartHandle()5252         private boolean isStartHandle() {
5253             return mHandleType == HANDLE_TYPE_SELECTION_START;
5254         }
5255 
5256         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)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
getHorizontalGravity(boolean isRtlRun)5266         protected int getHorizontalGravity(boolean isRtlRun) {
5267             return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
5268         }
5269 
5270         @Override
getCurrentCursorOffset()5271         public int getCurrentCursorOffset() {
5272             return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
5273         }
5274 
5275         @Override
updateSelection(int offset)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
updatePosition(float x, float y, boolean fromTouchScreen)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