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