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