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