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