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