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