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