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