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