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