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