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