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