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