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