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