Editor.java revision 520969191b37ddfca441b8065543fd0dee7dd74e
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        protected final void updateMagnifier(@NonNull final MotionEvent event) {
4828            if (mMagnifierAnimator == null) {
4829                return;
4830            }
4831
4832            final PointF showPosInView = new PointF();
4833            final boolean shouldShow = obtainMagnifierShowCoordinates(event, showPosInView);
4834            if (shouldShow) {
4835                // Make the cursor visible and stop blinking.
4836                mRenderCursorRegardlessTiming = true;
4837                mTextView.invalidateCursorPath();
4838                suspendBlink();
4839                mMagnifierAnimator.show(showPosInView.x, showPosInView.y);
4840            } else {
4841                dismissMagnifier();
4842            }
4843        }
4844
4845        protected final void dismissMagnifier() {
4846            if (mMagnifierAnimator != null) {
4847                mMagnifierAnimator.dismiss();
4848                mRenderCursorRegardlessTiming = false;
4849                resumeBlink();
4850            }
4851        }
4852
4853        @Override
4854        public boolean onTouchEvent(MotionEvent ev) {
4855            updateFloatingToolbarVisibility(ev);
4856
4857            switch (ev.getActionMasked()) {
4858                case MotionEvent.ACTION_DOWN: {
4859                    startTouchUpFilter(getCurrentCursorOffset());
4860
4861                    final PositionListener positionListener = getPositionListener();
4862                    mLastParentX = positionListener.getPositionX();
4863                    mLastParentY = positionListener.getPositionY();
4864                    mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4865                    mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4866
4867                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4868                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4869                    mTouchToWindowOffsetX = xInWindow - mPositionX;
4870                    mTouchToWindowOffsetY = yInWindow - mPositionY;
4871
4872                    mIsDragging = true;
4873                    mPreviousLineTouched = UNSET_LINE;
4874                    break;
4875                }
4876
4877                case MotionEvent.ACTION_MOVE: {
4878                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4879                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4880
4881                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4882                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
4883                    final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
4884                    float newVerticalOffset;
4885                    if (previousVerticalOffset < mIdealVerticalOffset) {
4886                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4887                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4888                    } else {
4889                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4890                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4891                    }
4892                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4893
4894                    final float newPosX =
4895                            xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4896                    final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
4897
4898                    updatePosition(newPosX, newPosY,
4899                            ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
4900                    break;
4901                }
4902
4903                case MotionEvent.ACTION_UP:
4904                    filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
4905                    // Fall through.
4906                case MotionEvent.ACTION_CANCEL:
4907                    mIsDragging = false;
4908                    updateDrawable();
4909                    break;
4910            }
4911            return true;
4912        }
4913
4914        public boolean isDragging() {
4915            return mIsDragging;
4916        }
4917
4918        void onHandleMoved() {}
4919
4920        public void onDetached() {}
4921    }
4922
4923    private class InsertionHandleView extends HandleView {
4924        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4925        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4926
4927        // Used to detect taps on the insertion handle, which will affect the insertion action mode
4928        private float mDownPositionX, mDownPositionY;
4929        private Runnable mHider;
4930
4931        public InsertionHandleView(Drawable drawable) {
4932            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
4933        }
4934
4935        @Override
4936        public void show() {
4937            super.show();
4938
4939            final long durationSinceCutOrCopy =
4940                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
4941
4942            // Cancel the single tap delayed runnable.
4943            if (mInsertionActionModeRunnable != null
4944                    && ((mTapState == TAP_STATE_DOUBLE_TAP)
4945                            || (mTapState == TAP_STATE_TRIPLE_CLICK)
4946                            || isCursorInsideEasyCorrectionSpan())) {
4947                mTextView.removeCallbacks(mInsertionActionModeRunnable);
4948            }
4949
4950            // Prepare and schedule the single tap runnable to run exactly after the double tap
4951            // timeout has passed.
4952            if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4953                    && !isCursorInsideEasyCorrectionSpan()
4954                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
4955                if (mTextActionMode == null) {
4956                    if (mInsertionActionModeRunnable == null) {
4957                        mInsertionActionModeRunnable = new Runnable() {
4958                            @Override
4959                            public void run() {
4960                                startInsertionActionMode();
4961                            }
4962                        };
4963                    }
4964                    mTextView.postDelayed(
4965                            mInsertionActionModeRunnable,
4966                            ViewConfiguration.getDoubleTapTimeout() + 1);
4967                }
4968
4969            }
4970
4971            hideAfterDelay();
4972        }
4973
4974        private void hideAfterDelay() {
4975            if (mHider == null) {
4976                mHider = new Runnable() {
4977                    public void run() {
4978                        hide();
4979                    }
4980                };
4981            } else {
4982                removeHiderCallback();
4983            }
4984            mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4985        }
4986
4987        private void removeHiderCallback() {
4988            if (mHider != null) {
4989                mTextView.removeCallbacks(mHider);
4990            }
4991        }
4992
4993        @Override
4994        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4995            return drawable.getIntrinsicWidth() / 2;
4996        }
4997
4998        @Override
4999        protected int getHorizontalGravity(boolean isRtlRun) {
5000            return Gravity.CENTER_HORIZONTAL;
5001        }
5002
5003        @Override
5004        protected int getCursorOffset() {
5005            int offset = super.getCursorOffset();
5006            if (mDrawableForCursor != null) {
5007                mDrawableForCursor.getPadding(mTempRect);
5008                offset += (mDrawableForCursor.getIntrinsicWidth()
5009                           - mTempRect.left - mTempRect.right) / 2;
5010            }
5011            return offset;
5012        }
5013
5014        @Override
5015        int getCursorHorizontalPosition(Layout layout, int offset) {
5016            if (mDrawableForCursor != null) {
5017                final float horizontal = getHorizontal(layout, offset);
5018                return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left;
5019            }
5020            return super.getCursorHorizontalPosition(layout, offset);
5021        }
5022
5023        @Override
5024        public boolean onTouchEvent(MotionEvent ev) {
5025            final boolean result = super.onTouchEvent(ev);
5026
5027            switch (ev.getActionMasked()) {
5028                case MotionEvent.ACTION_DOWN:
5029                    mDownPositionX = ev.getRawX();
5030                    mDownPositionY = ev.getRawY();
5031                    updateMagnifier(ev);
5032                    break;
5033
5034                case MotionEvent.ACTION_MOVE:
5035                    updateMagnifier(ev);
5036                    break;
5037
5038                case MotionEvent.ACTION_UP:
5039                    if (!offsetHasBeenChanged()) {
5040                        final float deltaX = mDownPositionX - ev.getRawX();
5041                        final float deltaY = mDownPositionY - ev.getRawY();
5042                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5043
5044                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
5045                                mTextView.getContext());
5046                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
5047
5048                        if (distanceSquared < touchSlop * touchSlop) {
5049                            // Tapping on the handle toggles the insertion action mode.
5050                            if (mTextActionMode != null) {
5051                                stopTextActionMode();
5052                            } else {
5053                                startInsertionActionMode();
5054                            }
5055                        }
5056                    } else {
5057                        if (mTextActionMode != null) {
5058                            mTextActionMode.invalidateContentRect();
5059                        }
5060                    }
5061                    // Fall through.
5062                case MotionEvent.ACTION_CANCEL:
5063                    hideAfterDelay();
5064                    dismissMagnifier();
5065                    break;
5066
5067                default:
5068                    break;
5069            }
5070
5071            return result;
5072        }
5073
5074        @Override
5075        public int getCurrentCursorOffset() {
5076            return mTextView.getSelectionStart();
5077        }
5078
5079        @Override
5080        public void updateSelection(int offset) {
5081            Selection.setSelection((Spannable) mTextView.getText(), offset);
5082        }
5083
5084        @Override
5085        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
5086            Layout layout = mTextView.getLayout();
5087            int offset;
5088            if (layout != null) {
5089                if (mPreviousLineTouched == UNSET_LINE) {
5090                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5091                }
5092                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
5093                offset = getOffsetAtCoordinate(layout, currLine, x);
5094                mPreviousLineTouched = currLine;
5095            } else {
5096                offset = -1;
5097            }
5098            positionAtCursorOffset(offset, false, fromTouchScreen);
5099            if (mTextActionMode != null) {
5100                invalidateActionMode();
5101            }
5102        }
5103
5104        @Override
5105        void onHandleMoved() {
5106            super.onHandleMoved();
5107            removeHiderCallback();
5108        }
5109
5110        @Override
5111        public void onDetached() {
5112            super.onDetached();
5113            removeHiderCallback();
5114        }
5115
5116        @Override
5117        @MagnifierHandleTrigger
5118        protected int getMagnifierHandleTrigger() {
5119            return MagnifierHandleTrigger.INSERTION;
5120        }
5121    }
5122
5123    @Retention(RetentionPolicy.SOURCE)
5124    @IntDef(prefix = { "HANDLE_TYPE_" }, value = {
5125            HANDLE_TYPE_SELECTION_START,
5126            HANDLE_TYPE_SELECTION_END
5127    })
5128    public @interface HandleType {}
5129    public static final int HANDLE_TYPE_SELECTION_START = 0;
5130    public static final int HANDLE_TYPE_SELECTION_END = 1;
5131
5132    /** For selection handles */
5133    @VisibleForTesting
5134    public final class SelectionHandleView extends HandleView {
5135        // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
5136        // end (HANDLE_TYPE_SELECTION_END).
5137        @HandleType
5138        private final int mHandleType;
5139        // Indicates whether the cursor is making adjustments within a word.
5140        private boolean mInWord = false;
5141        // Difference between touch position and word boundary position.
5142        private float mTouchWordDelta;
5143        // X value of the previous updatePosition call.
5144        private float mPrevX;
5145        // Indicates if the handle has moved a boundary between LTR and RTL text.
5146        private boolean mLanguageDirectionChanged = false;
5147        // Distance from edge of horizontally scrolling text view
5148        // to use to switch to character mode.
5149        private final float mTextViewEdgeSlop;
5150        // Used to save text view location.
5151        private final int[] mTextViewLocation = new int[2];
5152
5153        public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
5154                @HandleType int handleType) {
5155            super(drawableLtr, drawableRtl, id);
5156            mHandleType = handleType;
5157            ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
5158            mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
5159        }
5160
5161        private boolean isStartHandle() {
5162            return mHandleType == HANDLE_TYPE_SELECTION_START;
5163        }
5164
5165        @Override
5166        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
5167            if (isRtlRun == isStartHandle()) {
5168                return drawable.getIntrinsicWidth() / 4;
5169            } else {
5170                return (drawable.getIntrinsicWidth() * 3) / 4;
5171            }
5172        }
5173
5174        @Override
5175        protected int getHorizontalGravity(boolean isRtlRun) {
5176            return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
5177        }
5178
5179        @Override
5180        public int getCurrentCursorOffset() {
5181            return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
5182        }
5183
5184        @Override
5185        protected void updateSelection(int offset) {
5186            if (isStartHandle()) {
5187                Selection.setSelection((Spannable) mTextView.getText(), offset,
5188                        mTextView.getSelectionEnd());
5189            } else {
5190                Selection.setSelection((Spannable) mTextView.getText(),
5191                        mTextView.getSelectionStart(), offset);
5192            }
5193            updateDrawable();
5194            if (mTextActionMode != null) {
5195                invalidateActionMode();
5196            }
5197        }
5198
5199        @Override
5200        protected void updatePosition(float x, float y, boolean fromTouchScreen) {
5201            final Layout layout = mTextView.getLayout();
5202            if (layout == null) {
5203                // HandleView will deal appropriately in positionAtCursorOffset when
5204                // layout is null.
5205                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y),
5206                        fromTouchScreen);
5207                return;
5208            }
5209
5210            if (mPreviousLineTouched == UNSET_LINE) {
5211                mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
5212            }
5213
5214            boolean positionCursor = false;
5215            final int anotherHandleOffset =
5216                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5217            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
5218            int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
5219
5220            if (isStartHandle() && initialOffset >= anotherHandleOffset
5221                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
5222                // Handles have crossed, bound it to the first selected line and
5223                // adjust by word / char as normal.
5224                currLine = layout.getLineForOffset(anotherHandleOffset);
5225                initialOffset = getOffsetAtCoordinate(layout, currLine, x);
5226            }
5227
5228            int offset = initialOffset;
5229            final int wordEnd = getWordEnd(offset);
5230            final int wordStart = getWordStart(offset);
5231
5232            if (mPrevX == UNSET_X_VALUE) {
5233                mPrevX = x;
5234            }
5235
5236            final int currentOffset = getCurrentCursorOffset();
5237            final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
5238            final boolean atRtl = isAtRtlRun(layout, offset);
5239            final boolean isLvlBoundary = layout.isLevelBoundary(offset);
5240
5241            // We can't determine if the user is expanding or shrinking the selection if they're
5242            // on a bi-di boundary, so until they've moved past the boundary we'll just place
5243            // the cursor at the current position.
5244            if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
5245                // We're on a boundary or this is the first direction change -- just update
5246                // to the current position.
5247                mLanguageDirectionChanged = true;
5248                mTouchWordDelta = 0.0f;
5249                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
5250                return;
5251            } else if (mLanguageDirectionChanged && !isLvlBoundary) {
5252                // We've just moved past the boundary so update the position. After this we can
5253                // figure out if the user is expanding or shrinking to go by word or character.
5254                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
5255                mTouchWordDelta = 0.0f;
5256                mLanguageDirectionChanged = false;
5257                return;
5258            }
5259
5260            boolean isExpanding;
5261            final float xDiff = x - mPrevX;
5262            if (isStartHandle()) {
5263                isExpanding = currLine < mPreviousLineTouched;
5264            } else {
5265                isExpanding = currLine > mPreviousLineTouched;
5266            }
5267            if (atRtl == isStartHandle()) {
5268                isExpanding |= xDiff > 0;
5269            } else {
5270                isExpanding |= xDiff < 0;
5271            }
5272
5273            if (mTextView.getHorizontallyScrolling()) {
5274                if (positionNearEdgeOfScrollingView(x, atRtl)
5275                        && ((isStartHandle() && mTextView.getScrollX() != 0)
5276                                || (!isStartHandle()
5277                                        && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
5278                        && ((isExpanding && ((isStartHandle() && offset < currentOffset)
5279                                || (!isStartHandle() && offset > currentOffset)))
5280                                        || !isExpanding)) {
5281                    // If we're expanding ensure that the offset is actually expanding compared to
5282                    // the current offset, if the handle snapped to the word, the finger position
5283                    // may be out of sync and we don't want the selection to jump back.
5284                    mTouchWordDelta = 0.0f;
5285                    final int nextOffset = (atRtl == isStartHandle())
5286                            ? layout.getOffsetToRightOf(mPreviousOffset)
5287                            : layout.getOffsetToLeftOf(mPreviousOffset);
5288                    positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen);
5289                    return;
5290                }
5291            }
5292
5293            if (isExpanding) {
5294                // User is increasing the selection.
5295                int wordBoundary = isStartHandle() ? wordStart : wordEnd;
5296                final boolean snapToWord = (!mInWord
5297                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
5298                                && atRtl == isAtRtlRun(layout, wordBoundary);
5299                if (snapToWord) {
5300                    // Sometimes words can be broken across lines (Chinese, hyphenation).
5301                    // We still snap to the word boundary but we only use the letters on the
5302                    // current line to determine if the user is far enough into the word to snap.
5303                    if (layout.getLineForOffset(wordBoundary) != currLine) {
5304                        wordBoundary = isStartHandle()
5305                                ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
5306                    }
5307                    final int offsetThresholdToSnap = isStartHandle()
5308                            ? wordEnd - ((wordEnd - wordBoundary) / 2)
5309                            : wordStart + ((wordBoundary - wordStart) / 2);
5310                    if (isStartHandle()
5311                            && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
5312                        // User is far enough into the word or on a different line so we expand by
5313                        // word.
5314                        offset = wordStart;
5315                    } else if (!isStartHandle()
5316                            && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
5317                        // User is far enough into the word or on a different line so we expand by
5318                        // word.
5319                        offset = wordEnd;
5320                    } else {
5321                        offset = mPreviousOffset;
5322                    }
5323                }
5324                if ((isStartHandle() && offset < initialOffset)
5325                        || (!isStartHandle() && offset > initialOffset)) {
5326                    final float adjustedX = getHorizontal(layout, offset);
5327                    mTouchWordDelta =
5328                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5329                } else {
5330                    mTouchWordDelta = 0.0f;
5331                }
5332                positionCursor = true;
5333            } else {
5334                final int adjustedOffset =
5335                        getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
5336                final boolean shrinking = isStartHandle()
5337                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
5338                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
5339                if (shrinking) {
5340                    // User is shrinking the selection.
5341                    if (currLine != mPrevLine) {
5342                        // We're on a different line, so we'll snap to word boundaries.
5343                        offset = isStartHandle() ? wordStart : wordEnd;
5344                        if ((isStartHandle() && offset < initialOffset)
5345                                || (!isStartHandle() && offset > initialOffset)) {
5346                            final float adjustedX = getHorizontal(layout, offset);
5347                            mTouchWordDelta =
5348                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
5349                        } else {
5350                            mTouchWordDelta = 0.0f;
5351                        }
5352                    } else {
5353                        offset = adjustedOffset;
5354                    }
5355                    positionCursor = true;
5356                } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
5357                        || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
5358                    // Handle has jumped to the word boundary, and the user is moving
5359                    // their finger towards the handle, the delta should be updated.
5360                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
5361                            - getHorizontal(layout, mPreviousOffset);
5362                }
5363            }
5364
5365            if (positionCursor) {
5366                mPreviousLineTouched = currLine;
5367                positionAndAdjustForCrossingHandles(offset, fromTouchScreen);
5368            }
5369            mPrevX = x;
5370        }
5371
5372        @Override
5373        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition,
5374                boolean fromTouchScreen) {
5375            super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen);
5376            mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
5377        }
5378
5379        @Override
5380        public boolean onTouchEvent(MotionEvent event) {
5381            boolean superResult = super.onTouchEvent(event);
5382
5383            switch (event.getActionMasked()) {
5384                case MotionEvent.ACTION_DOWN:
5385                    // Reset the touch word offset and x value when the user
5386                    // re-engages the handle.
5387                    mTouchWordDelta = 0.0f;
5388                    mPrevX = UNSET_X_VALUE;
5389                    updateMagnifier(event);
5390                    break;
5391
5392                case MotionEvent.ACTION_MOVE:
5393                    updateMagnifier(event);
5394                    break;
5395
5396                case MotionEvent.ACTION_UP:
5397                case MotionEvent.ACTION_CANCEL:
5398                    dismissMagnifier();
5399                    break;
5400            }
5401
5402            return superResult;
5403        }
5404
5405        private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) {
5406            final int anotherHandleOffset =
5407                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5408            if ((isStartHandle() && offset >= anotherHandleOffset)
5409                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
5410                mTouchWordDelta = 0.0f;
5411                final Layout layout = mTextView.getLayout();
5412                if (layout != null && offset != anotherHandleOffset) {
5413                    final float horiz = getHorizontal(layout, offset);
5414                    final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5415                            !isStartHandle());
5416                    final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5417                    if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5418                            || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5419                        // This handle passes another one as it crossed a direction boundary.
5420                        // Don't minimize the selection, but keep the handle at the run boundary.
5421                        final int currentOffset = getCurrentCursorOffset();
5422                        final int offsetToGetRunRange = isStartHandle()
5423                                ? currentOffset : Math.max(currentOffset - 1, 0);
5424                        final long range = layout.getRunRange(offsetToGetRunRange);
5425                        if (isStartHandle()) {
5426                            offset = TextUtils.unpackRangeStartFromLong(range);
5427                        } else {
5428                            offset = TextUtils.unpackRangeEndFromLong(range);
5429                        }
5430                        positionAtCursorOffset(offset, false, fromTouchScreen);
5431                        return;
5432                    }
5433                }
5434                // Handles can not cross and selection is at least one character.
5435                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
5436            }
5437            positionAtCursorOffset(offset, false, fromTouchScreen);
5438        }
5439
5440        private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5441            mTextView.getLocationOnScreen(mTextViewLocation);
5442            boolean nearEdge;
5443            if (atRtl == isStartHandle()) {
5444                int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5445                        - mTextView.getPaddingRight();
5446                nearEdge = x > rightEdge - mTextViewEdgeSlop;
5447            } else {
5448                int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5449                nearEdge = x < leftEdge + mTextViewEdgeSlop;
5450            }
5451            return nearEdge;
5452        }
5453
5454        @Override
5455        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5456            final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5457            return layout.isRtlCharAt(offsetToCheck);
5458        }
5459
5460        @Override
5461        public float getHorizontal(@NonNull Layout layout, int offset) {
5462            return getHorizontal(layout, offset, isStartHandle());
5463        }
5464
5465        private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
5466            final int line = layout.getLineForOffset(offset);
5467            final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5468            final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5469            final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5470            return (isRtlChar == isRtlParagraph)
5471                    ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
5472        }
5473
5474        @Override
5475        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
5476            final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5477            final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
5478            if (!layout.isLevelBoundary(primaryOffset)) {
5479                return primaryOffset;
5480            }
5481            final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
5482            final int currentOffset = getCurrentCursorOffset();
5483            final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5484            final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5485            if (primaryDiff < secondaryDiff) {
5486                return primaryOffset;
5487            } else if (primaryDiff > secondaryDiff) {
5488                return secondaryOffset;
5489            } else {
5490                final int offsetToCheck = isStartHandle()
5491                        ? currentOffset : Math.max(currentOffset - 1, 0);
5492                final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5493                final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5494                return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5495            }
5496        }
5497
5498        @MagnifierHandleTrigger
5499        protected int getMagnifierHandleTrigger() {
5500            return isStartHandle()
5501                    ? MagnifierHandleTrigger.SELECTION_START
5502                    : MagnifierHandleTrigger.SELECTION_END;
5503        }
5504    }
5505
5506    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
5507        final int trueLine = mTextView.getLineAtCoordinate(y);
5508        if (layout == null || prevLine > layout.getLineCount()
5509                || layout.getLineCount() <= 0 || prevLine < 0) {
5510            // Invalid parameters, just return whatever line is at y.
5511            return trueLine;
5512        }
5513
5514        if (Math.abs(trueLine - prevLine) >= 2) {
5515            // Only stick to lines if we're within a line of the previous selection.
5516            return trueLine;
5517        }
5518
5519        final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5520        final int lineCount = layout.getLineCount();
5521        final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5522
5523        final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5524        final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5525        final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5526
5527        final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5528        final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5529        final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5530
5531        // Determine if we've moved lines based on y position and previous line.
5532        int currLine;
5533        if (y <= yTopBound) {
5534            currLine = Math.max(prevLine - 1, 0);
5535        } else if (y >= yBottomBound) {
5536            currLine = Math.min(prevLine + 1, lineCount - 1);
5537        } else {
5538            currLine = prevLine;
5539        }
5540        return currLine;
5541    }
5542
5543    /**
5544     * A CursorController instance can be used to control a cursor in the text.
5545     */
5546    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5547        /**
5548         * Makes the cursor controller visible on screen.
5549         * See also {@link #hide()}.
5550         */
5551        public void show();
5552
5553        /**
5554         * Hide the cursor controller from screen.
5555         * See also {@link #show()}.
5556         */
5557        public void hide();
5558
5559        /**
5560         * Called when the view is detached from window. Perform house keeping task, such as
5561         * stopping Runnable thread that would otherwise keep a reference on the context, thus
5562         * preventing the activity from being recycled.
5563         */
5564        public void onDetached();
5565
5566        public boolean isCursorBeingModified();
5567
5568        public boolean isActive();
5569    }
5570
5571    private class InsertionPointCursorController implements CursorController {
5572        private InsertionHandleView mHandle;
5573
5574        public void show() {
5575            getHandle().show();
5576
5577            if (mSelectionModifierCursorController != null) {
5578                mSelectionModifierCursorController.hide();
5579            }
5580        }
5581
5582        public void hide() {
5583            if (mHandle != null) {
5584                mHandle.hide();
5585            }
5586        }
5587
5588        public void onTouchModeChanged(boolean isInTouchMode) {
5589            if (!isInTouchMode) {
5590                hide();
5591            }
5592        }
5593
5594        private InsertionHandleView getHandle() {
5595            if (mSelectHandleCenter == null) {
5596                mSelectHandleCenter = mTextView.getContext().getDrawable(
5597                        mTextView.mTextSelectHandleRes);
5598            }
5599            if (mHandle == null) {
5600                mHandle = new InsertionHandleView(mSelectHandleCenter);
5601            }
5602            return mHandle;
5603        }
5604
5605        @Override
5606        public void onDetached() {
5607            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5608            observer.removeOnTouchModeChangeListener(this);
5609
5610            if (mHandle != null) mHandle.onDetached();
5611        }
5612
5613        @Override
5614        public boolean isCursorBeingModified() {
5615            return mHandle != null && mHandle.isDragging();
5616        }
5617
5618        @Override
5619        public boolean isActive() {
5620            return mHandle != null && mHandle.isShowing();
5621        }
5622
5623        public void invalidateHandle() {
5624            if (mHandle != null) {
5625                mHandle.invalidate();
5626            }
5627        }
5628    }
5629
5630    class SelectionModifierCursorController implements CursorController {
5631        // The cursor controller handles, lazily created when shown.
5632        private SelectionHandleView mStartHandle;
5633        private SelectionHandleView mEndHandle;
5634        // The offsets of that last touch down event. Remembered to start selection there.
5635        private int mMinTouchOffset, mMaxTouchOffset;
5636
5637        private float mDownPositionX, mDownPositionY;
5638        private boolean mGestureStayedInTapRegion;
5639
5640        // Where the user first starts the drag motion.
5641        private int mStartOffset = -1;
5642
5643        private boolean mHaventMovedEnoughToStartDrag;
5644        // The line that a selection happened most recently with the drag accelerator.
5645        private int mLineSelectionIsOn = -1;
5646        // Whether the drag accelerator has selected past the initial line.
5647        private boolean mSwitchedLines = false;
5648
5649        // Indicates the drag accelerator mode that the user is currently using.
5650        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5651        // Drag accelerator is inactive.
5652        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5653        // Character based selection by dragging. Only for mouse.
5654        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5655        // Word based selection by dragging. Enabled after long pressing or double tapping.
5656        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
5657        // Paragraph based selection by dragging. Enabled after mouse triple click.
5658        private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
5659
5660        SelectionModifierCursorController() {
5661            resetTouchOffsets();
5662        }
5663
5664        public void show() {
5665            if (mTextView.isInBatchEditMode()) {
5666                return;
5667            }
5668            initDrawables();
5669            initHandles();
5670        }
5671
5672        private void initDrawables() {
5673            if (mSelectHandleLeft == null) {
5674                mSelectHandleLeft = mTextView.getContext().getDrawable(
5675                        mTextView.mTextSelectHandleLeftRes);
5676            }
5677            if (mSelectHandleRight == null) {
5678                mSelectHandleRight = mTextView.getContext().getDrawable(
5679                        mTextView.mTextSelectHandleRightRes);
5680            }
5681        }
5682
5683        private void initHandles() {
5684            // Lazy object creation has to be done before updatePosition() is called.
5685            if (mStartHandle == null) {
5686                mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5687                        com.android.internal.R.id.selection_start_handle,
5688                        HANDLE_TYPE_SELECTION_START);
5689            }
5690            if (mEndHandle == null) {
5691                mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5692                        com.android.internal.R.id.selection_end_handle,
5693                        HANDLE_TYPE_SELECTION_END);
5694            }
5695
5696            mStartHandle.show();
5697            mEndHandle.show();
5698
5699            hideInsertionPointCursorController();
5700        }
5701
5702        public void hide() {
5703            if (mStartHandle != null) mStartHandle.hide();
5704            if (mEndHandle != null) mEndHandle.hide();
5705        }
5706
5707        public void enterDrag(int dragAcceleratorMode) {
5708            // Just need to init the handles / hide insertion cursor.
5709            show();
5710            mDragAcceleratorMode = dragAcceleratorMode;
5711            // Start location of selection.
5712            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5713                    mLastDownPositionY);
5714            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
5715            // Don't show the handles until user has lifted finger.
5716            hide();
5717
5718            // This stops scrolling parents from intercepting the touch event, allowing
5719            // the user to continue dragging across the screen to select text; TextView will
5720            // scroll as necessary.
5721            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
5722            mTextView.cancelLongPress();
5723        }
5724
5725        public void onTouchEvent(MotionEvent event) {
5726            // This is done even when the View does not have focus, so that long presses can start
5727            // selection and tap can move cursor from this tap position.
5728            final float eventX = event.getX();
5729            final float eventY = event.getY();
5730            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5731            switch (event.getActionMasked()) {
5732                case MotionEvent.ACTION_DOWN:
5733                    if (extractedTextModeWillBeStarted()) {
5734                        // Prevent duplicating the selection handles until the mode starts.
5735                        hide();
5736                    } else {
5737                        // Remember finger down position, to be able to start selection from there.
5738                        mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5739                                eventX, eventY);
5740
5741                        // Double tap detection
5742                        if (mGestureStayedInTapRegion) {
5743                            if (mTapState == TAP_STATE_DOUBLE_TAP
5744                                    || mTapState == TAP_STATE_TRIPLE_CLICK) {
5745                                final float deltaX = eventX - mDownPositionX;
5746                                final float deltaY = eventY - mDownPositionY;
5747                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5748
5749                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
5750                                        mTextView.getContext());
5751                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5752                                boolean stayedInArea =
5753                                        distanceSquared < doubleTapSlop * doubleTapSlop;
5754
5755                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
5756                                    if (mTapState == TAP_STATE_DOUBLE_TAP) {
5757                                        selectCurrentWordAndStartDrag();
5758                                    } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
5759                                        selectCurrentParagraphAndStartDrag();
5760                                    }
5761                                    mDiscardNextActionUp = true;
5762                                }
5763                            }
5764                        }
5765
5766                        mDownPositionX = eventX;
5767                        mDownPositionY = eventY;
5768                        mGestureStayedInTapRegion = true;
5769                        mHaventMovedEnoughToStartDrag = true;
5770                    }
5771                    break;
5772
5773                case MotionEvent.ACTION_POINTER_DOWN:
5774                case MotionEvent.ACTION_POINTER_UP:
5775                    // Handle multi-point gestures. Keep min and max offset positions.
5776                    // Only activated for devices that correctly handle multi-touch.
5777                    if (mTextView.getContext().getPackageManager().hasSystemFeature(
5778                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5779                        updateMinAndMaxOffsets(event);
5780                    }
5781                    break;
5782
5783                case MotionEvent.ACTION_MOVE:
5784                    final ViewConfiguration viewConfig = ViewConfiguration.get(
5785                            mTextView.getContext());
5786                    final int touchSlop = viewConfig.getScaledTouchSlop();
5787
5788                    if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5789                        final float deltaX = eventX - mDownPositionX;
5790                        final float deltaY = eventY - mDownPositionY;
5791                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5792
5793                        if (mGestureStayedInTapRegion) {
5794                            int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5795                            mGestureStayedInTapRegion =
5796                                    distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5797                        }
5798                        if (mHaventMovedEnoughToStartDrag) {
5799                            // We don't start dragging until the user has moved enough.
5800                            mHaventMovedEnoughToStartDrag =
5801                                    distanceSquared <= touchSlop * touchSlop;
5802                        }
5803                    }
5804
5805                    if (isMouse && !isDragAcceleratorActive()) {
5806                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
5807                        if (mTextView.hasSelection()
5808                                && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5809                                && offset >= mTextView.getSelectionStart()
5810                                && offset <= mTextView.getSelectionEnd()) {
5811                            startDragAndDrop();
5812                            break;
5813                        }
5814
5815                        if (mStartOffset != offset) {
5816                            // Start character based drag accelerator.
5817                            stopTextActionMode();
5818                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5819                            mDiscardNextActionUp = true;
5820                            mHaventMovedEnoughToStartDrag = false;
5821                        }
5822                    }
5823
5824                    if (mStartHandle != null && mStartHandle.isShowing()) {
5825                        // Don't do the drag if the handles are showing already.
5826                        break;
5827                    }
5828
5829                    updateSelection(event);
5830                    break;
5831
5832                case MotionEvent.ACTION_UP:
5833                    if (!isDragAcceleratorActive()) {
5834                        break;
5835                    }
5836                    updateSelection(event);
5837
5838                    // No longer dragging to select text, let the parent intercept events.
5839                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
5840
5841                    // No longer the first dragging motion, reset.
5842                    resetDragAcceleratorState();
5843
5844                    if (mTextView.hasSelection()) {
5845                        // Drag selection should not be adjusted by the text classifier.
5846                        startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
5847                    }
5848                    break;
5849            }
5850        }
5851
5852        private void updateSelection(MotionEvent event) {
5853            if (mTextView.getLayout() != null) {
5854                switch (mDragAcceleratorMode) {
5855                    case DRAG_ACCELERATOR_MODE_CHARACTER:
5856                        updateCharacterBasedSelection(event);
5857                        break;
5858                    case DRAG_ACCELERATOR_MODE_WORD:
5859                        updateWordBasedSelection(event);
5860                        break;
5861                    case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5862                        updateParagraphBasedSelection(event);
5863                        break;
5864                }
5865            }
5866        }
5867
5868        /**
5869         * If the TextView allows text selection, selects the current paragraph and starts a drag.
5870         *
5871         * @return true if the drag was started.
5872         */
5873        private boolean selectCurrentParagraphAndStartDrag() {
5874            if (mInsertionActionModeRunnable != null) {
5875                mTextView.removeCallbacks(mInsertionActionModeRunnable);
5876            }
5877            stopTextActionMode();
5878            if (!selectCurrentParagraph()) {
5879                return false;
5880            }
5881            enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5882            return true;
5883        }
5884
5885        private void updateCharacterBasedSelection(MotionEvent event) {
5886            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5887            updateSelectionInternal(mStartOffset, offset,
5888                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5889        }
5890
5891        private void updateWordBasedSelection(MotionEvent event) {
5892            if (mHaventMovedEnoughToStartDrag) {
5893                return;
5894            }
5895            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5896            final ViewConfiguration viewConfig = ViewConfiguration.get(
5897                    mTextView.getContext());
5898            final float eventX = event.getX();
5899            final float eventY = event.getY();
5900            final int currLine;
5901            if (isMouse) {
5902                // No need to offset the y coordinate for mouse input.
5903                currLine = mTextView.getLineAtCoordinate(eventY);
5904            } else {
5905                float y = eventY;
5906                if (mSwitchedLines) {
5907                    // Offset the finger by the same vertical offset as the handles.
5908                    // This improves visibility of the content being selected by
5909                    // shifting the finger below the content, this is applied once
5910                    // the user has switched lines.
5911                    final int touchSlop = viewConfig.getScaledTouchSlop();
5912                    final float fingerOffset = (mStartHandle != null)
5913                            ? mStartHandle.getIdealVerticalOffset()
5914                            : touchSlop;
5915                    y = eventY - fingerOffset;
5916                }
5917
5918                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5919                        y);
5920                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5921                    // Break early here, we want to offset the finger position from
5922                    // the selection highlight, once the user moved their finger
5923                    // to a different line we should apply the offset and *not* switch
5924                    // lines until recomputing the position with the finger offset.
5925                    mSwitchedLines = true;
5926                    return;
5927                }
5928            }
5929
5930            int startOffset;
5931            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5932            // Snap to word boundaries.
5933            if (mStartOffset < offset) {
5934                // Expanding with end handle.
5935                offset = getWordEnd(offset);
5936                startOffset = getWordStart(mStartOffset);
5937            } else {
5938                // Expanding with start handle.
5939                offset = getWordStart(offset);
5940                startOffset = getWordEnd(mStartOffset);
5941                if (startOffset == offset) {
5942                    offset = getNextCursorOffset(offset, false);
5943                }
5944            }
5945            mLineSelectionIsOn = currLine;
5946            updateSelectionInternal(startOffset, offset,
5947                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5948        }
5949
5950        private void updateParagraphBasedSelection(MotionEvent event) {
5951            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5952
5953            final int start = Math.min(offset, mStartOffset);
5954            final int end = Math.max(offset, mStartOffset);
5955            final long paragraphsRange = getParagraphsRange(start, end);
5956            final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5957            final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
5958            updateSelectionInternal(selectionStart, selectionEnd,
5959                    event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN));
5960        }
5961
5962        private void updateSelectionInternal(int selectionStart, int selectionEnd,
5963                boolean fromTouchScreen) {
5964            final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled
5965                    && ((mTextView.getSelectionStart() != selectionStart)
5966                            || (mTextView.getSelectionEnd() != selectionEnd));
5967            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
5968            if (performHapticFeedback) {
5969                mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE);
5970            }
5971        }
5972
5973        /**
5974         * @param event
5975         */
5976        private void updateMinAndMaxOffsets(MotionEvent event) {
5977            int pointerCount = event.getPointerCount();
5978            for (int index = 0; index < pointerCount; index++) {
5979                int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5980                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5981                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5982            }
5983        }
5984
5985        public int getMinTouchOffset() {
5986            return mMinTouchOffset;
5987        }
5988
5989        public int getMaxTouchOffset() {
5990            return mMaxTouchOffset;
5991        }
5992
5993        public void resetTouchOffsets() {
5994            mMinTouchOffset = mMaxTouchOffset = -1;
5995            resetDragAcceleratorState();
5996        }
5997
5998        private void resetDragAcceleratorState() {
5999            mStartOffset = -1;
6000            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
6001            mSwitchedLines = false;
6002            final int selectionStart = mTextView.getSelectionStart();
6003            final int selectionEnd = mTextView.getSelectionEnd();
6004            if (selectionStart > selectionEnd) {
6005                Selection.setSelection((Spannable) mTextView.getText(),
6006                        selectionEnd, selectionStart);
6007            }
6008        }
6009
6010        /**
6011         * @return true iff this controller is currently used to move the selection start.
6012         */
6013        public boolean isSelectionStartDragged() {
6014            return mStartHandle != null && mStartHandle.isDragging();
6015        }
6016
6017        @Override
6018        public boolean isCursorBeingModified() {
6019            return isDragAcceleratorActive() || isSelectionStartDragged()
6020                    || (mEndHandle != null && mEndHandle.isDragging());
6021        }
6022
6023        /**
6024         * @return true if the user is selecting text using the drag accelerator.
6025         */
6026        public boolean isDragAcceleratorActive() {
6027            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
6028        }
6029
6030        public void onTouchModeChanged(boolean isInTouchMode) {
6031            if (!isInTouchMode) {
6032                hide();
6033            }
6034        }
6035
6036        @Override
6037        public void onDetached() {
6038            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
6039            observer.removeOnTouchModeChangeListener(this);
6040
6041            if (mStartHandle != null) mStartHandle.onDetached();
6042            if (mEndHandle != null) mEndHandle.onDetached();
6043        }
6044
6045        @Override
6046        public boolean isActive() {
6047            return mStartHandle != null && mStartHandle.isShowing();
6048        }
6049
6050        public void invalidateHandles() {
6051            if (mStartHandle != null) {
6052                mStartHandle.invalidate();
6053            }
6054            if (mEndHandle != null) {
6055                mEndHandle.invalidate();
6056            }
6057        }
6058    }
6059
6060    private class CorrectionHighlighter {
6061        private final Path mPath = new Path();
6062        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
6063        private int mStart, mEnd;
6064        private long mFadingStartTime;
6065        private RectF mTempRectF;
6066        private static final int FADE_OUT_DURATION = 400;
6067
6068        public CorrectionHighlighter() {
6069            mPaint.setCompatibilityScaling(
6070                    mTextView.getResources().getCompatibilityInfo().applicationScale);
6071            mPaint.setStyle(Paint.Style.FILL);
6072        }
6073
6074        public void highlight(CorrectionInfo info) {
6075            mStart = info.getOffset();
6076            mEnd = mStart + info.getNewText().length();
6077            mFadingStartTime = SystemClock.uptimeMillis();
6078
6079            if (mStart < 0 || mEnd < 0) {
6080                stopAnimation();
6081            }
6082        }
6083
6084        public void draw(Canvas canvas, int cursorOffsetVertical) {
6085            if (updatePath() && updatePaint()) {
6086                if (cursorOffsetVertical != 0) {
6087                    canvas.translate(0, cursorOffsetVertical);
6088                }
6089
6090                canvas.drawPath(mPath, mPaint);
6091
6092                if (cursorOffsetVertical != 0) {
6093                    canvas.translate(0, -cursorOffsetVertical);
6094                }
6095                invalidate(true); // TODO invalidate cursor region only
6096            } else {
6097                stopAnimation();
6098                invalidate(false); // TODO invalidate cursor region only
6099            }
6100        }
6101
6102        private boolean updatePaint() {
6103            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
6104            if (duration > FADE_OUT_DURATION) return false;
6105
6106            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
6107            final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
6108            final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
6109                    + ((int) (highlightColorAlpha * coef) << 24);
6110            mPaint.setColor(color);
6111            return true;
6112        }
6113
6114        private boolean updatePath() {
6115            final Layout layout = mTextView.getLayout();
6116            if (layout == null) return false;
6117
6118            // Update in case text is edited while the animation is run
6119            final int length = mTextView.getText().length();
6120            int start = Math.min(length, mStart);
6121            int end = Math.min(length, mEnd);
6122
6123            mPath.reset();
6124            layout.getSelectionPath(start, end, mPath);
6125            return true;
6126        }
6127
6128        private void invalidate(boolean delayed) {
6129            if (mTextView.getLayout() == null) return;
6130
6131            if (mTempRectF == null) mTempRectF = new RectF();
6132            mPath.computeBounds(mTempRectF, false);
6133
6134            int left = mTextView.getCompoundPaddingLeft();
6135            int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
6136
6137            if (delayed) {
6138                mTextView.postInvalidateOnAnimation(
6139                        left + (int) mTempRectF.left, top + (int) mTempRectF.top,
6140                        left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
6141            } else {
6142                mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
6143                        (int) mTempRectF.right, (int) mTempRectF.bottom);
6144            }
6145        }
6146
6147        private void stopAnimation() {
6148            Editor.this.mCorrectionHighlighter = null;
6149        }
6150    }
6151
6152    private static class ErrorPopup extends PopupWindow {
6153        private boolean mAbove = false;
6154        private final TextView mView;
6155        private int mPopupInlineErrorBackgroundId = 0;
6156        private int mPopupInlineErrorAboveBackgroundId = 0;
6157
6158        ErrorPopup(TextView v, int width, int height) {
6159            super(v, width, height);
6160            mView = v;
6161            // Make sure the TextView has a background set as it will be used the first time it is
6162            // shown and positioned. Initialized with below background, which should have
6163            // dimensions identical to the above version for this to work (and is more likely).
6164            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6165                    com.android.internal.R.styleable.Theme_errorMessageBackground);
6166            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
6167        }
6168
6169        void fixDirection(boolean above) {
6170            mAbove = above;
6171
6172            if (above) {
6173                mPopupInlineErrorAboveBackgroundId =
6174                    getResourceId(mPopupInlineErrorAboveBackgroundId,
6175                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
6176            } else {
6177                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
6178                        com.android.internal.R.styleable.Theme_errorMessageBackground);
6179            }
6180
6181            mView.setBackgroundResource(
6182                    above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
6183        }
6184
6185        private int getResourceId(int currentId, int index) {
6186            if (currentId == 0) {
6187                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
6188                        R.styleable.Theme);
6189                currentId = styledAttributes.getResourceId(index, 0);
6190                styledAttributes.recycle();
6191            }
6192            return currentId;
6193        }
6194
6195        @Override
6196        public void update(int x, int y, int w, int h, boolean force) {
6197            super.update(x, y, w, h, force);
6198
6199            boolean above = isAboveAnchor();
6200            if (above != mAbove) {
6201                fixDirection(above);
6202            }
6203        }
6204    }
6205
6206    static class InputContentType {
6207        int imeOptions = EditorInfo.IME_NULL;
6208        String privateImeOptions;
6209        CharSequence imeActionLabel;
6210        int imeActionId;
6211        Bundle extras;
6212        OnEditorActionListener onEditorActionListener;
6213        boolean enterDown;
6214        LocaleList imeHintLocales;
6215    }
6216
6217    static class InputMethodState {
6218        ExtractedTextRequest mExtractedTextRequest;
6219        final ExtractedText mExtractedText = new ExtractedText();
6220        int mBatchEditNesting;
6221        boolean mCursorChanged;
6222        boolean mSelectionModeChanged;
6223        boolean mContentChanged;
6224        int mChangedStart, mChangedEnd, mChangedDelta;
6225    }
6226
6227    /**
6228     * @return True iff (start, end) is a valid range within the text.
6229     */
6230    private static boolean isValidRange(CharSequence text, int start, int end) {
6231        return 0 <= start && start <= end && end <= text.length();
6232    }
6233
6234    @VisibleForTesting
6235    public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
6236        return mSuggestionsPopupWindow;
6237    }
6238
6239    /**
6240     * An InputFilter that monitors text input to maintain undo history. It does not modify the
6241     * text being typed (and hence always returns null from the filter() method).
6242     *
6243     * TODO: Make this span aware.
6244     */
6245    public static class UndoInputFilter implements InputFilter {
6246        private final Editor mEditor;
6247
6248        // Whether the current filter pass is directly caused by an end-user text edit.
6249        private boolean mIsUserEdit;
6250
6251        // Whether the text field is handling an IME composition. Must be parceled in case the user
6252        // rotates the screen during composition.
6253        private boolean mHasComposition;
6254
6255        // Whether the user is expanding or shortening the text
6256        private boolean mExpanding;
6257
6258        // Whether the previous edit operation was in the current batch edit.
6259        private boolean mPreviousOperationWasInSameBatchEdit;
6260
6261        public UndoInputFilter(Editor editor) {
6262            mEditor = editor;
6263        }
6264
6265        public void saveInstanceState(Parcel parcel) {
6266            parcel.writeInt(mIsUserEdit ? 1 : 0);
6267            parcel.writeInt(mHasComposition ? 1 : 0);
6268            parcel.writeInt(mExpanding ? 1 : 0);
6269            parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
6270        }
6271
6272        public void restoreInstanceState(Parcel parcel) {
6273            mIsUserEdit = parcel.readInt() != 0;
6274            mHasComposition = parcel.readInt() != 0;
6275            mExpanding = parcel.readInt() != 0;
6276            mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
6277        }
6278
6279        /**
6280         * Signals that a user-triggered edit is starting.
6281         */
6282        public void beginBatchEdit() {
6283            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
6284            mIsUserEdit = true;
6285        }
6286
6287        public void endBatchEdit() {
6288            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
6289            mIsUserEdit = false;
6290            mPreviousOperationWasInSameBatchEdit = false;
6291        }
6292
6293        @Override
6294        public CharSequence filter(CharSequence source, int start, int end,
6295                Spanned dest, int dstart, int dend) {
6296            if (DEBUG_UNDO) {
6297                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
6298                        + "dest=" + dest + " (" + dstart + "-" + dend + ")");
6299            }
6300
6301            // Check to see if this edit should be tracked for undo.
6302            if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
6303                return null;
6304            }
6305
6306            final boolean hadComposition = mHasComposition;
6307            mHasComposition = isComposition(source);
6308            final boolean wasExpanding = mExpanding;
6309            boolean shouldCreateSeparateState = false;
6310            if ((end - start) != (dend - dstart)) {
6311                mExpanding = (end - start) > (dend - dstart);
6312                if (hadComposition && mExpanding != wasExpanding) {
6313                    shouldCreateSeparateState = true;
6314                }
6315            }
6316
6317            // Handle edit.
6318            handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
6319            return null;
6320        }
6321
6322        void freezeLastEdit() {
6323            mEditor.mUndoManager.beginUpdate("Edit text");
6324            EditOperation lastEdit = getLastEdit();
6325            if (lastEdit != null) {
6326                lastEdit.mFrozen = true;
6327            }
6328            mEditor.mUndoManager.endUpdate();
6329        }
6330
6331        @Retention(RetentionPolicy.SOURCE)
6332        @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = {
6333                MERGE_EDIT_MODE_FORCE_MERGE,
6334                MERGE_EDIT_MODE_NEVER_MERGE,
6335                MERGE_EDIT_MODE_NORMAL
6336        })
6337        private @interface MergeMode {}
6338        private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
6339        private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
6340        /** Use {@link EditOperation#mergeWith} to merge */
6341        private static final int MERGE_EDIT_MODE_NORMAL = 2;
6342
6343        private void handleEdit(CharSequence source, int start, int end,
6344                Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
6345            // An application may install a TextWatcher to provide additional modifications after
6346            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
6347            // string). This results in multiple filter() calls for what the user considers to be
6348            // a single operation. Always undo the whole set of changes in one step.
6349            @MergeMode
6350            final int mergeMode;
6351            if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
6352                mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
6353            } else if (shouldCreateSeparateState) {
6354                mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
6355            } else {
6356                mergeMode = MERGE_EDIT_MODE_NORMAL;
6357            }
6358            // Build a new operation with all the information from this edit.
6359            String newText = TextUtils.substring(source, start, end);
6360            String oldText = TextUtils.substring(dest, dstart, dend);
6361            EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
6362                    mHasComposition);
6363            if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
6364                return;
6365            }
6366            recordEdit(edit, mergeMode);
6367        }
6368
6369        private EditOperation getLastEdit() {
6370            final UndoManager um = mEditor.mUndoManager;
6371            return um.getLastOperation(
6372                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
6373        }
6374        /**
6375         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
6376         * If forceMerge is true then the new edit is always merged.
6377         */
6378        private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
6379            // Fetch the last edit operation and attempt to merge in the new edit.
6380            final UndoManager um = mEditor.mUndoManager;
6381            um.beginUpdate("Edit text");
6382            EditOperation lastEdit = getLastEdit();
6383            if (lastEdit == null) {
6384                // Add this as the first edit.
6385                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
6386                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
6387            } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
6388                // Forced merges take priority because they could be the result of a non-user-edit
6389                // change and this case should not create a new undo operation.
6390                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
6391                lastEdit.forceMergeWith(edit);
6392            } else if (!mIsUserEdit) {
6393                // An application directly modified the Editable outside of a text edit. Treat this
6394                // as a new change and don't attempt to merge.
6395                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
6396                um.commitState(mEditor.mUndoOwner);
6397                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
6398            } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
6399                // Merge succeeded, nothing else to do.
6400                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
6401            } else {
6402                // Could not merge with the last edit, so commit the last edit and add this edit.
6403                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
6404                um.commitState(mEditor.mUndoOwner);
6405                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
6406            }
6407            mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
6408            um.endUpdate();
6409        }
6410
6411        private boolean canUndoEdit(CharSequence source, int start, int end,
6412                Spanned dest, int dstart, int dend) {
6413            if (!mEditor.mAllowUndo) {
6414                if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
6415                return false;
6416            }
6417
6418            if (mEditor.mUndoManager.isInUndo()) {
6419                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
6420                return false;
6421            }
6422
6423            // Text filters run before input operations are applied. However, some input operations
6424            // are invalid and will throw exceptions when applied. This is common in tests. Don't
6425            // attempt to undo invalid operations.
6426            if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
6427                if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
6428                return false;
6429            }
6430
6431            // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6432            // on an input field. Skip no-op changes.
6433            if (start == end && dstart == dend) {
6434                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6435                return false;
6436            }
6437
6438            return true;
6439        }
6440
6441        private static boolean isComposition(CharSequence source) {
6442            if (!(source instanceof Spannable)) {
6443                return false;
6444            }
6445            // This is a composition edit if the source has a non-zero-length composing span.
6446            Spannable text = (Spannable) source;
6447            int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6448            int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6449            return composeBegin < composeEnd;
6450        }
6451
6452        private boolean isInTextWatcher() {
6453            CharSequence text = mEditor.mTextView.getText();
6454            return (text instanceof SpannableStringBuilder)
6455                    && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6456        }
6457    }
6458
6459    /**
6460     * An operation to undo a single "edit" to a text view.
6461     */
6462    public static class EditOperation extends UndoOperation<Editor> {
6463        private static final int TYPE_INSERT = 0;
6464        private static final int TYPE_DELETE = 1;
6465        private static final int TYPE_REPLACE = 2;
6466
6467        private int mType;
6468        private String mOldText;
6469        private String mNewText;
6470        private int mStart;
6471
6472        private int mOldCursorPos;
6473        private int mNewCursorPos;
6474        private boolean mFrozen;
6475        private boolean mIsComposition;
6476
6477        /**
6478         * Constructs an edit operation from a text input operation on editor that replaces the
6479         * oldText starting at dstart with newText.
6480         */
6481        public EditOperation(Editor editor, String oldText, int dstart, String newText,
6482                boolean isComposition) {
6483            super(editor.mUndoOwner);
6484            mOldText = oldText;
6485            mNewText = newText;
6486
6487            // Determine the type of the edit.
6488            if (mNewText.length() > 0 && mOldText.length() == 0) {
6489                mType = TYPE_INSERT;
6490            } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6491                mType = TYPE_DELETE;
6492            } else {
6493                mType = TYPE_REPLACE;
6494            }
6495
6496            mStart = dstart;
6497            // Store cursor data.
6498            mOldCursorPos = editor.mTextView.getSelectionStart();
6499            mNewCursorPos = dstart + mNewText.length();
6500            mIsComposition = isComposition;
6501        }
6502
6503        public EditOperation(Parcel src, ClassLoader loader) {
6504            super(src, loader);
6505            mType = src.readInt();
6506            mOldText = src.readString();
6507            mNewText = src.readString();
6508            mStart = src.readInt();
6509            mOldCursorPos = src.readInt();
6510            mNewCursorPos = src.readInt();
6511            mFrozen = src.readInt() == 1;
6512            mIsComposition = src.readInt() == 1;
6513        }
6514
6515        @Override
6516        public void writeToParcel(Parcel dest, int flags) {
6517            dest.writeInt(mType);
6518            dest.writeString(mOldText);
6519            dest.writeString(mNewText);
6520            dest.writeInt(mStart);
6521            dest.writeInt(mOldCursorPos);
6522            dest.writeInt(mNewCursorPos);
6523            dest.writeInt(mFrozen ? 1 : 0);
6524            dest.writeInt(mIsComposition ? 1 : 0);
6525        }
6526
6527        private int getNewTextEnd() {
6528            return mStart + mNewText.length();
6529        }
6530
6531        private int getOldTextEnd() {
6532            return mStart + mOldText.length();
6533        }
6534
6535        @Override
6536        public void commit() {
6537        }
6538
6539        @Override
6540        public void undo() {
6541            if (DEBUG_UNDO) Log.d(TAG, "undo");
6542            // Remove the new text and insert the old.
6543            Editor editor = getOwnerData();
6544            Editable text = (Editable) editor.mTextView.getText();
6545            modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
6546        }
6547
6548        @Override
6549        public void redo() {
6550            if (DEBUG_UNDO) Log.d(TAG, "redo");
6551            // Remove the old text and insert the new.
6552            Editor editor = getOwnerData();
6553            Editable text = (Editable) editor.mTextView.getText();
6554            modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
6555        }
6556
6557        /**
6558         * Attempts to merge this existing operation with a new edit.
6559         * @param edit The new edit operation.
6560         * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6561         * object unchanged.
6562         */
6563        private boolean mergeWith(EditOperation edit) {
6564            if (DEBUG_UNDO) {
6565                Log.d(TAG, "mergeWith old " + this);
6566                Log.d(TAG, "mergeWith new " + edit);
6567            }
6568
6569            if (mFrozen) {
6570                return false;
6571            }
6572
6573            switch (mType) {
6574                case TYPE_INSERT:
6575                    return mergeInsertWith(edit);
6576                case TYPE_DELETE:
6577                    return mergeDeleteWith(edit);
6578                case TYPE_REPLACE:
6579                    return mergeReplaceWith(edit);
6580                default:
6581                    return false;
6582            }
6583        }
6584
6585        private boolean mergeInsertWith(EditOperation edit) {
6586            if (edit.mType == TYPE_INSERT) {
6587                // Merge insertions that are contiguous even when it's frozen.
6588                if (getNewTextEnd() != edit.mStart) {
6589                    return false;
6590                }
6591                mNewText += edit.mNewText;
6592                mNewCursorPos = edit.mNewCursorPos;
6593                mFrozen = edit.mFrozen;
6594                mIsComposition = edit.mIsComposition;
6595                return true;
6596            }
6597            if (mIsComposition && edit.mType == TYPE_REPLACE
6598                    && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
6599                // Merge insertion with replace as they can be single insertion.
6600                mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6601                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6602                mNewCursorPos = edit.mNewCursorPos;
6603                mIsComposition = edit.mIsComposition;
6604                return true;
6605            }
6606            return false;
6607        }
6608
6609        // TODO: Support forward delete.
6610        private boolean mergeDeleteWith(EditOperation edit) {
6611            // Only merge continuous deletes.
6612            if (edit.mType != TYPE_DELETE) {
6613                return false;
6614            }
6615            // Only merge deletions that are contiguous.
6616            if (mStart != edit.getOldTextEnd()) {
6617                return false;
6618            }
6619            mStart = edit.mStart;
6620            mOldText = edit.mOldText + mOldText;
6621            mNewCursorPos = edit.mNewCursorPos;
6622            mIsComposition = edit.mIsComposition;
6623            return true;
6624        }
6625
6626        private boolean mergeReplaceWith(EditOperation edit) {
6627            if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6628                // Merge with adjacent insert.
6629                mNewText += edit.mNewText;
6630                mNewCursorPos = edit.mNewCursorPos;
6631                return true;
6632            }
6633            if (!mIsComposition) {
6634                return false;
6635            }
6636            if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6637                    && getNewTextEnd() >= edit.getOldTextEnd()) {
6638                // Merge with delete as they can be single operation.
6639                mNewText = mNewText.substring(0, edit.mStart - mStart)
6640                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6641                if (mNewText.isEmpty()) {
6642                    mType = TYPE_DELETE;
6643                }
6644                mNewCursorPos = edit.mNewCursorPos;
6645                mIsComposition = edit.mIsComposition;
6646                return true;
6647            }
6648            if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6649                    && TextUtils.equals(mNewText, edit.mOldText)) {
6650                // Merge with the replace that replaces the same region.
6651                mNewText = edit.mNewText;
6652                mNewCursorPos = edit.mNewCursorPos;
6653                mIsComposition = edit.mIsComposition;
6654                return true;
6655            }
6656            return false;
6657        }
6658
6659        /**
6660         * Forcibly creates a single merged edit operation by simulating the entire text
6661         * contents being replaced.
6662         */
6663        public void forceMergeWith(EditOperation edit) {
6664            if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
6665            if (mergeWith(edit)) {
6666                return;
6667            }
6668            Editor editor = getOwnerData();
6669
6670            // Copy the text of the current field.
6671            // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6672            // but would require two parallel implementations of modifyText() because Editable and
6673            // StringBuilder do not share an interface for replace/delete/insert.
6674            Editable editable = (Editable) editor.mTextView.getText();
6675            Editable originalText = new SpannableStringBuilder(editable.toString());
6676
6677            // Roll back the last operation.
6678            modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
6679
6680            // Clone the text again and apply the new operation.
6681            Editable finalText = new SpannableStringBuilder(editable.toString());
6682            modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6683                    edit.mNewText, edit.mStart, edit.mNewCursorPos);
6684
6685            // Convert this operation into a replace operation.
6686            mType = TYPE_REPLACE;
6687            mNewText = finalText.toString();
6688            mOldText = originalText.toString();
6689            mStart = 0;
6690            mNewCursorPos = edit.mNewCursorPos;
6691            mIsComposition = edit.mIsComposition;
6692            // mOldCursorPos is unchanged.
6693        }
6694
6695        private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6696                CharSequence newText, int newTextInsertAt, int newCursorPos) {
6697            // Apply the edit if it is still valid.
6698            if (isValidRange(text, deleteFrom, deleteTo)
6699                    && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
6700                if (deleteFrom != deleteTo) {
6701                    text.delete(deleteFrom, deleteTo);
6702                }
6703                if (newText.length() != 0) {
6704                    text.insert(newTextInsertAt, newText);
6705                }
6706            }
6707            // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6708            // don't explicitly set it and rely on SpannableStringBuilder to position it.
6709            // TODO: Select all the text that was undone.
6710            if (0 <= newCursorPos && newCursorPos <= text.length()) {
6711                Selection.setSelection(text, newCursorPos);
6712            }
6713        }
6714
6715        private String getTypeString() {
6716            switch (mType) {
6717                case TYPE_INSERT:
6718                    return "insert";
6719                case TYPE_DELETE:
6720                    return "delete";
6721                case TYPE_REPLACE:
6722                    return "replace";
6723                default:
6724                    return "";
6725            }
6726        }
6727
6728        @Override
6729        public String toString() {
6730            return "[mType=" + getTypeString() + ", "
6731                    + "mOldText=" + mOldText + ", "
6732                    + "mNewText=" + mNewText + ", "
6733                    + "mStart=" + mStart + ", "
6734                    + "mOldCursorPos=" + mOldCursorPos + ", "
6735                    + "mNewCursorPos=" + mNewCursorPos + ", "
6736                    + "mFrozen=" + mFrozen + ", "
6737                    + "mIsComposition=" + mIsComposition + "]";
6738        }
6739
6740        public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6741                new Parcelable.ClassLoaderCreator<EditOperation>() {
6742            @Override
6743            public EditOperation createFromParcel(Parcel in) {
6744                return new EditOperation(in, null);
6745            }
6746
6747            @Override
6748            public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6749                return new EditOperation(in, loader);
6750            }
6751
6752            @Override
6753            public EditOperation[] newArray(int size) {
6754                return new EditOperation[size];
6755            }
6756        };
6757    }
6758
6759    /**
6760     * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6761     * These allow external applications to plug into currently selected text.
6762     */
6763    static final class ProcessTextIntentActionsHandler {
6764
6765        private final Editor mEditor;
6766        private final TextView mTextView;
6767        private final Context mContext;
6768        private final PackageManager mPackageManager;
6769        private final String mPackageName;
6770        private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
6771        private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6772                new SparseArray<>();
6773        private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
6774
6775        private ProcessTextIntentActionsHandler(Editor editor) {
6776            mEditor = Preconditions.checkNotNull(editor);
6777            mTextView = Preconditions.checkNotNull(mEditor.mTextView);
6778            mContext = Preconditions.checkNotNull(mTextView.getContext());
6779            mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6780            mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
6781        }
6782
6783        /**
6784         * Adds "PROCESS_TEXT" menu items to the specified menu.
6785         */
6786        public void onInitializeMenu(Menu menu) {
6787            loadSupportedActivities();
6788            final int size = mSupportedActivities.size();
6789            for (int i = 0; i < size; i++) {
6790                final ResolveInfo resolveInfo = mSupportedActivities.get(i);
6791                menu.add(Menu.NONE, Menu.NONE,
6792                        Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i,
6793                        getLabel(resolveInfo))
6794                        .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
6795                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER);
6796            }
6797        }
6798
6799        /**
6800         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6801         * menu item.
6802         *
6803         * @return True if the action was performed, false otherwise.
6804         */
6805        public boolean performMenuItemAction(MenuItem item) {
6806            return fireIntent(item.getIntent());
6807        }
6808
6809        /**
6810         * Initializes and caches "PROCESS_TEXT" accessibility actions.
6811         */
6812        public void initializeAccessibilityActions() {
6813            mAccessibilityIntents.clear();
6814            mAccessibilityActions.clear();
6815            int i = 0;
6816            loadSupportedActivities();
6817            for (ResolveInfo resolveInfo : mSupportedActivities) {
6818                int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6819                mAccessibilityActions.put(
6820                        actionId,
6821                        new AccessibilityNodeInfo.AccessibilityAction(
6822                                actionId, getLabel(resolveInfo)));
6823                mAccessibilityIntents.put(
6824                        actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6825            }
6826        }
6827
6828        /**
6829         * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6830         * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6831         * latest accessibility actions available for this call.
6832         */
6833        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6834            for (int i = 0; i < mAccessibilityActions.size(); i++) {
6835                nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6836            }
6837        }
6838
6839        /**
6840         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6841         * accessibility action id.
6842         *
6843         * @return True if the action was performed, false otherwise.
6844         */
6845        public boolean performAccessibilityAction(int actionId) {
6846            return fireIntent(mAccessibilityIntents.get(actionId));
6847        }
6848
6849        private boolean fireIntent(Intent intent) {
6850            if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
6851                String selectedText = mTextView.getSelectedText();
6852                selectedText = TextUtils.trimToParcelableSize(selectedText);
6853                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText);
6854                mEditor.mPreserveSelection = true;
6855                mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6856                return true;
6857            }
6858            return false;
6859        }
6860
6861        private void loadSupportedActivities() {
6862            mSupportedActivities.clear();
6863            if (!mContext.canStartActivityForResult()) {
6864                return;
6865            }
6866            PackageManager packageManager = mTextView.getContext().getPackageManager();
6867            List<ResolveInfo> unfiltered =
6868                    packageManager.queryIntentActivities(createProcessTextIntent(), 0);
6869            for (ResolveInfo info : unfiltered) {
6870                if (isSupportedActivity(info)) {
6871                    mSupportedActivities.add(info);
6872                }
6873            }
6874        }
6875
6876        private boolean isSupportedActivity(ResolveInfo info) {
6877            return mPackageName.equals(info.activityInfo.packageName)
6878                    || info.activityInfo.exported
6879                            && (info.activityInfo.permission == null
6880                                    || mContext.checkSelfPermission(info.activityInfo.permission)
6881                                            == PackageManager.PERMISSION_GRANTED);
6882        }
6883
6884        private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6885            return createProcessTextIntent()
6886                    .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6887                    .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6888        }
6889
6890        private Intent createProcessTextIntent() {
6891            return new Intent()
6892                    .setAction(Intent.ACTION_PROCESS_TEXT)
6893                    .setType("text/plain");
6894        }
6895
6896        private CharSequence getLabel(ResolveInfo resolveInfo) {
6897            return resolveInfo.loadLabel(mPackageManager);
6898        }
6899    }
6900}
6901