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