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