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