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