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