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