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