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