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