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