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
2588        stopTextActionModeWithPreservingSelection();
2589        if (mTextView.canSelectText()) {
2590            final boolean isOnSelection = mTextView.hasSelection()
2591                    && offset >= mTextView.getSelectionStart()
2592                    && offset <= mTextView.getSelectionEnd();
2593            if (!isOnSelection) {
2594                // Right clicked position is not on the selection. Remove the selection and move the
2595                // cursor to the right clicked position.
2596                Selection.setSelection((Spannable) mTextView.getText(), offset);
2597                stopTextActionMode();
2598            }
2599        }
2600
2601        if (shouldOfferToShowSuggestions()) {
2602            final SuggestionInfo[] suggestionInfoArray =
2603                    new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2604            for (int i = 0; i < suggestionInfoArray.length; i++) {
2605                suggestionInfoArray[i] = new SuggestionInfo();
2606            }
2607            final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2608                    com.android.internal.R.string.replace);
2609            final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
2610            for (int i = 0; i < numItems; i++) {
2611                final SuggestionInfo info = suggestionInfoArray[i];
2612                subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2613                        .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2614                            @Override
2615                            public boolean onMenuItemClick(MenuItem item) {
2616                                replaceWithSuggestion(info);
2617                                return true;
2618                            }
2619                        });
2620            }
2621        }
2622
2623        menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2624                com.android.internal.R.string.undo)
2625                .setAlphabeticShortcut('z')
2626                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2627                .setEnabled(mTextView.canUndo());
2628        menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2629                com.android.internal.R.string.redo)
2630                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2631                .setEnabled(mTextView.canRedo());
2632
2633        menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2634                com.android.internal.R.string.cut)
2635                .setAlphabeticShortcut('x')
2636                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2637                .setEnabled(mTextView.canCut());
2638        menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2639                com.android.internal.R.string.copy)
2640                .setAlphabeticShortcut('c')
2641                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2642                .setEnabled(mTextView.canCopy());
2643        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2644                com.android.internal.R.string.paste)
2645                .setAlphabeticShortcut('v')
2646                .setEnabled(mTextView.canPaste())
2647                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2648        menu.add(Menu.NONE, TextView.ID_PASTE_AS_PLAIN_TEXT, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
2649                com.android.internal.R.string.paste_as_plain_text)
2650                .setEnabled(mTextView.canPasteAsPlainText())
2651                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2652        menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2653                com.android.internal.R.string.share)
2654                .setEnabled(mTextView.canShare())
2655                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2656        menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2657                com.android.internal.R.string.selectAll)
2658                .setAlphabeticShortcut('a')
2659                .setEnabled(mTextView.canSelectAllText())
2660                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2661        menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
2662                com.android.internal.R.string.autofill)
2663                .setEnabled(mTextView.canRequestAutofill())
2664                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2665
2666        mPreserveSelection = true;
2667    }
2668
2669    @Nullable
2670    private SuggestionSpan findEquivalentSuggestionSpan(
2671            @NonNull SuggestionSpanInfo suggestionSpanInfo) {
2672        final Editable editable = (Editable) mTextView.getText();
2673        if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2674            // Exactly same span is found.
2675            return suggestionSpanInfo.mSuggestionSpan;
2676        }
2677        // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2678        // contents.
2679        final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2680                suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2681        for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2682            final int start = editable.getSpanStart(suggestionSpan);
2683            if (start != suggestionSpanInfo.mSpanStart) {
2684                continue;
2685            }
2686            final int end = editable.getSpanEnd(suggestionSpan);
2687            if (end != suggestionSpanInfo.mSpanEnd) {
2688                continue;
2689            }
2690            if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2691                return suggestionSpan;
2692            }
2693        }
2694        return null;
2695    }
2696
2697    private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2698        final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2699                suggestionInfo.mSuggestionSpanInfo);
2700        if (targetSuggestionSpan == null) {
2701            // Span has been removed
2702            return;
2703        }
2704        final Editable editable = (Editable) mTextView.getText();
2705        final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2706        final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
2707        if (spanStart < 0 || spanEnd <= spanStart) {
2708            // Span has been removed
2709            return;
2710        }
2711
2712        final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2713        // SuggestionSpans are removed by replace: save them before
2714        SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2715                SuggestionSpan.class);
2716        final int length = suggestionSpans.length;
2717        int[] suggestionSpansStarts = new int[length];
2718        int[] suggestionSpansEnds = new int[length];
2719        int[] suggestionSpansFlags = new int[length];
2720        for (int i = 0; i < length; i++) {
2721            final SuggestionSpan suggestionSpan = suggestionSpans[i];
2722            suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2723            suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2724            suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2725
2726            // Remove potential misspelled flags
2727            int suggestionSpanFlags = suggestionSpan.getFlags();
2728            if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2729                suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2730                suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2731                suggestionSpan.setFlags(suggestionSpanFlags);
2732            }
2733        }
2734
2735        // Notify source IME of the suggestion pick. Do this before swapping texts.
2736        targetSuggestionSpan.notifySelection(
2737                mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2738
2739        // Swap text content between actual text and Suggestion span
2740        final int suggestionStart = suggestionInfo.mSuggestionStart;
2741        final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2742        final String suggestion = suggestionInfo.mText.subSequence(
2743                suggestionStart, suggestionEnd).toString();
2744        mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2745
2746        String[] suggestions = targetSuggestionSpan.getSuggestions();
2747        suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2748
2749        // Restore previous SuggestionSpans
2750        final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2751        for (int i = 0; i < length; i++) {
2752            // Only spans that include the modified region make sense after replacement
2753            // Spans partially included in the replaced region are removed, there is no
2754            // way to assign them a valid range after replacement
2755            if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2756                mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2757                        suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2758            }
2759        }
2760        // Move cursor at the end of the replaced word
2761        final int newCursorPosition = spanEnd + lengthDelta;
2762        mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2763    }
2764
2765    private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2766            new MenuItem.OnMenuItemClickListener() {
2767        @Override
2768        public boolean onMenuItemClick(MenuItem item) {
2769            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2770                return true;
2771            }
2772            return mTextView.onTextContextMenuItem(item.getItemId());
2773        }
2774    };
2775
2776    /**
2777     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2778     * pop-up should be displayed.
2779     * Also monitors {@link Selection} to call back to the attached input method.
2780     */
2781    private class SpanController implements SpanWatcher {
2782
2783        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2784
2785        private EasyEditPopupWindow mPopupWindow;
2786
2787        private Runnable mHidePopup;
2788
2789        // This function is pure but inner classes can't have static functions
2790        private boolean isNonIntermediateSelectionSpan(final Spannable text,
2791                final Object span) {
2792            return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2793                    && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2794        }
2795
2796        @Override
2797        public void onSpanAdded(Spannable text, Object span, int start, int end) {
2798            if (isNonIntermediateSelectionSpan(text, span)) {
2799                sendUpdateSelection();
2800            } else if (span instanceof EasyEditSpan) {
2801                if (mPopupWindow == null) {
2802                    mPopupWindow = new EasyEditPopupWindow();
2803                    mHidePopup = new Runnable() {
2804                        @Override
2805                        public void run() {
2806                            hide();
2807                        }
2808                    };
2809                }
2810
2811                // Make sure there is only at most one EasyEditSpan in the text
2812                if (mPopupWindow.mEasyEditSpan != null) {
2813                    mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
2814                }
2815
2816                mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
2817                mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2818                    @Override
2819                    public void onDeleteClick(EasyEditSpan span) {
2820                        Editable editable = (Editable) mTextView.getText();
2821                        int start = editable.getSpanStart(span);
2822                        int end = editable.getSpanEnd(span);
2823                        if (start >= 0 && end >= 0) {
2824                            sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
2825                            mTextView.deleteText_internal(start, end);
2826                        }
2827                        editable.removeSpan(span);
2828                    }
2829                });
2830
2831                if (mTextView.getWindowVisibility() != View.VISIBLE) {
2832                    // The window is not visible yet, ignore the text change.
2833                    return;
2834                }
2835
2836                if (mTextView.getLayout() == null) {
2837                    // The view has not been laid out yet, ignore the text change
2838                    return;
2839                }
2840
2841                if (extractedTextModeWillBeStarted()) {
2842                    // The input is in extract mode. Do not handle the easy edit in
2843                    // the original TextView, as the ExtractEditText will do
2844                    return;
2845                }
2846
2847                mPopupWindow.show();
2848                mTextView.removeCallbacks(mHidePopup);
2849                mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2850            }
2851        }
2852
2853        @Override
2854        public void onSpanRemoved(Spannable text, Object span, int start, int end) {
2855            if (isNonIntermediateSelectionSpan(text, span)) {
2856                sendUpdateSelection();
2857            } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
2858                hide();
2859            }
2860        }
2861
2862        @Override
2863        public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2864                int newStart, int newEnd) {
2865            if (isNonIntermediateSelectionSpan(text, span)) {
2866                sendUpdateSelection();
2867            } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
2868                EasyEditSpan easyEditSpan = (EasyEditSpan) span;
2869                sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
2870                text.removeSpan(easyEditSpan);
2871            }
2872        }
2873
2874        public void hide() {
2875            if (mPopupWindow != null) {
2876                mPopupWindow.hide();
2877                mTextView.removeCallbacks(mHidePopup);
2878            }
2879        }
2880
2881        private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
2882            try {
2883                PendingIntent pendingIntent = span.getPendingIntent();
2884                if (pendingIntent != null) {
2885                    Intent intent = new Intent();
2886                    intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2887                    pendingIntent.send(mTextView.getContext(), 0, intent);
2888                }
2889            } catch (CanceledException e) {
2890                // This should not happen, as we should try to send the intent only once.
2891                Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2892            }
2893        }
2894    }
2895
2896    /**
2897     * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2898     */
2899    private interface EasyEditDeleteListener {
2900
2901        /**
2902         * Clicks the delete pop-up.
2903         */
2904        void onDeleteClick(EasyEditSpan span);
2905    }
2906
2907    /**
2908     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
2909     * by {@link SpanController}.
2910     */
2911    private class EasyEditPopupWindow extends PinnedPopupWindow
2912            implements OnClickListener {
2913        private static final int POPUP_TEXT_LAYOUT =
2914                com.android.internal.R.layout.text_edit_action_popup_text;
2915        private TextView mDeleteTextView;
2916        private EasyEditSpan mEasyEditSpan;
2917        private EasyEditDeleteListener mOnDeleteListener;
2918
2919        @Override
2920        protected void createPopupWindow() {
2921            mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2922                    com.android.internal.R.attr.textSelectHandleWindowStyle);
2923            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2924            mPopupWindow.setClippingEnabled(true);
2925        }
2926
2927        @Override
2928        protected void initContentView() {
2929            LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2930            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2931            mContentView = linearLayout;
2932            mContentView.setBackgroundResource(
2933                    com.android.internal.R.drawable.text_edit_side_paste_window);
2934
2935            LayoutInflater inflater = (LayoutInflater) mTextView.getContext()
2936                    .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2937
2938            LayoutParams wrapContent = new LayoutParams(
2939                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2940
2941            mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2942            mDeleteTextView.setLayoutParams(wrapContent);
2943            mDeleteTextView.setText(com.android.internal.R.string.delete);
2944            mDeleteTextView.setOnClickListener(this);
2945            mContentView.addView(mDeleteTextView);
2946        }
2947
2948        public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
2949            mEasyEditSpan = easyEditSpan;
2950        }
2951
2952        private void setOnDeleteListener(EasyEditDeleteListener listener) {
2953            mOnDeleteListener = listener;
2954        }
2955
2956        @Override
2957        public void onClick(View view) {
2958            if (view == mDeleteTextView
2959                    && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2960                    && mOnDeleteListener != null) {
2961                mOnDeleteListener.onDeleteClick(mEasyEditSpan);
2962            }
2963        }
2964
2965        @Override
2966        public void hide() {
2967            if (mEasyEditSpan != null) {
2968                mEasyEditSpan.setDeleteEnabled(false);
2969            }
2970            mOnDeleteListener = null;
2971            super.hide();
2972        }
2973
2974        @Override
2975        protected int getTextOffset() {
2976            // Place the pop-up at the end of the span
2977            Editable editable = (Editable) mTextView.getText();
2978            return editable.getSpanEnd(mEasyEditSpan);
2979        }
2980
2981        @Override
2982        protected int getVerticalLocalPosition(int line) {
2983            return mTextView.getLayout().getLineBottom(line);
2984        }
2985
2986        @Override
2987        protected int clipVertically(int positionY) {
2988            // As we display the pop-up below the span, no vertical clipping is required.
2989            return positionY;
2990        }
2991    }
2992
2993    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2994        // 3 handles
2995        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
2996        // 1 CursorAnchorInfoNotifier
2997        private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
2998        private TextViewPositionListener[] mPositionListeners =
2999                new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
3000        private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
3001        private boolean mPositionHasChanged = true;
3002        // Absolute position of the TextView with respect to its parent window
3003        private int mPositionX, mPositionY;
3004        private int mPositionXOnScreen, mPositionYOnScreen;
3005        private int mNumberOfListeners;
3006        private boolean mScrollHasChanged;
3007        final int[] mTempCoords = new int[2];
3008
3009        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
3010            if (mNumberOfListeners == 0) {
3011                updatePosition();
3012                ViewTreeObserver vto = mTextView.getViewTreeObserver();
3013                vto.addOnPreDrawListener(this);
3014            }
3015
3016            int emptySlotIndex = -1;
3017            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3018                TextViewPositionListener listener = mPositionListeners[i];
3019                if (listener == positionListener) {
3020                    return;
3021                } else if (emptySlotIndex < 0 && listener == null) {
3022                    emptySlotIndex = i;
3023                }
3024            }
3025
3026            mPositionListeners[emptySlotIndex] = positionListener;
3027            mCanMove[emptySlotIndex] = canMove;
3028            mNumberOfListeners++;
3029        }
3030
3031        public void removeSubscriber(TextViewPositionListener positionListener) {
3032            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3033                if (mPositionListeners[i] == positionListener) {
3034                    mPositionListeners[i] = null;
3035                    mNumberOfListeners--;
3036                    break;
3037                }
3038            }
3039
3040            if (mNumberOfListeners == 0) {
3041                ViewTreeObserver vto = mTextView.getViewTreeObserver();
3042                vto.removeOnPreDrawListener(this);
3043            }
3044        }
3045
3046        public int getPositionX() {
3047            return mPositionX;
3048        }
3049
3050        public int getPositionY() {
3051            return mPositionY;
3052        }
3053
3054        public int getPositionXOnScreen() {
3055            return mPositionXOnScreen;
3056        }
3057
3058        public int getPositionYOnScreen() {
3059            return mPositionYOnScreen;
3060        }
3061
3062        @Override
3063        public boolean onPreDraw() {
3064            updatePosition();
3065
3066            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
3067                if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
3068                    TextViewPositionListener positionListener = mPositionListeners[i];
3069                    if (positionListener != null) {
3070                        positionListener.updatePosition(mPositionX, mPositionY,
3071                                mPositionHasChanged, mScrollHasChanged);
3072                    }
3073                }
3074            }
3075
3076            mScrollHasChanged = false;
3077            return true;
3078        }
3079
3080        private void updatePosition() {
3081            mTextView.getLocationInWindow(mTempCoords);
3082
3083            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
3084
3085            mPositionX = mTempCoords[0];
3086            mPositionY = mTempCoords[1];
3087
3088            mTextView.getLocationOnScreen(mTempCoords);
3089
3090            mPositionXOnScreen = mTempCoords[0];
3091            mPositionYOnScreen = mTempCoords[1];
3092        }
3093
3094        public void onScrollChanged() {
3095            mScrollHasChanged = true;
3096        }
3097    }
3098
3099    private abstract class PinnedPopupWindow implements TextViewPositionListener {
3100        protected PopupWindow mPopupWindow;
3101        protected ViewGroup mContentView;
3102        int mPositionX, mPositionY;
3103        int mClippingLimitLeft, mClippingLimitRight;
3104
3105        protected abstract void createPopupWindow();
3106        protected abstract void initContentView();
3107        protected abstract int getTextOffset();
3108        protected abstract int getVerticalLocalPosition(int line);
3109        protected abstract int clipVertically(int positionY);
3110        protected void setUp() {
3111        }
3112
3113        public PinnedPopupWindow() {
3114            // Due to calling subclass methods in base constructor, subclass constructor is not
3115            // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3116            // a chance to initialize subclasses, call setUp() method here.
3117            // TODO: It is good to extract non trivial initialization code from constructor.
3118            setUp();
3119
3120            createPopupWindow();
3121
3122            mPopupWindow.setWindowLayoutType(
3123                    WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
3124            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3125            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3126
3127            initContentView();
3128
3129            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3130                    ViewGroup.LayoutParams.WRAP_CONTENT);
3131            mContentView.setLayoutParams(wrapContent);
3132
3133            mPopupWindow.setContentView(mContentView);
3134        }
3135
3136        public void show() {
3137            getPositionListener().addSubscriber(this, false /* offset is fixed */);
3138
3139            computeLocalPosition();
3140
3141            final PositionListener positionListener = getPositionListener();
3142            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3143        }
3144
3145        protected void measureContent() {
3146            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3147            mContentView.measure(
3148                    View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3149                            View.MeasureSpec.AT_MOST),
3150                    View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3151                            View.MeasureSpec.AT_MOST));
3152        }
3153
3154        /* The popup window will be horizontally centered on the getTextOffset() and vertically
3155         * positioned according to viewportToContentHorizontalOffset.
3156         *
3157         * This method assumes that mContentView has properly been measured from its content. */
3158        private void computeLocalPosition() {
3159            measureContent();
3160            final int width = mContentView.getMeasuredWidth();
3161            final int offset = getTextOffset();
3162            mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3163            mPositionX += mTextView.viewportToContentHorizontalOffset();
3164
3165            final int line = mTextView.getLayout().getLineForOffset(offset);
3166            mPositionY = getVerticalLocalPosition(line);
3167            mPositionY += mTextView.viewportToContentVerticalOffset();
3168        }
3169
3170        private void updatePosition(int parentPositionX, int parentPositionY) {
3171            int positionX = parentPositionX + mPositionX;
3172            int positionY = parentPositionY + mPositionY;
3173
3174            positionY = clipVertically(positionY);
3175
3176            // Horizontal clipping
3177            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3178            final int width = mContentView.getMeasuredWidth();
3179            positionX = Math.min(
3180                    displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3181            positionX = Math.max(-mClippingLimitLeft, positionX);
3182
3183            if (isShowing()) {
3184                mPopupWindow.update(positionX, positionY, -1, -1);
3185            } else {
3186                mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3187                        positionX, positionY);
3188            }
3189        }
3190
3191        public void hide() {
3192            if (!isShowing()) {
3193                return;
3194            }
3195            mPopupWindow.dismiss();
3196            getPositionListener().removeSubscriber(this);
3197        }
3198
3199        @Override
3200        public void updatePosition(int parentPositionX, int parentPositionY,
3201                boolean parentPositionChanged, boolean parentScrolled) {
3202            // Either parentPositionChanged or parentScrolled is true, check if still visible
3203            if (isShowing() && isOffsetVisible(getTextOffset())) {
3204                if (parentScrolled) computeLocalPosition();
3205                updatePosition(parentPositionX, parentPositionY);
3206            } else {
3207                hide();
3208            }
3209        }
3210
3211        public boolean isShowing() {
3212            return mPopupWindow.isShowing();
3213        }
3214    }
3215
3216    private static final class SuggestionInfo {
3217        // Range of actual suggestion within mText
3218        int mSuggestionStart, mSuggestionEnd;
3219
3220        // The SuggestionSpan that this TextView represents
3221        final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
3222
3223        // The index of this suggestion inside suggestionSpan
3224        int mSuggestionIndex;
3225
3226        final SpannableStringBuilder mText = new SpannableStringBuilder();
3227
3228        void clear() {
3229            mSuggestionSpanInfo.clear();
3230            mText.clear();
3231        }
3232
3233        // Utility method to set attributes about a SuggestionSpan.
3234        void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3235            mSuggestionSpanInfo.mSuggestionSpan = span;
3236            mSuggestionSpanInfo.mSpanStart = spanStart;
3237            mSuggestionSpanInfo.mSpanEnd = spanEnd;
3238        }
3239    }
3240
3241    private static final class SuggestionSpanInfo {
3242        // The SuggestionSpan;
3243        @Nullable
3244        SuggestionSpan mSuggestionSpan;
3245
3246        // The SuggestionSpan start position
3247        int mSpanStart;
3248
3249        // The SuggestionSpan end position
3250        int mSpanEnd;
3251
3252        void clear() {
3253            mSuggestionSpan = null;
3254        }
3255    }
3256
3257    private class SuggestionHelper {
3258        private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3259                new SuggestionSpanComparator();
3260        private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3261                new HashMap<SuggestionSpan, Integer>();
3262
3263        private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3264            public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3265                final int flag1 = span1.getFlags();
3266                final int flag2 = span2.getFlags();
3267                if (flag1 != flag2) {
3268                    // The order here should match what is used in updateDrawState
3269                    final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3270                    final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3271                    final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3272                    final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3273                    if (easy1 && !misspelled1) return -1;
3274                    if (easy2 && !misspelled2) return 1;
3275                    if (misspelled1) return -1;
3276                    if (misspelled2) return 1;
3277                }
3278
3279                return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3280            }
3281        }
3282
3283        /**
3284         * Returns the suggestion spans that cover the current cursor position. The suggestion
3285         * spans are sorted according to the length of text that they are attached to.
3286         */
3287        private SuggestionSpan[] getSortedSuggestionSpans() {
3288            int pos = mTextView.getSelectionStart();
3289            Spannable spannable = (Spannable) mTextView.getText();
3290            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3291
3292            mSpansLengths.clear();
3293            for (SuggestionSpan suggestionSpan : suggestionSpans) {
3294                int start = spannable.getSpanStart(suggestionSpan);
3295                int end = spannable.getSpanEnd(suggestionSpan);
3296                mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3297            }
3298
3299            // The suggestions are sorted according to their types (easy correction first, then
3300            // misspelled) and to the length of the text that they cover (shorter first).
3301            Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3302            mSpansLengths.clear();
3303
3304            return suggestionSpans;
3305        }
3306
3307        /**
3308         * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3309         * position.
3310         *
3311         * @param suggestionInfos SuggestionInfo array the results will be set.
3312         * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
3313         * @return the number of suggestions actually fetched.
3314         */
3315        public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3316                @Nullable SuggestionSpanInfo misspelledSpanInfo) {
3317            final Spannable spannable = (Spannable) mTextView.getText();
3318            final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3319            final int nbSpans = suggestionSpans.length;
3320            if (nbSpans == 0) return 0;
3321
3322            int numberOfSuggestions = 0;
3323            for (final SuggestionSpan suggestionSpan : suggestionSpans) {
3324                final int spanStart = spannable.getSpanStart(suggestionSpan);
3325                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3326
3327                if (misspelledSpanInfo != null
3328                        && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3329                    misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3330                    misspelledSpanInfo.mSpanStart = spanStart;
3331                    misspelledSpanInfo.mSpanEnd = spanEnd;
3332                }
3333
3334                final String[] suggestions = suggestionSpan.getSuggestions();
3335                final int nbSuggestions = suggestions.length;
3336                suggestionLoop:
3337                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3338                    final String suggestion = suggestions[suggestionIndex];
3339                    for (int i = 0; i < numberOfSuggestions; i++) {
3340                        final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3341                        if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3342                            final int otherSpanStart =
3343                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3344                            final int otherSpanEnd =
3345                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
3346                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
3347                                continue suggestionLoop;
3348                            }
3349                        }
3350                    }
3351
3352                    SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
3353                    suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
3354                    suggestionInfo.mSuggestionIndex = suggestionIndex;
3355                    suggestionInfo.mSuggestionStart = 0;
3356                    suggestionInfo.mSuggestionEnd = suggestion.length();
3357                    suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3358                    numberOfSuggestions++;
3359                    if (numberOfSuggestions >= suggestionInfos.length) {
3360                        return numberOfSuggestions;
3361                    }
3362                }
3363            }
3364            return numberOfSuggestions;
3365        }
3366    }
3367
3368    @VisibleForTesting
3369    public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
3370        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
3371
3372        // Key of intent extras for inserting new word into user dictionary.
3373        private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3374        private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3375
3376        private SuggestionInfo[] mSuggestionInfos;
3377        private int mNumberOfSuggestions;
3378        private boolean mCursorWasVisibleBeforeSuggestions;
3379        private boolean mIsShowingUp = false;
3380        private SuggestionAdapter mSuggestionsAdapter;
3381        private TextAppearanceSpan mHighlightSpan;  // TODO: Make mHighlightSpan final.
3382        private TextView mAddToDictionaryButton;
3383        private TextView mDeleteButton;
3384        private ListView mSuggestionListView;
3385        private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
3386        private int mContainerMarginWidth;
3387        private int mContainerMarginTop;
3388        private LinearLayout mContainerView;
3389        private Context mContext;  // TODO: Make mContext final.
3390
3391        private class CustomPopupWindow extends PopupWindow {
3392
3393            @Override
3394            public void dismiss() {
3395                if (!isShowing()) {
3396                    return;
3397                }
3398                super.dismiss();
3399                getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3400
3401                // Safe cast since show() checks that mTextView.getText() is an Editable
3402                ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3403
3404                mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
3405                if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
3406                    getInsertionController().show();
3407                }
3408            }
3409        }
3410
3411        public SuggestionsPopupWindow() {
3412            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3413        }
3414
3415        @Override
3416        protected void setUp() {
3417            mContext = applyDefaultTheme(mTextView.getContext());
3418            mHighlightSpan = new TextAppearanceSpan(mContext,
3419                    mTextView.mTextEditSuggestionHighlightStyle);
3420        }
3421
3422        private Context applyDefaultTheme(Context originalContext) {
3423            TypedArray a = originalContext.obtainStyledAttributes(
3424                    new int[]{com.android.internal.R.attr.isLightTheme});
3425            boolean isLightTheme = a.getBoolean(0, true);
3426            int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3427                    : R.style.ThemeOverlay_Material_Dark;
3428            a.recycle();
3429            return new ContextThemeWrapper(originalContext, themeId);
3430        }
3431
3432        @Override
3433        protected void createPopupWindow() {
3434            mPopupWindow = new CustomPopupWindow();
3435            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3436            mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
3437            mPopupWindow.setFocusable(true);
3438            mPopupWindow.setClippingEnabled(false);
3439        }
3440
3441        @Override
3442        protected void initContentView() {
3443            final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3444                    Context.LAYOUT_INFLATER_SERVICE);
3445            mContentView = (ViewGroup) inflater.inflate(
3446                    mTextView.mTextEditSuggestionContainerLayout, null);
3447
3448            mContainerView = (LinearLayout) mContentView.findViewById(
3449                    com.android.internal.R.id.suggestionWindowContainer);
3450            ViewGroup.MarginLayoutParams lp =
3451                    (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
3452            mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3453            mContainerMarginTop = lp.topMargin;
3454            mClippingLimitLeft = lp.leftMargin;
3455            mClippingLimitRight = lp.rightMargin;
3456
3457            mSuggestionListView = (ListView) mContentView.findViewById(
3458                    com.android.internal.R.id.suggestionContainer);
3459
3460            mSuggestionsAdapter = new SuggestionAdapter();
3461            mSuggestionListView.setAdapter(mSuggestionsAdapter);
3462            mSuggestionListView.setOnItemClickListener(this);
3463
3464            // Inflate the suggestion items once and for all.
3465            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
3466            for (int i = 0; i < mSuggestionInfos.length; i++) {
3467                mSuggestionInfos[i] = new SuggestionInfo();
3468            }
3469
3470            mAddToDictionaryButton = (TextView) mContentView.findViewById(
3471                    com.android.internal.R.id.addToDictionaryButton);
3472            mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3473                public void onClick(View v) {
3474                    final SuggestionSpan misspelledSpan =
3475                            findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3476                    if (misspelledSpan == null) {
3477                        // Span has been removed.
3478                        return;
3479                    }
3480                    final Editable editable = (Editable) mTextView.getText();
3481                    final int spanStart = editable.getSpanStart(misspelledSpan);
3482                    final int spanEnd = editable.getSpanEnd(misspelledSpan);
3483                    if (spanStart < 0 || spanEnd <= spanStart) {
3484                        return;
3485                    }
3486                    final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3487
3488                    final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3489                    intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3490                    intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3491                            mTextView.getTextServicesLocale().toString());
3492                    intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3493                    mTextView.getContext().startActivity(intent);
3494                    // There is no way to know if the word was indeed added. Re-check.
3495                    // TODO The ExtractEditText should remove the span in the original text instead
3496                    editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
3497                    Selection.setSelection(editable, spanEnd);
3498                    updateSpellCheckSpans(spanStart, spanEnd, false);
3499                    hideWithCleanUp();
3500                }
3501            });
3502
3503            mDeleteButton = (TextView) mContentView.findViewById(
3504                    com.android.internal.R.id.deleteButton);
3505            mDeleteButton.setOnClickListener(new View.OnClickListener() {
3506                public void onClick(View v) {
3507                    final Editable editable = (Editable) mTextView.getText();
3508
3509                    final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3510                    int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3511                    if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3512                        // Do not leave two adjacent spaces after deletion, or one at beginning of
3513                        // text
3514                        if (spanUnionEnd < editable.length()
3515                                && Character.isSpaceChar(editable.charAt(spanUnionEnd))
3516                                && (spanUnionStart == 0
3517                                        || Character.isSpaceChar(
3518                                                editable.charAt(spanUnionStart - 1)))) {
3519                            spanUnionEnd = spanUnionEnd + 1;
3520                        }
3521                        mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3522                    }
3523                    hideWithCleanUp();
3524                }
3525            });
3526
3527        }
3528
3529        public boolean isShowingUp() {
3530            return mIsShowingUp;
3531        }
3532
3533        public void onParentLostFocus() {
3534            mIsShowingUp = false;
3535        }
3536
3537        private class SuggestionAdapter extends BaseAdapter {
3538            private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3539                    Context.LAYOUT_INFLATER_SERVICE);
3540
3541            @Override
3542            public int getCount() {
3543                return mNumberOfSuggestions;
3544            }
3545
3546            @Override
3547            public Object getItem(int position) {
3548                return mSuggestionInfos[position];
3549            }
3550
3551            @Override
3552            public long getItemId(int position) {
3553                return position;
3554            }
3555
3556            @Override
3557            public View getView(int position, View convertView, ViewGroup parent) {
3558                TextView textView = (TextView) convertView;
3559
3560                if (textView == null) {
3561                    textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3562                            parent, false);
3563                }
3564
3565                final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3566                textView.setText(suggestionInfo.mText);
3567                return textView;
3568            }
3569        }
3570
3571        @VisibleForTesting
3572        public ViewGroup getContentViewForTesting() {
3573            return mContentView;
3574        }
3575
3576        @Override
3577        public void show() {
3578            if (!(mTextView.getText() instanceof Editable)) return;
3579            if (extractedTextModeWillBeStarted()) {
3580                return;
3581            }
3582
3583            if (updateSuggestions()) {
3584                mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3585                mTextView.setCursorVisible(false);
3586                mIsShowingUp = true;
3587                super.show();
3588            }
3589        }
3590
3591        @Override
3592        protected void measureContent() {
3593            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3594            final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3595                    displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3596            final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3597                    displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3598
3599            int width = 0;
3600            View view = null;
3601            for (int i = 0; i < mNumberOfSuggestions; i++) {
3602                view = mSuggestionsAdapter.getView(i, view, mContentView);
3603                view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3604                view.measure(horizontalMeasure, verticalMeasure);
3605                width = Math.max(width, view.getMeasuredWidth());
3606            }
3607
3608            if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3609                mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3610                width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3611            }
3612
3613            mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3614            width = Math.max(width, mDeleteButton.getMeasuredWidth());
3615
3616            width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3617                    + mContainerMarginWidth;
3618
3619            // Enforce the width based on actual text widths
3620            mContentView.measure(
3621                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3622                    verticalMeasure);
3623
3624            Drawable popupBackground = mPopupWindow.getBackground();
3625            if (popupBackground != null) {
3626                if (mTempRect == null) mTempRect = new Rect();
3627                popupBackground.getPadding(mTempRect);
3628                width += mTempRect.left + mTempRect.right;
3629            }
3630            mPopupWindow.setWidth(width);
3631        }
3632
3633        @Override
3634        protected int getTextOffset() {
3635            return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
3636        }
3637
3638        @Override
3639        protected int getVerticalLocalPosition(int line) {
3640            return mTextView.getLayout().getLineBottom(line) - mContainerMarginTop;
3641        }
3642
3643        @Override
3644        protected int clipVertically(int positionY) {
3645            final int height = mContentView.getMeasuredHeight();
3646            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3647            return Math.min(positionY, displayMetrics.heightPixels - height);
3648        }
3649
3650        private void hideWithCleanUp() {
3651            for (final SuggestionInfo info : mSuggestionInfos) {
3652                info.clear();
3653            }
3654            mMisspelledSpanInfo.clear();
3655            hide();
3656        }
3657
3658        private boolean updateSuggestions() {
3659            Spannable spannable = (Spannable) mTextView.getText();
3660            mNumberOfSuggestions =
3661                    mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3662            if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
3663                return false;
3664            }
3665
3666            int spanUnionStart = mTextView.getText().length();
3667            int spanUnionEnd = 0;
3668
3669            for (int i = 0; i < mNumberOfSuggestions; i++) {
3670                final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3671                spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3672                spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3673            }
3674            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3675                spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3676                spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
3677            }
3678
3679            for (int i = 0; i < mNumberOfSuggestions; i++) {
3680                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3681            }
3682
3683            // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3684            int addToDictionaryButtonVisibility = View.GONE;
3685            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3686                if (mMisspelledSpanInfo.mSpanStart >= 0
3687                        && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
3688                    addToDictionaryButtonVisibility = View.VISIBLE;
3689                }
3690            }
3691            mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
3692
3693            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
3694            final int underlineColor;
3695            if (mNumberOfSuggestions != 0) {
3696                underlineColor =
3697                        mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3698            } else {
3699                underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3700            }
3701
3702            if (underlineColor == 0) {
3703                // Fallback on the default highlight color when the first span does not provide one
3704                mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3705            } else {
3706                final float BACKGROUND_TRANSPARENCY = 0.4f;
3707                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3708                mSuggestionRangeSpan.setBackgroundColor(
3709                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3710            }
3711            spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3712                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3713
3714            mSuggestionsAdapter.notifyDataSetChanged();
3715            return true;
3716        }
3717
3718        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3719                int unionEnd) {
3720            final Spannable text = (Spannable) mTextView.getText();
3721            final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3722            final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
3723
3724            // Adjust the start/end of the suggestion span
3725            suggestionInfo.mSuggestionStart = spanStart - unionStart;
3726            suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3727                    + suggestionInfo.mText.length();
3728
3729            suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
3730                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3731
3732            // Add the text before and after the span.
3733            final String textAsString = text.toString();
3734            suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3735            suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
3736        }
3737
3738        @Override
3739        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3740            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3741            replaceWithSuggestion(suggestionInfo);
3742            hideWithCleanUp();
3743        }
3744    }
3745
3746    /**
3747     * An ActionMode Callback class that is used to provide actions while in text insertion or
3748     * selection mode.
3749     *
3750     * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3751     * actions, depending on which of these this TextView supports and the current selection.
3752     */
3753    private class TextActionModeCallback extends ActionMode.Callback2 {
3754        private final Path mSelectionPath = new Path();
3755        private final RectF mSelectionBounds = new RectF();
3756        private final boolean mHasSelection;
3757        private final int mHandleHeight;
3758
3759        public TextActionModeCallback(boolean hasSelection) {
3760            mHasSelection = hasSelection;
3761            if (mHasSelection) {
3762                SelectionModifierCursorController selectionController = getSelectionController();
3763                if (selectionController.mStartHandle == null) {
3764                    // As these are for initializing selectionController, hide() must be called.
3765                    selectionController.initDrawables();
3766                    selectionController.initHandles();
3767                    selectionController.hide();
3768                }
3769                mHandleHeight = Math.max(
3770                        mSelectHandleLeft.getMinimumHeight(),
3771                        mSelectHandleRight.getMinimumHeight());
3772            } else {
3773                InsertionPointCursorController insertionController = getInsertionController();
3774                if (insertionController != null) {
3775                    insertionController.getHandle();
3776                    mHandleHeight = mSelectHandleCenter.getMinimumHeight();
3777                } else {
3778                    mHandleHeight = 0;
3779                }
3780            }
3781        }
3782
3783        @Override
3784        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
3785            mode.setTitle(null);
3786            mode.setSubtitle(null);
3787            mode.setTitleOptionalHint(true);
3788            populateMenuWithItems(menu);
3789
3790            Callback customCallback = getCustomCallback();
3791            if (customCallback != null) {
3792                if (!customCallback.onCreateActionMode(mode, menu)) {
3793                    // The custom mode can choose to cancel the action mode, dismiss selection.
3794                    Selection.setSelection((Spannable) mTextView.getText(),
3795                            mTextView.getSelectionEnd());
3796                    return false;
3797                }
3798            }
3799
3800            if (mTextView.canProcessText()) {
3801                mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3802            }
3803
3804            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
3805                if (mHasSelection && !mTextView.hasTransientState()) {
3806                    mTextView.setHasTransientState(true);
3807                }
3808                return true;
3809            } else {
3810                return false;
3811            }
3812        }
3813
3814        private Callback getCustomCallback() {
3815            return mHasSelection
3816                    ? mCustomSelectionActionModeCallback
3817                    : mCustomInsertionActionModeCallback;
3818        }
3819
3820        private void populateMenuWithItems(Menu menu) {
3821            if (mTextView.canCut()) {
3822                menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
3823                        com.android.internal.R.string.cut)
3824                                .setAlphabeticShortcut('x')
3825                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3826            }
3827
3828            if (mTextView.canCopy()) {
3829                menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
3830                        com.android.internal.R.string.copy)
3831                                .setAlphabeticShortcut('c')
3832                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3833            }
3834
3835            if (mTextView.canPaste()) {
3836                menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
3837                        com.android.internal.R.string.paste)
3838                                .setAlphabeticShortcut('v')
3839                                .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3840            }
3841
3842            if (mTextView.canShare()) {
3843                menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
3844                        com.android.internal.R.string.share)
3845                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3846            }
3847
3848            if (mTextView.canRequestAutofill()) {
3849                final String selected = mTextView.getSelectedText();
3850                if (selected == null || selected.isEmpty()) {
3851                    menu.add(Menu.NONE, TextView.ID_AUTOFILL, MENU_ITEM_ORDER_AUTOFILL,
3852                            com.android.internal.R.string.autofill)
3853                            .setShowAsAction(MenuItem.SHOW_AS_OVERFLOW_ALWAYS);
3854                }
3855            }
3856
3857            if (mTextView.canPasteAsPlainText()) {
3858                menu.add(
3859                        Menu.NONE,
3860                        TextView.ID_PASTE_AS_PLAIN_TEXT,
3861                        MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
3862                        com.android.internal.R.string.paste_as_plain_text)
3863                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3864            }
3865
3866            updateSelectAllItem(menu);
3867            updateReplaceItem(menu);
3868            updateAssistMenuItem(menu);
3869        }
3870
3871        @Override
3872        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
3873            updateSelectAllItem(menu);
3874            updateReplaceItem(menu);
3875            updateAssistMenuItem(menu);
3876
3877            Callback customCallback = getCustomCallback();
3878            if (customCallback != null) {
3879                return customCallback.onPrepareActionMode(mode, menu);
3880            }
3881            return true;
3882        }
3883
3884        private void updateSelectAllItem(Menu menu) {
3885            boolean canSelectAll = mTextView.canSelectAllText();
3886            boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3887            if (canSelectAll && !selectAllItemExists) {
3888                menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3889                        com.android.internal.R.string.selectAll)
3890                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3891            } else if (!canSelectAll && selectAllItemExists) {
3892                menu.removeItem(TextView.ID_SELECT_ALL);
3893            }
3894        }
3895
3896        private void updateReplaceItem(Menu menu) {
3897            boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
3898            boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3899            if (canReplace && !replaceItemExists) {
3900                menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3901                        com.android.internal.R.string.replace)
3902                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3903            } else if (!canReplace && replaceItemExists) {
3904                menu.removeItem(TextView.ID_REPLACE);
3905            }
3906        }
3907
3908        private void updateAssistMenuItem(Menu menu) {
3909            menu.removeItem(TextView.ID_ASSIST);
3910            final TextClassification textClassification =
3911                    getSelectionActionModeHelper().getTextClassification();
3912            if (textClassification != null) {
3913                final Drawable icon = textClassification.getIcon();
3914                final CharSequence label = textClassification.getLabel();
3915                final OnClickListener onClickListener =
3916                        textClassification.getOnClickListener();
3917                final Intent intent = textClassification.getIntent();
3918                if ((icon != null || !TextUtils.isEmpty(label))
3919                        && (onClickListener != null || intent != null)) {
3920                    menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, MENU_ITEM_ORDER_ASSIST, label)
3921                            .setIcon(icon)
3922                            .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3923                    mMetricsLogger.write(
3924                            new LogMaker(MetricsEvent.TEXT_SELECTION_MENU_ITEM_ASSIST)
3925                                    .setType(MetricsEvent.TYPE_OPEN)
3926                                    .setSubtype(textClassification.getLogType()));
3927                }
3928            }
3929        }
3930
3931        @Override
3932        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
3933            getSelectionActionModeHelper().onSelectionAction();
3934
3935            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
3936                return true;
3937            }
3938            Callback customCallback = getCustomCallback();
3939            if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
3940                return true;
3941            }
3942            final TextClassification textClassification =
3943                    getSelectionActionModeHelper().getTextClassification();
3944            if (TextView.ID_ASSIST == item.getItemId() && textClassification != null) {
3945                final OnClickListener onClickListener =
3946                        textClassification.getOnClickListener();
3947                if (onClickListener != null) {
3948                    onClickListener.onClick(mTextView);
3949                } else {
3950                    final Intent intent = textClassification.getIntent();
3951                    if (intent != null) {
3952                        TextClassification.createStartActivityOnClickListener(
3953                                mTextView.getContext(), intent)
3954                                .onClick(mTextView);
3955                    }
3956                }
3957                mMetricsLogger.action(
3958                        MetricsEvent.ACTION_TEXT_SELECTION_MENU_ITEM_ASSIST,
3959                        textClassification.getLogType());
3960                stopTextActionMode();
3961                return true;
3962            }
3963            return mTextView.onTextContextMenuItem(item.getItemId());
3964        }
3965
3966        @Override
3967        public void onDestroyActionMode(ActionMode mode) {
3968            // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
3969            getSelectionActionModeHelper().onDestroyActionMode();
3970            mTextActionMode = null;
3971            Callback customCallback = getCustomCallback();
3972            if (customCallback != null) {
3973                customCallback.onDestroyActionMode(mode);
3974            }
3975
3976            if (!mPreserveSelection) {
3977                /*
3978                 * Leave current selection when we tentatively destroy action mode for the
3979                 * selection. If we're detaching from a window, we'll bring back the selection
3980                 * mode when (if) we get reattached.
3981                 */
3982                Selection.setSelection((Spannable) mTextView.getText(),
3983                        mTextView.getSelectionEnd());
3984            }
3985
3986            if (mSelectionModifierCursorController != null) {
3987                mSelectionModifierCursorController.hide();
3988            }
3989        }
3990
3991        @Override
3992        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
3993            if (!view.equals(mTextView) || mTextView.getLayout() == null) {
3994                super.onGetContentRect(mode, view, outRect);
3995                return;
3996            }
3997            if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
3998                // We have a selection.
3999                mSelectionPath.reset();
4000                mTextView.getLayout().getSelectionPath(
4001                        mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
4002                mSelectionPath.computeBounds(mSelectionBounds, true);
4003                mSelectionBounds.bottom += mHandleHeight;
4004            } else if (mCursorCount == 2) {
4005                // We have a split cursor. In this case, we take the rectangle that includes both
4006                // parts of the cursor to ensure we don't obscure either of them.
4007                Rect firstCursorBounds = mCursorDrawable[0].getBounds();
4008                Rect secondCursorBounds = mCursorDrawable[1].getBounds();
4009                mSelectionBounds.set(
4010                        Math.min(firstCursorBounds.left, secondCursorBounds.left),
4011                        Math.min(firstCursorBounds.top, secondCursorBounds.top),
4012                        Math.max(firstCursorBounds.right, secondCursorBounds.right),
4013                        Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
4014                                + mHandleHeight);
4015            } else {
4016                // We have a single cursor.
4017                Layout layout = mTextView.getLayout();
4018                int line = layout.getLineForOffset(mTextView.getSelectionStart());
4019                float primaryHorizontal = clampHorizontalPosition(null,
4020                        layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
4021                mSelectionBounds.set(
4022                        primaryHorizontal,
4023                        layout.getLineTop(line),
4024                        primaryHorizontal,
4025                        layout.getLineTop(line + 1) + mHandleHeight);
4026            }
4027            // Take TextView's padding and scroll into account.
4028            int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
4029            int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
4030            outRect.set(
4031                    (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
4032                    (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
4033                    (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
4034                    (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
4035        }
4036    }
4037
4038    /**
4039     * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
4040     * while the input method is requesting the cursor/anchor position. Does nothing as long as
4041     * {@link InputMethodManager#isWatchingCursor(View)} returns false.
4042     */
4043    private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
4044        final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
4045        final int[] mTmpIntOffset = new int[2];
4046        final Matrix mViewToScreenMatrix = new Matrix();
4047
4048        @Override
4049        public void updatePosition(int parentPositionX, int parentPositionY,
4050                boolean parentPositionChanged, boolean parentScrolled) {
4051            final InputMethodState ims = mInputMethodState;
4052            if (ims == null || ims.mBatchEditNesting > 0) {
4053                return;
4054            }
4055            final InputMethodManager imm = InputMethodManager.peekInstance();
4056            if (null == imm) {
4057                return;
4058            }
4059            if (!imm.isActive(mTextView)) {
4060                return;
4061            }
4062            // Skip if the IME has not requested the cursor/anchor position.
4063            if (!imm.isCursorAnchorInfoEnabled()) {
4064                return;
4065            }
4066            Layout layout = mTextView.getLayout();
4067            if (layout == null) {
4068                return;
4069            }
4070
4071            final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
4072            builder.reset();
4073
4074            final int selectionStart = mTextView.getSelectionStart();
4075            builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
4076
4077            // Construct transformation matrix from view local coordinates to screen coordinates.
4078            mViewToScreenMatrix.set(mTextView.getMatrix());
4079            mTextView.getLocationOnScreen(mTmpIntOffset);
4080            mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
4081            builder.setMatrix(mViewToScreenMatrix);
4082
4083            final float viewportToContentHorizontalOffset =
4084                    mTextView.viewportToContentHorizontalOffset();
4085            final float viewportToContentVerticalOffset =
4086                    mTextView.viewportToContentVerticalOffset();
4087
4088            final CharSequence text = mTextView.getText();
4089            if (text instanceof Spannable) {
4090                final Spannable sp = (Spannable) text;
4091                int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
4092                int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
4093                if (composingTextEnd < composingTextStart) {
4094                    final int temp = composingTextEnd;
4095                    composingTextEnd = composingTextStart;
4096                    composingTextStart = temp;
4097                }
4098                final boolean hasComposingText =
4099                        (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
4100                if (hasComposingText) {
4101                    final CharSequence composingText = text.subSequence(composingTextStart,
4102                            composingTextEnd);
4103                    builder.setComposingText(composingTextStart, composingText);
4104                    mTextView.populateCharacterBounds(builder, composingTextStart,
4105                            composingTextEnd, viewportToContentHorizontalOffset,
4106                            viewportToContentVerticalOffset);
4107                }
4108            }
4109
4110            // Treat selectionStart as the insertion point.
4111            if (0 <= selectionStart) {
4112                final int offset = selectionStart;
4113                final int line = layout.getLineForOffset(offset);
4114                final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
4115                        + viewportToContentHorizontalOffset;
4116                final float insertionMarkerTop = layout.getLineTop(line)
4117                        + viewportToContentVerticalOffset;
4118                final float insertionMarkerBaseline = layout.getLineBaseline(line)
4119                        + viewportToContentVerticalOffset;
4120                final float insertionMarkerBottom = layout.getLineBottom(line)
4121                        + viewportToContentVerticalOffset;
4122                final boolean isTopVisible = mTextView
4123                        .isPositionVisible(insertionMarkerX, insertionMarkerTop);
4124                final boolean isBottomVisible = mTextView
4125                        .isPositionVisible(insertionMarkerX, insertionMarkerBottom);
4126                int insertionMarkerFlags = 0;
4127                if (isTopVisible || isBottomVisible) {
4128                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4129                }
4130                if (!isTopVisible || !isBottomVisible) {
4131                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4132                }
4133                if (layout.isRtlCharAt(offset)) {
4134                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4135                }
4136                builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
4137                        insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
4138            }
4139
4140            imm.updateCursorAnchorInfo(mTextView, builder.build());
4141        }
4142    }
4143
4144    @VisibleForTesting
4145    public abstract class HandleView extends View implements TextViewPositionListener {
4146        protected Drawable mDrawable;
4147        protected Drawable mDrawableLtr;
4148        protected Drawable mDrawableRtl;
4149        private final PopupWindow mContainer;
4150        // Position with respect to the parent TextView
4151        private int mPositionX, mPositionY;
4152        private boolean mIsDragging;
4153        // Offset from touch position to mPosition
4154        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4155        protected int mHotspotX;
4156        protected int mHorizontalGravity;
4157        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4158        private float mTouchOffsetY;
4159        // Where the touch position should be on the handle to ensure a maximum cursor visibility
4160        private float mIdealVerticalOffset;
4161        // Parent's (TextView) previous position in window
4162        private int mLastParentX, mLastParentY;
4163        // Parent's (TextView) previous position on screen
4164        private int mLastParentXOnScreen, mLastParentYOnScreen;
4165        // Previous text character offset
4166        protected int mPreviousOffset = -1;
4167        // Previous text character offset
4168        private boolean mPositionHasChanged = true;
4169        // Minimum touch target size for handles
4170        private int mMinSize;
4171        // Indicates the line of text that the handle is on.
4172        protected int mPrevLine = UNSET_LINE;
4173        // Indicates the line of text that the user was touching. This can differ from mPrevLine
4174        // when selecting text when the handles jump to the end / start of words which may be on
4175        // a different line.
4176        protected int mPreviousLineTouched = UNSET_LINE;
4177
4178        private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
4179            super(mTextView.getContext());
4180            setId(id);
4181            mContainer = new PopupWindow(mTextView.getContext(), null,
4182                    com.android.internal.R.attr.textSelectHandleWindowStyle);
4183            mContainer.setSplitTouchEnabled(true);
4184            mContainer.setClippingEnabled(false);
4185            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
4186            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4187            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
4188            mContainer.setContentView(this);
4189
4190            mDrawableLtr = drawableLtr;
4191            mDrawableRtl = drawableRtl;
4192            mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4193                    com.android.internal.R.dimen.text_handle_min_size);
4194
4195            updateDrawable();
4196
4197            final int handleHeight = getPreferredHeight();
4198            mTouchOffsetY = -0.3f * handleHeight;
4199            mIdealVerticalOffset = 0.7f * handleHeight;
4200        }
4201
4202        public float getIdealVerticalOffset() {
4203            return mIdealVerticalOffset;
4204        }
4205
4206        protected void updateDrawable() {
4207            if (mIsDragging) {
4208                // Don't update drawable during dragging.
4209                return;
4210            }
4211            final Layout layout = mTextView.getLayout();
4212            if (layout == null) {
4213                return;
4214            }
4215            final int offset = getCurrentCursorOffset();
4216            final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
4217            final Drawable oldDrawable = mDrawable;
4218            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4219            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
4220            mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
4221            if (oldDrawable != mDrawable && isShowing()) {
4222                // Update popup window position.
4223                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4224                        - getHorizontalOffset() + getCursorOffset();
4225                mPositionX += mTextView.viewportToContentHorizontalOffset();
4226                mPositionHasChanged = true;
4227                updatePosition(mLastParentX, mLastParentY, false, false);
4228                postInvalidate();
4229            }
4230        }
4231
4232        protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
4233        protected abstract int getHorizontalGravity(boolean isRtlRun);
4234
4235        // Touch-up filter: number of previous positions remembered
4236        private static final int HISTORY_SIZE = 5;
4237        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4238        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4239        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4240        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4241        private int mPreviousOffsetIndex = 0;
4242        private int mNumberPreviousOffsets = 0;
4243
4244        private void startTouchUpFilter(int offset) {
4245            mNumberPreviousOffsets = 0;
4246            addPositionToTouchUpFilter(offset);
4247        }
4248
4249        private void addPositionToTouchUpFilter(int offset) {
4250            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4251            mPreviousOffsets[mPreviousOffsetIndex] = offset;
4252            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4253            mNumberPreviousOffsets++;
4254        }
4255
4256        private void filterOnTouchUp() {
4257            final long now = SystemClock.uptimeMillis();
4258            int i = 0;
4259            int index = mPreviousOffsetIndex;
4260            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4261            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4262                i++;
4263                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4264            }
4265
4266            if (i > 0 && i < iMax
4267                    && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
4268                positionAtCursorOffset(mPreviousOffsets[index], false);
4269            }
4270        }
4271
4272        public boolean offsetHasBeenChanged() {
4273            return mNumberPreviousOffsets > 1;
4274        }
4275
4276        @Override
4277        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
4278            setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4279        }
4280
4281        @Override
4282        public void invalidate() {
4283            super.invalidate();
4284            if (isShowing()) {
4285                positionAtCursorOffset(getCurrentCursorOffset(), true);
4286            }
4287        };
4288
4289        private int getPreferredWidth() {
4290            return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4291        }
4292
4293        private int getPreferredHeight() {
4294            return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
4295        }
4296
4297        public void show() {
4298            if (isShowing()) return;
4299
4300            getPositionListener().addSubscriber(this, true /* local position may change */);
4301
4302            // Make sure the offset is always considered new, even when focusing at same position
4303            mPreviousOffset = -1;
4304            positionAtCursorOffset(getCurrentCursorOffset(), false);
4305        }
4306
4307        protected void dismiss() {
4308            mIsDragging = false;
4309            mContainer.dismiss();
4310            onDetached();
4311        }
4312
4313        public void hide() {
4314            dismiss();
4315
4316            getPositionListener().removeSubscriber(this);
4317        }
4318
4319        public boolean isShowing() {
4320            return mContainer.isShowing();
4321        }
4322
4323        private boolean isVisible() {
4324            // Always show a dragging handle.
4325            if (mIsDragging) {
4326                return true;
4327            }
4328
4329            if (mTextView.isInBatchEditMode()) {
4330                return false;
4331            }
4332
4333            return mTextView.isPositionVisible(
4334                    mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
4335        }
4336
4337        public abstract int getCurrentCursorOffset();
4338
4339        protected abstract void updateSelection(int offset);
4340
4341        public abstract void updatePosition(float x, float y);
4342
4343        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4344            return layout.isRtlCharAt(offset);
4345        }
4346
4347        @VisibleForTesting
4348        public float getHorizontal(@NonNull Layout layout, int offset) {
4349            return layout.getPrimaryHorizontal(offset);
4350        }
4351
4352        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4353            return mTextView.getOffsetAtCoordinate(line, x);
4354        }
4355
4356        /**
4357         * @param offset Cursor offset. Must be in [-1, length].
4358         * @param forceUpdatePosition whether to force update the position.  This should be true
4359         * when If the parent has been scrolled, for example.
4360         */
4361        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
4362            // A HandleView relies on the layout, which may be nulled by external methods
4363            Layout layout = mTextView.getLayout();
4364            if (layout == null) {
4365                // Will update controllers' state, hiding them and stopping selection mode if needed
4366                prepareCursorControllers();
4367                return;
4368            }
4369            layout = mTextView.getLayout();
4370
4371            boolean offsetChanged = offset != mPreviousOffset;
4372            if (offsetChanged || forceUpdatePosition) {
4373                if (offsetChanged) {
4374                    updateSelection(offset);
4375                    addPositionToTouchUpFilter(offset);
4376                }
4377                final int line = layout.getLineForOffset(offset);
4378                mPrevLine = line;
4379
4380                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX
4381                        - getHorizontalOffset() + getCursorOffset();
4382                mPositionY = layout.getLineBottom(line);
4383
4384                // Take TextView's padding and scroll into account.
4385                mPositionX += mTextView.viewportToContentHorizontalOffset();
4386                mPositionY += mTextView.viewportToContentVerticalOffset();
4387
4388                mPreviousOffset = offset;
4389                mPositionHasChanged = true;
4390            }
4391        }
4392
4393        /**
4394         * Return the clamped horizontal position for the first cursor.
4395         *
4396         * @param layout Text layout.
4397         * @param offset Character offset for the cursor.
4398         * @return The clamped horizontal position for the cursor.
4399         */
4400        int getCursorHorizontalPosition(Layout layout, int offset) {
4401            return (int) (getHorizontal(layout, offset) - 0.5f);
4402        }
4403
4404        @Override
4405        public void updatePosition(int parentPositionX, int parentPositionY,
4406                boolean parentPositionChanged, boolean parentScrolled) {
4407            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
4408            if (parentPositionChanged || mPositionHasChanged) {
4409                if (mIsDragging) {
4410                    // Update touchToWindow offset in case of parent scrolling while dragging
4411                    if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4412                        mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4413                        mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4414                        mLastParentX = parentPositionX;
4415                        mLastParentY = parentPositionY;
4416                    }
4417
4418                    onHandleMoved();
4419                }
4420
4421                if (isVisible()) {
4422                    // Transform to the window coordinates to follow the view tranformation.
4423                    final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4424                    mTextView.transformFromViewToWindowSpace(pts);
4425                    pts[0] -= mHotspotX + getHorizontalOffset();
4426
4427                    if (isShowing()) {
4428                        mContainer.update(pts[0], pts[1], -1, -1);
4429                    } else {
4430                        mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
4431                    }
4432                } else {
4433                    if (isShowing()) {
4434                        dismiss();
4435                    }
4436                }
4437
4438                mPositionHasChanged = false;
4439            }
4440        }
4441
4442        @Override
4443        protected void onDraw(Canvas c) {
4444            final int drawWidth = mDrawable.getIntrinsicWidth();
4445            final int left = getHorizontalOffset();
4446
4447            mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
4448            mDrawable.draw(c);
4449        }
4450
4451        private int getHorizontalOffset() {
4452            final int width = getPreferredWidth();
4453            final int drawWidth = mDrawable.getIntrinsicWidth();
4454            final int left;
4455            switch (mHorizontalGravity) {
4456                case Gravity.LEFT:
4457                    left = 0;
4458                    break;
4459                default:
4460                case Gravity.CENTER:
4461                    left = (width - drawWidth) / 2;
4462                    break;
4463                case Gravity.RIGHT:
4464                    left = width - drawWidth;
4465                    break;
4466            }
4467            return left;
4468        }
4469
4470        protected int getCursorOffset() {
4471            return 0;
4472        }
4473
4474        @Override
4475        public boolean onTouchEvent(MotionEvent ev) {
4476            updateFloatingToolbarVisibility(ev);
4477
4478            switch (ev.getActionMasked()) {
4479                case MotionEvent.ACTION_DOWN: {
4480                    startTouchUpFilter(getCurrentCursorOffset());
4481
4482                    final PositionListener positionListener = getPositionListener();
4483                    mLastParentX = positionListener.getPositionX();
4484                    mLastParentY = positionListener.getPositionY();
4485                    mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4486                    mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4487
4488                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4489                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4490                    mTouchToWindowOffsetX = xInWindow - mPositionX;
4491                    mTouchToWindowOffsetY = yInWindow - mPositionY;
4492
4493                    mIsDragging = true;
4494                    mPreviousLineTouched = UNSET_LINE;
4495                    break;
4496                }
4497
4498                case MotionEvent.ACTION_MOVE: {
4499                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4500                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4501
4502                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4503                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
4504                    final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
4505                    float newVerticalOffset;
4506                    if (previousVerticalOffset < mIdealVerticalOffset) {
4507                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4508                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4509                    } else {
4510                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4511                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4512                    }
4513                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4514
4515                    final float newPosX =
4516                            xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4517                    final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
4518
4519                    updatePosition(newPosX, newPosY);
4520                    break;
4521                }
4522
4523                case MotionEvent.ACTION_UP:
4524                    filterOnTouchUp();
4525                    mIsDragging = false;
4526                    updateDrawable();
4527                    break;
4528
4529                case MotionEvent.ACTION_CANCEL:
4530                    mIsDragging = false;
4531                    updateDrawable();
4532                    break;
4533            }
4534            return true;
4535        }
4536
4537        public boolean isDragging() {
4538            return mIsDragging;
4539        }
4540
4541        void onHandleMoved() {}
4542
4543        public void onDetached() {}
4544    }
4545
4546    private class InsertionHandleView extends HandleView {
4547        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4548        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4549
4550        // Used to detect taps on the insertion handle, which will affect the insertion action mode
4551        private float mDownPositionX, mDownPositionY;
4552        private Runnable mHider;
4553
4554        public InsertionHandleView(Drawable drawable) {
4555            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
4556        }
4557
4558        @Override
4559        public void show() {
4560            super.show();
4561
4562            final long durationSinceCutOrCopy =
4563                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
4564
4565            // Cancel the single tap delayed runnable.
4566            if (mInsertionActionModeRunnable != null
4567                    && ((mTapState == TAP_STATE_DOUBLE_TAP)
4568                            || (mTapState == TAP_STATE_TRIPLE_CLICK)
4569                            || isCursorInsideEasyCorrectionSpan())) {
4570                mTextView.removeCallbacks(mInsertionActionModeRunnable);
4571            }
4572
4573            // Prepare and schedule the single tap runnable to run exactly after the double tap
4574            // timeout has passed.
4575            if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4576                    && !isCursorInsideEasyCorrectionSpan()
4577                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
4578                if (mTextActionMode == null) {
4579                    if (mInsertionActionModeRunnable == null) {
4580                        mInsertionActionModeRunnable = new Runnable() {
4581                            @Override
4582                            public void run() {
4583                                startInsertionActionMode();
4584                            }
4585                        };
4586                    }
4587                    mTextView.postDelayed(
4588                            mInsertionActionModeRunnable,
4589                            ViewConfiguration.getDoubleTapTimeout() + 1);
4590                }
4591
4592            }
4593
4594            hideAfterDelay();
4595        }
4596
4597        private void hideAfterDelay() {
4598            if (mHider == null) {
4599                mHider = new Runnable() {
4600                    public void run() {
4601                        hide();
4602                    }
4603                };
4604            } else {
4605                removeHiderCallback();
4606            }
4607            mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4608        }
4609
4610        private void removeHiderCallback() {
4611            if (mHider != null) {
4612                mTextView.removeCallbacks(mHider);
4613            }
4614        }
4615
4616        @Override
4617        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4618            return drawable.getIntrinsicWidth() / 2;
4619        }
4620
4621        @Override
4622        protected int getHorizontalGravity(boolean isRtlRun) {
4623            return Gravity.CENTER_HORIZONTAL;
4624        }
4625
4626        @Override
4627        protected int getCursorOffset() {
4628            int offset = super.getCursorOffset();
4629            final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
4630            if (cursor != null) {
4631                cursor.getPadding(mTempRect);
4632                offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
4633            }
4634            return offset;
4635        }
4636
4637        @Override
4638        int getCursorHorizontalPosition(Layout layout, int offset) {
4639            final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
4640            if (drawable != null) {
4641                final float horizontal = getHorizontal(layout, offset);
4642                return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
4643            }
4644            return super.getCursorHorizontalPosition(layout, offset);
4645        }
4646
4647        @Override
4648        public boolean onTouchEvent(MotionEvent ev) {
4649            final boolean result = super.onTouchEvent(ev);
4650
4651            switch (ev.getActionMasked()) {
4652                case MotionEvent.ACTION_DOWN:
4653                    mDownPositionX = ev.getRawX();
4654                    mDownPositionY = ev.getRawY();
4655                    break;
4656
4657                case MotionEvent.ACTION_UP:
4658                    if (!offsetHasBeenChanged()) {
4659                        final float deltaX = mDownPositionX - ev.getRawX();
4660                        final float deltaY = mDownPositionY - ev.getRawY();
4661                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4662
4663                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4664                                mTextView.getContext());
4665                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
4666
4667                        if (distanceSquared < touchSlop * touchSlop) {
4668                            // Tapping on the handle toggles the insertion action mode.
4669                            if (mTextActionMode != null) {
4670                                stopTextActionMode();
4671                            } else {
4672                                startInsertionActionMode();
4673                            }
4674                        }
4675                    } else {
4676                        if (mTextActionMode != null) {
4677                            mTextActionMode.invalidateContentRect();
4678                        }
4679                    }
4680                    hideAfterDelay();
4681                    break;
4682
4683                case MotionEvent.ACTION_CANCEL:
4684                    hideAfterDelay();
4685                    break;
4686
4687                default:
4688                    break;
4689            }
4690
4691            return result;
4692        }
4693
4694        @Override
4695        public int getCurrentCursorOffset() {
4696            return mTextView.getSelectionStart();
4697        }
4698
4699        @Override
4700        public void updateSelection(int offset) {
4701            Selection.setSelection((Spannable) mTextView.getText(), offset);
4702        }
4703
4704        @Override
4705        public void updatePosition(float x, float y) {
4706            Layout layout = mTextView.getLayout();
4707            int offset;
4708            if (layout != null) {
4709                if (mPreviousLineTouched == UNSET_LINE) {
4710                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4711                }
4712                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4713                offset = getOffsetAtCoordinate(layout, currLine, x);
4714                mPreviousLineTouched = currLine;
4715            } else {
4716                offset = -1;
4717            }
4718            positionAtCursorOffset(offset, false);
4719            if (mTextActionMode != null) {
4720                invalidateActionMode();
4721            }
4722        }
4723
4724        @Override
4725        void onHandleMoved() {
4726            super.onHandleMoved();
4727            removeHiderCallback();
4728        }
4729
4730        @Override
4731        public void onDetached() {
4732            super.onDetached();
4733            removeHiderCallback();
4734        }
4735    }
4736
4737    @Retention(RetentionPolicy.SOURCE)
4738    @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4739    public @interface HandleType {}
4740    public static final int HANDLE_TYPE_SELECTION_START = 0;
4741    public static final int HANDLE_TYPE_SELECTION_END = 1;
4742
4743    private class SelectionHandleView extends HandleView {
4744        // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4745        // end (HANDLE_TYPE_SELECTION_END).
4746        @HandleType
4747        private final int mHandleType;
4748        // Indicates whether the cursor is making adjustments within a word.
4749        private boolean mInWord = false;
4750        // Difference between touch position and word boundary position.
4751        private float mTouchWordDelta;
4752        // X value of the previous updatePosition call.
4753        private float mPrevX;
4754        // Indicates if the handle has moved a boundary between LTR and RTL text.
4755        private boolean mLanguageDirectionChanged = false;
4756        // Distance from edge of horizontally scrolling text view
4757        // to use to switch to character mode.
4758        private final float mTextViewEdgeSlop;
4759        // Used to save text view location.
4760        private final int[] mTextViewLocation = new int[2];
4761
4762        public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4763                @HandleType int handleType) {
4764            super(drawableLtr, drawableRtl, id);
4765            mHandleType = handleType;
4766            ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
4767            mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
4768        }
4769
4770        private boolean isStartHandle() {
4771            return mHandleType == HANDLE_TYPE_SELECTION_START;
4772        }
4773
4774        @Override
4775        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4776            if (isRtlRun == isStartHandle()) {
4777                return drawable.getIntrinsicWidth() / 4;
4778            } else {
4779                return (drawable.getIntrinsicWidth() * 3) / 4;
4780            }
4781        }
4782
4783        @Override
4784        protected int getHorizontalGravity(boolean isRtlRun) {
4785            return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
4786        }
4787
4788        @Override
4789        public int getCurrentCursorOffset() {
4790            return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
4791        }
4792
4793        @Override
4794        protected void updateSelection(int offset) {
4795            if (isStartHandle()) {
4796                Selection.setSelection((Spannable) mTextView.getText(), offset,
4797                        mTextView.getSelectionEnd());
4798            } else {
4799                Selection.setSelection((Spannable) mTextView.getText(),
4800                        mTextView.getSelectionStart(), offset);
4801            }
4802            updateDrawable();
4803            if (mTextActionMode != null) {
4804                invalidateActionMode();
4805            }
4806        }
4807
4808        @Override
4809        public void updatePosition(float x, float y) {
4810            final Layout layout = mTextView.getLayout();
4811            if (layout == null) {
4812                // HandleView will deal appropriately in positionAtCursorOffset when
4813                // layout is null.
4814                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y));
4815                return;
4816            }
4817
4818            if (mPreviousLineTouched == UNSET_LINE) {
4819                mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4820            }
4821
4822            boolean positionCursor = false;
4823            final int anotherHandleOffset =
4824                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
4825            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4826            int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
4827
4828            if (isStartHandle() && initialOffset >= anotherHandleOffset
4829                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4830                // Handles have crossed, bound it to the first selected line and
4831                // adjust by word / char as normal.
4832                currLine = layout.getLineForOffset(anotherHandleOffset);
4833                initialOffset = getOffsetAtCoordinate(layout, currLine, x);
4834            }
4835
4836            int offset = initialOffset;
4837            final int wordEnd = getWordEnd(offset);
4838            final int wordStart = getWordStart(offset);
4839
4840            if (mPrevX == UNSET_X_VALUE) {
4841                mPrevX = x;
4842            }
4843
4844            final int currentOffset = getCurrentCursorOffset();
4845            final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
4846            final boolean atRtl = isAtRtlRun(layout, offset);
4847            final boolean isLvlBoundary = layout.isLevelBoundary(offset);
4848
4849            // We can't determine if the user is expanding or shrinking the selection if they're
4850            // on a bi-di boundary, so until they've moved past the boundary we'll just place
4851            // the cursor at the current position.
4852            if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
4853                // We're on a boundary or this is the first direction change -- just update
4854                // to the current position.
4855                mLanguageDirectionChanged = true;
4856                mTouchWordDelta = 0.0f;
4857                positionAndAdjustForCrossingHandles(offset);
4858                return;
4859            } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4860                // We've just moved past the boundary so update the position. After this we can
4861                // figure out if the user is expanding or shrinking to go by word or character.
4862                positionAndAdjustForCrossingHandles(offset);
4863                mTouchWordDelta = 0.0f;
4864                mLanguageDirectionChanged = false;
4865                return;
4866            }
4867
4868            boolean isExpanding;
4869            final float xDiff = x - mPrevX;
4870            if (isStartHandle()) {
4871                isExpanding = currLine < mPreviousLineTouched;
4872            } else {
4873                isExpanding = currLine > mPreviousLineTouched;
4874            }
4875            if (atRtl == isStartHandle()) {
4876                isExpanding |= xDiff > 0;
4877            } else {
4878                isExpanding |= xDiff < 0;
4879            }
4880
4881            if (mTextView.getHorizontallyScrolling()) {
4882                if (positionNearEdgeOfScrollingView(x, atRtl)
4883                        && ((isStartHandle() && mTextView.getScrollX() != 0)
4884                                || (!isStartHandle()
4885                                        && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4886                        && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4887                                || (!isStartHandle() && offset > currentOffset)))
4888                                        || !isExpanding)) {
4889                    // If we're expanding ensure that the offset is actually expanding compared to
4890                    // the current offset, if the handle snapped to the word, the finger position
4891                    // may be out of sync and we don't want the selection to jump back.
4892                    mTouchWordDelta = 0.0f;
4893                    final int nextOffset = (atRtl == isStartHandle())
4894                            ? layout.getOffsetToRightOf(mPreviousOffset)
4895                            : layout.getOffsetToLeftOf(mPreviousOffset);
4896                    positionAndAdjustForCrossingHandles(nextOffset);
4897                    return;
4898                }
4899            }
4900
4901            if (isExpanding) {
4902                // User is increasing the selection.
4903                int wordBoundary = isStartHandle() ? wordStart : wordEnd;
4904                final boolean snapToWord = (!mInWord
4905                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
4906                                && atRtl == isAtRtlRun(layout, wordBoundary);
4907                if (snapToWord) {
4908                    // Sometimes words can be broken across lines (Chinese, hyphenation).
4909                    // We still snap to the word boundary but we only use the letters on the
4910                    // current line to determine if the user is far enough into the word to snap.
4911                    if (layout.getLineForOffset(wordBoundary) != currLine) {
4912                        wordBoundary = isStartHandle()
4913                                ? layout.getLineStart(currLine) : layout.getLineEnd(currLine);
4914                    }
4915                    final int offsetThresholdToSnap = isStartHandle()
4916                            ? wordEnd - ((wordEnd - wordBoundary) / 2)
4917                            : wordStart + ((wordBoundary - wordStart) / 2);
4918                    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 = wordStart;
4923                    } else if (!isStartHandle()
4924                            && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4925                        // User is far enough into the word or on a different line so we expand by
4926                        // word.
4927                        offset = wordEnd;
4928                    } else {
4929                        offset = mPreviousOffset;
4930                    }
4931                }
4932                if ((isStartHandle() && offset < initialOffset)
4933                        || (!isStartHandle() && offset > initialOffset)) {
4934                    final float adjustedX = getHorizontal(layout, offset);
4935                    mTouchWordDelta =
4936                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4937                } else {
4938                    mTouchWordDelta = 0.0f;
4939                }
4940                positionCursor = true;
4941            } else {
4942                final int adjustedOffset =
4943                        getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
4944                final boolean shrinking = isStartHandle()
4945                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4946                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4947                if (shrinking) {
4948                    // User is shrinking the selection.
4949                    if (currLine != mPrevLine) {
4950                        // We're on a different line, so we'll snap to word boundaries.
4951                        offset = isStartHandle() ? wordStart : wordEnd;
4952                        if ((isStartHandle() && offset < initialOffset)
4953                                || (!isStartHandle() && offset > initialOffset)) {
4954                            final float adjustedX = getHorizontal(layout, offset);
4955                            mTouchWordDelta =
4956                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4957                        } else {
4958                            mTouchWordDelta = 0.0f;
4959                        }
4960                    } else {
4961                        offset = adjustedOffset;
4962                    }
4963                    positionCursor = true;
4964                } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4965                        || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4966                    // Handle has jumped to the word boundary, and the user is moving
4967                    // their finger towards the handle, the delta should be updated.
4968                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x)
4969                            - getHorizontal(layout, mPreviousOffset);
4970                }
4971            }
4972
4973            if (positionCursor) {
4974                mPreviousLineTouched = currLine;
4975                positionAndAdjustForCrossingHandles(offset);
4976            }
4977            mPrevX = x;
4978        }
4979
4980        @Override
4981        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
4982            super.positionAtCursorOffset(offset, forceUpdatePosition);
4983            mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
4984        }
4985
4986        @Override
4987        public boolean onTouchEvent(MotionEvent event) {
4988            boolean superResult = super.onTouchEvent(event);
4989            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
4990                // Reset the touch word offset and x value when the user
4991                // re-engages the handle.
4992                mTouchWordDelta = 0.0f;
4993                mPrevX = UNSET_X_VALUE;
4994            }
4995            return superResult;
4996        }
4997
4998        private void positionAndAdjustForCrossingHandles(int offset) {
4999            final int anotherHandleOffset =
5000                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
5001            if ((isStartHandle() && offset >= anotherHandleOffset)
5002                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
5003                mTouchWordDelta = 0.0f;
5004                final Layout layout = mTextView.getLayout();
5005                if (layout != null && offset != anotherHandleOffset) {
5006                    final float horiz = getHorizontal(layout, offset);
5007                    final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
5008                            !isStartHandle());
5009                    final float currentHoriz = getHorizontal(layout, mPreviousOffset);
5010                    if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
5011                            || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
5012                        // This handle passes another one as it crossed a direction boundary.
5013                        // Don't minimize the selection, but keep the handle at the run boundary.
5014                        final int currentOffset = getCurrentCursorOffset();
5015                        final int offsetToGetRunRange = isStartHandle()
5016                                ? currentOffset : Math.max(currentOffset - 1, 0);
5017                        final long range = layout.getRunRange(offsetToGetRunRange);
5018                        if (isStartHandle()) {
5019                            offset = TextUtils.unpackRangeStartFromLong(range);
5020                        } else {
5021                            offset = TextUtils.unpackRangeEndFromLong(range);
5022                        }
5023                        positionAtCursorOffset(offset, false);
5024                        return;
5025                    }
5026                }
5027                // Handles can not cross and selection is at least one character.
5028                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
5029            }
5030            positionAtCursorOffset(offset, false);
5031        }
5032
5033        private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
5034            mTextView.getLocationOnScreen(mTextViewLocation);
5035            boolean nearEdge;
5036            if (atRtl == isStartHandle()) {
5037                int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
5038                        - mTextView.getPaddingRight();
5039                nearEdge = x > rightEdge - mTextViewEdgeSlop;
5040            } else {
5041                int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
5042                nearEdge = x < leftEdge + mTextViewEdgeSlop;
5043            }
5044            return nearEdge;
5045        }
5046
5047        @Override
5048        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
5049            final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
5050            return layout.isRtlCharAt(offsetToCheck);
5051        }
5052
5053        @Override
5054        public float getHorizontal(@NonNull Layout layout, int offset) {
5055            return getHorizontal(layout, offset, isStartHandle());
5056        }
5057
5058        private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
5059            final int line = layout.getLineForOffset(offset);
5060            final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
5061            final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5062            final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5063            return (isRtlChar == isRtlParagraph)
5064                    ? layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
5065        }
5066
5067        @Override
5068        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
5069            final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
5070            final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
5071            if (!layout.isLevelBoundary(primaryOffset)) {
5072                return primaryOffset;
5073            }
5074            final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
5075            final int currentOffset = getCurrentCursorOffset();
5076            final int primaryDiff = Math.abs(primaryOffset - currentOffset);
5077            final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
5078            if (primaryDiff < secondaryDiff) {
5079                return primaryOffset;
5080            } else if (primaryDiff > secondaryDiff) {
5081                return secondaryOffset;
5082            } else {
5083                final int offsetToCheck = isStartHandle()
5084                        ? currentOffset : Math.max(currentOffset - 1, 0);
5085                final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
5086                final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
5087                return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
5088            }
5089        }
5090    }
5091
5092    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
5093        final int trueLine = mTextView.getLineAtCoordinate(y);
5094        if (layout == null || prevLine > layout.getLineCount()
5095                || layout.getLineCount() <= 0 || prevLine < 0) {
5096            // Invalid parameters, just return whatever line is at y.
5097            return trueLine;
5098        }
5099
5100        if (Math.abs(trueLine - prevLine) >= 2) {
5101            // Only stick to lines if we're within a line of the previous selection.
5102            return trueLine;
5103        }
5104
5105        final float verticalOffset = mTextView.viewportToContentVerticalOffset();
5106        final int lineCount = layout.getLineCount();
5107        final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
5108
5109        final float firstLineTop = layout.getLineTop(0) + verticalOffset;
5110        final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
5111        final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
5112
5113        final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
5114        final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
5115        final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
5116
5117        // Determine if we've moved lines based on y position and previous line.
5118        int currLine;
5119        if (y <= yTopBound) {
5120            currLine = Math.max(prevLine - 1, 0);
5121        } else if (y >= yBottomBound) {
5122            currLine = Math.min(prevLine + 1, lineCount - 1);
5123        } else {
5124            currLine = prevLine;
5125        }
5126        return currLine;
5127    }
5128
5129    /**
5130     * A CursorController instance can be used to control a cursor in the text.
5131     */
5132    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5133        /**
5134         * Makes the cursor controller visible on screen.
5135         * See also {@link #hide()}.
5136         */
5137        public void show();
5138
5139        /**
5140         * Hide the cursor controller from screen.
5141         * See also {@link #show()}.
5142         */
5143        public void hide();
5144
5145        /**
5146         * Called when the view is detached from window. Perform house keeping task, such as
5147         * stopping Runnable thread that would otherwise keep a reference on the context, thus
5148         * preventing the activity from being recycled.
5149         */
5150        public void onDetached();
5151
5152        public boolean isCursorBeingModified();
5153
5154        public boolean isActive();
5155    }
5156
5157    private class InsertionPointCursorController implements CursorController {
5158        private InsertionHandleView mHandle;
5159
5160        public void show() {
5161            getHandle().show();
5162
5163            if (mSelectionModifierCursorController != null) {
5164                mSelectionModifierCursorController.hide();
5165            }
5166        }
5167
5168        public void hide() {
5169            if (mHandle != null) {
5170                mHandle.hide();
5171            }
5172        }
5173
5174        public void onTouchModeChanged(boolean isInTouchMode) {
5175            if (!isInTouchMode) {
5176                hide();
5177            }
5178        }
5179
5180        private InsertionHandleView getHandle() {
5181            if (mSelectHandleCenter == null) {
5182                mSelectHandleCenter = mTextView.getContext().getDrawable(
5183                        mTextView.mTextSelectHandleRes);
5184            }
5185            if (mHandle == null) {
5186                mHandle = new InsertionHandleView(mSelectHandleCenter);
5187            }
5188            return mHandle;
5189        }
5190
5191        @Override
5192        public void onDetached() {
5193            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5194            observer.removeOnTouchModeChangeListener(this);
5195
5196            if (mHandle != null) mHandle.onDetached();
5197        }
5198
5199        @Override
5200        public boolean isCursorBeingModified() {
5201            return mHandle != null && mHandle.isDragging();
5202        }
5203
5204        @Override
5205        public boolean isActive() {
5206            return mHandle != null && mHandle.isShowing();
5207        }
5208
5209        public void invalidateHandle() {
5210            if (mHandle != null) {
5211                mHandle.invalidate();
5212            }
5213        }
5214    }
5215
5216    class SelectionModifierCursorController implements CursorController {
5217        // The cursor controller handles, lazily created when shown.
5218        private SelectionHandleView mStartHandle;
5219        private SelectionHandleView mEndHandle;
5220        // The offsets of that last touch down event. Remembered to start selection there.
5221        private int mMinTouchOffset, mMaxTouchOffset;
5222
5223        private float mDownPositionX, mDownPositionY;
5224        private boolean mGestureStayedInTapRegion;
5225
5226        // Where the user first starts the drag motion.
5227        private int mStartOffset = -1;
5228
5229        private boolean mHaventMovedEnoughToStartDrag;
5230        // The line that a selection happened most recently with the drag accelerator.
5231        private int mLineSelectionIsOn = -1;
5232        // Whether the drag accelerator has selected past the initial line.
5233        private boolean mSwitchedLines = false;
5234
5235        // Indicates the drag accelerator mode that the user is currently using.
5236        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5237        // Drag accelerator is inactive.
5238        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5239        // Character based selection by dragging. Only for mouse.
5240        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5241        // Word based selection by dragging. Enabled after long pressing or double tapping.
5242        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
5243        // Paragraph based selection by dragging. Enabled after mouse triple click.
5244        private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
5245
5246        SelectionModifierCursorController() {
5247            resetTouchOffsets();
5248        }
5249
5250        public void show() {
5251            if (mTextView.isInBatchEditMode()) {
5252                return;
5253            }
5254            initDrawables();
5255            initHandles();
5256        }
5257
5258        private void initDrawables() {
5259            if (mSelectHandleLeft == null) {
5260                mSelectHandleLeft = mTextView.getContext().getDrawable(
5261                        mTextView.mTextSelectHandleLeftRes);
5262            }
5263            if (mSelectHandleRight == null) {
5264                mSelectHandleRight = mTextView.getContext().getDrawable(
5265                        mTextView.mTextSelectHandleRightRes);
5266            }
5267        }
5268
5269        private void initHandles() {
5270            // Lazy object creation has to be done before updatePosition() is called.
5271            if (mStartHandle == null) {
5272                mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5273                        com.android.internal.R.id.selection_start_handle,
5274                        HANDLE_TYPE_SELECTION_START);
5275            }
5276            if (mEndHandle == null) {
5277                mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5278                        com.android.internal.R.id.selection_end_handle,
5279                        HANDLE_TYPE_SELECTION_END);
5280            }
5281
5282            mStartHandle.show();
5283            mEndHandle.show();
5284
5285            hideInsertionPointCursorController();
5286        }
5287
5288        public void hide() {
5289            if (mStartHandle != null) mStartHandle.hide();
5290            if (mEndHandle != null) mEndHandle.hide();
5291        }
5292
5293        public void enterDrag(int dragAcceleratorMode) {
5294            // Just need to init the handles / hide insertion cursor.
5295            show();
5296            mDragAcceleratorMode = dragAcceleratorMode;
5297            // Start location of selection.
5298            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5299                    mLastDownPositionY);
5300            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
5301            // Don't show the handles until user has lifted finger.
5302            hide();
5303
5304            // This stops scrolling parents from intercepting the touch event, allowing
5305            // the user to continue dragging across the screen to select text; TextView will
5306            // scroll as necessary.
5307            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
5308            mTextView.cancelLongPress();
5309        }
5310
5311        public void onTouchEvent(MotionEvent event) {
5312            // This is done even when the View does not have focus, so that long presses can start
5313            // selection and tap can move cursor from this tap position.
5314            final float eventX = event.getX();
5315            final float eventY = event.getY();
5316            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5317            switch (event.getActionMasked()) {
5318                case MotionEvent.ACTION_DOWN:
5319                    if (extractedTextModeWillBeStarted()) {
5320                        // Prevent duplicating the selection handles until the mode starts.
5321                        hide();
5322                    } else {
5323                        // Remember finger down position, to be able to start selection from there.
5324                        mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5325                                eventX, eventY);
5326
5327                        // Double tap detection
5328                        if (mGestureStayedInTapRegion) {
5329                            if (mTapState == TAP_STATE_DOUBLE_TAP
5330                                    || mTapState == TAP_STATE_TRIPLE_CLICK) {
5331                                final float deltaX = eventX - mDownPositionX;
5332                                final float deltaY = eventY - mDownPositionY;
5333                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5334
5335                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
5336                                        mTextView.getContext());
5337                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5338                                boolean stayedInArea =
5339                                        distanceSquared < doubleTapSlop * doubleTapSlop;
5340
5341                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
5342                                    if (mTapState == TAP_STATE_DOUBLE_TAP) {
5343                                        selectCurrentWordAndStartDrag();
5344                                    } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
5345                                        selectCurrentParagraphAndStartDrag();
5346                                    }
5347                                    mDiscardNextActionUp = true;
5348                                }
5349                            }
5350                        }
5351
5352                        mDownPositionX = eventX;
5353                        mDownPositionY = eventY;
5354                        mGestureStayedInTapRegion = true;
5355                        mHaventMovedEnoughToStartDrag = true;
5356                    }
5357                    break;
5358
5359                case MotionEvent.ACTION_POINTER_DOWN:
5360                case MotionEvent.ACTION_POINTER_UP:
5361                    // Handle multi-point gestures. Keep min and max offset positions.
5362                    // Only activated for devices that correctly handle multi-touch.
5363                    if (mTextView.getContext().getPackageManager().hasSystemFeature(
5364                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5365                        updateMinAndMaxOffsets(event);
5366                    }
5367                    break;
5368
5369                case MotionEvent.ACTION_MOVE:
5370                    final ViewConfiguration viewConfig = ViewConfiguration.get(
5371                            mTextView.getContext());
5372                    final int touchSlop = viewConfig.getScaledTouchSlop();
5373
5374                    if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5375                        final float deltaX = eventX - mDownPositionX;
5376                        final float deltaY = eventY - mDownPositionY;
5377                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5378
5379                        if (mGestureStayedInTapRegion) {
5380                            int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5381                            mGestureStayedInTapRegion =
5382                                    distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5383                        }
5384                        if (mHaventMovedEnoughToStartDrag) {
5385                            // We don't start dragging until the user has moved enough.
5386                            mHaventMovedEnoughToStartDrag =
5387                                    distanceSquared <= touchSlop * touchSlop;
5388                        }
5389                    }
5390
5391                    if (isMouse && !isDragAcceleratorActive()) {
5392                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
5393                        if (mTextView.hasSelection()
5394                                && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5395                                && offset >= mTextView.getSelectionStart()
5396                                && offset <= mTextView.getSelectionEnd()) {
5397                            startDragAndDrop();
5398                            break;
5399                        }
5400
5401                        if (mStartOffset != offset) {
5402                            // Start character based drag accelerator.
5403                            stopTextActionMode();
5404                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5405                            mDiscardNextActionUp = true;
5406                            mHaventMovedEnoughToStartDrag = false;
5407                        }
5408                    }
5409
5410                    if (mStartHandle != null && mStartHandle.isShowing()) {
5411                        // Don't do the drag if the handles are showing already.
5412                        break;
5413                    }
5414
5415                    updateSelection(event);
5416                    break;
5417
5418                case MotionEvent.ACTION_UP:
5419                    if (!isDragAcceleratorActive()) {
5420                        break;
5421                    }
5422                    updateSelection(event);
5423
5424                    // No longer dragging to select text, let the parent intercept events.
5425                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
5426
5427                    // No longer the first dragging motion, reset.
5428                    resetDragAcceleratorState();
5429
5430                    if (mTextView.hasSelection()) {
5431                        // Drag selection should not be adjusted by the text classifier.
5432                        startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag);
5433                    }
5434                    break;
5435            }
5436        }
5437
5438        private void updateSelection(MotionEvent event) {
5439            if (mTextView.getLayout() != null) {
5440                switch (mDragAcceleratorMode) {
5441                    case DRAG_ACCELERATOR_MODE_CHARACTER:
5442                        updateCharacterBasedSelection(event);
5443                        break;
5444                    case DRAG_ACCELERATOR_MODE_WORD:
5445                        updateWordBasedSelection(event);
5446                        break;
5447                    case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5448                        updateParagraphBasedSelection(event);
5449                        break;
5450                }
5451            }
5452        }
5453
5454        /**
5455         * If the TextView allows text selection, selects the current paragraph and starts a drag.
5456         *
5457         * @return true if the drag was started.
5458         */
5459        private boolean selectCurrentParagraphAndStartDrag() {
5460            if (mInsertionActionModeRunnable != null) {
5461                mTextView.removeCallbacks(mInsertionActionModeRunnable);
5462            }
5463            stopTextActionMode();
5464            if (!selectCurrentParagraph()) {
5465                return false;
5466            }
5467            enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5468            return true;
5469        }
5470
5471        private void updateCharacterBasedSelection(MotionEvent event) {
5472            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5473            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
5474        }
5475
5476        private void updateWordBasedSelection(MotionEvent event) {
5477            if (mHaventMovedEnoughToStartDrag) {
5478                return;
5479            }
5480            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5481            final ViewConfiguration viewConfig = ViewConfiguration.get(
5482                    mTextView.getContext());
5483            final float eventX = event.getX();
5484            final float eventY = event.getY();
5485            final int currLine;
5486            if (isMouse) {
5487                // No need to offset the y coordinate for mouse input.
5488                currLine = mTextView.getLineAtCoordinate(eventY);
5489            } else {
5490                float y = eventY;
5491                if (mSwitchedLines) {
5492                    // Offset the finger by the same vertical offset as the handles.
5493                    // This improves visibility of the content being selected by
5494                    // shifting the finger below the content, this is applied once
5495                    // the user has switched lines.
5496                    final int touchSlop = viewConfig.getScaledTouchSlop();
5497                    final float fingerOffset = (mStartHandle != null)
5498                            ? mStartHandle.getIdealVerticalOffset()
5499                            : touchSlop;
5500                    y = eventY - fingerOffset;
5501                }
5502
5503                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5504                        y);
5505                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5506                    // Break early here, we want to offset the finger position from
5507                    // the selection highlight, once the user moved their finger
5508                    // to a different line we should apply the offset and *not* switch
5509                    // lines until recomputing the position with the finger offset.
5510                    mSwitchedLines = true;
5511                    return;
5512                }
5513            }
5514
5515            int startOffset;
5516            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5517            // Snap to word boundaries.
5518            if (mStartOffset < offset) {
5519                // Expanding with end handle.
5520                offset = getWordEnd(offset);
5521                startOffset = getWordStart(mStartOffset);
5522            } else {
5523                // Expanding with start handle.
5524                offset = getWordStart(offset);
5525                startOffset = getWordEnd(mStartOffset);
5526                if (startOffset == offset) {
5527                    offset = getNextCursorOffset(offset, false);
5528                }
5529            }
5530            mLineSelectionIsOn = currLine;
5531            Selection.setSelection((Spannable) mTextView.getText(), startOffset, offset);
5532        }
5533
5534        private void updateParagraphBasedSelection(MotionEvent event) {
5535            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5536
5537            final int start = Math.min(offset, mStartOffset);
5538            final int end = Math.max(offset, mStartOffset);
5539            final long paragraphsRange = getParagraphsRange(start, end);
5540            final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5541            final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
5542            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
5543        }
5544
5545        /**
5546         * @param event
5547         */
5548        private void updateMinAndMaxOffsets(MotionEvent event) {
5549            int pointerCount = event.getPointerCount();
5550            for (int index = 0; index < pointerCount; index++) {
5551                int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5552                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5553                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5554            }
5555        }
5556
5557        public int getMinTouchOffset() {
5558            return mMinTouchOffset;
5559        }
5560
5561        public int getMaxTouchOffset() {
5562            return mMaxTouchOffset;
5563        }
5564
5565        public void resetTouchOffsets() {
5566            mMinTouchOffset = mMaxTouchOffset = -1;
5567            resetDragAcceleratorState();
5568        }
5569
5570        private void resetDragAcceleratorState() {
5571            mStartOffset = -1;
5572            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5573            mSwitchedLines = false;
5574            final int selectionStart = mTextView.getSelectionStart();
5575            final int selectionEnd = mTextView.getSelectionEnd();
5576            if (selectionStart > selectionEnd) {
5577                Selection.setSelection((Spannable) mTextView.getText(),
5578                        selectionEnd, selectionStart);
5579            }
5580        }
5581
5582        /**
5583         * @return true iff this controller is currently used to move the selection start.
5584         */
5585        public boolean isSelectionStartDragged() {
5586            return mStartHandle != null && mStartHandle.isDragging();
5587        }
5588
5589        @Override
5590        public boolean isCursorBeingModified() {
5591            return isDragAcceleratorActive() || isSelectionStartDragged()
5592                    || (mEndHandle != null && mEndHandle.isDragging());
5593        }
5594
5595        /**
5596         * @return true if the user is selecting text using the drag accelerator.
5597         */
5598        public boolean isDragAcceleratorActive() {
5599            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
5600        }
5601
5602        public void onTouchModeChanged(boolean isInTouchMode) {
5603            if (!isInTouchMode) {
5604                hide();
5605            }
5606        }
5607
5608        @Override
5609        public void onDetached() {
5610            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5611            observer.removeOnTouchModeChangeListener(this);
5612
5613            if (mStartHandle != null) mStartHandle.onDetached();
5614            if (mEndHandle != null) mEndHandle.onDetached();
5615        }
5616
5617        @Override
5618        public boolean isActive() {
5619            return mStartHandle != null && mStartHandle.isShowing();
5620        }
5621
5622        public void invalidateHandles() {
5623            if (mStartHandle != null) {
5624                mStartHandle.invalidate();
5625            }
5626            if (mEndHandle != null) {
5627                mEndHandle.invalidate();
5628            }
5629        }
5630    }
5631
5632    private class CorrectionHighlighter {
5633        private final Path mPath = new Path();
5634        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5635        private int mStart, mEnd;
5636        private long mFadingStartTime;
5637        private RectF mTempRectF;
5638        private static final int FADE_OUT_DURATION = 400;
5639
5640        public CorrectionHighlighter() {
5641            mPaint.setCompatibilityScaling(
5642                    mTextView.getResources().getCompatibilityInfo().applicationScale);
5643            mPaint.setStyle(Paint.Style.FILL);
5644        }
5645
5646        public void highlight(CorrectionInfo info) {
5647            mStart = info.getOffset();
5648            mEnd = mStart + info.getNewText().length();
5649            mFadingStartTime = SystemClock.uptimeMillis();
5650
5651            if (mStart < 0 || mEnd < 0) {
5652                stopAnimation();
5653            }
5654        }
5655
5656        public void draw(Canvas canvas, int cursorOffsetVertical) {
5657            if (updatePath() && updatePaint()) {
5658                if (cursorOffsetVertical != 0) {
5659                    canvas.translate(0, cursorOffsetVertical);
5660                }
5661
5662                canvas.drawPath(mPath, mPaint);
5663
5664                if (cursorOffsetVertical != 0) {
5665                    canvas.translate(0, -cursorOffsetVertical);
5666                }
5667                invalidate(true); // TODO invalidate cursor region only
5668            } else {
5669                stopAnimation();
5670                invalidate(false); // TODO invalidate cursor region only
5671            }
5672        }
5673
5674        private boolean updatePaint() {
5675            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5676            if (duration > FADE_OUT_DURATION) return false;
5677
5678            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5679            final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
5680            final int color = (mTextView.mHighlightColor & 0x00FFFFFF)
5681                    + ((int) (highlightColorAlpha * coef) << 24);
5682            mPaint.setColor(color);
5683            return true;
5684        }
5685
5686        private boolean updatePath() {
5687            final Layout layout = mTextView.getLayout();
5688            if (layout == null) return false;
5689
5690            // Update in case text is edited while the animation is run
5691            final int length = mTextView.getText().length();
5692            int start = Math.min(length, mStart);
5693            int end = Math.min(length, mEnd);
5694
5695            mPath.reset();
5696            layout.getSelectionPath(start, end, mPath);
5697            return true;
5698        }
5699
5700        private void invalidate(boolean delayed) {
5701            if (mTextView.getLayout() == null) return;
5702
5703            if (mTempRectF == null) mTempRectF = new RectF();
5704            mPath.computeBounds(mTempRectF, false);
5705
5706            int left = mTextView.getCompoundPaddingLeft();
5707            int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5708
5709            if (delayed) {
5710                mTextView.postInvalidateOnAnimation(
5711                        left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5712                        left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5713            } else {
5714                mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5715                        (int) mTempRectF.right, (int) mTempRectF.bottom);
5716            }
5717        }
5718
5719        private void stopAnimation() {
5720            Editor.this.mCorrectionHighlighter = null;
5721        }
5722    }
5723
5724    private static class ErrorPopup extends PopupWindow {
5725        private boolean mAbove = false;
5726        private final TextView mView;
5727        private int mPopupInlineErrorBackgroundId = 0;
5728        private int mPopupInlineErrorAboveBackgroundId = 0;
5729
5730        ErrorPopup(TextView v, int width, int height) {
5731            super(v, width, height);
5732            mView = v;
5733            // Make sure the TextView has a background set as it will be used the first time it is
5734            // shown and positioned. Initialized with below background, which should have
5735            // dimensions identical to the above version for this to work (and is more likely).
5736            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5737                    com.android.internal.R.styleable.Theme_errorMessageBackground);
5738            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5739        }
5740
5741        void fixDirection(boolean above) {
5742            mAbove = above;
5743
5744            if (above) {
5745                mPopupInlineErrorAboveBackgroundId =
5746                    getResourceId(mPopupInlineErrorAboveBackgroundId,
5747                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5748            } else {
5749                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5750                        com.android.internal.R.styleable.Theme_errorMessageBackground);
5751            }
5752
5753            mView.setBackgroundResource(
5754                    above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId);
5755        }
5756
5757        private int getResourceId(int currentId, int index) {
5758            if (currentId == 0) {
5759                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
5760                        R.styleable.Theme);
5761                currentId = styledAttributes.getResourceId(index, 0);
5762                styledAttributes.recycle();
5763            }
5764            return currentId;
5765        }
5766
5767        @Override
5768        public void update(int x, int y, int w, int h, boolean force) {
5769            super.update(x, y, w, h, force);
5770
5771            boolean above = isAboveAnchor();
5772            if (above != mAbove) {
5773                fixDirection(above);
5774            }
5775        }
5776    }
5777
5778    static class InputContentType {
5779        int imeOptions = EditorInfo.IME_NULL;
5780        String privateImeOptions;
5781        CharSequence imeActionLabel;
5782        int imeActionId;
5783        Bundle extras;
5784        OnEditorActionListener onEditorActionListener;
5785        boolean enterDown;
5786        LocaleList imeHintLocales;
5787    }
5788
5789    static class InputMethodState {
5790        ExtractedTextRequest mExtractedTextRequest;
5791        final ExtractedText mExtractedText = new ExtractedText();
5792        int mBatchEditNesting;
5793        boolean mCursorChanged;
5794        boolean mSelectionModeChanged;
5795        boolean mContentChanged;
5796        int mChangedStart, mChangedEnd, mChangedDelta;
5797    }
5798
5799    /**
5800     * @return True iff (start, end) is a valid range within the text.
5801     */
5802    private static boolean isValidRange(CharSequence text, int start, int end) {
5803        return 0 <= start && start <= end && end <= text.length();
5804    }
5805
5806    @VisibleForTesting
5807    public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5808        return mSuggestionsPopupWindow;
5809    }
5810
5811    /**
5812     * An InputFilter that monitors text input to maintain undo history. It does not modify the
5813     * text being typed (and hence always returns null from the filter() method).
5814     *
5815     * TODO: Make this span aware.
5816     */
5817    public static class UndoInputFilter implements InputFilter {
5818        private final Editor mEditor;
5819
5820        // Whether the current filter pass is directly caused by an end-user text edit.
5821        private boolean mIsUserEdit;
5822
5823        // Whether the text field is handling an IME composition. Must be parceled in case the user
5824        // rotates the screen during composition.
5825        private boolean mHasComposition;
5826
5827        // Whether the user is expanding or shortening the text
5828        private boolean mExpanding;
5829
5830        // Whether the previous edit operation was in the current batch edit.
5831        private boolean mPreviousOperationWasInSameBatchEdit;
5832
5833        public UndoInputFilter(Editor editor) {
5834            mEditor = editor;
5835        }
5836
5837        public void saveInstanceState(Parcel parcel) {
5838            parcel.writeInt(mIsUserEdit ? 1 : 0);
5839            parcel.writeInt(mHasComposition ? 1 : 0);
5840            parcel.writeInt(mExpanding ? 1 : 0);
5841            parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
5842        }
5843
5844        public void restoreInstanceState(Parcel parcel) {
5845            mIsUserEdit = parcel.readInt() != 0;
5846            mHasComposition = parcel.readInt() != 0;
5847            mExpanding = parcel.readInt() != 0;
5848            mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
5849        }
5850
5851        /**
5852         * Signals that a user-triggered edit is starting.
5853         */
5854        public void beginBatchEdit() {
5855            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5856            mIsUserEdit = true;
5857        }
5858
5859        public void endBatchEdit() {
5860            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5861            mIsUserEdit = false;
5862            mPreviousOperationWasInSameBatchEdit = false;
5863        }
5864
5865        @Override
5866        public CharSequence filter(CharSequence source, int start, int end,
5867                Spanned dest, int dstart, int dend) {
5868            if (DEBUG_UNDO) {
5869                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") "
5870                        + "dest=" + dest + " (" + dstart + "-" + dend + ")");
5871            }
5872
5873            // Check to see if this edit should be tracked for undo.
5874            if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
5875                return null;
5876            }
5877
5878            final boolean hadComposition = mHasComposition;
5879            mHasComposition = isComposition(source);
5880            final boolean wasExpanding = mExpanding;
5881            boolean shouldCreateSeparateState = false;
5882            if ((end - start) != (dend - dstart)) {
5883                mExpanding = (end - start) > (dend - dstart);
5884                if (hadComposition && mExpanding != wasExpanding) {
5885                    shouldCreateSeparateState = true;
5886                }
5887            }
5888
5889            // Handle edit.
5890            handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
5891            return null;
5892        }
5893
5894        void freezeLastEdit() {
5895            mEditor.mUndoManager.beginUpdate("Edit text");
5896            EditOperation lastEdit = getLastEdit();
5897            if (lastEdit != null) {
5898                lastEdit.mFrozen = true;
5899            }
5900            mEditor.mUndoManager.endUpdate();
5901        }
5902
5903        @Retention(RetentionPolicy.SOURCE)
5904        @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
5905        private @interface MergeMode {}
5906        private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0;
5907        private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1;
5908        /** Use {@link EditOperation#mergeWith} to merge */
5909        private static final int MERGE_EDIT_MODE_NORMAL = 2;
5910
5911        private void handleEdit(CharSequence source, int start, int end,
5912                Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
5913            // An application may install a TextWatcher to provide additional modifications after
5914            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5915            // string). This results in multiple filter() calls for what the user considers to be
5916            // a single operation. Always undo the whole set of changes in one step.
5917            @MergeMode
5918            final int mergeMode;
5919            if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
5920                mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
5921            } else if (shouldCreateSeparateState) {
5922                mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
5923            } else {
5924                mergeMode = MERGE_EDIT_MODE_NORMAL;
5925            }
5926            // Build a new operation with all the information from this edit.
5927            String newText = TextUtils.substring(source, start, end);
5928            String oldText = TextUtils.substring(dest, dstart, dend);
5929            EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
5930                    mHasComposition);
5931            if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
5932                return;
5933            }
5934            recordEdit(edit, mergeMode);
5935        }
5936
5937        private EditOperation getLastEdit() {
5938            final UndoManager um = mEditor.mUndoManager;
5939            return um.getLastOperation(
5940                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5941        }
5942        /**
5943         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5944         * If forceMerge is true then the new edit is always merged.
5945         */
5946        private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
5947            // Fetch the last edit operation and attempt to merge in the new edit.
5948            final UndoManager um = mEditor.mUndoManager;
5949            um.beginUpdate("Edit text");
5950            EditOperation lastEdit = getLastEdit();
5951            if (lastEdit == null) {
5952                // Add this as the first edit.
5953                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5954                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5955            } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
5956                // Forced merges take priority because they could be the result of a non-user-edit
5957                // change and this case should not create a new undo operation.
5958                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5959                lastEdit.forceMergeWith(edit);
5960            } else if (!mIsUserEdit) {
5961                // An application directly modified the Editable outside of a text edit. Treat this
5962                // as a new change and don't attempt to merge.
5963                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
5964                um.commitState(mEditor.mUndoOwner);
5965                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5966            } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) {
5967                // Merge succeeded, nothing else to do.
5968                if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit);
5969            } else {
5970                // Could not merge with the last edit, so commit the last edit and add this edit.
5971                if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit);
5972                um.commitState(mEditor.mUndoOwner);
5973                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5974            }
5975            mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
5976            um.endUpdate();
5977        }
5978
5979        private boolean canUndoEdit(CharSequence source, int start, int end,
5980                Spanned dest, int dstart, int dend) {
5981            if (!mEditor.mAllowUndo) {
5982                if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled");
5983                return false;
5984            }
5985
5986            if (mEditor.mUndoManager.isInUndo()) {
5987                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo");
5988                return false;
5989            }
5990
5991            // Text filters run before input operations are applied. However, some input operations
5992            // are invalid and will throw exceptions when applied. This is common in tests. Don't
5993            // attempt to undo invalid operations.
5994            if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) {
5995                if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op");
5996                return false;
5997            }
5998
5999            // Earlier filters can rewrite input to be a no-op, for example due to a length limit
6000            // on an input field. Skip no-op changes.
6001            if (start == end && dstart == dend) {
6002                if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op");
6003                return false;
6004            }
6005
6006            return true;
6007        }
6008
6009        private static boolean isComposition(CharSequence source) {
6010            if (!(source instanceof Spannable)) {
6011                return false;
6012            }
6013            // This is a composition edit if the source has a non-zero-length composing span.
6014            Spannable text = (Spannable) source;
6015            int composeBegin = EditableInputConnection.getComposingSpanStart(text);
6016            int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
6017            return composeBegin < composeEnd;
6018        }
6019
6020        private boolean isInTextWatcher() {
6021            CharSequence text = mEditor.mTextView.getText();
6022            return (text instanceof SpannableStringBuilder)
6023                    && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
6024        }
6025    }
6026
6027    /**
6028     * An operation to undo a single "edit" to a text view.
6029     */
6030    public static class EditOperation extends UndoOperation<Editor> {
6031        private static final int TYPE_INSERT = 0;
6032        private static final int TYPE_DELETE = 1;
6033        private static final int TYPE_REPLACE = 2;
6034
6035        private int mType;
6036        private String mOldText;
6037        private String mNewText;
6038        private int mStart;
6039
6040        private int mOldCursorPos;
6041        private int mNewCursorPos;
6042        private boolean mFrozen;
6043        private boolean mIsComposition;
6044
6045        /**
6046         * Constructs an edit operation from a text input operation on editor that replaces the
6047         * oldText starting at dstart with newText.
6048         */
6049        public EditOperation(Editor editor, String oldText, int dstart, String newText,
6050                boolean isComposition) {
6051            super(editor.mUndoOwner);
6052            mOldText = oldText;
6053            mNewText = newText;
6054
6055            // Determine the type of the edit.
6056            if (mNewText.length() > 0 && mOldText.length() == 0) {
6057                mType = TYPE_INSERT;
6058            } else if (mNewText.length() == 0 && mOldText.length() > 0) {
6059                mType = TYPE_DELETE;
6060            } else {
6061                mType = TYPE_REPLACE;
6062            }
6063
6064            mStart = dstart;
6065            // Store cursor data.
6066            mOldCursorPos = editor.mTextView.getSelectionStart();
6067            mNewCursorPos = dstart + mNewText.length();
6068            mIsComposition = isComposition;
6069        }
6070
6071        public EditOperation(Parcel src, ClassLoader loader) {
6072            super(src, loader);
6073            mType = src.readInt();
6074            mOldText = src.readString();
6075            mNewText = src.readString();
6076            mStart = src.readInt();
6077            mOldCursorPos = src.readInt();
6078            mNewCursorPos = src.readInt();
6079            mFrozen = src.readInt() == 1;
6080            mIsComposition = src.readInt() == 1;
6081        }
6082
6083        @Override
6084        public void writeToParcel(Parcel dest, int flags) {
6085            dest.writeInt(mType);
6086            dest.writeString(mOldText);
6087            dest.writeString(mNewText);
6088            dest.writeInt(mStart);
6089            dest.writeInt(mOldCursorPos);
6090            dest.writeInt(mNewCursorPos);
6091            dest.writeInt(mFrozen ? 1 : 0);
6092            dest.writeInt(mIsComposition ? 1 : 0);
6093        }
6094
6095        private int getNewTextEnd() {
6096            return mStart + mNewText.length();
6097        }
6098
6099        private int getOldTextEnd() {
6100            return mStart + mOldText.length();
6101        }
6102
6103        @Override
6104        public void commit() {
6105        }
6106
6107        @Override
6108        public void undo() {
6109            if (DEBUG_UNDO) Log.d(TAG, "undo");
6110            // Remove the new text and insert the old.
6111            Editor editor = getOwnerData();
6112            Editable text = (Editable) editor.mTextView.getText();
6113            modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
6114        }
6115
6116        @Override
6117        public void redo() {
6118            if (DEBUG_UNDO) Log.d(TAG, "redo");
6119            // Remove the old text and insert the new.
6120            Editor editor = getOwnerData();
6121            Editable text = (Editable) editor.mTextView.getText();
6122            modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos);
6123        }
6124
6125        /**
6126         * Attempts to merge this existing operation with a new edit.
6127         * @param edit The new edit operation.
6128         * @return If the merge succeeded, returns true. Otherwise returns false and leaves this
6129         * object unchanged.
6130         */
6131        private boolean mergeWith(EditOperation edit) {
6132            if (DEBUG_UNDO) {
6133                Log.d(TAG, "mergeWith old " + this);
6134                Log.d(TAG, "mergeWith new " + edit);
6135            }
6136
6137            if (mFrozen) {
6138                return false;
6139            }
6140
6141            switch (mType) {
6142                case TYPE_INSERT:
6143                    return mergeInsertWith(edit);
6144                case TYPE_DELETE:
6145                    return mergeDeleteWith(edit);
6146                case TYPE_REPLACE:
6147                    return mergeReplaceWith(edit);
6148                default:
6149                    return false;
6150            }
6151        }
6152
6153        private boolean mergeInsertWith(EditOperation edit) {
6154            if (edit.mType == TYPE_INSERT) {
6155                // Merge insertions that are contiguous even when it's frozen.
6156                if (getNewTextEnd() != edit.mStart) {
6157                    return false;
6158                }
6159                mNewText += edit.mNewText;
6160                mNewCursorPos = edit.mNewCursorPos;
6161                mFrozen = edit.mFrozen;
6162                mIsComposition = edit.mIsComposition;
6163                return true;
6164            }
6165            if (mIsComposition && edit.mType == TYPE_REPLACE
6166                    && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) {
6167                // Merge insertion with replace as they can be single insertion.
6168                mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText
6169                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6170                mNewCursorPos = edit.mNewCursorPos;
6171                mIsComposition = edit.mIsComposition;
6172                return true;
6173            }
6174            return false;
6175        }
6176
6177        // TODO: Support forward delete.
6178        private boolean mergeDeleteWith(EditOperation edit) {
6179            // Only merge continuous deletes.
6180            if (edit.mType != TYPE_DELETE) {
6181                return false;
6182            }
6183            // Only merge deletions that are contiguous.
6184            if (mStart != edit.getOldTextEnd()) {
6185                return false;
6186            }
6187            mStart = edit.mStart;
6188            mOldText = edit.mOldText + mOldText;
6189            mNewCursorPos = edit.mNewCursorPos;
6190            mIsComposition = edit.mIsComposition;
6191            return true;
6192        }
6193
6194        private boolean mergeReplaceWith(EditOperation edit) {
6195            if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) {
6196                // Merge with adjacent insert.
6197                mNewText += edit.mNewText;
6198                mNewCursorPos = edit.mNewCursorPos;
6199                return true;
6200            }
6201            if (!mIsComposition) {
6202                return false;
6203            }
6204            if (edit.mType == TYPE_DELETE && mStart <= edit.mStart
6205                    && getNewTextEnd() >= edit.getOldTextEnd()) {
6206                // Merge with delete as they can be single operation.
6207                mNewText = mNewText.substring(0, edit.mStart - mStart)
6208                        + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length());
6209                if (mNewText.isEmpty()) {
6210                    mType = TYPE_DELETE;
6211                }
6212                mNewCursorPos = edit.mNewCursorPos;
6213                mIsComposition = edit.mIsComposition;
6214                return true;
6215            }
6216            if (edit.mType == TYPE_REPLACE && mStart == edit.mStart
6217                    && TextUtils.equals(mNewText, edit.mOldText)) {
6218                // Merge with the replace that replaces the same region.
6219                mNewText = edit.mNewText;
6220                mNewCursorPos = edit.mNewCursorPos;
6221                mIsComposition = edit.mIsComposition;
6222                return true;
6223            }
6224            return false;
6225        }
6226
6227        /**
6228         * Forcibly creates a single merged edit operation by simulating the entire text
6229         * contents being replaced.
6230         */
6231        public void forceMergeWith(EditOperation edit) {
6232            if (DEBUG_UNDO) Log.d(TAG, "forceMerge");
6233            if (mergeWith(edit)) {
6234                return;
6235            }
6236            Editor editor = getOwnerData();
6237
6238            // Copy the text of the current field.
6239            // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster,
6240            // but would require two parallel implementations of modifyText() because Editable and
6241            // StringBuilder do not share an interface for replace/delete/insert.
6242            Editable editable = (Editable) editor.mTextView.getText();
6243            Editable originalText = new SpannableStringBuilder(editable.toString());
6244
6245            // Roll back the last operation.
6246            modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos);
6247
6248            // Clone the text again and apply the new operation.
6249            Editable finalText = new SpannableStringBuilder(editable.toString());
6250            modifyText(finalText, edit.mStart, edit.getOldTextEnd(),
6251                    edit.mNewText, edit.mStart, edit.mNewCursorPos);
6252
6253            // Convert this operation into a replace operation.
6254            mType = TYPE_REPLACE;
6255            mNewText = finalText.toString();
6256            mOldText = originalText.toString();
6257            mStart = 0;
6258            mNewCursorPos = edit.mNewCursorPos;
6259            mIsComposition = edit.mIsComposition;
6260            // mOldCursorPos is unchanged.
6261        }
6262
6263        private static void modifyText(Editable text, int deleteFrom, int deleteTo,
6264                CharSequence newText, int newTextInsertAt, int newCursorPos) {
6265            // Apply the edit if it is still valid.
6266            if (isValidRange(text, deleteFrom, deleteTo)
6267                    && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) {
6268                if (deleteFrom != deleteTo) {
6269                    text.delete(deleteFrom, deleteTo);
6270                }
6271                if (newText.length() != 0) {
6272                    text.insert(newTextInsertAt, newText);
6273                }
6274            }
6275            // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then
6276            // don't explicitly set it and rely on SpannableStringBuilder to position it.
6277            // TODO: Select all the text that was undone.
6278            if (0 <= newCursorPos && newCursorPos <= text.length()) {
6279                Selection.setSelection(text, newCursorPos);
6280            }
6281        }
6282
6283        private String getTypeString() {
6284            switch (mType) {
6285                case TYPE_INSERT:
6286                    return "insert";
6287                case TYPE_DELETE:
6288                    return "delete";
6289                case TYPE_REPLACE:
6290                    return "replace";
6291                default:
6292                    return "";
6293            }
6294        }
6295
6296        @Override
6297        public String toString() {
6298            return "[mType=" + getTypeString() + ", "
6299                    + "mOldText=" + mOldText + ", "
6300                    + "mNewText=" + mNewText + ", "
6301                    + "mStart=" + mStart + ", "
6302                    + "mOldCursorPos=" + mOldCursorPos + ", "
6303                    + "mNewCursorPos=" + mNewCursorPos + ", "
6304                    + "mFrozen=" + mFrozen + ", "
6305                    + "mIsComposition=" + mIsComposition + "]";
6306        }
6307
6308        public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR =
6309                new Parcelable.ClassLoaderCreator<EditOperation>() {
6310            @Override
6311            public EditOperation createFromParcel(Parcel in) {
6312                return new EditOperation(in, null);
6313            }
6314
6315            @Override
6316            public EditOperation createFromParcel(Parcel in, ClassLoader loader) {
6317                return new EditOperation(in, loader);
6318            }
6319
6320            @Override
6321            public EditOperation[] newArray(int size) {
6322                return new EditOperation[size];
6323            }
6324        };
6325    }
6326
6327    /**
6328     * A helper for enabling and handling "PROCESS_TEXT" menu actions.
6329     * These allow external applications to plug into currently selected text.
6330     */
6331    static final class ProcessTextIntentActionsHandler {
6332
6333        private final Editor mEditor;
6334        private final TextView mTextView;
6335        private final Context mContext;
6336        private final PackageManager mPackageManager;
6337        private final String mPackageName;
6338        private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>();
6339        private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions =
6340                new SparseArray<>();
6341        private final List<ResolveInfo> mSupportedActivities = new ArrayList<>();
6342
6343        private ProcessTextIntentActionsHandler(Editor editor) {
6344            mEditor = Preconditions.checkNotNull(editor);
6345            mTextView = Preconditions.checkNotNull(mEditor.mTextView);
6346            mContext = Preconditions.checkNotNull(mTextView.getContext());
6347            mPackageManager = Preconditions.checkNotNull(mContext.getPackageManager());
6348            mPackageName = Preconditions.checkNotNull(mContext.getPackageName());
6349        }
6350
6351        /**
6352         * Adds "PROCESS_TEXT" menu items to the specified menu.
6353         */
6354        public void onInitializeMenu(Menu menu) {
6355            final int size = mSupportedActivities.size();
6356            loadSupportedActivities();
6357            for (int i = 0; i < size; i++) {
6358                final ResolveInfo resolveInfo = mSupportedActivities.get(i);
6359                menu.add(Menu.NONE, Menu.NONE,
6360                        Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++,
6361                        getLabel(resolveInfo))
6362                        .setIntent(createProcessTextIntentForResolveInfo(resolveInfo))
6363                        .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
6364            }
6365        }
6366
6367        /**
6368         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6369         * menu item.
6370         *
6371         * @return True if the action was performed, false otherwise.
6372         */
6373        public boolean performMenuItemAction(MenuItem item) {
6374            return fireIntent(item.getIntent());
6375        }
6376
6377        /**
6378         * Initializes and caches "PROCESS_TEXT" accessibility actions.
6379         */
6380        public void initializeAccessibilityActions() {
6381            mAccessibilityIntents.clear();
6382            mAccessibilityActions.clear();
6383            int i = 0;
6384            loadSupportedActivities();
6385            for (ResolveInfo resolveInfo : mSupportedActivities) {
6386                int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++;
6387                mAccessibilityActions.put(
6388                        actionId,
6389                        new AccessibilityNodeInfo.AccessibilityAction(
6390                                actionId, getLabel(resolveInfo)));
6391                mAccessibilityIntents.put(
6392                        actionId, createProcessTextIntentForResolveInfo(resolveInfo));
6393            }
6394        }
6395
6396        /**
6397         * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info.
6398         * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the
6399         * latest accessibility actions available for this call.
6400         */
6401        public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) {
6402            for (int i = 0; i < mAccessibilityActions.size(); i++) {
6403                nodeInfo.addAction(mAccessibilityActions.valueAt(i));
6404            }
6405        }
6406
6407        /**
6408         * Performs a "PROCESS_TEXT" action if there is one associated with the specified
6409         * accessibility action id.
6410         *
6411         * @return True if the action was performed, false otherwise.
6412         */
6413        public boolean performAccessibilityAction(int actionId) {
6414            return fireIntent(mAccessibilityIntents.get(actionId));
6415        }
6416
6417        private boolean fireIntent(Intent intent) {
6418            if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) {
6419                intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText());
6420                mEditor.mPreserveSelection = true;
6421                mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE);
6422                return true;
6423            }
6424            return false;
6425        }
6426
6427        private void loadSupportedActivities() {
6428            mSupportedActivities.clear();
6429            PackageManager packageManager = mTextView.getContext().getPackageManager();
6430            List<ResolveInfo> unfiltered =
6431                    packageManager.queryIntentActivities(createProcessTextIntent(), 0);
6432            for (ResolveInfo info : unfiltered) {
6433                if (isSupportedActivity(info)) {
6434                    mSupportedActivities.add(info);
6435                }
6436            }
6437        }
6438
6439        private boolean isSupportedActivity(ResolveInfo info) {
6440            return mPackageName.equals(info.activityInfo.packageName)
6441                    || info.activityInfo.exported
6442                            && (info.activityInfo.permission == null
6443                                    || mContext.checkSelfPermission(info.activityInfo.permission)
6444                                            == PackageManager.PERMISSION_GRANTED);
6445        }
6446
6447        private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) {
6448            return createProcessTextIntent()
6449                    .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable())
6450                    .setClassName(info.activityInfo.packageName, info.activityInfo.name);
6451        }
6452
6453        private Intent createProcessTextIntent() {
6454            return new Intent()
6455                    .setAction(Intent.ACTION_PROCESS_TEXT)
6456                    .setType("text/plain");
6457        }
6458
6459        private CharSequence getLabel(ResolveInfo resolveInfo) {
6460            return resolveInfo.loadLabel(mPackageManager);
6461        }
6462    }
6463}
6464