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