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