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