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