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