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 import static com.android.text.flags.Flags.contextMenuHideUnavailableItems; 24 25 import android.R; 26 import android.animation.ValueAnimator; 27 import android.annotation.IntDef; 28 import android.annotation.NonNull; 29 import android.annotation.Nullable; 30 import android.app.AppGlobals; 31 import android.app.PendingIntent; 32 import android.app.PendingIntent.CanceledException; 33 import android.app.RemoteAction; 34 import android.compat.annotation.UnsupportedAppUsage; 35 import android.content.ClipData; 36 import android.content.Context; 37 import android.content.Intent; 38 import android.content.UndoManager; 39 import android.content.UndoOperation; 40 import android.content.UndoOwner; 41 import android.content.pm.PackageManager; 42 import android.content.pm.ResolveInfo; 43 import android.content.res.Configuration; 44 import android.content.res.TypedArray; 45 import android.graphics.Canvas; 46 import android.graphics.Color; 47 import android.graphics.Matrix; 48 import android.graphics.Paint; 49 import android.graphics.Path; 50 import android.graphics.Point; 51 import android.graphics.PointF; 52 import android.graphics.RecordingCanvas; 53 import android.graphics.Rect; 54 import android.graphics.RectF; 55 import android.graphics.RenderNode; 56 import android.graphics.drawable.ColorDrawable; 57 import android.graphics.drawable.Drawable; 58 import android.graphics.drawable.GradientDrawable; 59 import android.os.Build; 60 import android.os.Bundle; 61 import android.os.LocaleList; 62 import android.os.Parcel; 63 import android.os.Parcelable; 64 import android.os.ParcelableParcel; 65 import android.os.SystemClock; 66 import android.provider.Settings; 67 import android.text.DynamicLayout; 68 import android.text.Editable; 69 import android.text.InputFilter; 70 import android.text.InputType; 71 import android.text.Layout; 72 import android.text.ParcelableSpan; 73 import android.text.Selection; 74 import android.text.SpanWatcher; 75 import android.text.Spannable; 76 import android.text.SpannableStringBuilder; 77 import android.text.Spanned; 78 import android.text.SpannedString; 79 import android.text.StaticLayout; 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 475 private final AccessibilitySmartActions mA11ySmartActions; 476 private InsertModeController mInsertModeController; 477 478 /** 479 * @hide 480 */ 481 @VisibleForTesting Editor(TextView textView)482 public Editor(TextView textView) { 483 mTextView = textView; 484 // Synchronize the filter list, which places the undo input filter at the end. 485 mTextView.setFilters(mTextView.getFilters()); 486 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this); 487 mA11ySmartActions = new AccessibilitySmartActions(mTextView); 488 mHapticTextHandleEnabled = mTextView.getContext().getResources().getBoolean( 489 com.android.internal.R.bool.config_enableHapticTextHandle); 490 491 mFlagCursorDragFromAnywhereEnabled = AppGlobals.getIntCoreSetting( 492 WidgetFlags.KEY_ENABLE_CURSOR_DRAG_FROM_ANYWHERE, 493 WidgetFlags.ENABLE_CURSOR_DRAG_FROM_ANYWHERE_DEFAULT ? 1 : 0) != 0; 494 final int cursorDragMinAngleFromVertical = AppGlobals.getIntCoreSetting( 495 WidgetFlags.KEY_CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL, 496 WidgetFlags.CURSOR_DRAG_MIN_ANGLE_FROM_VERTICAL_DEFAULT); 497 mCursorDragDirectionMinXYRatio = EditorTouchState.getXYRatio( 498 cursorDragMinAngleFromVertical); 499 mFlagInsertionHandleGesturesEnabled = AppGlobals.getIntCoreSetting( 500 WidgetFlags.KEY_ENABLE_INSERTION_HANDLE_GESTURES, 501 WidgetFlags.ENABLE_INSERTION_HANDLE_GESTURES_DEFAULT ? 1 : 0) != 0; 502 mNewMagnifierEnabled = AppGlobals.getIntCoreSetting( 503 WidgetFlags.KEY_ENABLE_NEW_MAGNIFIER, 504 WidgetFlags.ENABLE_NEW_MAGNIFIER_DEFAULT ? 1 : 0) != 0; 505 mLineSlopRatio = AppGlobals.getFloatCoreSetting( 506 WidgetFlags.KEY_LINE_SLOP_RATIO, 507 WidgetFlags.LINE_SLOP_RATIO_DEFAULT); 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 menu.setOptionalIconsVisible(true); 3215 menu.setGroupDividerEnabled(true); 3216 3217 setAssistContextMenuItems(menu); 3218 3219 final int keyboard = mTextView.getResources().getConfiguration().keyboard; 3220 menu.setQwertyMode(keyboard == Configuration.KEYBOARD_QWERTY); 3221 3222 setTextContextMenuItems(menu); 3223 3224 mPreserveSelection = true; 3225 3226 // No-op for the old context menu because it doesn't have icons. 3227 adjustIconSpacing(menu); 3228 } 3229 3230 /** @hide */ 3231 @VisibleForTesting setTextContextMenuItems(ContextMenu menu)3232 public void setTextContextMenuItems(ContextMenu menu) { 3233 final TypedArray a = mTextView.getContext().obtainStyledAttributes(new int[]{ 3234 // TODO: Make Undo/Redo be public attribute. 3235 com.android.internal.R.attr.actionModeUndoDrawable, 3236 com.android.internal.R.attr.actionModeRedoDrawable, 3237 android.R.attr.actionModeCutDrawable, 3238 android.R.attr.actionModeCopyDrawable, 3239 android.R.attr.actionModePasteDrawable, 3240 android.R.attr.actionModeSelectAllDrawable, 3241 android.R.attr.actionModeShareDrawable, 3242 }); 3243 3244 final int menuItemOrderUndo = 2; 3245 final int menuItemOrderRedo = 3; 3246 final int menuItemOrderCut = 4; 3247 final int menuItemOrderCopy = 5; 3248 final int menuItemOrderPaste = 6; 3249 final int menuItemOrderPasteAsPlainText = 7; 3250 final int menuItemOrderSelectAll = 8; 3251 final int menuItemOrderShare = 9; 3252 final int menuItemOrderAutofill = 10; 3253 3254 if (contextMenuHideUnavailableItems()) { 3255 if (mTextView.canUndo()) { 3256 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo, 3257 com.android.internal.R.string.undo) 3258 .setAlphabeticShortcut('z') 3259 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3260 .setIcon(a.getDrawable(0)); 3261 } 3262 3263 if (mTextView.canRedo()) { 3264 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, menuItemOrderRedo, 3265 com.android.internal.R.string.redo) 3266 .setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON) 3267 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3268 .setIcon(a.getDrawable(1)); 3269 } 3270 3271 if (mTextView.canCut()) { 3272 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut, 3273 com.android.internal.R.string.cut) 3274 .setAlphabeticShortcut('x') 3275 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3276 .setIcon(a.getDrawable(2)); 3277 } 3278 3279 if (mTextView.canCopy()) { 3280 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy, 3281 com.android.internal.R.string.copy) 3282 .setAlphabeticShortcut('c') 3283 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3284 .setIcon(a.getDrawable(3)); 3285 } 3286 3287 if (mTextView.canPaste()) { 3288 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste, 3289 com.android.internal.R.string.paste) 3290 .setAlphabeticShortcut('v') 3291 .setIcon(a.getDrawable(4)) 3292 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3293 } 3294 3295 if (mTextView.canPasteAsPlainText()) { 3296 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT, 3297 menuItemOrderPasteAsPlainText, 3298 com.android.internal.R.string.paste_as_plain_text) 3299 .setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON) 3300 .setIcon(a.getDrawable(4)) 3301 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3302 } 3303 3304 if (mTextView.canSelectAllText()) { 3305 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL, 3306 menuItemOrderSelectAll, com.android.internal.R.string.selectAll) 3307 .setAlphabeticShortcut('a') 3308 .setIcon(a.getDrawable(5)) 3309 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3310 } 3311 3312 if (mTextView.canShare()) { 3313 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare, 3314 com.android.internal.R.string.share) 3315 .setIcon(a.getDrawable(6)) 3316 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3317 } 3318 3319 final String selected = mTextView.getSelectedText(); 3320 if (mTextView.canRequestAutofill() && (selected == null || selected.isEmpty())) { 3321 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill, 3322 android.R.string.autofill) 3323 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3324 } 3325 } else { 3326 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_UNDO, menuItemOrderUndo, 3327 com.android.internal.R.string.undo) 3328 .setAlphabeticShortcut('z') 3329 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3330 .setIcon(a.getDrawable(0)) 3331 .setEnabled(mTextView.canUndo()); 3332 menu.add(CONTEXT_MENU_GROUP_UNDO_REDO, TextView.ID_REDO, menuItemOrderRedo, 3333 com.android.internal.R.string.redo) 3334 .setAlphabeticShortcut('z', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON) 3335 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3336 .setIcon(a.getDrawable(1)) 3337 .setEnabled(mTextView.canRedo()); 3338 3339 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_CUT, menuItemOrderCut, 3340 com.android.internal.R.string.cut) 3341 .setAlphabeticShortcut('x') 3342 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3343 .setIcon(a.getDrawable(2)) 3344 .setEnabled(mTextView.canCut()); 3345 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_COPY, menuItemOrderCopy, 3346 com.android.internal.R.string.copy) 3347 .setAlphabeticShortcut('c') 3348 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 3349 .setIcon(a.getDrawable(3)) 3350 .setEnabled(mTextView.canCopy()); 3351 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE, menuItemOrderPaste, 3352 com.android.internal.R.string.paste) 3353 .setAlphabeticShortcut('v') 3354 .setEnabled(mTextView.canPaste()) 3355 .setIcon(a.getDrawable(4)) 3356 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3357 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_PASTE_AS_PLAIN_TEXT, 3358 menuItemOrderPasteAsPlainText, 3359 com.android.internal.R.string.paste_as_plain_text) 3360 .setAlphabeticShortcut('v', KeyEvent.META_CTRL_ON | KeyEvent.META_SHIFT_ON) 3361 .setEnabled(mTextView.canPasteAsPlainText()) 3362 .setIcon(a.getDrawable(4)) 3363 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3364 menu.add(CONTEXT_MENU_GROUP_CLIPBOARD, TextView.ID_SELECT_ALL, 3365 menuItemOrderSelectAll, com.android.internal.R.string.selectAll) 3366 .setAlphabeticShortcut('a') 3367 .setEnabled(mTextView.canSelectAllText()) 3368 .setIcon(a.getDrawable(5)) 3369 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3370 3371 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_SHARE, menuItemOrderShare, 3372 com.android.internal.R.string.share) 3373 .setEnabled(mTextView.canShare()) 3374 .setIcon(a.getDrawable(6)) 3375 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3376 final String selected = mTextView.getSelectedText(); 3377 menu.add(CONTEXT_MENU_GROUP_MISC, TextView.ID_AUTOFILL, menuItemOrderAutofill, 3378 android.R.string.autofill) 3379 .setEnabled(mTextView.canRequestAutofill() 3380 && (selected == null || selected.isEmpty())) 3381 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 3382 } 3383 a.recycle(); 3384 } 3385 3386 /** 3387 * Adjust icon spacing to align the texts. 3388 * @hide 3389 */ 3390 @VisibleForTesting adjustIconSpacing(ContextMenu menu)3391 public void adjustIconSpacing(ContextMenu menu) { 3392 int width = -1; 3393 int height = -1; 3394 for (int i = 0; i < menu.size(); ++i) { 3395 final MenuItem item = menu.getItem(i); 3396 final Drawable d = item.getIcon(); 3397 if (d == null) { 3398 continue; 3399 } 3400 3401 width = Math.max(width, d.getIntrinsicWidth()); 3402 height = Math.max(height, d.getIntrinsicHeight()); 3403 } 3404 3405 if (width < 0 || height < 0) { 3406 return; // No menu has icon drawable. 3407 } 3408 3409 GradientDrawable paddingDrawable = new GradientDrawable(); 3410 paddingDrawable.setSize(width, height); 3411 3412 for (int i = 0; i < menu.size(); ++i) { 3413 final MenuItem item = menu.getItem(i); 3414 final Drawable d = item.getIcon(); 3415 if (d == null) { 3416 item.setIcon(paddingDrawable); 3417 } 3418 } 3419 } 3420 3421 @Nullable findEquivalentSuggestionSpan( @onNull SuggestionSpanInfo suggestionSpanInfo)3422 private SuggestionSpan findEquivalentSuggestionSpan( 3423 @NonNull SuggestionSpanInfo suggestionSpanInfo) { 3424 final Editable editable = (Editable) mTextView.getText(); 3425 if (editable.getSpanStart(suggestionSpanInfo.mSuggestionSpan) >= 0) { 3426 // Exactly same span is found. 3427 return suggestionSpanInfo.mSuggestionSpan; 3428 } 3429 // Suggestion span couldn't be found. Try to find a suggestion span that has the same 3430 // contents. 3431 final SuggestionSpan[] suggestionSpans = editable.getSpans(suggestionSpanInfo.mSpanStart, 3432 suggestionSpanInfo.mSpanEnd, SuggestionSpan.class); 3433 for (final SuggestionSpan suggestionSpan : suggestionSpans) { 3434 final int start = editable.getSpanStart(suggestionSpan); 3435 if (start != suggestionSpanInfo.mSpanStart) { 3436 continue; 3437 } 3438 final int end = editable.getSpanEnd(suggestionSpan); 3439 if (end != suggestionSpanInfo.mSpanEnd) { 3440 continue; 3441 } 3442 if (suggestionSpan.equals(suggestionSpanInfo.mSuggestionSpan)) { 3443 return suggestionSpan; 3444 } 3445 } 3446 return null; 3447 } 3448 replaceWithSuggestion(@onNull final SuggestionInfo suggestionInfo)3449 private void replaceWithSuggestion(@NonNull final SuggestionInfo suggestionInfo) { 3450 final SuggestionSpan targetSuggestionSpan = findEquivalentSuggestionSpan( 3451 suggestionInfo.mSuggestionSpanInfo); 3452 if (targetSuggestionSpan == null) { 3453 // Span has been removed 3454 return; 3455 } 3456 final Editable editable = (Editable) mTextView.getText(); 3457 final int spanStart = editable.getSpanStart(targetSuggestionSpan); 3458 final int spanEnd = editable.getSpanEnd(targetSuggestionSpan); 3459 if (spanStart < 0 || spanEnd <= spanStart) { 3460 // Span has been removed 3461 return; 3462 } 3463 3464 final String originalText = TextUtils.substring(editable, spanStart, spanEnd); 3465 // SuggestionSpans are removed by replace: save them before 3466 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, 3467 SuggestionSpan.class); 3468 final int length = suggestionSpans.length; 3469 int[] suggestionSpansStarts = new int[length]; 3470 int[] suggestionSpansEnds = new int[length]; 3471 int[] suggestionSpansFlags = new int[length]; 3472 for (int i = 0; i < length; i++) { 3473 final SuggestionSpan suggestionSpan = suggestionSpans[i]; 3474 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); 3475 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); 3476 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); 3477 3478 // Remove potential misspelled flags 3479 int suggestionSpanFlags = suggestionSpan.getFlags(); 3480 if ((suggestionSpanFlags & FLAG_MISSPELLED_OR_GRAMMAR_ERROR) != 0) { 3481 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; 3482 suggestionSpanFlags &= ~SuggestionSpan.FLAG_GRAMMAR_ERROR; 3483 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; 3484 suggestionSpan.setFlags(suggestionSpanFlags); 3485 } 3486 } 3487 3488 // Swap text content between actual text and Suggestion span 3489 final int suggestionStart = suggestionInfo.mSuggestionStart; 3490 final int suggestionEnd = suggestionInfo.mSuggestionEnd; 3491 final String suggestion = suggestionInfo.mText.subSequence( 3492 suggestionStart, suggestionEnd).toString(); 3493 mTextView.replaceText_internal(spanStart, spanEnd, suggestion); 3494 3495 String[] suggestions = targetSuggestionSpan.getSuggestions(); 3496 suggestions[suggestionInfo.mSuggestionIndex] = originalText; 3497 3498 // Restore previous SuggestionSpans 3499 final int lengthDelta = suggestion.length() - (spanEnd - spanStart); 3500 for (int i = 0; i < length; i++) { 3501 // Only spans that include the modified region make sense after replacement 3502 // Spans partially included in the replaced region are removed, there is no 3503 // way to assign them a valid range after replacement 3504 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) { 3505 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], 3506 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]); 3507 } 3508 } 3509 // Move cursor at the end of the replaced word 3510 final int newCursorPosition = spanEnd + lengthDelta; 3511 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); 3512 } 3513 3514 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener = 3515 new MenuItem.OnMenuItemClickListener() { 3516 @Override 3517 public boolean onMenuItemClick(MenuItem item) { 3518 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { 3519 return true; 3520 } 3521 return mTextView.onTextContextMenuItem(item.getItemId()); 3522 } 3523 }; 3524 3525 /** 3526 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related 3527 * pop-up should be displayed. 3528 * Also monitors {@link Selection} to call back to the attached input method. 3529 */ 3530 private class SpanController implements SpanWatcher { 3531 3532 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs 3533 3534 private EasyEditPopupWindow mPopupWindow; 3535 3536 private Runnable mHidePopup; 3537 3538 // This function is pure but inner classes can't have static functions isNonIntermediateSelectionSpan(final Spannable text, final Object span)3539 private boolean isNonIntermediateSelectionSpan(final Spannable text, 3540 final Object span) { 3541 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span) 3542 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0; 3543 } 3544 3545 @Override onSpanAdded(Spannable text, Object span, int start, int end)3546 public void onSpanAdded(Spannable text, Object span, int start, int end) { 3547 if (isNonIntermediateSelectionSpan(text, span)) { 3548 sendUpdateSelection(); 3549 } else if (span instanceof EasyEditSpan) { 3550 if (mPopupWindow == null) { 3551 mPopupWindow = new EasyEditPopupWindow(); 3552 mHidePopup = new Runnable() { 3553 @Override 3554 public void run() { 3555 hide(); 3556 } 3557 }; 3558 } 3559 3560 // Make sure there is only at most one EasyEditSpan in the text 3561 if (mPopupWindow.mEasyEditSpan != null) { 3562 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false); 3563 } 3564 3565 mPopupWindow.setEasyEditSpan((EasyEditSpan) span); 3566 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() { 3567 @Override 3568 public void onDeleteClick(EasyEditSpan span) { 3569 Editable editable = (Editable) mTextView.getText(); 3570 int start = editable.getSpanStart(span); 3571 int end = editable.getSpanEnd(span); 3572 if (start >= 0 && end >= 0) { 3573 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span); 3574 mTextView.deleteText_internal(start, end); 3575 } 3576 editable.removeSpan(span); 3577 } 3578 }); 3579 3580 if (mTextView.getWindowVisibility() != View.VISIBLE) { 3581 // The window is not visible yet, ignore the text change. 3582 return; 3583 } 3584 3585 if (mTextView.getLayout() == null) { 3586 // The view has not been laid out yet, ignore the text change 3587 return; 3588 } 3589 3590 if (extractedTextModeWillBeStarted()) { 3591 // The input is in extract mode. Do not handle the easy edit in 3592 // the original TextView, as the ExtractEditText will do 3593 return; 3594 } 3595 3596 mPopupWindow.show(); 3597 mTextView.removeCallbacks(mHidePopup); 3598 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); 3599 } 3600 } 3601 3602 @Override onSpanRemoved(Spannable text, Object span, int start, int end)3603 public void onSpanRemoved(Spannable text, Object span, int start, int end) { 3604 if (isNonIntermediateSelectionSpan(text, span)) { 3605 sendUpdateSelection(); 3606 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { 3607 hide(); 3608 } 3609 } 3610 3611 @Override onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, int newStart, int newEnd)3612 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, 3613 int newStart, int newEnd) { 3614 if (isNonIntermediateSelectionSpan(text, span)) { 3615 sendUpdateSelection(); 3616 } else if (mPopupWindow != null && span instanceof EasyEditSpan) { 3617 EasyEditSpan easyEditSpan = (EasyEditSpan) span; 3618 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan); 3619 text.removeSpan(easyEditSpan); 3620 } 3621 } 3622 hide()3623 public void hide() { 3624 if (mPopupWindow != null) { 3625 mPopupWindow.hide(); 3626 mTextView.removeCallbacks(mHidePopup); 3627 } 3628 } 3629 sendEasySpanNotification(int textChangedType, EasyEditSpan span)3630 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) { 3631 try { 3632 PendingIntent pendingIntent = span.getPendingIntent(); 3633 if (pendingIntent != null) { 3634 Intent intent = new Intent(); 3635 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType); 3636 pendingIntent.send(mTextView.getContext(), 0, intent); 3637 } 3638 } catch (CanceledException e) { 3639 // This should not happen, as we should try to send the intent only once. 3640 Log.w(TAG, "PendingIntent for notification cannot be sent", e); 3641 } 3642 } 3643 } 3644 3645 /** 3646 * Listens for the delete event triggered by {@link EasyEditPopupWindow}. 3647 */ 3648 private interface EasyEditDeleteListener { 3649 3650 /** 3651 * Clicks the delete pop-up. 3652 */ onDeleteClick(EasyEditSpan span)3653 void onDeleteClick(EasyEditSpan span); 3654 } 3655 3656 /** 3657 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled 3658 * by {@link SpanController}. 3659 */ 3660 private class EasyEditPopupWindow extends PinnedPopupWindow 3661 implements OnClickListener { 3662 private static final int POPUP_TEXT_LAYOUT = 3663 com.android.internal.R.layout.text_edit_action_popup_text; 3664 private TextView mDeleteTextView; 3665 private EasyEditSpan mEasyEditSpan; 3666 private EasyEditDeleteListener mOnDeleteListener; 3667 3668 @Override createPopupWindow()3669 protected void createPopupWindow() { 3670 mPopupWindow = new PopupWindow(mTextView.getContext(), null, 3671 com.android.internal.R.attr.textSelectHandleWindowStyle); 3672 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 3673 mPopupWindow.setClippingEnabled(true); 3674 } 3675 3676 @Override initContentView()3677 protected void initContentView() { 3678 LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); 3679 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 3680 mContentView = linearLayout; 3681 mContentView.setBackgroundResource( 3682 com.android.internal.R.drawable.text_edit_side_paste_window); 3683 3684 LayoutInflater inflater = (LayoutInflater) mTextView.getContext() 3685 .getSystemService(Context.LAYOUT_INFLATER_SERVICE); 3686 3687 LayoutParams wrapContent = new LayoutParams( 3688 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 3689 3690 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); 3691 mDeleteTextView.setLayoutParams(wrapContent); 3692 mDeleteTextView.setText(com.android.internal.R.string.delete); 3693 mDeleteTextView.setOnClickListener(this); 3694 mContentView.addView(mDeleteTextView); 3695 } 3696 setEasyEditSpan(EasyEditSpan easyEditSpan)3697 public void setEasyEditSpan(EasyEditSpan easyEditSpan) { 3698 mEasyEditSpan = easyEditSpan; 3699 } 3700 setOnDeleteListener(EasyEditDeleteListener listener)3701 private void setOnDeleteListener(EasyEditDeleteListener listener) { 3702 mOnDeleteListener = listener; 3703 } 3704 3705 @Override onClick(View view)3706 public void onClick(View view) { 3707 if (view == mDeleteTextView 3708 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled() 3709 && mOnDeleteListener != null) { 3710 mOnDeleteListener.onDeleteClick(mEasyEditSpan); 3711 } 3712 } 3713 3714 @Override hide()3715 public void hide() { 3716 if (mEasyEditSpan != null) { 3717 mEasyEditSpan.setDeleteEnabled(false); 3718 } 3719 mOnDeleteListener = null; 3720 super.hide(); 3721 } 3722 3723 @Override getTextOffset()3724 protected int getTextOffset() { 3725 // Place the pop-up at the end of the span 3726 Editable editable = (Editable) mTextView.getText(); 3727 return editable.getSpanEnd(mEasyEditSpan); 3728 } 3729 3730 @Override getVerticalLocalPosition(int line)3731 protected int getVerticalLocalPosition(int line) { 3732 final Layout layout = mTextView.getLayout(); 3733 return layout.getLineBottom(line, /* includeLineSpacing= */ false); 3734 } 3735 3736 @Override clipVertically(int positionY)3737 protected int clipVertically(int positionY) { 3738 // As we display the pop-up below the span, no vertical clipping is required. 3739 return positionY; 3740 } 3741 } 3742 3743 private class PositionListener implements ViewTreeObserver.OnPreDrawListener { 3744 // 3 handles 3745 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) 3746 // 1 CursorAnchorInfoNotifier 3747 private static final int MAXIMUM_NUMBER_OF_LISTENERS = 7; 3748 private TextViewPositionListener[] mPositionListeners = 3749 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; 3750 private boolean[] mCanMove = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; 3751 private boolean mPositionHasChanged = true; 3752 // Absolute position of the TextView with respect to its parent window 3753 private int mPositionX, mPositionY; 3754 private int mPositionXOnScreen, mPositionYOnScreen; 3755 private int mNumberOfListeners; 3756 private boolean mScrollHasChanged; 3757 final int[] mTempCoords = new int[2]; 3758 addSubscriber(TextViewPositionListener positionListener, boolean canMove)3759 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { 3760 if (mNumberOfListeners == 0) { 3761 updatePosition(); 3762 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 3763 vto.addOnPreDrawListener(this); 3764 } 3765 3766 int emptySlotIndex = -1; 3767 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 3768 TextViewPositionListener listener = mPositionListeners[i]; 3769 if (listener == positionListener) { 3770 return; 3771 } else if (emptySlotIndex < 0 && listener == null) { 3772 emptySlotIndex = i; 3773 } 3774 } 3775 3776 mPositionListeners[emptySlotIndex] = positionListener; 3777 mCanMove[emptySlotIndex] = canMove; 3778 mNumberOfListeners++; 3779 } 3780 removeSubscriber(TextViewPositionListener positionListener)3781 public void removeSubscriber(TextViewPositionListener positionListener) { 3782 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 3783 if (mPositionListeners[i] == positionListener) { 3784 mPositionListeners[i] = null; 3785 mNumberOfListeners--; 3786 break; 3787 } 3788 } 3789 3790 if (mNumberOfListeners == 0) { 3791 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 3792 vto.removeOnPreDrawListener(this); 3793 } 3794 } 3795 getPositionX()3796 public int getPositionX() { 3797 return mPositionX; 3798 } 3799 getPositionY()3800 public int getPositionY() { 3801 return mPositionY; 3802 } 3803 getPositionXOnScreen()3804 public int getPositionXOnScreen() { 3805 return mPositionXOnScreen; 3806 } 3807 getPositionYOnScreen()3808 public int getPositionYOnScreen() { 3809 return mPositionYOnScreen; 3810 } 3811 3812 @Override onPreDraw()3813 public boolean onPreDraw() { 3814 updatePosition(); 3815 3816 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 3817 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { 3818 TextViewPositionListener positionListener = mPositionListeners[i]; 3819 if (positionListener != null) { 3820 positionListener.updatePosition(mPositionX, mPositionY, 3821 mPositionHasChanged, mScrollHasChanged); 3822 } 3823 } 3824 } 3825 3826 mScrollHasChanged = false; 3827 return true; 3828 } 3829 updatePosition()3830 private void updatePosition() { 3831 mTextView.getLocationInWindow(mTempCoords); 3832 3833 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; 3834 3835 mPositionX = mTempCoords[0]; 3836 mPositionY = mTempCoords[1]; 3837 3838 mTextView.getLocationOnScreen(mTempCoords); 3839 3840 mPositionXOnScreen = mTempCoords[0]; 3841 mPositionYOnScreen = mTempCoords[1]; 3842 } 3843 onScrollChanged()3844 public void onScrollChanged() { 3845 mScrollHasChanged = true; 3846 } 3847 } 3848 3849 private abstract class PinnedPopupWindow implements TextViewPositionListener { 3850 protected PopupWindow mPopupWindow; 3851 protected ViewGroup mContentView; 3852 int mPositionX, mPositionY; 3853 int mClippingLimitLeft, mClippingLimitRight; 3854 createPopupWindow()3855 protected abstract void createPopupWindow(); initContentView()3856 protected abstract void initContentView(); getTextOffset()3857 protected abstract int getTextOffset(); getVerticalLocalPosition(int line)3858 protected abstract int getVerticalLocalPosition(int line); clipVertically(int positionY)3859 protected abstract int clipVertically(int positionY); setUp()3860 protected void setUp() { 3861 } 3862 PinnedPopupWindow()3863 public PinnedPopupWindow() { 3864 // Due to calling subclass methods in base constructor, subclass constructor is not 3865 // called before subclass methods, e.g. createPopupWindow or initContentView. To give 3866 // a chance to initialize subclasses, call setUp() method here. 3867 // TODO: It is good to extract non trivial initialization code from constructor. 3868 setUp(); 3869 3870 createPopupWindow(); 3871 3872 mPopupWindow.setWindowLayoutType( 3873 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 3874 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 3875 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 3876 3877 initContentView(); 3878 3879 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 3880 ViewGroup.LayoutParams.WRAP_CONTENT); 3881 mContentView.setLayoutParams(wrapContent); 3882 3883 mPopupWindow.setContentView(mContentView); 3884 } 3885 show()3886 public void show() { 3887 getPositionListener().addSubscriber(this, false /* offset is fixed */); 3888 3889 computeLocalPosition(); 3890 3891 final PositionListener positionListener = getPositionListener(); 3892 updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); 3893 } 3894 measureContent()3895 protected void measureContent() { 3896 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 3897 mContentView.measure( 3898 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, 3899 View.MeasureSpec.AT_MOST), 3900 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, 3901 View.MeasureSpec.AT_MOST)); 3902 } 3903 3904 /* The popup window will be horizontally centered on the getTextOffset() and vertically 3905 * positioned according to viewportToContentHorizontalOffset. 3906 * 3907 * This method assumes that mContentView has properly been measured from its content. */ computeLocalPosition()3908 private void computeLocalPosition() { 3909 measureContent(); 3910 final int width = mContentView.getMeasuredWidth(); 3911 final int offset = getTextOffset(); 3912 final int transformedOffset = mTextView.originalToTransformed(offset, 3913 OffsetMapping.MAP_STRATEGY_CURSOR); 3914 final Layout layout = mTextView.getLayout(); 3915 3916 mPositionX = (int) (layout.getPrimaryHorizontal(transformedOffset) - width / 2.0f); 3917 mPositionX += mTextView.viewportToContentHorizontalOffset(); 3918 3919 final int line = layout.getLineForOffset(transformedOffset); 3920 mPositionY = getVerticalLocalPosition(line); 3921 mPositionY += mTextView.viewportToContentVerticalOffset(); 3922 } 3923 updatePosition(int parentPositionX, int parentPositionY)3924 private void updatePosition(int parentPositionX, int parentPositionY) { 3925 int positionX = parentPositionX + mPositionX; 3926 int positionY = parentPositionY + mPositionY; 3927 3928 positionY = clipVertically(positionY); 3929 3930 // Horizontal clipping 3931 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 3932 final int width = mContentView.getMeasuredWidth(); 3933 positionX = Math.min( 3934 displayMetrics.widthPixels - width + mClippingLimitRight, positionX); 3935 positionX = Math.max(-mClippingLimitLeft, positionX); 3936 3937 if (isShowing()) { 3938 mPopupWindow.update(positionX, positionY, -1, -1); 3939 } else { 3940 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, 3941 positionX, positionY); 3942 } 3943 } 3944 hide()3945 public void hide() { 3946 if (!isShowing()) { 3947 return; 3948 } 3949 mPopupWindow.dismiss(); 3950 getPositionListener().removeSubscriber(this); 3951 } 3952 3953 @Override updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)3954 public void updatePosition(int parentPositionX, int parentPositionY, 3955 boolean parentPositionChanged, boolean parentScrolled) { 3956 // Either parentPositionChanged or parentScrolled is true, check if still visible 3957 if (isShowing() && isOffsetVisible(getTextOffset())) { 3958 if (parentScrolled) computeLocalPosition(); 3959 updatePosition(parentPositionX, parentPositionY); 3960 } else { 3961 hide(); 3962 } 3963 } 3964 isShowing()3965 public boolean isShowing() { 3966 return mPopupWindow.isShowing(); 3967 } 3968 } 3969 3970 private static final class SuggestionInfo { 3971 // Range of actual suggestion within mText 3972 int mSuggestionStart, mSuggestionEnd; 3973 3974 // The SuggestionSpan that this TextView represents 3975 final SuggestionSpanInfo mSuggestionSpanInfo = new SuggestionSpanInfo(); 3976 3977 // The index of this suggestion inside suggestionSpan 3978 int mSuggestionIndex; 3979 3980 final SpannableStringBuilder mText = new SpannableStringBuilder(); 3981 clear()3982 void clear() { 3983 mSuggestionSpanInfo.clear(); 3984 mText.clear(); 3985 } 3986 3987 // Utility method to set attributes about a SuggestionSpan. setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd)3988 void setSpanInfo(SuggestionSpan span, int spanStart, int spanEnd) { 3989 mSuggestionSpanInfo.mSuggestionSpan = span; 3990 mSuggestionSpanInfo.mSpanStart = spanStart; 3991 mSuggestionSpanInfo.mSpanEnd = spanEnd; 3992 } 3993 } 3994 3995 private static final class SuggestionSpanInfo { 3996 // The SuggestionSpan; 3997 @Nullable 3998 SuggestionSpan mSuggestionSpan; 3999 4000 // The SuggestionSpan start position 4001 int mSpanStart; 4002 4003 // The SuggestionSpan end position 4004 int mSpanEnd; 4005 clear()4006 void clear() { 4007 mSuggestionSpan = null; 4008 } 4009 } 4010 4011 private class SuggestionHelper { 4012 private final Comparator<SuggestionSpan> mSuggestionSpanComparator = 4013 new SuggestionSpanComparator(); 4014 private final HashMap<SuggestionSpan, Integer> mSpansLengths = 4015 new HashMap<SuggestionSpan, Integer>(); 4016 4017 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { compare(SuggestionSpan span1, SuggestionSpan span2)4018 public int compare(SuggestionSpan span1, SuggestionSpan span2) { 4019 final int flag1 = span1.getFlags(); 4020 final int flag2 = span2.getFlags(); 4021 if (flag1 != flag2) { 4022 // Compare so that the order will be: easy -> misspelled -> grammarError 4023 int easy = compareFlag(SuggestionSpan.FLAG_EASY_CORRECT, flag1, flag2); 4024 if (easy != 0) return easy; 4025 int misspelled = compareFlag(SuggestionSpan.FLAG_MISSPELLED, flag1, flag2); 4026 if (misspelled != 0) return misspelled; 4027 int grammarError = compareFlag(SuggestionSpan.FLAG_GRAMMAR_ERROR, flag1, flag2); 4028 if (grammarError != 0) return grammarError; 4029 } 4030 4031 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); 4032 } 4033 4034 /* 4035 * Returns -1 if flags1 has flagToCompare but flags2 does not. 4036 * Returns 1 if flags2 has flagToCompare but flags1 does not. 4037 * Otherwise, returns 0. 4038 */ compareFlag(int flagToCompare, int flags1, int flags2)4039 private int compareFlag(int flagToCompare, int flags1, int flags2) { 4040 boolean hasFlag1 = (flags1 & flagToCompare) != 0; 4041 boolean hasFlag2 = (flags2 & flagToCompare) != 0; 4042 if (hasFlag1 == hasFlag2) return 0; 4043 return hasFlag1 ? -1 : 1; 4044 } 4045 } 4046 4047 /** 4048 * Returns the suggestion spans that cover the current cursor position. The suggestion 4049 * spans are sorted according to the length of text that they are attached to. 4050 */ getSortedSuggestionSpans()4051 private SuggestionSpan[] getSortedSuggestionSpans() { 4052 int pos = mTextView.getSelectionStart(); 4053 Spannable spannable = (Spannable) mTextView.getText(); 4054 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); 4055 4056 mSpansLengths.clear(); 4057 for (SuggestionSpan suggestionSpan : suggestionSpans) { 4058 int start = spannable.getSpanStart(suggestionSpan); 4059 int end = spannable.getSpanEnd(suggestionSpan); 4060 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); 4061 } 4062 4063 // The suggestions are sorted according to their types (easy correction first, 4064 // misspelled second, then grammar error) and to the length of the text that they cover 4065 // (shorter first). 4066 Arrays.sort(suggestionSpans, mSuggestionSpanComparator); 4067 mSpansLengths.clear(); 4068 4069 return suggestionSpans; 4070 } 4071 4072 /** 4073 * Gets the SuggestionInfo list that contains suggestion information at the current cursor 4074 * position. 4075 * 4076 * @param suggestionInfos SuggestionInfo array the results will be set. 4077 * @param misspelledSpanInfo a struct the misspelled SuggestionSpan info will be set. 4078 * @return the number of suggestions actually fetched. 4079 */ getSuggestionInfo(SuggestionInfo[] suggestionInfos, @Nullable SuggestionSpanInfo misspelledSpanInfo)4080 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos, 4081 @Nullable SuggestionSpanInfo misspelledSpanInfo) { 4082 final Spannable spannable = (Spannable) mTextView.getText(); 4083 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans(); 4084 final int nbSpans = suggestionSpans.length; 4085 if (nbSpans == 0) return 0; 4086 4087 int numberOfSuggestions = 0; 4088 for (final SuggestionSpan suggestionSpan : suggestionSpans) { 4089 final int spanStart = spannable.getSpanStart(suggestionSpan); 4090 final int spanEnd = spannable.getSpanEnd(suggestionSpan); 4091 4092 if (misspelledSpanInfo != null 4093 && (suggestionSpan.getFlags() & FLAG_MISSPELLED_OR_GRAMMAR_ERROR) != 0) { 4094 misspelledSpanInfo.mSuggestionSpan = suggestionSpan; 4095 misspelledSpanInfo.mSpanStart = spanStart; 4096 misspelledSpanInfo.mSpanEnd = spanEnd; 4097 } 4098 4099 final String[] suggestions = suggestionSpan.getSuggestions(); 4100 final int nbSuggestions = suggestions.length; 4101 suggestionLoop: 4102 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { 4103 final String suggestion = suggestions[suggestionIndex]; 4104 for (int i = 0; i < numberOfSuggestions; i++) { 4105 final SuggestionInfo otherSuggestionInfo = suggestionInfos[i]; 4106 if (otherSuggestionInfo.mText.toString().equals(suggestion)) { 4107 final int otherSpanStart = 4108 otherSuggestionInfo.mSuggestionSpanInfo.mSpanStart; 4109 final int otherSpanEnd = 4110 otherSuggestionInfo.mSuggestionSpanInfo.mSpanEnd; 4111 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { 4112 continue suggestionLoop; 4113 } 4114 } 4115 } 4116 4117 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions]; 4118 suggestionInfo.setSpanInfo(suggestionSpan, spanStart, spanEnd); 4119 suggestionInfo.mSuggestionIndex = suggestionIndex; 4120 suggestionInfo.mSuggestionStart = 0; 4121 suggestionInfo.mSuggestionEnd = suggestion.length(); 4122 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion); 4123 numberOfSuggestions++; 4124 if (numberOfSuggestions >= suggestionInfos.length) { 4125 return numberOfSuggestions; 4126 } 4127 } 4128 } 4129 return numberOfSuggestions; 4130 } 4131 } 4132 4133 private final class SuggestionsPopupWindow extends PinnedPopupWindow 4134 implements OnItemClickListener { 4135 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; 4136 4137 // Key of intent extras for inserting new word into user dictionary. 4138 private static final String USER_DICTIONARY_EXTRA_WORD = "word"; 4139 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale"; 4140 4141 private SuggestionInfo[] mSuggestionInfos; 4142 private int mNumberOfSuggestions; 4143 private boolean mCursorWasVisibleBeforeSuggestions; 4144 private boolean mIsShowingUp = false; 4145 private SuggestionAdapter mSuggestionsAdapter; 4146 private TextAppearanceSpan mHighlightSpan; // TODO: Make mHighlightSpan final. 4147 private TextView mAddToDictionaryButton; 4148 private TextView mDeleteButton; 4149 private ListView mSuggestionListView; 4150 private final SuggestionSpanInfo mMisspelledSpanInfo = new SuggestionSpanInfo(); 4151 private int mContainerMarginWidth; 4152 private int mContainerMarginTop; 4153 private LinearLayout mContainerView; 4154 private Context mContext; // TODO: Make mContext final. 4155 4156 private class CustomPopupWindow extends PopupWindow { 4157 4158 @Override dismiss()4159 public void dismiss() { 4160 if (!isShowing()) { 4161 return; 4162 } 4163 super.dismiss(); 4164 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); 4165 4166 // Safe cast since show() checks that mTextView.getText() is an Editable 4167 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan); 4168 4169 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions); 4170 if (hasInsertionController() && !extractedTextModeWillBeStarted()) { 4171 getInsertionController().show(); 4172 } 4173 } 4174 } 4175 SuggestionsPopupWindow()4176 public SuggestionsPopupWindow() { 4177 mCursorWasVisibleBeforeSuggestions = mTextView.isCursorVisibleFromAttr(); 4178 } 4179 4180 @Override setUp()4181 protected void setUp() { 4182 mContext = applyDefaultTheme(mTextView.getContext()); 4183 mHighlightSpan = new TextAppearanceSpan(mContext, 4184 mTextView.mTextEditSuggestionHighlightStyle); 4185 } 4186 applyDefaultTheme(Context originalContext)4187 private Context applyDefaultTheme(Context originalContext) { 4188 TypedArray a = originalContext.obtainStyledAttributes( 4189 new int[]{com.android.internal.R.attr.isLightTheme}); 4190 boolean isLightTheme = a.getBoolean(0, true); 4191 int themeId = isLightTheme ? R.style.ThemeOverlay_Material_Light 4192 : R.style.ThemeOverlay_Material_Dark; 4193 a.recycle(); 4194 return new ContextThemeWrapper(originalContext, themeId); 4195 } 4196 4197 @Override createPopupWindow()4198 protected void createPopupWindow() { 4199 mPopupWindow = new CustomPopupWindow(); 4200 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 4201 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 4202 mPopupWindow.setFocusable(true); 4203 mPopupWindow.setClippingEnabled(false); 4204 } 4205 4206 @Override initContentView()4207 protected void initContentView() { 4208 final LayoutInflater inflater = (LayoutInflater) mContext.getSystemService( 4209 Context.LAYOUT_INFLATER_SERVICE); 4210 mContentView = (ViewGroup) inflater.inflate( 4211 mTextView.mTextEditSuggestionContainerLayout, null); 4212 4213 mContainerView = (LinearLayout) mContentView.findViewById( 4214 com.android.internal.R.id.suggestionWindowContainer); 4215 ViewGroup.MarginLayoutParams lp = 4216 (ViewGroup.MarginLayoutParams) mContainerView.getLayoutParams(); 4217 mContainerMarginWidth = lp.leftMargin + lp.rightMargin; 4218 mContainerMarginTop = lp.topMargin; 4219 mClippingLimitLeft = lp.leftMargin; 4220 mClippingLimitRight = lp.rightMargin; 4221 4222 mSuggestionListView = (ListView) mContentView.findViewById( 4223 com.android.internal.R.id.suggestionContainer); 4224 4225 mSuggestionsAdapter = new SuggestionAdapter(); 4226 mSuggestionListView.setAdapter(mSuggestionsAdapter); 4227 mSuggestionListView.setOnItemClickListener(this); 4228 4229 // Inflate the suggestion items once and for all. 4230 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS]; 4231 for (int i = 0; i < mSuggestionInfos.length; i++) { 4232 mSuggestionInfos[i] = new SuggestionInfo(); 4233 } 4234 4235 mAddToDictionaryButton = (TextView) mContentView.findViewById( 4236 com.android.internal.R.id.addToDictionaryButton); 4237 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() { 4238 public void onClick(View v) { 4239 final SuggestionSpan misspelledSpan = 4240 findEquivalentSuggestionSpan(mMisspelledSpanInfo); 4241 if (misspelledSpan == null) { 4242 // Span has been removed. 4243 return; 4244 } 4245 final Editable editable = (Editable) mTextView.getText(); 4246 final int spanStart = editable.getSpanStart(misspelledSpan); 4247 final int spanEnd = editable.getSpanEnd(misspelledSpan); 4248 if (spanStart < 0 || spanEnd <= spanStart) { 4249 return; 4250 } 4251 final String originalText = TextUtils.substring(editable, spanStart, spanEnd); 4252 4253 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); 4254 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText); 4255 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE, 4256 mTextView.getTextServicesLocale().toString()); 4257 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); 4258 mTextView.startActivityAsTextOperationUserIfNecessary(intent); 4259 // There is no way to know if the word was indeed added. Re-check. 4260 // TODO The ExtractEditText should remove the span in the original text instead 4261 editable.removeSpan(mMisspelledSpanInfo.mSuggestionSpan); 4262 Selection.setSelection(editable, spanEnd); 4263 updateSpellCheckSpans(spanStart, spanEnd, false); 4264 hideWithCleanUp(); 4265 } 4266 }); 4267 4268 mDeleteButton = (TextView) mContentView.findViewById( 4269 com.android.internal.R.id.deleteButton); 4270 mDeleteButton.setOnClickListener(new View.OnClickListener() { 4271 public void onClick(View v) { 4272 final Editable editable = (Editable) mTextView.getText(); 4273 4274 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); 4275 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); 4276 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { 4277 // Do not leave two adjacent spaces after deletion, or one at beginning of 4278 // text 4279 if (spanUnionEnd < editable.length() 4280 && Character.isSpaceChar(editable.charAt(spanUnionEnd)) 4281 && (spanUnionStart == 0 4282 || Character.isSpaceChar( 4283 editable.charAt(spanUnionStart - 1)))) { 4284 spanUnionEnd = spanUnionEnd + 1; 4285 } 4286 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); 4287 } 4288 hideWithCleanUp(); 4289 } 4290 }); 4291 4292 } 4293 isShowingUp()4294 public boolean isShowingUp() { 4295 return mIsShowingUp; 4296 } 4297 onParentLostFocus()4298 public void onParentLostFocus() { 4299 mIsShowingUp = false; 4300 } 4301 4302 private class SuggestionAdapter extends BaseAdapter { 4303 private LayoutInflater mInflater = (LayoutInflater) mContext.getSystemService( 4304 Context.LAYOUT_INFLATER_SERVICE); 4305 4306 @Override getCount()4307 public int getCount() { 4308 return mNumberOfSuggestions; 4309 } 4310 4311 @Override getItem(int position)4312 public Object getItem(int position) { 4313 return mSuggestionInfos[position]; 4314 } 4315 4316 @Override getItemId(int position)4317 public long getItemId(int position) { 4318 return position; 4319 } 4320 4321 @Override getView(int position, View convertView, ViewGroup parent)4322 public View getView(int position, View convertView, ViewGroup parent) { 4323 TextView textView = (TextView) convertView; 4324 4325 if (textView == null) { 4326 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout, 4327 parent, false); 4328 } 4329 4330 final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 4331 textView.setText(suggestionInfo.mText); 4332 return textView; 4333 } 4334 } 4335 4336 @Override show()4337 public void show() { 4338 if (!(mTextView.getText() instanceof Editable)) return; 4339 if (extractedTextModeWillBeStarted()) { 4340 return; 4341 } 4342 4343 if (updateSuggestions()) { 4344 mCursorWasVisibleBeforeSuggestions = mTextView.isCursorVisibleFromAttr(); 4345 mTextView.setCursorVisible(false); 4346 mIsShowingUp = true; 4347 super.show(); 4348 } 4349 4350 mSuggestionListView.setVisibility(mNumberOfSuggestions == 0 ? View.GONE : View.VISIBLE); 4351 } 4352 4353 @Override measureContent()4354 protected void measureContent() { 4355 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 4356 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( 4357 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); 4358 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( 4359 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); 4360 4361 int width = 0; 4362 View view = null; 4363 for (int i = 0; i < mNumberOfSuggestions; i++) { 4364 view = mSuggestionsAdapter.getView(i, view, mContentView); 4365 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; 4366 view.measure(horizontalMeasure, verticalMeasure); 4367 width = Math.max(width, view.getMeasuredWidth()); 4368 } 4369 4370 if (mAddToDictionaryButton.getVisibility() != View.GONE) { 4371 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure); 4372 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth()); 4373 } 4374 4375 mDeleteButton.measure(horizontalMeasure, verticalMeasure); 4376 width = Math.max(width, mDeleteButton.getMeasuredWidth()); 4377 4378 width += mContainerView.getPaddingLeft() + mContainerView.getPaddingRight() 4379 + mContainerMarginWidth; 4380 4381 // Enforce the width based on actual text widths 4382 mContentView.measure( 4383 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), 4384 verticalMeasure); 4385 4386 Drawable popupBackground = mPopupWindow.getBackground(); 4387 if (popupBackground != null) { 4388 if (mTempRect == null) mTempRect = new Rect(); 4389 popupBackground.getPadding(mTempRect); 4390 width += mTempRect.left + mTempRect.right; 4391 } 4392 mPopupWindow.setWidth(width); 4393 } 4394 4395 @Override getTextOffset()4396 protected int getTextOffset() { 4397 return (mTextView.getSelectionStart() + mTextView.getSelectionStart()) / 2; 4398 } 4399 4400 @Override getVerticalLocalPosition(int line)4401 protected int getVerticalLocalPosition(int line) { 4402 final Layout layout = mTextView.getLayout(); 4403 return layout.getLineBottom(line, /* includeLineSpacing= */ false) 4404 - mContainerMarginTop; 4405 } 4406 4407 @Override clipVertically(int positionY)4408 protected int clipVertically(int positionY) { 4409 final int height = mContentView.getMeasuredHeight(); 4410 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 4411 return Math.min(positionY, displayMetrics.heightPixels - height); 4412 } 4413 hideWithCleanUp()4414 private void hideWithCleanUp() { 4415 for (final SuggestionInfo info : mSuggestionInfos) { 4416 info.clear(); 4417 } 4418 mMisspelledSpanInfo.clear(); 4419 hide(); 4420 } 4421 updateSuggestions()4422 private boolean updateSuggestions() { 4423 Spannable spannable = (Spannable) mTextView.getText(); 4424 mNumberOfSuggestions = 4425 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos, mMisspelledSpanInfo); 4426 if (mNumberOfSuggestions == 0 && mMisspelledSpanInfo.mSuggestionSpan == null) { 4427 return false; 4428 } 4429 4430 int spanUnionStart = mTextView.getText().length(); 4431 int spanUnionEnd = 0; 4432 4433 for (int i = 0; i < mNumberOfSuggestions; i++) { 4434 final SuggestionSpanInfo spanInfo = mSuggestionInfos[i].mSuggestionSpanInfo; 4435 spanUnionStart = Math.min(spanUnionStart, spanInfo.mSpanStart); 4436 spanUnionEnd = Math.max(spanUnionEnd, spanInfo.mSpanEnd); 4437 } 4438 if (mMisspelledSpanInfo.mSuggestionSpan != null) { 4439 spanUnionStart = Math.min(spanUnionStart, mMisspelledSpanInfo.mSpanStart); 4440 spanUnionEnd = Math.max(spanUnionEnd, mMisspelledSpanInfo.mSpanEnd); 4441 } 4442 4443 for (int i = 0; i < mNumberOfSuggestions; i++) { 4444 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); 4445 } 4446 4447 // Make "Add to dictionary" item visible if there is a span with the misspelled flag 4448 int addToDictionaryButtonVisibility = View.GONE; 4449 if (mMisspelledSpanInfo.mSuggestionSpan != null) { 4450 if (mMisspelledSpanInfo.mSpanStart >= 0 4451 && mMisspelledSpanInfo.mSpanEnd > mMisspelledSpanInfo.mSpanStart) { 4452 addToDictionaryButtonVisibility = View.VISIBLE; 4453 } 4454 } 4455 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility); 4456 4457 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); 4458 final int underlineColor; 4459 if (mNumberOfSuggestions != 0) { 4460 underlineColor = 4461 mSuggestionInfos[0].mSuggestionSpanInfo.mSuggestionSpan.getUnderlineColor(); 4462 } else { 4463 underlineColor = mMisspelledSpanInfo.mSuggestionSpan.getUnderlineColor(); 4464 } 4465 4466 if (underlineColor == 0) { 4467 // Fallback on the default highlight color when the first span does not provide one 4468 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor); 4469 } else { 4470 final float BACKGROUND_TRANSPARENCY = 0.4f; 4471 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); 4472 mSuggestionRangeSpan.setBackgroundColor( 4473 (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); 4474 } 4475 boolean sendAccessibilityEvent = mTextView.isVisibleToAccessibility(); 4476 CharSequence beforeText = sendAccessibilityEvent 4477 ? new SpannedString(spannable, true) : null; 4478 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, 4479 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 4480 if (sendAccessibilityEvent) { 4481 mTextView.sendAccessibilityEventTypeViewTextChanged( 4482 beforeText, spanUnionStart, spanUnionEnd); 4483 } 4484 4485 mSuggestionsAdapter.notifyDataSetChanged(); 4486 return true; 4487 } 4488 highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, int unionEnd)4489 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, 4490 int unionEnd) { 4491 final Spannable text = (Spannable) mTextView.getText(); 4492 final int spanStart = suggestionInfo.mSuggestionSpanInfo.mSpanStart; 4493 final int spanEnd = suggestionInfo.mSuggestionSpanInfo.mSpanEnd; 4494 4495 // Adjust the start/end of the suggestion span 4496 suggestionInfo.mSuggestionStart = spanStart - unionStart; 4497 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart 4498 + suggestionInfo.mText.length(); 4499 4500 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(), 4501 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 4502 4503 // Add the text before and after the span. 4504 final String textAsString = text.toString(); 4505 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart)); 4506 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd)); 4507 } 4508 4509 @Override onItemClick(AdapterView<?> parent, View view, int position, long id)4510 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 4511 SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 4512 replaceWithSuggestion(suggestionInfo); 4513 hideWithCleanUp(); 4514 } 4515 } 4516 4517 /** 4518 * Helper class for UI component (e.g. ActionMode and ContextMenu) with TextClassification. 4519 * @hide 4520 */ 4521 @VisibleForTesting 4522 public class AssistantCallbackHelper { 4523 private final Map<MenuItem, OnClickListener> mAssistClickHandlers = new HashMap<>(); 4524 @Nullable private TextClassification mPrevTextClassification; 4525 @NonNull private final SelectionActionModeHelper mHelper; 4526 AssistantCallbackHelper(SelectionActionModeHelper helper)4527 public AssistantCallbackHelper(SelectionActionModeHelper helper) { 4528 mHelper = helper; 4529 } 4530 4531 /** 4532 * Clears callback handlers. 4533 */ clearCallbackHandlers()4534 public void clearCallbackHandlers() { 4535 mAssistClickHandlers.clear(); 4536 } 4537 4538 /** 4539 * Get on click listener assisiated with the MenuItem. 4540 */ getOnClickListener(MenuItem key)4541 public OnClickListener getOnClickListener(MenuItem key) { 4542 return mAssistClickHandlers.get(key); 4543 } 4544 4545 /** 4546 * Update menu items. 4547 * 4548 * Existing assist menu will be cleared and latest assist menu will be added. 4549 */ updateAssistMenuItems(Menu menu, MenuItem.OnMenuItemClickListener listener)4550 public void updateAssistMenuItems(Menu menu, MenuItem.OnMenuItemClickListener listener) { 4551 final TextClassification textClassification = mHelper.getTextClassification(); 4552 if (mPrevTextClassification == textClassification) { 4553 // Already handled. 4554 return; 4555 } 4556 clearAssistMenuItems(menu); 4557 if (textClassification == null) { 4558 return; 4559 } 4560 if (!shouldEnableAssistMenuItems()) { 4561 return; 4562 } 4563 if (!textClassification.getActions().isEmpty()) { 4564 // Primary assist action (Always shown). 4565 final MenuItem item = addAssistMenuItem(menu, 4566 textClassification.getActions().get(0), TextView.ID_ASSIST, 4567 ACTION_MODE_MENU_ITEM_ORDER_ASSIST, MenuItem.SHOW_AS_ACTION_ALWAYS, 4568 listener); 4569 item.setIntent(textClassification.getIntent()); 4570 } else if (hasLegacyAssistItem(textClassification)) { 4571 // Legacy primary assist action (Always shown). 4572 final MenuItem item = menu.add(TextView.ID_ASSIST, TextView.ID_ASSIST, 4573 ACTION_MODE_MENU_ITEM_ORDER_ASSIST, 4574 textClassification.getLabel()) 4575 .setIcon(textClassification.getIcon()) 4576 .setIntent(textClassification.getIntent()); 4577 item.setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 4578 mAssistClickHandlers.put(item, TextClassification.createIntentOnClickListener( 4579 TextClassification.createPendingIntent(mTextView.getContext(), 4580 textClassification.getIntent(), 4581 createAssistMenuItemPendingIntentRequestCode()))); 4582 } 4583 final int count = textClassification.getActions().size(); 4584 for (int i = 1; i < count; i++) { 4585 // Secondary assist action (Never shown). 4586 addAssistMenuItem(menu, textClassification.getActions().get(i), Menu.NONE, 4587 ACTION_MODE_MENU_ITEM_ORDER_SECONDARY_ASSIST_ACTIONS_START + i - 1, 4588 MenuItem.SHOW_AS_ACTION_NEVER, listener); 4589 } 4590 mPrevTextClassification = textClassification; 4591 } 4592 addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order, int showAsAction, MenuItem.OnMenuItemClickListener listener)4593 private MenuItem addAssistMenuItem(Menu menu, RemoteAction action, int itemId, int order, 4594 int showAsAction, MenuItem.OnMenuItemClickListener listener) { 4595 final MenuItem item = menu.add(TextView.ID_ASSIST, itemId, order, action.getTitle()) 4596 .setContentDescription(action.getContentDescription()); 4597 if (action.shouldShowIcon()) { 4598 item.setIcon(action.getIcon().loadDrawable(mTextView.getContext())); 4599 } 4600 item.setShowAsAction(showAsAction); 4601 mAssistClickHandlers.put(item, 4602 TextClassification.createIntentOnClickListener(action.getActionIntent())); 4603 mA11ySmartActions.addAction(action); 4604 if (listener != null) { 4605 item.setOnMenuItemClickListener(listener); 4606 } 4607 return item; 4608 } 4609 clearAssistMenuItems(Menu menu)4610 private void clearAssistMenuItems(Menu menu) { 4611 int i = 0; 4612 while (i < menu.size()) { 4613 final MenuItem menuItem = menu.getItem(i); 4614 if (menuItem.getGroupId() == TextView.ID_ASSIST) { 4615 menu.removeItem(menuItem.getItemId()); 4616 continue; 4617 } 4618 i++; 4619 } 4620 mA11ySmartActions.reset(); 4621 } 4622 hasLegacyAssistItem(TextClassification classification)4623 private boolean hasLegacyAssistItem(TextClassification classification) { 4624 // Check whether we have the UI data and action. 4625 return (classification.getIcon() != null || !TextUtils.isEmpty( 4626 classification.getLabel())) && (classification.getIntent() != null 4627 || classification.getOnClickListener() != null); 4628 } 4629 shouldEnableAssistMenuItems()4630 private boolean shouldEnableAssistMenuItems() { 4631 return mTextView.isDeviceProvisioned() 4632 && TextClassificationManager.getSettings(mTextView.getContext()) 4633 .isSmartTextShareEnabled(); 4634 } 4635 createAssistMenuItemPendingIntentRequestCode()4636 private int createAssistMenuItemPendingIntentRequestCode() { 4637 return mTextView.hasSelection() 4638 ? mTextView.getText().subSequence( 4639 mTextView.getSelectionStart(), mTextView.getSelectionEnd()) 4640 .hashCode() 4641 : 0; 4642 } 4643 4644 /** 4645 * Called when the assist menu on ActionMode or ContextMenu is called. 4646 */ onAssistMenuItemClicked(MenuItem assistMenuItem)4647 public boolean onAssistMenuItemClicked(MenuItem assistMenuItem) { 4648 Preconditions.checkArgument(assistMenuItem.getGroupId() == TextView.ID_ASSIST); 4649 4650 final TextClassification textClassification = 4651 getSelectionActionModeHelper().getTextClassification(); 4652 if (!shouldEnableAssistMenuItems() || textClassification == null) { 4653 // No textClassification result to handle the click. Eat the click. 4654 return true; 4655 } 4656 4657 OnClickListener onClickListener = getOnClickListener(assistMenuItem); 4658 if (onClickListener == null) { 4659 final Intent intent = assistMenuItem.getIntent(); 4660 if (intent != null) { 4661 onClickListener = TextClassification.createIntentOnClickListener( 4662 TextClassification.createPendingIntent( 4663 mTextView.getContext(), intent, 4664 createAssistMenuItemPendingIntentRequestCode())); 4665 } 4666 } 4667 if (onClickListener != null) { 4668 onClickListener.onClick(mTextView); 4669 stopTextActionMode(); 4670 } 4671 // We tried our best. 4672 return true; 4673 } 4674 } 4675 4676 /** 4677 * An ActionMode Callback class that is used to provide actions while in text insertion or 4678 * selection mode. 4679 * 4680 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace 4681 * actions, depending on which of these this TextView supports and the current selection. 4682 */ 4683 private class TextActionModeCallback extends ActionMode.Callback2 { 4684 private final Path mSelectionPath = new Path(); 4685 private final RectF mSelectionBounds = new RectF(); 4686 private final boolean mHasSelection; 4687 private final int mHandleHeight; 4688 private final AssistantCallbackHelper mHelper = new AssistantCallbackHelper( 4689 getSelectionActionModeHelper()); 4690 TextActionModeCallback(@extActionMode int mode)4691 TextActionModeCallback(@TextActionMode int mode) { 4692 mHasSelection = mode == TextActionMode.SELECTION 4693 || (mTextIsSelectable && mode == TextActionMode.TEXT_LINK); 4694 if (mHasSelection) { 4695 SelectionModifierCursorController selectionController = getSelectionController(); 4696 if (selectionController.mStartHandle == null) { 4697 // As these are for initializing selectionController, hide() must be called. 4698 loadHandleDrawables(false /* overwrite */); 4699 selectionController.initHandles(); 4700 selectionController.hide(); 4701 } 4702 mHandleHeight = Math.max( 4703 mSelectHandleLeft.getMinimumHeight(), 4704 mSelectHandleRight.getMinimumHeight()); 4705 } else { 4706 InsertionPointCursorController insertionController = getInsertionController(); 4707 if (insertionController != null) { 4708 insertionController.getHandle(); 4709 mHandleHeight = mSelectHandleCenter.getMinimumHeight(); 4710 } else { 4711 mHandleHeight = 0; 4712 } 4713 } 4714 } 4715 4716 @Override onCreateActionMode(ActionMode mode, Menu menu)4717 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 4718 mHelper.clearCallbackHandlers(); 4719 4720 mode.setTitle(null); 4721 mode.setSubtitle(null); 4722 mode.setTitleOptionalHint(true); 4723 populateMenuWithItems(menu); 4724 4725 Callback customCallback = getCustomCallback(); 4726 if (customCallback != null) { 4727 if (!customCallback.onCreateActionMode(mode, menu)) { 4728 // The custom mode can choose to cancel the action mode, dismiss selection. 4729 Selection.setSelection((Spannable) mTextView.getText(), 4730 mTextView.getSelectionEnd()); 4731 return false; 4732 } 4733 } 4734 4735 if (mTextView.canProcessText()) { 4736 mProcessTextIntentActionsHandler.onInitializeMenu(menu); 4737 } 4738 4739 if (mHasSelection && !mTextView.hasTransientState()) { 4740 mTextView.setHasTransientState(true); 4741 } 4742 return true; 4743 } 4744 getCustomCallback()4745 private Callback getCustomCallback() { 4746 return mHasSelection 4747 ? mCustomSelectionActionModeCallback 4748 : mCustomInsertionActionModeCallback; 4749 } 4750 populateMenuWithItems(Menu menu)4751 private void populateMenuWithItems(Menu menu) { 4752 if (mTextView.canCut()) { 4753 menu.add(Menu.NONE, TextView.ID_CUT, ACTION_MODE_MENU_ITEM_ORDER_CUT, 4754 com.android.internal.R.string.cut) 4755 .setAlphabeticShortcut('x') 4756 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 4757 } 4758 4759 if (mTextView.canCopy()) { 4760 menu.add(Menu.NONE, TextView.ID_COPY, ACTION_MODE_MENU_ITEM_ORDER_COPY, 4761 com.android.internal.R.string.copy) 4762 .setAlphabeticShortcut('c') 4763 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 4764 } 4765 4766 if (mTextView.canPaste()) { 4767 menu.add(Menu.NONE, TextView.ID_PASTE, ACTION_MODE_MENU_ITEM_ORDER_PASTE, 4768 com.android.internal.R.string.paste) 4769 .setAlphabeticShortcut('v') 4770 .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 4771 } 4772 4773 if (mTextView.canShare()) { 4774 menu.add(Menu.NONE, TextView.ID_SHARE, ACTION_MODE_MENU_ITEM_ORDER_SHARE, 4775 com.android.internal.R.string.share) 4776 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 4777 } 4778 4779 if (mTextView.canRequestAutofill()) { 4780 final String selected = mTextView.getSelectedText(); 4781 if (selected == null || selected.isEmpty()) { 4782 menu.add(Menu.NONE, TextView.ID_AUTOFILL, ACTION_MODE_MENU_ITEM_ORDER_AUTOFILL, 4783 com.android.internal.R.string.autofill) 4784 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 4785 } 4786 } 4787 4788 if (mTextView.canPasteAsPlainText()) { 4789 menu.add( 4790 Menu.NONE, 4791 TextView.ID_PASTE_AS_PLAIN_TEXT, 4792 ACTION_MODE_MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT, 4793 com.android.internal.R.string.paste_as_plain_text) 4794 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 4795 } 4796 4797 updateSelectAllItem(menu); 4798 updateReplaceItem(menu); 4799 mHelper.updateAssistMenuItems(menu, null); 4800 } 4801 4802 @Override onPrepareActionMode(ActionMode mode, Menu menu)4803 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 4804 updateSelectAllItem(menu); 4805 updateReplaceItem(menu); 4806 mHelper.updateAssistMenuItems(menu, null); 4807 4808 Callback customCallback = getCustomCallback(); 4809 if (customCallback != null) { 4810 return customCallback.onPrepareActionMode(mode, menu); 4811 } 4812 return true; 4813 } 4814 updateSelectAllItem(Menu menu)4815 private void updateSelectAllItem(Menu menu) { 4816 boolean canSelectAll = mTextView.canSelectAllText(); 4817 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null; 4818 if (canSelectAll && !selectAllItemExists) { 4819 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, ACTION_MODE_MENU_ITEM_ORDER_SELECT_ALL, 4820 com.android.internal.R.string.selectAll) 4821 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 4822 } else if (!canSelectAll && selectAllItemExists) { 4823 menu.removeItem(TextView.ID_SELECT_ALL); 4824 } 4825 } 4826 updateReplaceItem(Menu menu)4827 private void updateReplaceItem(Menu menu) { 4828 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions(); 4829 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null; 4830 if (canReplace && !replaceItemExists) { 4831 menu.add(Menu.NONE, TextView.ID_REPLACE, ACTION_MODE_MENU_ITEM_ORDER_REPLACE, 4832 com.android.internal.R.string.replace) 4833 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 4834 } else if (!canReplace && replaceItemExists) { 4835 menu.removeItem(TextView.ID_REPLACE); 4836 } 4837 } 4838 4839 @Override onActionItemClicked(ActionMode mode, MenuItem item)4840 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 4841 getSelectionActionModeHelper() 4842 .onSelectionAction(item.getItemId(), item.getTitle().toString()); 4843 4844 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { 4845 return true; 4846 } 4847 Callback customCallback = getCustomCallback(); 4848 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) { 4849 return true; 4850 } 4851 if (item.getGroupId() == TextView.ID_ASSIST && mHelper.onAssistMenuItemClicked(item)) { 4852 return true; 4853 } 4854 return mTextView.onTextContextMenuItem(item.getItemId()); 4855 } 4856 4857 @Override onDestroyActionMode(ActionMode mode)4858 public void onDestroyActionMode(ActionMode mode) { 4859 // Clear mTextActionMode not to recursively destroy action mode by clearing selection. 4860 getSelectionActionModeHelper().onDestroyActionMode(); 4861 mTextActionMode = null; 4862 Callback customCallback = getCustomCallback(); 4863 if (customCallback != null) { 4864 customCallback.onDestroyActionMode(mode); 4865 } 4866 4867 if (!mPreserveSelection) { 4868 /* 4869 * Leave current selection when we tentatively destroy action mode for the 4870 * selection. If we're detaching from a window, we'll bring back the selection 4871 * mode when (if) we get reattached. 4872 */ 4873 Selection.setSelection((Spannable) mTextView.getText(), 4874 mTextView.getSelectionEnd()); 4875 } 4876 4877 if (mSelectionModifierCursorController != null) { 4878 mSelectionModifierCursorController.hide(); 4879 } 4880 4881 mHelper.clearCallbackHandlers(); 4882 mRequestingLinkActionMode = false; 4883 } 4884 4885 @Override onGetContentRect(ActionMode mode, View view, Rect outRect)4886 public void onGetContentRect(ActionMode mode, View view, Rect outRect) { 4887 if (!view.equals(mTextView) || mTextView.getLayout() == null) { 4888 super.onGetContentRect(mode, view, outRect); 4889 return; 4890 } 4891 final int selectionStart = mTextView.getSelectionStartTransformed(); 4892 final int selectionEnd = mTextView.getSelectionEndTransformed(); 4893 final Layout layout = mTextView.getLayout(); 4894 if (selectionStart != selectionEnd) { 4895 // We have a selection. 4896 mSelectionPath.reset(); 4897 layout.getSelectionPath(selectionStart, selectionEnd, mSelectionPath); 4898 mSelectionPath.computeBounds(mSelectionBounds, true); 4899 mSelectionBounds.bottom += mHandleHeight; 4900 } else { 4901 // We have a cursor. 4902 int line = layout.getLineForOffset(selectionStart); 4903 float primaryHorizontal = 4904 clampHorizontalPosition(null, layout.getPrimaryHorizontal(selectionEnd)); 4905 mSelectionBounds.set( 4906 primaryHorizontal, 4907 layout.getLineTop(line), 4908 primaryHorizontal, 4909 layout.getLineBottom(line) + mHandleHeight); 4910 } 4911 // Take TextView's padding and scroll into account. 4912 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset(); 4913 int textVerticalOffset = mTextView.viewportToContentVerticalOffset(); 4914 outRect.set( 4915 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset), 4916 (int) Math.floor(mSelectionBounds.top + textVerticalOffset), 4917 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset), 4918 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset)); 4919 } 4920 } 4921 4922 /** 4923 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} 4924 * while the input method is requesting the cursor/anchor position. Does nothing as long as 4925 * {@link InputMethodManager#isWatchingCursor(View)} returns false. 4926 */ 4927 private final class CursorAnchorInfoNotifier implements TextViewPositionListener { 4928 final CursorAnchorInfo.Builder mCursorAnchorInfoBuilder = new CursorAnchorInfo.Builder(); 4929 final Matrix mViewToScreenMatrix = new Matrix(); 4930 4931 @Override updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)4932 public void updatePosition(int parentPositionX, int parentPositionY, 4933 boolean parentPositionChanged, boolean parentScrolled) { 4934 final InputMethodState ims = mInputMethodState; 4935 if (ims == null || ims.mBatchEditNesting > 0) { 4936 return; 4937 } 4938 final InputMethodManager imm = getInputMethodManager(); 4939 if (null == imm) { 4940 return; 4941 } 4942 if (!imm.hasActiveInputConnection(mTextView)) { 4943 return; 4944 } 4945 // Skip if the IME has not requested the cursor/anchor position. 4946 final int knownCursorAnchorInfoModes = 4947 InputConnection.CURSOR_UPDATE_IMMEDIATE | InputConnection.CURSOR_UPDATE_MONITOR; 4948 if ((ims.mUpdateCursorAnchorInfoMode & knownCursorAnchorInfoModes) == 0) { 4949 return; 4950 } 4951 4952 final CursorAnchorInfo cursorAnchorInfo = 4953 mTextView.getCursorAnchorInfo(ims.mUpdateCursorAnchorInfoFilter, 4954 mCursorAnchorInfoBuilder, mViewToScreenMatrix); 4955 4956 if (cursorAnchorInfo != null) { 4957 imm.updateCursorAnchorInfo(mTextView, cursorAnchorInfo); 4958 4959 // Drop the immediate flag if any. 4960 mInputMethodState.mUpdateCursorAnchorInfoMode &= 4961 ~InputConnection.CURSOR_UPDATE_IMMEDIATE; 4962 } 4963 } 4964 } 4965 4966 private static class MagnifierMotionAnimator { 4967 private static final long DURATION = 100 /* miliseconds */; 4968 4969 // The magnifier being animated. 4970 private final Magnifier mMagnifier; 4971 // A value animator used to animate the magnifier. 4972 private final ValueAnimator mAnimator; 4973 4974 // Whether the magnifier is currently visible. 4975 private boolean mMagnifierIsShowing; 4976 // The coordinates of the magnifier when the currently running animation started. 4977 private float mAnimationStartX; 4978 private float mAnimationStartY; 4979 // The coordinates of the magnifier in the latest animation frame. 4980 private float mAnimationCurrentX; 4981 private float mAnimationCurrentY; 4982 // The latest coordinates the motion animator was asked to #show() the magnifier at. 4983 private float mLastX; 4984 private float mLastY; 4985 MagnifierMotionAnimator(final Magnifier magnifier)4986 private MagnifierMotionAnimator(final Magnifier magnifier) { 4987 mMagnifier = magnifier; 4988 // Prepare the animator used to run the motion animation. 4989 mAnimator = ValueAnimator.ofFloat(0, 1); 4990 mAnimator.setDuration(DURATION); 4991 mAnimator.setInterpolator(new LinearInterpolator()); 4992 mAnimator.addUpdateListener((animation) -> { 4993 // Interpolate to find the current position of the magnifier. 4994 mAnimationCurrentX = mAnimationStartX 4995 + (mLastX - mAnimationStartX) * animation.getAnimatedFraction(); 4996 mAnimationCurrentY = mAnimationStartY 4997 + (mLastY - mAnimationStartY) * animation.getAnimatedFraction(); 4998 mMagnifier.show(mAnimationCurrentX, mAnimationCurrentY); 4999 }); 5000 } 5001 5002 /** 5003 * Shows the magnifier at a new position. 5004 * If the y coordinate is different from the previous y coordinate 5005 * (probably corresponding to a line jump in the text), a short 5006 * animation is added to the jump. 5007 */ show(final float x, final float y)5008 private void show(final float x, final float y) { 5009 final boolean startNewAnimation = mMagnifierIsShowing && y != mLastY; 5010 5011 if (startNewAnimation) { 5012 if (mAnimator.isRunning()) { 5013 mAnimator.cancel(); 5014 mAnimationStartX = mAnimationCurrentX; 5015 mAnimationStartY = mAnimationCurrentY; 5016 } else { 5017 mAnimationStartX = mLastX; 5018 mAnimationStartY = mLastY; 5019 } 5020 mAnimator.start(); 5021 } else { 5022 if (!mAnimator.isRunning()) { 5023 mMagnifier.show(x, y); 5024 } 5025 } 5026 mLastX = x; 5027 mLastY = y; 5028 mMagnifierIsShowing = true; 5029 } 5030 5031 /** 5032 * Updates the content of the magnifier. 5033 */ update()5034 private void update() { 5035 mMagnifier.update(); 5036 } 5037 5038 /** 5039 * Dismisses the magnifier, or does nothing if it is already dismissed. 5040 */ dismiss()5041 private void dismiss() { 5042 mMagnifier.dismiss(); 5043 mAnimator.cancel(); 5044 mMagnifierIsShowing = false; 5045 } 5046 } 5047 5048 @VisibleForTesting 5049 public abstract class HandleView extends View implements TextViewPositionListener { 5050 protected Drawable mDrawable; 5051 protected Drawable mDrawableLtr; 5052 protected Drawable mDrawableRtl; 5053 private final PopupWindow mContainer; 5054 // Position with respect to the parent TextView 5055 private int mPositionX, mPositionY; 5056 private boolean mIsDragging; 5057 // Offset from touch position to mPosition 5058 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; 5059 protected int mHotspotX; 5060 protected int mHorizontalGravity; 5061 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up 5062 private float mTouchOffsetY; 5063 // Where the touch position should be on the handle to ensure a maximum cursor visibility. 5064 // This is the distance in pixels from the top of the handle view. 5065 private final float mIdealVerticalOffset; 5066 // Parent's (TextView) previous position in window 5067 private int mLastParentX, mLastParentY; 5068 // Parent's (TextView) previous position on screen 5069 private int mLastParentXOnScreen, mLastParentYOnScreen; 5070 // Previous text character offset 5071 protected int mPreviousOffset = -1; 5072 // Previous text character offset 5073 private boolean mPositionHasChanged = true; 5074 // Minimum touch target size for handles 5075 private int mMinSize; 5076 // Indicates the line of text that the handle is on. 5077 protected int mPrevLine = UNSET_LINE; 5078 // Indicates the line of text that the user was touching. This can differ from mPrevLine 5079 // when selecting text when the handles jump to the end / start of words which may be on 5080 // a different line. 5081 protected int mPreviousLineTouched = UNSET_LINE; 5082 // The raw x coordinate of the motion down event which started the current dragging session. 5083 // Only used and stored when magnifier is used. 5084 private float mCurrentDragInitialTouchRawX = UNSET_X_VALUE; 5085 // The scale transform applied by containers to the TextView. Only used and computed 5086 // when magnifier is used. 5087 private float mTextViewScaleX; 5088 private float mTextViewScaleY; 5089 /** 5090 * The vertical distance in pixels from finger to the cursor Y while dragging. 5091 * See {@link Editor.InsertionPointCursorController#getLineDuringDrag}. 5092 */ 5093 private final int mIdealFingerToCursorOffset; 5094 HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id)5095 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) { 5096 super(mTextView.getContext()); 5097 setId(id); 5098 mContainer = new PopupWindow(mTextView.getContext(), null, 5099 com.android.internal.R.attr.textSelectHandleWindowStyle); 5100 mContainer.setSplitTouchEnabled(true); 5101 mContainer.setClippingEnabled(false); 5102 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); 5103 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 5104 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 5105 mContainer.setContentView(this); 5106 5107 setDrawables(drawableLtr, drawableRtl); 5108 5109 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize( 5110 com.android.internal.R.dimen.text_handle_min_size); 5111 5112 final int handleHeight = getPreferredHeight(); 5113 mTouchOffsetY = -0.3f * handleHeight; 5114 final int distance = AppGlobals.getIntCoreSetting( 5115 WidgetFlags.KEY_FINGER_TO_CURSOR_DISTANCE, 5116 WidgetFlags.FINGER_TO_CURSOR_DISTANCE_DEFAULT); 5117 if (distance < 0 || distance > 100) { 5118 mIdealVerticalOffset = 0.7f * handleHeight; 5119 mIdealFingerToCursorOffset = (int)(mIdealVerticalOffset - mTouchOffsetY); 5120 } else { 5121 mIdealFingerToCursorOffset = (int) TypedValue.applyDimension( 5122 TypedValue.COMPLEX_UNIT_DIP, distance, 5123 mTextView.getContext().getResources().getDisplayMetrics()); 5124 mIdealVerticalOffset = mIdealFingerToCursorOffset + mTouchOffsetY; 5125 } 5126 } 5127 getIdealVerticalOffset()5128 public float getIdealVerticalOffset() { 5129 return mIdealVerticalOffset; 5130 } 5131 getIdealFingerToCursorOffset()5132 final int getIdealFingerToCursorOffset() { 5133 return mIdealFingerToCursorOffset; 5134 } 5135 setDrawables(final Drawable drawableLtr, final Drawable drawableRtl)5136 void setDrawables(final Drawable drawableLtr, final Drawable drawableRtl) { 5137 mDrawableLtr = drawableLtr; 5138 mDrawableRtl = drawableRtl; 5139 updateDrawable(true /* updateDrawableWhenDragging */); 5140 } 5141 updateDrawable(final boolean updateDrawableWhenDragging)5142 protected void updateDrawable(final boolean updateDrawableWhenDragging) { 5143 if (!updateDrawableWhenDragging && mIsDragging) { 5144 return; 5145 } 5146 final Layout layout = mTextView.getLayout(); 5147 if (layout == null) { 5148 return; 5149 } 5150 final int offset = getCurrentCursorOffset(); 5151 final boolean isRtlCharAtOffset = isAtRtlRun(layout, offset); 5152 final Drawable oldDrawable = mDrawable; 5153 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; 5154 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); 5155 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset); 5156 if (oldDrawable != mDrawable && isShowing()) { 5157 // Update popup window position. 5158 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX 5159 - getHorizontalOffset() + getCursorOffset(); 5160 mPositionX += mTextView.viewportToContentHorizontalOffset(); 5161 mPositionHasChanged = true; 5162 updatePosition(mLastParentX, mLastParentY, false, false); 5163 postInvalidate(); 5164 } 5165 } 5166 getHotspotX(Drawable drawable, boolean isRtlRun)5167 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); getHorizontalGravity(boolean isRtlRun)5168 protected abstract int getHorizontalGravity(boolean isRtlRun); 5169 5170 // Touch-up filter: number of previous positions remembered 5171 private static final int HISTORY_SIZE = 5; 5172 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; 5173 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; 5174 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; 5175 private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; 5176 private int mPreviousOffsetIndex = 0; 5177 private int mNumberPreviousOffsets = 0; 5178 startTouchUpFilter(int offset)5179 private void startTouchUpFilter(int offset) { 5180 mNumberPreviousOffsets = 0; 5181 addPositionToTouchUpFilter(offset); 5182 } 5183 addPositionToTouchUpFilter(int offset)5184 private void addPositionToTouchUpFilter(int offset) { 5185 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; 5186 mPreviousOffsets[mPreviousOffsetIndex] = offset; 5187 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); 5188 mNumberPreviousOffsets++; 5189 } 5190 filterOnTouchUp(boolean fromTouchScreen)5191 private void filterOnTouchUp(boolean fromTouchScreen) { 5192 final long now = SystemClock.uptimeMillis(); 5193 int i = 0; 5194 int index = mPreviousOffsetIndex; 5195 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); 5196 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { 5197 i++; 5198 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; 5199 } 5200 5201 if (i > 0 && i < iMax 5202 && (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { 5203 positionAtCursorOffset(mPreviousOffsets[index], false, fromTouchScreen); 5204 } 5205 } 5206 offsetHasBeenChanged()5207 public boolean offsetHasBeenChanged() { 5208 return mNumberPreviousOffsets > 1; 5209 } 5210 5211 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)5212 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 5213 setMeasuredDimension(getPreferredWidth(), getPreferredHeight()); 5214 } 5215 5216 @Override invalidate()5217 public void invalidate() { 5218 super.invalidate(); 5219 if (isShowing()) { 5220 positionAtCursorOffset(getCurrentCursorOffset(), true, false); 5221 } 5222 }; 5223 getPreferredWidth()5224 protected final int getPreferredWidth() { 5225 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize); 5226 } 5227 getPreferredHeight()5228 protected final int getPreferredHeight() { 5229 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize); 5230 } 5231 show()5232 public void show() { 5233 if (TextView.DEBUG_CURSOR) { 5234 logCursor(getClass().getSimpleName() + ": HandleView: show()", "offset=%s", 5235 getCurrentCursorOffset()); 5236 } 5237 5238 if (isShowing()) return; 5239 5240 getPositionListener().addSubscriber(this, true /* local position may change */); 5241 5242 // Make sure the offset is always considered new, even when focusing at same position 5243 mPreviousOffset = -1; 5244 positionAtCursorOffset(getCurrentCursorOffset(), false, false); 5245 } 5246 dismiss()5247 protected void dismiss() { 5248 mIsDragging = false; 5249 mContainer.dismiss(); 5250 onDetached(); 5251 } 5252 hide()5253 public void hide() { 5254 if (TextView.DEBUG_CURSOR) { 5255 logCursor(getClass().getSimpleName() + ": HandleView: hide()", "offset=%s", 5256 getCurrentCursorOffset()); 5257 } 5258 5259 dismiss(); 5260 5261 getPositionListener().removeSubscriber(this); 5262 } 5263 isShowing()5264 public boolean isShowing() { 5265 return mContainer.isShowing(); 5266 } 5267 shouldShow()5268 private boolean shouldShow() { 5269 // A dragging handle should always be shown. 5270 if (mIsDragging) { 5271 return true; 5272 } 5273 5274 if (mTextView.isInBatchEditMode()) { 5275 return false; 5276 } 5277 5278 return mTextView.isPositionVisible( 5279 mPositionX + mHotspotX + getHorizontalOffset(), mPositionY); 5280 } 5281 setVisible(final boolean visible)5282 private void setVisible(final boolean visible) { 5283 mContainer.getContentView().setVisibility(visible ? VISIBLE : INVISIBLE); 5284 } 5285 getCurrentCursorOffset()5286 public abstract int getCurrentCursorOffset(); 5287 updateSelection(int offset)5288 protected abstract void updateSelection(int offset); 5289 updatePosition(float x, float y, boolean fromTouchScreen)5290 protected abstract void updatePosition(float x, float y, boolean fromTouchScreen); 5291 5292 @MagnifierHandleTrigger getMagnifierHandleTrigger()5293 protected abstract int getMagnifierHandleTrigger(); 5294 isAtRtlRun(@onNull Layout layout, int offset)5295 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) { 5296 final int transformedOffset = 5297 mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR); 5298 return layout.isRtlCharAt(transformedOffset); 5299 } 5300 5301 @VisibleForTesting getHorizontal(@onNull Layout layout, int offset)5302 public float getHorizontal(@NonNull Layout layout, int offset) { 5303 final int transformedOffset = 5304 mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR); 5305 return layout.getPrimaryHorizontal(transformedOffset); 5306 } 5307 5308 /** 5309 * Return the line number for a given offset. 5310 * @param layout the {@link Layout} to query. 5311 * @param offset the index of the character to query. 5312 * @return the index of the line the given offset belongs to. 5313 */ getLineForOffset(@onNull Layout layout, int offset)5314 public int getLineForOffset(@NonNull Layout layout, int offset) { 5315 final int transformedOffset = 5316 mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR); 5317 return layout.getLineForOffset(transformedOffset); 5318 } 5319 getOffsetAtCoordinate(@onNull Layout layout, int line, float x)5320 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) { 5321 return mTextView.getOffsetAtCoordinate(line, x); 5322 } 5323 5324 /** 5325 * @param offset Cursor offset. Must be in [-1, length]. 5326 * @param forceUpdatePosition whether to force update the position. This should be true 5327 * when If the parent has been scrolled, for example. 5328 * @param fromTouchScreen {@code true} if the cursor is moved with motion events from the 5329 * touch screen. 5330 */ positionAtCursorOffset(int offset, boolean forceUpdatePosition, boolean fromTouchScreen)5331 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition, 5332 boolean fromTouchScreen) { 5333 // A HandleView relies on the layout, which may be nulled by external methods 5334 final Layout layout = mTextView.getLayout(); 5335 if (layout == null) { 5336 // Will update controllers' state, hiding them and stopping selection mode if needed 5337 prepareCursorControllers(); 5338 return; 5339 } 5340 5341 boolean offsetChanged = offset != mPreviousOffset; 5342 if (offsetChanged || forceUpdatePosition) { 5343 if (offsetChanged) { 5344 updateSelection(offset); 5345 if (fromTouchScreen && mHapticTextHandleEnabled) { 5346 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); 5347 } 5348 addPositionToTouchUpFilter(offset); 5349 } 5350 final int line = getLineForOffset(layout, offset); 5351 mPrevLine = line; 5352 5353 mPositionX = getCursorHorizontalPosition(layout, offset) - mHotspotX 5354 - getHorizontalOffset() + getCursorOffset(); 5355 mPositionY = layout.getLineBottom(line, /* includeLineSpacing= */ false); 5356 5357 // Take TextView's padding and scroll into account. 5358 mPositionX += mTextView.viewportToContentHorizontalOffset(); 5359 mPositionY += mTextView.viewportToContentVerticalOffset(); 5360 5361 mPreviousOffset = offset; 5362 mPositionHasChanged = true; 5363 } 5364 } 5365 5366 /** 5367 * Return the clamped horizontal position for the cursor. 5368 * 5369 * @param layout Text layout. 5370 * @param offset Character offset for the cursor. 5371 * @return The clamped horizontal position for the cursor. 5372 */ getCursorHorizontalPosition(Layout layout, int offset)5373 int getCursorHorizontalPosition(Layout layout, int offset) { 5374 return (int) (getHorizontal(layout, offset) - 0.5f); 5375 } 5376 5377 @Override updatePosition(int parentPositionX, int parentPositionY, boolean parentPositionChanged, boolean parentScrolled)5378 public void updatePosition(int parentPositionX, int parentPositionY, 5379 boolean parentPositionChanged, boolean parentScrolled) { 5380 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled, false); 5381 if (parentPositionChanged || mPositionHasChanged) { 5382 if (mIsDragging) { 5383 // Update touchToWindow offset in case of parent scrolling while dragging 5384 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { 5385 mTouchToWindowOffsetX += parentPositionX - mLastParentX; 5386 mTouchToWindowOffsetY += parentPositionY - mLastParentY; 5387 mLastParentX = parentPositionX; 5388 mLastParentY = parentPositionY; 5389 } 5390 5391 onHandleMoved(); 5392 } 5393 5394 if (shouldShow()) { 5395 // Transform to the window coordinates to follow the view tranformation. 5396 final int[] pts = { mPositionX + mHotspotX + getHorizontalOffset(), mPositionY}; 5397 mTextView.transformFromViewToWindowSpace(pts); 5398 pts[0] -= mHotspotX + getHorizontalOffset(); 5399 5400 if (isShowing()) { 5401 mContainer.update(pts[0], pts[1], -1, -1); 5402 } else { 5403 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, pts[0], pts[1]); 5404 } 5405 } else { 5406 if (isShowing()) { 5407 dismiss(); 5408 } 5409 } 5410 5411 mPositionHasChanged = false; 5412 } 5413 } 5414 5415 @Override onDraw(Canvas c)5416 protected void onDraw(Canvas c) { 5417 final int drawWidth = mDrawable.getIntrinsicWidth(); 5418 final int left = getHorizontalOffset(); 5419 5420 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight()); 5421 mDrawable.draw(c); 5422 } 5423 getHorizontalOffset()5424 private int getHorizontalOffset() { 5425 final int width = getPreferredWidth(); 5426 final int drawWidth = mDrawable.getIntrinsicWidth(); 5427 final int left; 5428 switch (mHorizontalGravity) { 5429 case Gravity.LEFT: 5430 left = 0; 5431 break; 5432 default: 5433 case Gravity.CENTER: 5434 left = (width - drawWidth) / 2; 5435 break; 5436 case Gravity.RIGHT: 5437 left = width - drawWidth; 5438 break; 5439 } 5440 return left; 5441 } 5442 getCursorOffset()5443 protected int getCursorOffset() { 5444 return 0; 5445 } 5446 tooLargeTextForMagnifier()5447 private boolean tooLargeTextForMagnifier() { 5448 if (mNewMagnifierEnabled) { 5449 Layout layout = mTextView.getLayout(); 5450 final int line = getLineForOffset(layout, getCurrentCursorOffset()); 5451 return layout.getLineBottom(line, /* includeLineSpacing= */ false) 5452 - layout.getLineTop(line) >= mMaxLineHeightForMagnifier; 5453 } 5454 final float magnifierContentHeight = Math.round( 5455 mMagnifierAnimator.mMagnifier.getHeight() 5456 / mMagnifierAnimator.mMagnifier.getZoom()); 5457 final Paint.FontMetrics fontMetrics = mTextView.getPaint().getFontMetrics(); 5458 final float glyphHeight = fontMetrics.descent - fontMetrics.ascent; 5459 return glyphHeight * mTextViewScaleY > magnifierContentHeight; 5460 } 5461 5462 /** 5463 * Traverses the hierarchy above the text view, and computes the total scale applied 5464 * to it. If a rotation is encountered, the method returns {@code false}, indicating 5465 * that the magnifier should not be shown anyways. It would be nice to keep these two 5466 * pieces of logic separate (the rotation check and the total scale calculation), 5467 * but for efficiency we can do them in a single go. 5468 * @return whether the text view is rotated 5469 */ checkForTransforms()5470 private boolean checkForTransforms() { 5471 if (mMagnifierAnimator.mMagnifierIsShowing) { 5472 // Do not check again when the magnifier is currently showing. 5473 return true; 5474 } 5475 5476 if (mTextView.getRotation() != 0f || mTextView.getRotationX() != 0f 5477 || mTextView.getRotationY() != 0f) { 5478 return false; 5479 } 5480 mTextViewScaleX = mTextView.getScaleX(); 5481 mTextViewScaleY = mTextView.getScaleY(); 5482 5483 ViewParent viewParent = mTextView.getParent(); 5484 while (viewParent != null) { 5485 if (viewParent instanceof View) { 5486 final View view = (View) viewParent; 5487 if (view.getRotation() != 0f || view.getRotationX() != 0f 5488 || view.getRotationY() != 0f) { 5489 return false; 5490 } 5491 mTextViewScaleX *= view.getScaleX(); 5492 mTextViewScaleY *= view.getScaleY(); 5493 } 5494 viewParent = viewParent.getParent(); 5495 } 5496 return true; 5497 } 5498 5499 /** 5500 * Computes the position where the magnifier should be shown, relative to 5501 * {@code mTextView}, and writes them to {@code showPosInView}. Also decides 5502 * whether the magnifier should be shown or dismissed after this touch event. 5503 * @return Whether the magnifier should be shown at the computed coordinates or dismissed. 5504 */ obtainMagnifierShowCoordinates(@onNull final MotionEvent event, final PointF showPosInView)5505 private boolean obtainMagnifierShowCoordinates(@NonNull final MotionEvent event, 5506 final PointF showPosInView) { 5507 5508 final int trigger = getMagnifierHandleTrigger(); 5509 final int offset; 5510 final int otherHandleOffset; 5511 switch (trigger) { 5512 case MagnifierHandleTrigger.INSERTION: 5513 offset = mTextView.getSelectionStart(); 5514 otherHandleOffset = -1; 5515 break; 5516 case MagnifierHandleTrigger.SELECTION_START: 5517 offset = mTextView.getSelectionStart(); 5518 otherHandleOffset = mTextView.getSelectionEnd(); 5519 break; 5520 case MagnifierHandleTrigger.SELECTION_END: 5521 offset = mTextView.getSelectionEnd(); 5522 otherHandleOffset = mTextView.getSelectionStart(); 5523 break; 5524 default: 5525 offset = -1; 5526 otherHandleOffset = -1; 5527 break; 5528 } 5529 5530 if (offset == -1) { 5531 return false; 5532 } 5533 5534 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 5535 mCurrentDragInitialTouchRawX = event.getRawX(); 5536 } else if (event.getActionMasked() == MotionEvent.ACTION_UP) { 5537 mCurrentDragInitialTouchRawX = UNSET_X_VALUE; 5538 } 5539 5540 final Layout layout = mTextView.getLayout(); 5541 final int lineNumber = getLineForOffset(layout, offset); 5542 // Compute whether the selection handles are currently on the same line, and, 5543 // in this particular case, whether the selected text is right to left. 5544 final boolean sameLineSelection = otherHandleOffset != -1 5545 && lineNumber == getLineForOffset(layout, offset); 5546 final boolean rtl = sameLineSelection 5547 && (offset < otherHandleOffset) 5548 != (getHorizontal(mTextView.getLayout(), offset) 5549 < getHorizontal(mTextView.getLayout(), otherHandleOffset)); 5550 5551 // Horizontally move the magnifier smoothly, clamp inside the current line / selection. 5552 final int[] textViewLocationOnScreen = new int[2]; 5553 mTextView.getLocationOnScreen(textViewLocationOnScreen); 5554 final float touchXInView = event.getRawX() - textViewLocationOnScreen[0]; 5555 float leftBound, rightBound; 5556 if (mNewMagnifierEnabled) { 5557 leftBound = 0; 5558 rightBound = mTextView.getWidth(); 5559 if (touchXInView < leftBound || touchXInView > rightBound) { 5560 // The touch is too far from the current line / selection, so hide the magnifier. 5561 return false; 5562 } 5563 } else { 5564 leftBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); 5565 rightBound = mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); 5566 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_END) 5567 ^ rtl)) { 5568 leftBound += getHorizontal(mTextView.getLayout(), otherHandleOffset); 5569 } else { 5570 leftBound += mTextView.getLayout().getLineLeft(lineNumber); 5571 } 5572 if (sameLineSelection && ((trigger == MagnifierHandleTrigger.SELECTION_START) 5573 ^ rtl)) { 5574 rightBound += getHorizontal(mTextView.getLayout(), otherHandleOffset); 5575 } else { 5576 rightBound += mTextView.getLayout().getLineRight(lineNumber); 5577 } 5578 leftBound *= mTextViewScaleX; 5579 rightBound *= mTextViewScaleX; 5580 final float contentWidth = Math.round(mMagnifierAnimator.mMagnifier.getWidth() 5581 / mMagnifierAnimator.mMagnifier.getZoom()); 5582 if (touchXInView < leftBound - contentWidth / 2 5583 || touchXInView > rightBound + contentWidth / 2) { 5584 // The touch is too far from the current line / selection, so hide the magnifier. 5585 return false; 5586 } 5587 } 5588 5589 final float scaledTouchXInView; 5590 if (mTextViewScaleX == 1f) { 5591 // In the common case, do not use mCurrentDragInitialTouchRawX to compute this 5592 // coordinate, although the formula on the else branch should be equivalent. 5593 // Since the formula relies on mCurrentDragInitialTouchRawX being set on 5594 // MotionEvent.ACTION_DOWN, this makes us more defensive against cases when 5595 // the sequence of events might not look as expected: for example, a sequence of 5596 // ACTION_MOVE not preceded by ACTION_DOWN. 5597 scaledTouchXInView = touchXInView; 5598 } else { 5599 scaledTouchXInView = (event.getRawX() - mCurrentDragInitialTouchRawX) 5600 * mTextViewScaleX + mCurrentDragInitialTouchRawX 5601 - textViewLocationOnScreen[0]; 5602 } 5603 showPosInView.x = Math.max(leftBound, Math.min(rightBound, scaledTouchXInView)); 5604 5605 // Vertically snap to middle of current line. 5606 showPosInView.y = ((mTextView.getLayout().getLineTop(lineNumber) 5607 + mTextView.getLayout() 5608 .getLineBottom(lineNumber, /* includeLineSpacing= */ false)) / 2.0f 5609 + mTextView.getTotalPaddingTop() - mTextView.getScrollY()) * mTextViewScaleY; 5610 return true; 5611 } 5612 handleOverlapsMagnifier(@onNull final HandleView handle, @NonNull final Rect magnifierRect)5613 private boolean handleOverlapsMagnifier(@NonNull final HandleView handle, 5614 @NonNull final Rect magnifierRect) { 5615 final PopupWindow window = handle.mContainer; 5616 if (!window.hasDecorView()) { 5617 return false; 5618 } 5619 final Rect handleRect = new Rect( 5620 window.getDecorViewLayoutParams().x, 5621 window.getDecorViewLayoutParams().y, 5622 window.getDecorViewLayoutParams().x + window.getContentView().getWidth(), 5623 window.getDecorViewLayoutParams().y + window.getContentView().getHeight()); 5624 return Rect.intersects(handleRect, magnifierRect); 5625 } 5626 getOtherSelectionHandle()5627 private @Nullable HandleView getOtherSelectionHandle() { 5628 final SelectionModifierCursorController controller = getSelectionController(); 5629 if (controller == null || !controller.isActive()) { 5630 return null; 5631 } 5632 return controller.mStartHandle != this 5633 ? controller.mStartHandle 5634 : controller.mEndHandle; 5635 } 5636 updateHandlesVisibility()5637 private void updateHandlesVisibility() { 5638 final Point magnifierTopLeft = mMagnifierAnimator.mMagnifier.getPosition(); 5639 if (magnifierTopLeft == null) { 5640 return; 5641 } 5642 final Rect magnifierRect = new Rect(magnifierTopLeft.x, magnifierTopLeft.y, 5643 magnifierTopLeft.x + mMagnifierAnimator.mMagnifier.getWidth(), 5644 magnifierTopLeft.y + mMagnifierAnimator.mMagnifier.getHeight()); 5645 setVisible(!handleOverlapsMagnifier(HandleView.this, magnifierRect) 5646 && !mDrawCursorOnMagnifier); 5647 final HandleView otherHandle = getOtherSelectionHandle(); 5648 if (otherHandle != null) { 5649 otherHandle.setVisible(!handleOverlapsMagnifier(otherHandle, magnifierRect)); 5650 } 5651 } 5652 updateMagnifier(@onNull final MotionEvent event)5653 protected final void updateMagnifier(@NonNull final MotionEvent event) { 5654 if (getMagnifierAnimator() == null) { 5655 return; 5656 } 5657 5658 final PointF showPosInView = new PointF(); 5659 final boolean shouldShow = checkForTransforms() /*check not rotated and compute scale*/ 5660 && !tooLargeTextForMagnifier() 5661 && obtainMagnifierShowCoordinates(event, showPosInView) 5662 && mTextView.showUIForTouchScreen(); 5663 if (shouldShow) { 5664 // Make the cursor visible and stop blinking. 5665 mRenderCursorRegardlessTiming = true; 5666 mTextView.invalidateCursorPath(); 5667 suspendBlink(); 5668 5669 if (mNewMagnifierEnabled) { 5670 // Calculates the line bounds as the content source bounds to the magnifier. 5671 Layout layout = mTextView.getLayout(); 5672 int line = getLineForOffset(layout, getCurrentCursorOffset()); 5673 int lineLeft = (int) layout.getLineLeft(line); 5674 lineLeft += mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); 5675 int lineRight = (int) layout.getLineRight(line); 5676 lineRight += mTextView.getTotalPaddingLeft() - mTextView.getScrollX(); 5677 mDrawCursorOnMagnifier = 5678 showPosInView.x < lineLeft - CURSOR_START_FLOAT_DISTANCE_PX 5679 || showPosInView.x > lineRight + CURSOR_START_FLOAT_DISTANCE_PX; 5680 mMagnifierAnimator.mMagnifier.setDrawCursor( 5681 mDrawCursorOnMagnifier, mDrawableForCursor); 5682 boolean cursorVisible = mCursorVisible; 5683 // Updates cursor visibility, so that the real cursor and the float cursor on 5684 // magnifier surface won't appear at the same time. 5685 mCursorVisible = !mDrawCursorOnMagnifier; 5686 if (mCursorVisible && !cursorVisible) { 5687 // When the real cursor is a drawable, hiding/showing it would change its 5688 // bounds. So, call updateCursorPosition() to correct its position. 5689 updateCursorPosition(); 5690 } 5691 final int lineHeight = 5692 layout.getLineBottom(line, /* includeLineSpacing= */ false) 5693 - layout.getLineTop(line); 5694 float zoom = mInitialZoom; 5695 if (lineHeight < mMinLineHeightForMagnifier) { 5696 zoom = zoom * mMinLineHeightForMagnifier / lineHeight; 5697 } 5698 mMagnifierAnimator.mMagnifier.updateSourceFactors(lineHeight, zoom); 5699 mMagnifierAnimator.mMagnifier.show(showPosInView.x, showPosInView.y); 5700 } else { 5701 mMagnifierAnimator.show(showPosInView.x, showPosInView.y); 5702 } 5703 updateHandlesVisibility(); 5704 } else { 5705 dismissMagnifier(); 5706 } 5707 } 5708 dismissMagnifier()5709 protected final void dismissMagnifier() { 5710 if (mMagnifierAnimator != null) { 5711 mMagnifierAnimator.dismiss(); 5712 mRenderCursorRegardlessTiming = false; 5713 mDrawCursorOnMagnifier = false; 5714 if (!mCursorVisible) { 5715 mCursorVisible = true; 5716 mTextView.invalidate(); 5717 } 5718 resumeBlink(); 5719 setVisible(true); 5720 final HandleView otherHandle = getOtherSelectionHandle(); 5721 if (otherHandle != null) { 5722 otherHandle.setVisible(true); 5723 } 5724 } 5725 } 5726 5727 @Override onTouchEvent(MotionEvent ev)5728 public boolean onTouchEvent(MotionEvent ev) { 5729 if (TextView.DEBUG_CURSOR) { 5730 logCursor(this.getClass().getSimpleName() + ": HandleView: onTouchEvent", 5731 "%d: %s (%f,%f)", 5732 ev.getSequenceNumber(), 5733 MotionEvent.actionToString(ev.getActionMasked()), 5734 ev.getX(), ev.getY()); 5735 } 5736 5737 updateFloatingToolbarVisibility(ev); 5738 5739 switch (ev.getActionMasked()) { 5740 case MotionEvent.ACTION_DOWN: { 5741 startTouchUpFilter(getCurrentCursorOffset()); 5742 5743 final PositionListener positionListener = getPositionListener(); 5744 mLastParentX = positionListener.getPositionX(); 5745 mLastParentY = positionListener.getPositionY(); 5746 mLastParentXOnScreen = positionListener.getPositionXOnScreen(); 5747 mLastParentYOnScreen = positionListener.getPositionYOnScreen(); 5748 5749 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX; 5750 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY; 5751 mTouchToWindowOffsetX = xInWindow - mPositionX; 5752 mTouchToWindowOffsetY = yInWindow - mPositionY; 5753 5754 mIsDragging = true; 5755 mPreviousLineTouched = UNSET_LINE; 5756 break; 5757 } 5758 5759 case MotionEvent.ACTION_MOVE: { 5760 final float xInWindow = ev.getRawX() - mLastParentXOnScreen + mLastParentX; 5761 final float yInWindow = ev.getRawY() - mLastParentYOnScreen + mLastParentY; 5762 5763 // Vertical hysteresis: vertical down movement tends to snap to ideal offset 5764 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; 5765 final float currentVerticalOffset = yInWindow - mPositionY - mLastParentY; 5766 float newVerticalOffset; 5767 if (previousVerticalOffset < mIdealVerticalOffset) { 5768 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); 5769 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); 5770 } else { 5771 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); 5772 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); 5773 } 5774 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; 5775 5776 final float newPosX = 5777 xInWindow - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset(); 5778 final float newPosY = yInWindow - mTouchToWindowOffsetY + mTouchOffsetY; 5779 5780 updatePosition(newPosX, newPosY, 5781 ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); 5782 break; 5783 } 5784 5785 case MotionEvent.ACTION_UP: 5786 filterOnTouchUp(ev.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); 5787 // Fall through. 5788 case MotionEvent.ACTION_CANCEL: 5789 mIsDragging = false; 5790 updateDrawable(false /* updateDrawableWhenDragging */); 5791 break; 5792 } 5793 return true; 5794 } 5795 isDragging()5796 public boolean isDragging() { 5797 return mIsDragging; 5798 } 5799 onHandleMoved()5800 void onHandleMoved() {} 5801 5802 /** 5803 * Called back when the handle view was detached. 5804 */ onDetached()5805 public void onDetached() { 5806 dismissMagnifier(); 5807 } 5808 5809 @Override onSizeChanged(int w, int h, int oldw, int oldh)5810 protected void onSizeChanged(int w, int h, int oldw, int oldh) { 5811 super.onSizeChanged(w, h, oldw, oldh); 5812 setSystemGestureExclusionRects(Collections.singletonList(new Rect(0, 0, w, h))); 5813 } 5814 } 5815 5816 private class InsertionHandleView extends HandleView { 5817 // Used to detect taps on the insertion handle, which will affect the insertion action mode 5818 private float mLastDownRawX, mLastDownRawY; 5819 private Runnable mHider; 5820 5821 // Members for fake-dismiss effect in touch through mode. 5822 // It is to make InsertionHandleView can receive the MOVE/UP events after calling dismiss(), 5823 // which could happen in case of long-press (making selection will dismiss the insertion 5824 // handle). 5825 5826 // Whether the finger is down and hasn't been up yet. 5827 private boolean mIsTouchDown = false; 5828 // Whether the popup window is in the invisible state and will be dismissed when finger up. 5829 private boolean mPendingDismissOnUp = false; 5830 // The alpha value of the drawable. 5831 private final int mDrawableOpacity; 5832 5833 // Members for toggling the insertion menu in touch through mode. 5834 5835 // The coordinate for the touch down event, which is used for transforming the coordinates 5836 // of the events to the text view. 5837 private float mTouchDownX; 5838 private float mTouchDownY; 5839 // The cursor offset when touch down. This is to detect whether the cursor is moved when 5840 // finger move/up. 5841 private int mOffsetDown; 5842 // Whether the cursor offset has been changed on the move/up events. 5843 private boolean mOffsetChanged; 5844 // Whether it is in insertion action mode when finger down. 5845 private boolean mIsInActionMode; 5846 // The timestamp for the last up event, which is used for double tap detection. 5847 private long mLastUpTime; 5848 5849 // The delta height applied to the insertion handle view. 5850 private final int mDeltaHeight; 5851 InsertionHandleView(Drawable drawable)5852 InsertionHandleView(Drawable drawable) { 5853 super(drawable, drawable, com.android.internal.R.id.insertion_handle); 5854 5855 int deltaHeight = 0; 5856 int opacity = 255; 5857 if (mFlagInsertionHandleGesturesEnabled) { 5858 deltaHeight = AppGlobals.getIntCoreSetting( 5859 WidgetFlags.KEY_INSERTION_HANDLE_DELTA_HEIGHT, 5860 WidgetFlags.INSERTION_HANDLE_DELTA_HEIGHT_DEFAULT); 5861 opacity = AppGlobals.getIntCoreSetting( 5862 WidgetFlags.KEY_INSERTION_HANDLE_OPACITY, 5863 WidgetFlags.INSERTION_HANDLE_OPACITY_DEFAULT); 5864 // Avoid invalid/unsupported values. 5865 if (deltaHeight < -25 || deltaHeight > 50) { 5866 deltaHeight = 25; 5867 } 5868 if (opacity < 10 || opacity > 100) { 5869 opacity = 50; 5870 } 5871 // Converts the opacity value from range {0..100} to {0..255}. 5872 opacity = opacity * 255 / 100; 5873 } 5874 mDeltaHeight = deltaHeight; 5875 mDrawableOpacity = opacity; 5876 } 5877 hideAfterDelay()5878 private void hideAfterDelay() { 5879 if (mHider == null) { 5880 mHider = new Runnable() { 5881 public void run() { 5882 hide(); 5883 } 5884 }; 5885 } else { 5886 removeHiderCallback(); 5887 } 5888 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); 5889 } 5890 removeHiderCallback()5891 private void removeHiderCallback() { 5892 if (mHider != null) { 5893 mTextView.removeCallbacks(mHider); 5894 } 5895 } 5896 5897 @Override getHotspotX(Drawable drawable, boolean isRtlRun)5898 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 5899 return drawable.getIntrinsicWidth() / 2; 5900 } 5901 5902 @Override getHorizontalGravity(boolean isRtlRun)5903 protected int getHorizontalGravity(boolean isRtlRun) { 5904 return Gravity.CENTER_HORIZONTAL; 5905 } 5906 5907 @Override getCursorOffset()5908 protected int getCursorOffset() { 5909 int offset = super.getCursorOffset(); 5910 if (mDrawableForCursor != null) { 5911 mDrawableForCursor.getPadding(mTempRect); 5912 offset += (mDrawableForCursor.getIntrinsicWidth() 5913 - mTempRect.left - mTempRect.right) / 2; 5914 } 5915 return offset; 5916 } 5917 5918 @Override getCursorHorizontalPosition(Layout layout, int offset)5919 int getCursorHorizontalPosition(Layout layout, int offset) { 5920 if (mDrawableForCursor != null) { 5921 final float horizontal = getHorizontal(layout, offset); 5922 return clampHorizontalPosition(mDrawableForCursor, horizontal) + mTempRect.left; 5923 } 5924 return super.getCursorHorizontalPosition(layout, offset); 5925 } 5926 5927 @Override onMeasure(int widthMeasureSpec, int heightMeasureSpec)5928 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 5929 if (mFlagInsertionHandleGesturesEnabled) { 5930 final int height = Math.max( 5931 getPreferredHeight() + mDeltaHeight, mDrawable.getIntrinsicHeight()); 5932 setMeasuredDimension(getPreferredWidth(), height); 5933 return; 5934 } 5935 super.onMeasure(widthMeasureSpec, heightMeasureSpec); 5936 } 5937 5938 @Override onTouchEvent(MotionEvent ev)5939 public boolean onTouchEvent(MotionEvent ev) { 5940 if (!mTextView.isFromPrimePointer(ev, true)) { 5941 return true; 5942 } 5943 if (mFlagInsertionHandleGesturesEnabled && mFlagCursorDragFromAnywhereEnabled) { 5944 // Should only enable touch through when cursor drag is enabled. 5945 // Otherwise the insertion handle view cannot be moved. 5946 return touchThrough(ev); 5947 } 5948 final boolean result = super.onTouchEvent(ev); 5949 5950 switch (ev.getActionMasked()) { 5951 case MotionEvent.ACTION_DOWN: 5952 mLastDownRawX = ev.getRawX(); 5953 mLastDownRawY = ev.getRawY(); 5954 updateMagnifier(ev); 5955 break; 5956 5957 case MotionEvent.ACTION_MOVE: 5958 updateMagnifier(ev); 5959 break; 5960 5961 case MotionEvent.ACTION_UP: 5962 if (!offsetHasBeenChanged()) { 5963 ViewConfiguration config = ViewConfiguration.get(mTextView.getContext()); 5964 boolean isWithinTouchSlop = EditorTouchState.isDistanceWithin( 5965 mLastDownRawX, mLastDownRawY, ev.getRawX(), ev.getRawY(), 5966 config.getScaledTouchSlop()); 5967 if (isWithinTouchSlop) { 5968 // Tapping on the handle toggles the insertion action mode. 5969 toggleInsertionActionMode(); 5970 } 5971 } else { 5972 if (mTextActionMode != null) { 5973 mTextActionMode.invalidateContentRect(); 5974 } 5975 } 5976 // Fall through. 5977 case MotionEvent.ACTION_CANCEL: 5978 hideAfterDelay(); 5979 dismissMagnifier(); 5980 break; 5981 5982 default: 5983 break; 5984 } 5985 5986 return result; 5987 } 5988 5989 // Handles the touch events in touch through mode. touchThrough(MotionEvent ev)5990 private boolean touchThrough(MotionEvent ev) { 5991 final int actionType = ev.getActionMasked(); 5992 switch (actionType) { 5993 case MotionEvent.ACTION_DOWN: 5994 mIsTouchDown = true; 5995 mOffsetChanged = false; 5996 mOffsetDown = mTextView.getSelectionStart(); 5997 mTouchDownX = ev.getX(); 5998 mTouchDownY = ev.getY(); 5999 mIsInActionMode = mTextActionMode != null; 6000 if (ev.getEventTime() - mLastUpTime < ViewConfiguration.getDoubleTapTimeout()) { 6001 stopTextActionMode(); // Avoid crash when double tap and drag backwards. 6002 } 6003 mTouchState.setIsOnHandle(true); 6004 break; 6005 case MotionEvent.ACTION_UP: 6006 mLastUpTime = ev.getEventTime(); 6007 break; 6008 } 6009 // Performs the touch through by forward the events to the text view. 6010 boolean ret = mTextView.onTouchEvent(transformEventForTouchThrough(ev)); 6011 6012 if (actionType == MotionEvent.ACTION_UP || actionType == MotionEvent.ACTION_CANCEL) { 6013 mIsTouchDown = false; 6014 if (mPendingDismissOnUp) { 6015 dismiss(); 6016 } 6017 mTouchState.setIsOnHandle(false); 6018 } 6019 6020 // Checks for cursor offset change. 6021 if (!mOffsetChanged) { 6022 int start = mTextView.getSelectionStart(); 6023 int end = mTextView.getSelectionEnd(); 6024 if (start != end || mOffsetDown != start) { 6025 mOffsetChanged = true; 6026 } 6027 } 6028 6029 // Toggling the insertion action mode on finger up. 6030 if (!mOffsetChanged && actionType == MotionEvent.ACTION_UP) { 6031 if (mIsInActionMode) { 6032 stopTextActionMode(); 6033 } else { 6034 startInsertionActionMode(); 6035 } 6036 } 6037 return ret; 6038 } 6039 transformEventForTouchThrough(MotionEvent ev)6040 private MotionEvent transformEventForTouchThrough(MotionEvent ev) { 6041 final Layout layout = mTextView.getLayout(); 6042 final int line = getLineForOffset(layout, getCurrentCursorOffset()); 6043 final int textHeight = layout.getLineBottom(line, /* includeLineSpacing= */ false) 6044 - layout.getLineTop(line); 6045 // Transforms the touch events to screen coordinates. 6046 // And also shift up to make the hit point is on the text. 6047 // Note: 6048 // - The revised X should reflect the distance to the horizontal center of touch down. 6049 // - The revised Y should be at the top of the text. 6050 Matrix m = new Matrix(); 6051 m.setTranslate(ev.getRawX() - ev.getX() + (getMeasuredWidth() >> 1) - mTouchDownX, 6052 ev.getRawY() - ev.getY() - (textHeight >> 1) - mTouchDownY); 6053 ev.transform(m); 6054 // Transforms the touch events to text view coordinates. 6055 mTextView.toLocalMotionEvent(ev); 6056 if (TextView.DEBUG_CURSOR) { 6057 logCursor("InsertionHandleView#transformEventForTouchThrough", 6058 "Touch through: %d, (%f, %f)", 6059 ev.getAction(), ev.getX(), ev.getY()); 6060 } 6061 return ev; 6062 } 6063 6064 @Override isShowing()6065 public boolean isShowing() { 6066 if (mPendingDismissOnUp) { 6067 return false; 6068 } 6069 return super.isShowing(); 6070 } 6071 6072 @Override show()6073 public void show() { 6074 super.show(); 6075 mPendingDismissOnUp = false; 6076 mDrawable.setAlpha(mDrawableOpacity); 6077 } 6078 6079 @Override dismiss()6080 public void dismiss() { 6081 if (mIsTouchDown) { 6082 if (TextView.DEBUG_CURSOR) { 6083 logCursor("InsertionHandleView#dismiss", 6084 "Suppressed the real dismiss, only become invisible"); 6085 } 6086 mPendingDismissOnUp = true; 6087 mDrawable.setAlpha(0); 6088 } else { 6089 super.dismiss(); 6090 mPendingDismissOnUp = false; 6091 } 6092 } 6093 6094 @Override updateDrawable(final boolean updateDrawableWhenDragging)6095 protected void updateDrawable(final boolean updateDrawableWhenDragging) { 6096 super.updateDrawable(updateDrawableWhenDragging); 6097 mDrawable.setAlpha(mDrawableOpacity); 6098 } 6099 6100 @Override getCurrentCursorOffset()6101 public int getCurrentCursorOffset() { 6102 return mTextView.getSelectionStart(); 6103 } 6104 6105 @Override updateSelection(int offset)6106 public void updateSelection(int offset) { 6107 Selection.setSelection((Spannable) mTextView.getText(), offset); 6108 } 6109 6110 @Override updatePosition(float x, float y, boolean fromTouchScreen)6111 protected void updatePosition(float x, float y, boolean fromTouchScreen) { 6112 Layout layout = mTextView.getLayout(); 6113 int offset; 6114 if (layout != null) { 6115 if (mPreviousLineTouched == UNSET_LINE) { 6116 mPreviousLineTouched = mTextView.getLineAtCoordinate(y); 6117 } 6118 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y); 6119 offset = getOffsetAtCoordinate(layout, currLine, x); 6120 mPreviousLineTouched = currLine; 6121 } else { 6122 offset = -1; 6123 } 6124 if (TextView.DEBUG_CURSOR) { 6125 logCursor("InsertionHandleView: updatePosition", "x=%f, y=%f, offset=%d, line=%d", 6126 x, y, offset, mPreviousLineTouched); 6127 } 6128 positionAtCursorOffset(offset, false, fromTouchScreen); 6129 if (mTextActionMode != null) { 6130 invalidateActionMode(); 6131 } 6132 } 6133 6134 @Override onHandleMoved()6135 void onHandleMoved() { 6136 super.onHandleMoved(); 6137 removeHiderCallback(); 6138 } 6139 6140 @Override onDetached()6141 public void onDetached() { 6142 super.onDetached(); 6143 removeHiderCallback(); 6144 } 6145 6146 @Override 6147 @MagnifierHandleTrigger getMagnifierHandleTrigger()6148 protected int getMagnifierHandleTrigger() { 6149 return MagnifierHandleTrigger.INSERTION; 6150 } 6151 } 6152 6153 @Retention(RetentionPolicy.SOURCE) 6154 @IntDef(prefix = { "HANDLE_TYPE_" }, value = { 6155 HANDLE_TYPE_SELECTION_START, 6156 HANDLE_TYPE_SELECTION_END 6157 }) 6158 public @interface HandleType {} 6159 public static final int HANDLE_TYPE_SELECTION_START = 0; 6160 public static final int HANDLE_TYPE_SELECTION_END = 1; 6161 6162 /** For selection handles */ 6163 @VisibleForTesting 6164 public final class SelectionHandleView extends HandleView { 6165 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection 6166 // end (HANDLE_TYPE_SELECTION_END). 6167 @HandleType 6168 private final int mHandleType; 6169 // Indicates whether the cursor is making adjustments within a word. 6170 private boolean mInWord = false; 6171 // Difference between touch position and word boundary position. 6172 private float mTouchWordDelta; 6173 // X value of the previous updatePosition call. 6174 private float mPrevX; 6175 // Indicates if the handle has moved a boundary between LTR and RTL text. 6176 private boolean mLanguageDirectionChanged = false; 6177 // Distance from edge of horizontally scrolling text view 6178 // to use to switch to character mode. 6179 private final float mTextViewEdgeSlop; 6180 // Used to save text view location. 6181 private final int[] mTextViewLocation = new int[2]; 6182 SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id, @HandleType int handleType)6183 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id, 6184 @HandleType int handleType) { 6185 super(drawableLtr, drawableRtl, id); 6186 mHandleType = handleType; 6187 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext()); 6188 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4; 6189 } 6190 isStartHandle()6191 private boolean isStartHandle() { 6192 return mHandleType == HANDLE_TYPE_SELECTION_START; 6193 } 6194 6195 @Override getHotspotX(Drawable drawable, boolean isRtlRun)6196 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 6197 if (isRtlRun == isStartHandle()) { 6198 return drawable.getIntrinsicWidth() / 4; 6199 } else { 6200 return (drawable.getIntrinsicWidth() * 3) / 4; 6201 } 6202 } 6203 6204 @Override getHorizontalGravity(boolean isRtlRun)6205 protected int getHorizontalGravity(boolean isRtlRun) { 6206 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT; 6207 } 6208 6209 @Override getCurrentCursorOffset()6210 public int getCurrentCursorOffset() { 6211 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd(); 6212 } 6213 6214 @Override updateSelection(int offset)6215 protected void updateSelection(int offset) { 6216 if (isStartHandle()) { 6217 Selection.setSelection((Spannable) mTextView.getText(), offset, 6218 mTextView.getSelectionEnd()); 6219 } else { 6220 Selection.setSelection((Spannable) mTextView.getText(), 6221 mTextView.getSelectionStart(), offset); 6222 } 6223 updateDrawable(false /* updateDrawableWhenDragging */); 6224 if (mTextActionMode != null) { 6225 invalidateActionMode(); 6226 } 6227 } 6228 6229 @Override updatePosition(float x, float y, boolean fromTouchScreen)6230 protected void updatePosition(float x, float y, boolean fromTouchScreen) { 6231 final Layout layout = mTextView.getLayout(); 6232 if (layout == null) { 6233 // HandleView will deal appropriately in positionAtCursorOffset when 6234 // layout is null. 6235 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y), 6236 fromTouchScreen); 6237 return; 6238 } 6239 6240 if (mPreviousLineTouched == UNSET_LINE) { 6241 mPreviousLineTouched = mTextView.getLineAtCoordinate(y); 6242 } 6243 6244 boolean positionCursor = false; 6245 final int anotherHandleOffset = 6246 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart(); 6247 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y); 6248 int initialOffset = getOffsetAtCoordinate(layout, currLine, x); 6249 6250 if (isStartHandle() && initialOffset >= anotherHandleOffset 6251 || !isStartHandle() && initialOffset <= anotherHandleOffset) { 6252 // Handles have crossed, bound it to the first selected line and 6253 // adjust by word / char as normal. 6254 currLine = getLineForOffset(layout, anotherHandleOffset); 6255 initialOffset = getOffsetAtCoordinate(layout, currLine, x); 6256 } 6257 6258 int offset = initialOffset; 6259 final int wordEnd = getWordEnd(offset); 6260 final int wordStart = getWordStart(offset); 6261 6262 if (mPrevX == UNSET_X_VALUE) { 6263 mPrevX = x; 6264 } 6265 6266 final int currentOffset = getCurrentCursorOffset(); 6267 final boolean rtlAtCurrentOffset = isAtRtlRun(layout, currentOffset); 6268 final boolean atRtl = isAtRtlRun(layout, offset); 6269 final boolean isLvlBoundary = layout.isLevelBoundary( 6270 mTextView.originalToTransformed(offset, OffsetMapping.MAP_STRATEGY_CURSOR)); 6271 6272 // We can't determine if the user is expanding or shrinking the selection if they're 6273 // on a bi-di boundary, so until they've moved past the boundary we'll just place 6274 // the cursor at the current position. 6275 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) { 6276 // We're on a boundary or this is the first direction change -- just update 6277 // to the current position. 6278 mLanguageDirectionChanged = true; 6279 mTouchWordDelta = 0.0f; 6280 positionAndAdjustForCrossingHandles(offset, fromTouchScreen); 6281 return; 6282 } 6283 6284 if (mLanguageDirectionChanged) { 6285 // We've just moved past the boundary so update the position. After this we can 6286 // figure out if the user is expanding or shrinking to go by word or character. 6287 positionAndAdjustForCrossingHandles(offset, fromTouchScreen); 6288 mTouchWordDelta = 0.0f; 6289 mLanguageDirectionChanged = false; 6290 return; 6291 } 6292 6293 boolean isExpanding; 6294 final float xDiff = x - mPrevX; 6295 if (isStartHandle()) { 6296 isExpanding = currLine < mPreviousLineTouched; 6297 } else { 6298 isExpanding = currLine > mPreviousLineTouched; 6299 } 6300 if (atRtl == isStartHandle()) { 6301 isExpanding |= xDiff > 0; 6302 } else { 6303 isExpanding |= xDiff < 0; 6304 } 6305 6306 if (mTextView.getHorizontallyScrolling()) { 6307 if (positionNearEdgeOfScrollingView(x, atRtl) 6308 && ((isStartHandle() && mTextView.getScrollX() != 0) 6309 || (!isStartHandle() 6310 && mTextView.canScrollHorizontally(atRtl ? -1 : 1))) 6311 && ((isExpanding && ((isStartHandle() && offset < currentOffset) 6312 || (!isStartHandle() && offset > currentOffset))) 6313 || !isExpanding)) { 6314 // If we're expanding ensure that the offset is actually expanding compared to 6315 // the current offset, if the handle snapped to the word, the finger position 6316 // may be out of sync and we don't want the selection to jump back. 6317 mTouchWordDelta = 0.0f; 6318 final int nextOffset = (atRtl == isStartHandle()) 6319 ? layout.getOffsetToRightOf(mPreviousOffset) 6320 : layout.getOffsetToLeftOf(mPreviousOffset); 6321 positionAndAdjustForCrossingHandles(nextOffset, fromTouchScreen); 6322 return; 6323 } 6324 } 6325 6326 if (isExpanding) { 6327 // User is increasing the selection. 6328 int wordBoundary = isStartHandle() ? wordStart : wordEnd; 6329 final boolean snapToWord = (!mInWord 6330 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine)) 6331 && atRtl == isAtRtlRun(layout, wordBoundary); 6332 if (snapToWord) { 6333 // Sometimes words can be broken across lines (Chinese, hyphenation). 6334 // We still snap to the word boundary but we only use the letters on the 6335 // current line to determine if the user is far enough into the word to snap. 6336 if (getLineForOffset(layout, wordBoundary) != currLine) { 6337 wordBoundary = isStartHandle() 6338 ? layout.getLineStart(currLine) : layout.getLineEnd(currLine); 6339 } 6340 final int offsetThresholdToSnap = isStartHandle() 6341 ? wordEnd - ((wordEnd - wordBoundary) / 2) 6342 : wordStart + ((wordBoundary - wordStart) / 2); 6343 if (isStartHandle() 6344 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) { 6345 // User is far enough into the word or on a different line so we expand by 6346 // word. 6347 offset = wordStart; 6348 } else if (!isStartHandle() 6349 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) { 6350 // User is far enough into the word or on a different line so we expand by 6351 // word. 6352 offset = wordEnd; 6353 } else { 6354 offset = mPreviousOffset; 6355 } 6356 } 6357 if ((isStartHandle() && offset < initialOffset) 6358 || (!isStartHandle() && offset > initialOffset)) { 6359 final float adjustedX = getHorizontal(layout, offset); 6360 mTouchWordDelta = 6361 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX; 6362 } else { 6363 mTouchWordDelta = 0.0f; 6364 } 6365 positionCursor = true; 6366 } else { 6367 final int adjustedOffset = 6368 getOffsetAtCoordinate(layout, currLine, x - mTouchWordDelta); 6369 final boolean shrinking = isStartHandle() 6370 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine 6371 : adjustedOffset < mPreviousOffset || currLine < mPrevLine; 6372 if (shrinking) { 6373 // User is shrinking the selection. 6374 if (currLine != mPrevLine) { 6375 // We're on a different line, so we'll snap to word boundaries. 6376 offset = isStartHandle() ? wordStart : wordEnd; 6377 if ((isStartHandle() && offset < initialOffset) 6378 || (!isStartHandle() && offset > initialOffset)) { 6379 final float adjustedX = getHorizontal(layout, offset); 6380 mTouchWordDelta = 6381 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX; 6382 } else { 6383 mTouchWordDelta = 0.0f; 6384 } 6385 } else { 6386 offset = adjustedOffset; 6387 } 6388 positionCursor = true; 6389 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset) 6390 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) { 6391 // Handle has jumped to the word boundary, and the user is moving 6392 // their finger towards the handle, the delta should be updated. 6393 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) 6394 - getHorizontal(layout, mPreviousOffset); 6395 } 6396 } 6397 6398 if (positionCursor) { 6399 mPreviousLineTouched = currLine; 6400 positionAndAdjustForCrossingHandles(offset, fromTouchScreen); 6401 } 6402 mPrevX = x; 6403 } 6404 6405 @Override 6406 protected void positionAtCursorOffset(int offset, boolean forceUpdatePosition, 6407 boolean fromTouchScreen) { 6408 super.positionAtCursorOffset(offset, forceUpdatePosition, fromTouchScreen); 6409 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset); 6410 } 6411 6412 @Override 6413 public boolean onTouchEvent(MotionEvent event) { 6414 if (!mTextView.isFromPrimePointer(event, true)) { 6415 return true; 6416 } 6417 boolean superResult = super.onTouchEvent(event); 6418 6419 switch (event.getActionMasked()) { 6420 case MotionEvent.ACTION_DOWN: 6421 // Reset the touch word offset and x value when the user 6422 // re-engages the handle. 6423 mTouchWordDelta = 0.0f; 6424 mPrevX = UNSET_X_VALUE; 6425 updateMagnifier(event); 6426 break; 6427 6428 case MotionEvent.ACTION_MOVE: 6429 updateMagnifier(event); 6430 break; 6431 6432 case MotionEvent.ACTION_UP: 6433 case MotionEvent.ACTION_CANCEL: 6434 dismissMagnifier(); 6435 break; 6436 } 6437 6438 return superResult; 6439 } 6440 6441 private void positionAndAdjustForCrossingHandles(int offset, boolean fromTouchScreen) { 6442 final int anotherHandleOffset = 6443 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart(); 6444 if ((isStartHandle() && offset >= anotherHandleOffset) 6445 || (!isStartHandle() && offset <= anotherHandleOffset)) { 6446 mTouchWordDelta = 0.0f; 6447 final Layout layout = mTextView.getLayout(); 6448 if (layout != null && offset != anotherHandleOffset) { 6449 final float horiz = getHorizontal(layout, offset); 6450 final float anotherHandleHoriz = getHorizontal(layout, anotherHandleOffset, 6451 !isStartHandle()); 6452 final float currentHoriz = getHorizontal(layout, mPreviousOffset); 6453 if (currentHoriz < anotherHandleHoriz && horiz < anotherHandleHoriz 6454 || currentHoriz > anotherHandleHoriz && horiz > anotherHandleHoriz) { 6455 // This handle passes another one as it crossed a direction boundary. 6456 // Don't minimize the selection, but keep the handle at the run boundary. 6457 final int currentOffset = getCurrentCursorOffset(); 6458 final int offsetToGetRunRange = isStartHandle() 6459 ? currentOffset : Math.max(currentOffset - 1, 0); 6460 final long range = layout.getRunRange(mTextView.originalToTransformed( 6461 offsetToGetRunRange, OffsetMapping.MAP_STRATEGY_CURSOR)); 6462 if (isStartHandle()) { 6463 offset = TextUtils.unpackRangeStartFromLong(range); 6464 } else { 6465 offset = TextUtils.unpackRangeEndFromLong(range); 6466 } 6467 offset = mTextView.transformedToOriginal(offset, 6468 OffsetMapping.MAP_STRATEGY_CURSOR); 6469 positionAtCursorOffset(offset, false, fromTouchScreen); 6470 return; 6471 } 6472 } 6473 // Handles can not cross and selection is at least one character. 6474 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle()); 6475 } 6476 positionAtCursorOffset(offset, false, fromTouchScreen); 6477 } 6478 6479 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) { 6480 mTextView.getLocationOnScreen(mTextViewLocation); 6481 boolean nearEdge; 6482 if (atRtl == isStartHandle()) { 6483 int rightEdge = mTextViewLocation[0] + mTextView.getWidth() 6484 - mTextView.getPaddingRight(); 6485 nearEdge = x > rightEdge - mTextViewEdgeSlop; 6486 } else { 6487 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft(); 6488 nearEdge = x < leftEdge + mTextViewEdgeSlop; 6489 } 6490 return nearEdge; 6491 } 6492 6493 @Override 6494 protected boolean isAtRtlRun(@NonNull Layout layout, int offset) { 6495 final int transformedOffset = 6496 mTextView.transformedToOriginal(offset, OffsetMapping.MAP_STRATEGY_CHARACTER); 6497 final int offsetToCheck = isStartHandle() ? transformedOffset 6498 : Math.max(transformedOffset - 1, 0); 6499 return layout.isRtlCharAt(offsetToCheck); 6500 } 6501 6502 @Override 6503 public float getHorizontal(@NonNull Layout layout, int offset) { 6504 return getHorizontal(layout, offset, isStartHandle()); 6505 } 6506 6507 private float getHorizontal(@NonNull Layout layout, int offset, boolean startHandle) { 6508 final int offsetTransformed = mTextView.originalToTransformed(offset, 6509 OffsetMapping.MAP_STRATEGY_CURSOR); 6510 final int line = layout.getLineForOffset(offsetTransformed); 6511 final int offsetToCheck = 6512 startHandle ? offsetTransformed : Math.max(offsetTransformed - 1, 0); 6513 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); 6514 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; 6515 if (isRtlChar != isRtlParagraph) { 6516 return layout.getSecondaryHorizontal(offsetTransformed); 6517 } 6518 return layout.getPrimaryHorizontal(offsetTransformed); 6519 } 6520 6521 @Override 6522 protected int getOffsetAtCoordinate(@NonNull Layout layout, int line, float x) { 6523 final float localX = mTextView.convertToLocalHorizontalCoordinate(x); 6524 final int primaryOffset = layout.getOffsetForHorizontal(line, localX, true); 6525 if (!layout.isLevelBoundary(primaryOffset)) { 6526 return mTextView.transformedToOriginal(primaryOffset, 6527 OffsetMapping.MAP_STRATEGY_CURSOR); 6528 } 6529 final int secondaryOffset = layout.getOffsetForHorizontal(line, localX, false); 6530 final int currentOffset = mTextView.originalToTransformed(getCurrentCursorOffset(), 6531 OffsetMapping.MAP_STRATEGY_CURSOR); 6532 final int primaryDiff = Math.abs(primaryOffset - currentOffset); 6533 final int secondaryDiff = Math.abs(secondaryOffset - currentOffset); 6534 final int offset; 6535 if (primaryDiff < secondaryDiff) { 6536 offset = primaryOffset; 6537 } else if (primaryDiff > secondaryDiff) { 6538 offset = secondaryOffset; 6539 } else { 6540 final int offsetToCheck = isStartHandle() 6541 ? currentOffset : Math.max(currentOffset - 1, 0); 6542 final boolean isRtlChar = layout.isRtlCharAt(offsetToCheck); 6543 final boolean isRtlParagraph = layout.getParagraphDirection(line) == -1; 6544 offset = (isRtlChar == isRtlParagraph) ? primaryOffset : secondaryOffset; 6545 } 6546 return mTextView.transformedToOriginal(offset, OffsetMapping.MAP_STRATEGY_CURSOR); 6547 } 6548 6549 @MagnifierHandleTrigger 6550 protected int getMagnifierHandleTrigger() { 6551 return isStartHandle() 6552 ? MagnifierHandleTrigger.SELECTION_START 6553 : MagnifierHandleTrigger.SELECTION_END; 6554 } 6555 } 6556 6557 @VisibleForTesting 6558 public void setLineChangeSlopMinMaxForTesting(final int min, final int max) { 6559 mLineChangeSlopMin = min; 6560 mLineChangeSlopMax = max; 6561 } 6562 6563 @VisibleForTesting 6564 public int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) { 6565 final int trueLine = mTextView.getLineAtCoordinate(y); 6566 if (layout == null || prevLine >= layout.getLineCount() 6567 || layout.getLineCount() <= 0 || prevLine < 0) { 6568 // Invalid parameters, just return whatever line is at y. 6569 return trueLine; 6570 } 6571 6572 if (Math.abs(trueLine - prevLine) >= 2) { 6573 // Only stick to lines if we're within a line of the previous selection. 6574 return trueLine; 6575 } 6576 6577 final int lineHeight = mTextView.getLineHeight(); 6578 int slop = (int)(mLineSlopRatio * lineHeight); 6579 slop = Math.max(mLineChangeSlopMin, 6580 Math.min(mLineChangeSlopMax, lineHeight + slop)) - lineHeight; 6581 slop = Math.max(0, slop); 6582 6583 final float verticalOffset = mTextView.viewportToContentVerticalOffset(); 6584 if (trueLine > prevLine && y >= layout.getLineBottom(prevLine) + slop + verticalOffset) { 6585 return trueLine; 6586 } 6587 if (trueLine < prevLine && y <= layout.getLineTop(prevLine) - slop + verticalOffset) { 6588 return trueLine; 6589 } 6590 return prevLine; 6591 } 6592 6593 /** 6594 * A CursorController instance can be used to control a cursor in the text. 6595 */ 6596 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { 6597 /** 6598 * Makes the cursor controller visible on screen. 6599 * See also {@link #hide()}. 6600 */ 6601 public void show(); 6602 6603 /** 6604 * Hide the cursor controller from screen. 6605 * See also {@link #show()}. 6606 */ 6607 public void hide(); 6608 6609 /** 6610 * Called when the view is detached from window. Perform house keeping task, such as 6611 * stopping Runnable thread that would otherwise keep a reference on the context, thus 6612 * preventing the activity from being recycled. 6613 */ 6614 public void onDetached(); 6615 6616 public boolean isCursorBeingModified(); 6617 6618 public boolean isActive(); 6619 } 6620 loadCursorDrawable()6621 void loadCursorDrawable() { 6622 if (mDrawableForCursor == null) { 6623 mDrawableForCursor = mTextView.getTextCursorDrawable(); 6624 } 6625 } 6626 6627 /** Controller for the insertion cursor. */ 6628 @VisibleForTesting 6629 public class InsertionPointCursorController implements CursorController { 6630 private InsertionHandleView mHandle; 6631 // Tracks whether the cursor is currently being dragged. 6632 private boolean mIsDraggingCursor; 6633 // During a drag, tracks whether the user's finger has adjusted to be over the handle rather 6634 // than the cursor bar. 6635 private boolean mIsTouchSnappedToHandleDuringDrag; 6636 // During a drag, tracks the line of text where the cursor was last positioned. 6637 private int mPrevLineDuringDrag; 6638 onTouchEvent(MotionEvent event)6639 public void onTouchEvent(MotionEvent event) { 6640 if (hasSelectionController() && getSelectionController().isCursorBeingModified()) { 6641 return; 6642 } 6643 switch (event.getActionMasked()) { 6644 case MotionEvent.ACTION_MOVE: 6645 if (event.isFromSource(InputDevice.SOURCE_MOUSE) 6646 || (mTextView.isAutoHandwritingEnabled() && isFromStylus(event))) { 6647 break; 6648 } 6649 if (mIsDraggingCursor) { 6650 performCursorDrag(event); 6651 } else if (mFlagCursorDragFromAnywhereEnabled 6652 && mTextView.getLayout() != null 6653 && mTextView.isFocused() 6654 && mTouchState.isMovedEnoughForDrag() 6655 && (mTouchState.getInitialDragDirectionXYRatio() 6656 > mCursorDragDirectionMinXYRatio || mTouchState.isOnHandle())) { 6657 startCursorDrag(event); 6658 } 6659 break; 6660 case MotionEvent.ACTION_UP: 6661 case MotionEvent.ACTION_CANCEL: 6662 if (mIsDraggingCursor) { 6663 endCursorDrag(event); 6664 } 6665 break; 6666 } 6667 } 6668 isFromStylus(MotionEvent motionEvent)6669 private boolean isFromStylus(MotionEvent motionEvent) { 6670 final int pointerIndex = motionEvent.getActionIndex(); 6671 return motionEvent.getToolType(pointerIndex) == MotionEvent.TOOL_TYPE_STYLUS; 6672 } 6673 positionCursorDuringDrag(MotionEvent event)6674 private void positionCursorDuringDrag(MotionEvent event) { 6675 mPrevLineDuringDrag = getLineDuringDrag(event); 6676 int offset = mTextView.getOffsetAtCoordinate(mPrevLineDuringDrag, event.getX()); 6677 int oldSelectionStart = mTextView.getSelectionStart(); 6678 int oldSelectionEnd = mTextView.getSelectionEnd(); 6679 if (offset == oldSelectionStart && offset == oldSelectionEnd) { 6680 return; 6681 } 6682 Selection.setSelection((Spannable) mTextView.getText(), offset); 6683 updateCursorPosition(); 6684 if (mHapticTextHandleEnabled) { 6685 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); 6686 } 6687 } 6688 6689 /** 6690 * Returns the line where the cursor should be positioned during a cursor drag. Rather than 6691 * simply returning the line directly at the touch position, this function has the following 6692 * additional logic: 6693 * 1) Apply some slop to avoid switching lines if the touch moves just slightly off the 6694 * current line. 6695 * 2) Allow the user's finger to slide down and "snap" to the handle to provide better 6696 * visibility of the cursor and text. 6697 */ getLineDuringDrag(MotionEvent event)6698 private int getLineDuringDrag(MotionEvent event) { 6699 final Layout layout = mTextView.getLayout(); 6700 if (mPrevLineDuringDrag == UNSET_LINE) { 6701 return getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, event.getY()); 6702 } 6703 // In case of touch through on handle (when isOnHandle() returns true), event.getY() 6704 // returns the midpoint of the cursor vertical bar, while event.getRawY() returns the 6705 // finger location on the screen. See {@link InsertionHandleView#touchThrough}. 6706 final float fingerY = mTouchState.isOnHandle() 6707 ? event.getRawY() - mTextView.getLocationOnScreen()[1] 6708 : event.getY(); 6709 final float cursorY = fingerY - getHandle().getIdealFingerToCursorOffset(); 6710 int line = getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, cursorY); 6711 if (mIsTouchSnappedToHandleDuringDrag) { 6712 // Just returns the line hit by cursor Y when already snapped. 6713 return line; 6714 } 6715 if (line < mPrevLineDuringDrag) { 6716 // The cursor Y aims too high & not yet snapped, check the finger Y. 6717 // If finger Y is moving downwards, don't jump to lower line (until snap). 6718 // If finger Y is moving upwards, can jump to upper line. 6719 return Math.min(mPrevLineDuringDrag, 6720 getCurrentLineAdjustedForSlop(layout, mPrevLineDuringDrag, fingerY)); 6721 } 6722 // The cursor Y aims not too high, so snap! 6723 mIsTouchSnappedToHandleDuringDrag = true; 6724 if (TextView.DEBUG_CURSOR) { 6725 logCursor("InsertionPointCursorController", 6726 "snapped touch to handle: fingerY=%d, cursorY=%d, mLastLine=%d, line=%d", 6727 (int) fingerY, (int) cursorY, mPrevLineDuringDrag, line); 6728 } 6729 return line; 6730 } 6731 startCursorDrag(MotionEvent event)6732 private void startCursorDrag(MotionEvent event) { 6733 if (TextView.DEBUG_CURSOR) { 6734 logCursor("InsertionPointCursorController", "start cursor drag"); 6735 } 6736 mIsDraggingCursor = true; 6737 mIsTouchSnappedToHandleDuringDrag = false; 6738 mPrevLineDuringDrag = UNSET_LINE; 6739 // We don't want the parent scroll/long-press handlers to take over while dragging. 6740 mTextView.getParent().requestDisallowInterceptTouchEvent(true); 6741 mTextView.cancelLongPress(); 6742 // Update the cursor position. 6743 positionCursorDuringDrag(event); 6744 // Show the cursor handle and magnifier. 6745 show(); 6746 getHandle().removeHiderCallback(); 6747 getHandle().updateMagnifier(event); 6748 // TODO(b/146555651): Figure out if suspendBlink() should be called here. 6749 } 6750 performCursorDrag(MotionEvent event)6751 private void performCursorDrag(MotionEvent event) { 6752 positionCursorDuringDrag(event); 6753 getHandle().updateMagnifier(event); 6754 } 6755 endCursorDrag(MotionEvent event)6756 private void endCursorDrag(MotionEvent event) { 6757 if (TextView.DEBUG_CURSOR) { 6758 logCursor("InsertionPointCursorController", "end cursor drag"); 6759 } 6760 mIsDraggingCursor = false; 6761 mIsTouchSnappedToHandleDuringDrag = false; 6762 mPrevLineDuringDrag = UNSET_LINE; 6763 // Hide the magnifier and set the handle to be hidden after a delay. 6764 getHandle().dismissMagnifier(); 6765 getHandle().hideAfterDelay(); 6766 // We're no longer dragging, so let the parent receive events. 6767 mTextView.getParent().requestDisallowInterceptTouchEvent(false); 6768 } 6769 show()6770 public void show() { 6771 getHandle().show(); 6772 6773 final long durationSinceCutOrCopy = 6774 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime; 6775 6776 if (mInsertionActionModeRunnable != null) { 6777 if (mIsDraggingCursor 6778 || mTouchState.isMultiTap() 6779 || isCursorInsideEasyCorrectionSpan()) { 6780 // Cancel the runnable for showing the floating toolbar. 6781 mTextView.removeCallbacks(mInsertionActionModeRunnable); 6782 } 6783 } 6784 6785 // If the user recently performed a Cut or Copy action, we want to show the floating 6786 // toolbar even for a single tap. 6787 if (!mIsDraggingCursor 6788 && !mTouchState.isMultiTap() 6789 && !isCursorInsideEasyCorrectionSpan() 6790 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION_MS)) { 6791 if (mTextActionMode == null) { 6792 if (mInsertionActionModeRunnable == null) { 6793 mInsertionActionModeRunnable = new Runnable() { 6794 @Override 6795 public void run() { 6796 startInsertionActionMode(); 6797 } 6798 }; 6799 } 6800 mTextView.postDelayed( 6801 mInsertionActionModeRunnable, 6802 ViewConfiguration.getDoubleTapTimeout() + 1); 6803 } 6804 } 6805 6806 if (!mIsDraggingCursor) { 6807 getHandle().hideAfterDelay(); 6808 } 6809 6810 if (mSelectionModifierCursorController != null) { 6811 mSelectionModifierCursorController.hide(); 6812 } 6813 } 6814 hide()6815 public void hide() { 6816 if (mHandle != null) { 6817 mHandle.hide(); 6818 } 6819 } 6820 onTouchModeChanged(boolean isInTouchMode)6821 public void onTouchModeChanged(boolean isInTouchMode) { 6822 if (!isInTouchMode) { 6823 hide(); 6824 } 6825 } 6826 getHandle()6827 public InsertionHandleView getHandle() { 6828 if (mHandle == null) { 6829 loadHandleDrawables(false /* overwrite */); 6830 mHandle = new InsertionHandleView(mSelectHandleCenter); 6831 } 6832 return mHandle; 6833 } 6834 reloadHandleDrawable()6835 private void reloadHandleDrawable() { 6836 if (mHandle == null) { 6837 // No need to reload, the potentially new drawable will 6838 // be used when the handle is created. 6839 return; 6840 } 6841 mHandle.setDrawables(mSelectHandleCenter, mSelectHandleCenter); 6842 } 6843 6844 @Override onDetached()6845 public void onDetached() { 6846 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 6847 observer.removeOnTouchModeChangeListener(this); 6848 6849 if (mHandle != null) mHandle.onDetached(); 6850 } 6851 6852 @Override isCursorBeingModified()6853 public boolean isCursorBeingModified() { 6854 return mIsDraggingCursor || (mHandle != null && mHandle.isDragging()); 6855 } 6856 6857 @Override isActive()6858 public boolean isActive() { 6859 return mHandle != null && mHandle.isShowing(); 6860 } 6861 invalidateHandle()6862 public void invalidateHandle() { 6863 if (mHandle != null) { 6864 mHandle.invalidate(); 6865 } 6866 } 6867 } 6868 6869 /** Controller for selection. */ 6870 @VisibleForTesting 6871 public class SelectionModifierCursorController implements CursorController { 6872 // The cursor controller handles, lazily created when shown. 6873 private SelectionHandleView mStartHandle; 6874 private SelectionHandleView mEndHandle; 6875 // The offsets of that last touch down event. Remembered to start selection there. 6876 private int mMinTouchOffset, mMaxTouchOffset; 6877 6878 private boolean mGestureStayedInTapRegion; 6879 6880 // Where the user first starts the drag motion. 6881 private int mStartOffset = -1; 6882 6883 private boolean mHaventMovedEnoughToStartDrag; 6884 // The line that a selection happened most recently with the drag accelerator. 6885 private int mLineSelectionIsOn = -1; 6886 // Whether the drag accelerator has selected past the initial line. 6887 private boolean mSwitchedLines = false; 6888 6889 // Indicates the drag accelerator mode that the user is currently using. 6890 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE; 6891 // Drag accelerator is inactive. 6892 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0; 6893 // Character based selection by dragging. Only for mouse. 6894 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1; 6895 // Word based selection by dragging. Enabled after long pressing or double tapping. 6896 private static final int DRAG_ACCELERATOR_MODE_WORD = 2; 6897 // Paragraph based selection by dragging. Enabled after mouse triple click. 6898 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3; 6899 SelectionModifierCursorController()6900 SelectionModifierCursorController() { 6901 resetTouchOffsets(); 6902 } 6903 show()6904 public void show() { 6905 if (mTextView.isInBatchEditMode()) { 6906 return; 6907 } 6908 loadHandleDrawables(false /* overwrite */); 6909 initHandles(); 6910 } 6911 initHandles()6912 private void initHandles() { 6913 // Lazy object creation has to be done before updatePosition() is called. 6914 if (mStartHandle == null) { 6915 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight, 6916 com.android.internal.R.id.selection_start_handle, 6917 HANDLE_TYPE_SELECTION_START); 6918 } 6919 if (mEndHandle == null) { 6920 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft, 6921 com.android.internal.R.id.selection_end_handle, 6922 HANDLE_TYPE_SELECTION_END); 6923 } 6924 6925 mStartHandle.show(); 6926 mEndHandle.show(); 6927 6928 hideInsertionPointCursorController(); 6929 } 6930 reloadHandleDrawables()6931 private void reloadHandleDrawables() { 6932 if (mStartHandle == null) { 6933 // No need to reload, the potentially new drawables will 6934 // be used when the handles are created. 6935 return; 6936 } 6937 mStartHandle.setDrawables(mSelectHandleLeft, mSelectHandleRight); 6938 mEndHandle.setDrawables(mSelectHandleRight, mSelectHandleLeft); 6939 } 6940 hide()6941 public void hide() { 6942 if (mStartHandle != null) mStartHandle.hide(); 6943 if (mEndHandle != null) mEndHandle.hide(); 6944 } 6945 enterDrag(int dragAcceleratorMode)6946 public void enterDrag(int dragAcceleratorMode) { 6947 if (TextView.DEBUG_CURSOR) { 6948 logCursor("SelectionModifierCursorController: enterDrag", 6949 "starting selection drag: mode=%s", dragAcceleratorMode); 6950 } 6951 6952 // Just need to init the handles / hide insertion cursor. 6953 show(); 6954 mDragAcceleratorMode = dragAcceleratorMode; 6955 // Start location of selection. 6956 mStartOffset = mTextView.getOffsetForPosition(mTouchState.getLastDownX(), 6957 mTouchState.getLastDownY()); 6958 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mTouchState.getLastDownY()); 6959 // Don't show the handles until user has lifted finger. 6960 hide(); 6961 6962 // This stops scrolling parents from intercepting the touch event, allowing 6963 // the user to continue dragging across the screen to select text; TextView will 6964 // scroll as necessary. 6965 mTextView.getParent().requestDisallowInterceptTouchEvent(true); 6966 mTextView.cancelLongPress(); 6967 } 6968 onTouchEvent(MotionEvent event)6969 public void onTouchEvent(MotionEvent event) { 6970 // This is done even when the View does not have focus, so that long presses can start 6971 // selection and tap can move cursor from this tap position. 6972 final float eventX = event.getX(); 6973 final float eventY = event.getY(); 6974 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 6975 switch (event.getActionMasked()) { 6976 case MotionEvent.ACTION_DOWN: 6977 if (extractedTextModeWillBeStarted()) { 6978 // Prevent duplicating the selection handles until the mode starts. 6979 hide(); 6980 } else { 6981 // Remember finger down position, to be able to start selection from there. 6982 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition( 6983 eventX, eventY); 6984 6985 // Double tap detection 6986 if (mGestureStayedInTapRegion 6987 && mTouchState.isMultiTapInSameArea() 6988 && (isMouse || isPositionOnText(eventX, eventY) 6989 || mTouchState.isOnHandle())) { 6990 if (TextView.DEBUG_CURSOR) { 6991 logCursor("SelectionModifierCursorController: onTouchEvent", 6992 "ACTION_DOWN: select and start drag"); 6993 } 6994 if (mTouchState.isDoubleTap()) { 6995 selectCurrentWordAndStartDrag(); 6996 } else if (mTouchState.isTripleClick()) { 6997 selectCurrentParagraphAndStartDrag(); 6998 } 6999 mDiscardNextActionUp = true; 7000 } 7001 mGestureStayedInTapRegion = true; 7002 mHaventMovedEnoughToStartDrag = true; 7003 } 7004 break; 7005 7006 case MotionEvent.ACTION_POINTER_DOWN: 7007 case MotionEvent.ACTION_POINTER_UP: 7008 // Handle multi-point gestures. Keep min and max offset positions. 7009 // Only activated for devices that correctly handle multi-touch. 7010 if (mTextView.getContext().getPackageManager().hasSystemFeature( 7011 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { 7012 updateMinAndMaxOffsets(event); 7013 } 7014 break; 7015 7016 case MotionEvent.ACTION_MOVE: 7017 if (mGestureStayedInTapRegion) { 7018 final ViewConfiguration viewConfig = ViewConfiguration.get( 7019 mTextView.getContext()); 7020 mGestureStayedInTapRegion = EditorTouchState.isDistanceWithin( 7021 mTouchState.getLastDownX(), mTouchState.getLastDownY(), 7022 eventX, eventY, viewConfig.getScaledDoubleTapTouchSlop()); 7023 } 7024 7025 if (mHaventMovedEnoughToStartDrag) { 7026 mHaventMovedEnoughToStartDrag = !mTouchState.isMovedEnoughForDrag(); 7027 } 7028 7029 if (isMouse && !isDragAcceleratorActive()) { 7030 final int offset = mTextView.getOffsetForPosition(eventX, eventY); 7031 if (mTextView.hasSelection() 7032 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset) 7033 && offset >= mTextView.getSelectionStart() 7034 && offset <= mTextView.getSelectionEnd()) { 7035 startDragAndDrop(); 7036 break; 7037 } 7038 7039 if (mStartOffset != offset) { 7040 // Start character based drag accelerator. 7041 stopTextActionMode(); 7042 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER); 7043 mDiscardNextActionUp = true; 7044 mHaventMovedEnoughToStartDrag = false; 7045 } 7046 } 7047 7048 if (mStartHandle != null && mStartHandle.isShowing()) { 7049 // Don't do the drag if the handles are showing already. 7050 break; 7051 } 7052 7053 updateSelection(event); 7054 if (mTextView.hasSelection() && mEndHandle != null && 7055 isDragAcceleratorActive() 7056 ) { 7057 mEndHandle.updateMagnifier(event); 7058 } 7059 break; 7060 7061 case MotionEvent.ACTION_UP: 7062 if (TextView.DEBUG_CURSOR) { 7063 logCursor("SelectionModifierCursorController: onTouchEvent", "ACTION_UP"); 7064 } 7065 if (mEndHandle != null) { 7066 mEndHandle.dismissMagnifier(); 7067 } 7068 if (!isDragAcceleratorActive()) { 7069 break; 7070 } 7071 updateSelection(event); 7072 7073 // No longer dragging to select text, let the parent intercept events. 7074 mTextView.getParent().requestDisallowInterceptTouchEvent(false); 7075 7076 // No longer the first dragging motion, reset. 7077 resetDragAcceleratorState(); 7078 7079 if (mTextView.hasSelection()) { 7080 // Drag selection should not be adjusted by the text classifier. 7081 startSelectionActionModeAsync(mHaventMovedEnoughToStartDrag); 7082 } 7083 break; 7084 } 7085 } 7086 updateSelection(MotionEvent event)7087 private void updateSelection(MotionEvent event) { 7088 if (mTextView.getLayout() != null) { 7089 switch (mDragAcceleratorMode) { 7090 case DRAG_ACCELERATOR_MODE_CHARACTER: 7091 updateCharacterBasedSelection(event); 7092 break; 7093 case DRAG_ACCELERATOR_MODE_WORD: 7094 updateWordBasedSelection(event); 7095 break; 7096 case DRAG_ACCELERATOR_MODE_PARAGRAPH: 7097 updateParagraphBasedSelection(event); 7098 break; 7099 } 7100 } 7101 } 7102 7103 /** 7104 * If the TextView allows text selection, selects the current paragraph and starts a drag. 7105 * 7106 * @return true if the drag was started. 7107 */ selectCurrentParagraphAndStartDrag()7108 private boolean selectCurrentParagraphAndStartDrag() { 7109 if (mInsertionActionModeRunnable != null) { 7110 mTextView.removeCallbacks(mInsertionActionModeRunnable); 7111 } 7112 stopTextActionMode(); 7113 if (!selectCurrentParagraph()) { 7114 return false; 7115 } 7116 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH); 7117 return true; 7118 } 7119 updateCharacterBasedSelection(MotionEvent event)7120 private void updateCharacterBasedSelection(MotionEvent event) { 7121 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 7122 updateSelectionInternal(mStartOffset, offset, 7123 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); 7124 } 7125 updateWordBasedSelection(MotionEvent event)7126 private void updateWordBasedSelection(MotionEvent event) { 7127 if (mHaventMovedEnoughToStartDrag) { 7128 return; 7129 } 7130 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 7131 final ViewConfiguration viewConfig = ViewConfiguration.get( 7132 mTextView.getContext()); 7133 final float eventX = event.getX(); 7134 final float eventY = event.getY(); 7135 final int currLine; 7136 if (isMouse) { 7137 // No need to offset the y coordinate for mouse input. 7138 currLine = mTextView.getLineAtCoordinate(eventY); 7139 } else { 7140 float y = eventY; 7141 if (mSwitchedLines) { 7142 // Offset the finger by the same vertical offset as the handles. 7143 // This improves visibility of the content being selected by 7144 // shifting the finger below the content, this is applied once 7145 // the user has switched lines. 7146 final int touchSlop = viewConfig.getScaledTouchSlop(); 7147 final float fingerOffset = (mStartHandle != null) 7148 ? mStartHandle.getIdealVerticalOffset() 7149 : touchSlop; 7150 y = eventY - fingerOffset; 7151 } 7152 7153 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn, 7154 y); 7155 if (!mSwitchedLines && currLine != mLineSelectionIsOn) { 7156 // Break early here, we want to offset the finger position from 7157 // the selection highlight, once the user moved their finger 7158 // to a different line we should apply the offset and *not* switch 7159 // lines until recomputing the position with the finger offset. 7160 mSwitchedLines = true; 7161 return; 7162 } 7163 } 7164 7165 int startOffset; 7166 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX); 7167 // Snap to word boundaries. 7168 if (mStartOffset < offset) { 7169 // Expanding with end handle. 7170 offset = getWordEnd(offset); 7171 startOffset = getWordStart(mStartOffset); 7172 } else { 7173 // Expanding with start handle. 7174 offset = getWordStart(offset); 7175 startOffset = getWordEnd(mStartOffset); 7176 if (startOffset == offset) { 7177 offset = getNextCursorOffset(offset, false); 7178 } 7179 } 7180 mLineSelectionIsOn = currLine; 7181 updateSelectionInternal(startOffset, offset, 7182 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); 7183 } 7184 updateParagraphBasedSelection(MotionEvent event)7185 private void updateParagraphBasedSelection(MotionEvent event) { 7186 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 7187 7188 final int start = Math.min(offset, mStartOffset); 7189 final int end = Math.max(offset, mStartOffset); 7190 final long paragraphsRange = getParagraphsRange(start, end); 7191 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange); 7192 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange); 7193 updateSelectionInternal(selectionStart, selectionEnd, 7194 event.isFromSource(InputDevice.SOURCE_TOUCHSCREEN)); 7195 } 7196 updateSelectionInternal(int selectionStart, int selectionEnd, boolean fromTouchScreen)7197 private void updateSelectionInternal(int selectionStart, int selectionEnd, 7198 boolean fromTouchScreen) { 7199 final boolean performHapticFeedback = fromTouchScreen && mHapticTextHandleEnabled 7200 && ((mTextView.getSelectionStart() != selectionStart) 7201 || (mTextView.getSelectionEnd() != selectionEnd)); 7202 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 7203 if (performHapticFeedback) { 7204 mTextView.performHapticFeedback(HapticFeedbackConstants.TEXT_HANDLE_MOVE); 7205 } 7206 } 7207 7208 /** 7209 * @param event 7210 */ updateMinAndMaxOffsets(MotionEvent event)7211 private void updateMinAndMaxOffsets(MotionEvent event) { 7212 int pointerCount = event.getPointerCount(); 7213 for (int index = 0; index < pointerCount; index++) { 7214 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index)); 7215 if (offset < mMinTouchOffset) mMinTouchOffset = offset; 7216 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; 7217 } 7218 } 7219 getMinTouchOffset()7220 public int getMinTouchOffset() { 7221 return mMinTouchOffset; 7222 } 7223 getMaxTouchOffset()7224 public int getMaxTouchOffset() { 7225 return mMaxTouchOffset; 7226 } 7227 resetTouchOffsets()7228 public void resetTouchOffsets() { 7229 mMinTouchOffset = mMaxTouchOffset = -1; 7230 resetDragAcceleratorState(); 7231 } 7232 resetDragAcceleratorState()7233 private void resetDragAcceleratorState() { 7234 mStartOffset = -1; 7235 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE; 7236 mSwitchedLines = false; 7237 final int selectionStart = mTextView.getSelectionStart(); 7238 final int selectionEnd = mTextView.getSelectionEnd(); 7239 if (selectionStart < 0 || selectionEnd < 0) { 7240 Selection.removeSelection((Spannable) mTextView.getText()); 7241 } else if (selectionStart > selectionEnd) { 7242 Selection.setSelection((Spannable) mTextView.getText(), 7243 selectionEnd, selectionStart); 7244 } 7245 } 7246 7247 /** 7248 * @return true iff this controller is currently used to move the selection start. 7249 */ isSelectionStartDragged()7250 public boolean isSelectionStartDragged() { 7251 return mStartHandle != null && mStartHandle.isDragging(); 7252 } 7253 7254 @Override isCursorBeingModified()7255 public boolean isCursorBeingModified() { 7256 return isDragAcceleratorActive() || isSelectionStartDragged() 7257 || (mEndHandle != null && mEndHandle.isDragging()); 7258 } 7259 7260 /** 7261 * @return true if the user is selecting text using the drag accelerator. 7262 */ isDragAcceleratorActive()7263 public boolean isDragAcceleratorActive() { 7264 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE; 7265 } 7266 onTouchModeChanged(boolean isInTouchMode)7267 public void onTouchModeChanged(boolean isInTouchMode) { 7268 if (!isInTouchMode) { 7269 hide(); 7270 } 7271 } 7272 7273 @Override onDetached()7274 public void onDetached() { 7275 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 7276 observer.removeOnTouchModeChangeListener(this); 7277 7278 if (mStartHandle != null) mStartHandle.onDetached(); 7279 if (mEndHandle != null) mEndHandle.onDetached(); 7280 } 7281 7282 @Override isActive()7283 public boolean isActive() { 7284 return mStartHandle != null && mStartHandle.isShowing(); 7285 } 7286 invalidateHandles()7287 public void invalidateHandles() { 7288 if (mStartHandle != null) { 7289 mStartHandle.invalidate(); 7290 } 7291 if (mEndHandle != null) { 7292 mEndHandle.invalidate(); 7293 } 7294 } 7295 } 7296 7297 /** 7298 * Loads the insertion and selection handle Drawables from TextView. If the handle 7299 * drawables are already loaded, do not overwrite them unless the method parameter 7300 * is set to true. This logic is required to avoid overwriting Drawables assigned 7301 * to mSelectHandle[Center/Left/Right] by developers using reflection, unless they 7302 * explicitly call the setters in TextView. 7303 * 7304 * @param overwrite whether to overwrite already existing nonnull Drawables 7305 */ loadHandleDrawables(final boolean overwrite)7306 void loadHandleDrawables(final boolean overwrite) { 7307 if (mSelectHandleCenter == null || overwrite) { 7308 mSelectHandleCenter = mTextView.getTextSelectHandle(); 7309 if (hasInsertionController()) { 7310 getInsertionController().reloadHandleDrawable(); 7311 } 7312 } 7313 7314 if (mSelectHandleLeft == null || mSelectHandleRight == null || overwrite) { 7315 mSelectHandleLeft = mTextView.getTextSelectHandleLeft(); 7316 mSelectHandleRight = mTextView.getTextSelectHandleRight(); 7317 if (hasSelectionController()) { 7318 getSelectionController().reloadHandleDrawables(); 7319 } 7320 } 7321 } 7322 7323 private class CorrectionHighlighter { 7324 private final Path mPath = new Path(); 7325 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 7326 private int mStart, mEnd; 7327 private long mFadingStartTime; 7328 private RectF mTempRectF; 7329 private static final int FADE_OUT_DURATION = 400; 7330 CorrectionHighlighter()7331 public CorrectionHighlighter() { 7332 mPaint.setCompatibilityScaling( 7333 mTextView.getResources().getCompatibilityInfo().applicationScale); 7334 mPaint.setStyle(Paint.Style.FILL); 7335 } 7336 highlight(CorrectionInfo info)7337 public void highlight(CorrectionInfo info) { 7338 mStart = info.getOffset(); 7339 mEnd = mStart + info.getNewText().length(); 7340 mFadingStartTime = SystemClock.uptimeMillis(); 7341 7342 if (mStart < 0 || mEnd < 0) { 7343 stopAnimation(); 7344 } 7345 } 7346 draw(Canvas canvas, int cursorOffsetVertical)7347 public void draw(Canvas canvas, int cursorOffsetVertical) { 7348 if (updatePath() && updatePaint()) { 7349 if (cursorOffsetVertical != 0) { 7350 canvas.translate(0, cursorOffsetVertical); 7351 } 7352 7353 canvas.drawPath(mPath, mPaint); 7354 7355 if (cursorOffsetVertical != 0) { 7356 canvas.translate(0, -cursorOffsetVertical); 7357 } 7358 invalidate(true); // TODO invalidate cursor region only 7359 } else { 7360 stopAnimation(); 7361 invalidate(false); // TODO invalidate cursor region only 7362 } 7363 } 7364 updatePaint()7365 private boolean updatePaint() { 7366 final long duration = SystemClock.uptimeMillis() - mFadingStartTime; 7367 if (duration > FADE_OUT_DURATION) return false; 7368 7369 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; 7370 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor); 7371 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) 7372 + ((int) (highlightColorAlpha * coef) << 24); 7373 mPaint.setColor(color); 7374 return true; 7375 } 7376 updatePath()7377 private boolean updatePath() { 7378 final Layout layout = mTextView.getLayout(); 7379 if (layout == null) return false; 7380 7381 // Update in case text is edited while the animation is run 7382 final int length = mTextView.getText().length(); 7383 int start = Math.min(length, mStart); 7384 int end = Math.min(length, mEnd); 7385 7386 mPath.reset(); 7387 layout.getSelectionPath( 7388 mTextView.originalToTransformed(start, OffsetMapping.MAP_STRATEGY_CHARACTER), 7389 mTextView.originalToTransformed(end, OffsetMapping.MAP_STRATEGY_CHARACTER), 7390 mPath); 7391 return true; 7392 } 7393 invalidate(boolean delayed)7394 private void invalidate(boolean delayed) { 7395 if (mTextView.getLayout() == null) return; 7396 7397 if (mTempRectF == null) mTempRectF = new RectF(); 7398 mPath.computeBounds(mTempRectF, false); 7399 7400 int left = mTextView.getCompoundPaddingLeft(); 7401 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true); 7402 7403 if (delayed) { 7404 mTextView.postInvalidateOnAnimation( 7405 left + (int) mTempRectF.left, top + (int) mTempRectF.top, 7406 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom); 7407 } else { 7408 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top, 7409 (int) mTempRectF.right, (int) mTempRectF.bottom); 7410 } 7411 } 7412 stopAnimation()7413 private void stopAnimation() { 7414 Editor.this.mCorrectionHighlighter = null; 7415 } 7416 } 7417 7418 private static class ErrorPopup extends PopupWindow { 7419 private boolean mAbove = false; 7420 private final TextView mView; 7421 private int mPopupInlineErrorBackgroundId = 0; 7422 private int mPopupInlineErrorAboveBackgroundId = 0; 7423 ErrorPopup(TextView v, int width, int height)7424 ErrorPopup(TextView v, int width, int height) { 7425 super(v, width, height); 7426 mView = v; 7427 // Make sure the TextView has a background set as it will be used the first time it is 7428 // shown and positioned. Initialized with below background, which should have 7429 // dimensions identical to the above version for this to work (and is more likely). 7430 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 7431 com.android.internal.R.styleable.Theme_errorMessageBackground); 7432 mView.setBackgroundResource(mPopupInlineErrorBackgroundId); 7433 } 7434 fixDirection(boolean above)7435 void fixDirection(boolean above) { 7436 mAbove = above; 7437 7438 if (above) { 7439 mPopupInlineErrorAboveBackgroundId = 7440 getResourceId(mPopupInlineErrorAboveBackgroundId, 7441 com.android.internal.R.styleable.Theme_errorMessageAboveBackground); 7442 } else { 7443 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 7444 com.android.internal.R.styleable.Theme_errorMessageBackground); 7445 } 7446 7447 mView.setBackgroundResource( 7448 above ? mPopupInlineErrorAboveBackgroundId : mPopupInlineErrorBackgroundId); 7449 } 7450 getResourceId(int currentId, int index)7451 private int getResourceId(int currentId, int index) { 7452 if (currentId == 0) { 7453 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( 7454 R.styleable.Theme); 7455 currentId = styledAttributes.getResourceId(index, 0); 7456 styledAttributes.recycle(); 7457 } 7458 return currentId; 7459 } 7460 7461 @Override update(int x, int y, int w, int h, boolean force)7462 public void update(int x, int y, int w, int h, boolean force) { 7463 super.update(x, y, w, h, force); 7464 7465 boolean above = isAboveAnchor(); 7466 if (above != mAbove) { 7467 fixDirection(above); 7468 } 7469 } 7470 } 7471 7472 static class InputContentType { 7473 int imeOptions = EditorInfo.IME_NULL; 7474 @UnsupportedAppUsage 7475 String privateImeOptions; 7476 CharSequence imeActionLabel; 7477 int imeActionId; 7478 Bundle extras; 7479 OnEditorActionListener onEditorActionListener; 7480 boolean enterDown; 7481 LocaleList imeHintLocales; 7482 } 7483 7484 static class InputMethodState { 7485 ExtractedTextRequest mExtractedTextRequest; 7486 final ExtractedText mExtractedText = new ExtractedText(); 7487 int mBatchEditNesting; 7488 boolean mCursorChanged; 7489 boolean mSelectionModeChanged; 7490 boolean mContentChanged; 7491 int mChangedStart, mChangedEnd, mChangedDelta; 7492 @InputConnection.CursorUpdateMode 7493 int mUpdateCursorAnchorInfoMode; 7494 @InputConnection.CursorUpdateFilter 7495 int mUpdateCursorAnchorInfoFilter; 7496 } 7497 7498 /** 7499 * @return True iff (start, end) is a valid range within the text. 7500 */ isValidRange(CharSequence text, int start, int end)7501 private static boolean isValidRange(CharSequence text, int start, int end) { 7502 return 0 <= start && start <= end && end <= text.length(); 7503 } 7504 7505 /** 7506 * An InputFilter that monitors text input to maintain undo history. It does not modify the 7507 * text being typed (and hence always returns null from the filter() method). 7508 * 7509 * TODO: Make this span aware. 7510 */ 7511 public static class UndoInputFilter implements InputFilter { 7512 private final Editor mEditor; 7513 7514 // Whether the current filter pass is directly caused by an end-user text edit. 7515 private boolean mIsUserEdit; 7516 7517 // Whether the text field is handling an IME composition. Must be parceled in case the user 7518 // rotates the screen during composition. 7519 private boolean mHasComposition; 7520 7521 // Whether the user is expanding or shortening the text 7522 private boolean mExpanding; 7523 7524 // Whether the previous edit operation was in the current batch edit. 7525 private boolean mPreviousOperationWasInSameBatchEdit; 7526 UndoInputFilter(Editor editor)7527 public UndoInputFilter(Editor editor) { 7528 mEditor = editor; 7529 } 7530 saveInstanceState(Parcel parcel)7531 public void saveInstanceState(Parcel parcel) { 7532 parcel.writeInt(mIsUserEdit ? 1 : 0); 7533 parcel.writeInt(mHasComposition ? 1 : 0); 7534 parcel.writeInt(mExpanding ? 1 : 0); 7535 parcel.writeInt(mPreviousOperationWasInSameBatchEdit ? 1 : 0); 7536 } 7537 restoreInstanceState(Parcel parcel)7538 public void restoreInstanceState(Parcel parcel) { 7539 mIsUserEdit = parcel.readInt() != 0; 7540 mHasComposition = parcel.readInt() != 0; 7541 mExpanding = parcel.readInt() != 0; 7542 mPreviousOperationWasInSameBatchEdit = parcel.readInt() != 0; 7543 } 7544 7545 /** 7546 * Signals that a user-triggered edit is starting. 7547 */ beginBatchEdit()7548 public void beginBatchEdit() { 7549 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit"); 7550 mIsUserEdit = true; 7551 } 7552 endBatchEdit()7553 public void endBatchEdit() { 7554 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit"); 7555 mIsUserEdit = false; 7556 mPreviousOperationWasInSameBatchEdit = false; 7557 } 7558 7559 @Override filter(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)7560 public CharSequence filter(CharSequence source, int start, int end, 7561 Spanned dest, int dstart, int dend) { 7562 if (DEBUG_UNDO) { 7563 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " 7564 + "dest=" + dest + " (" + dstart + "-" + dend + ")"); 7565 } 7566 7567 // Check to see if this edit should be tracked for undo. 7568 if (!canUndoEdit(source, start, end, dest, dstart, dend)) { 7569 return null; 7570 } 7571 7572 final boolean hadComposition = mHasComposition; 7573 mHasComposition = isComposition(source); 7574 final boolean wasExpanding = mExpanding; 7575 boolean shouldCreateSeparateState = false; 7576 if ((end - start) != (dend - dstart)) { 7577 mExpanding = (end - start) > (dend - dstart); 7578 if (hadComposition && mExpanding != wasExpanding) { 7579 shouldCreateSeparateState = true; 7580 } 7581 } 7582 7583 // Handle edit. 7584 handleEdit(source, start, end, dest, dstart, dend, shouldCreateSeparateState); 7585 return null; 7586 } 7587 freezeLastEdit()7588 void freezeLastEdit() { 7589 mEditor.mUndoManager.beginUpdate("Edit text"); 7590 EditOperation lastEdit = getLastEdit(); 7591 if (lastEdit != null) { 7592 lastEdit.mFrozen = true; 7593 } 7594 mEditor.mUndoManager.endUpdate(); 7595 } 7596 7597 @Retention(RetentionPolicy.SOURCE) 7598 @IntDef(prefix = { "MERGE_EDIT_MODE_" }, value = { 7599 MERGE_EDIT_MODE_FORCE_MERGE, 7600 MERGE_EDIT_MODE_NEVER_MERGE, 7601 MERGE_EDIT_MODE_NORMAL 7602 }) 7603 private @interface MergeMode {} 7604 private static final int MERGE_EDIT_MODE_FORCE_MERGE = 0; 7605 private static final int MERGE_EDIT_MODE_NEVER_MERGE = 1; 7606 /** Use {@link EditOperation#mergeWith} to merge */ 7607 private static final int MERGE_EDIT_MODE_NORMAL = 2; 7608 handleEdit(CharSequence source, int start, int end, Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState)7609 private void handleEdit(CharSequence source, int start, int end, 7610 Spanned dest, int dstart, int dend, boolean shouldCreateSeparateState) { 7611 // An application may install a TextWatcher to provide additional modifications after 7612 // the initial input filters run (e.g. a credit card formatter that adds spaces to a 7613 // string). This results in multiple filter() calls for what the user considers to be 7614 // a single operation. Always undo the whole set of changes in one step. 7615 @MergeMode 7616 final int mergeMode; 7617 if (isInTextWatcher() || mPreviousOperationWasInSameBatchEdit) { 7618 mergeMode = MERGE_EDIT_MODE_FORCE_MERGE; 7619 } else if (shouldCreateSeparateState) { 7620 mergeMode = MERGE_EDIT_MODE_NEVER_MERGE; 7621 } else { 7622 mergeMode = MERGE_EDIT_MODE_NORMAL; 7623 } 7624 // Build a new operation with all the information from this edit. 7625 String newText = TextUtils.substring(source, start, end); 7626 String oldText = TextUtils.substring(dest, dstart, dend); 7627 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText, 7628 mHasComposition); 7629 if (mHasComposition && TextUtils.equals(edit.mNewText, edit.mOldText)) { 7630 return; 7631 } 7632 recordEdit(edit, mergeMode); 7633 } 7634 getLastEdit()7635 private EditOperation getLastEdit() { 7636 final UndoManager um = mEditor.mUndoManager; 7637 return um.getLastOperation( 7638 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE); 7639 } 7640 /** 7641 * Fetches the last undo operation and checks to see if a new edit should be merged into it. 7642 * If forceMerge is true then the new edit is always merged. 7643 */ recordEdit(EditOperation edit, @MergeMode int mergeMode)7644 private void recordEdit(EditOperation edit, @MergeMode int mergeMode) { 7645 // Fetch the last edit operation and attempt to merge in the new edit. 7646 final UndoManager um = mEditor.mUndoManager; 7647 um.beginUpdate("Edit text"); 7648 EditOperation lastEdit = getLastEdit(); 7649 if (lastEdit == null) { 7650 // Add this as the first edit. 7651 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit); 7652 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 7653 } else if (mergeMode == MERGE_EDIT_MODE_FORCE_MERGE) { 7654 // Forced merges take priority because they could be the result of a non-user-edit 7655 // change and this case should not create a new undo operation. 7656 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit); 7657 lastEdit.forceMergeWith(edit); 7658 } else if (!mIsUserEdit) { 7659 // An application directly modified the Editable outside of a text edit. Treat this 7660 // as a new change and don't attempt to merge. 7661 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit); 7662 um.commitState(mEditor.mUndoOwner); 7663 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 7664 } else if (mergeMode == MERGE_EDIT_MODE_NORMAL && lastEdit.mergeWith(edit)) { 7665 // Merge succeeded, nothing else to do. 7666 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit); 7667 } else { 7668 // Could not merge with the last edit, so commit the last edit and add this edit. 7669 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit); 7670 um.commitState(mEditor.mUndoOwner); 7671 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 7672 } 7673 mPreviousOperationWasInSameBatchEdit = mIsUserEdit; 7674 um.endUpdate(); 7675 } 7676 canUndoEdit(CharSequence source, int start, int end, Spanned dest, int dstart, int dend)7677 private boolean canUndoEdit(CharSequence source, int start, int end, 7678 Spanned dest, int dstart, int dend) { 7679 if (!mEditor.mAllowUndo) { 7680 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled"); 7681 return false; 7682 } 7683 7684 if (mEditor.mUndoManager.isInUndo()) { 7685 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo"); 7686 return false; 7687 } 7688 7689 // Text filters run before input operations are applied. However, some input operations 7690 // are invalid and will throw exceptions when applied. This is common in tests. Don't 7691 // attempt to undo invalid operations. 7692 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) { 7693 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op"); 7694 return false; 7695 } 7696 7697 // Earlier filters can rewrite input to be a no-op, for example due to a length limit 7698 // on an input field. Skip no-op changes. 7699 if (start == end && dstart == dend) { 7700 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op"); 7701 return false; 7702 } 7703 7704 return true; 7705 } 7706 isComposition(CharSequence source)7707 private static boolean isComposition(CharSequence source) { 7708 if (!(source instanceof Spannable)) { 7709 return false; 7710 } 7711 // This is a composition edit if the source has a non-zero-length composing span. 7712 Spannable text = (Spannable) source; 7713 int composeBegin = EditableInputConnection.getComposingSpanStart(text); 7714 int composeEnd = EditableInputConnection.getComposingSpanEnd(text); 7715 return composeBegin < composeEnd; 7716 } 7717 isInTextWatcher()7718 private boolean isInTextWatcher() { 7719 CharSequence text = mEditor.mTextView.getText(); 7720 return (text instanceof SpannableStringBuilder) 7721 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0; 7722 } 7723 } 7724 7725 /** 7726 * An operation to undo a single "edit" to a text view. 7727 */ 7728 public static class EditOperation extends UndoOperation<Editor> { 7729 private static final int TYPE_INSERT = 0; 7730 private static final int TYPE_DELETE = 1; 7731 private static final int TYPE_REPLACE = 2; 7732 7733 private int mType; 7734 private String mOldText; 7735 private String mNewText; 7736 private int mStart; 7737 7738 private int mOldCursorPos; 7739 private int mNewCursorPos; 7740 private boolean mFrozen; 7741 private boolean mIsComposition; 7742 7743 /** 7744 * Constructs an edit operation from a text input operation on editor that replaces the 7745 * oldText starting at dstart with newText. 7746 */ EditOperation(Editor editor, String oldText, int dstart, String newText, boolean isComposition)7747 public EditOperation(Editor editor, String oldText, int dstart, String newText, 7748 boolean isComposition) { 7749 super(editor.mUndoOwner); 7750 mOldText = oldText; 7751 mNewText = newText; 7752 7753 // Determine the type of the edit. 7754 if (mNewText.length() > 0 && mOldText.length() == 0) { 7755 mType = TYPE_INSERT; 7756 } else if (mNewText.length() == 0 && mOldText.length() > 0) { 7757 mType = TYPE_DELETE; 7758 } else { 7759 mType = TYPE_REPLACE; 7760 } 7761 7762 mStart = dstart; 7763 // Store cursor data. 7764 mOldCursorPos = editor.mTextView.getSelectionStart(); 7765 mNewCursorPos = dstart + mNewText.length(); 7766 mIsComposition = isComposition; 7767 } 7768 EditOperation(Parcel src, ClassLoader loader)7769 public EditOperation(Parcel src, ClassLoader loader) { 7770 super(src, loader); 7771 mType = src.readInt(); 7772 mOldText = src.readString(); 7773 mNewText = src.readString(); 7774 mStart = src.readInt(); 7775 mOldCursorPos = src.readInt(); 7776 mNewCursorPos = src.readInt(); 7777 mFrozen = src.readInt() == 1; 7778 mIsComposition = src.readInt() == 1; 7779 } 7780 7781 @Override writeToParcel(Parcel dest, int flags)7782 public void writeToParcel(Parcel dest, int flags) { 7783 dest.writeInt(mType); 7784 dest.writeString(mOldText); 7785 dest.writeString(mNewText); 7786 dest.writeInt(mStart); 7787 dest.writeInt(mOldCursorPos); 7788 dest.writeInt(mNewCursorPos); 7789 dest.writeInt(mFrozen ? 1 : 0); 7790 dest.writeInt(mIsComposition ? 1 : 0); 7791 } 7792 getNewTextEnd()7793 private int getNewTextEnd() { 7794 return mStart + mNewText.length(); 7795 } 7796 getOldTextEnd()7797 private int getOldTextEnd() { 7798 return mStart + mOldText.length(); 7799 } 7800 7801 @Override commit()7802 public void commit() { 7803 } 7804 7805 @Override undo()7806 public void undo() { 7807 if (DEBUG_UNDO) Log.d(TAG, "undo"); 7808 // Remove the new text and insert the old. 7809 Editor editor = getOwnerData(); 7810 Editable text = (Editable) editor.mTextView.getText(); 7811 modifyText(text, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos); 7812 } 7813 7814 @Override redo()7815 public void redo() { 7816 if (DEBUG_UNDO) Log.d(TAG, "redo"); 7817 // Remove the old text and insert the new. 7818 Editor editor = getOwnerData(); 7819 Editable text = (Editable) editor.mTextView.getText(); 7820 modifyText(text, mStart, getOldTextEnd(), mNewText, mStart, mNewCursorPos); 7821 } 7822 7823 /** 7824 * Attempts to merge this existing operation with a new edit. 7825 * @param edit The new edit operation. 7826 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this 7827 * object unchanged. 7828 */ mergeWith(EditOperation edit)7829 private boolean mergeWith(EditOperation edit) { 7830 if (DEBUG_UNDO) { 7831 Log.d(TAG, "mergeWith old " + this); 7832 Log.d(TAG, "mergeWith new " + edit); 7833 } 7834 7835 if (mFrozen) { 7836 return false; 7837 } 7838 7839 switch (mType) { 7840 case TYPE_INSERT: 7841 return mergeInsertWith(edit); 7842 case TYPE_DELETE: 7843 return mergeDeleteWith(edit); 7844 case TYPE_REPLACE: 7845 return mergeReplaceWith(edit); 7846 default: 7847 return false; 7848 } 7849 } 7850 mergeInsertWith(EditOperation edit)7851 private boolean mergeInsertWith(EditOperation edit) { 7852 if (edit.mType == TYPE_INSERT) { 7853 // Merge insertions that are contiguous even when it's frozen. 7854 if (getNewTextEnd() != edit.mStart) { 7855 return false; 7856 } 7857 mNewText += edit.mNewText; 7858 mNewCursorPos = edit.mNewCursorPos; 7859 mFrozen = edit.mFrozen; 7860 mIsComposition = edit.mIsComposition; 7861 return true; 7862 } 7863 if (mIsComposition && edit.mType == TYPE_REPLACE 7864 && mStart <= edit.mStart && getNewTextEnd() >= edit.getOldTextEnd()) { 7865 // Merge insertion with replace as they can be single insertion. 7866 mNewText = mNewText.substring(0, edit.mStart - mStart) + edit.mNewText 7867 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length()); 7868 mNewCursorPos = edit.mNewCursorPos; 7869 mIsComposition = edit.mIsComposition; 7870 return true; 7871 } 7872 return false; 7873 } 7874 7875 // TODO: Support forward delete. mergeDeleteWith(EditOperation edit)7876 private boolean mergeDeleteWith(EditOperation edit) { 7877 // Only merge continuous deletes. 7878 if (edit.mType != TYPE_DELETE) { 7879 return false; 7880 } 7881 // Only merge deletions that are contiguous. 7882 if (mStart != edit.getOldTextEnd()) { 7883 return false; 7884 } 7885 mStart = edit.mStart; 7886 mOldText = edit.mOldText + mOldText; 7887 mNewCursorPos = edit.mNewCursorPos; 7888 mIsComposition = edit.mIsComposition; 7889 return true; 7890 } 7891 mergeReplaceWith(EditOperation edit)7892 private boolean mergeReplaceWith(EditOperation edit) { 7893 if (edit.mType == TYPE_INSERT && getNewTextEnd() == edit.mStart) { 7894 // Merge with adjacent insert. 7895 mNewText += edit.mNewText; 7896 mNewCursorPos = edit.mNewCursorPos; 7897 return true; 7898 } 7899 if (!mIsComposition) { 7900 return false; 7901 } 7902 if (edit.mType == TYPE_DELETE && mStart <= edit.mStart 7903 && getNewTextEnd() >= edit.getOldTextEnd()) { 7904 // Merge with delete as they can be single operation. 7905 mNewText = mNewText.substring(0, edit.mStart - mStart) 7906 + mNewText.substring(edit.getOldTextEnd() - mStart, mNewText.length()); 7907 if (mNewText.isEmpty()) { 7908 mType = TYPE_DELETE; 7909 } 7910 mNewCursorPos = edit.mNewCursorPos; 7911 mIsComposition = edit.mIsComposition; 7912 return true; 7913 } 7914 if (edit.mType == TYPE_REPLACE && mStart == edit.mStart 7915 && TextUtils.equals(mNewText, edit.mOldText)) { 7916 // Merge with the replace that replaces the same region. 7917 mNewText = edit.mNewText; 7918 mNewCursorPos = edit.mNewCursorPos; 7919 mIsComposition = edit.mIsComposition; 7920 return true; 7921 } 7922 return false; 7923 } 7924 7925 /** 7926 * Forcibly creates a single merged edit operation by simulating the entire text 7927 * contents being replaced. 7928 */ forceMergeWith(EditOperation edit)7929 public void forceMergeWith(EditOperation edit) { 7930 if (DEBUG_UNDO) Log.d(TAG, "forceMerge"); 7931 if (mergeWith(edit)) { 7932 return; 7933 } 7934 Editor editor = getOwnerData(); 7935 7936 // Copy the text of the current field. 7937 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster, 7938 // but would require two parallel implementations of modifyText() because Editable and 7939 // StringBuilder do not share an interface for replace/delete/insert. 7940 Editable editable = (Editable) editor.mTextView.getText(); 7941 Editable originalText = new SpannableStringBuilder(editable.toString()); 7942 7943 // Roll back the last operation. 7944 modifyText(originalText, mStart, getNewTextEnd(), mOldText, mStart, mOldCursorPos); 7945 7946 // Clone the text again and apply the new operation. 7947 Editable finalText = new SpannableStringBuilder(editable.toString()); 7948 modifyText(finalText, edit.mStart, edit.getOldTextEnd(), 7949 edit.mNewText, edit.mStart, edit.mNewCursorPos); 7950 7951 // Convert this operation into a replace operation. 7952 mType = TYPE_REPLACE; 7953 mNewText = finalText.toString(); 7954 mOldText = originalText.toString(); 7955 mStart = 0; 7956 mNewCursorPos = edit.mNewCursorPos; 7957 mIsComposition = edit.mIsComposition; 7958 // mOldCursorPos is unchanged. 7959 } 7960 modifyText(Editable text, int deleteFrom, int deleteTo, CharSequence newText, int newTextInsertAt, int newCursorPos)7961 private static void modifyText(Editable text, int deleteFrom, int deleteTo, 7962 CharSequence newText, int newTextInsertAt, int newCursorPos) { 7963 // Apply the edit if it is still valid. 7964 if (isValidRange(text, deleteFrom, deleteTo) 7965 && newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) { 7966 if (deleteFrom != deleteTo) { 7967 text.delete(deleteFrom, deleteTo); 7968 } 7969 if (newText.length() != 0) { 7970 text.insert(newTextInsertAt, newText); 7971 } 7972 } 7973 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then 7974 // don't explicitly set it and rely on SpannableStringBuilder to position it. 7975 // TODO: Select all the text that was undone. 7976 if (0 <= newCursorPos && newCursorPos <= text.length()) { 7977 Selection.setSelection(text, newCursorPos); 7978 } 7979 } 7980 getTypeString()7981 private String getTypeString() { 7982 switch (mType) { 7983 case TYPE_INSERT: 7984 return "insert"; 7985 case TYPE_DELETE: 7986 return "delete"; 7987 case TYPE_REPLACE: 7988 return "replace"; 7989 default: 7990 return ""; 7991 } 7992 } 7993 7994 @Override toString()7995 public String toString() { 7996 return "[mType=" + getTypeString() + ", " 7997 + "mOldText=" + mOldText + ", " 7998 + "mNewText=" + mNewText + ", " 7999 + "mStart=" + mStart + ", " 8000 + "mOldCursorPos=" + mOldCursorPos + ", " 8001 + "mNewCursorPos=" + mNewCursorPos + ", " 8002 + "mFrozen=" + mFrozen + ", " 8003 + "mIsComposition=" + mIsComposition + "]"; 8004 } 8005 8006 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR = 8007 new Parcelable.ClassLoaderCreator<EditOperation>() { 8008 @Override 8009 public EditOperation createFromParcel(Parcel in) { 8010 return new EditOperation(in, null); 8011 } 8012 8013 @Override 8014 public EditOperation createFromParcel(Parcel in, ClassLoader loader) { 8015 return new EditOperation(in, loader); 8016 } 8017 8018 @Override 8019 public EditOperation[] newArray(int size) { 8020 return new EditOperation[size]; 8021 } 8022 }; 8023 } 8024 8025 /** 8026 * A helper for enabling and handling "PROCESS_TEXT" menu actions. 8027 * These allow external applications to plug into currently selected text. 8028 */ 8029 static final class ProcessTextIntentActionsHandler { 8030 8031 private final Editor mEditor; 8032 private final TextView mTextView; 8033 private final Context mContext; 8034 private final PackageManager mPackageManager; 8035 private final String mPackageName; 8036 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<>(); 8037 private final SparseArray<AccessibilityAction> mAccessibilityActions = 8038 new SparseArray<>(); 8039 private final List<ResolveInfo> mSupportedActivities = new ArrayList<>(); 8040 ProcessTextIntentActionsHandler(Editor editor)8041 private ProcessTextIntentActionsHandler(Editor editor) { 8042 mEditor = Objects.requireNonNull(editor); 8043 mTextView = Objects.requireNonNull(mEditor.mTextView); 8044 mContext = Objects.requireNonNull(mTextView.getContext()); 8045 mPackageManager = Objects.requireNonNull(mContext.getPackageManager()); 8046 mPackageName = Objects.requireNonNull(mContext.getPackageName()); 8047 } 8048 8049 /** 8050 * Adds "PROCESS_TEXT" menu items to the specified menu. 8051 */ onInitializeMenu(Menu menu)8052 public void onInitializeMenu(Menu menu) { 8053 loadSupportedActivities(); 8054 final int size = mSupportedActivities.size(); 8055 for (int i = 0; i < size; i++) { 8056 final ResolveInfo resolveInfo = mSupportedActivities.get(i); 8057 menu.add(Menu.NONE, Menu.NONE, 8058 Editor.ACTION_MODE_MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i, 8059 getLabel(resolveInfo)) 8060 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) 8061 .setShowAsAction(MenuItem.SHOW_AS_ACTION_NEVER); 8062 } 8063 } 8064 8065 /** 8066 * Performs a "PROCESS_TEXT" action if there is one associated with the specified 8067 * menu item. 8068 * 8069 * @return True if the action was performed, false otherwise. 8070 */ performMenuItemAction(MenuItem item)8071 public boolean performMenuItemAction(MenuItem item) { 8072 return fireIntent(item.getIntent()); 8073 } 8074 8075 /** 8076 * Initializes and caches "PROCESS_TEXT" accessibility actions. 8077 */ initializeAccessibilityActions()8078 public void initializeAccessibilityActions() { 8079 mAccessibilityIntents.clear(); 8080 mAccessibilityActions.clear(); 8081 int i = 0; 8082 loadSupportedActivities(); 8083 for (ResolveInfo resolveInfo : mSupportedActivities) { 8084 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++; 8085 mAccessibilityActions.put( 8086 actionId, 8087 new AccessibilityAction(actionId, getLabel(resolveInfo))); 8088 mAccessibilityIntents.put( 8089 actionId, createProcessTextIntentForResolveInfo(resolveInfo)); 8090 } 8091 } 8092 8093 /** 8094 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info. 8095 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the 8096 * latest accessibility actions available for this call. 8097 */ onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8098 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) { 8099 for (int i = 0; i < mAccessibilityActions.size(); i++) { 8100 nodeInfo.addAction(mAccessibilityActions.valueAt(i)); 8101 } 8102 } 8103 8104 /** 8105 * Performs a "PROCESS_TEXT" action if there is one associated with the specified 8106 * accessibility action id. 8107 * 8108 * @return True if the action was performed, false otherwise. 8109 */ performAccessibilityAction(int actionId)8110 public boolean performAccessibilityAction(int actionId) { 8111 return fireIntent(mAccessibilityIntents.get(actionId)); 8112 } 8113 fireIntent(Intent intent)8114 private boolean fireIntent(Intent intent) { 8115 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) { 8116 String selectedText = mTextView.getSelectedText(); 8117 selectedText = TextUtils.trimToParcelableSize(selectedText); 8118 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, selectedText); 8119 mEditor.mPreserveSelection = true; 8120 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE); 8121 return true; 8122 } 8123 return false; 8124 } 8125 loadSupportedActivities()8126 private void loadSupportedActivities() { 8127 mSupportedActivities.clear(); 8128 if (!mContext.canStartActivityForResult()) { 8129 return; 8130 } 8131 PackageManager packageManager = mTextView.getContext().getPackageManager(); 8132 List<ResolveInfo> unfiltered = 8133 packageManager.queryIntentActivities(createProcessTextIntent(), 0); 8134 for (ResolveInfo info : unfiltered) { 8135 if (isSupportedActivity(info)) { 8136 mSupportedActivities.add(info); 8137 } 8138 } 8139 } 8140 isSupportedActivity(ResolveInfo info)8141 private boolean isSupportedActivity(ResolveInfo info) { 8142 return mPackageName.equals(info.activityInfo.packageName) 8143 || info.activityInfo.exported 8144 && (info.activityInfo.permission == null 8145 || mContext.checkSelfPermission(info.activityInfo.permission) 8146 == PackageManager.PERMISSION_GRANTED); 8147 } 8148 createProcessTextIntentForResolveInfo(ResolveInfo info)8149 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) { 8150 return createProcessTextIntent() 8151 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable()) 8152 .setClassName(info.activityInfo.packageName, info.activityInfo.name); 8153 } 8154 createProcessTextIntent()8155 private Intent createProcessTextIntent() { 8156 return new Intent() 8157 .setAction(Intent.ACTION_PROCESS_TEXT) 8158 .setType("text/plain"); 8159 } 8160 getLabel(ResolveInfo resolveInfo)8161 private CharSequence getLabel(ResolveInfo resolveInfo) { 8162 return resolveInfo.loadLabel(mPackageManager); 8163 } 8164 } 8165 8166 /** 8167 * Accessibility helper for "smart" (i.e. textAssist) actions. 8168 * Helps ensure that "smart" actions are shown in the accessibility menu. 8169 * NOTE that these actions are only available when an action mode is live. 8170 * 8171 * @hide 8172 */ 8173 private static final class AccessibilitySmartActions { 8174 8175 private final TextView mTextView; 8176 private final SparseArray<Pair<AccessibilityAction, RemoteAction>> mActions = 8177 new SparseArray<>(); 8178 AccessibilitySmartActions(TextView textView)8179 private AccessibilitySmartActions(TextView textView) { 8180 mTextView = Objects.requireNonNull(textView); 8181 } 8182 addAction(RemoteAction action)8183 private void addAction(RemoteAction action) { 8184 final int actionId = ACCESSIBILITY_ACTION_SMART_START_ID + mActions.size(); 8185 mActions.put(actionId, 8186 new Pair(new AccessibilityAction(actionId, action.getTitle()), action)); 8187 } 8188 reset()8189 private void reset() { 8190 mActions.clear(); 8191 } 8192 onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8193 void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) { 8194 for (int i = 0; i < mActions.size(); i++) { 8195 nodeInfo.addAction(mActions.valueAt(i).first); 8196 } 8197 } 8198 performAccessibilityAction(int actionId)8199 boolean performAccessibilityAction(int actionId) { 8200 final Pair<AccessibilityAction, RemoteAction> pair = mActions.get(actionId); 8201 if (pair != null) { 8202 TextClassification.createIntentOnClickListener(pair.second.getActionIntent()) 8203 .onClick(mTextView); 8204 return true; 8205 } 8206 return false; 8207 } 8208 } 8209 8210 private static final class InsertModeController { 8211 private final TextView mTextView; 8212 private boolean mIsInsertModeActive; 8213 private InsertModeTransformationMethod mInsertModeTransformationMethod; 8214 private final Paint mHighlightPaint; 8215 private final Path mHighlightPath; 8216 8217 /** 8218 * Whether it is in the progress of updating transformation method. It's needed because 8219 * {@link TextView#setTransformationMethod(TransformationMethod)} will eventually call 8220 * {@link TextView#setText(CharSequence)}. 8221 * Because it normally should exit insert mode when {@link TextView#setText(CharSequence)} 8222 * is called externally, we need this boolean to distinguish whether setText is triggered 8223 * by setTransformation or not. 8224 */ 8225 private boolean mUpdatingTransformationMethod; 8226 InsertModeController(@onNull TextView textView)8227 InsertModeController(@NonNull TextView textView) { 8228 mTextView = Objects.requireNonNull(textView); 8229 mIsInsertModeActive = false; 8230 mInsertModeTransformationMethod = null; 8231 mHighlightPaint = new Paint(); 8232 mHighlightPath = new Path(); 8233 8234 // Insert mode highlight color is 20% opacity of the default text color. 8235 int color = mTextView.getTextColors().getDefaultColor(); 8236 color = ColorUtils.setAlphaComponent(color, (int) (0.2f * Color.alpha(color))); 8237 mHighlightPaint.setColor(color); 8238 } 8239 8240 /** 8241 * Enter insert mode. 8242 * @param offset the index to set the cursor. 8243 * @return true if the call is successful. false if a) it's already in the insert mode, 8244 * b) it failed to enter the insert mode. 8245 */ enterInsertMode(int offset)8246 boolean enterInsertMode(int offset) { 8247 if (mIsInsertModeActive) return false; 8248 8249 TransformationMethod oldTransformationMethod = 8250 mTextView.getTransformationMethod(); 8251 if (oldTransformationMethod instanceof OffsetMapping) { 8252 // We can't support the case where the oldTransformationMethod is an OffsetMapping. 8253 return false; 8254 } 8255 8256 final boolean isSingleLine = mTextView.isSingleLine(); 8257 mInsertModeTransformationMethod = new InsertModeTransformationMethod(offset, 8258 isSingleLine, oldTransformationMethod); 8259 setTransformationMethod(mInsertModeTransformationMethod, true); 8260 Selection.setSelection((Spannable) mTextView.getText(), offset); 8261 8262 mIsInsertModeActive = true; 8263 return true; 8264 } 8265 exitInsertMode()8266 void exitInsertMode() { 8267 exitInsertMode(true); 8268 } 8269 exitInsertMode(boolean updateText)8270 void exitInsertMode(boolean updateText) { 8271 if (!mIsInsertModeActive) return; 8272 if (mInsertModeTransformationMethod == null 8273 || mInsertModeTransformationMethod != mTextView.getTransformationMethod()) { 8274 mIsInsertModeActive = false; 8275 return; 8276 } 8277 // Changing TransformationMethod will reset selection range to [0, 0), we need to 8278 // manually restore the old selection range. 8279 final int selectionStart = mTextView.getSelectionStart(); 8280 final int selectionEnd = mTextView.getSelectionEnd(); 8281 final TransformationMethod oldTransformationMethod = 8282 mInsertModeTransformationMethod.getOldTransformationMethod(); 8283 setTransformationMethod(oldTransformationMethod, updateText); 8284 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 8285 mIsInsertModeActive = false; 8286 } 8287 onDraw(Canvas canvas)8288 void onDraw(Canvas canvas) { 8289 if (!mIsInsertModeActive) return; 8290 final CharSequence transformedText = mTextView.getTransformed(); 8291 if (transformedText instanceof InsertModeTransformationMethod.TransformedText) { 8292 final Layout layout = mTextView.getLayout(); 8293 if (layout == null) return; 8294 final InsertModeTransformationMethod.TransformedText insertModeTransformedText = 8295 ((InsertModeTransformationMethod.TransformedText) transformedText); 8296 final int highlightStart = insertModeTransformedText.getHighlightStart(); 8297 final int highlightEnd = insertModeTransformedText.getHighlightEnd(); 8298 layout.getSelectionPath(highlightStart, highlightEnd, mHighlightPath); 8299 canvas.drawPath(mHighlightPath, mHighlightPaint); 8300 } 8301 } 8302 8303 /** 8304 * Update the TransformationMethod on the {@link TextView}. 8305 * @param method the new method to be set on the {@link TextView}/ 8306 * @param updateText whether to update the text during setTransformationMethod call. 8307 */ setTransformationMethod(TransformationMethod method, boolean updateText)8308 private void setTransformationMethod(TransformationMethod method, boolean updateText) { 8309 mUpdatingTransformationMethod = true; 8310 mTextView.setTransformationMethodInternal(method, updateText); 8311 mUpdatingTransformationMethod = false; 8312 } 8313 8314 /** 8315 * Notify the InsertMode controller that the {@link TextView} is about to set its text. 8316 */ beforeSetText()8317 void beforeSetText() { 8318 // TextView#setText is called because our call to 8319 // TextView#setTransformationMethodInternal in enterInsertMode(), exitInsertMode() or 8320 // updateTransformationMethod(). 8321 // Do nothing in this case. 8322 if (mUpdatingTransformationMethod) { 8323 return; 8324 } 8325 // TextView#setText is called externally. Exit InsertMode but don't update text again 8326 // when calling setTransformationMethod. 8327 exitInsertMode(/* updateText */ false); 8328 } 8329 8330 /** 8331 * Notify the {@link InsertModeController} that TextView#setTransformationMethod is called. 8332 * If it's not in the insert mode, the given transformation method is directly set to the 8333 * TextView. Otherwise, it will wrap the given transformation method with an 8334 * {@link InsertModeTransformationMethod} and then set it on the TextView. 8335 * 8336 * @param transformationMethod the new {@link TransformationMethod} to be set on the 8337 * TextView. 8338 */ updateTransformationMethod(TransformationMethod transformationMethod)8339 void updateTransformationMethod(TransformationMethod transformationMethod) { 8340 if (!mIsInsertModeActive) { 8341 setTransformationMethod(transformationMethod, /* updateText */ true); 8342 return; 8343 } 8344 8345 // Changing TransformationMethod will reset selection range to [0, 0), we need to 8346 // manually restore the old selection range. 8347 final int selectionStart = mTextView.getSelectionStart(); 8348 final int selectionEnd = mTextView.getSelectionEnd(); 8349 mInsertModeTransformationMethod = mInsertModeTransformationMethod.update( 8350 transformationMethod, mTextView.isSingleLine()); 8351 setTransformationMethod(mInsertModeTransformationMethod, /* updateText */ true); 8352 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 8353 } 8354 } 8355 enterInsertMode(int offset)8356 boolean enterInsertMode(int offset) { 8357 if (mInsertModeController == null) { 8358 if (mTextView == null) return false; 8359 mInsertModeController = new InsertModeController(mTextView); 8360 } 8361 return mInsertModeController.enterInsertMode(offset); 8362 } 8363 8364 /** 8365 * Exit insert mode if this editor is in insert mode. 8366 */ exitInsertMode()8367 void exitInsertMode() { 8368 if (mInsertModeController == null) return; 8369 mInsertModeController.exitInsertMode(); 8370 } 8371 8372 /** 8373 * Called by the {@link TextView} when the {@link TransformationMethod} is updated. 8374 * 8375 * @param method the {@link TransformationMethod} to be set on the TextView. 8376 */ setTransformationMethod(TransformationMethod method)8377 void setTransformationMethod(TransformationMethod method) { 8378 if (mInsertModeController == null) { 8379 mTextView.setTransformationMethodInternal(method, /* updateText */ true); 8380 return; 8381 } 8382 mInsertModeController.updateTransformationMethod(method); 8383 } 8384 8385 /** 8386 * Notify that the Editor that the associated {@link TextView} is about to set its text. 8387 */ beforeSetText()8388 void beforeSetText() { 8389 if (mInsertModeController == null) return; 8390 mInsertModeController.beforeSetText(); 8391 } 8392 8393 /** 8394 * Initializes the nodeInfo with smart actions. 8395 */ onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo)8396 void onInitializeSmartActionsAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) { 8397 mA11ySmartActions.onInitializeAccessibilityNodeInfo(nodeInfo); 8398 } 8399 8400 /** 8401 * Handles the accessibility action if it is an active smart action. 8402 * Return false if this method does not hanle the action. 8403 */ performSmartActionsAccessibilityAction(int actionId)8404 boolean performSmartActionsAccessibilityAction(int actionId) { 8405 return mA11ySmartActions.performAccessibilityAction(actionId); 8406 } 8407 logCursor(String location, @Nullable String msgFormat, Object ... msgArgs)8408 static void logCursor(String location, @Nullable String msgFormat, Object ... msgArgs) { 8409 if (msgFormat == null) { 8410 Log.d(TAG, location); 8411 } else { 8412 Log.d(TAG, location + ": " + String.format(msgFormat, msgArgs)); 8413 } 8414 } 8415 } 8416