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