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