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