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