• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 import static com.android.text.flags.Flags.contextMenuHideUnavailableItems;
24 
25 import android.R;
26 import android.animation.ValueAnimator;
27 import android.annotation.IntDef;
28 import android.annotation.NonNull;
29 import android.annotation.Nullable;
30 import android.app.AppGlobals;
31 import android.app.PendingIntent;
32 import android.app.PendingIntent.CanceledException;
33 import android.app.RemoteAction;
34 import android.compat.annotation.UnsupportedAppUsage;
35 import android.content.ClipData;
36 import android.content.Context;
37 import android.content.Intent;
38 import android.content.UndoManager;
39 import android.content.UndoOperation;
40 import android.content.UndoOwner;
41 import android.content.pm.PackageManager;
42 import android.content.pm.ResolveInfo;
43 import android.content.res.Configuration;
44 import android.content.res.TypedArray;
45 import android.graphics.Canvas;
46 import android.graphics.Color;
47 import android.graphics.Matrix;
48 import android.graphics.Paint;
49 import android.graphics.Path;
50 import android.graphics.Point;
51 import android.graphics.PointF;
52 import android.graphics.RecordingCanvas;
53 import android.graphics.Rect;
54 import android.graphics.RectF;
55 import android.graphics.RenderNode;
56 import android.graphics.drawable.ColorDrawable;
57 import android.graphics.drawable.Drawable;
58 import android.graphics.drawable.GradientDrawable;
59 import android.os.Build;
60 import android.os.Bundle;
61 import android.os.LocaleList;
62 import android.os.Parcel;
63 import android.os.Parcelable;
64 import android.os.ParcelableParcel;
65 import android.os.SystemClock;
66 import android.provider.Settings;
67 import android.text.DynamicLayout;
68 import android.text.Editable;
69 import android.text.InputFilter;
70 import android.text.InputType;
71 import android.text.Layout;
72 import android.text.ParcelableSpan;
73 import android.text.Selection;
74 import android.text.SpanWatcher;
75 import android.text.Spannable;
76 import android.text.SpannableStringBuilder;
77 import android.text.Spanned;
78 import android.text.SpannedString;
79 import android.text.StaticLayout;
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 
475     private final AccessibilitySmartActions mA11ySmartActions;
476     private InsertModeController mInsertModeController;
477 
478     /**
479      * @hide
480      */
481     @VisibleForTesting
Editor(TextView textView)482     public Editor(TextView textView) {
483         mTextView = textView;
484         // Synchronize the filter list, which places the undo input filter at the end.
485         mTextView.setFilters(mTextView.getFilters());
486         mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this);
487         mA11ySmartActions = new AccessibilitySmartActions(mTextView);
488         mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean(
489                 com.android.internal.R.bool.config_enableHapticTextHandle);
490 
491         mFlagCursorDragFromAnywhereEnabled = AppGlobals.getIntCoreSetting(
492                 WidgetFlags.KEY_ENABLE_CURSOR_DRAG_FROM_ANYWHERE,
493                 WidgetFlags.ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT ? 1 : 0) != 0;
494         final int cursorDragMinAngleFromVertical = AppGlobals.getIntCoreSetting(
495                 WidgetFlags.KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL,
496                 WidgetFlags.CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT);
497         mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio(
498                 cursorDragMinAngleFromVertical);
499         mFlagInsertionHandleGesturesEnabled = AppGlobals.getIntCoreSetting(
500                 WidgetFlags.KEY_ENABLE_INSERTION_HANDLE_GESTURES,
501                 WidgetFlags.ENABLE_INSERTION_HANDLE_GESTURES_DEFAULT ? 1 : 0) != 0;
502         mNewMagnifierEnabled = AppGlobals.getIntCoreSetting(
503                 WidgetFlags.KEY_ENABLE_NEW_MAGNIFIER,
504                 WidgetFlags.ENABLE_NEW_MAGNIFIER_DEFAULT ? 1 : 0) != 0;
505         mLineSlopRatio = AppGlobals.getFloatCoreSetting(
506                 WidgetFlags.KEY_LINE_SLOP_RATIO,
507                 WidgetFlags.LINE_SLOP_RATIO_DEFAULT);
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         menu.setOptionalIconsVisible(true);
3215         menu.setGroupDividerEnabled(true);
3216 
3217         setAssistContextMenuItems(menu);
3218 
3219         final int keyboard = mTextView.getResources().getConfiguration().keyboard;
3220         menu.setQwertyMode(keyboard == Configuration.KEYBOARD_QWERTY);
3221 
3222         setTextContextMenuItems(menu);
3223 
3224         mPreserveSelection = true;
3225 
3226         // No-op for the old context menu because it doesn't have icons.
3227         adjustIconSpacing(menu);
3228     }
3229 
3230     /** @hide */
3231     @VisibleForTesting
setTextContextMenuItems(ContextMenu menu)3232     public void setTextContextMenuItems(ContextMenu menu) {
3233         final TypedArray a = mTextView.getContext().obtainStyledAttributes(new int[]{
3234                 // TODO: Make Undo/Redo be public attribute.
3235                 com.android.internal.R.attr.actionModeUndoDrawable,
3236                 com.android.internal.R.attr.actionModeRedoDrawable,
3237                 android.R.attr.actionModeCutDrawable,
3238                 android.R.attr.actionModeCopyDrawable,
3239                 android.R.attr.actionModePasteDrawable,
3240                 android.R.attr.actionModeSelectAllDrawable,
3241                 android.R.attr.actionModeShareDrawable,
3242         });
3243 
3244         final int menuItemOrderUndo = 2;
3245         final int menuItemOrderRedo = 3;
3246         final int menuItemOrderCut = 4;
3247         final int menuItemOrderCopy = 5;
3248         final int menuItemOrderPaste = 6;
3249         final int menuItemOrderPasteAsPlainText = 7;
3250         final int menuItemOrderSelectAll = 8;
3251         final int menuItemOrderShare = 9;
3252         final int menuItemOrderAutofill = 10;
3253 
3254         if (contextMenuHideUnavailableItems()) {
3255             if (mTextView.canUndo()) {
3256                 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo,
3257                                 com.android.internal.R.string.undo)
3258                         .setAlphabeticShortcut('z')
3259                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3260                         .setIcon(a.getDrawable(0));
3261             }
3262 
3263             if (mTextView.canRedo()) {
3264                 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, menuItemOrderRedo,
3265                                 com.android.internal.R.string.redo)
3266                         .setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
3267                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3268                         .setIcon(a.getDrawable(1));
3269             }
3270 
3271             if (mTextView.canCut()) {
3272                 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut,
3273                                 com.android.internal.R.string.cut)
3274                         .setAlphabeticShortcut('x')
3275                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3276                         .setIcon(a.getDrawable(2));
3277             }
3278 
3279             if (mTextView.canCopy()) {
3280                 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy,
3281                                 com.android.internal.R.string.copy)
3282                         .setAlphabeticShortcut('c')
3283                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3284                         .setIcon(a.getDrawable(3));
3285             }
3286 
3287             if (mTextView.canPaste()) {
3288                 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste,
3289                                 com.android.internal.R.string.paste)
3290                         .setAlphabeticShortcut('v')
3291                         .setIcon(a.getDrawable(4))
3292                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3293             }
3294 
3295             if (mTextView.canPasteAsPlainText()) {
3296                 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT,
3297                                 menuItemOrderPasteAsPlainText,
3298                                 com.android.internal.R.string.paste_as_plain_text)
3299                         .setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
3300                         .setIcon(a.getDrawable(4))
3301                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3302             }
3303 
3304             if (mTextView.canSelectAllText()) {
3305                 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL,
3306                                 menuItemOrderSelectAll, com.android.internal.R.string.selectAll)
3307                         .setAlphabeticShortcut('a')
3308                         .setIcon(a.getDrawable(5))
3309                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3310             }
3311 
3312             if (mTextView.canShare()) {
3313                 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare,
3314                                 com.android.internal.R.string.share)
3315                         .setIcon(a.getDrawable(6))
3316                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3317             }
3318 
3319             final String selected = mTextView.getSelectedText();
3320             if (mTextView.canRequestAutofill() && (selected == null || selected.isEmpty())) {
3321                 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill,
3322                                 android.R.string.autofill)
3323                         .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3324             }
3325         } else {
3326             menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo,
3327                             com.android.internal.R.string.undo)
3328                     .setAlphabeticShortcut('z')
3329                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3330                     .setIcon(a.getDrawable(0))
3331                     .setEnabled(mTextView.canUndo());
3332             menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, menuItemOrderRedo,
3333                             com.android.internal.R.string.redo)
3334                     .setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
3335                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3336                     .setIcon(a.getDrawable(1))
3337                     .setEnabled(mTextView.canRedo());
3338 
3339             menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut,
3340                             com.android.internal.R.string.cut)
3341                     .setAlphabeticShortcut('x')
3342                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3343                     .setIcon(a.getDrawable(2))
3344                     .setEnabled(mTextView.canCut());
3345             menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy,
3346                             com.android.internal.R.string.copy)
3347                     .setAlphabeticShortcut('c')
3348                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
3349                     .setIcon(a.getDrawable(3))
3350                     .setEnabled(mTextView.canCopy());
3351             menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste,
3352                             com.android.internal.R.string.paste)
3353                     .setAlphabeticShortcut('v')
3354                     .setEnabled(mTextView.canPaste())
3355                     .setIcon(a.getDrawable(4))
3356                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3357             menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT,
3358                             menuItemOrderPasteAsPlainText,
3359                             com.android.internal.R.string.paste_as_plain_text)
3360                     .setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON)
3361                     .setEnabled(mTextView.canPasteAsPlainText())
3362                     .setIcon(a.getDrawable(4))
3363                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3364             menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL,
3365                             menuItemOrderSelectAll, com.android.internal.R.string.selectAll)
3366                     .setAlphabeticShortcut('a')
3367                     .setEnabled(mTextView.canSelectAllText())
3368                     .setIcon(a.getDrawable(5))
3369                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3370 
3371             menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare,
3372                             com.android.internal.R.string.share)
3373                     .setEnabled(mTextView.canShare())
3374                     .setIcon(a.getDrawable(6))
3375                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3376             final String selected = mTextView.getSelectedText();
3377             menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill,
3378                             android.R.string.autofill)
3379                     .setEnabled(mTextView.canRequestAutofill()
3380                             && (selected == null || selected.isEmpty()))
3381                     .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
3382         }
3383         a.recycle();
3384     }
3385 
3386     /**
3387      * Adjust icon spacing to align the texts.
3388      * @hide
3389      */
3390     @VisibleForTesting
adjustIconSpacing(ContextMenu menu)3391     public void adjustIconSpacing(ContextMenu menu) {
3392         int width = -1;
3393         int height = -1;
3394         for (int i = 0; i < menu.size(); ++i) {
3395             final MenuItem item = menu.getItem(i);
3396             final Drawable d = item.getIcon();
3397             if (d == null) {
3398                 continue;
3399             }
3400 
3401             width = Math.max(width, d.getIntrinsicWidth());
3402             height = Math.max(height, d.getIntrinsicHeight());
3403         }
3404 
3405         if (width < 0 || height < 0) {
3406             return;  // No menu has icon drawable.
3407         }
3408 
3409         GradientDrawable paddingDrawable = new GradientDrawable();
3410         paddingDrawable.setSize(width, height);
3411 
3412         for (int i = 0; i < menu.size(); ++i) {
3413             final MenuItem item = menu.getItem(i);
3414             final Drawable d = item.getIcon();
3415             if (d == null) {
3416                 item.setIcon(paddingDrawable);
3417             }
3418         }
3419     }
3420 
3421     @Nullable
findEquivalentSuggestionSpan( @onNull SuggestionSpanInfo suggestionSpanInfo)3422     private SuggestionSpan findEquivalentSuggestionSpan(
3423             @NonNull SuggestionSpanInfo suggestionSpanInfo) {
3424         final Editable editable = (Editable) mTextView.getText();
3425         if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
3426             // Exactly same span is found.
3427             return suggestionSpanInfo.mSuggestionSpan;
3428         }
3429         // Suggestion span couldn't be found. Try to find a suggestion span that has the same
3430         // contents.
3431         final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
3432                 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
3433         for (final SuggestionSpan suggestionSpan : suggestionSpans) {
3434             final int start = editable.getSpanStart(suggestionSpan);
3435             if (start != suggestionSpanInfo.mSpanStart) {
3436                 continue;
3437             }
3438             final int end = editable.getSpanEnd(suggestionSpan);
3439             if (end != suggestionSpanInfo.mSpanEnd) {
3440                 continue;
3441             }
3442             if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
3443                 return suggestionSpan;
3444             }
3445         }
3446         return null;
3447     }
3448 
replaceWithSuggestion(@onNull final SuggestionInfo suggestionInfo)3449     private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
3450         final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
3451                 suggestionInfo.mSuggestionSpanInfo);
3452         if (targetSuggestionSpan == null) {
3453             // Span has been removed
3454             return;
3455         }
3456         final Editable editable = (Editable) mTextView.getText();
3457         final int spanStart = editable.getSpanStart(targetSuggestionSpan);
3458         final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
3459         if (spanStart < 0 || spanEnd <= spanStart) {
3460             // Span has been removed
3461             return;
3462         }
3463 
3464         final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3465         // SuggestionSpans are removed by replace: save them before
3466         SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
3467                 SuggestionSpan.class);
3468         final int length = suggestionSpans.length;
3469         int[] suggestionSpansStarts = new int[length];
3470         int[] suggestionSpansEnds = new int[length];
3471         int[] suggestionSpansFlags = new int[length];
3472         for (int i = 0; i < length; i++) {
3473             final SuggestionSpan suggestionSpan = suggestionSpans[i];
3474             suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
3475             suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
3476             suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
3477 
3478             // Remove potential misspelled flags
3479             int suggestionSpanFlags = suggestionSpan.getFlags();
3480             if ((suggestionSpanFlags & FLAG_MISSPELLED_OR_GRAMMAR_ERROR) != 0) {
3481                 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
3482                 suggestionSpanFlags &= ~SuggestionSpan.FLAG_GRAMMAR_ERROR;
3483                 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
3484                 suggestionSpan.setFlags(suggestionSpanFlags);
3485             }
3486         }
3487 
3488         // Swap text content between actual text and Suggestion span
3489         final int suggestionStart = suggestionInfo.mSuggestionStart;
3490         final int suggestionEnd = suggestionInfo.mSuggestionEnd;
3491         final String suggestion = suggestionInfo.mText.subSequence(
3492                 suggestionStart, suggestionEnd).toString();
3493         mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
3494 
3495         String[] suggestions = targetSuggestionSpan.getSuggestions();
3496         suggestions[suggestionInfo.mSuggestionIndex] = originalText;
3497 
3498         // Restore previous SuggestionSpans
3499         final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
3500         for (int i = 0; i < length; i++) {
3501             // Only spans that include the modified region make sense after replacement
3502             // Spans partially included in the replaced region are removed, there is no
3503             // way to assign them a valid range after replacement
3504             if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
3505                 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
3506                         suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
3507             }
3508         }
3509         // Move cursor at the end of the replaced word
3510         final int newCursorPosition = spanEnd + lengthDelta;
3511         mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
3512     }
3513 
3514     private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
3515             new MenuItem.OnMenuItemClickListener() {
3516         @Override
3517         public boolean onMenuItemClick(MenuItem item) {
3518             if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
3519                 return true;
3520             }
3521             return mTextView.onTextContextMenuItem(item.getItemId());
3522         }
3523     };
3524 
3525     /**
3526      * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
3527      * pop-up should be displayed.
3528      * Also monitors {@link Selection} to call back to the attached input method.
3529      */
3530     private class SpanController implements SpanWatcher {
3531 
3532         private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
3533 
3534         private EasyEditPopupWindow mPopupWindow;
3535 
3536         private Runnable mHidePopup;
3537 
3538         // This function is pure but inner classes can't have static functions
isNonIntermediateSelectionSpan(final Spannable text, final Object span)3539         private boolean isNonIntermediateSelectionSpan(final Spannable text,
3540                 final Object span) {
3541             return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
3542                     && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
3543         }
3544 
3545         @Override
onSpanAdded(Spannable text, Object span, int start, int end)3546         public void onSpanAdded(Spannable text, Object span, int start, int end) {
3547             if (isNonIntermediateSelectionSpan(text, span)) {
3548                 sendUpdateSelection();
3549             } else if (span instanceof EasyEditSpan) {
3550                 if (mPopupWindow == null) {
3551                     mPopupWindow = new EasyEditPopupWindow();
3552                     mHidePopup = new Runnable() {
3553                         @Override
3554                         public void run() {
3555                             hide();
3556                         }
3557                     };
3558                 }
3559 
3560                 // Make sure there is only at most one EasyEditSpan in the text
3561                 if (mPopupWindow.mEasyEditSpan != null) {
3562                     mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
3563                 }
3564 
3565                 mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
3566                 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
3567                     @Override
3568                     public void onDeleteClick(EasyEditSpan span) {
3569                         Editable editable = (Editable) mTextView.getText();
3570                         int start = editable.getSpanStart(span);
3571                         int end = editable.getSpanEnd(span);
3572                         if (start >= 0 && end >= 0) {
3573                             sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
3574                             mTextView.deleteText_internal(start, end);
3575                         }
3576                         editable.removeSpan(span);
3577                     }
3578                 });
3579 
3580                 if (mTextView.getWindowVisibility() != View.VISIBLE) {
3581                     // The window is not visible yet, ignore the text change.
3582                     return;
3583                 }
3584 
3585                 if (mTextView.getLayout() == null) {
3586                     // The view has not been laid out yet, ignore the text change
3587                     return;
3588                 }
3589 
3590                 if (extractedTextModeWillBeStarted()) {
3591                     // The input is in extract mode. Do not handle the easy edit in
3592                     // the original TextView, as the ExtractEditText will do
3593                     return;
3594                 }
3595 
3596                 mPopupWindow.show();
3597                 mTextView.removeCallbacks(mHidePopup);
3598                 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
3599             }
3600         }
3601 
3602         @Override
onSpanRemoved(Spannable text, Object span, int start, int end)3603         public void onSpanRemoved(Spannable text, Object span, int start, int end) {
3604             if (isNonIntermediateSelectionSpan(text, span)) {
3605                 sendUpdateSelection();
3606             } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
3607                 hide();
3608             }
3609         }
3610 
3611         @Override
onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, int newStart, int newEnd)3612         public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
3613                 int newStart, int newEnd) {
3614             if (isNonIntermediateSelectionSpan(text, span)) {
3615                 sendUpdateSelection();
3616             } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
3617                 EasyEditSpan easyEditSpan = (EasyEditSpan) span;
3618                 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
3619                 text.removeSpan(easyEditSpan);
3620             }
3621         }
3622 
hide()3623         public void hide() {
3624             if (mPopupWindow != null) {
3625                 mPopupWindow.hide();
3626                 mTextView.removeCallbacks(mHidePopup);
3627             }
3628         }
3629 
sendEasySpanNotification(int textChangedType, EasyEditSpan span)3630         private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
3631             try {
3632                 PendingIntent pendingIntent = span.getPendingIntent();
3633                 if (pendingIntent != null) {
3634                     Intent intent = new Intent();
3635                     intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
3636                     pendingIntent.send(mTextView.getContext(), 0, intent);
3637                 }
3638             } catch (CanceledException e) {
3639                 // This should not happen, as we should try to send the intent only once.
3640                 Log.w(TAG, "PendingIntent for notification cannot be sent", e);
3641             }
3642         }
3643     }
3644 
3645     /**
3646      * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
3647      */
3648     private interface EasyEditDeleteListener {
3649 
3650         /**
3651          * Clicks the delete pop-up.
3652          */
onDeleteClick(EasyEditSpan span)3653         void onDeleteClick(EasyEditSpan span);
3654     }
3655 
3656     /**
3657      * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
3658      * by {@link SpanController}.
3659      */
3660     private class EasyEditPopupWindow extends PinnedPopupWindow
3661             implements OnClickListener {
3662         private static final int POPUP_TEXT_LAYOUT =
3663                 com.android.internal.R.layout.text_edit_action_popup_text;
3664         private TextView mDeleteTextView;
3665         private EasyEditSpan mEasyEditSpan;
3666         private EasyEditDeleteListener mOnDeleteListener;
3667 
3668         @Override
createPopupWindow()3669         protected void createPopupWindow() {
3670             mPopupWindow = new PopupWindow(mTextView.getContext(), null,
3671                     com.android.internal.R.attr.textSelectHandleWindowStyle);
3672             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3673             mPopupWindow.setClippingEnabled(true);
3674         }
3675 
3676         @Override
initContentView()3677         protected void initContentView() {
3678             LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
3679             linearLayout.setOrientation(LinearLayout.HORIZONTAL);
3680             mContentView = linearLayout;
3681             mContentView.setBackgroundResource(
3682                     com.android.internal.R.drawable.text_edit_side_paste_window);
3683 
3684             LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
3685                     .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
3686 
3687             LayoutParams wrapContent = new LayoutParams(
3688                     ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
3689 
3690             mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
3691             mDeleteTextView.setLayoutParams(wrapContent);
3692             mDeleteTextView.setText(com.android.internal.R.string.delete);
3693             mDeleteTextView.setOnClickListener(this);
3694             mContentView.addView(mDeleteTextView);
3695         }
3696 
setEasyEditSpan(EasyEditSpan easyEditSpan)3697         public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
3698             mEasyEditSpan = easyEditSpan;
3699         }
3700 
setOnDeleteListener(EasyEditDeleteListener listener)3701         private void setOnDeleteListener(EasyEditDeleteListener listener) {
3702             mOnDeleteListener = listener;
3703         }
3704 
3705         @Override
onClick(View view)3706         public void onClick(View view) {
3707             if (view == mDeleteTextView
3708                     && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
3709                     && mOnDeleteListener != null) {
3710                 mOnDeleteListener.onDeleteClick(mEasyEditSpan);
3711             }
3712         }
3713 
3714         @Override
hide()3715         public void hide() {
3716             if (mEasyEditSpan != null) {
3717                 mEasyEditSpan.setDeleteEnabled(false);
3718             }
3719             mOnDeleteListener = null;
3720             super.hide();
3721         }
3722 
3723         @Override
getTextOffset()3724         protected int getTextOffset() {
3725             // Place the pop-up at the end of the span
3726             Editable editable = (Editable) mTextView.getText();
3727             return editable.getSpanEnd(mEasyEditSpan);
3728         }
3729 
3730         @Override
getVerticalLocalPosition(int line)3731         protected int getVerticalLocalPosition(int line) {
3732             final Layout layout = mTextView.getLayout();
3733             return layout.getLineBottom(line, /* includeLineSpacing= */ false);
3734         }
3735 
3736         @Override
clipVertically(int positionY)3737         protected int clipVertically(int positionY) {
3738             // As we display the pop-up below the span, no vertical clipping is required.
3739             return positionY;
3740         }
3741     }
3742 
3743     private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
3744         // 3 handles
3745         // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
3746         // 1 CursorAnchorInfoNotifier
3747         private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
3748         private TextViewPositionListener[] mPositionListeners =
3749                 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
3750         private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
3751         private boolean mPositionHasChanged = true;
3752         // Absolute position of the TextView with respect to its parent window
3753         private int mPositionX, mPositionY;
3754         private int mPositionXOnScreen, mPositionYOnScreen;
3755         private int mNumberOfListeners;
3756         private boolean mScrollHasChanged;
3757         final int[] mTempCoords = new int[2];
3758 
addSubscriber(TextViewPositionListener positionListener, boolean canMove)3759         public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3760             if (mNumberOfListeners == 0) {
3761                 updatePosition();
3762                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3763                 vto.addOnPreDrawListener(this);
3764             }
3765 
3766             int emptySlotIndex = -1;
3767             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3768                 TextViewPositionListener listener = mPositionListeners[i];
3769                 if (listener == positionListener) {
3770                     return;
3771                 } else if (emptySlotIndex < 0 && listener == null) {
3772                     emptySlotIndex = i;
3773                 }
3774             }
3775 
3776             mPositionListeners[emptySlotIndex] = positionListener;
3777             mCanMove[emptySlotIndex] = canMove;
3778             mNumberOfListeners++;
3779         }
3780 
removeSubscriber(TextViewPositionListener positionListener)3781         public void removeSubscriber(TextViewPositionListener positionListener) {
3782             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3783                 if (mPositionListeners[i] == positionListener) {
3784                     mPositionListeners[i] = null;
3785                     mNumberOfListeners--;
3786                     break;
3787                 }
3788             }
3789 
3790             if (mNumberOfListeners == 0) {
3791                 ViewTreeObserver vto = mTextView.getViewTreeObserver();
3792                 vto.removeOnPreDrawListener(this);
3793             }
3794         }
3795 
getPositionX()3796         public int getPositionX() {
3797             return mPositionX;
3798         }
3799 
getPositionY()3800         public int getPositionY() {
3801             return mPositionY;
3802         }
3803 
getPositionXOnScreen()3804         public int getPositionXOnScreen() {
3805             return mPositionXOnScreen;
3806         }
3807 
getPositionYOnScreen()3808         public int getPositionYOnScreen() {
3809             return mPositionYOnScreen;
3810         }
3811 
3812         @Override
onPreDraw()3813         public boolean onPreDraw() {
3814             updatePosition();
3815 
3816             for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3817                 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3818                     TextViewPositionListener positionListener = mPositionListeners[i];
3819                     if (positionListener != null) {
3820                         positionListener.updatePosition(mPositionX, mPositionY,
3821                                 mPositionHasChanged, mScrollHasChanged);
3822                     }
3823                 }
3824             }
3825 
3826             mScrollHasChanged = false;
3827             return true;
3828         }
3829 
updatePosition()3830         private void updatePosition() {
3831             mTextView.getLocationInWindow(mTempCoords);
3832 
3833             mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3834 
3835             mPositionX = mTempCoords[0];
3836             mPositionY = mTempCoords[1];
3837 
3838             mTextView.getLocationOnScreen(mTempCoords);
3839 
3840             mPositionXOnScreen = mTempCoords[0];
3841             mPositionYOnScreen = mTempCoords[1];
3842         }
3843 
onScrollChanged()3844         public void onScrollChanged() {
3845             mScrollHasChanged = true;
3846         }
3847     }
3848 
3849     private abstract class PinnedPopupWindow implements TextViewPositionListener {
3850         protected PopupWindow mPopupWindow;
3851         protected ViewGroup mContentView;
3852         int mPositionX, mPositionY;
3853         int mClippingLimitLeft, mClippingLimitRight;
3854 
createPopupWindow()3855         protected abstract void createPopupWindow();
initContentView()3856         protected abstract void initContentView();
getTextOffset()3857         protected abstract int getTextOffset();
getVerticalLocalPosition(int line)3858         protected abstract int getVerticalLocalPosition(int line);
clipVertically(int positionY)3859         protected abstract int clipVertically(int positionY);
setUp()3860         protected void setUp() {
3861         }
3862 
PinnedPopupWindow()3863         public PinnedPopupWindow() {
3864             // Due to calling subclass methods in base constructor, subclass constructor is not
3865             // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3866             // a chance to initialize subclasses, call setUp() method here.
3867             // TODO: It is good to extract non trivial initialization code from constructor.
3868             setUp();
3869 
3870             createPopupWindow();
3871 
3872             mPopupWindow.setWindowLayoutType(
3873                     WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
3874             mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3875             mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3876 
3877             initContentView();
3878 
3879             LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3880                     ViewGroup.LayoutParams.WRAP_CONTENT);
3881             mContentView.setLayoutParams(wrapContent);
3882 
3883             mPopupWindow.setContentView(mContentView);
3884         }
3885 
show()3886         public void show() {
3887             getPositionListener().addSubscriber(this, false /* offset is fixed */);
3888 
3889             computeLocalPosition();
3890 
3891             final PositionListener positionListener = getPositionListener();
3892             updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3893         }
3894 
measureContent()3895         protected void measureContent() {
3896             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3897             mContentView.measure(
3898                     View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3899                             View.MeasureSpec.AT_MOST),
3900                     View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3901                             View.MeasureSpec.AT_MOST));
3902         }
3903 
3904         /* The popup window will be horizontally centered on the getTextOffset() and vertically
3905          * positioned according to viewportToContentHorizontalOffset.
3906          *
3907          * This method assumes that mContentView has properly been measured from its content. */
computeLocalPosition()3908         private void computeLocalPosition() {
3909             measureContent();
3910             final int width = mContentView.getMeasuredWidth();
3911             final int offset = getTextOffset();
3912             final int transformedOffset = mTextView.originalToTransformed(offset,
3913                     OffsetMapping.MAP_STRATEGY_CURSOR);
3914             final Layout layout = mTextView.getLayout();
3915 
3916             mPositionX = (int) (layout.getPrimaryHorizontal(transformedOffset) - width / 2.0f);
3917             mPositionX += mTextView.viewportToContentHorizontalOffset();
3918 
3919             final int line = layout.getLineForOffset(transformedOffset);
3920             mPositionY = getVerticalLocalPosition(line);
3921             mPositionY += mTextView.viewportToContentVerticalOffset();
3922         }
3923 
updatePosition(int parentPositionX, int parentPositionY)3924         private void updatePosition(int parentPositionX, int parentPositionY) {
3925             int positionX = parentPositionX + mPositionX;
3926             int positionY = parentPositionY + mPositionY;
3927 
3928             positionY = clipVertically(positionY);
3929 
3930             // Horizontal clipping
3931             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3932             final int width = mContentView.getMeasuredWidth();
3933             positionX = Math.min(
3934                     displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3935             positionX = Math.max(-mClippingLimitLeft, positionX);
3936 
3937             if (isShowing()) {
3938                 mPopupWindow.update(positionX, positionY, -1, -1);
3939             } else {
3940                 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3941                         positionX, positionY);
3942             }
3943         }
3944 
hide()3945         public void hide() {
3946             if (!isShowing()) {
3947                 return;
3948             }
3949             mPopupWindow.dismiss();
3950             getPositionListener().removeSubscriber(this);
3951         }
3952 
3953         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)3954         public void updatePosition(int parentPositionX, int parentPositionY,
3955                 boolean parentPositionChanged, boolean parentScrolled) {
3956             // Either parentPositionChanged or parentScrolled is true, check if still visible
3957             if (isShowing() && isOffsetVisible(getTextOffset())) {
3958                 if (parentScrolled) computeLocalPosition();
3959                 updatePosition(parentPositionX, parentPositionY);
3960             } else {
3961                 hide();
3962             }
3963         }
3964 
isShowing()3965         public boolean isShowing() {
3966             return mPopupWindow.isShowing();
3967         }
3968     }
3969 
3970     private static final class SuggestionInfo {
3971         // Range of actual suggestion within mText
3972         int mSuggestionStart, mSuggestionEnd;
3973 
3974         // The SuggestionSpan that this TextView represents
3975         final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
3976 
3977         // The index of this suggestion inside suggestionSpan
3978         int mSuggestionIndex;
3979 
3980         final SpannableStringBuilder mText = new SpannableStringBuilder();
3981 
clear()3982         void clear() {
3983             mSuggestionSpanInfo.clear();
3984             mText.clear();
3985         }
3986 
3987         // Utility method to set attributes about a SuggestionSpan.
setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd)3988         void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3989             mSuggestionSpanInfo.mSuggestionSpan = span;
3990             mSuggestionSpanInfo.mSpanStart = spanStart;
3991             mSuggestionSpanInfo.mSpanEnd = spanEnd;
3992         }
3993     }
3994 
3995     private static final class SuggestionSpanInfo {
3996         // The SuggestionSpan;
3997         @Nullable
3998         SuggestionSpan mSuggestionSpan;
3999 
4000         // The SuggestionSpan start position
4001         int mSpanStart;
4002 
4003         // The SuggestionSpan end position
4004         int mSpanEnd;
4005 
clear()4006         void clear() {
4007             mSuggestionSpan = null;
4008         }
4009     }
4010 
4011     private class SuggestionHelper {
4012         private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
4013                 new SuggestionSpanComparator();
4014         private final HashMap<SuggestionSpan, Integer> mSpansLengths =
4015                 new HashMap<SuggestionSpan, Integer>();
4016 
4017         private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
compare(SuggestionSpan span1, SuggestionSpan span2)4018             public int compare(SuggestionSpan span1, SuggestionSpan span2) {
4019                 final int flag1 = span1.getFlags();
4020                 final int flag2 = span2.getFlags();
4021                 if (flag1 != flag2) {
4022                     // Compare so that the order will be: easy -> misspelled -> grammarError
4023                     int easy = compareFlag(SuggestionSpan.FLAG_EASY_CORRECT, flag1, flag2);
4024                     if (easy != 0) return easy;
4025                     int misspelled = compareFlag(SuggestionSpan.FLAG_MISSPELLED, flag1, flag2);
4026                     if (misspelled != 0) return misspelled;
4027                     int grammarError = compareFlag(SuggestionSpan.FLAG_GRAMMAR_ERROR, flag1, flag2);
4028                     if (grammarError != 0) return grammarError;
4029                 }
4030 
4031                 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
4032             }
4033 
4034             /*
4035              * Returns -1 if flags1 has flagToCompare but flags2 does not.
4036              * Returns 1 if flags2 has flagToCompare but flags1 does not.
4037              * Otherwise, returns 0.
4038              */
compareFlag(int flagToCompare, int flags1, int flags2)4039             private int compareFlag(int flagToCompare, int flags1, int flags2) {
4040                 boolean hasFlag1 = (flags1 & flagToCompare) != 0;
4041                 boolean hasFlag2 = (flags2 & flagToCompare) != 0;
4042                 if (hasFlag1 == hasFlag2) return 0;
4043                 return hasFlag1 ? -1 : 1;
4044             }
4045         }
4046 
4047         /**
4048          * Returns the suggestion spans that cover the current cursor position. The suggestion
4049          * spans are sorted according to the length of text that they are attached to.
4050          */
getSortedSuggestionSpans()4051         private SuggestionSpan[] getSortedSuggestionSpans() {
4052             int pos = mTextView.getSelectionStart();
4053             Spannable spannable = (Spannable) mTextView.getText();
4054             SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
4055 
4056             mSpansLengths.clear();
4057             for (SuggestionSpan suggestionSpan : suggestionSpans) {
4058                 int start = spannable.getSpanStart(suggestionSpan);
4059                 int end = spannable.getSpanEnd(suggestionSpan);
4060                 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
4061             }
4062 
4063             // The suggestions are sorted according to their types (easy correction first,
4064             // misspelled second, then grammar error) and to the length of the text that they cover
4065             // (shorter first).
4066             Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
4067             mSpansLengths.clear();
4068 
4069             return suggestionSpans;
4070         }
4071 
4072         /**
4073          * Gets the SuggestionInfo list that contains suggestion information at the current cursor
4074          * position.
4075          *
4076          * @param suggestionInfos SuggestionInfo array the results will be set.
4077          * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
4078          * @return the number of suggestions actually fetched.
4079          */
getSuggestionInfo(SuggestionInfo[] suggestionInfos, @Nullable SuggestionSpanInfo misspelledSpanInfo)4080         public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
4081                 @Nullable SuggestionSpanInfo misspelledSpanInfo) {
4082             final Spannable spannable = (Spannable) mTextView.getText();
4083             final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
4084             final int nbSpans = suggestionSpans.length;
4085             if (nbSpans == 0) return 0;
4086 
4087             int numberOfSuggestions = 0;
4088             for (final SuggestionSpan suggestionSpan : suggestionSpans) {
4089                 final int spanStart = spannable.getSpanStart(suggestionSpan);
4090                 final int spanEnd = spannable.getSpanEnd(suggestionSpan);
4091 
4092                 if (misspelledSpanInfo != null
4093                         && (suggestionSpan.getFlags() & FLAG_MISSPELLED_OR_GRAMMAR_ERROR) != 0) {
4094                     misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
4095                     misspelledSpanInfo.mSpanStart = spanStart;
4096                     misspelledSpanInfo.mSpanEnd = spanEnd;
4097                 }
4098 
4099                 final String[] suggestions = suggestionSpan.getSuggestions();
4100                 final int nbSuggestions = suggestions.length;
4101                 suggestionLoop:
4102                 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
4103                     final String suggestion = suggestions[suggestionIndex];
4104                     for (int i = 0; i < numberOfSuggestions; i++) {
4105                         final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
4106                         if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
4107                             final int otherSpanStart =
4108                                     otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
4109                             final int otherSpanEnd =
4110                                     otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
4111                             if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
4112                                 continue suggestionLoop;
4113                             }
4114                         }
4115                     }
4116 
4117                     SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
4118                     suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
4119                     suggestionInfo.mSuggestionIndex = suggestionIndex;
4120                     suggestionInfo.mSuggestionStart = 0;
4121                     suggestionInfo.mSuggestionEnd = suggestion.length();
4122                     suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
4123                     numberOfSuggestions++;
4124                     if (numberOfSuggestions >= suggestionInfos.length) {
4125                         return numberOfSuggestions;
4126                     }
4127                 }
4128             }
4129             return numberOfSuggestions;
4130         }
4131     }
4132 
4133     private final class SuggestionsPopupWindow extends PinnedPopupWindow
4134             implements OnItemClickListener {
4135         private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
4136 
4137         // Key of intent extras for inserting new word into user dictionary.
4138         private static final String USER_DICTIONARY_EXTRA_WORD = "word";
4139         private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
4140 
4141         private SuggestionInfo[] mSuggestionInfos;
4142         private int mNumberOfSuggestions;
4143         private boolean mCursorWasVisibleBeforeSuggestions;
4144         private boolean mIsShowingUp = false;
4145         private SuggestionAdapter mSuggestionsAdapter;
4146         private TextAppearanceSpan mHighlightSpan;  // TODO: Make mHighlightSpan final.
4147         private TextView mAddToDictionaryButton;
4148         private TextView mDeleteButton;
4149         private ListView mSuggestionListView;
4150         private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
4151         private int mContainerMarginWidth;
4152         private int mContainerMarginTop;
4153         private LinearLayout mContainerView;
4154         private Context mContext;  // TODO: Make mContext final.
4155 
4156         private class CustomPopupWindow extends PopupWindow {
4157 
4158             @Override
dismiss()4159             public void dismiss() {
4160                 if (!isShowing()) {
4161                     return;
4162                 }
4163                 super.dismiss();
4164                 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
4165 
4166                 // Safe cast since show() checks that mTextView.getText() is an Editable
4167                 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
4168 
4169                 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
4170                 if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
4171                     getInsertionController().show();
4172                 }
4173             }
4174         }
4175 
SuggestionsPopupWindow()4176         public SuggestionsPopupWindow() {
4177             mCursorWasVisibleBeforeSuggestions = mTextView.isCursorVisibleFromAttr();
4178         }
4179 
4180         @Override
setUp()4181         protected void setUp() {
4182             mContext = applyDefaultTheme(mTextView.getContext());
4183             mHighlightSpan = new TextAppearanceSpan(mContext,
4184                     mTextView.mTextEditSuggestionHighlightStyle);
4185         }
4186 
applyDefaultTheme(Context originalContext)4187         private Context applyDefaultTheme(Context originalContext) {
4188             TypedArray a = originalContext.obtainStyledAttributes(
4189                     new int[]{com.android.internal.R.attr.isLightTheme});
4190             boolean isLightTheme = a.getBoolean(0, true);
4191             int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
4192                     : R.style.ThemeOverlay_Material_Dark;
4193             a.recycle();
4194             return new ContextThemeWrapper(originalContext, themeId);
4195         }
4196 
4197         @Override
createPopupWindow()4198         protected void createPopupWindow() {
4199             mPopupWindow = new CustomPopupWindow();
4200             mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
4201             mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
4202             mPopupWindow.setFocusable(true);
4203             mPopupWindow.setClippingEnabled(false);
4204         }
4205 
4206         @Override
initContentView()4207         protected void initContentView() {
4208             final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
4209                     Context.LAYOUT_INFLATER_SERVICE);
4210             mContentView = (ViewGroup) inflater.inflate(
4211                     mTextView.mTextEditSuggestionContainerLayout, null);
4212 
4213             mContainerView = (LinearLayout) mContentView.findViewById(
4214                     com.android.internal.R.id.suggestionWindowContainer);
4215             ViewGroup.MarginLayoutParams lp =
4216                     (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
4217             mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
4218             mContainerMarginTop = lp.topMargin;
4219             mClippingLimitLeft = lp.leftMargin;
4220             mClippingLimitRight = lp.rightMargin;
4221 
4222             mSuggestionListView = (ListView) mContentView.findViewById(
4223                     com.android.internal.R.id.suggestionContainer);
4224 
4225             mSuggestionsAdapter = new SuggestionAdapter();
4226             mSuggestionListView.setAdapter(mSuggestionsAdapter);
4227             mSuggestionListView.setOnItemClickListener(this);
4228 
4229             // Inflate the suggestion items once and for all.
4230             mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
4231             for (int i = 0; i < mSuggestionInfos.length; i++) {
4232                 mSuggestionInfos[i] = new SuggestionInfo();
4233             }
4234 
4235             mAddToDictionaryButton = (TextView) mContentView.findViewById(
4236                     com.android.internal.R.id.addToDictionaryButton);
4237             mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
4238                 public void onClick(View v) {
4239                     final SuggestionSpan misspelledSpan =
4240                             findEquivalentSuggestionSpan(mMisspelledSpanInfo);
4241                     if (misspelledSpan == null) {
4242                         // Span has been removed.
4243                         return;
4244                     }
4245                     final Editable editable = (Editable) mTextView.getText();
4246                     final int spanStart = editable.getSpanStart(misspelledSpan);
4247                     final int spanEnd = editable.getSpanEnd(misspelledSpan);
4248                     if (spanStart < 0 || spanEnd <= spanStart) {
4249                         return;
4250                     }
4251                     final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
4252 
4253                     final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
4254                     intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
4255                     intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
4256                             mTextView.getTextServicesLocale().toString());
4257                     intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
4258                     mTextView.startActivityAsTextOperationUserIfNecessary(intent);
4259                     // There is no way to know if the word was indeed added. Re-check.
4260                     // TODO The ExtractEditText should remove the span in the original text instead
4261                     editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
4262                     Selection.setSelection(editable, spanEnd);
4263                     updateSpellCheckSpans(spanStart, spanEnd, false);
4264                     hideWithCleanUp();
4265                 }
4266             });
4267 
4268             mDeleteButton = (TextView) mContentView.findViewById(
4269                     com.android.internal.R.id.deleteButton);
4270             mDeleteButton.setOnClickListener(new View.OnClickListener() {
4271                 public void onClick(View v) {
4272                     final Editable editable = (Editable) mTextView.getText();
4273 
4274                     final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
4275                     int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
4276                     if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
4277                         // Do not leave two adjacent spaces after deletion, or one at beginning of
4278                         // text
4279                         if (spanUnionEnd < editable.length()
4280                                 && Character.isSpaceChar(editable.charAt(spanUnionEnd))
4281                                 && (spanUnionStart == 0
4282                                         || Character.isSpaceChar(
4283                                                 editable.charAt(spanUnionStart - 1)))) {
4284                             spanUnionEnd = spanUnionEnd + 1;
4285                         }
4286                         mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
4287                     }
4288                     hideWithCleanUp();
4289                 }
4290             });
4291 
4292         }
4293 
isShowingUp()4294         public boolean isShowingUp() {
4295             return mIsShowingUp;
4296         }
4297 
onParentLostFocus()4298         public void onParentLostFocus() {
4299             mIsShowingUp = false;
4300         }
4301 
4302         private class SuggestionAdapter extends BaseAdapter {
4303             private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
4304                     Context.LAYOUT_INFLATER_SERVICE);
4305 
4306             @Override
getCount()4307             public int getCount() {
4308                 return mNumberOfSuggestions;
4309             }
4310 
4311             @Override
getItem(int position)4312             public Object getItem(int position) {
4313                 return mSuggestionInfos[position];
4314             }
4315 
4316             @Override
getItemId(int position)4317             public long getItemId(int position) {
4318                 return position;
4319             }
4320 
4321             @Override
getView(int position, View convertView, ViewGroup parent)4322             public View getView(int position, View convertView, ViewGroup parent) {
4323                 TextView textView = (TextView) convertView;
4324 
4325                 if (textView == null) {
4326                     textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
4327                             parent, false);
4328                 }
4329 
4330                 final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
4331                 textView.setText(suggestionInfo.mText);
4332                 return textView;
4333             }
4334         }
4335 
4336         @Override
show()4337         public void show() {
4338             if (!(mTextView.getText() instanceof Editable)) return;
4339             if (extractedTextModeWillBeStarted()) {
4340                 return;
4341             }
4342 
4343             if (updateSuggestions()) {
4344                 mCursorWasVisibleBeforeSuggestions = mTextView.isCursorVisibleFromAttr();
4345                 mTextView.setCursorVisible(false);
4346                 mIsShowingUp = true;
4347                 super.show();
4348             }
4349 
4350             mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE);
4351         }
4352 
4353         @Override
measureContent()4354         protected void measureContent() {
4355             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
4356             final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
4357                     displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
4358             final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
4359                     displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
4360 
4361             int width = 0;
4362             View view = null;
4363             for (int i = 0; i < mNumberOfSuggestions; i++) {
4364                 view = mSuggestionsAdapter.getView(i, view, mContentView);
4365                 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
4366                 view.measure(horizontalMeasure, verticalMeasure);
4367                 width = Math.max(width, view.getMeasuredWidth());
4368             }
4369 
4370             if (mAddToDictionaryButton.getVisibility() != View.GONE) {
4371                 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
4372                 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
4373             }
4374 
4375             mDeleteButton.measure(horizontalMeasure, verticalMeasure);
4376             width = Math.max(width, mDeleteButton.getMeasuredWidth());
4377 
4378             width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
4379                     + mContainerMarginWidth;
4380 
4381             // Enforce the width based on actual text widths
4382             mContentView.measure(
4383                     View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
4384                     verticalMeasure);
4385 
4386             Drawable popupBackground = mPopupWindow.getBackground();
4387             if (popupBackground != null) {
4388                 if (mTempRect == null) mTempRect = new Rect();
4389                 popupBackground.getPadding(mTempRect);
4390                 width += mTempRect.left + mTempRect.right;
4391             }
4392             mPopupWindow.setWidth(width);
4393         }
4394 
4395         @Override
getTextOffset()4396         protected int getTextOffset() {
4397             return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
4398         }
4399 
4400         @Override
getVerticalLocalPosition(int line)4401         protected int getVerticalLocalPosition(int line) {
4402             final Layout layout = mTextView.getLayout();
4403             return layout.getLineBottom(line, /* includeLineSpacing= */ false)
4404                     - mContainerMarginTop;
4405         }
4406 
4407         @Override
clipVertically(int positionY)4408         protected int clipVertically(int positionY) {
4409             final int height = mContentView.getMeasuredHeight();
4410             final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
4411             return Math.min(positionY, displayMetrics.heightPixels - height);
4412         }
4413 
hideWithCleanUp()4414         private void hideWithCleanUp() {
4415             for (final SuggestionInfo info : mSuggestionInfos) {
4416                 info.clear();
4417             }
4418             mMisspelledSpanInfo.clear();
4419             hide();
4420         }
4421 
updateSuggestions()4422         private boolean updateSuggestions() {
4423             Spannable spannable = (Spannable) mTextView.getText();
4424             mNumberOfSuggestions =
4425                     mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
4426             if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
4427                 return false;
4428             }
4429 
4430             int spanUnionStart = mTextView.getText().length();
4431             int spanUnionEnd = 0;
4432 
4433             for (int i = 0; i < mNumberOfSuggestions; i++) {
4434                 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
4435                 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
4436                 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
4437             }
4438             if (mMisspelledSpanInfo.mSuggestionSpan != null) {
4439                 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
4440                 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
4441             }
4442 
4443             for (int i = 0; i < mNumberOfSuggestions; i++) {
4444                 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
4445             }
4446 
4447             // Make "Add to dictionary" item visible if there is a span with the misspelled flag
4448             int addToDictionaryButtonVisibility = View.GONE;
4449             if (mMisspelledSpanInfo.mSuggestionSpan != null) {
4450                 if (mMisspelledSpanInfo.mSpanStart >= 0
4451                         && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
4452                     addToDictionaryButtonVisibility = View.VISIBLE;
4453                 }
4454             }
4455             mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
4456 
4457             if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
4458             final int underlineColor;
4459             if (mNumberOfSuggestions != 0) {
4460                 underlineColor =
4461                         mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
4462             } else {
4463                 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
4464             }
4465 
4466             if (underlineColor == 0) {
4467                 // Fallback on the default highlight color when the first span does not provide one
4468                 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
4469             } else {
4470                 final float BACKGROUND_TRANSPARENCY = 0.4f;
4471                 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
4472                 mSuggestionRangeSpan.setBackgroundColor(
4473                         (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
4474             }
4475             boolean sendAccessibilityEvent = mTextView.isVisibleToAccessibility();
4476             CharSequence beforeText = sendAccessibilityEvent
4477                     ? new SpannedString(spannable, true) : null;
4478             spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
4479                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
4480             if (sendAccessibilityEvent) {
4481                 mTextView.sendAccessibilityEventTypeViewTextChanged(
4482                         beforeText, spanUnionStart, spanUnionEnd);
4483             }
4484 
4485             mSuggestionsAdapter.notifyDataSetChanged();
4486             return true;
4487         }
4488 
highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, int unionEnd)4489         private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
4490                 int unionEnd) {
4491             final Spannable text = (Spannable) mTextView.getText();
4492             final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
4493             final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
4494 
4495             // Adjust the start/end of the suggestion span
4496             suggestionInfo.mSuggestionStart = spanStart - unionStart;
4497             suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
4498                     + suggestionInfo.mText.length();
4499 
4500             suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
4501                     Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
4502 
4503             // Add the text before and after the span.
4504             final String textAsString = text.toString();
4505             suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
4506             suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
4507         }
4508 
4509         @Override
onItemClick(AdapterView<?> parent, View view, int position, long id)4510         public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
4511             SuggestionInfo suggestionInfo = mSuggestionInfos[position];
4512             replaceWithSuggestion(suggestionInfo);
4513             hideWithCleanUp();
4514         }
4515     }
4516 
4517     /**
4518      * Helper class for UI component (e.g. ActionMode and ContextMenu) with TextClassification.
4519      * @hide
4520      */
4521     @VisibleForTesting
4522     public class AssistantCallbackHelper {
4523         private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>();
4524         @Nullable private TextClassification mPrevTextClassification;
4525         @NonNull private final SelectionActionModeHelper mHelper;
4526 
AssistantCallbackHelper(SelectionActionModeHelper helper)4527         public AssistantCallbackHelper(SelectionActionModeHelper helper) {
4528             mHelper = helper;
4529         }
4530 
4531         /**
4532          * Clears callback handlers.
4533          */
clearCallbackHandlers()4534         public void clearCallbackHandlers() {
4535             mAssistClickHandlers.clear();
4536         }
4537 
4538         /**
4539          * Get on click listener assisiated with the MenuItem.
4540          */
getOnClickListener(MenuItem key)4541         public OnClickListener getOnClickListener(MenuItem key) {
4542             return mAssistClickHandlers.get(key);
4543         }
4544 
4545         /**
4546          * Update menu items.
4547          *
4548          * Existing assist menu will be cleared and latest assist menu will be added.
4549          */
updateAssistMenuItems(Menu menu, MenuItem.OnMenuItemClickListener listener)4550         public void updateAssistMenuItems(Menu menu, MenuItem.OnMenuItemClickListener listener) {
4551             final TextClassification textClassification = mHelper.getTextClassification();
4552             if (mPrevTextClassification == textClassification) {
4553                 // Already handled.
4554                 return;
4555             }
4556             clearAssistMenuItems(menu);
4557             if (textClassification == null) {
4558                 return;
4559             }
4560             if (!shouldEnableAssistMenuItems()) {
4561                 return;
4562             }
4563             if (!textClassification.getActions().isEmpty()) {
4564                 // Primary assist action (Always shown).
4565                 final MenuItem item = addAssistMenuItem(menu,
4566                         textClassification.getActions().get(0), TextView.ID_ASSIST,
4567                         ACTION_MODE_MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS,
4568                         listener);
4569                 item.setIntent(textClassification.getIntent());
4570             } else if (hasLegacyAssistItem(textClassification)) {
4571                 // Legacy primary assist action (Always shown).
4572                 final MenuItem item = menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST,
4573                                 ACTION_MODE_MENU_ITEM_ORDER_ASSIST,
4574                                 textClassification.getLabel())
4575                         .setIcon(textClassification.getIcon())
4576                         .setIntent(textClassification.getIntent());
4577                 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
4578                 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener(
4579                         TextClassification.createPendingIntent(mTextView.getContext(),
4580                                 textClassification.getIntent(),
4581                                 createAssistMenuItemPendingIntentRequestCode())));
4582             }
4583             final int count = textClassification.getActions().size();
4584             for (int i = 1; i < count; i++) {
4585                 // Secondary assist action (Never shown).
4586                 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE,
4587                         ACTION_MODE_MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1,
4588                         MenuItem.SHOW_AS_ACTION_NEVER, listener);
4589             }
4590             mPrevTextClassification = textClassification;
4591         }
4592 
addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order, int showAsAction, MenuItem.OnMenuItemClickListener listener)4593         private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order,
4594                 int showAsAction, MenuItem.OnMenuItemClickListener listener) {
4595             final MenuItem item = menu.add(TextView.ID_ASSIST, itemId, order, action.getTitle())
4596                     .setContentDescription(action.getContentDescription());
4597             if (action.shouldShowIcon()) {
4598                 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext()));
4599             }
4600             item.setShowAsAction(showAsAction);
4601             mAssistClickHandlers.put(item,
4602                     TextClassification.createIntentOnClickListener(action.getActionIntent()));
4603             mA11ySmartActions.addAction(action);
4604             if (listener != null) {
4605                 item.setOnMenuItemClickListener(listener);
4606             }
4607             return item;
4608         }
4609 
clearAssistMenuItems(Menu menu)4610         private void clearAssistMenuItems(Menu menu) {
4611             int i = 0;
4612             while (i < menu.size()) {
4613                 final MenuItem menuItem = menu.getItem(i);
4614                 if (menuItem.getGroupId() == TextView.ID_ASSIST) {
4615                     menu.removeItem(menuItem.getItemId());
4616                     continue;
4617                 }
4618                 i++;
4619             }
4620             mA11ySmartActions.reset();
4621         }
4622 
hasLegacyAssistItem(TextClassification classification)4623         private boolean hasLegacyAssistItem(TextClassification classification) {
4624             // Check whether we have the UI data and action.
4625             return (classification.getIcon() != null || !TextUtils.isEmpty(
4626                     classification.getLabel())) && (classification.getIntent() != null
4627                     || classification.getOnClickListener() != null);
4628         }
4629 
shouldEnableAssistMenuItems()4630         private boolean shouldEnableAssistMenuItems() {
4631             return mTextView.isDeviceProvisioned()
4632                     && TextClassificationManager.getSettings(mTextView.getContext())
4633                     .isSmartTextShareEnabled();
4634         }
4635 
createAssistMenuItemPendingIntentRequestCode()4636         private int createAssistMenuItemPendingIntentRequestCode() {
4637             return mTextView.hasSelection()
4638                     ? mTextView.getText().subSequence(
4639                             mTextView.getSelectionStart(), mTextView.getSelectionEnd())
4640                     .hashCode()
4641                     : 0;
4642         }
4643 
4644         /**
4645          * Called when the assist menu on ActionMode or ContextMenu is called.
4646          */
onAssistMenuItemClicked(MenuItem assistMenuItem)4647         public boolean onAssistMenuItemClicked(MenuItem assistMenuItem) {
4648             Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST);
4649 
4650             final TextClassification textClassification =
4651                     getSelectionActionModeHelper().getTextClassification();
4652             if (!shouldEnableAssistMenuItems() || textClassification == null) {
4653                 // No textClassification result to handle the click. Eat the click.
4654                 return true;
4655             }
4656 
4657             OnClickListener onClickListener = getOnClickListener(assistMenuItem);
4658             if (onClickListener == null) {
4659                 final Intent intent = assistMenuItem.getIntent();
4660                 if (intent != null) {
4661                     onClickListener = TextClassification.createIntentOnClickListener(
4662                             TextClassification.createPendingIntent(
4663                                     mTextView.getContext(), intent,
4664                                     createAssistMenuItemPendingIntentRequestCode()));
4665                 }
4666             }
4667             if (onClickListener != null) {
4668                 onClickListener.onClick(mTextView);
4669                 stopTextActionMode();
4670             }
4671             // We tried our best.
4672             return true;
4673         }
4674     }
4675 
4676     /**
4677      * An ActionMode Callback class that is used to provide actions while in text insertion or
4678      * selection mode.
4679      *
4680      * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
4681      * actions, depending on which of these this TextView supports and the current selection.
4682      */
4683     private class TextActionModeCallback extends ActionMode.Callback2 {
4684         private final Path mSelectionPath = new Path();
4685         private final RectF mSelectionBounds = new RectF();
4686         private final boolean mHasSelection;
4687         private final int mHandleHeight;
4688         private final AssistantCallbackHelper mHelper = new AssistantCallbackHelper(
4689                 getSelectionActionModeHelper());
4690 
TextActionModeCallback(@extActionMode int mode)4691         TextActionModeCallback(@TextActionMode int mode) {
4692             mHasSelection = mode == TextActionMode.SELECTION
4693                     || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK);
4694             if (mHasSelection) {
4695                 SelectionModifierCursorController selectionController = getSelectionController();
4696                 if (selectionController.mStartHandle == null) {
4697                     // As these are for initializing selectionController, hide() must be called.
4698                     loadHandleDrawables(false /* overwrite */);
4699                     selectionController.initHandles();
4700                     selectionController.hide();
4701                 }
4702                 mHandleHeight = Math.max(
4703                         mSelectHandleLeft.getMinimumHeight(),
4704                         mSelectHandleRight.getMinimumHeight());
4705             } else {
4706                 InsertionPointCursorController insertionController = getInsertionController();
4707                 if (insertionController != null) {
4708                     insertionController.getHandle();
4709                     mHandleHeight = mSelectHandleCenter.getMinimumHeight();
4710                 } else {
4711                     mHandleHeight = 0;
4712                 }
4713             }
4714         }
4715 
4716         @Override
onCreateActionMode(ActionMode mode, Menu menu)4717         public boolean onCreateActionMode(ActionMode mode, Menu menu) {
4718             mHelper.clearCallbackHandlers();
4719 
4720             mode.setTitle(null);
4721             mode.setSubtitle(null);
4722             mode.setTitleOptionalHint(true);
4723             populateMenuWithItems(menu);
4724 
4725             Callback customCallback = getCustomCallback();
4726             if (customCallback != null) {
4727                 if (!customCallback.onCreateActionMode(mode, menu)) {
4728                     // The custom mode can choose to cancel the action mode, dismiss selection.
4729                     Selection.setSelection((Spannable) mTextView.getText(),
4730                             mTextView.getSelectionEnd());
4731                     return false;
4732                 }
4733             }
4734 
4735             if (mTextView.canProcessText()) {
4736                 mProcessTextIntentActionsHandler.onInitializeMenu(menu);
4737             }
4738 
4739             if (mHasSelection && !mTextView.hasTransientState()) {
4740                 mTextView.setHasTransientState(true);
4741             }
4742             return true;
4743         }
4744 
getCustomCallback()4745         private Callback getCustomCallback() {
4746             return mHasSelection
4747                     ? mCustomSelectionActionModeCallback
4748                     : mCustomInsertionActionModeCallback;
4749         }
4750 
populateMenuWithItems(Menu menu)4751         private void populateMenuWithItems(Menu menu) {
4752             if (mTextView.canCut()) {
4753                 menu.add(Menu.NONE, TextView.ID_CUT, ACTION_MODE_MENU_ITEM_ORDER_CUT,
4754                         com.android.internal.R.string.cut)
4755                                 .setAlphabeticShortcut('x')
4756                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
4757             }
4758 
4759             if (mTextView.canCopy()) {
4760                 menu.add(Menu.NONE, TextView.ID_COPY, ACTION_MODE_MENU_ITEM_ORDER_COPY,
4761                         com.android.internal.R.string.copy)
4762                                 .setAlphabeticShortcut('c')
4763                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
4764             }
4765 
4766             if (mTextView.canPaste()) {
4767                 menu.add(Menu.NONE, TextView.ID_PASTE, ACTION_MODE_MENU_ITEM_ORDER_PASTE,
4768                         com.android.internal.R.string.paste)
4769                                 .setAlphabeticShortcut('v')
4770                                 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
4771             }
4772 
4773             if (mTextView.canShare()) {
4774                 menu.add(Menu.NONE, TextView.ID_SHARE, ACTION_MODE_MENU_ITEM_ORDER_SHARE,
4775                         com.android.internal.R.string.share)
4776                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4777             }
4778 
4779             if (mTextView.canRequestAutofill()) {
4780                 final String selected = mTextView.getSelectedText();
4781                 if (selected == null || selected.isEmpty()) {
4782                     menu.add(Menu.NONE, TextView.ID_AUTOFILL, ACTION_MODE_MENU_ITEM_ORDER_AUTOFILL,
4783                             com.android.internal.R.string.autofill)
4784                             .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
4785                 }
4786             }
4787 
4788             if (mTextView.canPasteAsPlainText()) {
4789                 menu.add(
4790                         Menu.NONE,
4791                         TextView.ID_PASTE_AS_PLAIN_TEXT,
4792                                 ACTION_MODE_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
4793                         com.android.internal.R.string.paste_as_plain_text)
4794                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4795             }
4796 
4797             updateSelectAllItem(menu);
4798             updateReplaceItem(menu);
4799             mHelper.updateAssistMenuItems(menu, null);
4800         }
4801 
4802         @Override
onPrepareActionMode(ActionMode mode, Menu menu)4803         public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
4804             updateSelectAllItem(menu);
4805             updateReplaceItem(menu);
4806             mHelper.updateAssistMenuItems(menu, null);
4807 
4808             Callback customCallback = getCustomCallback();
4809             if (customCallback != null) {
4810                 return customCallback.onPrepareActionMode(mode, menu);
4811             }
4812             return true;
4813         }
4814 
updateSelectAllItem(Menu menu)4815         private void updateSelectAllItem(Menu menu) {
4816             boolean canSelectAll = mTextView.canSelectAllText();
4817             boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
4818             if (canSelectAll && !selectAllItemExists) {
4819                 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, ACTION_MODE_MENU_ITEM_ORDER_SELECT_ALL,
4820                         com.android.internal.R.string.selectAll)
4821                     .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4822             } else if (!canSelectAll && selectAllItemExists) {
4823                 menu.removeItem(TextView.ID_SELECT_ALL);
4824             }
4825         }
4826 
updateReplaceItem(Menu menu)4827         private void updateReplaceItem(Menu menu) {
4828             boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
4829             boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
4830             if (canReplace && !replaceItemExists) {
4831                 menu.add(Menu.NONE, TextView.ID_REPLACE, ACTION_MODE_MENU_ITEM_ORDER_REPLACE,
4832                         com.android.internal.R.string.replace)
4833                     .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
4834             } else if (!canReplace && replaceItemExists) {
4835                 menu.removeItem(TextView.ID_REPLACE);
4836             }
4837         }
4838 
4839         @Override
onActionItemClicked(ActionMode mode, MenuItem item)4840         public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
4841             getSelectionActionModeHelper()
4842                     .onSelectionAction(item.getItemId(), item.getTitle().toString());
4843 
4844             if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
4845                 return true;
4846             }
4847             Callback customCallback = getCustomCallback();
4848             if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
4849                 return true;
4850             }
4851             if (item.getGroupId() == TextView.ID_ASSIST && mHelper.onAssistMenuItemClicked(item)) {
4852                 return true;
4853             }
4854             return mTextView.onTextContextMenuItem(item.getItemId());
4855         }
4856 
4857         @Override
onDestroyActionMode(ActionMode mode)4858         public void onDestroyActionMode(ActionMode mode) {
4859             // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
4860             getSelectionActionModeHelper().onDestroyActionMode();
4861             mTextActionMode = null;
4862             Callback customCallback = getCustomCallback();
4863             if (customCallback != null) {
4864                 customCallback.onDestroyActionMode(mode);
4865             }
4866 
4867             if (!mPreserveSelection) {
4868                 /*
4869                  * Leave current selection when we tentatively destroy action mode for the
4870                  * selection. If we're detaching from a window, we'll bring back the selection
4871                  * mode when (if) we get reattached.
4872                  */
4873                 Selection.setSelection((Spannable) mTextView.getText(),
4874                         mTextView.getSelectionEnd());
4875             }
4876 
4877             if (mSelectionModifierCursorController != null) {
4878                 mSelectionModifierCursorController.hide();
4879             }
4880 
4881             mHelper.clearCallbackHandlers();
4882             mRequestingLinkActionMode = false;
4883         }
4884 
4885         @Override
onGetContentRect(ActionMode mode, View view, Rect outRect)4886         public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
4887             if (!view.equals(mTextView) || mTextView.getLayout() == null) {
4888                 super.onGetContentRect(mode, view, outRect);
4889                 return;
4890             }
4891             final int selectionStart = mTextView.getSelectionStartTransformed();
4892             final int selectionEnd = mTextView.getSelectionEndTransformed();
4893             final Layout layout = mTextView.getLayout();
4894             if (selectionStart != selectionEnd) {
4895                 // We have a selection.
4896                 mSelectionPath.reset();
4897                 layout.getSelectionPath(selectionStart, selectionEnd, mSelectionPath);
4898                 mSelectionPath.computeBounds(mSelectionBounds, true);
4899                 mSelectionBounds.bottom += mHandleHeight;
4900             } else {
4901                 // We have a cursor.
4902                 int line = layout.getLineForOffset(selectionStart);
4903                 float primaryHorizontal =
4904                         clampHorizontalPosition(null, layout.getPrimaryHorizontal(selectionEnd));
4905                 mSelectionBounds.set(
4906                         primaryHorizontal,
4907                         layout.getLineTop(line),
4908                         primaryHorizontal,
4909                         layout.getLineBottom(line) + mHandleHeight);
4910             }
4911             // Take TextView's padding and scroll into account.
4912             int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4913             int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4914             outRect.set(
4915                     (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4916                     (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4917                     (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4918                     (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4919         }
4920     }
4921 
4922     /**
4923      * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4924      * while the input method is requesting the cursor/anchor position. Does nothing as long as
4925      * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4926      */
4927     private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
4928         final CursorAnchorInfo.Builder mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder();
4929         final Matrix mViewToScreenMatrix = new Matrix();
4930 
4931         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)4932         public void updatePosition(int parentPositionX, int parentPositionY,
4933                 boolean parentPositionChanged, boolean parentScrolled) {
4934             final InputMethodState ims = mInputMethodState;
4935             if (ims == null || ims.mBatchEditNesting > 0) {
4936                 return;
4937             }
4938             final InputMethodManager imm = getInputMethodManager();
4939             if (null == imm) {
4940                 return;
4941             }
4942             if (!imm.hasActiveInputConnection(mTextView)) {
4943                 return;
4944             }
4945             // Skip if the IME has not requested the cursor/anchor position.
4946             final int knownCursorAnchorInfoModes =
4947                     InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR;
4948             if ((ims.mUpdateCursorAnchorInfoMode & knownCursorAnchorInfoModes) == 0) {
4949                 return;
4950             }
4951 
4952             final CursorAnchorInfo cursorAnchorInfo =
4953                     mTextView.getCursorAnchorInfo(ims.mUpdateCursorAnchorInfoFilter,
4954                             mCursorAnchorInfoBuilder, mViewToScreenMatrix);
4955 
4956             if (cursorAnchorInfo != null) {
4957                 imm.updateCursorAnchorInfo(mTextView, cursorAnchorInfo);
4958 
4959                 // Drop the immediate flag if any.
4960                 mInputMethodState.mUpdateCursorAnchorInfoMode &=
4961                         ~InputConnection.CURSOR_UPDATE_IMMEDIATE;
4962             }
4963         }
4964     }
4965 
4966     private static class MagnifierMotionAnimator {
4967         private static final long DURATION = 100 /* miliseconds */;
4968 
4969         // The magnifier being animated.
4970         private final Magnifier mMagnifier;
4971         // A value animator used to animate the magnifier.
4972         private final ValueAnimator mAnimator;
4973 
4974         // Whether the magnifier is currently visible.
4975         private boolean mMagnifierIsShowing;
4976         // The coordinates of the magnifier when the currently running animation started.
4977         private float mAnimationStartX;
4978         private float mAnimationStartY;
4979         // The coordinates of the magnifier in the latest animation frame.
4980         private float mAnimationCurrentX;
4981         private float mAnimationCurrentY;
4982         // The latest coordinates the motion animator was asked to #show() the magnifier at.
4983         private float mLastX;
4984         private float mLastY;
4985 
MagnifierMotionAnimator(final Magnifier magnifier)4986         private MagnifierMotionAnimator(final Magnifier magnifier) {
4987             mMagnifier = magnifier;
4988             // Prepare the animator used to run the motion animation.
4989             mAnimator = ValueAnimator.ofFloat(0, 1);
4990             mAnimator.setDuration(DURATION);
4991             mAnimator.setInterpolator(new LinearInterpolator());
4992             mAnimator.addUpdateListener((animation) -> {
4993                 // Interpolate to find the current position of the magnifier.
4994                 mAnimationCurrentX = mAnimationStartX
4995                         + (mLastX - mAnimationStartX) * animation.getAnimatedFraction();
4996                 mAnimationCurrentY = mAnimationStartY
4997                         + (mLastY - mAnimationStartY) * animation.getAnimatedFraction();
4998                 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY);
4999             });
5000         }
5001 
5002         /**
5003          * Shows the magnifier at a new position.
5004          * If the y coordinate is different from the previous y coordinate
5005          * (probably corresponding to a line jump in the text), a short
5006          * animation is added to the jump.
5007          */
show(final float x, final float y)5008         private void show(final float x, final float y) {
5009             final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY;
5010 
5011             if (startNewAnimation) {
5012                 if (mAnimator.isRunning()) {
5013                     mAnimator.cancel();
5014                     mAnimationStartX = mAnimationCurrentX;
5015                     mAnimationStartY = mAnimationCurrentY;
5016                 } else {
5017                     mAnimationStartX = mLastX;
5018                     mAnimationStartY = mLastY;
5019                 }
5020                 mAnimator.start();
5021             } else {
5022                 if (!mAnimator.isRunning()) {
5023                     mMagnifier.show(x, y);
5024                 }
5025             }
5026             mLastX = x;
5027             mLastY = y;
5028             mMagnifierIsShowing = true;
5029         }
5030 
5031         /**
5032          * Updates the content of the magnifier.
5033          */
update()5034         private void update() {
5035             mMagnifier.update();
5036         }
5037 
5038         /**
5039          * Dismisses the magnifier, or does nothing if it is already dismissed.
5040          */
dismiss()5041         private void dismiss() {
5042             mMagnifier.dismiss();
5043             mAnimator.cancel();
5044             mMagnifierIsShowing = false;
5045         }
5046     }
5047 
5048     @VisibleForTesting
5049     public abstract class HandleView extends View implements TextViewPositionListener {
5050         protected Drawable mDrawable;
5051         protected Drawable mDrawableLtr;
5052         protected Drawable mDrawableRtl;
5053         private final PopupWindow mContainer;
5054         // Position with respect to the parent TextView
5055         private int mPositionX, mPositionY;
5056         private boolean mIsDragging;
5057         // Offset from touch position to mPosition
5058         private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
5059         protected int mHotspotX;
5060         protected int mHorizontalGravity;
5061         // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
5062         private float mTouchOffsetY;
5063         // Where the touch position should be on the handle to ensure a maximum cursor visibility.
5064         // This is the distance in pixels from the top of the handle view.
5065         private final float mIdealVerticalOffset;
5066         // Parent's (TextView) previous position in window
5067         private int mLastParentX, mLastParentY;
5068         // Parent's (TextView) previous position on screen
5069         private int mLastParentXOnScreen, mLastParentYOnScreen;
5070         // Previous text character offset
5071         protected int mPreviousOffset = -1;
5072         // Previous text character offset
5073         private boolean mPositionHasChanged = true;
5074         // Minimum touch target size for handles
5075         private int mMinSize;
5076         // Indicates the line of text that the handle is on.
5077         protected int mPrevLine = UNSET_LINE;
5078         // Indicates the line of text that the user was touching. This can differ from mPrevLine
5079         // when selecting text when the handles jump to the end / start of words which may be on
5080         // a different line.
5081         protected int mPreviousLineTouched = UNSET_LINE;
5082         // The raw x coordinate of the motion down event which started the current dragging session.
5083         // Only used and stored when magnifier is used.
5084         private float mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
5085         // The scale transform applied by containers to the TextView. Only used and computed
5086         // when magnifier is used.
5087         private float mTextViewScaleX;
5088         private float mTextViewScaleY;
5089         /**
5090          * The vertical distance in pixels from finger to the cursor Y while dragging.
5091          * See {@link Editor.InsertionPointCursorController#getLineDuringDrag}.
5092          */
5093         private final int mIdealFingerToCursorOffset;
5094 
HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id)5095         private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
5096             super(mTextView.getContext());
5097             setId(id);
5098             mContainer = new PopupWindow(mTextView.getContext(), null,
5099                     com.android.internal.R.attr.textSelectHandleWindowStyle);
5100             mContainer.setSplitTouchEnabled(true);
5101             mContainer.setClippingEnabled(false);
5102             mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
5103             mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
5104             mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
5105             mContainer.setContentView(this);
5106 
5107             setDrawables(drawableLtr, drawableRtl);
5108 
5109             mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
5110                     com.android.internal.R.dimen.text_handle_min_size);
5111 
5112             final int handleHeight = getPreferredHeight();
5113             mTouchOffsetY = -0.3f * handleHeight;
5114             final int distance = AppGlobals.getIntCoreSetting(
5115                     WidgetFlags.KEY_FINGER_TO_CURSOR_DISTANCE,
5116                     WidgetFlags.FINGER_TO_CURSOR_DISTANCE_DEFAULT);
5117             if (distance < 0 || distance > 100) {
5118                 mIdealVerticalOffset = 0.7f * handleHeight;
5119                 mIdealFingerToCursorOffset = (int)(mIdealVerticalOffset - mTouchOffsetY);
5120             } else {
5121                 mIdealFingerToCursorOffset = (int) TypedValue.applyDimension(
5122                         TypedValue.COMPLEX_UNIT_DIP, distance,
5123                         mTextView.getContext().getResources().getDisplayMetrics());
5124                 mIdealVerticalOffset = mIdealFingerToCursorOffset + mTouchOffsetY;
5125             }
5126         }
5127 
getIdealVerticalOffset()5128         public float getIdealVerticalOffset() {
5129             return mIdealVerticalOffset;
5130         }
5131 
getIdealFingerToCursorOffset()5132         final int getIdealFingerToCursorOffset() {
5133             return mIdealFingerToCursorOffset;
5134         }
5135 
setDrawables(final Drawable drawableLtr, final Drawable drawableRtl)5136         void setDrawables(final Drawable drawableLtr, final Drawable drawableRtl) {
5137             mDrawableLtr = drawableLtr;
5138             mDrawableRtl = drawableRtl;
5139             updateDrawable(true /* updateDrawableWhenDragging */);
5140         }
5141 
updateDrawable(final boolean updateDrawableWhenDragging)5142         protected void updateDrawable(final boolean updateDrawableWhenDragging) {
5143             if (!updateDrawableWhenDragging && mIsDragging) {
5144                 return;
5145             }
5146             final Layout layout = mTextView.getLayout();
5147             if (layout == null) {
5148                 return;
5149             }
5150             final int offset = getCurrentCursorOffset();
5151             final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
5152             final Drawable oldDrawable = mDrawable;
5153             mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
5154             mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
5155             mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
5156             if (oldDrawable != mDrawable && isShowing()) {
5157                 // Update popup window position.
5158                 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
5159                         - getHorizontalOffset() + getCursorOffset();
5160                 mPositionX += mTextView.viewportToContentHorizontalOffset();
5161                 mPositionHasChanged = true;
5162                 updatePosition(mLastParentX, mLastParentY, false, false);
5163                 postInvalidate();
5164             }
5165         }
5166 
getHotspotX(Drawable drawable, boolean isRtlRun)5167         protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
getHorizontalGravity(boolean isRtlRun)5168         protected abstract int getHorizontalGravity(boolean isRtlRun);
5169 
5170         // Touch-up filter: number of previous positions remembered
5171         private static final int HISTORY_SIZE = 5;
5172         private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
5173         private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
5174         private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
5175         private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
5176         private int mPreviousOffsetIndex = 0;
5177         private int mNumberPreviousOffsets = 0;
5178 
startTouchUpFilter(int offset)5179         private void startTouchUpFilter(int offset) {
5180             mNumberPreviousOffsets = 0;
5181             addPositionToTouchUpFilter(offset);
5182         }
5183 
addPositionToTouchUpFilter(int offset)5184         private void addPositionToTouchUpFilter(int offset) {
5185             mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
5186             mPreviousOffsets[mPreviousOffsetIndex] = offset;
5187             mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
5188             mNumberPreviousOffsets++;
5189         }
5190 
filterOnTouchUp(boolean fromTouchScreen)5191         private void filterOnTouchUp(boolean fromTouchScreen) {
5192             final long now = SystemClock.uptimeMillis();
5193             int i = 0;
5194             int index = mPreviousOffsetIndex;
5195             final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
5196             while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
5197                 i++;
5198                 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
5199             }
5200 
5201             if (i > 0 && i < iMax
5202                     && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
5203                 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen);
5204             }
5205         }
5206 
offsetHasBeenChanged()5207         public boolean offsetHasBeenChanged() {
5208             return mNumberPreviousOffsets > 1;
5209         }
5210 
5211         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)5212         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5213             setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
5214         }
5215 
5216         @Override
invalidate()5217         public void invalidate() {
5218             super.invalidate();
5219             if (isShowing()) {
5220                 positionAtCursorOffset(getCurrentCursorOffset(), true, false);
5221             }
5222         };
5223 
getPreferredWidth()5224         protected final int getPreferredWidth() {
5225             return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
5226         }
5227 
getPreferredHeight()5228         protected final int getPreferredHeight() {
5229             return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
5230         }
5231 
show()5232         public void show() {
5233             if (TextView.DEBUG_CURSOR) {
5234                 logCursor(getClass().getSimpleName() + ": HandleView: show()", "offset=%s",
5235                         getCurrentCursorOffset());
5236             }
5237 
5238             if (isShowing()) return;
5239 
5240             getPositionListener().addSubscriber(this, true /* local position may change */);
5241 
5242             // Make sure the offset is always considered new, even when focusing at same position
5243             mPreviousOffset = -1;
5244             positionAtCursorOffset(getCurrentCursorOffset(), false, false);
5245         }
5246 
dismiss()5247         protected void dismiss() {
5248             mIsDragging = false;
5249             mContainer.dismiss();
5250             onDetached();
5251         }
5252 
hide()5253         public void hide() {
5254             if (TextView.DEBUG_CURSOR) {
5255                 logCursor(getClass().getSimpleName() + ": HandleView: hide()", "offset=%s",
5256                         getCurrentCursorOffset());
5257             }
5258 
5259             dismiss();
5260 
5261             getPositionListener().removeSubscriber(this);
5262         }
5263 
isShowing()5264         public boolean isShowing() {
5265             return mContainer.isShowing();
5266         }
5267 
shouldShow()5268         private boolean shouldShow() {
5269             // A dragging handle should always be shown.
5270             if (mIsDragging) {
5271                 return true;
5272             }
5273 
5274             if (mTextView.isInBatchEditMode()) {
5275                 return false;
5276             }
5277 
5278             return mTextView.isPositionVisible(
5279                     mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
5280         }
5281 
setVisible(final boolean visible)5282         private void setVisible(final boolean visible) {
5283             mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE);
5284         }
5285 
getCurrentCursorOffset()5286         public abstract int getCurrentCursorOffset();
5287 
updateSelection(int offset)5288         protected abstract void updateSelection(int offset);
5289 
updatePosition(float x, float y, boolean fromTouchScreen)5290         protected abstract void updatePosition(float x, float y, boolean fromTouchScreen);
5291 
5292         @MagnifierHandleTrigger
getMagnifierHandleTrigger()5293         protected abstract int getMagnifierHandleTrigger();
5294 
isAtRtlRun(@onNull Layout layout, int offset)5295         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5296             final int transformedOffset =
5297                     mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR);
5298             return layout.isRtlCharAt(transformedOffset);
5299         }
5300 
5301         @VisibleForTesting
getHorizontal(@onNull Layout layout, int offset)5302         public float getHorizontal(@NonNull Layout layout, int offset) {
5303             final int transformedOffset =
5304                     mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR);
5305             return layout.getPrimaryHorizontal(transformedOffset);
5306         }
5307 
5308         /**
5309          * Return the line number for a given offset.
5310          * @param layout the {@link Layout} to query.
5311          * @param offset the index of the character to query.
5312          * @return the index of the line the given offset belongs to.
5313          */
getLineForOffset(@onNull Layout layout, int offset)5314         public int getLineForOffset(@NonNull Layout layout, int offset) {
5315             final int transformedOffset =
5316                     mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR);
5317             return layout.getLineForOffset(transformedOffset);
5318         }
5319 
getOffsetAtCoordinate(@onNull Layout layout, int line, float x)5320         protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
5321             return mTextView.getOffsetAtCoordinate(line, x);
5322         }
5323 
5324         /**
5325          * @param offset Cursor offset. Must be in [-1, length].
5326          * @param forceUpdatePosition whether to force update the position.  This should be true
5327          * when If the parent has been scrolled, for example.
5328          * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the
5329          * touch screen.
5330          */
positionAtCursorOffset(int offset, boolean forceUpdatePosition, boolean fromTouchScreen)5331         protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5332                 boolean fromTouchScreen) {
5333             // A HandleView relies on the layout, which may be nulled by external methods
5334             final Layout layout = mTextView.getLayout();
5335             if (layout == null) {
5336                 // Will update controllers' state, hiding them and stopping selection mode if needed
5337                 prepareCursorControllers();
5338                 return;
5339             }
5340 
5341             boolean offsetChanged = offset != mPreviousOffset;
5342             if (offsetChanged || forceUpdatePosition) {
5343                 if (offsetChanged) {
5344                     updateSelection(offset);
5345                     if (fromTouchScreen && mHapticTextHandleEnabled) {
5346                         mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5347                     }
5348                     addPositionToTouchUpFilter(offset);
5349                 }
5350                 final int line = getLineForOffset(layout, offset);
5351                 mPrevLine = line;
5352 
5353                 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
5354                         - getHorizontalOffset() + getCursorOffset();
5355                 mPositionY = layout.getLineBottom(line, /* includeLineSpacing= */ false);
5356 
5357                 // Take TextView's padding and scroll into account.
5358                 mPositionX += mTextView.viewportToContentHorizontalOffset();
5359                 mPositionY += mTextView.viewportToContentVerticalOffset();
5360 
5361                 mPreviousOffset = offset;
5362                 mPositionHasChanged = true;
5363             }
5364         }
5365 
5366         /**
5367          * Return the clamped horizontal position for the cursor.
5368          *
5369          * @param layout Text layout.
5370          * @param offset Character offset for the cursor.
5371          * @return The clamped horizontal position for the cursor.
5372          */
getCursorHorizontalPosition(Layout layout, int offset)5373         int getCursorHorizontalPosition(Layout layout, int offset) {
5374             return (int) (getHorizontal(layout, offset) - 0.5f);
5375         }
5376 
5377         @Override
updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)5378         public void updatePosition(int parentPositionX, int parentPositionY,
5379                 boolean parentPositionChanged, boolean parentScrolled) {
5380             positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false);
5381             if (parentPositionChanged || mPositionHasChanged) {
5382                 if (mIsDragging) {
5383                     // Update touchToWindow offset in case of parent scrolling while dragging
5384                     if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
5385                         mTouchToWindowOffsetX += parentPositionX - mLastParentX;
5386                         mTouchToWindowOffsetY += parentPositionY - mLastParentY;
5387                         mLastParentX = parentPositionX;
5388                         mLastParentY = parentPositionY;
5389                     }
5390 
5391                     onHandleMoved();
5392                 }
5393 
5394                 if (shouldShow()) {
5395                     // Transform to the window coordinates to follow the view tranformation.
5396                     final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
5397                     mTextView.transformFromViewToWindowSpace(pts);
5398                     pts[0] -= mHotspotX + getHorizontalOffset();
5399 
5400                     if (isShowing()) {
5401                         mContainer.update(pts[0], pts[1], -1, -1);
5402                     } else {
5403                         mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
5404                     }
5405                 } else {
5406                     if (isShowing()) {
5407                         dismiss();
5408                     }
5409                 }
5410 
5411                 mPositionHasChanged = false;
5412             }
5413         }
5414 
5415         @Override
onDraw(Canvas c)5416         protected void onDraw(Canvas c) {
5417             final int drawWidth = mDrawable.getIntrinsicWidth();
5418             final int left = getHorizontalOffset();
5419 
5420             mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
5421             mDrawable.draw(c);
5422         }
5423 
getHorizontalOffset()5424         private int getHorizontalOffset() {
5425             final int width = getPreferredWidth();
5426             final int drawWidth = mDrawable.getIntrinsicWidth();
5427             final int left;
5428             switch (mHorizontalGravity) {
5429                 case Gravity.LEFT:
5430                     left = 0;
5431                     break;
5432                 default:
5433                 case Gravity.CENTER:
5434                     left = (width - drawWidth) / 2;
5435                     break;
5436                 case Gravity.RIGHT:
5437                     left = width - drawWidth;
5438                     break;
5439             }
5440             return left;
5441         }
5442 
getCursorOffset()5443         protected int getCursorOffset() {
5444             return 0;
5445         }
5446 
tooLargeTextForMagnifier()5447         private boolean tooLargeTextForMagnifier() {
5448             if (mNewMagnifierEnabled) {
5449                 Layout layout = mTextView.getLayout();
5450                 final int line = getLineForOffset(layout, getCurrentCursorOffset());
5451                 return layout.getLineBottom(line, /* includeLineSpacing= */ false)
5452                         - layout.getLineTop(line) >= mMaxLineHeightForMagnifier;
5453             }
5454             final float magnifierContentHeight = Math.round(
5455                     mMagnifierAnimator.mMagnifier.getHeight()
5456                             / mMagnifierAnimator.mMagnifier.getZoom());
5457             final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics();
5458             final float glyphHeight = fontMetrics.descent - fontMetrics.ascent;
5459             return glyphHeight * mTextViewScaleY > magnifierContentHeight;
5460         }
5461 
5462         /**
5463          * Traverses the hierarchy above the text view, and computes the total scale applied
5464          * to it. If a rotation is encountered, the method returns {@code false}, indicating
5465          * that the magnifier should not be shown anyways. It would be nice to keep these two
5466          * pieces of logic separate (the rotation check and the total scale calculation),
5467          * but for efficiency we can do them in a single go.
5468          * @return whether the text view is rotated
5469          */
checkForTransforms()5470         private boolean checkForTransforms() {
5471             if (mMagnifierAnimator.mMagnifierIsShowing) {
5472                 // Do not check again when the magnifier is currently showing.
5473                 return true;
5474             }
5475 
5476             if (mTextView.getRotation() != 0f || mTextView.getRotationX() != 0f
5477                     || mTextView.getRotationY() != 0f) {
5478                 return false;
5479             }
5480             mTextViewScaleX = mTextView.getScaleX();
5481             mTextViewScaleY = mTextView.getScaleY();
5482 
5483             ViewParent viewParent = mTextView.getParent();
5484             while (viewParent != null) {
5485                 if (viewParent instanceof View) {
5486                     final View view = (View) viewParent;
5487                     if (view.getRotation() != 0f || view.getRotationX() != 0f
5488                             || view.getRotationY() != 0f) {
5489                         return false;
5490                     }
5491                     mTextViewScaleX *= view.getScaleX();
5492                     mTextViewScaleY *= view.getScaleY();
5493                 }
5494                 viewParent = viewParent.getParent();
5495             }
5496             return true;
5497         }
5498 
5499         /**
5500          * Computes the position where the magnifier should be shown, relative to
5501          * {@code mTextView}, and writes them to {@code showPosInView}. Also decides
5502          * whether the magnifier should be shown or dismissed after this touch event.
5503          * @return Whether the magnifier should be shown at the computed coordinates or dismissed.
5504          */
obtainMagnifierShowCoordinates(@onNull final MotionEvent event, final PointF showPosInView)5505         private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event,
5506                 final PointF showPosInView) {
5507 
5508             final int trigger = getMagnifierHandleTrigger();
5509             final int offset;
5510             final int otherHandleOffset;
5511             switch (trigger) {
5512                 case MagnifierHandleTrigger.INSERTION:
5513                     offset = mTextView.getSelectionStart();
5514                     otherHandleOffset = -1;
5515                     break;
5516                 case MagnifierHandleTrigger.SELECTION_START:
5517                     offset = mTextView.getSelectionStart();
5518                     otherHandleOffset = mTextView.getSelectionEnd();
5519                     break;
5520                 case MagnifierHandleTrigger.SELECTION_END:
5521                     offset = mTextView.getSelectionEnd();
5522                     otherHandleOffset = mTextView.getSelectionStart();
5523                     break;
5524                 default:
5525                     offset = -1;
5526                     otherHandleOffset = -1;
5527                     break;
5528             }
5529 
5530             if (offset == -1) {
5531                 return false;
5532             }
5533 
5534             if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
5535                 mCurrentDragInitialTouchRawX = event.getRawX();
5536             } else if (event.getActionMasked() == MotionEvent.ACTION_UP) {
5537                 mCurrentDragInitialTouchRawX = UNSET_X_VALUE;
5538             }
5539 
5540             final Layout layout = mTextView.getLayout();
5541             final int lineNumber = getLineForOffset(layout, offset);
5542             // Compute whether the selection handles are currently on the same line, and,
5543             // in this particular case, whether the selected text is right to left.
5544             final boolean sameLineSelection = otherHandleOffset != -1
5545                     && lineNumber == getLineForOffset(layout, offset);
5546             final boolean rtl = sameLineSelection
5547                     && (offset < otherHandleOffset)
5548                         != (getHorizontal(mTextView.getLayout(), offset)
5549                             < getHorizontal(mTextView.getLayout(), otherHandleOffset));
5550 
5551             // Horizontally move the magnifier smoothly, clamp inside the current line / selection.
5552             final int[] textViewLocationOnScreen = new int[2];
5553             mTextView.getLocationOnScreen(textViewLocationOnScreen);
5554             final float touchXInView = event.getRawX() - textViewLocationOnScreen[0];
5555             float leftBound, rightBound;
5556             if (mNewMagnifierEnabled) {
5557                 leftBound = 0;
5558                 rightBound = mTextView.getWidth();
5559                 if (touchXInView < leftBound || touchXInView > rightBound) {
5560                     // The touch is too far from the current line / selection, so hide the magnifier.
5561                     return false;
5562                 }
5563             } else {
5564                 leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5565                 rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5566                 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END)
5567                         ^ rtl)) {
5568                     leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
5569                 } else {
5570                     leftBound += mTextView.getLayout().getLineLeft(lineNumber);
5571                 }
5572                 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START)
5573                         ^ rtl)) {
5574                     rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset);
5575                 } else {
5576                     rightBound += mTextView.getLayout().getLineRight(lineNumber);
5577                 }
5578                 leftBound *= mTextViewScaleX;
5579                 rightBound *= mTextViewScaleX;
5580                 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth()
5581                         / mMagnifierAnimator.mMagnifier.getZoom());
5582                 if (touchXInView < leftBound - contentWidth / 2
5583                         || touchXInView > rightBound + contentWidth / 2) {
5584                     // The touch is too far from the current line / selection, so hide the magnifier.
5585                     return false;
5586                 }
5587             }
5588 
5589             final float scaledTouchXInView;
5590             if (mTextViewScaleX == 1f) {
5591                 // In the common case, do not use mCurrentDragInitialTouchRawX to compute this
5592                 // coordinate, although the formula on the else branch should be equivalent.
5593                 // Since the formula relies on mCurrentDragInitialTouchRawX being set on
5594                 // MotionEvent.ACTION_DOWN, this makes us more defensive against cases when
5595                 // the sequence of events might not look as expected: for example, a sequence of
5596                 // ACTION_MOVE not preceded by ACTION_DOWN.
5597                 scaledTouchXInView = touchXInView;
5598             } else {
5599                 scaledTouchXInView = (event.getRawX() - mCurrentDragInitialTouchRawX)
5600                         * mTextViewScaleX + mCurrentDragInitialTouchRawX
5601                         - textViewLocationOnScreen[0];
5602             }
5603             showPosInView.x = Math.max(leftBound, Math.min(rightBound, scaledTouchXInView));
5604 
5605             // Vertically snap to middle of current line.
5606             showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber)
5607                     + mTextView.getLayout()
5608                             .getLineBottom(lineNumber, /* includeLineSpacing= */ false)) / 2.0f
5609                     + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY;
5610             return true;
5611         }
5612 
handleOverlapsMagnifier(@onNull final HandleView handle, @NonNull final Rect magnifierRect)5613         private boolean handleOverlapsMagnifier(@NonNull final HandleView handle,
5614                 @NonNull final Rect magnifierRect) {
5615             final PopupWindow window = handle.mContainer;
5616             if (!window.hasDecorView()) {
5617                 return false;
5618             }
5619             final Rect handleRect = new Rect(
5620                     window.getDecorViewLayoutParams().x,
5621                     window.getDecorViewLayoutParams().y,
5622                     window.getDecorViewLayoutParams().x + window.getContentView().getWidth(),
5623                     window.getDecorViewLayoutParams().y + window.getContentView().getHeight());
5624             return Rect.intersects(handleRect, magnifierRect);
5625         }
5626 
getOtherSelectionHandle()5627         private @Nullable HandleView getOtherSelectionHandle() {
5628             final SelectionModifierCursorController controller = getSelectionController();
5629             if (controller == null || !controller.isActive()) {
5630                 return null;
5631             }
5632             return controller.mStartHandle != this
5633                     ? controller.mStartHandle
5634                     : controller.mEndHandle;
5635         }
5636 
updateHandlesVisibility()5637         private void updateHandlesVisibility() {
5638             final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getPosition();
5639             if (magnifierTopLeft == null) {
5640                 return;
5641             }
5642             final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y,
5643                     magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(),
5644                     magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight());
5645             setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect)
5646                     && !mDrawCursorOnMagnifier);
5647             final HandleView otherHandle = getOtherSelectionHandle();
5648             if (otherHandle != null) {
5649                 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect));
5650             }
5651         }
5652 
updateMagnifier(@onNull final MotionEvent event)5653         protected final void updateMagnifier(@NonNull final MotionEvent event) {
5654             if (getMagnifierAnimator() == null) {
5655                 return;
5656             }
5657 
5658             final PointF showPosInView = new PointF();
5659             final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/
5660                     && !tooLargeTextForMagnifier()
5661                     && obtainMagnifierShowCoordinates(event, showPosInView)
5662                     && mTextView.showUIForTouchScreen();
5663             if (shouldShow) {
5664                 // Make the cursor visible and stop blinking.
5665                 mRenderCursorRegardlessTiming = true;
5666                 mTextView.invalidateCursorPath();
5667                 suspendBlink();
5668 
5669                 if (mNewMagnifierEnabled) {
5670                     // Calculates the line bounds as the content source bounds to the magnifier.
5671                     Layout layout = mTextView.getLayout();
5672                     int line = getLineForOffset(layout, getCurrentCursorOffset());
5673                     int lineLeft = (int) layout.getLineLeft(line);
5674                     lineLeft += mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5675                     int lineRight = (int) layout.getLineRight(line);
5676                     lineRight += mTextView.getTotalPaddingLeft() - mTextView.getScrollX();
5677                     mDrawCursorOnMagnifier =
5678                             showPosInView.x < lineLeft - CURSOR_START_FLOAT_DISTANCE_PX
5679                             || showPosInView.x > lineRight + CURSOR_START_FLOAT_DISTANCE_PX;
5680                     mMagnifierAnimator.mMagnifier.setDrawCursor(
5681                             mDrawCursorOnMagnifier, mDrawableForCursor);
5682                     boolean cursorVisible = mCursorVisible;
5683                     // Updates cursor visibility, so that the real cursor and the float cursor on
5684                     // magnifier surface won't appear at the same time.
5685                     mCursorVisible = !mDrawCursorOnMagnifier;
5686                     if (mCursorVisible && !cursorVisible) {
5687                         // When the real cursor is a drawable, hiding/showing it would change its
5688                         // bounds. So, call updateCursorPosition() to correct its position.
5689                         updateCursorPosition();
5690                     }
5691                     final int lineHeight =
5692                             layout.getLineBottom(line, /* includeLineSpacing= */ false)
5693                                     - layout.getLineTop(line);
5694                     float zoom = mInitialZoom;
5695                     if (lineHeight < mMinLineHeightForMagnifier) {
5696                         zoom = zoom * mMinLineHeightForMagnifier / lineHeight;
5697                     }
5698                     mMagnifierAnimator.mMagnifier.updateSourceFactors(lineHeight, zoom);
5699                     mMagnifierAnimator.mMagnifier.show(showPosInView.x, showPosInView.y);
5700                 } else {
5701                     mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
5702                 }
5703                 updateHandlesVisibility();
5704             } else {
5705                 dismissMagnifier();
5706             }
5707         }
5708 
dismissMagnifier()5709         protected final void dismissMagnifier() {
5710             if (mMagnifierAnimator != null) {
5711                 mMagnifierAnimator.dismiss();
5712                 mRenderCursorRegardlessTiming = false;
5713                 mDrawCursorOnMagnifier = false;
5714                 if (!mCursorVisible) {
5715                     mCursorVisible = true;
5716                     mTextView.invalidate();
5717                 }
5718                 resumeBlink();
5719                 setVisible(true);
5720                 final HandleView otherHandle = getOtherSelectionHandle();
5721                 if (otherHandle != null) {
5722                     otherHandle.setVisible(true);
5723                 }
5724             }
5725         }
5726 
5727         @Override
onTouchEvent(MotionEvent ev)5728         public boolean onTouchEvent(MotionEvent ev) {
5729             if (TextView.DEBUG_CURSOR) {
5730                 logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent",
5731                         "%d: %s (%f,%f)",
5732                         ev.getSequenceNumber(),
5733                         MotionEvent.actionToString(ev.getActionMasked()),
5734                         ev.getX(), ev.getY());
5735             }
5736 
5737             updateFloatingToolbarVisibility(ev);
5738 
5739             switch (ev.getActionMasked()) {
5740                 case MotionEvent.ACTION_DOWN: {
5741                     startTouchUpFilter(getCurrentCursorOffset());
5742 
5743                     final PositionListener positionListener = getPositionListener();
5744                     mLastParentX = positionListener.getPositionX();
5745                     mLastParentY = positionListener.getPositionY();
5746                     mLastParentXOnScreen = positionListener.getPositionXOnScreen();
5747                     mLastParentYOnScreen = positionListener.getPositionYOnScreen();
5748 
5749                     final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5750                     final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
5751                     mTouchToWindowOffsetX = xInWindow - mPositionX;
5752                     mTouchToWindowOffsetY = yInWindow - mPositionY;
5753 
5754                     mIsDragging = true;
5755                     mPreviousLineTouched = UNSET_LINE;
5756                     break;
5757                 }
5758 
5759                 case MotionEvent.ACTION_MOVE: {
5760                     final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
5761                     final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
5762 
5763                     // Vertical hysteresis: vertical down movement tends to snap to ideal offset
5764                     final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
5765                     final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
5766                     float newVerticalOffset;
5767                     if (previousVerticalOffset < mIdealVerticalOffset) {
5768                         newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
5769                         newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
5770                     } else {
5771                         newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
5772                         newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
5773                     }
5774                     mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
5775 
5776                     final float newPosX =
5777                             xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
5778                     final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
5779 
5780                     updatePosition(newPosX, newPosY,
5781                             ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5782                     break;
5783                 }
5784 
5785                 case MotionEvent.ACTION_UP:
5786                     filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5787                     // Fall through.
5788                 case MotionEvent.ACTION_CANCEL:
5789                     mIsDragging = false;
5790                     updateDrawable(false /* updateDrawableWhenDragging */);
5791                     break;
5792             }
5793             return true;
5794         }
5795 
isDragging()5796         public boolean isDragging() {
5797             return mIsDragging;
5798         }
5799 
onHandleMoved()5800         void onHandleMoved() {}
5801 
5802         /**
5803          * Called back when the handle view was detached.
5804          */
onDetached()5805         public void onDetached() {
5806             dismissMagnifier();
5807         }
5808 
5809         @Override
onSizeChanged(int w, int h, int oldw, int oldh)5810         protected void onSizeChanged(int w, int h, int oldw, int oldh) {
5811             super.onSizeChanged(w, h, oldw, oldh);
5812             setSystemGestureExclusionRects(Collections.singletonList(new Rect(0, 0, w, h)));
5813         }
5814     }
5815 
5816     private class InsertionHandleView extends HandleView {
5817         // Used to detect taps on the insertion handle, which will affect the insertion action mode
5818         private float mLastDownRawX, mLastDownRawY;
5819         private Runnable mHider;
5820 
5821         // Members for fake-dismiss effect in touch through mode.
5822         // It is to make InsertionHandleView can receive the MOVE/UP events after calling dismiss(),
5823         // which could happen in case of long-press (making selection will dismiss the insertion
5824         // handle).
5825 
5826         // Whether the finger is down and hasn't been up yet.
5827         private boolean mIsTouchDown = false;
5828         // Whether the popup window is in the invisible state and will be dismissed when finger up.
5829         private boolean mPendingDismissOnUp = false;
5830         // The alpha value of the drawable.
5831         private final int mDrawableOpacity;
5832 
5833         // Members for toggling the insertion menu in touch through mode.
5834 
5835         // The coordinate for the touch down event, which is used for transforming the coordinates
5836         // of the events to the text view.
5837         private float mTouchDownX;
5838         private float mTouchDownY;
5839         // The cursor offset when touch down. This is to detect whether the cursor is moved when
5840         // finger move/up.
5841         private int mOffsetDown;
5842         // Whether the cursor offset has been changed on the move/up events.
5843         private boolean mOffsetChanged;
5844         // Whether it is in insertion action mode when finger down.
5845         private boolean mIsInActionMode;
5846         // The timestamp for the last up event, which is used for double tap detection.
5847         private long mLastUpTime;
5848 
5849         // The delta height applied to the insertion handle view.
5850         private final int mDeltaHeight;
5851 
InsertionHandleView(Drawable drawable)5852         InsertionHandleView(Drawable drawable) {
5853             super(drawable, drawable, com.android.internal.R.id.insertion_handle);
5854 
5855             int deltaHeight = 0;
5856             int opacity = 255;
5857             if (mFlagInsertionHandleGesturesEnabled) {
5858                 deltaHeight = AppGlobals.getIntCoreSetting(
5859                         WidgetFlags.KEY_INSERTION_HANDLE_DELTA_HEIGHT,
5860                         WidgetFlags.INSERTION_HANDLE_DELTA_HEIGHT_DEFAULT);
5861                 opacity = AppGlobals.getIntCoreSetting(
5862                         WidgetFlags.KEY_INSERTION_HANDLE_OPACITY,
5863                         WidgetFlags.INSERTION_HANDLE_OPACITY_DEFAULT);
5864                 // Avoid invalid/unsupported values.
5865                 if (deltaHeight < -25 || deltaHeight > 50) {
5866                     deltaHeight = 25;
5867                 }
5868                 if (opacity < 10 || opacity > 100) {
5869                     opacity = 50;
5870                 }
5871                 // Converts the opacity value from range {0..100} to {0..255}.
5872                 opacity = opacity * 255 / 100;
5873             }
5874             mDeltaHeight = deltaHeight;
5875             mDrawableOpacity = opacity;
5876         }
5877 
hideAfterDelay()5878         private void hideAfterDelay() {
5879             if (mHider == null) {
5880                 mHider = new Runnable() {
5881                     public void run() {
5882                         hide();
5883                     }
5884                 };
5885             } else {
5886                 removeHiderCallback();
5887             }
5888             mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
5889         }
5890 
removeHiderCallback()5891         private void removeHiderCallback() {
5892             if (mHider != null) {
5893                 mTextView.removeCallbacks(mHider);
5894             }
5895         }
5896 
5897         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)5898         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5899             return drawable.getIntrinsicWidth() / 2;
5900         }
5901 
5902         @Override
getHorizontalGravity(boolean isRtlRun)5903         protected int getHorizontalGravity(boolean isRtlRun) {
5904             return Gravity.CENTER_HORIZONTAL;
5905         }
5906 
5907         @Override
getCursorOffset()5908         protected int getCursorOffset() {
5909             int offset = super.getCursorOffset();
5910             if (mDrawableForCursor != null) {
5911                 mDrawableForCursor.getPadding(mTempRect);
5912                 offset += (mDrawableForCursor.getIntrinsicWidth()
5913                            - mTempRect.left - mTempRect.right) / 2;
5914             }
5915             return offset;
5916         }
5917 
5918         @Override
getCursorHorizontalPosition(Layout layout, int offset)5919         int getCursorHorizontalPosition(Layout layout, int offset) {
5920             if (mDrawableForCursor != null) {
5921                 final float horizontal = getHorizontal(layout, offset);
5922                 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
5923             }
5924             return super.getCursorHorizontalPosition(layout, offset);
5925         }
5926 
5927         @Override
onMeasure(int widthMeasureSpec, int heightMeasureSpec)5928         protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
5929             if (mFlagInsertionHandleGesturesEnabled) {
5930                 final int height = Math.max(
5931                         getPreferredHeight() + mDeltaHeight, mDrawable.getIntrinsicHeight());
5932                 setMeasuredDimension(getPreferredWidth(), height);
5933                 return;
5934             }
5935             super.onMeasure(widthMeasureSpec, heightMeasureSpec);
5936         }
5937 
5938         @Override
onTouchEvent(MotionEvent ev)5939         public boolean onTouchEvent(MotionEvent ev) {
5940             if (!mTextView.isFromPrimePointer(ev, true)) {
5941                 return true;
5942             }
5943             if (mFlagInsertionHandleGesturesEnabled && mFlagCursorDragFromAnywhereEnabled) {
5944                 // Should only enable touch through when cursor drag is enabled.
5945                 // Otherwise the insertion handle view cannot be moved.
5946                 return touchThrough(ev);
5947             }
5948             final boolean result = super.onTouchEvent(ev);
5949 
5950             switch (ev.getActionMasked()) {
5951                 case MotionEvent.ACTION_DOWN:
5952                     mLastDownRawX = ev.getRawX();
5953                     mLastDownRawY = ev.getRawY();
5954                     updateMagnifier(ev);
5955                     break;
5956 
5957                 case MotionEvent.ACTION_MOVE:
5958                     updateMagnifier(ev);
5959                     break;
5960 
5961                 case MotionEvent.ACTION_UP:
5962                     if (!offsetHasBeenChanged()) {
5963                         ViewConfiguration config = ViewConfiguration.get(mTextView.getContext());
5964                         boolean isWithinTouchSlop = EditorTouchState.isDistanceWithin(
5965                                 mLastDownRawX, mLastDownRawY, ev.getRawX(), ev.getRawY(),
5966                                 config.getScaledTouchSlop());
5967                         if (isWithinTouchSlop) {
5968                             // Tapping on the handle toggles the insertion action mode.
5969                             toggleInsertionActionMode();
5970                         }
5971                     } else {
5972                         if (mTextActionMode != null) {
5973                             mTextActionMode.invalidateContentRect();
5974                         }
5975                     }
5976                     // Fall through.
5977                 case MotionEvent.ACTION_CANCEL:
5978                     hideAfterDelay();
5979                     dismissMagnifier();
5980                     break;
5981 
5982                 default:
5983                     break;
5984             }
5985 
5986             return result;
5987         }
5988 
5989         // Handles the touch events in touch through mode.
touchThrough(MotionEvent ev)5990         private boolean touchThrough(MotionEvent ev) {
5991             final int actionType = ev.getActionMasked();
5992             switch (actionType) {
5993                 case MotionEvent.ACTION_DOWN:
5994                     mIsTouchDown = true;
5995                     mOffsetChanged = false;
5996                     mOffsetDown = mTextView.getSelectionStart();
5997                     mTouchDownX = ev.getX();
5998                     mTouchDownY = ev.getY();
5999                     mIsInActionMode = mTextActionMode != null;
6000                     if (ev.getEventTime() - mLastUpTime < ViewConfiguration.getDoubleTapTimeout()) {
6001                         stopTextActionMode();  // Avoid crash when double tap and drag backwards.
6002                     }
6003                     mTouchState.setIsOnHandle(true);
6004                     break;
6005                 case MotionEvent.ACTION_UP:
6006                     mLastUpTime = ev.getEventTime();
6007                     break;
6008             }
6009             // Performs the touch through by forward the events to the text view.
6010             boolean ret = mTextView.onTouchEvent(transformEventForTouchThrough(ev));
6011 
6012             if (actionType == MotionEvent.ACTION_UP || actionType == MotionEvent.ACTION_CANCEL) {
6013                 mIsTouchDown = false;
6014                 if (mPendingDismissOnUp) {
6015                     dismiss();
6016                 }
6017                 mTouchState.setIsOnHandle(false);
6018             }
6019 
6020             // Checks for cursor offset change.
6021             if (!mOffsetChanged) {
6022                 int start = mTextView.getSelectionStart();
6023                 int end = mTextView.getSelectionEnd();
6024                 if (start != end || mOffsetDown != start) {
6025                     mOffsetChanged = true;
6026                 }
6027             }
6028 
6029             // Toggling the insertion action mode on finger up.
6030             if (!mOffsetChanged && actionType == MotionEvent.ACTION_UP) {
6031                 if (mIsInActionMode) {
6032                     stopTextActionMode();
6033                 } else {
6034                     startInsertionActionMode();
6035                 }
6036             }
6037             return ret;
6038         }
6039 
transformEventForTouchThrough(MotionEvent ev)6040         private MotionEvent transformEventForTouchThrough(MotionEvent ev) {
6041             final Layout layout = mTextView.getLayout();
6042             final int line = getLineForOffset(layout, getCurrentCursorOffset());
6043             final int textHeight = layout.getLineBottom(line, /* includeLineSpacing= */ false)
6044                     - layout.getLineTop(line);
6045             // Transforms the touch events to screen coordinates.
6046             // And also shift up to make the hit point is on the text.
6047             // Note:
6048             //  - The revised X should reflect the distance to the horizontal center of touch down.
6049             //  - The revised Y should be at the top of the text.
6050             Matrix m = new Matrix();
6051             m.setTranslate(ev.getRawX() - ev.getX() + (getMeasuredWidth() >> 1) - mTouchDownX,
6052                     ev.getRawY() - ev.getY() - (textHeight >> 1) - mTouchDownY);
6053             ev.transform(m);
6054             // Transforms the touch events to text view coordinates.
6055             mTextView.toLocalMotionEvent(ev);
6056             if (TextView.DEBUG_CURSOR) {
6057                 logCursor("InsertionHandleView#transformEventForTouchThrough",
6058                         "Touch through: %d, (%f, %f)",
6059                         ev.getAction(), ev.getX(), ev.getY());
6060             }
6061             return ev;
6062         }
6063 
6064         @Override
isShowing()6065         public boolean isShowing() {
6066             if (mPendingDismissOnUp) {
6067                 return false;
6068             }
6069             return super.isShowing();
6070         }
6071 
6072         @Override
show()6073         public void show() {
6074             super.show();
6075             mPendingDismissOnUp = false;
6076             mDrawable.setAlpha(mDrawableOpacity);
6077         }
6078 
6079         @Override
dismiss()6080         public void dismiss() {
6081             if (mIsTouchDown) {
6082                 if (TextView.DEBUG_CURSOR) {
6083                     logCursor("InsertionHandleView#dismiss",
6084                             "Suppressed the real dismiss, only become invisible");
6085                 }
6086                 mPendingDismissOnUp = true;
6087                 mDrawable.setAlpha(0);
6088             } else {
6089                 super.dismiss();
6090                 mPendingDismissOnUp = false;
6091             }
6092         }
6093 
6094         @Override
updateDrawable(final boolean updateDrawableWhenDragging)6095         protected void updateDrawable(final boolean updateDrawableWhenDragging) {
6096             super.updateDrawable(updateDrawableWhenDragging);
6097             mDrawable.setAlpha(mDrawableOpacity);
6098         }
6099 
6100         @Override
getCurrentCursorOffset()6101         public int getCurrentCursorOffset() {
6102             return mTextView.getSelectionStart();
6103         }
6104 
6105         @Override
updateSelection(int offset)6106         public void updateSelection(int offset) {
6107             Selection.setSelection((Spannable) mTextView.getText(), offset);
6108         }
6109 
6110         @Override
updatePosition(float x, float y, boolean fromTouchScreen)6111         protected void updatePosition(float x, float y, boolean fromTouchScreen) {
6112             Layout layout = mTextView.getLayout();
6113             int offset;
6114             if (layout != null) {
6115                 if (mPreviousLineTouched == UNSET_LINE) {
6116                     mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
6117                 }
6118                 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
6119                 offset = getOffsetAtCoordinate(layout, currLine, x);
6120                 mPreviousLineTouched = currLine;
6121             } else {
6122                 offset = -1;
6123             }
6124             if (TextView.DEBUG_CURSOR) {
6125                 logCursor("InsertionHandleView: updatePosition", "x=%f, y=%f, offset=%d, line=%d",
6126                         x, y, offset, mPreviousLineTouched);
6127             }
6128             positionAtCursorOffset(offset, false, fromTouchScreen);
6129             if (mTextActionMode != null) {
6130                 invalidateActionMode();
6131             }
6132         }
6133 
6134         @Override
onHandleMoved()6135         void onHandleMoved() {
6136             super.onHandleMoved();
6137             removeHiderCallback();
6138         }
6139 
6140         @Override
onDetached()6141         public void onDetached() {
6142             super.onDetached();
6143             removeHiderCallback();
6144         }
6145 
6146         @Override
6147         @MagnifierHandleTrigger
getMagnifierHandleTrigger()6148         protected int getMagnifierHandleTrigger() {
6149             return MagnifierHandleTrigger.INSERTION;
6150         }
6151     }
6152 
6153     @Retention(RetentionPolicy.SOURCE)
6154     @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
6155             HANDLE_TYPE_SELECTION_START,
6156             HANDLE_TYPE_SELECTION_END
6157     })
6158     public @interface HandleType {}
6159     public static final int HANDLE_TYPE_SELECTION_START = 0;
6160     public static final int HANDLE_TYPE_SELECTION_END = 1;
6161 
6162     /** For selection handles */
6163     @VisibleForTesting
6164     public final class SelectionHandleView extends HandleView {
6165         // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
6166         // end (HANDLE_TYPE_SELECTION_END).
6167         @HandleType
6168         private final int mHandleType;
6169         // Indicates whether the cursor is making adjustments within a word.
6170         private boolean mInWord = false;
6171         // Difference between touch position and word boundary position.
6172         private float mTouchWordDelta;
6173         // X value of the previous updatePosition call.
6174         private float mPrevX;
6175         // Indicates if the handle has moved a boundary between LTR and RTL text.
6176         private boolean mLanguageDirectionChanged = false;
6177         // Distance from edge of horizontally scrolling text view
6178         // to use to switch to character mode.
6179         private final float mTextViewEdgeSlop;
6180         // Used to save text view location.
6181         private final int[] mTextViewLocation = new int[2];
6182 
SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id, @HandleType int handleType)6183         public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
6184                 @HandleType int handleType) {
6185             super(drawableLtr, drawableRtl, id);
6186             mHandleType = handleType;
6187             ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
6188             mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
6189         }
6190 
isStartHandle()6191         private boolean isStartHandle() {
6192             return mHandleType == HANDLE_TYPE_SELECTION_START;
6193         }
6194 
6195         @Override
getHotspotX(Drawable drawable, boolean isRtlRun)6196         protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
6197             if (isRtlRun == isStartHandle()) {
6198                 return drawable.getIntrinsicWidth() / 4;
6199             } else {
6200                 return (drawable.getIntrinsicWidth() * 3) / 4;
6201             }
6202         }
6203 
6204         @Override
getHorizontalGravity(boolean isRtlRun)6205         protected int getHorizontalGravity(boolean isRtlRun) {
6206             return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
6207         }
6208 
6209         @Override
getCurrentCursorOffset()6210         public int getCurrentCursorOffset() {
6211             return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
6212         }
6213 
6214         @Override
updateSelection(int offset)6215         protected void updateSelection(int offset) {
6216             if (isStartHandle()) {
6217                 Selection.setSelection((Spannable) mTextView.getText(), offset,
6218                         mTextView.getSelectionEnd());
6219             } else {
6220                 Selection.setSelection((Spannable) mTextView.getText(),
6221                         mTextView.getSelectionStart(), offset);
6222             }
6223             updateDrawable(false /* updateDrawableWhenDragging */);
6224             if (mTextActionMode != null) {
6225                 invalidateActionMode();
6226             }
6227         }
6228 
6229         @Override
updatePosition(float x, float y, boolean fromTouchScreen)6230         protected void updatePosition(float x, float y, boolean fromTouchScreen) {
6231             final Layout layout = mTextView.getLayout();
6232             if (layout == null) {
6233                 // HandleView will deal appropriately in positionAtCursorOffset when
6234                 // layout is null.
6235                 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
6236                         fromTouchScreen);
6237                 return;
6238             }
6239 
6240             if (mPreviousLineTouched == UNSET_LINE) {
6241                 mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
6242             }
6243 
6244             boolean positionCursor = false;
6245             final int anotherHandleOffset =
6246                     isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
6247             int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
6248             int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
6249 
6250             if (isStartHandle() && initialOffset >= anotherHandleOffset
6251                     || !isStartHandle() && initialOffset <= anotherHandleOffset) {
6252                 // Handles have crossed, bound it to the first selected line and
6253                 // adjust by word / char as normal.
6254                 currLine = getLineForOffset(layout, anotherHandleOffset);
6255                 initialOffset = getOffsetAtCoordinate(layout, currLine, x);
6256             }
6257 
6258             int offset = initialOffset;
6259             final int wordEnd = getWordEnd(offset);
6260             final int wordStart = getWordStart(offset);
6261 
6262             if (mPrevX == UNSET_X_VALUE) {
6263                 mPrevX = x;
6264             }
6265 
6266             final int currentOffset = getCurrentCursorOffset();
6267             final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
6268             final boolean atRtl = isAtRtlRun(layout, offset);
6269             final boolean isLvlBoundary = layout.isLevelBoundary(
6270                     mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR));
6271 
6272             // We can't determine if the user is expanding or shrinking the selection if they're
6273             // on a bi-di boundary, so until they've moved past the boundary we'll just place
6274             // the cursor at the current position.
6275             if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
6276                 // We're on a boundary or this is the first direction change -- just update
6277                 // to the current position.
6278                 mLanguageDirectionChanged = true;
6279                 mTouchWordDelta = 0.0f;
6280                 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
6281                 return;
6282             }
6283 
6284             if (mLanguageDirectionChanged) {
6285                 // We've just moved past the boundary so update the position. After this we can
6286                 // figure out if the user is expanding or shrinking to go by word or character.
6287                 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
6288                 mTouchWordDelta = 0.0f;
6289                 mLanguageDirectionChanged = false;
6290                 return;
6291             }
6292 
6293             boolean isExpanding;
6294             final float xDiff = x - mPrevX;
6295             if (isStartHandle()) {
6296                 isExpanding = currLine < mPreviousLineTouched;
6297             } else {
6298                 isExpanding = currLine > mPreviousLineTouched;
6299             }
6300             if (atRtl == isStartHandle()) {
6301                 isExpanding |= xDiff > 0;
6302             } else {
6303                 isExpanding |= xDiff < 0;
6304             }
6305 
6306             if (mTextView.getHorizontallyScrolling()) {
6307                 if (positionNearEdgeOfScrollingView(x, atRtl)
6308                         && ((isStartHandle() && mTextView.getScrollX() != 0)
6309                                 || (!isStartHandle()
6310                                         && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
6311                         && ((isExpanding && ((isStartHandle() && offset < currentOffset)
6312                                 || (!isStartHandle() && offset > currentOffset)))
6313                                         || !isExpanding)) {
6314                     // If we're expanding ensure that the offset is actually expanding compared to
6315                     // the current offset, if the handle snapped to the word, the finger position
6316                     // may be out of sync and we don't want the selection to jump back.
6317                     mTouchWordDelta = 0.0f;
6318                     final int nextOffset = (atRtl == isStartHandle())
6319                             ? layout.getOffsetToRightOf(mPreviousOffset)
6320                             : layout.getOffsetToLeftOf(mPreviousOffset);
6321                     positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
6322                     return;
6323                 }
6324             }
6325 
6326             if (isExpanding) {
6327                 // User is increasing the selection.
6328                 int wordBoundary = isStartHandle() ? wordStart : wordEnd;
6329                 final boolean snapToWord = (!mInWord
6330                         || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
6331                                 && atRtl == isAtRtlRun(layout, wordBoundary);
6332                 if (snapToWord) {
6333                     // Sometimes words can be broken across lines (Chinese, hyphenation).
6334                     // We still snap to the word boundary but we only use the letters on the
6335                     // current line to determine if the user is far enough into the word to snap.
6336                     if (getLineForOffset(layout, wordBoundary) != currLine) {
6337                         wordBoundary = isStartHandle()
6338                                 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
6339                     }
6340                     final int offsetThresholdToSnap = isStartHandle()
6341                             ? wordEnd - ((wordEnd - wordBoundary) / 2)
6342                             : wordStart + ((wordBoundary - wordStart) / 2);
6343                     if (isStartHandle()
6344                             && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
6345                         // User is far enough into the word or on a different line so we expand by
6346                         // word.
6347                         offset = wordStart;
6348                     } else if (!isStartHandle()
6349                             && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
6350                         // User is far enough into the word or on a different line so we expand by
6351                         // word.
6352                         offset = wordEnd;
6353                     } else {
6354                         offset = mPreviousOffset;
6355                     }
6356                 }
6357                 if ((isStartHandle() && offset < initialOffset)
6358                         || (!isStartHandle() && offset > initialOffset)) {
6359                     final float adjustedX = getHorizontal(layout, offset);
6360                     mTouchWordDelta =
6361                             mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
6362                 } else {
6363                     mTouchWordDelta = 0.0f;
6364                 }
6365                 positionCursor = true;
6366             } else {
6367                 final int adjustedOffset =
6368                         getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
6369                 final boolean shrinking = isStartHandle()
6370                         ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
6371                         : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
6372                 if (shrinking) {
6373                     // User is shrinking the selection.
6374                     if (currLine != mPrevLine) {
6375                         // We're on a different line, so we'll snap to word boundaries.
6376                         offset = isStartHandle() ? wordStart : wordEnd;
6377                         if ((isStartHandle() && offset < initialOffset)
6378                                 || (!isStartHandle() && offset > initialOffset)) {
6379                             final float adjustedX = getHorizontal(layout, offset);
6380                             mTouchWordDelta =
6381                                     mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
6382                         } else {
6383                             mTouchWordDelta = 0.0f;
6384                         }
6385                     } else {
6386                         offset = adjustedOffset;
6387                     }
6388                     positionCursor = true;
6389                 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
6390                         || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
6391                     // Handle has jumped to the word boundary, and the user is moving
6392                     // their finger towards the handle, the delta should be updated.
6393                     mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
6394                             - getHorizontal(layout, mPreviousOffset);
6395                 }
6396             }
6397 
6398             if (positionCursor) {
6399                 mPreviousLineTouched = currLine;
6400                 positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
6401             }
6402             mPrevX = x;
6403         }
6404 
6405         @Override
6406         protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
6407                 boolean fromTouchScreen) {
6408             super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
6409             mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
6410         }
6411 
6412         @Override
6413         public boolean onTouchEvent(MotionEvent event) {
6414             if (!mTextView.isFromPrimePointer(event, true)) {
6415                 return true;
6416             }
6417             boolean superResult = super.onTouchEvent(event);
6418 
6419             switch (event.getActionMasked()) {
6420                 case MotionEvent.ACTION_DOWN:
6421                     // Reset the touch word offset and x value when the user
6422                     // re-engages the handle.
6423                     mTouchWordDelta = 0.0f;
6424                     mPrevX = UNSET_X_VALUE;
6425                     updateMagnifier(event);
6426                     break;
6427 
6428                 case MotionEvent.ACTION_MOVE:
6429                     updateMagnifier(event);
6430                     break;
6431 
6432                 case MotionEvent.ACTION_UP:
6433                 case MotionEvent.ACTION_CANCEL:
6434                     dismissMagnifier();
6435                     break;
6436             }
6437 
6438             return superResult;
6439         }
6440 
6441         private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
6442             final int anotherHandleOffset =
6443                     isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
6444             if ((isStartHandle() && offset >= anotherHandleOffset)
6445                     || (!isStartHandle() && offset <= anotherHandleOffset)) {
6446                 mTouchWordDelta = 0.0f;
6447                 final Layout layout = mTextView.getLayout();
6448                 if (layout != null && offset != anotherHandleOffset) {
6449                     final float horiz = getHorizontal(layout, offset);
6450                     final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
6451                             !isStartHandle());
6452                     final float currentHoriz = getHorizontal(layout, mPreviousOffset);
6453                     if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
6454                             || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
6455                         // This handle passes another one as it crossed a direction boundary.
6456                         // Don't minimize the selection, but keep the handle at the run boundary.
6457                         final int currentOffset = getCurrentCursorOffset();
6458                         final int offsetToGetRunRange = isStartHandle()
6459                                 ? currentOffset : Math.max(currentOffset - 1, 0);
6460                         final long range = layout.getRunRange(mTextView.originalToTransformed(
6461                                 offsetToGetRunRange, OffsetMapping.MAP_STRATEGY_CURSOR));
6462                         if (isStartHandle()) {
6463                             offset = TextUtils.unpackRangeStartFromLong(range);
6464                         } else {
6465                             offset = TextUtils.unpackRangeEndFromLong(range);
6466                         }
6467                         offset = mTextView.transformedToOriginal(offset,
6468                                 OffsetMapping.MAP_STRATEGY_CURSOR);
6469                         positionAtCursorOffset(offset, false, fromTouchScreen);
6470                         return;
6471                     }
6472                 }
6473                 // Handles can not cross and selection is at least one character.
6474                 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
6475             }
6476             positionAtCursorOffset(offset, false, fromTouchScreen);
6477         }
6478 
6479         private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
6480             mTextView.getLocationOnScreen(mTextViewLocation);
6481             boolean nearEdge;
6482             if (atRtl == isStartHandle()) {
6483                 int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
6484                         - mTextView.getPaddingRight();
6485                 nearEdge = x > rightEdge - mTextViewEdgeSlop;
6486             } else {
6487                 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
6488                 nearEdge = x < leftEdge + mTextViewEdgeSlop;
6489             }
6490             return nearEdge;
6491         }
6492 
6493         @Override
6494         protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
6495             final int transformedOffset =
6496                     mTextView.transformedToOriginal(offset, OffsetMapping.MAP_STRATEGY_CHARACTER);
6497             final int offsetToCheck = isStartHandle() ? transformedOffset
6498                     : Math.max(transformedOffset - 1, 0);
6499             return layout.isRtlCharAt(offsetToCheck);
6500         }
6501 
6502         @Override
6503         public float getHorizontal(@NonNull Layout layout, int offset) {
6504             return getHorizontal(layout, offset, isStartHandle());
6505         }
6506 
6507         private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
6508             final int offsetTransformed = mTextView.originalToTransformed(offset,
6509                     OffsetMapping.MAP_STRATEGY_CURSOR);
6510             final int line = layout.getLineForOffset(offsetTransformed);
6511             final int offsetToCheck =
6512                     startHandle ? offsetTransformed : Math.max(offsetTransformed - 1, 0);
6513             final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
6514             final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
6515             if  (isRtlChar != isRtlParagraph) {
6516                 return layout.getSecondaryHorizontal(offsetTransformed);
6517             }
6518             return layout.getPrimaryHorizontal(offsetTransformed);
6519         }
6520 
6521         @Override
6522         protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
6523             final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
6524             final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
6525             if (!layout.isLevelBoundary(primaryOffset)) {
6526                 return mTextView.transformedToOriginal(primaryOffset,
6527                         OffsetMapping.MAP_STRATEGY_CURSOR);
6528             }
6529             final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
6530             final int currentOffset = mTextView.originalToTransformed(getCurrentCursorOffset(),
6531                     OffsetMapping.MAP_STRATEGY_CURSOR);
6532             final int primaryDiff = Math.abs(primaryOffset - currentOffset);
6533             final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
6534             final int offset;
6535             if (primaryDiff < secondaryDiff) {
6536                 offset = primaryOffset;
6537             } else if (primaryDiff > secondaryDiff) {
6538                 offset = secondaryOffset;
6539             } else {
6540                 final int offsetToCheck = isStartHandle()
6541                         ? currentOffset : Math.max(currentOffset - 1, 0);
6542                 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
6543                 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
6544                 offset = (isRtlChar == isRtlParagraph) ? primaryOffset : secondaryOffset;
6545             }
6546             return mTextView.transformedToOriginal(offset, OffsetMapping.MAP_STRATEGY_CURSOR);
6547         }
6548 
6549         @MagnifierHandleTrigger
6550         protected int getMagnifierHandleTrigger() {
6551             return isStartHandle()
6552                     ? MagnifierHandleTrigger.SELECTION_START
6553                     : MagnifierHandleTrigger.SELECTION_END;
6554         }
6555     }
6556 
6557     @VisibleForTesting
6558     public void setLineChangeSlopMinMaxForTesting(final int min, final int max) {
6559         mLineChangeSlopMin = min;
6560         mLineChangeSlopMax = max;
6561     }
6562 
6563     @VisibleForTesting
6564     public int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
6565         final int trueLine = mTextView.getLineAtCoordinate(y);
6566         if (layout == null || prevLine >= layout.getLineCount()
6567                 || layout.getLineCount() <= 0 || prevLine < 0) {
6568             // Invalid parameters, just return whatever line is at y.
6569             return trueLine;
6570         }
6571 
6572         if (Math.abs(trueLine - prevLine) >= 2) {
6573             // Only stick to lines if we're within a line of the previous selection.
6574             return trueLine;
6575         }
6576 
6577         final int lineHeight = mTextView.getLineHeight();
6578         int slop = (int)(mLineSlopRatio * lineHeight);
6579         slop = Math.max(mLineChangeSlopMin,
6580                 Math.min(mLineChangeSlopMax, lineHeight + slop)) - lineHeight;
6581         slop = Math.max(0, slop);
6582 
6583         final float verticalOffset = mTextView.viewportToContentVerticalOffset();
6584         if (trueLine > prevLine && y >= layout.getLineBottom(prevLine) + slop + verticalOffset) {
6585             return trueLine;
6586         }
6587         if (trueLine < prevLine && y <= layout.getLineTop(prevLine) - slop + verticalOffset) {
6588             return trueLine;
6589         }
6590         return prevLine;
6591     }
6592 
6593     /**
6594      * A CursorController instance can be used to control a cursor in the text.
6595      */
6596     private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
6597         /**
6598          * Makes the cursor controller visible on screen.
6599          * See also {@link #hide()}.
6600          */
6601         public void show();
6602 
6603         /**
6604          * Hide the cursor controller from screen.
6605          * See also {@link #show()}.
6606          */
6607         public void hide();
6608 
6609         /**
6610          * Called when the view is detached from window. Perform house keeping task, such as
6611          * stopping Runnable thread that would otherwise keep a reference on the context, thus
6612          * preventing the activity from being recycled.
6613          */
6614         public void onDetached();
6615 
6616         public boolean isCursorBeingModified();
6617 
6618         public boolean isActive();
6619     }
6620 
loadCursorDrawable()6621     void loadCursorDrawable() {
6622         if (mDrawableForCursor == null) {
6623             mDrawableForCursor = mTextView.getTextCursorDrawable();
6624         }
6625     }
6626 
6627     /** Controller for the insertion cursor. */
6628     @VisibleForTesting
6629     public class InsertionPointCursorController implements CursorController {
6630         private InsertionHandleView mHandle;
6631         // Tracks whether the cursor is currently being dragged.
6632         private boolean mIsDraggingCursor;
6633         // During a drag, tracks whether the user's finger has adjusted to be over the handle rather
6634         // than the cursor bar.
6635         private boolean mIsTouchSnappedToHandleDuringDrag;
6636         // During a drag, tracks the line of text where the cursor was last positioned.
6637         private int mPrevLineDuringDrag;
6638 
onTouchEvent(MotionEvent event)6639         public void onTouchEvent(MotionEvent event) {
6640             if (hasSelectionController() && getSelectionController().isCursorBeingModified()) {
6641                 return;
6642             }
6643             switch (event.getActionMasked()) {
6644                 case MotionEvent.ACTION_MOVE:
6645                     if (event.isFromSource(InputDevice.SOURCE_MOUSE)
6646                             || (mTextView.isAutoHandwritingEnabled() && isFromStylus(event))) {
6647                         break;
6648                     }
6649                     if (mIsDraggingCursor) {
6650                         performCursorDrag(event);
6651                     } else if (mFlagCursorDragFromAnywhereEnabled
6652                             && mTextView.getLayout() != null
6653                             && mTextView.isFocused()
6654                             && mTouchState.isMovedEnoughForDrag()
6655                             && (mTouchState.getInitialDragDirectionXYRatio()
6656                             > mCursorDragDirectionMinXYRatio || mTouchState.isOnHandle())) {
6657                         startCursorDrag(event);
6658                     }
6659                     break;
6660                 case MotionEvent.ACTION_UP:
6661                 case MotionEvent.ACTION_CANCEL:
6662                     if (mIsDraggingCursor) {
6663                         endCursorDrag(event);
6664                     }
6665                     break;
6666             }
6667         }
6668 
isFromStylus(MotionEvent motionEvent)6669         private boolean isFromStylus(MotionEvent motionEvent) {
6670             final int pointerIndex = motionEvent.getActionIndex();
6671             return motionEvent.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_STYLUS;
6672         }
6673 
positionCursorDuringDrag(MotionEvent event)6674         private void positionCursorDuringDrag(MotionEvent event) {
6675             mPrevLineDuringDrag = getLineDuringDrag(event);
6676             int offset = mTextView.getOffsetAtCoordinate(mPrevLineDuringDrag, event.getX());
6677             int oldSelectionStart = mTextView.getSelectionStart();
6678             int oldSelectionEnd = mTextView.getSelectionEnd();
6679             if (offset == oldSelectionStart && offset == oldSelectionEnd) {
6680                 return;
6681             }
6682             Selection.setSelection((Spannable) mTextView.getText(), offset);
6683             updateCursorPosition();
6684             if (mHapticTextHandleEnabled) {
6685                 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
6686             }
6687         }
6688 
6689         /**
6690          * Returns the line where the cursor should be positioned during a cursor drag. Rather than
6691          * simply returning the line directly at the touch position, this function has the following
6692          * additional logic:
6693          * 1) Apply some slop to avoid switching lines if the touch moves just slightly off the
6694          * current line.
6695          * 2) Allow the user's finger to slide down and "snap" to the handle to provide better
6696          * visibility of the cursor and text.
6697          */
getLineDuringDrag(MotionEvent event)6698         private int getLineDuringDrag(MotionEvent event) {
6699             final Layout layout = mTextView.getLayout();
6700             if (mPrevLineDuringDrag == UNSET_LINE) {
6701                 return getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, event.getY());
6702             }
6703             // In case of touch through on handle (when isOnHandle() returns true), event.getY()
6704             // returns the midpoint of the cursor vertical bar, while event.getRawY() returns the
6705             // finger location on the screen. See {@link InsertionHandleView#touchThrough}.
6706             final float fingerY = mTouchState.isOnHandle()
6707                     ? event.getRawY() - mTextView.getLocationOnScreen()[1]
6708                     : event.getY();
6709             final float cursorY = fingerY - getHandle().getIdealFingerToCursorOffset();
6710             int line = getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, cursorY);
6711             if (mIsTouchSnappedToHandleDuringDrag) {
6712                 // Just returns the line hit by cursor Y when already snapped.
6713                 return line;
6714             }
6715             if (line < mPrevLineDuringDrag) {
6716                 // The cursor Y aims too high & not yet snapped, check the finger Y.
6717                 // If finger Y is moving downwards, don't jump to lower line (until snap).
6718                 // If finger Y is moving upwards, can jump to upper line.
6719                 return Math.min(mPrevLineDuringDrag,
6720                         getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, fingerY));
6721             }
6722             // The cursor Y aims not too high, so snap!
6723             mIsTouchSnappedToHandleDuringDrag = true;
6724             if (TextView.DEBUG_CURSOR) {
6725                 logCursor("InsertionPointCursorController",
6726                         "snapped touch to handle: fingerY=%d, cursorY=%d, mLastLine=%d, line=%d",
6727                         (int) fingerY, (int) cursorY, mPrevLineDuringDrag, line);
6728             }
6729             return line;
6730         }
6731 
startCursorDrag(MotionEvent event)6732         private void startCursorDrag(MotionEvent event) {
6733             if (TextView.DEBUG_CURSOR) {
6734                 logCursor("InsertionPointCursorController", "start cursor drag");
6735             }
6736             mIsDraggingCursor = true;
6737             mIsTouchSnappedToHandleDuringDrag = false;
6738             mPrevLineDuringDrag = UNSET_LINE;
6739             // We don't want the parent scroll/long-press handlers to take over while dragging.
6740             mTextView.getParent().requestDisallowInterceptTouchEvent(true);
6741             mTextView.cancelLongPress();
6742             // Update the cursor position.
6743             positionCursorDuringDrag(event);
6744             // Show the cursor handle and magnifier.
6745             show();
6746             getHandle().removeHiderCallback();
6747             getHandle().updateMagnifier(event);
6748             // TODO(b/146555651): Figure out if suspendBlink() should be called here.
6749         }
6750 
performCursorDrag(MotionEvent event)6751         private void performCursorDrag(MotionEvent event) {
6752             positionCursorDuringDrag(event);
6753             getHandle().updateMagnifier(event);
6754         }
6755 
endCursorDrag(MotionEvent event)6756         private void endCursorDrag(MotionEvent event) {
6757             if (TextView.DEBUG_CURSOR) {
6758                 logCursor("InsertionPointCursorController", "end cursor drag");
6759             }
6760             mIsDraggingCursor = false;
6761             mIsTouchSnappedToHandleDuringDrag = false;
6762             mPrevLineDuringDrag = UNSET_LINE;
6763             // Hide the magnifier and set the handle to be hidden after a delay.
6764             getHandle().dismissMagnifier();
6765             getHandle().hideAfterDelay();
6766             // We're no longer dragging, so let the parent receive events.
6767             mTextView.getParent().requestDisallowInterceptTouchEvent(false);
6768         }
6769 
show()6770         public void show() {
6771             getHandle().show();
6772 
6773             final long durationSinceCutOrCopy =
6774                     SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
6775 
6776             if (mInsertionActionModeRunnable != null) {
6777                 if (mIsDraggingCursor
6778                         || mTouchState.isMultiTap()
6779                         || isCursorInsideEasyCorrectionSpan()) {
6780                     // Cancel the runnable for showing the floating toolbar.
6781                     mTextView.removeCallbacks(mInsertionActionModeRunnable);
6782                 }
6783             }
6784 
6785             // If the user recently performed a Cut or Copy action, we want to show the floating
6786             // toolbar even for a single tap.
6787             if (!mIsDraggingCursor
6788                     && !mTouchState.isMultiTap()
6789                     && !isCursorInsideEasyCorrectionSpan()
6790                     && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) {
6791                 if (mTextActionMode == null) {
6792                     if (mInsertionActionModeRunnable == null) {
6793                         mInsertionActionModeRunnable = new Runnable() {
6794                             @Override
6795                             public void run() {
6796                                 startInsertionActionMode();
6797                             }
6798                         };
6799                     }
6800                     mTextView.postDelayed(
6801                             mInsertionActionModeRunnable,
6802                             ViewConfiguration.getDoubleTapTimeout() + 1);
6803                 }
6804             }
6805 
6806             if (!mIsDraggingCursor) {
6807                 getHandle().hideAfterDelay();
6808             }
6809 
6810             if (mSelectionModifierCursorController != null) {
6811                 mSelectionModifierCursorController.hide();
6812             }
6813         }
6814 
hide()6815         public void hide() {
6816             if (mHandle != null) {
6817                 mHandle.hide();
6818             }
6819         }
6820 
onTouchModeChanged(boolean isInTouchMode)6821         public void onTouchModeChanged(boolean isInTouchMode) {
6822             if (!isInTouchMode) {
6823                 hide();
6824             }
6825         }
6826 
getHandle()6827         public InsertionHandleView getHandle() {
6828             if (mHandle == null) {
6829                 loadHandleDrawables(false /* overwrite */);
6830                 mHandle = new InsertionHandleView(mSelectHandleCenter);
6831             }
6832             return mHandle;
6833         }
6834 
reloadHandleDrawable()6835         private void reloadHandleDrawable() {
6836             if (mHandle == null) {
6837                 // No need to reload, the potentially new drawable will
6838                 // be used when the handle is created.
6839                 return;
6840             }
6841             mHandle.setDrawables(mSelectHandleCenter, mSelectHandleCenter);
6842         }
6843 
6844         @Override
onDetached()6845         public void onDetached() {
6846             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6847             observer.removeOnTouchModeChangeListener(this);
6848 
6849             if (mHandle != null) mHandle.onDetached();
6850         }
6851 
6852         @Override
isCursorBeingModified()6853         public boolean isCursorBeingModified() {
6854             return mIsDraggingCursor || (mHandle != null && mHandle.isDragging());
6855         }
6856 
6857         @Override
isActive()6858         public boolean isActive() {
6859             return mHandle != null && mHandle.isShowing();
6860         }
6861 
invalidateHandle()6862         public void invalidateHandle() {
6863             if (mHandle != null) {
6864                 mHandle.invalidate();
6865             }
6866         }
6867     }
6868 
6869     /** Controller for selection. */
6870     @VisibleForTesting
6871     public class SelectionModifierCursorController implements CursorController {
6872         // The cursor controller handles, lazily created when shown.
6873         private SelectionHandleView mStartHandle;
6874         private SelectionHandleView mEndHandle;
6875         // The offsets of that last touch down event. Remembered to start selection there.
6876         private int mMinTouchOffset, mMaxTouchOffset;
6877 
6878         private boolean mGestureStayedInTapRegion;
6879 
6880         // Where the user first starts the drag motion.
6881         private int mStartOffset = -1;
6882 
6883         private boolean mHaventMovedEnoughToStartDrag;
6884         // The line that a selection happened most recently with the drag accelerator.
6885         private int mLineSelectionIsOn = -1;
6886         // Whether the drag accelerator has selected past the initial line.
6887         private boolean mSwitchedLines = false;
6888 
6889         // Indicates the drag accelerator mode that the user is currently using.
6890         private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
6891         // Drag accelerator is inactive.
6892         private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
6893         // Character based selection by dragging. Only for mouse.
6894         private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
6895         // Word based selection by dragging. Enabled after long pressing or double tapping.
6896         private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
6897         // Paragraph based selection by dragging. Enabled after mouse triple click.
6898         private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
6899 
SelectionModifierCursorController()6900         SelectionModifierCursorController() {
6901             resetTouchOffsets();
6902         }
6903 
show()6904         public void show() {
6905             if (mTextView.isInBatchEditMode()) {
6906                 return;
6907             }
6908             loadHandleDrawables(false /* overwrite */);
6909             initHandles();
6910         }
6911 
initHandles()6912         private void initHandles() {
6913             // Lazy object creation has to be done before updatePosition() is called.
6914             if (mStartHandle == null) {
6915                 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
6916                         com.android.internal.R.id.selection_start_handle,
6917                         HANDLE_TYPE_SELECTION_START);
6918             }
6919             if (mEndHandle == null) {
6920                 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
6921                         com.android.internal.R.id.selection_end_handle,
6922                         HANDLE_TYPE_SELECTION_END);
6923             }
6924 
6925             mStartHandle.show();
6926             mEndHandle.show();
6927 
6928             hideInsertionPointCursorController();
6929         }
6930 
reloadHandleDrawables()6931         private void reloadHandleDrawables() {
6932             if (mStartHandle == null) {
6933                 // No need to reload, the potentially new drawables will
6934                 // be used when the handles are created.
6935                 return;
6936             }
6937             mStartHandle.setDrawables(mSelectHandleLeft, mSelectHandleRight);
6938             mEndHandle.setDrawables(mSelectHandleRight, mSelectHandleLeft);
6939         }
6940 
hide()6941         public void hide() {
6942             if (mStartHandle != null) mStartHandle.hide();
6943             if (mEndHandle != null) mEndHandle.hide();
6944         }
6945 
enterDrag(int dragAcceleratorMode)6946         public void enterDrag(int dragAcceleratorMode) {
6947             if (TextView.DEBUG_CURSOR) {
6948                 logCursor("SelectionModifierCursorController: enterDrag",
6949                         "starting selection drag: mode=%s", dragAcceleratorMode);
6950             }
6951 
6952             // Just need to init the handles / hide insertion cursor.
6953             show();
6954             mDragAcceleratorMode = dragAcceleratorMode;
6955             // Start location of selection.
6956             mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(),
6957                     mTouchState.getLastDownY());
6958             mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY());
6959             // Don't show the handles until user has lifted finger.
6960             hide();
6961 
6962             // This stops scrolling parents from intercepting the touch event, allowing
6963             // the user to continue dragging across the screen to select text; TextView will
6964             // scroll as necessary.
6965             mTextView.getParent().requestDisallowInterceptTouchEvent(true);
6966             mTextView.cancelLongPress();
6967         }
6968 
onTouchEvent(MotionEvent event)6969         public void onTouchEvent(MotionEvent event) {
6970             // This is done even when the View does not have focus, so that long presses can start
6971             // selection and tap can move cursor from this tap position.
6972             final float eventX = event.getX();
6973             final float eventY = event.getY();
6974             final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
6975             switch (event.getActionMasked()) {
6976                 case MotionEvent.ACTION_DOWN:
6977                     if (extractedTextModeWillBeStarted()) {
6978                         // Prevent duplicating the selection handles until the mode starts.
6979                         hide();
6980                     } else {
6981                         // Remember finger down position, to be able to start selection from there.
6982                         mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
6983                                 eventX, eventY);
6984 
6985                         // Double tap detection
6986                         if (mGestureStayedInTapRegion
6987                                 && mTouchState.isMultiTapInSameArea()
6988                                 && (isMouse || isPositionOnText(eventX, eventY)
6989                                 || mTouchState.isOnHandle())) {
6990                             if (TextView.DEBUG_CURSOR) {
6991                                 logCursor("SelectionModifierCursorController: onTouchEvent",
6992                                         "ACTION_DOWN: select and start drag");
6993                             }
6994                             if (mTouchState.isDoubleTap()) {
6995                                 selectCurrentWordAndStartDrag();
6996                             } else if (mTouchState.isTripleClick()) {
6997                                 selectCurrentParagraphAndStartDrag();
6998                             }
6999                             mDiscardNextActionUp = true;
7000                         }
7001                         mGestureStayedInTapRegion = true;
7002                         mHaventMovedEnoughToStartDrag = true;
7003                     }
7004                     break;
7005 
7006                 case MotionEvent.ACTION_POINTER_DOWN:
7007                 case MotionEvent.ACTION_POINTER_UP:
7008                     // Handle multi-point gestures. Keep min and max offset positions.
7009                     // Only activated for devices that correctly handle multi-touch.
7010                     if (mTextView.getContext().getPackageManager().hasSystemFeature(
7011                             PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
7012                         updateMinAndMaxOffsets(event);
7013                     }
7014                     break;
7015 
7016                 case MotionEvent.ACTION_MOVE:
7017                     if (mGestureStayedInTapRegion) {
7018                         final ViewConfiguration viewConfig = ViewConfiguration.get(
7019                                 mTextView.getContext());
7020                         mGestureStayedInTapRegion = EditorTouchState.isDistanceWithin(
7021                                 mTouchState.getLastDownX(), mTouchState.getLastDownY(),
7022                                 eventX, eventY, viewConfig.getScaledDoubleTapTouchSlop());
7023                     }
7024 
7025                     if (mHaventMovedEnoughToStartDrag) {
7026                         mHaventMovedEnoughToStartDrag = !mTouchState.isMovedEnoughForDrag();
7027                     }
7028 
7029                     if (isMouse && !isDragAcceleratorActive()) {
7030                         final int offset = mTextView.getOffsetForPosition(eventX, eventY);
7031                         if (mTextView.hasSelection()
7032                                 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
7033                                 && offset >= mTextView.getSelectionStart()
7034                                 && offset <= mTextView.getSelectionEnd()) {
7035                             startDragAndDrop();
7036                             break;
7037                         }
7038 
7039                         if (mStartOffset != offset) {
7040                             // Start character based drag accelerator.
7041                             stopTextActionMode();
7042                             enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
7043                             mDiscardNextActionUp = true;
7044                             mHaventMovedEnoughToStartDrag = false;
7045                         }
7046                     }
7047 
7048                     if (mStartHandle != null && mStartHandle.isShowing()) {
7049                         // Don't do the drag if the handles are showing already.
7050                         break;
7051                     }
7052 
7053                     updateSelection(event);
7054                     if (mTextView.hasSelection() && mEndHandle != null &&
7055                         isDragAcceleratorActive()
7056                     ) {
7057                         mEndHandle.updateMagnifier(event);
7058                     }
7059                     break;
7060 
7061                 case MotionEvent.ACTION_UP:
7062                     if (TextView.DEBUG_CURSOR) {
7063                         logCursor("SelectionModifierCursorController: onTouchEvent", "ACTION_UP");
7064                     }
7065                     if (mEndHandle != null) {
7066                         mEndHandle.dismissMagnifier();
7067                     }
7068                     if (!isDragAcceleratorActive()) {
7069                         break;
7070                     }
7071                     updateSelection(event);
7072 
7073                     // No longer dragging to select text, let the parent intercept events.
7074                     mTextView.getParent().requestDisallowInterceptTouchEvent(false);
7075 
7076                     // No longer the first dragging motion, reset.
7077                     resetDragAcceleratorState();
7078 
7079                     if (mTextView.hasSelection()) {
7080                         // Drag selection should not be adjusted by the text classifier.
7081                         startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
7082                     }
7083                     break;
7084             }
7085         }
7086 
updateSelection(MotionEvent event)7087         private void updateSelection(MotionEvent event) {
7088             if (mTextView.getLayout() != null) {
7089                 switch (mDragAcceleratorMode) {
7090                     case DRAG_ACCELERATOR_MODE_CHARACTER:
7091                         updateCharacterBasedSelection(event);
7092                         break;
7093                     case DRAG_ACCELERATOR_MODE_WORD:
7094                         updateWordBasedSelection(event);
7095                         break;
7096                     case DRAG_ACCELERATOR_MODE_PARAGRAPH:
7097                         updateParagraphBasedSelection(event);
7098                         break;
7099                 }
7100             }
7101         }
7102 
7103         /**
7104          * If the TextView allows text selection, selects the current paragraph and starts a drag.
7105          *
7106          * @return true if the drag was started.
7107          */
selectCurrentParagraphAndStartDrag()7108         private boolean selectCurrentParagraphAndStartDrag() {
7109             if (mInsertionActionModeRunnable != null) {
7110                 mTextView.removeCallbacks(mInsertionActionModeRunnable);
7111             }
7112             stopTextActionMode();
7113             if (!selectCurrentParagraph()) {
7114                 return false;
7115             }
7116             enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
7117             return true;
7118         }
7119 
updateCharacterBasedSelection(MotionEvent event)7120         private void updateCharacterBasedSelection(MotionEvent event) {
7121             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
7122             updateSelectionInternal(mStartOffset, offset,
7123                     event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
7124         }
7125 
updateWordBasedSelection(MotionEvent event)7126         private void updateWordBasedSelection(MotionEvent event) {
7127             if (mHaventMovedEnoughToStartDrag) {
7128                 return;
7129             }
7130             final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
7131             final ViewConfiguration viewConfig = ViewConfiguration.get(
7132                     mTextView.getContext());
7133             final float eventX = event.getX();
7134             final float eventY = event.getY();
7135             final int currLine;
7136             if (isMouse) {
7137                 // No need to offset the y coordinate for mouse input.
7138                 currLine = mTextView.getLineAtCoordinate(eventY);
7139             } else {
7140                 float y = eventY;
7141                 if (mSwitchedLines) {
7142                     // Offset the finger by the same vertical offset as the handles.
7143                     // This improves visibility of the content being selected by
7144                     // shifting the finger below the content, this is applied once
7145                     // the user has switched lines.
7146                     final int touchSlop = viewConfig.getScaledTouchSlop();
7147                     final float fingerOffset = (mStartHandle != null)
7148                             ? mStartHandle.getIdealVerticalOffset()
7149                             : touchSlop;
7150                     y = eventY - fingerOffset;
7151                 }
7152 
7153                 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
7154                         y);
7155                 if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
7156                     // Break early here, we want to offset the finger position from
7157                     // the selection highlight, once the user moved their finger
7158                     // to a different line we should apply the offset and *not* switch
7159                     // lines until recomputing the position with the finger offset.
7160                     mSwitchedLines = true;
7161                     return;
7162                 }
7163             }
7164 
7165             int startOffset;
7166             int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
7167             // Snap to word boundaries.
7168             if (mStartOffset < offset) {
7169                 // Expanding with end handle.
7170                 offset = getWordEnd(offset);
7171                 startOffset = getWordStart(mStartOffset);
7172             } else {
7173                 // Expanding with start handle.
7174                 offset = getWordStart(offset);
7175                 startOffset = getWordEnd(mStartOffset);
7176                 if (startOffset == offset) {
7177                     offset = getNextCursorOffset(offset, false);
7178                 }
7179             }
7180             mLineSelectionIsOn = currLine;
7181             updateSelectionInternal(startOffset, offset,
7182                     event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
7183         }
7184 
updateParagraphBasedSelection(MotionEvent event)7185         private void updateParagraphBasedSelection(MotionEvent event) {
7186             final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
7187 
7188             final int start = Math.min(offset, mStartOffset);
7189             final int end = Math.max(offset, mStartOffset);
7190             final long paragraphsRange = getParagraphsRange(start, end);
7191             final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
7192             final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
7193             updateSelectionInternal(selectionStart, selectionEnd,
7194                     event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
7195         }
7196 
updateSelectionInternal(int selectionStart, int selectionEnd, boolean fromTouchScreen)7197         private void updateSelectionInternal(int selectionStart, int selectionEnd,
7198                 boolean fromTouchScreen) {
7199             final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
7200                     && ((mTextView.getSelectionStart() != selectionStart)
7201                             || (mTextView.getSelectionEnd() != selectionEnd));
7202             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
7203             if (performHapticFeedback) {
7204                 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
7205             }
7206         }
7207 
7208         /**
7209          * @param event
7210          */
updateMinAndMaxOffsets(MotionEvent event)7211         private void updateMinAndMaxOffsets(MotionEvent event) {
7212             int pointerCount = event.getPointerCount();
7213             for (int index = 0; index < pointerCount; index++) {
7214                 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
7215                 if (offset < mMinTouchOffset) mMinTouchOffset = offset;
7216                 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
7217             }
7218         }
7219 
getMinTouchOffset()7220         public int getMinTouchOffset() {
7221             return mMinTouchOffset;
7222         }
7223 
getMaxTouchOffset()7224         public int getMaxTouchOffset() {
7225             return mMaxTouchOffset;
7226         }
7227 
resetTouchOffsets()7228         public void resetTouchOffsets() {
7229             mMinTouchOffset = mMaxTouchOffset = -1;
7230             resetDragAcceleratorState();
7231         }
7232 
resetDragAcceleratorState()7233         private void resetDragAcceleratorState() {
7234             mStartOffset = -1;
7235             mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
7236             mSwitchedLines = false;
7237             final int selectionStart = mTextView.getSelectionStart();
7238             final int selectionEnd = mTextView.getSelectionEnd();
7239             if (selectionStart < 0 || selectionEnd < 0) {
7240                 Selection.removeSelection((Spannable) mTextView.getText());
7241             } else if (selectionStart > selectionEnd) {
7242                 Selection.setSelection((Spannable) mTextView.getText(),
7243                         selectionEnd, selectionStart);
7244             }
7245         }
7246 
7247         /**
7248          * @return true iff this controller is currently used to move the selection start.
7249          */
isSelectionStartDragged()7250         public boolean isSelectionStartDragged() {
7251             return mStartHandle != null && mStartHandle.isDragging();
7252         }
7253 
7254         @Override
isCursorBeingModified()7255         public boolean isCursorBeingModified() {
7256             return isDragAcceleratorActive() || isSelectionStartDragged()
7257                     || (mEndHandle != null && mEndHandle.isDragging());
7258         }
7259 
7260         /**
7261          * @return true if the user is selecting text using the drag accelerator.
7262          */
isDragAcceleratorActive()7263         public boolean isDragAcceleratorActive() {
7264             return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
7265         }
7266 
onTouchModeChanged(boolean isInTouchMode)7267         public void onTouchModeChanged(boolean isInTouchMode) {
7268             if (!isInTouchMode) {
7269                 hide();
7270             }
7271         }
7272 
7273         @Override
onDetached()7274         public void onDetached() {
7275             final ViewTreeObserver observer = mTextView.getViewTreeObserver();
7276             observer.removeOnTouchModeChangeListener(this);
7277 
7278             if (mStartHandle != null) mStartHandle.onDetached();
7279             if (mEndHandle != null) mEndHandle.onDetached();
7280         }
7281 
7282         @Override
isActive()7283         public boolean isActive() {
7284             return mStartHandle != null && mStartHandle.isShowing();
7285         }
7286 
invalidateHandles()7287         public void invalidateHandles() {
7288             if (mStartHandle != null) {
7289                 mStartHandle.invalidate();
7290             }
7291             if (mEndHandle != null) {
7292                 mEndHandle.invalidate();
7293             }
7294         }
7295     }
7296 
7297     /**
7298      * Loads the insertion and selection handle Drawables from TextView. If the handle
7299      * drawables are already loaded, do not overwrite them unless the method parameter
7300      * is set to true. This logic is required to avoid overwriting Drawables assigned
7301      * to mSelectHandle[Center/Left/Right] by developers using reflection, unless they
7302      * explicitly call the setters in TextView.
7303      *
7304      * @param overwrite whether to overwrite already existing nonnull Drawables
7305      */
loadHandleDrawables(final boolean overwrite)7306     void loadHandleDrawables(final boolean overwrite) {
7307         if (mSelectHandleCenter == null || overwrite) {
7308             mSelectHandleCenter = mTextView.getTextSelectHandle();
7309             if (hasInsertionController()) {
7310                 getInsertionController().reloadHandleDrawable();
7311             }
7312         }
7313 
7314         if (mSelectHandleLeft == null || mSelectHandleRight == null || overwrite) {
7315             mSelectHandleLeft = mTextView.getTextSelectHandleLeft();
7316             mSelectHandleRight = mTextView.getTextSelectHandleRight();
7317             if (hasSelectionController()) {
7318                 getSelectionController().reloadHandleDrawables();
7319             }
7320         }
7321     }
7322 
7323     private class CorrectionHighlighter {
7324         private final Path mPath = new Path();
7325         private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
7326         private int mStart, mEnd;
7327         private long mFadingStartTime;
7328         private RectF mTempRectF;
7329         private static final int FADE_OUT_DURATION = 400;
7330 
CorrectionHighlighter()7331         public CorrectionHighlighter() {
7332             mPaint.setCompatibilityScaling(
7333                     mTextView.getResources().getCompatibilityInfo().applicationScale);
7334             mPaint.setStyle(Paint.Style.FILL);
7335         }
7336 
highlight(CorrectionInfo info)7337         public void highlight(CorrectionInfo info) {
7338             mStart = info.getOffset();
7339             mEnd = mStart + info.getNewText().length();
7340             mFadingStartTime = SystemClock.uptimeMillis();
7341 
7342             if (mStart < 0 || mEnd < 0) {
7343                 stopAnimation();
7344             }
7345         }
7346 
draw(Canvas canvas, int cursorOffsetVertical)7347         public void draw(Canvas canvas, int cursorOffsetVertical) {
7348             if (updatePath() && updatePaint()) {
7349                 if (cursorOffsetVertical != 0) {
7350                     canvas.translate(0, cursorOffsetVertical);
7351                 }
7352 
7353                 canvas.drawPath(mPath, mPaint);
7354 
7355                 if (cursorOffsetVertical != 0) {
7356                     canvas.translate(0, -cursorOffsetVertical);
7357                 }
7358                 invalidate(true); // TODO invalidate cursor region only
7359             } else {
7360                 stopAnimation();
7361                 invalidate(false); // TODO invalidate cursor region only
7362             }
7363         }
7364 
updatePaint()7365         private boolean updatePaint() {
7366             final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
7367             if (duration > FADE_OUT_DURATION) return false;
7368 
7369             final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
7370             final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
7371             final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
7372                     + ((int) (highlightColorAlpha * coef) << 24);
7373             mPaint.setColor(color);
7374             return true;
7375         }
7376 
updatePath()7377         private boolean updatePath() {
7378             final Layout layout = mTextView.getLayout();
7379             if (layout == null) return false;
7380 
7381             // Update in case text is edited while the animation is run
7382             final int length = mTextView.getText().length();
7383             int start = Math.min(length, mStart);
7384             int end = Math.min(length, mEnd);
7385 
7386             mPath.reset();
7387             layout.getSelectionPath(
7388                     mTextView.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER),
7389                     mTextView.originalToTransformed(end, OffsetMapping.MAP_STRATEGY_CHARACTER),
7390                     mPath);
7391             return true;
7392         }
7393 
invalidate(boolean delayed)7394         private void invalidate(boolean delayed) {
7395             if (mTextView.getLayout() == null) return;
7396 
7397             if (mTempRectF == null) mTempRectF = new RectF();
7398             mPath.computeBounds(mTempRectF, false);
7399 
7400             int left = mTextView.getCompoundPaddingLeft();
7401             int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
7402 
7403             if (delayed) {
7404                 mTextView.postInvalidateOnAnimation(
7405                         left + (int) mTempRectF.left, top + (int) mTempRectF.top,
7406                         left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
7407             } else {
7408                 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
7409                         (int) mTempRectF.right, (int) mTempRectF.bottom);
7410             }
7411         }
7412 
stopAnimation()7413         private void stopAnimation() {
7414             Editor.this.mCorrectionHighlighter = null;
7415         }
7416     }
7417 
7418     private static class ErrorPopup extends PopupWindow {
7419         private boolean mAbove = false;
7420         private final TextView mView;
7421         private int mPopupInlineErrorBackgroundId = 0;
7422         private int mPopupInlineErrorAboveBackgroundId = 0;
7423 
ErrorPopup(TextView v, int width, int height)7424         ErrorPopup(TextView v, int width, int height) {
7425             super(v, width, height);
7426             mView = v;
7427             // Make sure the TextView has a background set as it will be used the first time it is
7428             // shown and positioned. Initialized with below background, which should have
7429             // dimensions identical to the above version for this to work (and is more likely).
7430             mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
7431                     com.android.internal.R.styleable.Theme_errorMessageBackground);
7432             mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
7433         }
7434 
fixDirection(boolean above)7435         void fixDirection(boolean above) {
7436             mAbove = above;
7437 
7438             if (above) {
7439                 mPopupInlineErrorAboveBackgroundId =
7440                     getResourceId(mPopupInlineErrorAboveBackgroundId,
7441                             com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
7442             } else {
7443                 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
7444                         com.android.internal.R.styleable.Theme_errorMessageBackground);
7445             }
7446 
7447             mView.setBackgroundResource(
7448                     above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
7449         }
7450 
getResourceId(int currentId, int index)7451         private int getResourceId(int currentId, int index) {
7452             if (currentId == 0) {
7453                 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
7454                         R.styleable.Theme);
7455                 currentId = styledAttributes.getResourceId(index, 0);
7456                 styledAttributes.recycle();
7457             }
7458             return currentId;
7459         }
7460 
7461         @Override
update(int x, int y, int w, int h, boolean force)7462         public void update(int x, int y, int w, int h, boolean force) {
7463             super.update(x, y, w, h, force);
7464 
7465             boolean above = isAboveAnchor();
7466             if (above != mAbove) {
7467                 fixDirection(above);
7468             }
7469         }
7470     }
7471 
7472     static class InputContentType {
7473         int imeOptions = EditorInfo.IME_NULL;
7474         @UnsupportedAppUsage
7475         String privateImeOptions;
7476         CharSequence imeActionLabel;
7477         int imeActionId;
7478         Bundle extras;
7479         OnEditorActionListener onEditorActionListener;
7480         boolean enterDown;
7481         LocaleList imeHintLocales;
7482     }
7483 
7484     static class InputMethodState {
7485         ExtractedTextRequest mExtractedTextRequest;
7486         final ExtractedText mExtractedText = new ExtractedText();
7487         int mBatchEditNesting;
7488         boolean mCursorChanged;
7489         boolean mSelectionModeChanged;
7490         boolean mContentChanged;
7491         int mChangedStart, mChangedEnd, mChangedDelta;
7492         @InputConnection.CursorUpdateMode
7493         int mUpdateCursorAnchorInfoMode;
7494         @InputConnection.CursorUpdateFilter
7495         int mUpdateCursorAnchorInfoFilter;
7496     }
7497 
7498     /**
7499      * @return True iff (start, end) is a valid range within the text.
7500      */
isValidRange(CharSequence text, int start, int end)7501     private static boolean isValidRange(CharSequence text, int start, int end) {
7502         return 0 <= start && start <= end && end <= text.length();
7503     }
7504 
7505     /**
7506      * An InputFilter that monitors text input to maintain undo history. It does not modify the
7507      * text being typed (and hence always returns null from the filter() method).
7508      *
7509      * TODO: Make this span aware.
7510      */
7511     public static class UndoInputFilter implements InputFilter {
7512         private final Editor mEditor;
7513 
7514         // Whether the current filter pass is directly caused by an end-user text edit.
7515         private boolean mIsUserEdit;
7516 
7517         // Whether the text field is handling an IME composition. Must be parceled in case the user
7518         // rotates the screen during composition.
7519         private boolean mHasComposition;
7520 
7521         // Whether the user is expanding or shortening the text
7522         private boolean mExpanding;
7523 
7524         // Whether the previous edit operation was in the current batch edit.
7525         private boolean mPreviousOperationWasInSameBatchEdit;
7526 
UndoInputFilter(Editor editor)7527         public UndoInputFilter(Editor editor) {
7528             mEditor = editor;
7529         }
7530 
saveInstanceState(Parcel parcel)7531         public void saveInstanceState(Parcel parcel) {
7532             parcel.writeInt(mIsUserEdit ? 1 : 0);
7533             parcel.writeInt(mHasComposition ? 1 : 0);
7534             parcel.writeInt(mExpanding ? 1 : 0);
7535             parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
7536         }
7537 
restoreInstanceState(Parcel parcel)7538         public void restoreInstanceState(Parcel parcel) {
7539             mIsUserEdit = parcel.readInt() != 0;
7540             mHasComposition = parcel.readInt() != 0;
7541             mExpanding = parcel.readInt() != 0;
7542             mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
7543         }
7544 
7545         /**
7546          * Signals that a user-triggered edit is starting.
7547          */
beginBatchEdit()7548         public void beginBatchEdit() {
7549             if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
7550             mIsUserEdit = true;
7551         }
7552 
endBatchEdit()7553         public void endBatchEdit() {
7554             if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
7555             mIsUserEdit = false;
7556             mPreviousOperationWasInSameBatchEdit = false;
7557         }
7558 
7559         @Override
filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)7560         public CharSequence filter(CharSequence source, int start, int end,
7561                 Spanned dest, int dstart, int dend) {
7562             if (DEBUG_UNDO) {
7563                 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
7564                         + "dest=" + dest + " (" + dstart + "-" + dend + ")");
7565             }
7566 
7567             // Check to see if this edit should be tracked for undo.
7568             if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
7569                 return null;
7570             }
7571 
7572             final boolean hadComposition = mHasComposition;
7573             mHasComposition = isComposition(source);
7574             final boolean wasExpanding = mExpanding;
7575             boolean shouldCreateSeparateState = false;
7576             if ((end - start) != (dend - dstart)) {
7577                 mExpanding = (end - start) > (dend - dstart);
7578                 if (hadComposition && mExpanding != wasExpanding) {
7579                     shouldCreateSeparateState = true;
7580                 }
7581             }
7582 
7583             // Handle edit.
7584             handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
7585             return null;
7586         }
7587 
freezeLastEdit()7588         void freezeLastEdit() {
7589             mEditor.mUndoManager.beginUpdate("Edit text");
7590             EditOperation lastEdit = getLastEdit();
7591             if (lastEdit != null) {
7592                 lastEdit.mFrozen = true;
7593             }
7594             mEditor.mUndoManager.endUpdate();
7595         }
7596 
7597         @Retention(RetentionPolicy.SOURCE)
7598         @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
7599                 MERGE_EDIT_MODE_FORCE_MERGE,
7600                 MERGE_EDIT_MODE_NEVER_MERGE,
7601                 MERGE_EDIT_MODE_NORMAL
7602         })
7603         private @interface MergeMode {}
7604         private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
7605         private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
7606         /** Use {@link EditOperation#mergeWith} to merge */
7607         private static final int MERGE_EDIT_MODE_NORMAL = 2;
7608 
handleEdit(CharSequence source, int start, int end, Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState)7609         private void handleEdit(CharSequence source, int start, int end,
7610                 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
7611             // An application may install a TextWatcher to provide additional modifications after
7612             // the initial input filters run (e.g. a credit card formatter that adds spaces to a
7613             // string). This results in multiple filter() calls for what the user considers to be
7614             // a single operation. Always undo the whole set of changes in one step.
7615             @MergeMode
7616             final int mergeMode;
7617             if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
7618                 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
7619             } else if (shouldCreateSeparateState) {
7620                 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
7621             } else {
7622                 mergeMode = MERGE_EDIT_MODE_NORMAL;
7623             }
7624             // Build a new operation with all the information from this edit.
7625             String newText = TextUtils.substring(source, start, end);
7626             String oldText = TextUtils.substring(dest, dstart, dend);
7627             EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
7628                     mHasComposition);
7629             if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
7630                 return;
7631             }
7632             recordEdit(edit, mergeMode);
7633         }
7634 
getLastEdit()7635         private EditOperation getLastEdit() {
7636             final UndoManager um = mEditor.mUndoManager;
7637             return um.getLastOperation(
7638                   EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
7639         }
7640         /**
7641          * Fetches the last undo operation and checks to see if a new edit should be merged into it.
7642          * If forceMerge is true then the new edit is always merged.
7643          */
recordEdit(EditOperation edit, @MergeMode int mergeMode)7644         private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
7645             // Fetch the last edit operation and attempt to merge in the new edit.
7646             final UndoManager um = mEditor.mUndoManager;
7647             um.beginUpdate("Edit text");
7648             EditOperation lastEdit = getLastEdit();
7649             if (lastEdit == null) {
7650                 // Add this as the first edit.
7651                 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
7652                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
7653             } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
7654                 // Forced merges take priority because they could be the result of a non-user-edit
7655                 // change and this case should not create a new undo operation.
7656                 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
7657                 lastEdit.forceMergeWith(edit);
7658             } else if (!mIsUserEdit) {
7659                 // An application directly modified the Editable outside of a text edit. Treat this
7660                 // as a new change and don't attempt to merge.
7661                 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
7662                 um.commitState(mEditor.mUndoOwner);
7663                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
7664             } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
7665                 // Merge succeeded, nothing else to do.
7666                 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
7667             } else {
7668                 // Could not merge with the last edit, so commit the last edit and add this edit.
7669                 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
7670                 um.commitState(mEditor.mUndoOwner);
7671                 um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
7672             }
7673             mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
7674             um.endUpdate();
7675         }
7676 
canUndoEdit(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)7677         private boolean canUndoEdit(CharSequence source, int start, int end,
7678                 Spanned dest, int dstart, int dend) {
7679             if (!mEditor.mAllowUndo) {
7680                 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
7681                 return false;
7682             }
7683 
7684             if (mEditor.mUndoManager.isInUndo()) {
7685                 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
7686                 return false;
7687             }
7688 
7689             // Text filters run before input operations are applied. However, some input operations
7690             // are invalid and will throw exceptions when applied. This is common in tests. Don't
7691             // attempt to undo invalid operations.
7692             if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
7693                 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
7694                 return false;
7695             }
7696 
7697             // Earlier filters can rewrite input to be a no-op, for example due to a length limit
7698             // on an input field. Skip no-op changes.
7699             if (start == end && dstart == dend) {
7700                 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
7701                 return false;
7702             }
7703 
7704             return true;
7705         }
7706 
isComposition(CharSequence source)7707         private static boolean isComposition(CharSequence source) {
7708             if (!(source instanceof Spannable)) {
7709                 return false;
7710             }
7711             // This is a composition edit if the source has a non-zero-length composing span.
7712             Spannable text = (Spannable) source;
7713             int composeBegin = EditableInputConnection.getComposingSpanStart(text);
7714             int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
7715             return composeBegin < composeEnd;
7716         }
7717 
isInTextWatcher()7718         private boolean isInTextWatcher() {
7719             CharSequence text = mEditor.mTextView.getText();
7720             return (text instanceof SpannableStringBuilder)
7721                     && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
7722         }
7723     }
7724 
7725     /**
7726      * An operation to undo a single "edit" to a text view.
7727      */
7728     public static class EditOperation extends UndoOperation<Editor> {
7729         private static final int TYPE_INSERT = 0;
7730         private static final int TYPE_DELETE = 1;
7731         private static final int TYPE_REPLACE = 2;
7732 
7733         private int mType;
7734         private String mOldText;
7735         private String mNewText;
7736         private int mStart;
7737 
7738         private int mOldCursorPos;
7739         private int mNewCursorPos;
7740         private boolean mFrozen;
7741         private boolean mIsComposition;
7742 
7743         /**
7744          * Constructs an edit operation from a text input operation on editor that replaces the
7745          * oldText starting at dstart with newText.
7746          */
EditOperation(Editor editor, String oldText, int dstart, String newText, boolean isComposition)7747         public EditOperation(Editor editor, String oldText, int dstart, String newText,
7748                 boolean isComposition) {
7749             super(editor.mUndoOwner);
7750             mOldText = oldText;
7751             mNewText = newText;
7752 
7753             // Determine the type of the edit.
7754             if (mNewText.length() > 0 && mOldText.length() == 0) {
7755                 mType = TYPE_INSERT;
7756             } else if (mNewText.length() == 0 && mOldText.length() > 0) {
7757                 mType = TYPE_DELETE;
7758             } else {
7759                 mType = TYPE_REPLACE;
7760             }
7761 
7762             mStart = dstart;
7763             // Store cursor data.
7764             mOldCursorPos = editor.mTextView.getSelectionStart();
7765             mNewCursorPos = dstart + mNewText.length();
7766             mIsComposition = isComposition;
7767         }
7768 
EditOperation(Parcel src, ClassLoader loader)7769         public EditOperation(Parcel src, ClassLoader loader) {
7770             super(src, loader);
7771             mType = src.readInt();
7772             mOldText = src.readString();
7773             mNewText = src.readString();
7774             mStart = src.readInt();
7775             mOldCursorPos = src.readInt();
7776             mNewCursorPos = src.readInt();
7777             mFrozen = src.readInt() == 1;
7778             mIsComposition = src.readInt() == 1;
7779         }
7780 
7781         @Override
writeToParcel(Parcel dest, int flags)7782         public void writeToParcel(Parcel dest, int flags) {
7783             dest.writeInt(mType);
7784             dest.writeString(mOldText);
7785             dest.writeString(mNewText);
7786             dest.writeInt(mStart);
7787             dest.writeInt(mOldCursorPos);
7788             dest.writeInt(mNewCursorPos);
7789             dest.writeInt(mFrozen ? 1 : 0);
7790             dest.writeInt(mIsComposition ? 1 : 0);
7791         }
7792 
getNewTextEnd()7793         private int getNewTextEnd() {
7794             return mStart + mNewText.length();
7795         }
7796 
getOldTextEnd()7797         private int getOldTextEnd() {
7798             return mStart + mOldText.length();
7799         }
7800 
7801         @Override
commit()7802         public void commit() {
7803         }
7804 
7805         @Override
undo()7806         public void undo() {
7807             if (DEBUG_UNDO) Log.d(TAG, "undo");
7808             // Remove the new text and insert the old.
7809             Editor editor = getOwnerData();
7810             Editable text = (Editable) editor.mTextView.getText();
7811             modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
7812         }
7813 
7814         @Override
redo()7815         public void redo() {
7816             if (DEBUG_UNDO) Log.d(TAG, "redo");
7817             // Remove the old text and insert the new.
7818             Editor editor = getOwnerData();
7819             Editable text = (Editable) editor.mTextView.getText();
7820             modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
7821         }
7822 
7823         /**
7824          * Attempts to merge this existing operation with a new edit.
7825          * @param edit The new edit operation.
7826          * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
7827          * object unchanged.
7828          */
mergeWith(EditOperation edit)7829         private boolean mergeWith(EditOperation edit) {
7830             if (DEBUG_UNDO) {
7831                 Log.d(TAG, "mergeWith old " + this);
7832                 Log.d(TAG, "mergeWith new " + edit);
7833             }
7834 
7835             if (mFrozen) {
7836                 return false;
7837             }
7838 
7839             switch (mType) {
7840                 case TYPE_INSERT:
7841                     return mergeInsertWith(edit);
7842                 case TYPE_DELETE:
7843                     return mergeDeleteWith(edit);
7844                 case TYPE_REPLACE:
7845                     return mergeReplaceWith(edit);
7846                 default:
7847                     return false;
7848             }
7849         }
7850 
mergeInsertWith(EditOperation edit)7851         private boolean mergeInsertWith(EditOperation edit) {
7852             if (edit.mType == TYPE_INSERT) {
7853                 // Merge insertions that are contiguous even when it's frozen.
7854                 if (getNewTextEnd() != edit.mStart) {
7855                     return false;
7856                 }
7857                 mNewText += edit.mNewText;
7858                 mNewCursorPos = edit.mNewCursorPos;
7859                 mFrozen = edit.mFrozen;
7860                 mIsComposition = edit.mIsComposition;
7861                 return true;
7862             }
7863             if (mIsComposition && edit.mType == TYPE_REPLACE
7864                     && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
7865                 // Merge insertion with replace as they can be single insertion.
7866                 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
7867                         + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
7868                 mNewCursorPos = edit.mNewCursorPos;
7869                 mIsComposition = edit.mIsComposition;
7870                 return true;
7871             }
7872             return false;
7873         }
7874 
7875         // TODO: Support forward delete.
mergeDeleteWith(EditOperation edit)7876         private boolean mergeDeleteWith(EditOperation edit) {
7877             // Only merge continuous deletes.
7878             if (edit.mType != TYPE_DELETE) {
7879                 return false;
7880             }
7881             // Only merge deletions that are contiguous.
7882             if (mStart != edit.getOldTextEnd()) {
7883                 return false;
7884             }
7885             mStart = edit.mStart;
7886             mOldText = edit.mOldText + mOldText;
7887             mNewCursorPos = edit.mNewCursorPos;
7888             mIsComposition = edit.mIsComposition;
7889             return true;
7890         }
7891 
mergeReplaceWith(EditOperation edit)7892         private boolean mergeReplaceWith(EditOperation edit) {
7893             if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
7894                 // Merge with adjacent insert.
7895                 mNewText += edit.mNewText;
7896                 mNewCursorPos = edit.mNewCursorPos;
7897                 return true;
7898             }
7899             if (!mIsComposition) {
7900                 return false;
7901             }
7902             if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
7903                     && getNewTextEnd() >= edit.getOldTextEnd()) {
7904                 // Merge with delete as they can be single operation.
7905                 mNewText = mNewText.substring(0, edit.mStart - mStart)
7906                         + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
7907                 if (mNewText.isEmpty()) {
7908                     mType = TYPE_DELETE;
7909                 }
7910                 mNewCursorPos = edit.mNewCursorPos;
7911                 mIsComposition = edit.mIsComposition;
7912                 return true;
7913             }
7914             if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
7915                     && TextUtils.equals(mNewText, edit.mOldText)) {
7916                 // Merge with the replace that replaces the same region.
7917                 mNewText = edit.mNewText;
7918                 mNewCursorPos = edit.mNewCursorPos;
7919                 mIsComposition = edit.mIsComposition;
7920                 return true;
7921             }
7922             return false;
7923         }
7924 
7925         /**
7926          * Forcibly creates a single merged edit operation by simulating the entire text
7927          * contents being replaced.
7928          */
forceMergeWith(EditOperation edit)7929         public void forceMergeWith(EditOperation edit) {
7930             if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
7931             if (mergeWith(edit)) {
7932                 return;
7933             }
7934             Editor editor = getOwnerData();
7935 
7936             // Copy the text of the current field.
7937             // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
7938             // but would require two parallel implementations of modifyText() because Editable and
7939             // StringBuilder do not share an interface for replace/delete/insert.
7940             Editable editable = (Editable) editor.mTextView.getText();
7941             Editable originalText = new SpannableStringBuilder(editable.toString());
7942 
7943             // Roll back the last operation.
7944             modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
7945 
7946             // Clone the text again and apply the new operation.
7947             Editable finalText = new SpannableStringBuilder(editable.toString());
7948             modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
7949                     edit.mNewText, edit.mStart, edit.mNewCursorPos);
7950 
7951             // Convert this operation into a replace operation.
7952             mType = TYPE_REPLACE;
7953             mNewText = finalText.toString();
7954             mOldText = originalText.toString();
7955             mStart = 0;
7956             mNewCursorPos = edit.mNewCursorPos;
7957             mIsComposition = edit.mIsComposition;
7958             // mOldCursorPos is unchanged.
7959         }
7960 
modifyText(Editable text, int deleteFrom, int deleteTo, CharSequence newText, int newTextInsertAt, int newCursorPos)7961         private static void modifyText(Editable text, int deleteFrom, int deleteTo,
7962                 CharSequence newText, int newTextInsertAt, int newCursorPos) {
7963             // Apply the edit if it is still valid.
7964             if (isValidRange(text, deleteFrom, deleteTo)
7965                     && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
7966                 if (deleteFrom != deleteTo) {
7967                     text.delete(deleteFrom, deleteTo);
7968                 }
7969                 if (newText.length() != 0) {
7970                     text.insert(newTextInsertAt, newText);
7971                 }
7972             }
7973             // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
7974             // don't explicitly set it and rely on SpannableStringBuilder to position it.
7975             // TODO: Select all the text that was undone.
7976             if (0 <= newCursorPos && newCursorPos <= text.length()) {
7977                 Selection.setSelection(text, newCursorPos);
7978             }
7979         }
7980 
getTypeString()7981         private String getTypeString() {
7982             switch (mType) {
7983                 case TYPE_INSERT:
7984                     return "insert";
7985                 case TYPE_DELETE:
7986                     return "delete";
7987                 case TYPE_REPLACE:
7988                     return "replace";
7989                 default:
7990                     return "";
7991             }
7992         }
7993 
7994         @Override
toString()7995         public String toString() {
7996             return "[mType=" + getTypeString() + ", "
7997                     + "mOldText=" + mOldText + ", "
7998                     + "mNewText=" + mNewText + ", "
7999                     + "mStart=" + mStart + ", "
8000                     + "mOldCursorPos=" + mOldCursorPos + ", "
8001                     + "mNewCursorPos=" + mNewCursorPos + ", "
8002                     + "mFrozen=" + mFrozen + ", "
8003                     + "mIsComposition=" + mIsComposition + "]";
8004         }
8005 
8006         public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
8007                 new Parcelable.ClassLoaderCreator<EditOperation>() {
8008             @Override
8009             public EditOperation createFromParcel(Parcel in) {
8010                 return new EditOperation(in, null);
8011             }
8012 
8013             @Override
8014             public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
8015                 return new EditOperation(in, loader);
8016             }
8017 
8018             @Override
8019             public EditOperation[] newArray(int size) {
8020                 return new EditOperation[size];
8021             }
8022         };
8023     }
8024 
8025     /**
8026      * A helper for enabling and handling "PROCESS_TEXT" menu actions.
8027      * These allow external applications to plug into currently selected text.
8028      */
8029     static final class ProcessTextIntentActionsHandler {
8030 
8031         private final Editor mEditor;
8032         private final TextView mTextView;
8033         private final Context mContext;
8034         private final PackageManager mPackageManager;
8035         private final String mPackageName;
8036         private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
8037         private final SparseArray<AccessibilityAction> mAccessibilityActions =
8038                 new SparseArray<>();
8039         private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
8040 
ProcessTextIntentActionsHandler(Editor editor)8041         private ProcessTextIntentActionsHandler(Editor editor) {
8042             mEditor = Objects.requireNonNull(editor);
8043             mTextView = Objects.requireNonNull(mEditor.mTextView);
8044             mContext = Objects.requireNonNull(mTextView.getContext());
8045             mPackageManager = Objects.requireNonNull(mContext.getPackageManager());
8046             mPackageName = Objects.requireNonNull(mContext.getPackageName());
8047         }
8048 
8049         /**
8050          * Adds "PROCESS_TEXT" menu items to the specified menu.
8051          */
onInitializeMenu(Menu menu)8052         public void onInitializeMenu(Menu menu) {
8053             loadSupportedActivities();
8054             final int size = mSupportedActivities.size();
8055             for (int i = 0; i < size; i++) {
8056                 final ResolveInfo resolveInfo = mSupportedActivities.get(i);
8057                 menu.add(Menu.NONE, Menu.NONE,
8058                         Editor.ACTION_MODE_MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
8059                         getLabel(resolveInfo))
8060                         .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
8061                         .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
8062             }
8063         }
8064 
8065         /**
8066          * Performs a "PROCESS_TEXT" action if there is one associated with the specified
8067          * menu item.
8068          *
8069          * @return True if the action was performed, false otherwise.
8070          */
performMenuItemAction(MenuItem item)8071         public boolean performMenuItemAction(MenuItem item) {
8072             return fireIntent(item.getIntent());
8073         }
8074 
8075         /**
8076          * Initializes and caches "PROCESS_TEXT" accessibility actions.
8077          */
initializeAccessibilityActions()8078         public void initializeAccessibilityActions() {
8079             mAccessibilityIntents.clear();
8080             mAccessibilityActions.clear();
8081             int i = 0;
8082             loadSupportedActivities();
8083             for (ResolveInfo resolveInfo : mSupportedActivities) {
8084                 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
8085                 mAccessibilityActions.put(
8086                         actionId,
8087                         new AccessibilityAction(actionId, getLabel(resolveInfo)));
8088                 mAccessibilityIntents.put(
8089                         actionId, createProcessTextIntentForResolveInfo(resolveInfo));
8090             }
8091         }
8092 
8093         /**
8094          * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
8095          * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
8096          * latest accessibility actions available for this call.
8097          */
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8098         public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
8099             for (int i = 0; i < mAccessibilityActions.size(); i++) {
8100                 nodeInfo.addAction(mAccessibilityActions.valueAt(i));
8101             }
8102         }
8103 
8104         /**
8105          * Performs a "PROCESS_TEXT" action if there is one associated with the specified
8106          * accessibility action id.
8107          *
8108          * @return True if the action was performed, false otherwise.
8109          */
performAccessibilityAction(int actionId)8110         public boolean performAccessibilityAction(int actionId) {
8111             return fireIntent(mAccessibilityIntents.get(actionId));
8112         }
8113 
fireIntent(Intent intent)8114         private boolean fireIntent(Intent intent) {
8115             if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
8116                 String selectedText = mTextView.getSelectedText();
8117                 selectedText = TextUtils.trimToParcelableSize(selectedText);
8118                 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
8119                 mEditor.mPreserveSelection = true;
8120                 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
8121                 return true;
8122             }
8123             return false;
8124         }
8125 
loadSupportedActivities()8126         private void loadSupportedActivities() {
8127             mSupportedActivities.clear();
8128             if (!mContext.canStartActivityForResult()) {
8129                 return;
8130             }
8131             PackageManager packageManager = mTextView.getContext().getPackageManager();
8132             List<ResolveInfo> unfiltered =
8133                     packageManager.queryIntentActivities(createProcessTextIntent(), 0);
8134             for (ResolveInfo info : unfiltered) {
8135                 if (isSupportedActivity(info)) {
8136                     mSupportedActivities.add(info);
8137                 }
8138             }
8139         }
8140 
isSupportedActivity(ResolveInfo info)8141         private boolean isSupportedActivity(ResolveInfo info) {
8142             return mPackageName.equals(info.activityInfo.packageName)
8143                     || info.activityInfo.exported
8144                             && (info.activityInfo.permission == null
8145                                     || mContext.checkSelfPermission(info.activityInfo.permission)
8146                                             == PackageManager.PERMISSION_GRANTED);
8147         }
8148 
createProcessTextIntentForResolveInfo(ResolveInfo info)8149         private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
8150             return createProcessTextIntent()
8151                     .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
8152                     .setClassName(info.activityInfo.packageName, info.activityInfo.name);
8153         }
8154 
createProcessTextIntent()8155         private Intent createProcessTextIntent() {
8156             return new Intent()
8157                     .setAction(Intent.ACTION_PROCESS_TEXT)
8158                     .setType("text/plain");
8159         }
8160 
getLabel(ResolveInfo resolveInfo)8161         private CharSequence getLabel(ResolveInfo resolveInfo) {
8162             return resolveInfo.loadLabel(mPackageManager);
8163         }
8164     }
8165 
8166     /**
8167      * Accessibility helper for "smart" (i.e. textAssist) actions.
8168      * Helps ensure that "smart" actions are shown in the accessibility menu.
8169      * NOTE that these actions are only available when an action mode is live.
8170      *
8171      * @hide
8172      */
8173     private static final class AccessibilitySmartActions {
8174 
8175         private final TextView mTextView;
8176         private final SparseArray<Pair<AccessibilityAction, RemoteAction>> mActions =
8177                 new SparseArray<>();
8178 
AccessibilitySmartActions(TextView textView)8179         private AccessibilitySmartActions(TextView textView) {
8180             mTextView = Objects.requireNonNull(textView);
8181         }
8182 
addAction(RemoteAction action)8183         private void addAction(RemoteAction action) {
8184             final int actionId = ACCESSIBILITY_ACTION_SMART_START_ID + mActions.size();
8185             mActions.put(actionId,
8186                     new Pair(new AccessibilityAction(actionId, action.getTitle()), action));
8187         }
8188 
reset()8189         private void reset() {
8190             mActions.clear();
8191         }
8192 
onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8193         void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
8194             for (int i = 0; i < mActions.size(); i++) {
8195                 nodeInfo.addAction(mActions.valueAt(i).first);
8196             }
8197         }
8198 
performAccessibilityAction(int actionId)8199         boolean performAccessibilityAction(int actionId) {
8200             final Pair<AccessibilityAction, RemoteAction> pair = mActions.get(actionId);
8201             if (pair != null) {
8202                 TextClassification.createIntentOnClickListener(pair.second.getActionIntent())
8203                         .onClick(mTextView);
8204                 return true;
8205             }
8206             return false;
8207         }
8208     }
8209 
8210     private static final class InsertModeController {
8211         private final TextView mTextView;
8212         private boolean mIsInsertModeActive;
8213         private InsertModeTransformationMethod mInsertModeTransformationMethod;
8214         private final Paint mHighlightPaint;
8215         private final Path mHighlightPath;
8216 
8217         /**
8218          * Whether it is in the progress of updating transformation method. It's needed because
8219          * {@link TextView#setTransformationMethod(TransformationMethod)} will eventually call
8220          * {@link TextView#setText(CharSequence)}.
8221          * Because it normally should exit insert mode when {@link TextView#setText(CharSequence)}
8222          * is called externally, we need this boolean to distinguish whether setText is triggered
8223          * by setTransformation or not.
8224          */
8225         private boolean mUpdatingTransformationMethod;
8226 
InsertModeController(@onNull TextView textView)8227         InsertModeController(@NonNull TextView textView) {
8228             mTextView = Objects.requireNonNull(textView);
8229             mIsInsertModeActive = false;
8230             mInsertModeTransformationMethod = null;
8231             mHighlightPaint = new Paint();
8232             mHighlightPath = new Path();
8233 
8234             // Insert mode highlight color is 20% opacity of the default text color.
8235             int color = mTextView.getTextColors().getDefaultColor();
8236             color = ColorUtils.setAlphaComponent(color, (int) (0.2f * Color.alpha(color)));
8237             mHighlightPaint.setColor(color);
8238         }
8239 
8240         /**
8241          * Enter insert mode.
8242          * @param offset the index to set the cursor.
8243          * @return true if the call is successful. false if a) it's already in the insert mode,
8244          * b) it failed to enter the insert mode.
8245          */
enterInsertMode(int offset)8246         boolean enterInsertMode(int offset) {
8247             if (mIsInsertModeActive) return false;
8248 
8249             TransformationMethod oldTransformationMethod =
8250                     mTextView.getTransformationMethod();
8251             if (oldTransformationMethod instanceof OffsetMapping) {
8252                 // We can't support the case where the oldTransformationMethod is an OffsetMapping.
8253                 return false;
8254             }
8255 
8256             final boolean isSingleLine = mTextView.isSingleLine();
8257             mInsertModeTransformationMethod = new InsertModeTransformationMethod(offset,
8258                     isSingleLine, oldTransformationMethod);
8259             setTransformationMethod(mInsertModeTransformationMethod, true);
8260             Selection.setSelection((Spannable) mTextView.getText(), offset);
8261 
8262             mIsInsertModeActive = true;
8263             return true;
8264         }
8265 
exitInsertMode()8266         void exitInsertMode() {
8267             exitInsertMode(true);
8268         }
8269 
exitInsertMode(boolean updateText)8270         void exitInsertMode(boolean updateText) {
8271             if (!mIsInsertModeActive) return;
8272             if (mInsertModeTransformationMethod == null
8273                     || mInsertModeTransformationMethod != mTextView.getTransformationMethod()) {
8274                 mIsInsertModeActive = false;
8275                 return;
8276             }
8277             // Changing TransformationMethod will reset selection range to [0, 0), we need to
8278             // manually restore the old selection range.
8279             final int selectionStart = mTextView.getSelectionStart();
8280             final int selectionEnd = mTextView.getSelectionEnd();
8281             final TransformationMethod oldTransformationMethod =
8282                     mInsertModeTransformationMethod.getOldTransformationMethod();
8283             setTransformationMethod(oldTransformationMethod, updateText);
8284             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
8285             mIsInsertModeActive = false;
8286         }
8287 
onDraw(Canvas canvas)8288         void onDraw(Canvas canvas) {
8289             if (!mIsInsertModeActive) return;
8290             final CharSequence transformedText = mTextView.getTransformed();
8291             if (transformedText instanceof InsertModeTransformationMethod.TransformedText) {
8292                 final Layout layout = mTextView.getLayout();
8293                 if (layout == null) return;
8294                 final InsertModeTransformationMethod.TransformedText insertModeTransformedText =
8295                         ((InsertModeTransformationMethod.TransformedText) transformedText);
8296                 final int highlightStart = insertModeTransformedText.getHighlightStart();
8297                 final int highlightEnd = insertModeTransformedText.getHighlightEnd();
8298                 layout.getSelectionPath(highlightStart, highlightEnd, mHighlightPath);
8299                 canvas.drawPath(mHighlightPath, mHighlightPaint);
8300             }
8301         }
8302 
8303         /**
8304          * Update the TransformationMethod on the {@link TextView}.
8305          * @param method the new method to be set on the {@link TextView}/
8306          * @param updateText whether to update the text during setTransformationMethod call.
8307          */
setTransformationMethod(TransformationMethod method, boolean updateText)8308         private void setTransformationMethod(TransformationMethod method, boolean updateText) {
8309             mUpdatingTransformationMethod = true;
8310             mTextView.setTransformationMethodInternal(method, updateText);
8311             mUpdatingTransformationMethod = false;
8312         }
8313 
8314         /**
8315          * Notify the InsertMode controller that the {@link TextView} is about to set its text.
8316          */
beforeSetText()8317         void beforeSetText() {
8318             // TextView#setText is called because our call to
8319             // TextView#setTransformationMethodInternal in enterInsertMode(), exitInsertMode() or
8320             // updateTransformationMethod().
8321             // Do nothing in this case.
8322             if (mUpdatingTransformationMethod) {
8323                 return;
8324             }
8325             // TextView#setText is called externally. Exit InsertMode but don't update text again
8326             // when calling setTransformationMethod.
8327             exitInsertMode(/* updateText */ false);
8328         }
8329 
8330         /**
8331          * Notify the {@link InsertModeController} that TextView#setTransformationMethod is called.
8332          * If it's not in the insert mode, the given transformation method is directly set to the
8333          * TextView. Otherwise, it will wrap the given transformation method with an
8334          * {@link InsertModeTransformationMethod} and then set it on the TextView.
8335          *
8336          * @param transformationMethod the new {@link TransformationMethod} to be set on the
8337          *                             TextView.
8338          */
updateTransformationMethod(TransformationMethod transformationMethod)8339         void updateTransformationMethod(TransformationMethod transformationMethod) {
8340             if (!mIsInsertModeActive) {
8341                 setTransformationMethod(transformationMethod, /* updateText */ true);
8342                 return;
8343             }
8344 
8345             // Changing TransformationMethod will reset selection range to [0, 0), we need to
8346             // manually restore the old selection range.
8347             final int selectionStart = mTextView.getSelectionStart();
8348             final int selectionEnd = mTextView.getSelectionEnd();
8349             mInsertModeTransformationMethod = mInsertModeTransformationMethod.update(
8350                     transformationMethod, mTextView.isSingleLine());
8351             setTransformationMethod(mInsertModeTransformationMethod, /* updateText */ true);
8352             Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
8353         }
8354     }
8355 
enterInsertMode(int offset)8356     boolean enterInsertMode(int offset) {
8357         if (mInsertModeController == null) {
8358             if (mTextView == null) return false;
8359             mInsertModeController = new InsertModeController(mTextView);
8360         }
8361         return mInsertModeController.enterInsertMode(offset);
8362     }
8363 
8364     /**
8365      * Exit insert mode if this editor is in insert mode.
8366      */
exitInsertMode()8367     void exitInsertMode() {
8368         if (mInsertModeController == null) return;
8369         mInsertModeController.exitInsertMode();
8370     }
8371 
8372     /**
8373      * Called by the {@link TextView} when the {@link TransformationMethod} is updated.
8374      *
8375      * @param method the {@link TransformationMethod} to be set on the TextView.
8376      */
setTransformationMethod(TransformationMethod method)8377     void setTransformationMethod(TransformationMethod method) {
8378         if (mInsertModeController == null) {
8379             mTextView.setTransformationMethodInternal(method, /* updateText */ true);
8380             return;
8381         }
8382         mInsertModeController.updateTransformationMethod(method);
8383     }
8384 
8385     /**
8386      * Notify that the Editor that the associated {@link TextView} is about to set its text.
8387      */
beforeSetText()8388     void beforeSetText() {
8389         if (mInsertModeController == null) return;
8390         mInsertModeController.beforeSetText();
8391     }
8392 
8393     /**
8394      * Initializes the nodeInfo with smart actions.
8395      */
onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8396     void onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
8397         mA11ySmartActions.onInitializeAccessibilityNodeInfo(nodeInfo);
8398     }
8399 
8400     /**
8401      * Handles the accessibility action if it is an active smart action.
8402      * Return false if this method does not hanle the action.
8403      */
performSmartActionsAccessibilityAction(int actionId)8404     boolean performSmartActionsAccessibilityAction(int actionId) {
8405         return mA11ySmartActions.performAccessibilityAction(actionId);
8406     }
8407 
logCursor(String location, @Nullable String msgFormat, Object ... msgArgs)8408     static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) {
8409         if (msgFormat == null) {
8410             Log.d(TAG, location);
8411         } else {
8412             Log.d(TAG, location + ": " + String.format(msgFormat, msgArgs));
8413         }
8414     }
8415 }
8416