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