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