OSDN Git Service

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