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