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