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