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