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