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