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