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