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