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