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