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