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