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