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