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