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