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