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