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