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