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