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