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