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