Editor.java revision fa2d7b1f21641a94134906bbadcf1cdcef54697e
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.freezeLastEdit();
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        mUndoInputFilter.freezeLastEdit();
2408        try {
2409            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
2410            Object localState = event.getLocalState();
2411            DragLocalState dragLocalState = null;
2412            if (localState instanceof DragLocalState) {
2413                dragLocalState = (DragLocalState) localState;
2414            }
2415            boolean dragDropIntoItself = dragLocalState != null &&
2416                    dragLocalState.sourceTextView == mTextView;
2417
2418            if (dragDropIntoItself) {
2419                if (offset >= dragLocalState.start && offset < dragLocalState.end) {
2420                    // A drop inside the original selection discards the drop.
2421                    return;
2422                }
2423            }
2424
2425            final int originalLength = mTextView.getText().length();
2426            int min = offset;
2427            int max = offset;
2428
2429            Selection.setSelection((Spannable) mTextView.getText(), max);
2430            mTextView.replaceText_internal(min, max, content);
2431
2432            if (dragDropIntoItself) {
2433                int dragSourceStart = dragLocalState.start;
2434                int dragSourceEnd = dragLocalState.end;
2435                if (max <= dragSourceStart) {
2436                    // Inserting text before selection has shifted positions
2437                    final int shift = mTextView.getText().length() - originalLength;
2438                    dragSourceStart += shift;
2439                    dragSourceEnd += shift;
2440                }
2441
2442                // Delete original selection
2443                mTextView.deleteText_internal(dragSourceStart, dragSourceEnd);
2444
2445                // Make sure we do not leave two adjacent spaces.
2446                final int prevCharIdx = Math.max(0,  dragSourceStart - 1);
2447                final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1);
2448                if (nextCharIdx > prevCharIdx + 1) {
2449                    CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx);
2450                    if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) {
2451                        mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1);
2452                    }
2453                }
2454            }
2455        } finally {
2456            mTextView.endBatchEdit();
2457            mUndoInputFilter.freezeLastEdit();
2458        }
2459    }
2460
2461    public void addSpanWatchers(Spannable text) {
2462        final int textLength = text.length();
2463
2464        if (mKeyListener != null) {
2465            text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2466        }
2467
2468        if (mSpanController == null) {
2469            mSpanController = new SpanController();
2470        }
2471        text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE);
2472    }
2473
2474    void setContextMenuAnchor(float x, float y) {
2475        mContextMenuAnchorX = x;
2476        mContextMenuAnchorY = y;
2477    }
2478
2479    void onCreateContextMenu(ContextMenu menu) {
2480        if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX)
2481                || Float.isNaN(mContextMenuAnchorY)) {
2482            return;
2483        }
2484        final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY);
2485        if (offset == -1) {
2486            return;
2487        }
2488        stopTextActionModeWithPreservingSelection();
2489        final boolean isOnSelection = mTextView.hasSelection()
2490                && offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd();
2491        if (!isOnSelection) {
2492            // Right clicked position is not on the selection. Remove the selection and move the
2493            // cursor to the right clicked position.
2494            Selection.setSelection((Spannable) mTextView.getText(), offset);
2495            stopTextActionMode();
2496        }
2497
2498        if (shouldOfferToShowSuggestions()) {
2499            final SuggestionInfo[] suggestionInfoArray =
2500                    new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE];
2501            for (int i = 0; i < suggestionInfoArray.length; i++) {
2502                suggestionInfoArray[i] = new SuggestionInfo();
2503            }
2504            final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE,
2505                    com.android.internal.R.string.replace);
2506            final int numItems = mSuggestionHelper.getSuggestionInfo(suggestionInfoArray, null);
2507            for (int i = 0; i < numItems; i++) {
2508                final SuggestionInfo info = suggestionInfoArray[i];
2509                subMenu.add(Menu.NONE, Menu.NONE, i, info.mText)
2510                        .setOnMenuItemClickListener(new MenuItem.OnMenuItemClickListener() {
2511                            @Override
2512                            public boolean onMenuItemClick(MenuItem item) {
2513                                replaceWithSuggestion(info);
2514                                return true;
2515                            }
2516                        });
2517            }
2518        }
2519
2520        menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO,
2521                com.android.internal.R.string.undo)
2522                .setAlphabeticShortcut('z')
2523                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2524                .setEnabled(mTextView.canUndo());
2525        menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO,
2526                com.android.internal.R.string.redo)
2527                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2528                .setEnabled(mTextView.canRedo());
2529
2530        menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
2531                com.android.internal.R.string.cut)
2532                .setAlphabeticShortcut('x')
2533                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2534                .setEnabled(mTextView.canCut());
2535        menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
2536                com.android.internal.R.string.copy)
2537                .setAlphabeticShortcut('c')
2538                .setOnMenuItemClickListener(mOnContextMenuItemClickListener)
2539                .setEnabled(mTextView.canCopy());
2540        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
2541                com.android.internal.R.string.paste)
2542                .setAlphabeticShortcut('v')
2543                .setEnabled(mTextView.canPaste())
2544                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2545        menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT,
2546                com.android.internal.R.string.paste_as_plain_text)
2547                .setEnabled(mTextView.canPaste())
2548                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2549        menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
2550                com.android.internal.R.string.share)
2551                .setEnabled(mTextView.canShare())
2552                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2553        menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
2554                com.android.internal.R.string.selectAll)
2555                .setAlphabeticShortcut('a')
2556                .setEnabled(mTextView.canSelectAllText())
2557                .setOnMenuItemClickListener(mOnContextMenuItemClickListener);
2558
2559        mPreserveSelection = true;
2560    }
2561
2562    @Nullable
2563    private SuggestionSpan findEquivalentSuggestionSpan(
2564            @NonNull SuggestionSpanInfo suggestionSpanInfo) {
2565        final Editable editable = (Editable) mTextView.getText();
2566        if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) {
2567            // Exactly same span is found.
2568            return suggestionSpanInfo.mSuggestionSpan;
2569        }
2570        // Suggestion span couldn't be found. Try to find a suggestion span that has the same
2571        // contents.
2572        final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart,
2573                suggestionSpanInfo.mSpanEnd, SuggestionSpan.class);
2574        for (final SuggestionSpan suggestionSpan : suggestionSpans) {
2575            final int start = editable.getSpanStart(suggestionSpan);
2576            if (start != suggestionSpanInfo.mSpanStart) {
2577                continue;
2578            }
2579            final int end = editable.getSpanEnd(suggestionSpan);
2580            if (end != suggestionSpanInfo.mSpanEnd) {
2581                continue;
2582            }
2583            if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) {
2584                return suggestionSpan;
2585            }
2586        }
2587        return null;
2588    }
2589
2590    private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) {
2591        final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan(
2592                suggestionInfo.mSuggestionSpanInfo);
2593        if (targetSuggestionSpan == null) {
2594            // Span has been removed
2595            return;
2596        }
2597        final Editable editable = (Editable) mTextView.getText();
2598        final int spanStart = editable.getSpanStart(targetSuggestionSpan);
2599        final int spanEnd = editable.getSpanEnd(targetSuggestionSpan);
2600        if (spanStart < 0 || spanEnd <= spanStart) {
2601            // Span has been removed
2602            return;
2603        }
2604
2605        final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
2606        // SuggestionSpans are removed by replace: save them before
2607        SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd,
2608                SuggestionSpan.class);
2609        final int length = suggestionSpans.length;
2610        int[] suggestionSpansStarts = new int[length];
2611        int[] suggestionSpansEnds = new int[length];
2612        int[] suggestionSpansFlags = new int[length];
2613        for (int i = 0; i < length; i++) {
2614            final SuggestionSpan suggestionSpan = suggestionSpans[i];
2615            suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan);
2616            suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan);
2617            suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan);
2618
2619            // Remove potential misspelled flags
2620            int suggestionSpanFlags = suggestionSpan.getFlags();
2621            if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) {
2622                suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED;
2623                suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT;
2624                suggestionSpan.setFlags(suggestionSpanFlags);
2625            }
2626        }
2627
2628        // Notify source IME of the suggestion pick. Do this before swapping texts.
2629        targetSuggestionSpan.notifySelection(
2630                mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex);
2631
2632        // Swap text content between actual text and Suggestion span
2633        final int suggestionStart = suggestionInfo.mSuggestionStart;
2634        final int suggestionEnd = suggestionInfo.mSuggestionEnd;
2635        final String suggestion = suggestionInfo.mText.subSequence(
2636                suggestionStart, suggestionEnd).toString();
2637        mTextView.replaceText_internal(spanStart, spanEnd, suggestion);
2638
2639        String[] suggestions = targetSuggestionSpan.getSuggestions();
2640        suggestions[suggestionInfo.mSuggestionIndex] = originalText;
2641
2642        // Restore previous SuggestionSpans
2643        final int lengthDelta = suggestion.length() - (spanEnd - spanStart);
2644        for (int i = 0; i < length; i++) {
2645            // Only spans that include the modified region make sense after replacement
2646            // Spans partially included in the replaced region are removed, there is no
2647            // way to assign them a valid range after replacement
2648            if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) {
2649                mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i],
2650                        suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]);
2651            }
2652        }
2653        // Move cursor at the end of the replaced word
2654        final int newCursorPosition = spanEnd + lengthDelta;
2655        mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition);
2656    }
2657
2658    private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener =
2659            new MenuItem.OnMenuItemClickListener() {
2660        @Override
2661        public boolean onMenuItemClick(MenuItem item) {
2662            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
2663                return true;
2664            }
2665            return mTextView.onTextContextMenuItem(item.getItemId());
2666        }
2667    };
2668
2669    /**
2670     * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related
2671     * pop-up should be displayed.
2672     * Also monitors {@link Selection} to call back to the attached input method.
2673     */
2674    private class SpanController implements SpanWatcher {
2675
2676        private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs
2677
2678        private EasyEditPopupWindow mPopupWindow;
2679
2680        private Runnable mHidePopup;
2681
2682        // This function is pure but inner classes can't have static functions
2683        private boolean isNonIntermediateSelectionSpan(final Spannable text,
2684                final Object span) {
2685            return (Selection.SELECTION_START == span || Selection.SELECTION_END == span)
2686                    && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0;
2687        }
2688
2689        @Override
2690        public void onSpanAdded(Spannable text, Object span, int start, int end) {
2691            if (isNonIntermediateSelectionSpan(text, span)) {
2692                sendUpdateSelection();
2693            } else if (span instanceof EasyEditSpan) {
2694                if (mPopupWindow == null) {
2695                    mPopupWindow = new EasyEditPopupWindow();
2696                    mHidePopup = new Runnable() {
2697                        @Override
2698                        public void run() {
2699                            hide();
2700                        }
2701                    };
2702                }
2703
2704                // Make sure there is only at most one EasyEditSpan in the text
2705                if (mPopupWindow.mEasyEditSpan != null) {
2706                    mPopupWindow.mEasyEditSpan.setDeleteEnabled(false);
2707                }
2708
2709                mPopupWindow.setEasyEditSpan((EasyEditSpan) span);
2710                mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() {
2711                    @Override
2712                    public void onDeleteClick(EasyEditSpan span) {
2713                        Editable editable = (Editable) mTextView.getText();
2714                        int start = editable.getSpanStart(span);
2715                        int end = editable.getSpanEnd(span);
2716                        if (start >= 0 && end >= 0) {
2717                            sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span);
2718                            mTextView.deleteText_internal(start, end);
2719                        }
2720                        editable.removeSpan(span);
2721                    }
2722                });
2723
2724                if (mTextView.getWindowVisibility() != View.VISIBLE) {
2725                    // The window is not visible yet, ignore the text change.
2726                    return;
2727                }
2728
2729                if (mTextView.getLayout() == null) {
2730                    // The view has not been laid out yet, ignore the text change
2731                    return;
2732                }
2733
2734                if (extractedTextModeWillBeStarted()) {
2735                    // The input is in extract mode. Do not handle the easy edit in
2736                    // the original TextView, as the ExtractEditText will do
2737                    return;
2738                }
2739
2740                mPopupWindow.show();
2741                mTextView.removeCallbacks(mHidePopup);
2742                mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS);
2743            }
2744        }
2745
2746        @Override
2747        public void onSpanRemoved(Spannable text, Object span, int start, int end) {
2748            if (isNonIntermediateSelectionSpan(text, span)) {
2749                sendUpdateSelection();
2750            } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) {
2751                hide();
2752            }
2753        }
2754
2755        @Override
2756        public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd,
2757                int newStart, int newEnd) {
2758            if (isNonIntermediateSelectionSpan(text, span)) {
2759                sendUpdateSelection();
2760            } else if (mPopupWindow != null && span instanceof EasyEditSpan) {
2761                EasyEditSpan easyEditSpan = (EasyEditSpan) span;
2762                sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan);
2763                text.removeSpan(easyEditSpan);
2764            }
2765        }
2766
2767        public void hide() {
2768            if (mPopupWindow != null) {
2769                mPopupWindow.hide();
2770                mTextView.removeCallbacks(mHidePopup);
2771            }
2772        }
2773
2774        private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) {
2775            try {
2776                PendingIntent pendingIntent = span.getPendingIntent();
2777                if (pendingIntent != null) {
2778                    Intent intent = new Intent();
2779                    intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType);
2780                    pendingIntent.send(mTextView.getContext(), 0, intent);
2781                }
2782            } catch (CanceledException e) {
2783                // This should not happen, as we should try to send the intent only once.
2784                Log.w(TAG, "PendingIntent for notification cannot be sent", e);
2785            }
2786        }
2787    }
2788
2789    /**
2790     * Listens for the delete event triggered by {@link EasyEditPopupWindow}.
2791     */
2792    private interface EasyEditDeleteListener {
2793
2794        /**
2795         * Clicks the delete pop-up.
2796         */
2797        void onDeleteClick(EasyEditSpan span);
2798    }
2799
2800    /**
2801     * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled
2802     * by {@link SpanController}.
2803     */
2804    private class EasyEditPopupWindow extends PinnedPopupWindow
2805            implements OnClickListener {
2806        private static final int POPUP_TEXT_LAYOUT =
2807                com.android.internal.R.layout.text_edit_action_popup_text;
2808        private TextView mDeleteTextView;
2809        private EasyEditSpan mEasyEditSpan;
2810        private EasyEditDeleteListener mOnDeleteListener;
2811
2812        @Override
2813        protected void createPopupWindow() {
2814            mPopupWindow = new PopupWindow(mTextView.getContext(), null,
2815                    com.android.internal.R.attr.textSelectHandleWindowStyle);
2816            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
2817            mPopupWindow.setClippingEnabled(true);
2818        }
2819
2820        @Override
2821        protected void initContentView() {
2822            LinearLayout linearLayout = new LinearLayout(mTextView.getContext());
2823            linearLayout.setOrientation(LinearLayout.HORIZONTAL);
2824            mContentView = linearLayout;
2825            mContentView.setBackgroundResource(
2826                    com.android.internal.R.drawable.text_edit_side_paste_window);
2827
2828            LayoutInflater inflater = (LayoutInflater)mTextView.getContext().
2829                    getSystemService(Context.LAYOUT_INFLATER_SERVICE);
2830
2831            LayoutParams wrapContent = new LayoutParams(
2832                    ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
2833
2834            mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null);
2835            mDeleteTextView.setLayoutParams(wrapContent);
2836            mDeleteTextView.setText(com.android.internal.R.string.delete);
2837            mDeleteTextView.setOnClickListener(this);
2838            mContentView.addView(mDeleteTextView);
2839        }
2840
2841        public void setEasyEditSpan(EasyEditSpan easyEditSpan) {
2842            mEasyEditSpan = easyEditSpan;
2843        }
2844
2845        private void setOnDeleteListener(EasyEditDeleteListener listener) {
2846            mOnDeleteListener = listener;
2847        }
2848
2849        @Override
2850        public void onClick(View view) {
2851            if (view == mDeleteTextView
2852                    && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled()
2853                    && mOnDeleteListener != null) {
2854                mOnDeleteListener.onDeleteClick(mEasyEditSpan);
2855            }
2856        }
2857
2858        @Override
2859        public void hide() {
2860            if (mEasyEditSpan != null) {
2861                mEasyEditSpan.setDeleteEnabled(false);
2862            }
2863            mOnDeleteListener = null;
2864            super.hide();
2865        }
2866
2867        @Override
2868        protected int getTextOffset() {
2869            // Place the pop-up at the end of the span
2870            Editable editable = (Editable) mTextView.getText();
2871            return editable.getSpanEnd(mEasyEditSpan);
2872        }
2873
2874        @Override
2875        protected int getVerticalLocalPosition(int line) {
2876            return mTextView.getLayout().getLineBottom(line);
2877        }
2878
2879        @Override
2880        protected int clipVertically(int positionY) {
2881            // As we display the pop-up below the span, no vertical clipping is required.
2882            return positionY;
2883        }
2884    }
2885
2886    private class PositionListener implements ViewTreeObserver.OnPreDrawListener {
2887        // 3 handles
2888        // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others)
2889        // 1 CursorAnchorInfoNotifier
2890        private final int MAXIMUM_NUMBER_OF_LISTENERS = 7;
2891        private TextViewPositionListener[] mPositionListeners =
2892                new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS];
2893        private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS];
2894        private boolean mPositionHasChanged = true;
2895        // Absolute position of the TextView with respect to its parent window
2896        private int mPositionX, mPositionY;
2897        private int mPositionXOnScreen, mPositionYOnScreen;
2898        private int mNumberOfListeners;
2899        private boolean mScrollHasChanged;
2900        final int[] mTempCoords = new int[2];
2901
2902        public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) {
2903            if (mNumberOfListeners == 0) {
2904                updatePosition();
2905                ViewTreeObserver vto = mTextView.getViewTreeObserver();
2906                vto.addOnPreDrawListener(this);
2907            }
2908
2909            int emptySlotIndex = -1;
2910            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2911                TextViewPositionListener listener = mPositionListeners[i];
2912                if (listener == positionListener) {
2913                    return;
2914                } else if (emptySlotIndex < 0 && listener == null) {
2915                    emptySlotIndex = i;
2916                }
2917            }
2918
2919            mPositionListeners[emptySlotIndex] = positionListener;
2920            mCanMove[emptySlotIndex] = canMove;
2921            mNumberOfListeners++;
2922        }
2923
2924        public void removeSubscriber(TextViewPositionListener positionListener) {
2925            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2926                if (mPositionListeners[i] == positionListener) {
2927                    mPositionListeners[i] = null;
2928                    mNumberOfListeners--;
2929                    break;
2930                }
2931            }
2932
2933            if (mNumberOfListeners == 0) {
2934                ViewTreeObserver vto = mTextView.getViewTreeObserver();
2935                vto.removeOnPreDrawListener(this);
2936            }
2937        }
2938
2939        public int getPositionX() {
2940            return mPositionX;
2941        }
2942
2943        public int getPositionY() {
2944            return mPositionY;
2945        }
2946
2947        public int getPositionXOnScreen() {
2948            return mPositionXOnScreen;
2949        }
2950
2951        public int getPositionYOnScreen() {
2952            return mPositionYOnScreen;
2953        }
2954
2955        @Override
2956        public boolean onPreDraw() {
2957            updatePosition();
2958
2959            for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) {
2960                if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) {
2961                    TextViewPositionListener positionListener = mPositionListeners[i];
2962                    if (positionListener != null) {
2963                        positionListener.updatePosition(mPositionX, mPositionY,
2964                                mPositionHasChanged, mScrollHasChanged);
2965                    }
2966                }
2967            }
2968
2969            mScrollHasChanged = false;
2970            return true;
2971        }
2972
2973        private void updatePosition() {
2974            mTextView.getLocationInWindow(mTempCoords);
2975
2976            mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY;
2977
2978            mPositionX = mTempCoords[0];
2979            mPositionY = mTempCoords[1];
2980
2981            mTextView.getLocationOnScreen(mTempCoords);
2982
2983            mPositionXOnScreen = mTempCoords[0];
2984            mPositionYOnScreen = mTempCoords[1];
2985        }
2986
2987        public void onScrollChanged() {
2988            mScrollHasChanged = true;
2989        }
2990    }
2991
2992    private abstract class PinnedPopupWindow implements TextViewPositionListener {
2993        protected PopupWindow mPopupWindow;
2994        protected ViewGroup mContentView;
2995        int mPositionX, mPositionY;
2996        int mClippingLimitLeft, mClippingLimitRight;
2997
2998        protected abstract void createPopupWindow();
2999        protected abstract void initContentView();
3000        protected abstract int getTextOffset();
3001        protected abstract int getVerticalLocalPosition(int line);
3002        protected abstract int clipVertically(int positionY);
3003        protected void setUp() {
3004        }
3005
3006        public PinnedPopupWindow() {
3007            // Due to calling subclass methods in base constructor, subclass constructor is not
3008            // called before subclass methods, e.g. createPopupWindow or initContentView. To give
3009            // a chance to initialize subclasses, call setUp() method here.
3010            // TODO: It is good to extract non trivial initialization code from constructor.
3011            setUp();
3012
3013            createPopupWindow();
3014
3015            mPopupWindow.setWindowLayoutType(
3016                    WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL);
3017            mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
3018            mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
3019
3020            initContentView();
3021
3022            LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT,
3023                    ViewGroup.LayoutParams.WRAP_CONTENT);
3024            mContentView.setLayoutParams(wrapContent);
3025
3026            mPopupWindow.setContentView(mContentView);
3027        }
3028
3029        public void show() {
3030            getPositionListener().addSubscriber(this, false /* offset is fixed */);
3031
3032            computeLocalPosition();
3033
3034            final PositionListener positionListener = getPositionListener();
3035            updatePosition(positionListener.getPositionX(), positionListener.getPositionY());
3036        }
3037
3038        protected void measureContent() {
3039            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3040            mContentView.measure(
3041                    View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels,
3042                            View.MeasureSpec.AT_MOST),
3043                    View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels,
3044                            View.MeasureSpec.AT_MOST));
3045        }
3046
3047        /* The popup window will be horizontally centered on the getTextOffset() and vertically
3048         * positioned according to viewportToContentHorizontalOffset.
3049         *
3050         * This method assumes that mContentView has properly been measured from its content. */
3051        private void computeLocalPosition() {
3052            measureContent();
3053            final int width = mContentView.getMeasuredWidth();
3054            final int offset = getTextOffset();
3055            mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f);
3056            mPositionX += mTextView.viewportToContentHorizontalOffset();
3057
3058            final int line = mTextView.getLayout().getLineForOffset(offset);
3059            mPositionY = getVerticalLocalPosition(line);
3060            mPositionY += mTextView.viewportToContentVerticalOffset();
3061        }
3062
3063        private void updatePosition(int parentPositionX, int parentPositionY) {
3064            int positionX = parentPositionX + mPositionX;
3065            int positionY = parentPositionY + mPositionY;
3066
3067            positionY = clipVertically(positionY);
3068
3069            // Horizontal clipping
3070            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3071            final int width = mContentView.getMeasuredWidth();
3072            positionX = Math.min(
3073                    displayMetrics.widthPixels - width + mClippingLimitRight, positionX);
3074            positionX = Math.max(-mClippingLimitLeft, positionX);
3075
3076            if (isShowing()) {
3077                mPopupWindow.update(positionX, positionY, -1, -1);
3078            } else {
3079                mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY,
3080                        positionX, positionY);
3081            }
3082        }
3083
3084        public void hide() {
3085            if (!isShowing()) {
3086                return;
3087            }
3088            mPopupWindow.dismiss();
3089            getPositionListener().removeSubscriber(this);
3090        }
3091
3092        @Override
3093        public void updatePosition(int parentPositionX, int parentPositionY,
3094                boolean parentPositionChanged, boolean parentScrolled) {
3095            // Either parentPositionChanged or parentScrolled is true, check if still visible
3096            if (isShowing() && isOffsetVisible(getTextOffset())) {
3097                if (parentScrolled) computeLocalPosition();
3098                updatePosition(parentPositionX, parentPositionY);
3099            } else {
3100                hide();
3101            }
3102        }
3103
3104        public boolean isShowing() {
3105            return mPopupWindow.isShowing();
3106        }
3107    }
3108
3109    private static final class SuggestionInfo {
3110        // Range of actual suggestion within mText
3111        int mSuggestionStart, mSuggestionEnd;
3112
3113        // The SuggestionSpan that this TextView represents
3114        final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo();
3115
3116        // The index of this suggestion inside suggestionSpan
3117        int mSuggestionIndex;
3118
3119        final SpannableStringBuilder mText = new SpannableStringBuilder();
3120
3121        void clear() {
3122            mSuggestionSpanInfo.clear();
3123            mText.clear();
3124        }
3125
3126        // Utility method to set attributes about a SuggestionSpan.
3127        void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) {
3128            mSuggestionSpanInfo.mSuggestionSpan = span;
3129            mSuggestionSpanInfo.mSpanStart = spanStart;
3130            mSuggestionSpanInfo.mSpanEnd = spanEnd;
3131        }
3132    }
3133
3134    private static final class SuggestionSpanInfo {
3135        // The SuggestionSpan;
3136        @Nullable
3137        SuggestionSpan mSuggestionSpan;
3138
3139        // The SuggestionSpan start position
3140        int mSpanStart;
3141
3142        // The SuggestionSpan end position
3143        int mSpanEnd;
3144
3145        void clear() {
3146            mSuggestionSpan = null;
3147        }
3148    }
3149
3150    private class SuggestionHelper {
3151        private final Comparator<SuggestionSpan> mSuggestionSpanComparator =
3152                new SuggestionSpanComparator();
3153        private final HashMap<SuggestionSpan, Integer> mSpansLengths =
3154                new HashMap<SuggestionSpan, Integer>();
3155
3156        private class SuggestionSpanComparator implements Comparator<SuggestionSpan> {
3157            public int compare(SuggestionSpan span1, SuggestionSpan span2) {
3158                final int flag1 = span1.getFlags();
3159                final int flag2 = span2.getFlags();
3160                if (flag1 != flag2) {
3161                    // The order here should match what is used in updateDrawState
3162                    final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3163                    final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0;
3164                    final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3165                    final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0;
3166                    if (easy1 && !misspelled1) return -1;
3167                    if (easy2 && !misspelled2) return 1;
3168                    if (misspelled1) return -1;
3169                    if (misspelled2) return 1;
3170                }
3171
3172                return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue();
3173            }
3174        }
3175
3176        /**
3177         * Returns the suggestion spans that cover the current cursor position. The suggestion
3178         * spans are sorted according to the length of text that they are attached to.
3179         */
3180        private SuggestionSpan[] getSortedSuggestionSpans() {
3181            int pos = mTextView.getSelectionStart();
3182            Spannable spannable = (Spannable) mTextView.getText();
3183            SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class);
3184
3185            mSpansLengths.clear();
3186            for (SuggestionSpan suggestionSpan : suggestionSpans) {
3187                int start = spannable.getSpanStart(suggestionSpan);
3188                int end = spannable.getSpanEnd(suggestionSpan);
3189                mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start));
3190            }
3191
3192            // The suggestions are sorted according to their types (easy correction first, then
3193            // misspelled) and to the length of the text that they cover (shorter first).
3194            Arrays.sort(suggestionSpans, mSuggestionSpanComparator);
3195            mSpansLengths.clear();
3196
3197            return suggestionSpans;
3198        }
3199
3200        /**
3201         * Gets the SuggestionInfo list that contains suggestion information at the current cursor
3202         * position.
3203         *
3204         * @param suggestionInfos SuggestionInfo array the results will be set.
3205         * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set.
3206         * @return the number of suggestions actually fetched.
3207         */
3208        public int getSuggestionInfo(SuggestionInfo[] suggestionInfos,
3209                @Nullable SuggestionSpanInfo misspelledSpanInfo) {
3210            final Spannable spannable = (Spannable) mTextView.getText();
3211            final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans();
3212            final int nbSpans = suggestionSpans.length;
3213            if (nbSpans == 0) return 0;
3214
3215            int numberOfSuggestions = 0;
3216            for (final SuggestionSpan suggestionSpan : suggestionSpans) {
3217                final int spanStart = spannable.getSpanStart(suggestionSpan);
3218                final int spanEnd = spannable.getSpanEnd(suggestionSpan);
3219
3220                if (misspelledSpanInfo != null
3221                        && (suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) {
3222                    misspelledSpanInfo.mSuggestionSpan = suggestionSpan;
3223                    misspelledSpanInfo.mSpanStart = spanStart;
3224                    misspelledSpanInfo.mSpanEnd = spanEnd;
3225                }
3226
3227                final String[] suggestions = suggestionSpan.getSuggestions();
3228                final int nbSuggestions = suggestions.length;
3229                suggestionLoop:
3230                for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) {
3231                    final String suggestion = suggestions[suggestionIndex];
3232                    for (int i = 0; i < numberOfSuggestions; i++) {
3233                        final SuggestionInfo otherSuggestionInfo = suggestionInfos[i];
3234                        if (otherSuggestionInfo.mText.toString().equals(suggestion)) {
3235                            final int otherSpanStart =
3236                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart;
3237                            final int otherSpanEnd =
3238                                    otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd;
3239                            if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) {
3240                                continue suggestionLoop;
3241                            }
3242                        }
3243                    }
3244
3245                    SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions];
3246                    suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd);
3247                    suggestionInfo.mSuggestionIndex = suggestionIndex;
3248                    suggestionInfo.mSuggestionStart = 0;
3249                    suggestionInfo.mSuggestionEnd = suggestion.length();
3250                    suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion);
3251                    numberOfSuggestions++;
3252                    if (numberOfSuggestions >= suggestionInfos.length) {
3253                        return numberOfSuggestions;
3254                    }
3255                }
3256            }
3257            return numberOfSuggestions;
3258        }
3259    }
3260
3261    @VisibleForTesting
3262    public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener {
3263        private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE;
3264
3265        // Key of intent extras for inserting new word into user dictionary.
3266        private static final String USER_DICTIONARY_EXTRA_WORD = "word";
3267        private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale";
3268
3269        private SuggestionInfo[] mSuggestionInfos;
3270        private int mNumberOfSuggestions;
3271        private boolean mCursorWasVisibleBeforeSuggestions;
3272        private boolean mIsShowingUp = false;
3273        private SuggestionAdapter mSuggestionsAdapter;
3274        private TextAppearanceSpan mHighlightSpan;  // TODO: Make mHighlightSpan final.
3275        private TextView mAddToDictionaryButton;
3276        private TextView mDeleteButton;
3277        private ListView mSuggestionListView;
3278        private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo();
3279        private int mContainerMarginWidth;
3280        private int mContainerMarginTop;
3281        private LinearLayout mContainerView;
3282        private Context mContext;  // TODO: Make mContext final.
3283
3284        private class CustomPopupWindow extends PopupWindow {
3285
3286            @Override
3287            public void dismiss() {
3288                if (!isShowing()) {
3289                    return;
3290                }
3291                super.dismiss();
3292                getPositionListener().removeSubscriber(SuggestionsPopupWindow.this);
3293
3294                // Safe cast since show() checks that mTextView.getText() is an Editable
3295                ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan);
3296
3297                mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions);
3298                if (hasInsertionController() && !extractedTextModeWillBeStarted()) {
3299                    getInsertionController().show();
3300                }
3301            }
3302        }
3303
3304        public SuggestionsPopupWindow() {
3305            mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3306        }
3307
3308        @Override
3309        protected void setUp() {
3310            mContext = applyDefaultTheme(mTextView.getContext());
3311            mHighlightSpan = new TextAppearanceSpan(mContext,
3312                    mTextView.mTextEditSuggestionHighlightStyle);
3313        }
3314
3315        private Context applyDefaultTheme(Context originalContext) {
3316            TypedArray a = originalContext.obtainStyledAttributes(
3317                    new int[]{com.android.internal.R.attr.isLightTheme});
3318            boolean isLightTheme = a.getBoolean(0, true);
3319            int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light
3320                    : R.style.ThemeOverlay_Material_Dark;
3321            a.recycle();
3322            return new ContextThemeWrapper(originalContext, themeId);
3323        }
3324
3325        @Override
3326        protected void createPopupWindow() {
3327            mPopupWindow = new CustomPopupWindow();
3328            mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED);
3329            mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
3330            mPopupWindow.setFocusable(true);
3331            mPopupWindow.setClippingEnabled(false);
3332        }
3333
3334        @Override
3335        protected void initContentView() {
3336            final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService(
3337                    Context.LAYOUT_INFLATER_SERVICE);
3338            mContentView = (ViewGroup) inflater.inflate(
3339                    mTextView.mTextEditSuggestionContainerLayout, null);
3340
3341            mContainerView = (LinearLayout) mContentView.findViewById(
3342                    com.android.internal.R.id.suggestionWindowContainer);
3343            ViewGroup.MarginLayoutParams lp =
3344                    (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams();
3345            mContainerMarginWidth = lp.leftMargin + lp.rightMargin;
3346            mContainerMarginTop = lp.topMargin;
3347            mClippingLimitLeft = lp.leftMargin;
3348            mClippingLimitRight = lp.rightMargin;
3349
3350            mSuggestionListView = (ListView) mContentView.findViewById(
3351                    com.android.internal.R.id.suggestionContainer);
3352
3353            mSuggestionsAdapter = new SuggestionAdapter();
3354            mSuggestionListView.setAdapter(mSuggestionsAdapter);
3355            mSuggestionListView.setOnItemClickListener(this);
3356
3357            // Inflate the suggestion items once and for all.
3358            mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS];
3359            for (int i = 0; i < mSuggestionInfos.length; i++) {
3360                mSuggestionInfos[i] = new SuggestionInfo();
3361            }
3362
3363            mAddToDictionaryButton = (TextView) mContentView.findViewById(
3364                    com.android.internal.R.id.addToDictionaryButton);
3365            mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() {
3366                public void onClick(View v) {
3367                    final SuggestionSpan misspelledSpan =
3368                            findEquivalentSuggestionSpan(mMisspelledSpanInfo);
3369                    if (misspelledSpan == null) {
3370                        // Span has been removed.
3371                        return;
3372                    }
3373                    final Editable editable = (Editable) mTextView.getText();
3374                    final int spanStart = editable.getSpanStart(misspelledSpan);
3375                    final int spanEnd = editable.getSpanEnd(misspelledSpan);
3376                    if (spanStart < 0 || spanEnd <= spanStart) {
3377                        return;
3378                    }
3379                    final String originalText = TextUtils.substring(editable, spanStart, spanEnd);
3380
3381                    final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT);
3382                    intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText);
3383                    intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE,
3384                            mTextView.getTextServicesLocale().toString());
3385                    intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK);
3386                    mTextView.getContext().startActivity(intent);
3387                    // There is no way to know if the word was indeed added. Re-check.
3388                    // TODO The ExtractEditText should remove the span in the original text instead
3389                    editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan);
3390                    Selection.setSelection(editable, spanEnd);
3391                    updateSpellCheckSpans(spanStart, spanEnd, false);
3392                    hideWithCleanUp();
3393                }
3394            });
3395
3396            mDeleteButton = (TextView) mContentView.findViewById(
3397                    com.android.internal.R.id.deleteButton);
3398            mDeleteButton.setOnClickListener(new View.OnClickListener() {
3399                public void onClick(View v) {
3400                    final Editable editable = (Editable) mTextView.getText();
3401
3402                    final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan);
3403                    int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan);
3404                    if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) {
3405                        // Do not leave two adjacent spaces after deletion, or one at beginning of
3406                        // text
3407                        if (spanUnionEnd < editable.length() &&
3408                                Character.isSpaceChar(editable.charAt(spanUnionEnd)) &&
3409                                (spanUnionStart == 0 ||
3410                                Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) {
3411                            spanUnionEnd = spanUnionEnd + 1;
3412                        }
3413                        mTextView.deleteText_internal(spanUnionStart, spanUnionEnd);
3414                    }
3415                    hideWithCleanUp();
3416                }
3417            });
3418
3419        }
3420
3421        public boolean isShowingUp() {
3422            return mIsShowingUp;
3423        }
3424
3425        public void onParentLostFocus() {
3426            mIsShowingUp = false;
3427        }
3428
3429        private class SuggestionAdapter extends BaseAdapter {
3430            private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService(
3431                    Context.LAYOUT_INFLATER_SERVICE);
3432
3433            @Override
3434            public int getCount() {
3435                return mNumberOfSuggestions;
3436            }
3437
3438            @Override
3439            public Object getItem(int position) {
3440                return mSuggestionInfos[position];
3441            }
3442
3443            @Override
3444            public long getItemId(int position) {
3445                return position;
3446            }
3447
3448            @Override
3449            public View getView(int position, View convertView, ViewGroup parent) {
3450                TextView textView = (TextView) convertView;
3451
3452                if (textView == null) {
3453                    textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout,
3454                            parent, false);
3455                }
3456
3457                final SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3458                textView.setText(suggestionInfo.mText);
3459                return textView;
3460            }
3461        }
3462
3463        @VisibleForTesting
3464        public ViewGroup getContentViewForTesting() {
3465            return mContentView;
3466        }
3467
3468        @Override
3469        public void show() {
3470            if (!(mTextView.getText() instanceof Editable)) return;
3471            if (extractedTextModeWillBeStarted()) {
3472                return;
3473            }
3474
3475            if (updateSuggestions()) {
3476                mCursorWasVisibleBeforeSuggestions = mCursorVisible;
3477                mTextView.setCursorVisible(false);
3478                mIsShowingUp = true;
3479                super.show();
3480            }
3481        }
3482
3483        @Override
3484        protected void measureContent() {
3485            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3486            final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec(
3487                    displayMetrics.widthPixels, View.MeasureSpec.AT_MOST);
3488            final int verticalMeasure = View.MeasureSpec.makeMeasureSpec(
3489                    displayMetrics.heightPixels, View.MeasureSpec.AT_MOST);
3490
3491            int width = 0;
3492            View view = null;
3493            for (int i = 0; i < mNumberOfSuggestions; i++) {
3494                view = mSuggestionsAdapter.getView(i, view, mContentView);
3495                view.getLayoutParams().width = LayoutParams.WRAP_CONTENT;
3496                view.measure(horizontalMeasure, verticalMeasure);
3497                width = Math.max(width, view.getMeasuredWidth());
3498            }
3499
3500            if (mAddToDictionaryButton.getVisibility() != View.GONE) {
3501                mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure);
3502                width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth());
3503            }
3504
3505            mDeleteButton.measure(horizontalMeasure, verticalMeasure);
3506            width = Math.max(width, mDeleteButton.getMeasuredWidth());
3507
3508            width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight()
3509                    + mContainerMarginWidth;
3510
3511            // Enforce the width based on actual text widths
3512            mContentView.measure(
3513                    View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY),
3514                    verticalMeasure);
3515
3516            Drawable popupBackground = mPopupWindow.getBackground();
3517            if (popupBackground != null) {
3518                if (mTempRect == null) mTempRect = new Rect();
3519                popupBackground.getPadding(mTempRect);
3520                width += mTempRect.left + mTempRect.right;
3521            }
3522            mPopupWindow.setWidth(width);
3523        }
3524
3525        @Override
3526        protected int getTextOffset() {
3527            return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2;
3528        }
3529
3530        @Override
3531        protected int getVerticalLocalPosition(int line) {
3532            return mTextView.getLayout().getLineBottom(line) - mContainerMarginTop;
3533        }
3534
3535        @Override
3536        protected int clipVertically(int positionY) {
3537            final int height = mContentView.getMeasuredHeight();
3538            final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics();
3539            return Math.min(positionY, displayMetrics.heightPixels - height);
3540        }
3541
3542        private void hideWithCleanUp() {
3543            for (final SuggestionInfo info : mSuggestionInfos) {
3544                info.clear();
3545            }
3546            mMisspelledSpanInfo.clear();
3547            hide();
3548        }
3549
3550        private boolean updateSuggestions() {
3551            Spannable spannable = (Spannable) mTextView.getText();
3552            mNumberOfSuggestions =
3553                    mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo);
3554            if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) {
3555                return false;
3556            }
3557
3558            int spanUnionStart = mTextView.getText().length();
3559            int spanUnionEnd = 0;
3560
3561            for (int i = 0; i < mNumberOfSuggestions; i++) {
3562                final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo;
3563                spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart);
3564                spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd);
3565            }
3566            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3567                spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart);
3568                spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd);
3569            }
3570
3571            for (int i = 0; i < mNumberOfSuggestions; i++) {
3572                highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd);
3573            }
3574
3575            // Make "Add to dictionary" item visible if there is a span with the misspelled flag
3576            int addToDictionaryButtonVisibility = View.GONE;
3577            if (mMisspelledSpanInfo.mSuggestionSpan != null) {
3578                if (mMisspelledSpanInfo.mSpanStart >= 0
3579                        && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) {
3580                    addToDictionaryButtonVisibility = View.VISIBLE;
3581                }
3582            }
3583            mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility);
3584
3585            if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan();
3586            final int underlineColor;
3587            if (mNumberOfSuggestions != 0) {
3588                underlineColor =
3589                        mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor();
3590            } else {
3591                underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor();
3592            }
3593
3594            if (underlineColor == 0) {
3595                // Fallback on the default highlight color when the first span does not provide one
3596                mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor);
3597            } else {
3598                final float BACKGROUND_TRANSPARENCY = 0.4f;
3599                final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY);
3600                mSuggestionRangeSpan.setBackgroundColor(
3601                        (underlineColor & 0x00FFFFFF) + (newAlpha << 24));
3602            }
3603            spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd,
3604                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3605
3606            mSuggestionsAdapter.notifyDataSetChanged();
3607            return true;
3608        }
3609
3610        private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart,
3611                int unionEnd) {
3612            final Spannable text = (Spannable) mTextView.getText();
3613            final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart;
3614            final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd;
3615
3616            // Adjust the start/end of the suggestion span
3617            suggestionInfo.mSuggestionStart = spanStart - unionStart;
3618            suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart
3619                    + suggestionInfo.mText.length();
3620
3621            suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(),
3622                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
3623
3624            // Add the text before and after the span.
3625            final String textAsString = text.toString();
3626            suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart));
3627            suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd));
3628        }
3629
3630        @Override
3631        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
3632            SuggestionInfo suggestionInfo = mSuggestionInfos[position];
3633            replaceWithSuggestion(suggestionInfo);
3634            hideWithCleanUp();
3635        }
3636    }
3637
3638    /**
3639     * An ActionMode Callback class that is used to provide actions while in text insertion or
3640     * selection mode.
3641     *
3642     * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace
3643     * actions, depending on which of these this TextView supports and the current selection.
3644     */
3645    private class TextActionModeCallback extends ActionMode.Callback2 {
3646        private final Path mSelectionPath = new Path();
3647        private final RectF mSelectionBounds = new RectF();
3648        private final boolean mHasSelection;
3649
3650        private int mHandleHeight;
3651
3652        public TextActionModeCallback(boolean hasSelection) {
3653            mHasSelection = hasSelection;
3654            if (mHasSelection) {
3655                SelectionModifierCursorController selectionController = getSelectionController();
3656                if (selectionController.mStartHandle == null) {
3657                    // As these are for initializing selectionController, hide() must be called.
3658                    selectionController.initDrawables();
3659                    selectionController.initHandles();
3660                    selectionController.hide();
3661                }
3662                mHandleHeight = Math.max(
3663                        mSelectHandleLeft.getMinimumHeight(),
3664                        mSelectHandleRight.getMinimumHeight());
3665            } else {
3666                InsertionPointCursorController insertionController = getInsertionController();
3667                if (insertionController != null) {
3668                    insertionController.getHandle();
3669                    mHandleHeight = mSelectHandleCenter.getMinimumHeight();
3670                }
3671            }
3672        }
3673
3674        @Override
3675        public boolean onCreateActionMode(ActionMode mode, Menu menu) {
3676            mode.setTitle(null);
3677            mode.setSubtitle(null);
3678            mode.setTitleOptionalHint(true);
3679            populateMenuWithItems(menu);
3680
3681            Callback customCallback = getCustomCallback();
3682            if (customCallback != null) {
3683                if (!customCallback.onCreateActionMode(mode, menu)) {
3684                    // The custom mode can choose to cancel the action mode, dismiss selection.
3685                    Selection.setSelection((Spannable) mTextView.getText(),
3686                            mTextView.getSelectionEnd());
3687                    return false;
3688                }
3689            }
3690
3691            if (mTextView.canProcessText()) {
3692                mProcessTextIntentActionsHandler.onInitializeMenu(menu);
3693            }
3694
3695            if (menu.hasVisibleItems() || mode.getCustomView() != null) {
3696                if (mHasSelection && !mTextView.hasTransientState()) {
3697                    mTextView.setHasTransientState(true);
3698                }
3699                return true;
3700            } else {
3701                return false;
3702            }
3703        }
3704
3705        private Callback getCustomCallback() {
3706            return mHasSelection
3707                    ? mCustomSelectionActionModeCallback
3708                    : mCustomInsertionActionModeCallback;
3709        }
3710
3711        private void populateMenuWithItems(Menu menu) {
3712            if (mTextView.canCut()) {
3713                menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT,
3714                        com.android.internal.R.string.cut).
3715                    setAlphabeticShortcut('x').
3716                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3717            }
3718
3719            if (mTextView.canCopy()) {
3720                menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY,
3721                        com.android.internal.R.string.copy).
3722                    setAlphabeticShortcut('c').
3723                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3724            }
3725
3726            if (mTextView.canPaste()) {
3727                menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE,
3728                        com.android.internal.R.string.paste).
3729                    setAlphabeticShortcut('v').
3730                    setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS);
3731            }
3732
3733            if (mTextView.canShare()) {
3734                menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE,
3735                        com.android.internal.R.string.share).
3736                    setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3737            }
3738
3739            updateSelectAllItem(menu);
3740            updateReplaceItem(menu);
3741        }
3742
3743        @Override
3744        public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
3745            updateSelectAllItem(menu);
3746            updateReplaceItem(menu);
3747
3748            Callback customCallback = getCustomCallback();
3749            if (customCallback != null) {
3750                return customCallback.onPrepareActionMode(mode, menu);
3751            }
3752            return true;
3753        }
3754
3755        private void updateSelectAllItem(Menu menu) {
3756            boolean canSelectAll = mTextView.canSelectAllText();
3757            boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null;
3758            if (canSelectAll && !selectAllItemExists) {
3759                menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL,
3760                        com.android.internal.R.string.selectAll)
3761                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3762            } else if (!canSelectAll && selectAllItemExists) {
3763                menu.removeItem(TextView.ID_SELECT_ALL);
3764            }
3765        }
3766
3767        private void updateReplaceItem(Menu menu) {
3768            boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions();
3769            boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null;
3770            if (canReplace && !replaceItemExists) {
3771                menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE,
3772                        com.android.internal.R.string.replace)
3773                    .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM);
3774            } else if (!canReplace && replaceItemExists) {
3775                menu.removeItem(TextView.ID_REPLACE);
3776            }
3777        }
3778
3779        @Override
3780        public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
3781            if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) {
3782                return true;
3783            }
3784            Callback customCallback = getCustomCallback();
3785            if (customCallback != null && customCallback.onActionItemClicked(mode, item)) {
3786                return true;
3787            }
3788            return mTextView.onTextContextMenuItem(item.getItemId());
3789        }
3790
3791        @Override
3792        public void onDestroyActionMode(ActionMode mode) {
3793            // Clear mTextActionMode not to recursively destroy action mode by clearing selection.
3794            mTextActionMode = null;
3795            Callback customCallback = getCustomCallback();
3796            if (customCallback != null) {
3797                customCallback.onDestroyActionMode(mode);
3798            }
3799
3800            if (!mPreserveSelection) {
3801                /*
3802                 * Leave current selection when we tentatively destroy action mode for the
3803                 * selection. If we're detaching from a window, we'll bring back the selection
3804                 * mode when (if) we get reattached.
3805                 */
3806                Selection.setSelection((Spannable) mTextView.getText(),
3807                        mTextView.getSelectionEnd());
3808            }
3809
3810            if (mSelectionModifierCursorController != null) {
3811                mSelectionModifierCursorController.hide();
3812            }
3813        }
3814
3815        @Override
3816        public void onGetContentRect(ActionMode mode, View view, Rect outRect) {
3817            if (!view.equals(mTextView) || mTextView.getLayout() == null) {
3818                super.onGetContentRect(mode, view, outRect);
3819                return;
3820            }
3821            if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) {
3822                // We have a selection.
3823                mSelectionPath.reset();
3824                mTextView.getLayout().getSelectionPath(
3825                        mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath);
3826                mSelectionPath.computeBounds(mSelectionBounds, true);
3827                mSelectionBounds.bottom += mHandleHeight;
3828            } else if (mCursorCount == 2) {
3829                // We have a split cursor. In this case, we take the rectangle that includes both
3830                // parts of the cursor to ensure we don't obscure either of them.
3831                Rect firstCursorBounds = mCursorDrawable[0].getBounds();
3832                Rect secondCursorBounds = mCursorDrawable[1].getBounds();
3833                mSelectionBounds.set(
3834                        Math.min(firstCursorBounds.left, secondCursorBounds.left),
3835                        Math.min(firstCursorBounds.top, secondCursorBounds.top),
3836                        Math.max(firstCursorBounds.right, secondCursorBounds.right),
3837                        Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom)
3838                                + mHandleHeight);
3839            } else {
3840                // We have a single cursor.
3841                Layout layout = mTextView.getLayout();
3842                int line = layout.getLineForOffset(mTextView.getSelectionStart());
3843                float primaryHorizontal = clampHorizontalPosition(null,
3844                        layout.getPrimaryHorizontal(mTextView.getSelectionStart()));
3845                mSelectionBounds.set(
3846                        primaryHorizontal,
3847                        layout.getLineTop(line),
3848                        primaryHorizontal,
3849                        layout.getLineTop(line + 1) + mHandleHeight);
3850            }
3851            // Take TextView's padding and scroll into account.
3852            int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset();
3853            int textVerticalOffset = mTextView.viewportToContentVerticalOffset();
3854            outRect.set(
3855                    (int) Math.floor(mSelectionBounds.left + textHorizontalOffset),
3856                    (int) Math.floor(mSelectionBounds.top + textVerticalOffset),
3857                    (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset),
3858                    (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset));
3859        }
3860    }
3861
3862    /**
3863     * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)}
3864     * while the input method is requesting the cursor/anchor position. Does nothing as long as
3865     * {@link InputMethodManager#isWatchingCursor(View)} returns false.
3866     */
3867    private final class CursorAnchorInfoNotifier implements TextViewPositionListener {
3868        final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder();
3869        final int[] mTmpIntOffset = new int[2];
3870        final Matrix mViewToScreenMatrix = new Matrix();
3871
3872        @Override
3873        public void updatePosition(int parentPositionX, int parentPositionY,
3874                boolean parentPositionChanged, boolean parentScrolled) {
3875            final InputMethodState ims = mInputMethodState;
3876            if (ims == null || ims.mBatchEditNesting > 0) {
3877                return;
3878            }
3879            final InputMethodManager imm = InputMethodManager.peekInstance();
3880            if (null == imm) {
3881                return;
3882            }
3883            if (!imm.isActive(mTextView)) {
3884                return;
3885            }
3886            // Skip if the IME has not requested the cursor/anchor position.
3887            if (!imm.isCursorAnchorInfoEnabled()) {
3888                return;
3889            }
3890            Layout layout = mTextView.getLayout();
3891            if (layout == null) {
3892                return;
3893            }
3894
3895            final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder;
3896            builder.reset();
3897
3898            final int selectionStart = mTextView.getSelectionStart();
3899            builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd());
3900
3901            // Construct transformation matrix from view local coordinates to screen coordinates.
3902            mViewToScreenMatrix.set(mTextView.getMatrix());
3903            mTextView.getLocationOnScreen(mTmpIntOffset);
3904            mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]);
3905            builder.setMatrix(mViewToScreenMatrix);
3906
3907            final float viewportToContentHorizontalOffset =
3908                    mTextView.viewportToContentHorizontalOffset();
3909            final float viewportToContentVerticalOffset =
3910                    mTextView.viewportToContentVerticalOffset();
3911
3912            final CharSequence text = mTextView.getText();
3913            if (text instanceof Spannable) {
3914                final Spannable sp = (Spannable) text;
3915                int composingTextStart = EditableInputConnection.getComposingSpanStart(sp);
3916                int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp);
3917                if (composingTextEnd < composingTextStart) {
3918                    final int temp = composingTextEnd;
3919                    composingTextEnd = composingTextStart;
3920                    composingTextStart = temp;
3921                }
3922                final boolean hasComposingText =
3923                        (0 <= composingTextStart) && (composingTextStart < composingTextEnd);
3924                if (hasComposingText) {
3925                    final CharSequence composingText = text.subSequence(composingTextStart,
3926                            composingTextEnd);
3927                    builder.setComposingText(composingTextStart, composingText);
3928
3929                    final int minLine = layout.getLineForOffset(composingTextStart);
3930                    final int maxLine = layout.getLineForOffset(composingTextEnd - 1);
3931                    for (int line = minLine; line <= maxLine; ++line) {
3932                        final int lineStart = layout.getLineStart(line);
3933                        final int lineEnd = layout.getLineEnd(line);
3934                        final int offsetStart = Math.max(lineStart, composingTextStart);
3935                        final int offsetEnd = Math.min(lineEnd, composingTextEnd);
3936                        final boolean ltrLine =
3937                                layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT;
3938                        final float[] widths = new float[offsetEnd - offsetStart];
3939                        layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths);
3940                        final float top = layout.getLineTop(line);
3941                        final float bottom = layout.getLineBottom(line);
3942                        for (int offset = offsetStart; offset < offsetEnd; ++offset) {
3943                            final float charWidth = widths[offset - offsetStart];
3944                            final boolean isRtl = layout.isRtlCharAt(offset);
3945                            final float primary = layout.getPrimaryHorizontal(offset);
3946                            final float secondary = layout.getSecondaryHorizontal(offset);
3947                            // TODO: This doesn't work perfectly for text with custom styles and
3948                            // TAB chars.
3949                            final float left;
3950                            final float right;
3951                            if (ltrLine) {
3952                                if (isRtl) {
3953                                    left = secondary - charWidth;
3954                                    right = secondary;
3955                                } else {
3956                                    left = primary;
3957                                    right = primary + charWidth;
3958                                }
3959                            } else {
3960                                if (!isRtl) {
3961                                    left = secondary;
3962                                    right = secondary + charWidth;
3963                                } else {
3964                                    left = primary - charWidth;
3965                                    right = primary;
3966                                }
3967                            }
3968                            // TODO: Check top-right and bottom-left as well.
3969                            final float localLeft = left + viewportToContentHorizontalOffset;
3970                            final float localRight = right + viewportToContentHorizontalOffset;
3971                            final float localTop = top + viewportToContentVerticalOffset;
3972                            final float localBottom = bottom + viewportToContentVerticalOffset;
3973                            final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop);
3974                            final boolean isBottomRightVisible =
3975                                    isPositionVisible(localRight, localBottom);
3976                            int characterBoundsFlags = 0;
3977                            if (isTopLeftVisible || isBottomRightVisible) {
3978                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
3979                            }
3980                            if (!isTopLeftVisible || !isBottomRightVisible) {
3981                                characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
3982                            }
3983                            if (isRtl) {
3984                                characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL;
3985                            }
3986                            // Here offset is the index in Java chars.
3987                            builder.addCharacterBounds(offset, localLeft, localTop, localRight,
3988                                    localBottom, characterBoundsFlags);
3989                        }
3990                    }
3991                }
3992            }
3993
3994            // Treat selectionStart as the insertion point.
3995            if (0 <= selectionStart) {
3996                final int offset = selectionStart;
3997                final int line = layout.getLineForOffset(offset);
3998                final float insertionMarkerX = layout.getPrimaryHorizontal(offset)
3999                        + viewportToContentHorizontalOffset;
4000                final float insertionMarkerTop = layout.getLineTop(line)
4001                        + viewportToContentVerticalOffset;
4002                final float insertionMarkerBaseline = layout.getLineBaseline(line)
4003                        + viewportToContentVerticalOffset;
4004                final float insertionMarkerBottom = layout.getLineBottom(line)
4005                        + viewportToContentVerticalOffset;
4006                final boolean isTopVisible =
4007                        isPositionVisible(insertionMarkerX, insertionMarkerTop);
4008                final boolean isBottomVisible =
4009                        isPositionVisible(insertionMarkerX, insertionMarkerBottom);
4010                int insertionMarkerFlags = 0;
4011                if (isTopVisible || isBottomVisible) {
4012                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
4013                }
4014                if (!isTopVisible || !isBottomVisible) {
4015                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION;
4016                }
4017                if (layout.isRtlCharAt(offset)) {
4018                    insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL;
4019                }
4020                builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop,
4021                        insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags);
4022            }
4023
4024            imm.updateCursorAnchorInfo(mTextView, builder.build());
4025        }
4026    }
4027
4028    @VisibleForTesting
4029    public abstract class HandleView extends View implements TextViewPositionListener {
4030        protected Drawable mDrawable;
4031        protected Drawable mDrawableLtr;
4032        protected Drawable mDrawableRtl;
4033        private final PopupWindow mContainer;
4034        // Position with respect to the parent TextView
4035        private int mPositionX, mPositionY;
4036        private boolean mIsDragging;
4037        // Offset from touch position to mPosition
4038        private float mTouchToWindowOffsetX, mTouchToWindowOffsetY;
4039        protected int mHotspotX;
4040        protected int mHorizontalGravity;
4041        // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up
4042        private float mTouchOffsetY;
4043        // Where the touch position should be on the handle to ensure a maximum cursor visibility
4044        private float mIdealVerticalOffset;
4045        // Parent's (TextView) previous position in window
4046        private int mLastParentX, mLastParentY;
4047        // Parent's (TextView) previous position on screen
4048        private int mLastParentXOnScreen, mLastParentYOnScreen;
4049        // Previous text character offset
4050        protected int mPreviousOffset = -1;
4051        // Previous text character offset
4052        private boolean mPositionHasChanged = true;
4053        // Minimum touch target size for handles
4054        private int mMinSize;
4055        // Indicates the line of text that the handle is on.
4056        protected int mPrevLine = UNSET_LINE;
4057        // Indicates the line of text that the user was touching. This can differ from mPrevLine
4058        // when selecting text when the handles jump to the end / start of words which may be on
4059        // a different line.
4060        protected int mPreviousLineTouched = UNSET_LINE;
4061
4062        private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) {
4063            super(mTextView.getContext());
4064            setId(id);
4065            mContainer = new PopupWindow(mTextView.getContext(), null,
4066                    com.android.internal.R.attr.textSelectHandleWindowStyle);
4067            mContainer.setSplitTouchEnabled(true);
4068            mContainer.setClippingEnabled(false);
4069            mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL);
4070            mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
4071            mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
4072            mContainer.setContentView(this);
4073
4074            mDrawableLtr = drawableLtr;
4075            mDrawableRtl = drawableRtl;
4076            mMinSize = mTextView.getContext().getResources().getDimensionPixelSize(
4077                    com.android.internal.R.dimen.text_handle_min_size);
4078
4079            updateDrawable();
4080
4081            final int handleHeight = getPreferredHeight();
4082            mTouchOffsetY = -0.3f * handleHeight;
4083            mIdealVerticalOffset = 0.7f * handleHeight;
4084        }
4085
4086        public float getIdealVerticalOffset() {
4087            return mIdealVerticalOffset;
4088        }
4089
4090        protected void updateDrawable() {
4091            if (mIsDragging) {
4092                // Don't update drawable during dragging.
4093                return;
4094            }
4095            final Layout layout = mTextView.getLayout();
4096            if (layout == null) {
4097                return;
4098            }
4099            final int offset = getCurrentCursorOffset();
4100            final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset);
4101            final Drawable oldDrawable = mDrawable;
4102            mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr;
4103            mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset);
4104            mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset);
4105            if (oldDrawable != mDrawable && isShowing()) {
4106                // Update popup window position.
4107                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
4108                        getHorizontalOffset() + getCursorOffset();
4109                mPositionX += mTextView.viewportToContentHorizontalOffset();
4110                mPositionHasChanged = true;
4111                updatePosition(mLastParentX, mLastParentY, false, false);
4112                postInvalidate();
4113            }
4114        }
4115
4116        protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun);
4117        protected abstract int getHorizontalGravity(boolean isRtlRun);
4118
4119        // Touch-up filter: number of previous positions remembered
4120        private static final int HISTORY_SIZE = 5;
4121        private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150;
4122        private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350;
4123        private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE];
4124        private final int[] mPreviousOffsets = new int[HISTORY_SIZE];
4125        private int mPreviousOffsetIndex = 0;
4126        private int mNumberPreviousOffsets = 0;
4127
4128        private void startTouchUpFilter(int offset) {
4129            mNumberPreviousOffsets = 0;
4130            addPositionToTouchUpFilter(offset);
4131        }
4132
4133        private void addPositionToTouchUpFilter(int offset) {
4134            mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE;
4135            mPreviousOffsets[mPreviousOffsetIndex] = offset;
4136            mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis();
4137            mNumberPreviousOffsets++;
4138        }
4139
4140        private void filterOnTouchUp() {
4141            final long now = SystemClock.uptimeMillis();
4142            int i = 0;
4143            int index = mPreviousOffsetIndex;
4144            final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE);
4145            while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) {
4146                i++;
4147                index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE;
4148            }
4149
4150            if (i > 0 && i < iMax &&
4151                    (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) {
4152                positionAtCursorOffset(mPreviousOffsets[index], false);
4153            }
4154        }
4155
4156        public boolean offsetHasBeenChanged() {
4157            return mNumberPreviousOffsets > 1;
4158        }
4159
4160        @Override
4161        protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
4162            setMeasuredDimension(getPreferredWidth(), getPreferredHeight());
4163        }
4164
4165        @Override
4166        public void invalidate() {
4167            super.invalidate();
4168            if (isShowing()) {
4169                positionAtCursorOffset(getCurrentCursorOffset(), true);
4170            }
4171        };
4172
4173        private int getPreferredWidth() {
4174            return Math.max(mDrawable.getIntrinsicWidth(), mMinSize);
4175        }
4176
4177        private int getPreferredHeight() {
4178            return Math.max(mDrawable.getIntrinsicHeight(), mMinSize);
4179        }
4180
4181        public void show() {
4182            if (isShowing()) return;
4183
4184            getPositionListener().addSubscriber(this, true /* local position may change */);
4185
4186            // Make sure the offset is always considered new, even when focusing at same position
4187            mPreviousOffset = -1;
4188            positionAtCursorOffset(getCurrentCursorOffset(), false);
4189        }
4190
4191        protected void dismiss() {
4192            mIsDragging = false;
4193            mContainer.dismiss();
4194            onDetached();
4195        }
4196
4197        public void hide() {
4198            dismiss();
4199
4200            getPositionListener().removeSubscriber(this);
4201        }
4202
4203        public boolean isShowing() {
4204            return mContainer.isShowing();
4205        }
4206
4207        private boolean isVisible() {
4208            // Always show a dragging handle.
4209            if (mIsDragging) {
4210                return true;
4211            }
4212
4213            if (mTextView.isInBatchEditMode()) {
4214                return false;
4215            }
4216
4217            return isPositionVisible(mPositionX + mHotspotX + getHorizontalOffset(), mPositionY);
4218        }
4219
4220        public abstract int getCurrentCursorOffset();
4221
4222        protected abstract void updateSelection(int offset);
4223
4224        public abstract void updatePosition(float x, float y);
4225
4226        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4227            return layout.isRtlCharAt(offset);
4228        }
4229
4230        @VisibleForTesting
4231        public float getHorizontal(@NonNull Layout layout, int offset) {
4232            return layout.getPrimaryHorizontal(offset);
4233        }
4234
4235        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4236            return mTextView.getOffsetAtCoordinate(line, x);
4237        }
4238
4239        /**
4240         * @param offset Cursor offset. Must be in [-1, length].
4241         * @param forceUpdatePosition whether to force update the position.  This should be true
4242         * when If the parent has been scrolled, for example.
4243         */
4244        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
4245            // A HandleView relies on the layout, which may be nulled by external methods
4246            Layout layout = mTextView.getLayout();
4247            if (layout == null) {
4248                // Will update controllers' state, hiding them and stopping selection mode if needed
4249                prepareCursorControllers();
4250                return;
4251            }
4252            layout = mTextView.getLayout();
4253
4254            boolean offsetChanged = offset != mPreviousOffset;
4255            if (offsetChanged || forceUpdatePosition) {
4256                if (offsetChanged) {
4257                    updateSelection(offset);
4258                    addPositionToTouchUpFilter(offset);
4259                }
4260                final int line = layout.getLineForOffset(offset);
4261                mPrevLine = line;
4262
4263                mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX -
4264                        getHorizontalOffset() + getCursorOffset();
4265                mPositionY = layout.getLineBottom(line);
4266
4267                // Take TextView's padding and scroll into account.
4268                mPositionX += mTextView.viewportToContentHorizontalOffset();
4269                mPositionY += mTextView.viewportToContentVerticalOffset();
4270
4271                mPreviousOffset = offset;
4272                mPositionHasChanged = true;
4273            }
4274        }
4275
4276        /**
4277         * Return the clamped horizontal position for the first cursor.
4278         *
4279         * @param layout Text layout.
4280         * @param offset Character offset for the cursor.
4281         * @return The clamped horizontal position for the cursor.
4282         */
4283        int getCursorHorizontalPosition(Layout layout, int offset) {
4284            return (int) (getHorizontal(layout, offset) - 0.5f);
4285        }
4286
4287        @Override
4288        public void updatePosition(int parentPositionX, int parentPositionY,
4289                boolean parentPositionChanged, boolean parentScrolled) {
4290            positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled);
4291            if (parentPositionChanged || mPositionHasChanged) {
4292                if (mIsDragging) {
4293                    // Update touchToWindow offset in case of parent scrolling while dragging
4294                    if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) {
4295                        mTouchToWindowOffsetX += parentPositionX - mLastParentX;
4296                        mTouchToWindowOffsetY += parentPositionY - mLastParentY;
4297                        mLastParentX = parentPositionX;
4298                        mLastParentY = parentPositionY;
4299                    }
4300
4301                    onHandleMoved();
4302                }
4303
4304                if (isVisible()) {
4305                    // Transform to the window coordinates to follow the view tranformation.
4306                    final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY};
4307                    mTextView.transformFromViewToWindowSpace(pts);
4308                    pts[0] -= mHotspotX + getHorizontalOffset();
4309
4310                    if (isShowing()) {
4311                        mContainer.update(pts[0], pts[1], -1, -1);
4312                    } else {
4313                        mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]);
4314                    }
4315                } else {
4316                    if (isShowing()) {
4317                        dismiss();
4318                    }
4319                }
4320
4321                mPositionHasChanged = false;
4322            }
4323        }
4324
4325        @Override
4326        protected void onDraw(Canvas c) {
4327            final int drawWidth = mDrawable.getIntrinsicWidth();
4328            final int left = getHorizontalOffset();
4329
4330            mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight());
4331            mDrawable.draw(c);
4332        }
4333
4334        private int getHorizontalOffset() {
4335            final int width = getPreferredWidth();
4336            final int drawWidth = mDrawable.getIntrinsicWidth();
4337            final int left;
4338            switch (mHorizontalGravity) {
4339                case Gravity.LEFT:
4340                    left = 0;
4341                    break;
4342                default:
4343                case Gravity.CENTER:
4344                    left = (width - drawWidth) / 2;
4345                    break;
4346                case Gravity.RIGHT:
4347                    left = width - drawWidth;
4348                    break;
4349            }
4350            return left;
4351        }
4352
4353        protected int getCursorOffset() {
4354            return 0;
4355        }
4356
4357        @Override
4358        public boolean onTouchEvent(MotionEvent ev) {
4359            updateFloatingToolbarVisibility(ev);
4360
4361            switch (ev.getActionMasked()) {
4362                case MotionEvent.ACTION_DOWN: {
4363                    startTouchUpFilter(getCurrentCursorOffset());
4364
4365                    final PositionListener positionListener = getPositionListener();
4366                    mLastParentX = positionListener.getPositionX();
4367                    mLastParentY = positionListener.getPositionY();
4368                    mLastParentXOnScreen = positionListener.getPositionXOnScreen();
4369                    mLastParentYOnScreen = positionListener.getPositionYOnScreen();
4370
4371                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4372                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4373                    mTouchToWindowOffsetX = xInWindow - mPositionX;
4374                    mTouchToWindowOffsetY = yInWindow - mPositionY;
4375
4376                    mIsDragging = true;
4377                    mPreviousLineTouched = UNSET_LINE;
4378                    break;
4379                }
4380
4381                case MotionEvent.ACTION_MOVE: {
4382                    final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX;
4383                    final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY;
4384
4385                    // Vertical hysteresis: vertical down movement tends to snap to ideal offset
4386                    final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY;
4387                    final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY;
4388                    float newVerticalOffset;
4389                    if (previousVerticalOffset < mIdealVerticalOffset) {
4390                        newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset);
4391                        newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset);
4392                    } else {
4393                        newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset);
4394                        newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset);
4395                    }
4396                    mTouchToWindowOffsetY = newVerticalOffset + mLastParentY;
4397
4398                    final float newPosX =
4399                            xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset();
4400                    final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY;
4401
4402                    updatePosition(newPosX, newPosY);
4403                    break;
4404                }
4405
4406                case MotionEvent.ACTION_UP:
4407                    filterOnTouchUp();
4408                    mIsDragging = false;
4409                    updateDrawable();
4410                    break;
4411
4412                case MotionEvent.ACTION_CANCEL:
4413                    mIsDragging = false;
4414                    updateDrawable();
4415                    break;
4416            }
4417            return true;
4418        }
4419
4420        public boolean isDragging() {
4421            return mIsDragging;
4422        }
4423
4424        void onHandleMoved() {}
4425
4426        public void onDetached() {}
4427    }
4428
4429    private class InsertionHandleView extends HandleView {
4430        private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000;
4431        private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds
4432
4433        // Used to detect taps on the insertion handle, which will affect the insertion action mode
4434        private float mDownPositionX, mDownPositionY;
4435        private Runnable mHider;
4436
4437        public InsertionHandleView(Drawable drawable) {
4438            super(drawable, drawable, com.android.internal.R.id.insertion_handle);
4439        }
4440
4441        @Override
4442        public void show() {
4443            super.show();
4444
4445            final long durationSinceCutOrCopy =
4446                    SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime;
4447
4448            // Cancel the single tap delayed runnable.
4449            if (mInsertionActionModeRunnable != null
4450                    && ((mTapState == TAP_STATE_DOUBLE_TAP)
4451                            || (mTapState == TAP_STATE_TRIPLE_CLICK)
4452                            || isCursorInsideEasyCorrectionSpan())) {
4453                mTextView.removeCallbacks(mInsertionActionModeRunnable);
4454            }
4455
4456            // Prepare and schedule the single tap runnable to run exactly after the double tap
4457            // timeout has passed.
4458            if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK)
4459                    && !isCursorInsideEasyCorrectionSpan()
4460                    && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) {
4461                if (mTextActionMode == null) {
4462                    if (mInsertionActionModeRunnable == null) {
4463                        mInsertionActionModeRunnable = new Runnable() {
4464                            @Override
4465                            public void run() {
4466                                startInsertionActionMode();
4467                            }
4468                        };
4469                    }
4470                    mTextView.postDelayed(
4471                            mInsertionActionModeRunnable,
4472                            ViewConfiguration.getDoubleTapTimeout() + 1);
4473                }
4474
4475            }
4476
4477            hideAfterDelay();
4478        }
4479
4480        private void hideAfterDelay() {
4481            if (mHider == null) {
4482                mHider = new Runnable() {
4483                    public void run() {
4484                        hide();
4485                    }
4486                };
4487            } else {
4488                removeHiderCallback();
4489            }
4490            mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT);
4491        }
4492
4493        private void removeHiderCallback() {
4494            if (mHider != null) {
4495                mTextView.removeCallbacks(mHider);
4496            }
4497        }
4498
4499        @Override
4500        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4501            return drawable.getIntrinsicWidth() / 2;
4502        }
4503
4504        @Override
4505        protected int getHorizontalGravity(boolean isRtlRun) {
4506            return Gravity.CENTER_HORIZONTAL;
4507        }
4508
4509        @Override
4510        protected int getCursorOffset() {
4511            int offset = super.getCursorOffset();
4512            final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null;
4513            if (cursor != null) {
4514                cursor.getPadding(mTempRect);
4515                offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2;
4516            }
4517            return offset;
4518        }
4519
4520        @Override
4521        int getCursorHorizontalPosition(Layout layout, int offset) {
4522            final Drawable drawable = mCursorCount > 0 ? mCursorDrawable[0] : null;
4523            if (drawable != null) {
4524                final float horizontal = getHorizontal(layout, offset);
4525                return clampHorizontalPosition(drawable, horizontal) + mTempRect.left;
4526            }
4527            return super.getCursorHorizontalPosition(layout, offset);
4528        }
4529
4530        @Override
4531        public boolean onTouchEvent(MotionEvent ev) {
4532            final boolean result = super.onTouchEvent(ev);
4533
4534            switch (ev.getActionMasked()) {
4535                case MotionEvent.ACTION_DOWN:
4536                    mDownPositionX = ev.getRawX();
4537                    mDownPositionY = ev.getRawY();
4538                    break;
4539
4540                case MotionEvent.ACTION_UP:
4541                    if (!offsetHasBeenChanged()) {
4542                        final float deltaX = mDownPositionX - ev.getRawX();
4543                        final float deltaY = mDownPositionY - ev.getRawY();
4544                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
4545
4546                        final ViewConfiguration viewConfiguration = ViewConfiguration.get(
4547                                mTextView.getContext());
4548                        final int touchSlop = viewConfiguration.getScaledTouchSlop();
4549
4550                        if (distanceSquared < touchSlop * touchSlop) {
4551                            // Tapping on the handle toggles the insertion action mode.
4552                            if (mTextActionMode != null) {
4553                                stopTextActionMode();
4554                            } else {
4555                                startInsertionActionMode();
4556                            }
4557                        }
4558                    } else {
4559                        if (mTextActionMode != null) {
4560                            mTextActionMode.invalidateContentRect();
4561                        }
4562                    }
4563                    hideAfterDelay();
4564                    break;
4565
4566                case MotionEvent.ACTION_CANCEL:
4567                    hideAfterDelay();
4568                    break;
4569
4570                default:
4571                    break;
4572            }
4573
4574            return result;
4575        }
4576
4577        @Override
4578        public int getCurrentCursorOffset() {
4579            return mTextView.getSelectionStart();
4580        }
4581
4582        @Override
4583        public void updateSelection(int offset) {
4584            Selection.setSelection((Spannable) mTextView.getText(), offset);
4585        }
4586
4587        @Override
4588        public void updatePosition(float x, float y) {
4589            Layout layout = mTextView.getLayout();
4590            int offset;
4591            if (layout != null) {
4592                if (mPreviousLineTouched == UNSET_LINE) {
4593                    mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4594                }
4595                int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4596                offset = getOffsetAtCoordinate(layout, currLine, x);
4597                mPreviousLineTouched = currLine;
4598            } else {
4599                offset = -1;
4600            }
4601            positionAtCursorOffset(offset, false);
4602            if (mTextActionMode != null) {
4603                mTextActionMode.invalidate();
4604            }
4605        }
4606
4607        @Override
4608        void onHandleMoved() {
4609            super.onHandleMoved();
4610            removeHiderCallback();
4611        }
4612
4613        @Override
4614        public void onDetached() {
4615            super.onDetached();
4616            removeHiderCallback();
4617        }
4618    }
4619
4620    @Retention(RetentionPolicy.SOURCE)
4621    @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END})
4622    public @interface HandleType {}
4623    public static final int HANDLE_TYPE_SELECTION_START = 0;
4624    public static final int HANDLE_TYPE_SELECTION_END = 1;
4625
4626    private class SelectionHandleView extends HandleView {
4627        // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection
4628        // end (HANDLE_TYPE_SELECTION_END).
4629        @HandleType
4630        private final int mHandleType;
4631        // Indicates whether the cursor is making adjustments within a word.
4632        private boolean mInWord = false;
4633        // Difference between touch position and word boundary position.
4634        private float mTouchWordDelta;
4635        // X value of the previous updatePosition call.
4636        private float mPrevX;
4637        // Indicates if the handle has moved a boundary between LTR and RTL text.
4638        private boolean mLanguageDirectionChanged = false;
4639        // Distance from edge of horizontally scrolling text view
4640        // to use to switch to character mode.
4641        private final float mTextViewEdgeSlop;
4642        // Used to save text view location.
4643        private final int[] mTextViewLocation = new int[2];
4644
4645        public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id,
4646                @HandleType int handleType) {
4647            super(drawableLtr, drawableRtl, id);
4648            mHandleType = handleType;
4649            ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext());
4650            mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4;
4651        }
4652
4653        private boolean isStartHandle() {
4654            return mHandleType == HANDLE_TYPE_SELECTION_START;
4655        }
4656
4657        @Override
4658        protected int getHotspotX(Drawable drawable, boolean isRtlRun) {
4659            if (isRtlRun == isStartHandle()) {
4660                return drawable.getIntrinsicWidth() / 4;
4661            } else {
4662                return (drawable.getIntrinsicWidth() * 3) / 4;
4663            }
4664        }
4665
4666        @Override
4667        protected int getHorizontalGravity(boolean isRtlRun) {
4668            return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT;
4669        }
4670
4671        @Override
4672        public int getCurrentCursorOffset() {
4673            return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd();
4674        }
4675
4676        @Override
4677        protected void updateSelection(int offset) {
4678            if (isStartHandle()) {
4679                Selection.setSelection((Spannable) mTextView.getText(), offset,
4680                        mTextView.getSelectionEnd());
4681            } else {
4682                Selection.setSelection((Spannable) mTextView.getText(),
4683                        mTextView.getSelectionStart(), offset);
4684            }
4685            updateDrawable();
4686            if (mTextActionMode != null) {
4687                mTextActionMode.invalidate();
4688            }
4689        }
4690
4691        @Override
4692        public void updatePosition(float x, float y) {
4693            final Layout layout = mTextView.getLayout();
4694            if (layout == null) {
4695                // HandleView will deal appropriately in positionAtCursorOffset when
4696                // layout is null.
4697                positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y));
4698                return;
4699            }
4700
4701            if (mPreviousLineTouched == UNSET_LINE) {
4702                mPreviousLineTouched = mTextView.getLineAtCoordinate(y);
4703            }
4704
4705            boolean positionCursor = false;
4706            final int anotherHandleOffset =
4707                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
4708            int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y);
4709            int initialOffset = getOffsetAtCoordinate(layout, currLine, x);
4710
4711            if (isStartHandle() && initialOffset >= anotherHandleOffset
4712                    || !isStartHandle() && initialOffset <= anotherHandleOffset) {
4713                // Handles have crossed, bound it to the first selected line and
4714                // adjust by word / char as normal.
4715                currLine = layout.getLineForOffset(anotherHandleOffset);
4716                initialOffset = getOffsetAtCoordinate(layout, currLine, x);
4717            }
4718
4719            int offset = initialOffset;
4720            final int wordEnd = getWordEnd(offset);
4721            final int wordStart = getWordStart(offset);
4722
4723            if (mPrevX == UNSET_X_VALUE) {
4724                mPrevX = x;
4725            }
4726
4727            final int currentOffset = getCurrentCursorOffset();
4728            final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset);
4729            final boolean atRtl = isAtRtlRun(layout, offset);
4730            final boolean isLvlBoundary = layout.isLevelBoundary(offset);
4731
4732            // We can't determine if the user is expanding or shrinking the selection if they're
4733            // on a bi-di boundary, so until they've moved past the boundary we'll just place
4734            // the cursor at the current position.
4735            if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) {
4736                // We're on a boundary or this is the first direction change -- just update
4737                // to the current position.
4738                mLanguageDirectionChanged = true;
4739                mTouchWordDelta = 0.0f;
4740                positionAndAdjustForCrossingHandles(offset);
4741                return;
4742            } else if (mLanguageDirectionChanged && !isLvlBoundary) {
4743                // We've just moved past the boundary so update the position. After this we can
4744                // figure out if the user is expanding or shrinking to go by word or character.
4745                positionAndAdjustForCrossingHandles(offset);
4746                mTouchWordDelta = 0.0f;
4747                mLanguageDirectionChanged = false;
4748                return;
4749            }
4750
4751            boolean isExpanding;
4752            final float xDiff = x - mPrevX;
4753            if (isStartHandle()) {
4754                isExpanding = currLine < mPreviousLineTouched;
4755            } else {
4756                isExpanding = currLine > mPreviousLineTouched;
4757            }
4758            if (atRtl == isStartHandle()) {
4759                isExpanding |= xDiff > 0;
4760            } else {
4761                isExpanding |= xDiff < 0;
4762            }
4763
4764            if (mTextView.getHorizontallyScrolling()) {
4765                if (positionNearEdgeOfScrollingView(x, atRtl)
4766                        && ((isStartHandle() && mTextView.getScrollX() != 0)
4767                                || (!isStartHandle()
4768                                        && mTextView.canScrollHorizontally(atRtl ? -1 : 1)))
4769                        && ((isExpanding && ((isStartHandle() && offset < currentOffset)
4770                                || (!isStartHandle() && offset > currentOffset)))
4771                                        || !isExpanding)) {
4772                    // If we're expanding ensure that the offset is actually expanding compared to
4773                    // the current offset, if the handle snapped to the word, the finger position
4774                    // may be out of sync and we don't want the selection to jump back.
4775                    mTouchWordDelta = 0.0f;
4776                    final int nextOffset = (atRtl == isStartHandle())
4777                            ? layout.getOffsetToRightOf(mPreviousOffset)
4778                            : layout.getOffsetToLeftOf(mPreviousOffset);
4779                    positionAndAdjustForCrossingHandles(nextOffset);
4780                    return;
4781                }
4782            }
4783
4784            if (isExpanding) {
4785                // User is increasing the selection.
4786                int wordBoundary = isStartHandle() ? wordStart : wordEnd;
4787                final boolean snapToWord = (!mInWord
4788                        || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine))
4789                                && atRtl == isAtRtlRun(layout, wordBoundary);
4790                if (snapToWord) {
4791                    // Sometimes words can be broken across lines (Chinese, hyphenation).
4792                    // We still snap to the word boundary but we only use the letters on the
4793                    // current line to determine if the user is far enough into the word to snap.
4794                    if (layout.getLineForOffset(wordBoundary) != currLine) {
4795                        wordBoundary = isStartHandle() ?
4796                                layout.getLineStart(currLine) : layout.getLineEnd(currLine);
4797                    }
4798                    final int offsetThresholdToSnap = isStartHandle()
4799                            ? wordEnd - ((wordEnd - wordBoundary) / 2)
4800                            : wordStart + ((wordBoundary - wordStart) / 2);
4801                    if (isStartHandle()
4802                            && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) {
4803                        // User is far enough into the word or on a different line so we expand by
4804                        // word.
4805                        offset = wordStart;
4806                    } else if (!isStartHandle()
4807                            && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) {
4808                        // User is far enough into the word or on a different line so we expand by
4809                        // word.
4810                        offset = wordEnd;
4811                    } else {
4812                        offset = mPreviousOffset;
4813                    }
4814                }
4815                if ((isStartHandle() && offset < initialOffset)
4816                        || (!isStartHandle() && offset > initialOffset)) {
4817                    final float adjustedX = getHorizontal(layout, offset);
4818                    mTouchWordDelta =
4819                            mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4820                } else {
4821                    mTouchWordDelta = 0.0f;
4822                }
4823                positionCursor = true;
4824            } else {
4825                final int adjustedOffset =
4826                        getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta);
4827                final boolean shrinking = isStartHandle()
4828                        ? adjustedOffset > mPreviousOffset || currLine > mPrevLine
4829                        : adjustedOffset < mPreviousOffset || currLine < mPrevLine;
4830                if (shrinking) {
4831                    // User is shrinking the selection.
4832                    if (currLine != mPrevLine) {
4833                        // We're on a different line, so we'll snap to word boundaries.
4834                        offset = isStartHandle() ? wordStart : wordEnd;
4835                        if ((isStartHandle() && offset < initialOffset)
4836                                || (!isStartHandle() && offset > initialOffset)) {
4837                            final float adjustedX = getHorizontal(layout, offset);
4838                            mTouchWordDelta =
4839                                    mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX;
4840                        } else {
4841                            mTouchWordDelta = 0.0f;
4842                        }
4843                    } else {
4844                        offset = adjustedOffset;
4845                    }
4846                    positionCursor = true;
4847                } else if ((isStartHandle() && adjustedOffset < mPreviousOffset)
4848                        || (!isStartHandle() && adjustedOffset > mPreviousOffset)) {
4849                    // Handle has jumped to the word boundary, and the user is moving
4850                    // their finger towards the handle, the delta should be updated.
4851                    mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) -
4852                            getHorizontal(layout, mPreviousOffset);
4853                }
4854            }
4855
4856            if (positionCursor) {
4857                mPreviousLineTouched = currLine;
4858                positionAndAdjustForCrossingHandles(offset);
4859            }
4860            mPrevX = x;
4861        }
4862
4863        @Override
4864        protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition) {
4865            super.positionAtCursorOffset(offset, forceUpdatePosition);
4866            mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset);
4867        }
4868
4869        @Override
4870        public boolean onTouchEvent(MotionEvent event) {
4871            boolean superResult = super.onTouchEvent(event);
4872            if (event.getActionMasked() == MotionEvent.ACTION_DOWN) {
4873                // Reset the touch word offset and x value when the user
4874                // re-engages the handle.
4875                mTouchWordDelta = 0.0f;
4876                mPrevX = UNSET_X_VALUE;
4877            }
4878            return superResult;
4879        }
4880
4881        private void positionAndAdjustForCrossingHandles(int offset) {
4882            final int anotherHandleOffset =
4883                    isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart();
4884            if ((isStartHandle() && offset >= anotherHandleOffset)
4885                    || (!isStartHandle() && offset <= anotherHandleOffset)) {
4886                mTouchWordDelta = 0.0f;
4887                final Layout layout = mTextView.getLayout();
4888                if (layout != null && offset != anotherHandleOffset) {
4889                    final float horiz = getHorizontal(layout, offset);
4890                    final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset,
4891                            !isStartHandle());
4892                    final float currentHoriz = getHorizontal(layout, mPreviousOffset);
4893                    if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz
4894                            || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) {
4895                        // This handle passes another one as it crossed a direction boundary.
4896                        // Don't minimize the selection, but keep the handle at the run boundary.
4897                        final int currentOffset = getCurrentCursorOffset();
4898                        final int offsetToGetRunRange = isStartHandle() ?
4899                                currentOffset : Math.max(currentOffset - 1, 0);
4900                        final long range = layout.getRunRange(offsetToGetRunRange);
4901                        if (isStartHandle()) {
4902                            offset = TextUtils.unpackRangeStartFromLong(range);
4903                        } else {
4904                            offset = TextUtils.unpackRangeEndFromLong(range);
4905                        }
4906                        positionAtCursorOffset(offset, false);
4907                        return;
4908                    }
4909                }
4910                // Handles can not cross and selection is at least one character.
4911                offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle());
4912            }
4913            positionAtCursorOffset(offset, false);
4914        }
4915
4916        private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) {
4917            mTextView.getLocationOnScreen(mTextViewLocation);
4918            boolean nearEdge;
4919            if (atRtl == isStartHandle()) {
4920                int rightEdge = mTextViewLocation[0] + mTextView.getWidth()
4921                        - mTextView.getPaddingRight();
4922                nearEdge = x > rightEdge - mTextViewEdgeSlop;
4923            } else {
4924                int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft();
4925                nearEdge = x < leftEdge + mTextViewEdgeSlop;
4926            }
4927            return nearEdge;
4928        }
4929
4930        @Override
4931        protected boolean isAtRtlRun(@NonNull Layout layout, int offset) {
4932            final int offsetToCheck = isStartHandle() ? offset : Math.max(offset - 1, 0);
4933            return layout.isRtlCharAt(offsetToCheck);
4934        }
4935
4936        @Override
4937        public float getHorizontal(@NonNull Layout layout, int offset) {
4938            return getHorizontal(layout, offset, isStartHandle());
4939        }
4940
4941        private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) {
4942            final int line = layout.getLineForOffset(offset);
4943            final int offsetToCheck = startHandle ? offset : Math.max(offset - 1, 0);
4944            final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
4945            final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
4946            return (isRtlChar == isRtlParagraph) ?
4947                    layout.getPrimaryHorizontal(offset) : layout.getSecondaryHorizontal(offset);
4948        }
4949
4950        @Override
4951        protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) {
4952            final float localX = mTextView.convertToLocalHorizontalCoordinate(x);
4953            final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true);
4954            if (!layout.isLevelBoundary(primaryOffset)) {
4955                return primaryOffset;
4956            }
4957            final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false);
4958            final int currentOffset = getCurrentCursorOffset();
4959            final int primaryDiff = Math.abs(primaryOffset - currentOffset);
4960            final int secondaryDiff = Math.abs(secondaryOffset - currentOffset);
4961            if (primaryDiff < secondaryDiff) {
4962                return primaryOffset;
4963            } else if (primaryDiff > secondaryDiff) {
4964                return secondaryOffset;
4965            } else {
4966                final int offsetToCheck = isStartHandle() ?
4967                        currentOffset : Math.max(currentOffset - 1, 0);
4968                final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck);
4969                final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1;
4970                return isRtlChar == isRtlParagraph ? primaryOffset : secondaryOffset;
4971            }
4972        }
4973    }
4974
4975    private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) {
4976        final int trueLine = mTextView.getLineAtCoordinate(y);
4977        if (layout == null || prevLine > layout.getLineCount()
4978                || layout.getLineCount() <= 0 || prevLine < 0) {
4979            // Invalid parameters, just return whatever line is at y.
4980            return trueLine;
4981        }
4982
4983        if (Math.abs(trueLine - prevLine) >= 2) {
4984            // Only stick to lines if we're within a line of the previous selection.
4985            return trueLine;
4986        }
4987
4988        final float verticalOffset = mTextView.viewportToContentVerticalOffset();
4989        final int lineCount = layout.getLineCount();
4990        final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS;
4991
4992        final float firstLineTop = layout.getLineTop(0) + verticalOffset;
4993        final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset;
4994        final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop);
4995
4996        final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset;
4997        final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset;
4998        final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop);
4999
5000        // Determine if we've moved lines based on y position and previous line.
5001        int currLine;
5002        if (y <= yTopBound) {
5003            currLine = Math.max(prevLine - 1, 0);
5004        } else if (y >= yBottomBound) {
5005            currLine = Math.min(prevLine + 1, lineCount - 1);
5006        } else {
5007            currLine = prevLine;
5008        }
5009        return currLine;
5010    }
5011
5012    /**
5013     * A CursorController instance can be used to control a cursor in the text.
5014     */
5015    private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener {
5016        /**
5017         * Makes the cursor controller visible on screen.
5018         * See also {@link #hide()}.
5019         */
5020        public void show();
5021
5022        /**
5023         * Hide the cursor controller from screen.
5024         * See also {@link #show()}.
5025         */
5026        public void hide();
5027
5028        /**
5029         * Called when the view is detached from window. Perform house keeping task, such as
5030         * stopping Runnable thread that would otherwise keep a reference on the context, thus
5031         * preventing the activity from being recycled.
5032         */
5033        public void onDetached();
5034
5035        public boolean isCursorBeingModified();
5036
5037        public boolean isActive();
5038    }
5039
5040    private class InsertionPointCursorController implements CursorController {
5041        private InsertionHandleView mHandle;
5042
5043        public void show() {
5044            getHandle().show();
5045
5046            if (mSelectionModifierCursorController != null) {
5047                mSelectionModifierCursorController.hide();
5048            }
5049        }
5050
5051        public void hide() {
5052            if (mHandle != null) {
5053                mHandle.hide();
5054            }
5055        }
5056
5057        public void onTouchModeChanged(boolean isInTouchMode) {
5058            if (!isInTouchMode) {
5059                hide();
5060            }
5061        }
5062
5063        private InsertionHandleView getHandle() {
5064            if (mSelectHandleCenter == null) {
5065                mSelectHandleCenter = mTextView.getContext().getDrawable(
5066                        mTextView.mTextSelectHandleRes);
5067            }
5068            if (mHandle == null) {
5069                mHandle = new InsertionHandleView(mSelectHandleCenter);
5070            }
5071            return mHandle;
5072        }
5073
5074        @Override
5075        public void onDetached() {
5076            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5077            observer.removeOnTouchModeChangeListener(this);
5078
5079            if (mHandle != null) mHandle.onDetached();
5080        }
5081
5082        @Override
5083        public boolean isCursorBeingModified() {
5084            return mHandle != null && mHandle.isDragging();
5085        }
5086
5087        @Override
5088        public boolean isActive() {
5089            return mHandle != null && mHandle.isShowing();
5090        }
5091
5092        public void invalidateHandle() {
5093            if (mHandle != null) {
5094                mHandle.invalidate();
5095            }
5096        }
5097    }
5098
5099    class SelectionModifierCursorController implements CursorController {
5100        // The cursor controller handles, lazily created when shown.
5101        private SelectionHandleView mStartHandle;
5102        private SelectionHandleView mEndHandle;
5103        // The offsets of that last touch down event. Remembered to start selection there.
5104        private int mMinTouchOffset, mMaxTouchOffset;
5105
5106        private float mDownPositionX, mDownPositionY;
5107        private boolean mGestureStayedInTapRegion;
5108
5109        // Where the user first starts the drag motion.
5110        private int mStartOffset = -1;
5111
5112        private boolean mHaventMovedEnoughToStartDrag;
5113        // The line that a selection happened most recently with the drag accelerator.
5114        private int mLineSelectionIsOn = -1;
5115        // Whether the drag accelerator has selected past the initial line.
5116        private boolean mSwitchedLines = false;
5117
5118        // Indicates the drag accelerator mode that the user is currently using.
5119        private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5120        // Drag accelerator is inactive.
5121        private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0;
5122        // Character based selection by dragging. Only for mouse.
5123        private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1;
5124        // Word based selection by dragging. Enabled after long pressing or double tapping.
5125        private static final int DRAG_ACCELERATOR_MODE_WORD = 2;
5126        // Paragraph based selection by dragging. Enabled after mouse triple click.
5127        private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3;
5128
5129        SelectionModifierCursorController() {
5130            resetTouchOffsets();
5131        }
5132
5133        public void show() {
5134            if (mTextView.isInBatchEditMode()) {
5135                return;
5136            }
5137            initDrawables();
5138            initHandles();
5139        }
5140
5141        private void initDrawables() {
5142            if (mSelectHandleLeft == null) {
5143                mSelectHandleLeft = mTextView.getContext().getDrawable(
5144                        mTextView.mTextSelectHandleLeftRes);
5145            }
5146            if (mSelectHandleRight == null) {
5147                mSelectHandleRight = mTextView.getContext().getDrawable(
5148                        mTextView.mTextSelectHandleRightRes);
5149            }
5150        }
5151
5152        private void initHandles() {
5153            // Lazy object creation has to be done before updatePosition() is called.
5154            if (mStartHandle == null) {
5155                mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight,
5156                        com.android.internal.R.id.selection_start_handle,
5157                        HANDLE_TYPE_SELECTION_START);
5158            }
5159            if (mEndHandle == null) {
5160                mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft,
5161                        com.android.internal.R.id.selection_end_handle,
5162                        HANDLE_TYPE_SELECTION_END);
5163            }
5164
5165            mStartHandle.show();
5166            mEndHandle.show();
5167
5168            hideInsertionPointCursorController();
5169        }
5170
5171        public void hide() {
5172            if (mStartHandle != null) mStartHandle.hide();
5173            if (mEndHandle != null) mEndHandle.hide();
5174        }
5175
5176        public void enterDrag(int dragAcceleratorMode) {
5177            // Just need to init the handles / hide insertion cursor.
5178            show();
5179            mDragAcceleratorMode = dragAcceleratorMode;
5180            // Start location of selection.
5181            mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX,
5182                    mLastDownPositionY);
5183            mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY);
5184            // Don't show the handles until user has lifted finger.
5185            hide();
5186
5187            // This stops scrolling parents from intercepting the touch event, allowing
5188            // the user to continue dragging across the screen to select text; TextView will
5189            // scroll as necessary.
5190            mTextView.getParent().requestDisallowInterceptTouchEvent(true);
5191            mTextView.cancelLongPress();
5192        }
5193
5194        public void onTouchEvent(MotionEvent event) {
5195            // This is done even when the View does not have focus, so that long presses can start
5196            // selection and tap can move cursor from this tap position.
5197            final float eventX = event.getX();
5198            final float eventY = event.getY();
5199            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5200            switch (event.getActionMasked()) {
5201                case MotionEvent.ACTION_DOWN:
5202                    if (extractedTextModeWillBeStarted()) {
5203                        // Prevent duplicating the selection handles until the mode starts.
5204                        hide();
5205                    } else {
5206                        // Remember finger down position, to be able to start selection from there.
5207                        mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition(
5208                                eventX, eventY);
5209
5210                        // Double tap detection
5211                        if (mGestureStayedInTapRegion) {
5212                            if (mTapState == TAP_STATE_DOUBLE_TAP
5213                                    || mTapState == TAP_STATE_TRIPLE_CLICK) {
5214                                final float deltaX = eventX - mDownPositionX;
5215                                final float deltaY = eventY - mDownPositionY;
5216                                final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5217
5218                                ViewConfiguration viewConfiguration = ViewConfiguration.get(
5219                                        mTextView.getContext());
5220                                int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop();
5221                                boolean stayedInArea =
5222                                        distanceSquared < doubleTapSlop * doubleTapSlop;
5223
5224                                if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) {
5225                                    if (mTapState == TAP_STATE_DOUBLE_TAP) {
5226                                        selectCurrentWordAndStartDrag();
5227                                    } else if (mTapState == TAP_STATE_TRIPLE_CLICK) {
5228                                        selectCurrentParagraphAndStartDrag();
5229                                    }
5230                                    mDiscardNextActionUp = true;
5231                                }
5232                            }
5233                        }
5234
5235                        mDownPositionX = eventX;
5236                        mDownPositionY = eventY;
5237                        mGestureStayedInTapRegion = true;
5238                        mHaventMovedEnoughToStartDrag = true;
5239                    }
5240                    break;
5241
5242                case MotionEvent.ACTION_POINTER_DOWN:
5243                case MotionEvent.ACTION_POINTER_UP:
5244                    // Handle multi-point gestures. Keep min and max offset positions.
5245                    // Only activated for devices that correctly handle multi-touch.
5246                    if (mTextView.getContext().getPackageManager().hasSystemFeature(
5247                            PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) {
5248                        updateMinAndMaxOffsets(event);
5249                    }
5250                    break;
5251
5252                case MotionEvent.ACTION_MOVE:
5253                    final ViewConfiguration viewConfig = ViewConfiguration.get(
5254                            mTextView.getContext());
5255                    final int touchSlop = viewConfig.getScaledTouchSlop();
5256
5257                    if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) {
5258                        final float deltaX = eventX - mDownPositionX;
5259                        final float deltaY = eventY - mDownPositionY;
5260                        final float distanceSquared = deltaX * deltaX + deltaY * deltaY;
5261
5262                        if (mGestureStayedInTapRegion) {
5263                            int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop();
5264                            mGestureStayedInTapRegion =
5265                                    distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop;
5266                        }
5267                        if (mHaventMovedEnoughToStartDrag) {
5268                            // We don't start dragging until the user has moved enough.
5269                            mHaventMovedEnoughToStartDrag =
5270                                    distanceSquared <= touchSlop * touchSlop;
5271                        }
5272                    }
5273
5274                    if (isMouse && !isDragAcceleratorActive()) {
5275                        final int offset = mTextView.getOffsetForPosition(eventX, eventY);
5276                        if (mTextView.hasSelection()
5277                                && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset)
5278                                && offset >= mTextView.getSelectionStart()
5279                                && offset <= mTextView.getSelectionEnd()) {
5280                            startDragAndDrop();
5281                            break;
5282                        }
5283
5284                        if (mStartOffset != offset) {
5285                            // Start character based drag accelerator.
5286                            stopTextActionMode();
5287                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
5288                            mDiscardNextActionUp = true;
5289                            mHaventMovedEnoughToStartDrag = false;
5290                        }
5291                    }
5292
5293                    if (mStartHandle != null && mStartHandle.isShowing()) {
5294                        // Don't do the drag if the handles are showing already.
5295                        break;
5296                    }
5297
5298                    updateSelection(event);
5299                    break;
5300
5301                case MotionEvent.ACTION_UP:
5302                    if (!isDragAcceleratorActive()) {
5303                        break;
5304                    }
5305                    updateSelection(event);
5306
5307                    // No longer dragging to select text, let the parent intercept events.
5308                    mTextView.getParent().requestDisallowInterceptTouchEvent(false);
5309
5310                    // No longer the first dragging motion, reset.
5311                    resetDragAcceleratorState();
5312
5313                    if (mTextView.hasSelection()) {
5314                        startSelectionActionMode();
5315                    }
5316                    break;
5317            }
5318        }
5319
5320        private void updateSelection(MotionEvent event) {
5321            if (mTextView.getLayout() != null) {
5322                switch (mDragAcceleratorMode) {
5323                    case DRAG_ACCELERATOR_MODE_CHARACTER:
5324                        updateCharacterBasedSelection(event);
5325                        break;
5326                    case DRAG_ACCELERATOR_MODE_WORD:
5327                        updateWordBasedSelection(event);
5328                        break;
5329                    case DRAG_ACCELERATOR_MODE_PARAGRAPH:
5330                        updateParagraphBasedSelection(event);
5331                        break;
5332                }
5333            }
5334        }
5335
5336        /**
5337         * If the TextView allows text selection, selects the current paragraph and starts a drag.
5338         *
5339         * @return true if the drag was started.
5340         */
5341        private boolean selectCurrentParagraphAndStartDrag() {
5342            if (mInsertionActionModeRunnable != null) {
5343                mTextView.removeCallbacks(mInsertionActionModeRunnable);
5344            }
5345            stopTextActionMode();
5346            if (!selectCurrentParagraph()) {
5347                return false;
5348            }
5349            enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH);
5350            return true;
5351        }
5352
5353        private void updateCharacterBasedSelection(MotionEvent event) {
5354            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5355            Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset);
5356        }
5357
5358        private void updateWordBasedSelection(MotionEvent event) {
5359            if (mHaventMovedEnoughToStartDrag) {
5360                return;
5361            }
5362            final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE);
5363            final ViewConfiguration viewConfig = ViewConfiguration.get(
5364                    mTextView.getContext());
5365            final float eventX = event.getX();
5366            final float eventY = event.getY();
5367            final int currLine;
5368            if (isMouse) {
5369                // No need to offset the y coordinate for mouse input.
5370                currLine = mTextView.getLineAtCoordinate(eventY);
5371            } else {
5372                float y = eventY;
5373                if (mSwitchedLines) {
5374                    // Offset the finger by the same vertical offset as the handles.
5375                    // This improves visibility of the content being selected by
5376                    // shifting the finger below the content, this is applied once
5377                    // the user has switched lines.
5378                    final int touchSlop = viewConfig.getScaledTouchSlop();
5379                    final float fingerOffset = (mStartHandle != null)
5380                            ? mStartHandle.getIdealVerticalOffset()
5381                            : touchSlop;
5382                    y = eventY - fingerOffset;
5383                }
5384
5385                currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn,
5386                        y);
5387                if (!mSwitchedLines && currLine != mLineSelectionIsOn) {
5388                    // Break early here, we want to offset the finger position from
5389                    // the selection highlight, once the user moved their finger
5390                    // to a different line we should apply the offset and *not* switch
5391                    // lines until recomputing the position with the finger offset.
5392                    mSwitchedLines = true;
5393                    return;
5394                }
5395            }
5396
5397            int startOffset;
5398            int offset = mTextView.getOffsetAtCoordinate(currLine, eventX);
5399            // Snap to word boundaries.
5400            if (mStartOffset < offset) {
5401                // Expanding with end handle.
5402                offset = getWordEnd(offset);
5403                startOffset = getWordStart(mStartOffset);
5404            } else {
5405                // Expanding with start handle.
5406                offset = getWordStart(offset);
5407                startOffset = getWordEnd(mStartOffset);
5408            }
5409            mLineSelectionIsOn = currLine;
5410            Selection.setSelection((Spannable) mTextView.getText(),
5411                    startOffset, offset);
5412        }
5413
5414        private void updateParagraphBasedSelection(MotionEvent event) {
5415            final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY());
5416
5417            final int start = Math.min(offset, mStartOffset);
5418            final int end = Math.max(offset, mStartOffset);
5419            final long paragraphsRange = getParagraphsRange(start, end);
5420            final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange);
5421            final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange);
5422            Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd);
5423        }
5424
5425        /**
5426         * @param event
5427         */
5428        private void updateMinAndMaxOffsets(MotionEvent event) {
5429            int pointerCount = event.getPointerCount();
5430            for (int index = 0; index < pointerCount; index++) {
5431                int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index));
5432                if (offset < mMinTouchOffset) mMinTouchOffset = offset;
5433                if (offset > mMaxTouchOffset) mMaxTouchOffset = offset;
5434            }
5435        }
5436
5437        public int getMinTouchOffset() {
5438            return mMinTouchOffset;
5439        }
5440
5441        public int getMaxTouchOffset() {
5442            return mMaxTouchOffset;
5443        }
5444
5445        public void resetTouchOffsets() {
5446            mMinTouchOffset = mMaxTouchOffset = -1;
5447            resetDragAcceleratorState();
5448        }
5449
5450        private void resetDragAcceleratorState() {
5451            mStartOffset = -1;
5452            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
5453            mSwitchedLines = false;
5454            final int selectionStart = mTextView.getSelectionStart();
5455            final int selectionEnd = mTextView.getSelectionEnd();
5456            if (selectionStart > selectionEnd) {
5457                Selection.setSelection((Spannable) mTextView.getText(),
5458                        selectionEnd, selectionStart);
5459            }
5460        }
5461
5462        /**
5463         * @return true iff this controller is currently used to move the selection start.
5464         */
5465        public boolean isSelectionStartDragged() {
5466            return mStartHandle != null && mStartHandle.isDragging();
5467        }
5468
5469        @Override
5470        public boolean isCursorBeingModified() {
5471            return isDragAcceleratorActive() || isSelectionStartDragged()
5472                    || (mEndHandle != null && mEndHandle.isDragging());
5473        }
5474
5475        /**
5476         * @return true if the user is selecting text using the drag accelerator.
5477         */
5478        public boolean isDragAcceleratorActive() {
5479            return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE;
5480        }
5481
5482        public void onTouchModeChanged(boolean isInTouchMode) {
5483            if (!isInTouchMode) {
5484                hide();
5485            }
5486        }
5487
5488        @Override
5489        public void onDetached() {
5490            final ViewTreeObserver observer = mTextView.getViewTreeObserver();
5491            observer.removeOnTouchModeChangeListener(this);
5492
5493            if (mStartHandle != null) mStartHandle.onDetached();
5494            if (mEndHandle != null) mEndHandle.onDetached();
5495        }
5496
5497        @Override
5498        public boolean isActive() {
5499            return mStartHandle != null && mStartHandle.isShowing();
5500        }
5501
5502        public void invalidateHandles() {
5503            if (mStartHandle != null) {
5504                mStartHandle.invalidate();
5505            }
5506            if (mEndHandle != null) {
5507                mEndHandle.invalidate();
5508            }
5509        }
5510    }
5511
5512    private class CorrectionHighlighter {
5513        private final Path mPath = new Path();
5514        private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG);
5515        private int mStart, mEnd;
5516        private long mFadingStartTime;
5517        private RectF mTempRectF;
5518        private final static int FADE_OUT_DURATION = 400;
5519
5520        public CorrectionHighlighter() {
5521            mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo().
5522                    applicationScale);
5523            mPaint.setStyle(Paint.Style.FILL);
5524        }
5525
5526        public void highlight(CorrectionInfo info) {
5527            mStart = info.getOffset();
5528            mEnd = mStart + info.getNewText().length();
5529            mFadingStartTime = SystemClock.uptimeMillis();
5530
5531            if (mStart < 0 || mEnd < 0) {
5532                stopAnimation();
5533            }
5534        }
5535
5536        public void draw(Canvas canvas, int cursorOffsetVertical) {
5537            if (updatePath() && updatePaint()) {
5538                if (cursorOffsetVertical != 0) {
5539                    canvas.translate(0, cursorOffsetVertical);
5540                }
5541
5542                canvas.drawPath(mPath, mPaint);
5543
5544                if (cursorOffsetVertical != 0) {
5545                    canvas.translate(0, -cursorOffsetVertical);
5546                }
5547                invalidate(true); // TODO invalidate cursor region only
5548            } else {
5549                stopAnimation();
5550                invalidate(false); // TODO invalidate cursor region only
5551            }
5552        }
5553
5554        private boolean updatePaint() {
5555            final long duration = SystemClock.uptimeMillis() - mFadingStartTime;
5556            if (duration > FADE_OUT_DURATION) return false;
5557
5558            final float coef = 1.0f - (float) duration / FADE_OUT_DURATION;
5559            final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor);
5560            final int color = (mTextView.mHighlightColor & 0x00FFFFFF) +
5561                    ((int) (highlightColorAlpha * coef) << 24);
5562            mPaint.setColor(color);
5563            return true;
5564        }
5565
5566        private boolean updatePath() {
5567            final Layout layout = mTextView.getLayout();
5568            if (layout == null) return false;
5569
5570            // Update in case text is edited while the animation is run
5571            final int length = mTextView.getText().length();
5572            int start = Math.min(length, mStart);
5573            int end = Math.min(length, mEnd);
5574
5575            mPath.reset();
5576            layout.getSelectionPath(start, end, mPath);
5577            return true;
5578        }
5579
5580        private void invalidate(boolean delayed) {
5581            if (mTextView.getLayout() == null) return;
5582
5583            if (mTempRectF == null) mTempRectF = new RectF();
5584            mPath.computeBounds(mTempRectF, false);
5585
5586            int left = mTextView.getCompoundPaddingLeft();
5587            int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true);
5588
5589            if (delayed) {
5590                mTextView.postInvalidateOnAnimation(
5591                        left + (int) mTempRectF.left, top + (int) mTempRectF.top,
5592                        left + (int) mTempRectF.right, top + (int) mTempRectF.bottom);
5593            } else {
5594                mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top,
5595                        (int) mTempRectF.right, (int) mTempRectF.bottom);
5596            }
5597        }
5598
5599        private void stopAnimation() {
5600            Editor.this.mCorrectionHighlighter = null;
5601        }
5602    }
5603
5604    private static class ErrorPopup extends PopupWindow {
5605        private boolean mAbove = false;
5606        private final TextView mView;
5607        private int mPopupInlineErrorBackgroundId = 0;
5608        private int mPopupInlineErrorAboveBackgroundId = 0;
5609
5610        ErrorPopup(TextView v, int width, int height) {
5611            super(v, width, height);
5612            mView = v;
5613            // Make sure the TextView has a background set as it will be used the first time it is
5614            // shown and positioned. Initialized with below background, which should have
5615            // dimensions identical to the above version for this to work (and is more likely).
5616            mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5617                    com.android.internal.R.styleable.Theme_errorMessageBackground);
5618            mView.setBackgroundResource(mPopupInlineErrorBackgroundId);
5619        }
5620
5621        void fixDirection(boolean above) {
5622            mAbove = above;
5623
5624            if (above) {
5625                mPopupInlineErrorAboveBackgroundId =
5626                    getResourceId(mPopupInlineErrorAboveBackgroundId,
5627                            com.android.internal.R.styleable.Theme_errorMessageAboveBackground);
5628            } else {
5629                mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId,
5630                        com.android.internal.R.styleable.Theme_errorMessageBackground);
5631            }
5632
5633            mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId :
5634                mPopupInlineErrorBackgroundId);
5635        }
5636
5637        private int getResourceId(int currentId, int index) {
5638            if (currentId == 0) {
5639                TypedArray styledAttributes = mView.getContext().obtainStyledAttributes(
5640                        R.styleable.Theme);
5641                currentId = styledAttributes.getResourceId(index, 0);
5642                styledAttributes.recycle();
5643            }
5644            return currentId;
5645        }
5646
5647        @Override
5648        public void update(int x, int y, int w, int h, boolean force) {
5649            super.update(x, y, w, h, force);
5650
5651            boolean above = isAboveAnchor();
5652            if (above != mAbove) {
5653                fixDirection(above);
5654            }
5655        }
5656    }
5657
5658    static class InputContentType {
5659        int imeOptions = EditorInfo.IME_NULL;
5660        String privateImeOptions;
5661        CharSequence imeActionLabel;
5662        int imeActionId;
5663        Bundle extras;
5664        OnEditorActionListener onEditorActionListener;
5665        boolean enterDown;
5666        LocaleList imeHintLocales;
5667    }
5668
5669    static class InputMethodState {
5670        ExtractedTextRequest mExtractedTextRequest;
5671        final ExtractedText mExtractedText = new ExtractedText();
5672        int mBatchEditNesting;
5673        boolean mCursorChanged;
5674        boolean mSelectionModeChanged;
5675        boolean mContentChanged;
5676        int mChangedStart, mChangedEnd, mChangedDelta;
5677    }
5678
5679    /**
5680     * @return True iff (start, end) is a valid range within the text.
5681     */
5682    private static boolean isValidRange(CharSequence text, int start, int end) {
5683        return 0 <= start && start <= end && end <= text.length();
5684    }
5685
5686    @VisibleForTesting
5687    public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() {
5688        return mSuggestionsPopupWindow;
5689    }
5690
5691    /**
5692     * An InputFilter that monitors text input to maintain undo history. It does not modify the
5693     * text being typed (and hence always returns null from the filter() method).
5694     *
5695     * TODO: Make this span aware.
5696     */
5697    public static class UndoInputFilter implements InputFilter {
5698        private final Editor mEditor;
5699
5700        // Whether the current filter pass is directly caused by an end-user text edit.
5701        private boolean mIsUserEdit;
5702
5703        // Whether the text field is handling an IME composition. Must be parceled in case the user
5704        // rotates the screen during composition.
5705        private boolean mHasComposition;
5706
5707        // Whether the user is expanding or shortening the text
5708        private boolean mExpanding;
5709
5710        // Whether the previous edit operation was in the current batch edit.
5711        private boolean mPreviousOperationWasInSameBatchEdit;
5712
5713        public UndoInputFilter(Editor editor) {
5714            mEditor = editor;
5715        }
5716
5717        public void saveInstanceState(Parcel parcel) {
5718            parcel.writeInt(mIsUserEdit ? 1 : 0);
5719            parcel.writeInt(mHasComposition ? 1 : 0);
5720            parcel.writeInt(mExpanding ? 1 : 0);
5721            parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0);
5722        }
5723
5724        public void restoreInstanceState(Parcel parcel) {
5725            mIsUserEdit = parcel.readInt() != 0;
5726            mHasComposition = parcel.readInt() != 0;
5727            mExpanding = parcel.readInt() != 0;
5728            mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0;
5729        }
5730
5731        /**
5732         * Signals that a user-triggered edit is starting.
5733         */
5734        public void beginBatchEdit() {
5735            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
5736            mIsUserEdit = true;
5737        }
5738
5739        public void endBatchEdit() {
5740            if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit");
5741            mIsUserEdit = false;
5742            mPreviousOperationWasInSameBatchEdit = false;
5743        }
5744
5745        @Override
5746        public CharSequence filter(CharSequence source, int start, int end,
5747                Spanned dest, int dstart, int dend) {
5748            if (DEBUG_UNDO) {
5749                Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " +
5750                        "dest=" + dest + " (" + dstart + "-" + dend + ")");
5751            }
5752
5753            // Check to see if this edit should be tracked for undo.
5754            if (!canUndoEdit(source, start, end, dest, dstart, dend)) {
5755                return null;
5756            }
5757
5758            final boolean hadComposition = mHasComposition;
5759            mHasComposition = isComposition(source);
5760            final boolean wasExpanding = mExpanding;
5761            boolean shouldCreateSeparateState = false;
5762            if ((end - start) != (dend - dstart)) {
5763                mExpanding = (end - start) > (dend - dstart);
5764                if (hadComposition && mExpanding != wasExpanding) {
5765                    shouldCreateSeparateState = true;
5766                }
5767            }
5768
5769            // Handle edit.
5770            handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState);
5771            return null;
5772        }
5773
5774        void freezeLastEdit() {
5775            mEditor.mUndoManager.beginUpdate("Edit text");
5776            EditOperation lastEdit = getLastEdit();
5777            if (lastEdit != null) {
5778                lastEdit.mFrozen = true;
5779            }
5780            mEditor.mUndoManager.endUpdate();
5781        }
5782
5783        @Retention(RetentionPolicy.SOURCE)
5784        @IntDef({MERGE_EDIT_MODE_FORCE_MERGE, MERGE_EDIT_MODE_NEVER_MERGE, MERGE_EDIT_MODE_NORMAL})
5785        private @interface MergeMode {}
5786        private final static int MERGE_EDIT_MODE_FORCE_MERGE = 0;
5787        private final static int MERGE_EDIT_MODE_NEVER_MERGE = 1;
5788        /** Use {@link EditOperation#mergeWith} to merge */
5789        private final static int MERGE_EDIT_MODE_NORMAL = 2;
5790
5791        private void handleEdit(CharSequence source, int start, int end,
5792                Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) {
5793            // An application may install a TextWatcher to provide additional modifications after
5794            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
5795            // string). This results in multiple filter() calls for what the user considers to be
5796            // a single operation. Always undo the whole set of changes in one step.
5797            @MergeMode
5798            final int mergeMode;
5799            if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) {
5800                mergeMode = MERGE_EDIT_MODE_FORCE_MERGE;
5801            } else if (shouldCreateSeparateState) {
5802                mergeMode = MERGE_EDIT_MODE_NEVER_MERGE;
5803            } else {
5804                mergeMode = MERGE_EDIT_MODE_NORMAL;
5805            }
5806            // Build a new operation with all the information from this edit.
5807            String newText = TextUtils.substring(source, start, end);
5808            String oldText = TextUtils.substring(dest, dstart, dend);
5809            EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText,
5810                    mHasComposition);
5811            if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) {
5812                return;
5813            }
5814            recordEdit(edit, mergeMode);
5815        }
5816
5817        private EditOperation getLastEdit() {
5818            final UndoManager um = mEditor.mUndoManager;
5819            return um.getLastOperation(
5820                  EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE);
5821        }
5822        /**
5823         * Fetches the last undo operation and checks to see if a new edit should be merged into it.
5824         * If forceMerge is true then the new edit is always merged.
5825         */
5826        private void recordEdit(EditOperation edit, @MergeMode int mergeMode) {
5827            // Fetch the last edit operation and attempt to merge in the new edit.
5828            final UndoManager um = mEditor.mUndoManager;
5829            um.beginUpdate("Edit text");
5830            EditOperation lastEdit = getLastEdit();
5831            if (lastEdit == null) {
5832                // Add this as the first edit.
5833                if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit);
5834                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
5835            } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) {
5836                // Forced merges take priority because they could be the result of a non-user-edit
5837                // change and this case should not create a new undo operation.
5838                if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit);
5839                lastEdit.forceMergeWith(edit);
5840            } else if (!mIsUserEdit) {
5841                // An application directly modified the Editable outside of a text edit. Treat this
5842                // as a new change and don't attempt to merge.
5843                if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit);
5844                um.commitState(mEditor.mUndoOwner);
5845                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
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            }
5855            mPreviousOperationWasInSameBatchEdit = mIsUserEdit;
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