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