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