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