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