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