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