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