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