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