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