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