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