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