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