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