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