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