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