Editor.java revision dfea1a3336f1c738b24028fbb6d499b6b4a33292
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 17package android.widget; 18 19import android.R; 20import android.annotation.IntDef; 21import android.annotation.Nullable; 22import android.app.PendingIntent; 23import android.app.PendingIntent.CanceledException; 24import android.content.ClipData; 25import android.content.ClipData.Item; 26import android.content.Context; 27import android.content.Intent; 28import android.content.UndoManager; 29import android.content.UndoOperation; 30import android.content.UndoOwner; 31import android.content.pm.PackageManager; 32import android.content.pm.ResolveInfo; 33import android.content.res.TypedArray; 34import android.graphics.Canvas; 35import android.graphics.Color; 36import android.graphics.Matrix; 37import android.graphics.Paint; 38import android.graphics.Path; 39import android.graphics.Rect; 40import android.graphics.RectF; 41import android.graphics.drawable.ColorDrawable; 42import android.graphics.drawable.Drawable; 43import android.os.Bundle; 44import android.os.Parcel; 45import android.os.Parcelable; 46import android.os.ParcelableParcel; 47import android.os.SystemClock; 48import android.provider.Settings; 49import android.text.DynamicLayout; 50import android.text.Editable; 51import android.text.InputFilter; 52import android.text.InputType; 53import android.text.Layout; 54import android.text.ParcelableSpan; 55import android.text.Selection; 56import android.text.SpanWatcher; 57import android.text.Spannable; 58import android.text.SpannableStringBuilder; 59import android.text.Spanned; 60import android.text.StaticLayout; 61import android.text.TextUtils; 62import android.text.method.KeyListener; 63import android.text.method.MetaKeyKeyListener; 64import android.text.method.MovementMethod; 65import android.text.method.WordIterator; 66import android.text.style.EasyEditSpan; 67import android.text.style.SuggestionRangeSpan; 68import android.text.style.SuggestionSpan; 69import android.text.style.TextAppearanceSpan; 70import android.text.style.URLSpan; 71import android.util.DisplayMetrics; 72import android.util.LocaleList; 73import android.util.Log; 74import android.util.SparseArray; 75import android.view.ActionMode; 76import android.view.ActionMode.Callback; 77import android.view.ContextMenu; 78import android.view.DisplayListCanvas; 79import android.view.DragEvent; 80import android.view.Gravity; 81import android.view.InputDevice; 82import android.view.LayoutInflater; 83import android.view.Menu; 84import android.view.MenuItem; 85import android.view.MotionEvent; 86import android.view.RenderNode; 87import android.view.SubMenu; 88import android.view.View; 89import android.view.View.DragShadowBuilder; 90import android.view.View.OnClickListener; 91import android.view.ViewConfiguration; 92import android.view.ViewGroup; 93import android.view.ViewGroup.LayoutParams; 94import android.view.ViewParent; 95import android.view.ViewTreeObserver; 96import android.view.WindowManager; 97import android.view.accessibility.AccessibilityNodeInfo; 98import android.view.inputmethod.CorrectionInfo; 99import android.view.inputmethod.CursorAnchorInfo; 100import android.view.inputmethod.EditorInfo; 101import android.view.inputmethod.ExtractedText; 102import android.view.inputmethod.ExtractedTextRequest; 103import android.view.inputmethod.InputConnection; 104import android.view.inputmethod.InputMethodManager; 105import android.widget.AdapterView.OnItemClickListener; 106import android.widget.TextView.Drawables; 107import android.widget.TextView.OnEditorActionListener; 108 109import com.android.internal.annotations.VisibleForTesting; 110import com.android.internal.util.ArrayUtils; 111import com.android.internal.util.GrowingArrayUtils; 112import com.android.internal.util.Preconditions; 113import com.android.internal.widget.EditableInputConnection; 114 115import java.lang.annotation.Retention; 116import java.lang.annotation.RetentionPolicy; 117import java.text.BreakIterator; 118import java.util.Arrays; 119import java.util.Comparator; 120import java.util.HashMap; 121import java.util.List; 122 123 124/** 125 * Helper class used by TextView to handle editable text views. 126 * 127 * @hide 128 */ 129public class Editor { 130 private static final String TAG = "Editor"; 131 private static final boolean DEBUG_UNDO = false; 132 133 static final int BLINK = 500; 134 private static final float[] TEMP_POSITION = new float[2]; 135 private static int DRAG_SHADOW_MAX_TEXT_LENGTH = 20; 136 private static final float LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS = 0.5f; 137 private static final int UNSET_X_VALUE = -1; 138 private static final int UNSET_LINE = -1; 139 // Tag used when the Editor maintains its own separate UndoManager. 140 private static final String UNDO_OWNER_TAG = "Editor"; 141 142 // Ordering constants used to place the Action Mode or context menu items in their menu. 143 private static final int MENU_ITEM_ORDER_UNDO = 1; 144 private static final int MENU_ITEM_ORDER_REDO = 2; 145 private static final int MENU_ITEM_ORDER_CUT = 3; 146 private static final int MENU_ITEM_ORDER_COPY = 4; 147 private static final int MENU_ITEM_ORDER_PASTE = 5; 148 private static final int MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT = 6; 149 private static final int MENU_ITEM_ORDER_SHARE = 7; 150 private static final int MENU_ITEM_ORDER_SELECT_ALL = 8; 151 private static final int MENU_ITEM_ORDER_REPLACE = 9; 152 private static final int MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START = 10; 153 154 // Each Editor manages its own undo stack. 155 private final UndoManager mUndoManager = new UndoManager(); 156 private UndoOwner mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); 157 final UndoInputFilter mUndoInputFilter = new UndoInputFilter(this); 158 boolean mAllowUndo = true; 159 160 // Cursor Controllers. 161 InsertionPointCursorController mInsertionPointCursorController; 162 SelectionModifierCursorController mSelectionModifierCursorController; 163 // Action mode used when text is selected or when actions on an insertion cursor are triggered. 164 ActionMode mTextActionMode; 165 boolean mInsertionControllerEnabled; 166 boolean mSelectionControllerEnabled; 167 168 // Used to highlight a word when it is corrected by the IME 169 CorrectionHighlighter mCorrectionHighlighter; 170 171 InputContentType mInputContentType; 172 InputMethodState mInputMethodState; 173 174 private static class TextRenderNode { 175 RenderNode renderNode; 176 boolean isDirty; 177 public TextRenderNode(String name) { 178 isDirty = true; 179 renderNode = RenderNode.create(name, null); 180 } 181 boolean needsRecord() { return isDirty || !renderNode.isValid(); } 182 } 183 TextRenderNode[] mTextRenderNodes; 184 185 boolean mFrozenWithFocus; 186 boolean mSelectionMoved; 187 boolean mTouchFocusSelected; 188 189 KeyListener mKeyListener; 190 int mInputType = EditorInfo.TYPE_NULL; 191 192 boolean mDiscardNextActionUp; 193 boolean mIgnoreActionUpEvent; 194 private boolean mIgnoreNextMouseActionUpOrDown; 195 196 long mShowCursor; 197 Blink mBlink; 198 199 boolean mCursorVisible = true; 200 boolean mSelectAllOnFocus; 201 boolean mTextIsSelectable; 202 203 CharSequence mError; 204 boolean mErrorWasChanged; 205 ErrorPopup mErrorPopup; 206 207 /** 208 * This flag is set if the TextView tries to display an error before it 209 * is attached to the window (so its position is still unknown). 210 * It causes the error to be shown later, when onAttachedToWindow() 211 * is called. 212 */ 213 boolean mShowErrorAfterAttach; 214 215 boolean mInBatchEditControllers; 216 boolean mShowSoftInputOnFocus = true; 217 boolean mPreserveDetachedSelection; 218 boolean mTemporaryDetach; 219 220 boolean mIsBeingLongClicked; 221 222 SuggestionsPopupWindow mSuggestionsPopupWindow; 223 SuggestionRangeSpan mSuggestionRangeSpan; 224 Runnable mShowSuggestionRunnable; 225 226 final Drawable[] mCursorDrawable = new Drawable[2]; 227 int mCursorCount; // Current number of used mCursorDrawable: 0 (resource=0), 1 or 2 (split) 228 229 private Drawable mSelectHandleLeft; 230 private Drawable mSelectHandleRight; 231 private Drawable mSelectHandleCenter; 232 233 // Global listener that detects changes in the global position of the TextView 234 private PositionListener mPositionListener; 235 236 float mLastDownPositionX, mLastDownPositionY; 237 private float mContextMenuAnchorX, mContextMenuAnchorY; 238 Callback mCustomSelectionActionModeCallback; 239 Callback mCustomInsertionActionModeCallback; 240 241 // Set when this TextView gained focus with some text selected. Will start selection mode. 242 boolean mCreatedWithASelection; 243 244 // Indicates the current tap state (first tap, double tap, or triple click). 245 private int mTapState = TAP_STATE_INITIAL; 246 private long mLastTouchUpTime = 0; 247 private static final int TAP_STATE_INITIAL = 0; 248 private static final int TAP_STATE_FIRST_TAP = 1; 249 private static final int TAP_STATE_DOUBLE_TAP = 2; 250 // Only for mouse input. 251 private static final int TAP_STATE_TRIPLE_CLICK = 3; 252 253 // The button state as of the last time #onTouchEvent is called. 254 private int mLastButtonState; 255 256 private Runnable mInsertionActionModeRunnable; 257 258 // The span controller helps monitoring the changes to which the Editor needs to react: 259 // - EasyEditSpans, for which we have some UI to display on attach and on hide 260 // - SelectionSpans, for which we need to call updateSelection if an IME is attached 261 private SpanController mSpanController; 262 263 WordIterator mWordIterator; 264 SpellChecker mSpellChecker; 265 266 // This word iterator is set with text and used to determine word boundaries 267 // when a user is selecting text. 268 private WordIterator mWordIteratorWithText; 269 // Indicate that the text in the word iterator needs to be updated. 270 private boolean mUpdateWordIteratorText; 271 272 private Rect mTempRect; 273 274 private TextView mTextView; 275 276 final ProcessTextIntentActionsHandler mProcessTextIntentActionsHandler; 277 278 final CursorAnchorInfoNotifier mCursorAnchorInfoNotifier = new CursorAnchorInfoNotifier(); 279 280 private final Runnable mShowFloatingToolbar = new Runnable() { 281 @Override 282 public void run() { 283 if (mTextActionMode != null) { 284 mTextActionMode.hide(0); // hide off. 285 } 286 } 287 }; 288 289 boolean mIsInsertionActionModeStartPending = false; 290 291 private final SuggestionHelper mSuggestionHelper = new SuggestionHelper(); 292 private SuggestionInfo[] mSuggestionInfosInContextMenu; 293 294 Editor(TextView textView) { 295 mTextView = textView; 296 // Synchronize the filter list, which places the undo input filter at the end. 297 mTextView.setFilters(mTextView.getFilters()); 298 mProcessTextIntentActionsHandler = new ProcessTextIntentActionsHandler(this); 299 } 300 301 ParcelableParcel saveInstanceState() { 302 ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader()); 303 Parcel parcel = state.getParcel(); 304 mUndoManager.saveInstanceState(parcel); 305 mUndoInputFilter.saveInstanceState(parcel); 306 return state; 307 } 308 309 void restoreInstanceState(ParcelableParcel state) { 310 Parcel parcel = state.getParcel(); 311 mUndoManager.restoreInstanceState(parcel, state.getClassLoader()); 312 mUndoInputFilter.restoreInstanceState(parcel); 313 // Re-associate this object as the owner of undo state. 314 mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this); 315 } 316 317 /** 318 * Forgets all undo and redo operations for this Editor. 319 */ 320 void forgetUndoRedo() { 321 UndoOwner[] owners = { mUndoOwner }; 322 mUndoManager.forgetUndos(owners, -1 /* all */); 323 mUndoManager.forgetRedos(owners, -1 /* all */); 324 } 325 326 boolean canUndo() { 327 UndoOwner[] owners = { mUndoOwner }; 328 return mAllowUndo && mUndoManager.countUndos(owners) > 0; 329 } 330 331 boolean canRedo() { 332 UndoOwner[] owners = { mUndoOwner }; 333 return mAllowUndo && mUndoManager.countRedos(owners) > 0; 334 } 335 336 void undo() { 337 if (!mAllowUndo) { 338 return; 339 } 340 UndoOwner[] owners = { mUndoOwner }; 341 mUndoManager.undo(owners, 1); // Undo 1 action. 342 } 343 344 void redo() { 345 if (!mAllowUndo) { 346 return; 347 } 348 UndoOwner[] owners = { mUndoOwner }; 349 mUndoManager.redo(owners, 1); // Redo 1 action. 350 } 351 352 void replace() { 353 int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2; 354 stopTextActionMode(); 355 Selection.setSelection((Spannable) mTextView.getText(), middle); 356 showSuggestions(); 357 } 358 359 void onAttachedToWindow() { 360 if (mShowErrorAfterAttach) { 361 showError(); 362 mShowErrorAfterAttach = false; 363 } 364 mTemporaryDetach = false; 365 366 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 367 // No need to create the controller. 368 // The get method will add the listener on controller creation. 369 if (mInsertionPointCursorController != null) { 370 observer.addOnTouchModeChangeListener(mInsertionPointCursorController); 371 } 372 if (mSelectionModifierCursorController != null) { 373 mSelectionModifierCursorController.resetTouchOffsets(); 374 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); 375 } 376 updateSpellCheckSpans(0, mTextView.getText().length(), 377 true /* create the spell checker if needed */); 378 379 if (mTextView.hasTransientState() && 380 mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { 381 // Since transient state is reference counted make sure it stays matched 382 // with our own calls to it for managing selection. 383 // The action mode callback will set this back again when/if the action mode starts. 384 mTextView.setHasTransientState(false); 385 386 // We had an active selection from before, start the selection mode. 387 startSelectionActionMode(); 388 } 389 390 getPositionListener().addSubscriber(mCursorAnchorInfoNotifier, true); 391 resumeBlink(); 392 } 393 394 void onDetachedFromWindow() { 395 getPositionListener().removeSubscriber(mCursorAnchorInfoNotifier); 396 397 if (mError != null) { 398 hideError(); 399 } 400 401 suspendBlink(); 402 403 if (mInsertionPointCursorController != null) { 404 mInsertionPointCursorController.onDetached(); 405 } 406 407 if (mSelectionModifierCursorController != null) { 408 mSelectionModifierCursorController.onDetached(); 409 } 410 411 if (mShowSuggestionRunnable != null) { 412 mTextView.removeCallbacks(mShowSuggestionRunnable); 413 } 414 415 // Cancel the single tap delayed runnable. 416 if (mInsertionActionModeRunnable != null) { 417 mTextView.removeCallbacks(mInsertionActionModeRunnable); 418 } 419 420 mTextView.removeCallbacks(mShowFloatingToolbar); 421 422 discardTextDisplayLists(); 423 424 if (mSpellChecker != null) { 425 mSpellChecker.closeSession(); 426 // Forces the creation of a new SpellChecker next time this window is created. 427 // Will handle the cases where the settings has been changed in the meantime. 428 mSpellChecker = null; 429 } 430 431 mPreserveDetachedSelection = true; 432 hideCursorAndSpanControllers(); 433 stopTextActionMode(); 434 mPreserveDetachedSelection = false; 435 mTemporaryDetach = false; 436 } 437 438 private void discardTextDisplayLists() { 439 if (mTextRenderNodes != null) { 440 for (int i = 0; i < mTextRenderNodes.length; i++) { 441 RenderNode displayList = mTextRenderNodes[i] != null 442 ? mTextRenderNodes[i].renderNode : null; 443 if (displayList != null && displayList.isValid()) { 444 displayList.discardDisplayList(); 445 } 446 } 447 } 448 } 449 450 private void showError() { 451 if (mTextView.getWindowToken() == null) { 452 mShowErrorAfterAttach = true; 453 return; 454 } 455 456 if (mErrorPopup == null) { 457 LayoutInflater inflater = LayoutInflater.from(mTextView.getContext()); 458 final TextView err = (TextView) inflater.inflate( 459 com.android.internal.R.layout.textview_hint, null); 460 461 final float scale = mTextView.getResources().getDisplayMetrics().density; 462 mErrorPopup = new ErrorPopup(err, (int)(200 * scale + 0.5f), (int)(50 * scale + 0.5f)); 463 mErrorPopup.setFocusable(false); 464 // The user is entering text, so the input method is needed. We 465 // don't want the popup to be displayed on top of it. 466 mErrorPopup.setInputMethodMode(PopupWindow.INPUT_METHOD_NEEDED); 467 } 468 469 TextView tv = (TextView) mErrorPopup.getContentView(); 470 chooseSize(mErrorPopup, mError, tv); 471 tv.setText(mError); 472 473 mErrorPopup.showAsDropDown(mTextView, getErrorX(), getErrorY()); 474 mErrorPopup.fixDirection(mErrorPopup.isAboveAnchor()); 475 } 476 477 public void setError(CharSequence error, Drawable icon) { 478 mError = TextUtils.stringOrSpannedString(error); 479 mErrorWasChanged = true; 480 481 if (mError == null) { 482 setErrorIcon(null); 483 if (mErrorPopup != null) { 484 if (mErrorPopup.isShowing()) { 485 mErrorPopup.dismiss(); 486 } 487 488 mErrorPopup = null; 489 } 490 mShowErrorAfterAttach = false; 491 } else { 492 setErrorIcon(icon); 493 if (mTextView.isFocused()) { 494 showError(); 495 } 496 } 497 } 498 499 private void setErrorIcon(Drawable icon) { 500 Drawables dr = mTextView.mDrawables; 501 if (dr == null) { 502 mTextView.mDrawables = dr = new Drawables(mTextView.getContext()); 503 } 504 dr.setErrorDrawable(icon, mTextView); 505 506 mTextView.resetResolvedDrawables(); 507 mTextView.invalidate(); 508 mTextView.requestLayout(); 509 } 510 511 private void hideError() { 512 if (mErrorPopup != null) { 513 if (mErrorPopup.isShowing()) { 514 mErrorPopup.dismiss(); 515 } 516 } 517 518 mShowErrorAfterAttach = false; 519 } 520 521 /** 522 * Returns the X offset to make the pointy top of the error point 523 * at the middle of the error icon. 524 */ 525 private int getErrorX() { 526 /* 527 * The "25" is the distance between the point and the right edge 528 * of the background 529 */ 530 final float scale = mTextView.getResources().getDisplayMetrics().density; 531 532 final Drawables dr = mTextView.mDrawables; 533 534 final int layoutDirection = mTextView.getLayoutDirection(); 535 int errorX; 536 int offset; 537 switch (layoutDirection) { 538 default: 539 case View.LAYOUT_DIRECTION_LTR: 540 offset = - (dr != null ? dr.mDrawableSizeRight : 0) / 2 + (int) (25 * scale + 0.5f); 541 errorX = mTextView.getWidth() - mErrorPopup.getWidth() - 542 mTextView.getPaddingRight() + offset; 543 break; 544 case View.LAYOUT_DIRECTION_RTL: 545 offset = (dr != null ? dr.mDrawableSizeLeft : 0) / 2 - (int) (25 * scale + 0.5f); 546 errorX = mTextView.getPaddingLeft() + offset; 547 break; 548 } 549 return errorX; 550 } 551 552 /** 553 * Returns the Y offset to make the pointy top of the error point 554 * at the bottom of the error icon. 555 */ 556 private int getErrorY() { 557 /* 558 * Compound, not extended, because the icon is not clipped 559 * if the text height is smaller. 560 */ 561 final int compoundPaddingTop = mTextView.getCompoundPaddingTop(); 562 int vspace = mTextView.getBottom() - mTextView.getTop() - 563 mTextView.getCompoundPaddingBottom() - compoundPaddingTop; 564 565 final Drawables dr = mTextView.mDrawables; 566 567 final int layoutDirection = mTextView.getLayoutDirection(); 568 int height; 569 switch (layoutDirection) { 570 default: 571 case View.LAYOUT_DIRECTION_LTR: 572 height = (dr != null ? dr.mDrawableHeightRight : 0); 573 break; 574 case View.LAYOUT_DIRECTION_RTL: 575 height = (dr != null ? dr.mDrawableHeightLeft : 0); 576 break; 577 } 578 579 int icontop = compoundPaddingTop + (vspace - height) / 2; 580 581 /* 582 * The "2" is the distance between the point and the top edge 583 * of the background. 584 */ 585 final float scale = mTextView.getResources().getDisplayMetrics().density; 586 return icontop + height - mTextView.getHeight() - (int) (2 * scale + 0.5f); 587 } 588 589 void createInputContentTypeIfNeeded() { 590 if (mInputContentType == null) { 591 mInputContentType = new InputContentType(); 592 } 593 } 594 595 void createInputMethodStateIfNeeded() { 596 if (mInputMethodState == null) { 597 mInputMethodState = new InputMethodState(); 598 } 599 } 600 601 boolean isCursorVisible() { 602 // The default value is true, even when there is no associated Editor 603 return mCursorVisible && mTextView.isTextEditable(); 604 } 605 606 void prepareCursorControllers() { 607 boolean windowSupportsHandles = false; 608 609 ViewGroup.LayoutParams params = mTextView.getRootView().getLayoutParams(); 610 if (params instanceof WindowManager.LayoutParams) { 611 WindowManager.LayoutParams windowParams = (WindowManager.LayoutParams) params; 612 windowSupportsHandles = windowParams.type < WindowManager.LayoutParams.FIRST_SUB_WINDOW 613 || windowParams.type > WindowManager.LayoutParams.LAST_SUB_WINDOW; 614 } 615 616 boolean enabled = windowSupportsHandles && mTextView.getLayout() != null; 617 mInsertionControllerEnabled = enabled && isCursorVisible(); 618 mSelectionControllerEnabled = enabled && mTextView.textCanBeSelected(); 619 620 if (!mInsertionControllerEnabled) { 621 hideInsertionPointCursorController(); 622 if (mInsertionPointCursorController != null) { 623 mInsertionPointCursorController.onDetached(); 624 mInsertionPointCursorController = null; 625 } 626 } 627 628 if (!mSelectionControllerEnabled) { 629 stopTextActionMode(); 630 if (mSelectionModifierCursorController != null) { 631 mSelectionModifierCursorController.onDetached(); 632 mSelectionModifierCursorController = null; 633 } 634 } 635 } 636 637 void hideInsertionPointCursorController() { 638 if (mInsertionPointCursorController != null) { 639 mInsertionPointCursorController.hide(); 640 } 641 } 642 643 /** 644 * Hides the insertion and span controllers. 645 */ 646 void hideCursorAndSpanControllers() { 647 hideCursorControllers(); 648 hideSpanControllers(); 649 } 650 651 private void hideSpanControllers() { 652 if (mSpanController != null) { 653 mSpanController.hide(); 654 } 655 } 656 657 private void hideCursorControllers() { 658 // When mTextView is not ExtractEditText, we need to distinguish two kinds of focus-lost. 659 // One is the true focus lost where suggestions pop-up (if any) should be dismissed, and the 660 // other is an side effect of showing the suggestions pop-up itself. We use isShowingUp() 661 // to distinguish one from the other. 662 if (mSuggestionsPopupWindow != null && ((mTextView.isInExtractedMode()) || 663 !mSuggestionsPopupWindow.isShowingUp())) { 664 // Should be done before hide insertion point controller since it triggers a show of it 665 mSuggestionsPopupWindow.hide(); 666 } 667 hideInsertionPointCursorController(); 668 } 669 670 /** 671 * Create new SpellCheckSpans on the modified region. 672 */ 673 private void updateSpellCheckSpans(int start, int end, boolean createSpellChecker) { 674 // Remove spans whose adjacent characters are text not punctuation 675 mTextView.removeAdjacentSuggestionSpans(start); 676 mTextView.removeAdjacentSuggestionSpans(end); 677 678 if (mTextView.isTextEditable() && mTextView.isSuggestionsEnabled() && 679 !(mTextView.isInExtractedMode())) { 680 if (mSpellChecker == null && createSpellChecker) { 681 mSpellChecker = new SpellChecker(mTextView); 682 } 683 if (mSpellChecker != null) { 684 mSpellChecker.spellCheck(start, end); 685 } 686 } 687 } 688 689 void onScreenStateChanged(int screenState) { 690 switch (screenState) { 691 case View.SCREEN_STATE_ON: 692 resumeBlink(); 693 break; 694 case View.SCREEN_STATE_OFF: 695 suspendBlink(); 696 break; 697 } 698 } 699 700 private void suspendBlink() { 701 if (mBlink != null) { 702 mBlink.cancel(); 703 } 704 } 705 706 private void resumeBlink() { 707 if (mBlink != null) { 708 mBlink.uncancel(); 709 makeBlink(); 710 } 711 } 712 713 void adjustInputType(boolean password, boolean passwordInputType, 714 boolean webPasswordInputType, boolean numberPasswordInputType) { 715 // mInputType has been set from inputType, possibly modified by mInputMethod. 716 // Specialize mInputType to [web]password if we have a text class and the original input 717 // type was a password. 718 if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_TEXT) { 719 if (password || passwordInputType) { 720 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 721 | EditorInfo.TYPE_TEXT_VARIATION_PASSWORD; 722 } 723 if (webPasswordInputType) { 724 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 725 | EditorInfo.TYPE_TEXT_VARIATION_WEB_PASSWORD; 726 } 727 } else if ((mInputType & EditorInfo.TYPE_MASK_CLASS) == EditorInfo.TYPE_CLASS_NUMBER) { 728 if (numberPasswordInputType) { 729 mInputType = (mInputType & ~(EditorInfo.TYPE_MASK_VARIATION)) 730 | EditorInfo.TYPE_NUMBER_VARIATION_PASSWORD; 731 } 732 } 733 } 734 735 private void chooseSize(PopupWindow pop, CharSequence text, TextView tv) { 736 int wid = tv.getPaddingLeft() + tv.getPaddingRight(); 737 int ht = tv.getPaddingTop() + tv.getPaddingBottom(); 738 739 int defaultWidthInPixels = mTextView.getResources().getDimensionPixelSize( 740 com.android.internal.R.dimen.textview_error_popup_default_width); 741 Layout l = new StaticLayout(text, tv.getPaint(), defaultWidthInPixels, 742 Layout.Alignment.ALIGN_NORMAL, 1, 0, true); 743 float max = 0; 744 for (int i = 0; i < l.getLineCount(); i++) { 745 max = Math.max(max, l.getLineWidth(i)); 746 } 747 748 /* 749 * Now set the popup size to be big enough for the text plus the border capped 750 * to DEFAULT_MAX_POPUP_WIDTH 751 */ 752 pop.setWidth(wid + (int) Math.ceil(max)); 753 pop.setHeight(ht + l.getHeight()); 754 } 755 756 void setFrame() { 757 if (mErrorPopup != null) { 758 TextView tv = (TextView) mErrorPopup.getContentView(); 759 chooseSize(mErrorPopup, mError, tv); 760 mErrorPopup.update(mTextView, getErrorX(), getErrorY(), 761 mErrorPopup.getWidth(), mErrorPopup.getHeight()); 762 } 763 } 764 765 private int getWordStart(int offset) { 766 // FIXME - For this and similar methods we're not doing anything to check if there's 767 // a LocaleSpan in the text, this may be something we should try handling or checking for. 768 int retOffset = getWordIteratorWithText().prevBoundary(offset); 769 if (getWordIteratorWithText().isOnPunctuation(retOffset)) { 770 // On punctuation boundary or within group of punctuation, find punctuation start. 771 retOffset = getWordIteratorWithText().getPunctuationBeginning(offset); 772 } else { 773 // Not on a punctuation boundary, find the word start. 774 retOffset = getWordIteratorWithText().getPrevWordBeginningOnTwoWordsBoundary(offset); 775 } 776 if (retOffset == BreakIterator.DONE) { 777 return offset; 778 } 779 return retOffset; 780 } 781 782 private int getWordEnd(int offset) { 783 int retOffset = getWordIteratorWithText().nextBoundary(offset); 784 if (getWordIteratorWithText().isAfterPunctuation(retOffset)) { 785 // On punctuation boundary or within group of punctuation, find punctuation end. 786 retOffset = getWordIteratorWithText().getPunctuationEnd(offset); 787 } else { 788 // Not on a punctuation boundary, find the word end. 789 retOffset = getWordIteratorWithText().getNextWordEndOnTwoWordBoundary(offset); 790 } 791 if (retOffset == BreakIterator.DONE) { 792 return offset; 793 } 794 return retOffset; 795 } 796 797 private boolean needsToSelectAllToSelectWordOrParagraph() { 798 if (mTextView.hasPasswordTransformationMethod()) { 799 // Always select all on a password field. 800 // Cut/copy menu entries are not available for passwords, but being able to select all 801 // is however useful to delete or paste to replace the entire content. 802 return true; 803 } 804 805 int inputType = mTextView.getInputType(); 806 int klass = inputType & InputType.TYPE_MASK_CLASS; 807 int variation = inputType & InputType.TYPE_MASK_VARIATION; 808 809 // Specific text field types: select the entire text for these 810 if (klass == InputType.TYPE_CLASS_NUMBER || 811 klass == InputType.TYPE_CLASS_PHONE || 812 klass == InputType.TYPE_CLASS_DATETIME || 813 variation == InputType.TYPE_TEXT_VARIATION_URI || 814 variation == InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS || 815 variation == InputType.TYPE_TEXT_VARIATION_WEB_EMAIL_ADDRESS || 816 variation == InputType.TYPE_TEXT_VARIATION_FILTER) { 817 return true; 818 } 819 return false; 820 } 821 822 /** 823 * Adjusts selection to the word under last touch offset. Return true if the operation was 824 * successfully performed. 825 */ 826 private boolean selectCurrentWord() { 827 if (!mTextView.canSelectText()) { 828 return false; 829 } 830 831 if (needsToSelectAllToSelectWordOrParagraph()) { 832 return mTextView.selectAllText(); 833 } 834 835 long lastTouchOffsets = getLastTouchOffsets(); 836 final int minOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); 837 final int maxOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); 838 839 // Safety check in case standard touch event handling has been bypassed 840 if (minOffset < 0 || minOffset > mTextView.getText().length()) return false; 841 if (maxOffset < 0 || maxOffset > mTextView.getText().length()) return false; 842 843 int selectionStart, selectionEnd; 844 845 // If a URLSpan (web address, email, phone...) is found at that position, select it. 846 URLSpan[] urlSpans = ((Spanned) mTextView.getText()). 847 getSpans(minOffset, maxOffset, URLSpan.class); 848 if (urlSpans.length >= 1) { 849 URLSpan urlSpan = urlSpans[0]; 850 selectionStart = ((Spanned) mTextView.getText()).getSpanStart(urlSpan); 851 selectionEnd = ((Spanned) mTextView.getText()).getSpanEnd(urlSpan); 852 } else { 853 // FIXME - We should check if there's a LocaleSpan in the text, this may be 854 // something we should try handling or checking for. 855 final WordIterator wordIterator = getWordIterator(); 856 wordIterator.setCharSequence(mTextView.getText(), minOffset, maxOffset); 857 858 selectionStart = wordIterator.getBeginning(minOffset); 859 selectionEnd = wordIterator.getEnd(maxOffset); 860 861 if (selectionStart == BreakIterator.DONE || selectionEnd == BreakIterator.DONE || 862 selectionStart == selectionEnd) { 863 // Possible when the word iterator does not properly handle the text's language 864 long range = getCharClusterRange(minOffset); 865 selectionStart = TextUtils.unpackRangeStartFromLong(range); 866 selectionEnd = TextUtils.unpackRangeEndFromLong(range); 867 } 868 } 869 870 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 871 return selectionEnd > selectionStart; 872 } 873 874 /** 875 * Adjusts selection to the paragraph under last touch offset. Return true if the operation was 876 * successfully performed. 877 */ 878 private boolean selectCurrentParagraph() { 879 if (!mTextView.canSelectText()) { 880 return false; 881 } 882 883 if (needsToSelectAllToSelectWordOrParagraph()) { 884 return mTextView.selectAllText(); 885 } 886 887 long lastTouchOffsets = getLastTouchOffsets(); 888 final int minLastTouchOffset = TextUtils.unpackRangeStartFromLong(lastTouchOffsets); 889 final int maxLastTouchOffset = TextUtils.unpackRangeEndFromLong(lastTouchOffsets); 890 891 final long paragraphsRange = getParagraphsRange(minLastTouchOffset, maxLastTouchOffset); 892 final int start = TextUtils.unpackRangeStartFromLong(paragraphsRange); 893 final int end = TextUtils.unpackRangeEndFromLong(paragraphsRange); 894 if (start < end) { 895 Selection.setSelection((Spannable) mTextView.getText(), start, end); 896 return true; 897 } 898 return false; 899 } 900 901 /** 902 * Get the minimum range of paragraphs that contains startOffset and endOffset. 903 */ 904 private long getParagraphsRange(int startOffset, int endOffset) { 905 final Layout layout = mTextView.getLayout(); 906 if (layout == null) { 907 return TextUtils.packRangeInLong(-1, -1); 908 } 909 final CharSequence text = mTextView.getText(); 910 int minLine = layout.getLineForOffset(startOffset); 911 // Search paragraph start. 912 while (minLine > 0) { 913 final int prevLineEndOffset = layout.getLineEnd(minLine - 1); 914 if (text.charAt(prevLineEndOffset - 1) == '\n') { 915 break; 916 } 917 minLine--; 918 } 919 int maxLine = layout.getLineForOffset(endOffset); 920 // Search paragraph end. 921 while (maxLine < layout.getLineCount() - 1) { 922 final int lineEndOffset = layout.getLineEnd(maxLine); 923 if (text.charAt(lineEndOffset - 1) == '\n') { 924 break; 925 } 926 maxLine++; 927 } 928 return TextUtils.packRangeInLong(layout.getLineStart(minLine), layout.getLineEnd(maxLine)); 929 } 930 931 void onLocaleChanged() { 932 // Will be re-created on demand in getWordIterator with the proper new locale 933 mWordIterator = null; 934 mWordIteratorWithText = null; 935 } 936 937 /** 938 * @hide 939 */ 940 public WordIterator getWordIterator() { 941 if (mWordIterator == null) { 942 mWordIterator = new WordIterator(mTextView.getTextServicesLocale()); 943 } 944 return mWordIterator; 945 } 946 947 private WordIterator getWordIteratorWithText() { 948 if (mWordIteratorWithText == null) { 949 mWordIteratorWithText = new WordIterator(mTextView.getTextServicesLocale()); 950 mUpdateWordIteratorText = true; 951 } 952 if (mUpdateWordIteratorText) { 953 // FIXME - Shouldn't copy all of the text as only the area of the text relevant 954 // to the user's selection is needed. A possible solution would be to 955 // copy some number N of characters near the selection and then when the 956 // user approaches N then we'd do another copy of the next N characters. 957 CharSequence text = mTextView.getText(); 958 mWordIteratorWithText.setCharSequence(text, 0, text.length()); 959 mUpdateWordIteratorText = false; 960 } 961 return mWordIteratorWithText; 962 } 963 964 private int getNextCursorOffset(int offset, boolean findAfterGivenOffset) { 965 final Layout layout = mTextView.getLayout(); 966 if (layout == null) return offset; 967 final CharSequence text = mTextView.getText(); 968 final int nextOffset = layout.getPaint().getTextRunCursor(text, 0, text.length(), 969 layout.isRtlCharAt(offset) ? Paint.DIRECTION_RTL : Paint.DIRECTION_LTR, 970 offset, findAfterGivenOffset ? Paint.CURSOR_AFTER : Paint.CURSOR_BEFORE); 971 return nextOffset == -1 ? offset : nextOffset; 972 } 973 974 private long getCharClusterRange(int offset) { 975 final int textLength = mTextView.getText().length(); 976 if (offset < textLength) { 977 return TextUtils.packRangeInLong(offset, getNextCursorOffset(offset, true)); 978 } 979 if (offset - 1 >= 0) { 980 return TextUtils.packRangeInLong(getNextCursorOffset(offset, false), offset); 981 } 982 return TextUtils.packRangeInLong(offset, offset); 983 } 984 985 private boolean touchPositionIsInSelection() { 986 int selectionStart = mTextView.getSelectionStart(); 987 int selectionEnd = mTextView.getSelectionEnd(); 988 989 if (selectionStart == selectionEnd) { 990 return false; 991 } 992 993 if (selectionStart > selectionEnd) { 994 int tmp = selectionStart; 995 selectionStart = selectionEnd; 996 selectionEnd = tmp; 997 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 998 } 999 1000 SelectionModifierCursorController selectionController = getSelectionController(); 1001 int minOffset = selectionController.getMinTouchOffset(); 1002 int maxOffset = selectionController.getMaxTouchOffset(); 1003 1004 return ((minOffset >= selectionStart) && (maxOffset < selectionEnd)); 1005 } 1006 1007 private PositionListener getPositionListener() { 1008 if (mPositionListener == null) { 1009 mPositionListener = new PositionListener(); 1010 } 1011 return mPositionListener; 1012 } 1013 1014 private interface TextViewPositionListener { 1015 public void updatePosition(int parentPositionX, int parentPositionY, 1016 boolean parentPositionChanged, boolean parentScrolled); 1017 } 1018 1019 private boolean isPositionVisible(final float positionX, final float positionY) { 1020 synchronized (TEMP_POSITION) { 1021 final float[] position = TEMP_POSITION; 1022 position[0] = positionX; 1023 position[1] = positionY; 1024 View view = mTextView; 1025 1026 while (view != null) { 1027 if (view != mTextView) { 1028 // Local scroll is already taken into account in positionX/Y 1029 position[0] -= view.getScrollX(); 1030 position[1] -= view.getScrollY(); 1031 } 1032 1033 if (position[0] < 0 || position[1] < 0 || 1034 position[0] > view.getWidth() || position[1] > view.getHeight()) { 1035 return false; 1036 } 1037 1038 if (!view.getMatrix().isIdentity()) { 1039 view.getMatrix().mapPoints(position); 1040 } 1041 1042 position[0] += view.getLeft(); 1043 position[1] += view.getTop(); 1044 1045 final ViewParent parent = view.getParent(); 1046 if (parent instanceof View) { 1047 view = (View) parent; 1048 } else { 1049 // We've reached the ViewRoot, stop iterating 1050 view = null; 1051 } 1052 } 1053 } 1054 1055 // We've been able to walk up the view hierarchy and the position was never clipped 1056 return true; 1057 } 1058 1059 private boolean isOffsetVisible(int offset) { 1060 Layout layout = mTextView.getLayout(); 1061 if (layout == null) return false; 1062 1063 final int line = layout.getLineForOffset(offset); 1064 final int lineBottom = layout.getLineBottom(line); 1065 final int primaryHorizontal = (int) layout.getPrimaryHorizontal(offset); 1066 return isPositionVisible(primaryHorizontal + mTextView.viewportToContentHorizontalOffset(), 1067 lineBottom + mTextView.viewportToContentVerticalOffset()); 1068 } 1069 1070 /** Returns true if the screen coordinates position (x,y) corresponds to a character displayed 1071 * in the view. Returns false when the position is in the empty space of left/right of text. 1072 */ 1073 private boolean isPositionOnText(float x, float y) { 1074 Layout layout = mTextView.getLayout(); 1075 if (layout == null) return false; 1076 1077 final int line = mTextView.getLineAtCoordinate(y); 1078 x = mTextView.convertToLocalHorizontalCoordinate(x); 1079 1080 if (x < layout.getLineLeft(line)) return false; 1081 if (x > layout.getLineRight(line)) return false; 1082 return true; 1083 } 1084 1085 private void startDragAndDrop() { 1086 final int start = mTextView.getSelectionStart(); 1087 final int end = mTextView.getSelectionEnd(); 1088 CharSequence selectedText = mTextView.getTransformedText(start, end); 1089 ClipData data = ClipData.newPlainText(null, selectedText); 1090 DragLocalState localState = new DragLocalState(mTextView, start, end); 1091 mTextView.startDragAndDrop(data, getTextThumbnailBuilder(selectedText), localState, 1092 View.DRAG_FLAG_GLOBAL); 1093 stopTextActionMode(); 1094 if (hasSelectionController()) { 1095 getSelectionController().resetTouchOffsets(); 1096 } 1097 } 1098 1099 public boolean performLongClick(boolean handled) { 1100 // Long press in empty space moves cursor and starts the insertion action mode. 1101 if (!handled && !isPositionOnText(mLastDownPositionX, mLastDownPositionY) && 1102 mInsertionControllerEnabled) { 1103 final int offset = mTextView.getOffsetForPosition(mLastDownPositionX, 1104 mLastDownPositionY); 1105 stopTextActionMode(); 1106 Selection.setSelection((Spannable) mTextView.getText(), offset); 1107 getInsertionController().show(); 1108 mIsInsertionActionModeStartPending = true; 1109 handled = true; 1110 } 1111 1112 if (!handled && mTextActionMode != null) { 1113 if (touchPositionIsInSelection()) { 1114 startDragAndDrop(); 1115 } else { 1116 stopTextActionMode(); 1117 selectCurrentWordAndStartDrag(); 1118 } 1119 handled = true; 1120 } 1121 1122 // Start a new selection 1123 if (!handled) { 1124 handled = selectCurrentWordAndStartDrag(); 1125 } 1126 1127 return handled; 1128 } 1129 1130 private long getLastTouchOffsets() { 1131 SelectionModifierCursorController selectionController = getSelectionController(); 1132 final int minOffset = selectionController.getMinTouchOffset(); 1133 final int maxOffset = selectionController.getMaxTouchOffset(); 1134 return TextUtils.packRangeInLong(minOffset, maxOffset); 1135 } 1136 1137 void onFocusChanged(boolean focused, int direction) { 1138 mShowCursor = SystemClock.uptimeMillis(); 1139 ensureEndedBatchEdit(); 1140 1141 if (focused) { 1142 int selStart = mTextView.getSelectionStart(); 1143 int selEnd = mTextView.getSelectionEnd(); 1144 1145 // SelectAllOnFocus fields are highlighted and not selected. Do not start text selection 1146 // mode for these, unless there was a specific selection already started. 1147 final boolean isFocusHighlighted = mSelectAllOnFocus && selStart == 0 && 1148 selEnd == mTextView.getText().length(); 1149 1150 mCreatedWithASelection = mFrozenWithFocus && mTextView.hasSelection() && 1151 !isFocusHighlighted; 1152 1153 if (!mFrozenWithFocus || (selStart < 0 || selEnd < 0)) { 1154 // If a tap was used to give focus to that view, move cursor at tap position. 1155 // Has to be done before onTakeFocus, which can be overloaded. 1156 final int lastTapPosition = getLastTapPosition(); 1157 if (lastTapPosition >= 0) { 1158 Selection.setSelection((Spannable) mTextView.getText(), lastTapPosition); 1159 } 1160 1161 // Note this may have to be moved out of the Editor class 1162 MovementMethod mMovement = mTextView.getMovementMethod(); 1163 if (mMovement != null) { 1164 mMovement.onTakeFocus(mTextView, (Spannable) mTextView.getText(), direction); 1165 } 1166 1167 // The DecorView does not have focus when the 'Done' ExtractEditText button is 1168 // pressed. Since it is the ViewAncestor's mView, it requests focus before 1169 // ExtractEditText clears focus, which gives focus to the ExtractEditText. 1170 // This special case ensure that we keep current selection in that case. 1171 // It would be better to know why the DecorView does not have focus at that time. 1172 if (((mTextView.isInExtractedMode()) || mSelectionMoved) && 1173 selStart >= 0 && selEnd >= 0) { 1174 /* 1175 * Someone intentionally set the selection, so let them 1176 * do whatever it is that they wanted to do instead of 1177 * the default on-focus behavior. We reset the selection 1178 * here instead of just skipping the onTakeFocus() call 1179 * because some movement methods do something other than 1180 * just setting the selection in theirs and we still 1181 * need to go through that path. 1182 */ 1183 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); 1184 } 1185 1186 if (mSelectAllOnFocus) { 1187 mTextView.selectAllText(); 1188 } 1189 1190 mTouchFocusSelected = true; 1191 } 1192 1193 mFrozenWithFocus = false; 1194 mSelectionMoved = false; 1195 1196 if (mError != null) { 1197 showError(); 1198 } 1199 1200 makeBlink(); 1201 } else { 1202 if (mError != null) { 1203 hideError(); 1204 } 1205 // Don't leave us in the middle of a batch edit. 1206 mTextView.onEndBatchEdit(); 1207 1208 if (mTextView.isInExtractedMode()) { 1209 // terminateTextSelectionMode removes selection, which we want to keep when 1210 // ExtractEditText goes out of focus. 1211 final int selStart = mTextView.getSelectionStart(); 1212 final int selEnd = mTextView.getSelectionEnd(); 1213 hideCursorAndSpanControllers(); 1214 stopTextActionMode(); 1215 Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd); 1216 } else { 1217 if (mTemporaryDetach) mPreserveDetachedSelection = true; 1218 hideCursorAndSpanControllers(); 1219 stopTextActionMode(); 1220 if (mTemporaryDetach) mPreserveDetachedSelection = false; 1221 downgradeEasyCorrectionSpans(); 1222 } 1223 // No need to create the controller 1224 if (mSelectionModifierCursorController != null) { 1225 mSelectionModifierCursorController.resetTouchOffsets(); 1226 } 1227 } 1228 } 1229 1230 /** 1231 * Downgrades to simple suggestions all the easy correction spans that are not a spell check 1232 * span. 1233 */ 1234 private void downgradeEasyCorrectionSpans() { 1235 CharSequence text = mTextView.getText(); 1236 if (text instanceof Spannable) { 1237 Spannable spannable = (Spannable) text; 1238 SuggestionSpan[] suggestionSpans = spannable.getSpans(0, 1239 spannable.length(), SuggestionSpan.class); 1240 for (int i = 0; i < suggestionSpans.length; i++) { 1241 int flags = suggestionSpans[i].getFlags(); 1242 if ((flags & SuggestionSpan.FLAG_EASY_CORRECT) != 0 1243 && (flags & SuggestionSpan.FLAG_MISSPELLED) == 0) { 1244 flags &= ~SuggestionSpan.FLAG_EASY_CORRECT; 1245 suggestionSpans[i].setFlags(flags); 1246 } 1247 } 1248 } 1249 } 1250 1251 void sendOnTextChanged(int start, int after) { 1252 updateSpellCheckSpans(start, start + after, false); 1253 1254 // Flip flag to indicate the word iterator needs to have the text reset. 1255 mUpdateWordIteratorText = true; 1256 1257 // Hide the controllers as soon as text is modified (typing, procedural...) 1258 // We do not hide the span controllers, since they can be added when a new text is 1259 // inserted into the text view (voice IME). 1260 hideCursorControllers(); 1261 // Reset drag accelerator. 1262 if (mSelectionModifierCursorController != null) { 1263 mSelectionModifierCursorController.resetTouchOffsets(); 1264 } 1265 stopTextActionMode(); 1266 } 1267 1268 private int getLastTapPosition() { 1269 // No need to create the controller at that point, no last tap position saved 1270 if (mSelectionModifierCursorController != null) { 1271 int lastTapPosition = mSelectionModifierCursorController.getMinTouchOffset(); 1272 if (lastTapPosition >= 0) { 1273 // Safety check, should not be possible. 1274 if (lastTapPosition > mTextView.getText().length()) { 1275 lastTapPosition = mTextView.getText().length(); 1276 } 1277 return lastTapPosition; 1278 } 1279 } 1280 1281 return -1; 1282 } 1283 1284 void onWindowFocusChanged(boolean hasWindowFocus) { 1285 if (hasWindowFocus) { 1286 if (mBlink != null) { 1287 mBlink.uncancel(); 1288 makeBlink(); 1289 } 1290 final InputMethodManager imm = InputMethodManager.peekInstance(); 1291 final boolean immFullScreen = (imm != null && imm.isFullscreenMode()); 1292 if (mSelectionModifierCursorController != null && mTextView.hasSelection() 1293 && !immFullScreen && mTextActionMode != null) { 1294 mSelectionModifierCursorController.show(); 1295 } 1296 } else { 1297 if (mBlink != null) { 1298 mBlink.cancel(); 1299 } 1300 if (mInputContentType != null) { 1301 mInputContentType.enterDown = false; 1302 } 1303 // Order matters! Must be done before onParentLostFocus to rely on isShowingUp 1304 hideCursorAndSpanControllers(); 1305 if (mSelectionModifierCursorController != null) { 1306 mSelectionModifierCursorController.hide(); 1307 } 1308 if (mSuggestionsPopupWindow != null) { 1309 mSuggestionsPopupWindow.onParentLostFocus(); 1310 } 1311 1312 // Don't leave us in the middle of a batch edit. Same as in onFocusChanged 1313 ensureEndedBatchEdit(); 1314 } 1315 } 1316 1317 private void updateTapState(MotionEvent event) { 1318 final int action = event.getActionMasked(); 1319 if (action == MotionEvent.ACTION_DOWN) { 1320 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 1321 // Detect double tap and triple click. 1322 if (((mTapState == TAP_STATE_FIRST_TAP) 1323 || ((mTapState == TAP_STATE_DOUBLE_TAP) && isMouse)) 1324 && (SystemClock.uptimeMillis() - mLastTouchUpTime) <= 1325 ViewConfiguration.getDoubleTapTimeout()) { 1326 if (mTapState == TAP_STATE_FIRST_TAP) { 1327 mTapState = TAP_STATE_DOUBLE_TAP; 1328 } else { 1329 mTapState = TAP_STATE_TRIPLE_CLICK; 1330 } 1331 } else { 1332 mTapState = TAP_STATE_FIRST_TAP; 1333 } 1334 } 1335 if (action == MotionEvent.ACTION_UP) { 1336 mLastTouchUpTime = SystemClock.uptimeMillis(); 1337 } 1338 } 1339 1340 private boolean shouldFilterOutTouchEvent(MotionEvent event) { 1341 if (!event.isFromSource(InputDevice.SOURCE_MOUSE)) { 1342 return false; 1343 } 1344 final boolean primaryButtonStateChanged = 1345 ((mLastButtonState ^ event.getButtonState()) & MotionEvent.BUTTON_PRIMARY) != 0; 1346 final int action = event.getActionMasked(); 1347 if ((action == MotionEvent.ACTION_DOWN || action == MotionEvent.ACTION_UP) 1348 && !primaryButtonStateChanged) { 1349 return true; 1350 } 1351 if (action == MotionEvent.ACTION_MOVE 1352 && !event.isButtonPressed(MotionEvent.BUTTON_PRIMARY)) { 1353 return true; 1354 } 1355 return false; 1356 } 1357 1358 void onTouchEvent(MotionEvent event) { 1359 final boolean filterOutEvent = shouldFilterOutTouchEvent(event); 1360 mLastButtonState = event.getButtonState(); 1361 if (filterOutEvent) { 1362 if (event.getActionMasked() == MotionEvent.ACTION_UP) { 1363 mDiscardNextActionUp = true; 1364 } 1365 return; 1366 } 1367 updateTapState(event); 1368 updateFloatingToolbarVisibility(event); 1369 1370 if (hasSelectionController()) { 1371 getSelectionController().onTouchEvent(event); 1372 } 1373 1374 if (mShowSuggestionRunnable != null) { 1375 mTextView.removeCallbacks(mShowSuggestionRunnable); 1376 mShowSuggestionRunnable = null; 1377 } 1378 1379 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 1380 mLastDownPositionX = event.getX(); 1381 mLastDownPositionY = event.getY(); 1382 1383 // Reset this state; it will be re-set if super.onTouchEvent 1384 // causes focus to move to the view. 1385 mTouchFocusSelected = false; 1386 mIgnoreActionUpEvent = false; 1387 } 1388 } 1389 1390 private void updateFloatingToolbarVisibility(MotionEvent event) { 1391 if (mTextActionMode != null) { 1392 switch (event.getActionMasked()) { 1393 case MotionEvent.ACTION_MOVE: 1394 hideFloatingToolbar(); 1395 break; 1396 case MotionEvent.ACTION_UP: // fall through 1397 case MotionEvent.ACTION_CANCEL: 1398 showFloatingToolbar(); 1399 } 1400 } 1401 } 1402 1403 private void hideFloatingToolbar() { 1404 if (mTextActionMode != null) { 1405 mTextView.removeCallbacks(mShowFloatingToolbar); 1406 mTextActionMode.hide(ActionMode.DEFAULT_HIDE_DURATION); 1407 } 1408 } 1409 1410 private void showFloatingToolbar() { 1411 if (mTextActionMode != null) { 1412 // Delay "show" so it doesn't interfere with click confirmations 1413 // or double-clicks that could "dismiss" the floating toolbar. 1414 int delay = ViewConfiguration.getDoubleTapTimeout(); 1415 mTextView.postDelayed(mShowFloatingToolbar, delay); 1416 } 1417 } 1418 1419 public void beginBatchEdit() { 1420 mInBatchEditControllers = true; 1421 final InputMethodState ims = mInputMethodState; 1422 if (ims != null) { 1423 int nesting = ++ims.mBatchEditNesting; 1424 if (nesting == 1) { 1425 ims.mCursorChanged = false; 1426 ims.mChangedDelta = 0; 1427 if (ims.mContentChanged) { 1428 // We already have a pending change from somewhere else, 1429 // so turn this into a full update. 1430 ims.mChangedStart = 0; 1431 ims.mChangedEnd = mTextView.getText().length(); 1432 } else { 1433 ims.mChangedStart = EXTRACT_UNKNOWN; 1434 ims.mChangedEnd = EXTRACT_UNKNOWN; 1435 ims.mContentChanged = false; 1436 } 1437 mUndoInputFilter.beginBatchEdit(); 1438 mTextView.onBeginBatchEdit(); 1439 } 1440 } 1441 } 1442 1443 public void endBatchEdit() { 1444 mInBatchEditControllers = false; 1445 final InputMethodState ims = mInputMethodState; 1446 if (ims != null) { 1447 int nesting = --ims.mBatchEditNesting; 1448 if (nesting == 0) { 1449 finishBatchEdit(ims); 1450 } 1451 } 1452 } 1453 1454 void ensureEndedBatchEdit() { 1455 final InputMethodState ims = mInputMethodState; 1456 if (ims != null && ims.mBatchEditNesting != 0) { 1457 ims.mBatchEditNesting = 0; 1458 finishBatchEdit(ims); 1459 } 1460 } 1461 1462 void finishBatchEdit(final InputMethodState ims) { 1463 mTextView.onEndBatchEdit(); 1464 mUndoInputFilter.endBatchEdit(); 1465 1466 if (ims.mContentChanged || ims.mSelectionModeChanged) { 1467 mTextView.updateAfterEdit(); 1468 reportExtractedText(); 1469 } else if (ims.mCursorChanged) { 1470 // Cheesy way to get us to report the current cursor location. 1471 mTextView.invalidateCursor(); 1472 } 1473 // sendUpdateSelection knows to avoid sending if the selection did 1474 // not actually change. 1475 sendUpdateSelection(); 1476 } 1477 1478 static final int EXTRACT_NOTHING = -2; 1479 static final int EXTRACT_UNKNOWN = -1; 1480 1481 boolean extractText(ExtractedTextRequest request, ExtractedText outText) { 1482 return extractTextInternal(request, EXTRACT_UNKNOWN, EXTRACT_UNKNOWN, 1483 EXTRACT_UNKNOWN, outText); 1484 } 1485 1486 private boolean extractTextInternal(@Nullable ExtractedTextRequest request, 1487 int partialStartOffset, int partialEndOffset, int delta, 1488 @Nullable ExtractedText outText) { 1489 if (request == null || outText == null) { 1490 return false; 1491 } 1492 1493 final CharSequence content = mTextView.getText(); 1494 if (content == null) { 1495 return false; 1496 } 1497 1498 if (partialStartOffset != EXTRACT_NOTHING) { 1499 final int N = content.length(); 1500 if (partialStartOffset < 0) { 1501 outText.partialStartOffset = outText.partialEndOffset = -1; 1502 partialStartOffset = 0; 1503 partialEndOffset = N; 1504 } else { 1505 // Now use the delta to determine the actual amount of text 1506 // we need. 1507 partialEndOffset += delta; 1508 // Adjust offsets to ensure we contain full spans. 1509 if (content instanceof Spanned) { 1510 Spanned spanned = (Spanned)content; 1511 Object[] spans = spanned.getSpans(partialStartOffset, 1512 partialEndOffset, ParcelableSpan.class); 1513 int i = spans.length; 1514 while (i > 0) { 1515 i--; 1516 int j = spanned.getSpanStart(spans[i]); 1517 if (j < partialStartOffset) partialStartOffset = j; 1518 j = spanned.getSpanEnd(spans[i]); 1519 if (j > partialEndOffset) partialEndOffset = j; 1520 } 1521 } 1522 outText.partialStartOffset = partialStartOffset; 1523 outText.partialEndOffset = partialEndOffset - delta; 1524 1525 if (partialStartOffset > N) { 1526 partialStartOffset = N; 1527 } else if (partialStartOffset < 0) { 1528 partialStartOffset = 0; 1529 } 1530 if (partialEndOffset > N) { 1531 partialEndOffset = N; 1532 } else if (partialEndOffset < 0) { 1533 partialEndOffset = 0; 1534 } 1535 } 1536 if ((request.flags&InputConnection.GET_TEXT_WITH_STYLES) != 0) { 1537 outText.text = content.subSequence(partialStartOffset, 1538 partialEndOffset); 1539 } else { 1540 outText.text = TextUtils.substring(content, partialStartOffset, 1541 partialEndOffset); 1542 } 1543 } else { 1544 outText.partialStartOffset = 0; 1545 outText.partialEndOffset = 0; 1546 outText.text = ""; 1547 } 1548 outText.flags = 0; 1549 if (MetaKeyKeyListener.getMetaState(content, MetaKeyKeyListener.META_SELECTING) != 0) { 1550 outText.flags |= ExtractedText.FLAG_SELECTING; 1551 } 1552 if (mTextView.isSingleLine()) { 1553 outText.flags |= ExtractedText.FLAG_SINGLE_LINE; 1554 } 1555 outText.startOffset = 0; 1556 outText.selectionStart = mTextView.getSelectionStart(); 1557 outText.selectionEnd = mTextView.getSelectionEnd(); 1558 return true; 1559 } 1560 1561 boolean reportExtractedText() { 1562 final Editor.InputMethodState ims = mInputMethodState; 1563 if (ims != null) { 1564 final boolean contentChanged = ims.mContentChanged; 1565 if (contentChanged || ims.mSelectionModeChanged) { 1566 ims.mContentChanged = false; 1567 ims.mSelectionModeChanged = false; 1568 final ExtractedTextRequest req = ims.mExtractedTextRequest; 1569 if (req != null) { 1570 InputMethodManager imm = InputMethodManager.peekInstance(); 1571 if (imm != null) { 1572 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, 1573 "Retrieving extracted start=" + ims.mChangedStart + 1574 " end=" + ims.mChangedEnd + 1575 " delta=" + ims.mChangedDelta); 1576 if (ims.mChangedStart < 0 && !contentChanged) { 1577 ims.mChangedStart = EXTRACT_NOTHING; 1578 } 1579 if (extractTextInternal(req, ims.mChangedStart, ims.mChangedEnd, 1580 ims.mChangedDelta, ims.mExtractedText)) { 1581 if (TextView.DEBUG_EXTRACT) Log.v(TextView.LOG_TAG, 1582 "Reporting extracted start=" + 1583 ims.mExtractedText.partialStartOffset + 1584 " end=" + ims.mExtractedText.partialEndOffset + 1585 ": " + ims.mExtractedText.text); 1586 1587 imm.updateExtractedText(mTextView, req.token, ims.mExtractedText); 1588 ims.mChangedStart = EXTRACT_UNKNOWN; 1589 ims.mChangedEnd = EXTRACT_UNKNOWN; 1590 ims.mChangedDelta = 0; 1591 ims.mContentChanged = false; 1592 return true; 1593 } 1594 } 1595 } 1596 } 1597 } 1598 return false; 1599 } 1600 1601 private void sendUpdateSelection() { 1602 if (null != mInputMethodState && mInputMethodState.mBatchEditNesting <= 0) { 1603 final InputMethodManager imm = InputMethodManager.peekInstance(); 1604 if (null != imm) { 1605 final int selectionStart = mTextView.getSelectionStart(); 1606 final int selectionEnd = mTextView.getSelectionEnd(); 1607 int candStart = -1; 1608 int candEnd = -1; 1609 if (mTextView.getText() instanceof Spannable) { 1610 final Spannable sp = (Spannable) mTextView.getText(); 1611 candStart = EditableInputConnection.getComposingSpanStart(sp); 1612 candEnd = EditableInputConnection.getComposingSpanEnd(sp); 1613 } 1614 // InputMethodManager#updateSelection skips sending the message if 1615 // none of the parameters have changed since the last time we called it. 1616 imm.updateSelection(mTextView, 1617 selectionStart, selectionEnd, candStart, candEnd); 1618 } 1619 } 1620 } 1621 1622 void onDraw(Canvas canvas, Layout layout, Path highlight, Paint highlightPaint, 1623 int cursorOffsetVertical) { 1624 final int selectionStart = mTextView.getSelectionStart(); 1625 final int selectionEnd = mTextView.getSelectionEnd(); 1626 1627 final InputMethodState ims = mInputMethodState; 1628 if (ims != null && ims.mBatchEditNesting == 0) { 1629 InputMethodManager imm = InputMethodManager.peekInstance(); 1630 if (imm != null) { 1631 if (imm.isActive(mTextView)) { 1632 if (ims.mContentChanged || ims.mSelectionModeChanged) { 1633 // We are in extract mode and the content has changed 1634 // in some way... just report complete new text to the 1635 // input method. 1636 reportExtractedText(); 1637 } 1638 } 1639 } 1640 } 1641 1642 if (mCorrectionHighlighter != null) { 1643 mCorrectionHighlighter.draw(canvas, cursorOffsetVertical); 1644 } 1645 1646 if (highlight != null && selectionStart == selectionEnd && mCursorCount > 0) { 1647 drawCursor(canvas, cursorOffsetVertical); 1648 // Rely on the drawable entirely, do not draw the cursor line. 1649 // Has to be done after the IMM related code above which relies on the highlight. 1650 highlight = null; 1651 } 1652 1653 if (mTextView.canHaveDisplayList() && canvas.isHardwareAccelerated()) { 1654 drawHardwareAccelerated(canvas, layout, highlight, highlightPaint, 1655 cursorOffsetVertical); 1656 } else { 1657 layout.draw(canvas, highlight, highlightPaint, cursorOffsetVertical); 1658 } 1659 } 1660 1661 private void drawHardwareAccelerated(Canvas canvas, Layout layout, Path highlight, 1662 Paint highlightPaint, int cursorOffsetVertical) { 1663 final long lineRange = layout.getLineRangeForDraw(canvas); 1664 int firstLine = TextUtils.unpackRangeStartFromLong(lineRange); 1665 int lastLine = TextUtils.unpackRangeEndFromLong(lineRange); 1666 if (lastLine < 0) return; 1667 1668 layout.drawBackground(canvas, highlight, highlightPaint, cursorOffsetVertical, 1669 firstLine, lastLine); 1670 1671 if (layout instanceof DynamicLayout) { 1672 if (mTextRenderNodes == null) { 1673 mTextRenderNodes = ArrayUtils.emptyArray(TextRenderNode.class); 1674 } 1675 1676 DynamicLayout dynamicLayout = (DynamicLayout) layout; 1677 int[] blockEndLines = dynamicLayout.getBlockEndLines(); 1678 int[] blockIndices = dynamicLayout.getBlockIndices(); 1679 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); 1680 final int indexFirstChangedBlock = dynamicLayout.getIndexFirstChangedBlock(); 1681 1682 int endOfPreviousBlock = -1; 1683 int searchStartIndex = 0; 1684 for (int i = 0; i < numberOfBlocks; i++) { 1685 int blockEndLine = blockEndLines[i]; 1686 int blockIndex = blockIndices[i]; 1687 1688 final boolean blockIsInvalid = blockIndex == DynamicLayout.INVALID_BLOCK_INDEX; 1689 if (blockIsInvalid) { 1690 blockIndex = getAvailableDisplayListIndex(blockIndices, numberOfBlocks, 1691 searchStartIndex); 1692 // Note how dynamic layout's internal block indices get updated from Editor 1693 blockIndices[i] = blockIndex; 1694 if (mTextRenderNodes[blockIndex] != null) { 1695 mTextRenderNodes[blockIndex].isDirty = true; 1696 } 1697 searchStartIndex = blockIndex + 1; 1698 } 1699 1700 if (mTextRenderNodes[blockIndex] == null) { 1701 mTextRenderNodes[blockIndex] = 1702 new TextRenderNode("Text " + blockIndex); 1703 } 1704 1705 final boolean blockDisplayListIsInvalid = mTextRenderNodes[blockIndex].needsRecord(); 1706 RenderNode blockDisplayList = mTextRenderNodes[blockIndex].renderNode; 1707 if (i >= indexFirstChangedBlock || blockDisplayListIsInvalid) { 1708 final int blockBeginLine = endOfPreviousBlock + 1; 1709 final int top = layout.getLineTop(blockBeginLine); 1710 final int bottom = layout.getLineBottom(blockEndLine); 1711 int left = 0; 1712 int right = mTextView.getWidth(); 1713 if (mTextView.getHorizontallyScrolling()) { 1714 float min = Float.MAX_VALUE; 1715 float max = Float.MIN_VALUE; 1716 for (int line = blockBeginLine; line <= blockEndLine; line++) { 1717 min = Math.min(min, layout.getLineLeft(line)); 1718 max = Math.max(max, layout.getLineRight(line)); 1719 } 1720 left = (int) min; 1721 right = (int) (max + 0.5f); 1722 } 1723 1724 // Rebuild display list if it is invalid 1725 if (blockDisplayListIsInvalid) { 1726 final DisplayListCanvas displayListCanvas = blockDisplayList.start( 1727 right - left, bottom - top); 1728 try { 1729 // drawText is always relative to TextView's origin, this translation 1730 // brings this range of text back to the top left corner of the viewport 1731 displayListCanvas.translate(-left, -top); 1732 layout.drawText(displayListCanvas, blockBeginLine, blockEndLine); 1733 mTextRenderNodes[blockIndex].isDirty = false; 1734 // No need to untranslate, previous context is popped after 1735 // drawDisplayList 1736 } finally { 1737 blockDisplayList.end(displayListCanvas); 1738 // Same as drawDisplayList below, handled by our TextView's parent 1739 blockDisplayList.setClipToBounds(false); 1740 } 1741 } 1742 1743 // Valid disply list whose index is >= indexFirstChangedBlock 1744 // only needs to update its drawing location. 1745 blockDisplayList.setLeftTopRightBottom(left, top, right, bottom); 1746 } 1747 1748 ((DisplayListCanvas) canvas).drawRenderNode(blockDisplayList); 1749 1750 endOfPreviousBlock = blockEndLine; 1751 } 1752 1753 dynamicLayout.setIndexFirstChangedBlock(numberOfBlocks); 1754 } else { 1755 // Boring layout is used for empty and hint text 1756 layout.drawText(canvas, firstLine, lastLine); 1757 } 1758 } 1759 1760 private int getAvailableDisplayListIndex(int[] blockIndices, int numberOfBlocks, 1761 int searchStartIndex) { 1762 int length = mTextRenderNodes.length; 1763 for (int i = searchStartIndex; i < length; i++) { 1764 boolean blockIndexFound = false; 1765 for (int j = 0; j < numberOfBlocks; j++) { 1766 if (blockIndices[j] == i) { 1767 blockIndexFound = true; 1768 break; 1769 } 1770 } 1771 if (blockIndexFound) continue; 1772 return i; 1773 } 1774 1775 // No available index found, the pool has to grow 1776 mTextRenderNodes = GrowingArrayUtils.append(mTextRenderNodes, length, null); 1777 return length; 1778 } 1779 1780 private void drawCursor(Canvas canvas, int cursorOffsetVertical) { 1781 final boolean translate = cursorOffsetVertical != 0; 1782 if (translate) canvas.translate(0, cursorOffsetVertical); 1783 for (int i = 0; i < mCursorCount; i++) { 1784 mCursorDrawable[i].draw(canvas); 1785 } 1786 if (translate) canvas.translate(0, -cursorOffsetVertical); 1787 } 1788 1789 /** 1790 * Invalidates all the sub-display lists that overlap the specified character range 1791 */ 1792 void invalidateTextDisplayList(Layout layout, int start, int end) { 1793 if (mTextRenderNodes != null && layout instanceof DynamicLayout) { 1794 final int firstLine = layout.getLineForOffset(start); 1795 final int lastLine = layout.getLineForOffset(end); 1796 1797 DynamicLayout dynamicLayout = (DynamicLayout) layout; 1798 int[] blockEndLines = dynamicLayout.getBlockEndLines(); 1799 int[] blockIndices = dynamicLayout.getBlockIndices(); 1800 final int numberOfBlocks = dynamicLayout.getNumberOfBlocks(); 1801 1802 int i = 0; 1803 // Skip the blocks before firstLine 1804 while (i < numberOfBlocks) { 1805 if (blockEndLines[i] >= firstLine) break; 1806 i++; 1807 } 1808 1809 // Invalidate all subsequent blocks until lastLine is passed 1810 while (i < numberOfBlocks) { 1811 final int blockIndex = blockIndices[i]; 1812 if (blockIndex != DynamicLayout.INVALID_BLOCK_INDEX) { 1813 mTextRenderNodes[blockIndex].isDirty = true; 1814 } 1815 if (blockEndLines[i] >= lastLine) break; 1816 i++; 1817 } 1818 } 1819 } 1820 1821 void invalidateTextDisplayList() { 1822 if (mTextRenderNodes != null) { 1823 for (int i = 0; i < mTextRenderNodes.length; i++) { 1824 if (mTextRenderNodes[i] != null) mTextRenderNodes[i].isDirty = true; 1825 } 1826 } 1827 } 1828 1829 void updateCursorsPositions() { 1830 if (mTextView.mCursorDrawableRes == 0) { 1831 mCursorCount = 0; 1832 return; 1833 } 1834 1835 Layout layout = getActiveLayout(); 1836 final int offset = mTextView.getSelectionStart(); 1837 final int line = layout.getLineForOffset(offset); 1838 final int top = layout.getLineTop(line); 1839 final int bottom = layout.getLineTop(line + 1); 1840 1841 mCursorCount = layout.isLevelBoundary(offset) ? 2 : 1; 1842 1843 int middle = bottom; 1844 if (mCursorCount == 2) { 1845 // Similar to what is done in {@link Layout.#getCursorPath(int, Path, CharSequence)} 1846 middle = (top + bottom) >> 1; 1847 } 1848 1849 boolean clamped = layout.shouldClampCursor(line); 1850 updateCursorPosition(0, top, middle, layout.getPrimaryHorizontal(offset, clamped)); 1851 1852 if (mCursorCount == 2) { 1853 updateCursorPosition(1, middle, bottom, 1854 layout.getSecondaryHorizontal(offset, clamped)); 1855 } 1856 } 1857 1858 /** 1859 * Start an Insertion action mode. 1860 */ 1861 void startInsertionActionMode() { 1862 if (mInsertionActionModeRunnable != null) { 1863 mTextView.removeCallbacks(mInsertionActionModeRunnable); 1864 } 1865 if (extractedTextModeWillBeStarted()) { 1866 return; 1867 } 1868 stopTextActionMode(); 1869 1870 ActionMode.Callback actionModeCallback = 1871 new TextActionModeCallback(false /* hasSelection */); 1872 mTextActionMode = mTextView.startActionMode( 1873 actionModeCallback, ActionMode.TYPE_FLOATING); 1874 if (mTextActionMode != null && getInsertionController() != null) { 1875 getInsertionController().show(); 1876 } 1877 } 1878 1879 /** 1880 * Starts a Selection Action Mode with the current selection and ensures the selection handles 1881 * are shown if there is a selection, otherwise the insertion handle is shown. This should be 1882 * used when the mode is started from a non-touch event. 1883 * 1884 * @return true if the selection mode was actually started. 1885 */ 1886 boolean startSelectionActionMode() { 1887 boolean selectionStarted = startSelectionActionModeInternal(); 1888 if (selectionStarted) { 1889 getSelectionController().show(); 1890 } else if (getInsertionController() != null) { 1891 getInsertionController().show(); 1892 } 1893 return selectionStarted; 1894 } 1895 1896 /** 1897 * If the TextView allows text selection, selects the current word when no existing selection 1898 * was available and starts a drag. 1899 * 1900 * @return true if the drag was started. 1901 */ 1902 private boolean selectCurrentWordAndStartDrag() { 1903 if (mInsertionActionModeRunnable != null) { 1904 mTextView.removeCallbacks(mInsertionActionModeRunnable); 1905 } 1906 if (extractedTextModeWillBeStarted()) { 1907 return false; 1908 } 1909 if (mTextActionMode != null) { 1910 mTextActionMode.finish(); 1911 } 1912 if (!checkFieldAndSelectCurrentWord()) { 1913 return false; 1914 } 1915 1916 // Avoid dismissing the selection if it exists. 1917 mPreserveDetachedSelection = true; 1918 stopTextActionMode(); 1919 mPreserveDetachedSelection = false; 1920 1921 getSelectionController().enterDrag( 1922 SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD); 1923 return true; 1924 } 1925 1926 /** 1927 * Checks whether a selection can be performed on the current TextView and if so selects 1928 * the current word. 1929 * 1930 * @return true if there already was a selection or if the current word was selected. 1931 */ 1932 boolean checkFieldAndSelectCurrentWord() { 1933 if (!mTextView.canSelectText() || !mTextView.requestFocus()) { 1934 Log.w(TextView.LOG_TAG, 1935 "TextView does not support text selection. Selection cancelled."); 1936 return false; 1937 } 1938 1939 if (!mTextView.hasSelection()) { 1940 // There may already be a selection on device rotation 1941 return selectCurrentWord(); 1942 } 1943 return true; 1944 } 1945 1946 private boolean startSelectionActionModeInternal() { 1947 if (mTextActionMode != null) { 1948 // Text action mode is already started 1949 mTextActionMode.invalidate(); 1950 return false; 1951 } 1952 1953 if (!checkFieldAndSelectCurrentWord()) { 1954 return false; 1955 } 1956 1957 boolean willExtract = extractedTextModeWillBeStarted(); 1958 1959 // Do not start the action mode when extracted text will show up full screen, which would 1960 // immediately hide the newly created action bar and would be visually distracting. 1961 if (!willExtract) { 1962 ActionMode.Callback actionModeCallback = 1963 new TextActionModeCallback(true /* hasSelection */); 1964 mTextActionMode = mTextView.startActionMode( 1965 actionModeCallback, ActionMode.TYPE_FLOATING); 1966 } 1967 1968 final boolean selectionStarted = mTextActionMode != null || willExtract; 1969 if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) { 1970 // Show the IME to be able to replace text, except when selecting non editable text. 1971 final InputMethodManager imm = InputMethodManager.peekInstance(); 1972 if (imm != null) { 1973 imm.showSoftInput(mTextView, 0, null); 1974 } 1975 } 1976 return selectionStarted; 1977 } 1978 1979 boolean extractedTextModeWillBeStarted() { 1980 if (!(mTextView.isInExtractedMode())) { 1981 final InputMethodManager imm = InputMethodManager.peekInstance(); 1982 return imm != null && imm.isFullscreenMode(); 1983 } 1984 return false; 1985 } 1986 1987 /** 1988 * @return <code>true</code> if it's reasonable to offer to show suggestions depending on 1989 * the current cursor position or selection range. This method is consistent with the 1990 * method to show suggestions {@link SuggestionsPopupWindow#updateSuggestions}. 1991 */ 1992 private boolean shouldOfferToShowSuggestions() { 1993 CharSequence text = mTextView.getText(); 1994 if (!(text instanceof Spannable)) return false; 1995 1996 final Spannable spannable = (Spannable) text; 1997 final int selectionStart = mTextView.getSelectionStart(); 1998 final int selectionEnd = mTextView.getSelectionEnd(); 1999 final SuggestionSpan[] suggestionSpans = spannable.getSpans(selectionStart, selectionEnd, 2000 SuggestionSpan.class); 2001 if (suggestionSpans.length == 0) { 2002 return false; 2003 } 2004 if (selectionStart == selectionEnd) { 2005 // Spans overlap the cursor. 2006 for (int i = 0; i < suggestionSpans.length; i++) { 2007 if (suggestionSpans[i].getSuggestions().length > 0) { 2008 return true; 2009 } 2010 } 2011 return false; 2012 } 2013 int minSpanStart = mTextView.getText().length(); 2014 int maxSpanEnd = 0; 2015 int unionOfSpansCoveringSelectionStartStart = mTextView.getText().length(); 2016 int unionOfSpansCoveringSelectionStartEnd = 0; 2017 boolean hasValidSuggestions = false; 2018 for (int i = 0; i < suggestionSpans.length; i++) { 2019 final int spanStart = spannable.getSpanStart(suggestionSpans[i]); 2020 final int spanEnd = spannable.getSpanEnd(suggestionSpans[i]); 2021 minSpanStart = Math.min(minSpanStart, spanStart); 2022 maxSpanEnd = Math.max(maxSpanEnd, spanEnd); 2023 if (selectionStart < spanStart || selectionStart > spanEnd) { 2024 // The span doesn't cover the current selection start point. 2025 continue; 2026 } 2027 hasValidSuggestions = 2028 hasValidSuggestions || suggestionSpans[i].getSuggestions().length > 0; 2029 unionOfSpansCoveringSelectionStartStart = 2030 Math.min(unionOfSpansCoveringSelectionStartStart, spanStart); 2031 unionOfSpansCoveringSelectionStartEnd = 2032 Math.max(unionOfSpansCoveringSelectionStartEnd, spanEnd); 2033 } 2034 if (!hasValidSuggestions) { 2035 return false; 2036 } 2037 if (unionOfSpansCoveringSelectionStartStart >= unionOfSpansCoveringSelectionStartEnd) { 2038 // No spans cover the selection start point. 2039 return false; 2040 } 2041 if (minSpanStart < unionOfSpansCoveringSelectionStartStart 2042 || maxSpanEnd > unionOfSpansCoveringSelectionStartEnd) { 2043 // There is a span that is not covered by the union. In this case, we soouldn't offer 2044 // to show suggestions as it's confusing. 2045 return false; 2046 } 2047 return true; 2048 } 2049 2050 /** 2051 * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with 2052 * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. 2053 */ 2054 private boolean isCursorInsideEasyCorrectionSpan() { 2055 Spannable spannable = (Spannable) mTextView.getText(); 2056 SuggestionSpan[] suggestionSpans = spannable.getSpans(mTextView.getSelectionStart(), 2057 mTextView.getSelectionEnd(), SuggestionSpan.class); 2058 for (int i = 0; i < suggestionSpans.length; i++) { 2059 if ((suggestionSpans[i].getFlags() & SuggestionSpan.FLAG_EASY_CORRECT) != 0) { 2060 return true; 2061 } 2062 } 2063 return false; 2064 } 2065 2066 void onTouchUpEvent(MotionEvent event) { 2067 boolean selectAllGotFocus = mSelectAllOnFocus && mTextView.didTouchFocusSelect(); 2068 hideCursorAndSpanControllers(); 2069 stopTextActionMode(); 2070 CharSequence text = mTextView.getText(); 2071 if (!selectAllGotFocus && text.length() > 0) { 2072 // Move cursor 2073 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 2074 Selection.setSelection((Spannable) text, offset); 2075 if (mSpellChecker != null) { 2076 // When the cursor moves, the word that was typed may need spell check 2077 mSpellChecker.onSelectionChanged(); 2078 } 2079 2080 if (!extractedTextModeWillBeStarted()) { 2081 if (isCursorInsideEasyCorrectionSpan()) { 2082 // Cancel the single tap delayed runnable. 2083 if (mInsertionActionModeRunnable != null) { 2084 mTextView.removeCallbacks(mInsertionActionModeRunnable); 2085 } 2086 2087 mShowSuggestionRunnable = new Runnable() { 2088 public void run() { 2089 showSuggestions(); 2090 } 2091 }; 2092 // removeCallbacks is performed on every touch 2093 mTextView.postDelayed(mShowSuggestionRunnable, 2094 ViewConfiguration.getDoubleTapTimeout()); 2095 } else if (hasInsertionController()) { 2096 getInsertionController().show(); 2097 } 2098 } 2099 } 2100 } 2101 2102 protected void stopTextActionMode() { 2103 if (mTextActionMode != null) { 2104 // This will hide the mSelectionModifierCursorController 2105 mTextActionMode.finish(); 2106 } 2107 } 2108 2109 /** 2110 * @return True if this view supports insertion handles. 2111 */ 2112 boolean hasInsertionController() { 2113 return mInsertionControllerEnabled; 2114 } 2115 2116 /** 2117 * @return True if this view supports selection handles. 2118 */ 2119 boolean hasSelectionController() { 2120 return mSelectionControllerEnabled; 2121 } 2122 2123 InsertionPointCursorController getInsertionController() { 2124 if (!mInsertionControllerEnabled) { 2125 return null; 2126 } 2127 2128 if (mInsertionPointCursorController == null) { 2129 mInsertionPointCursorController = new InsertionPointCursorController(); 2130 2131 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 2132 observer.addOnTouchModeChangeListener(mInsertionPointCursorController); 2133 } 2134 2135 return mInsertionPointCursorController; 2136 } 2137 2138 SelectionModifierCursorController getSelectionController() { 2139 if (!mSelectionControllerEnabled) { 2140 return null; 2141 } 2142 2143 if (mSelectionModifierCursorController == null) { 2144 mSelectionModifierCursorController = new SelectionModifierCursorController(); 2145 2146 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 2147 observer.addOnTouchModeChangeListener(mSelectionModifierCursorController); 2148 } 2149 2150 return mSelectionModifierCursorController; 2151 } 2152 2153 private void updateCursorPosition(int cursorIndex, int top, int bottom, float horizontal) { 2154 if (mCursorDrawable[cursorIndex] == null) 2155 mCursorDrawable[cursorIndex] = mTextView.getContext().getDrawable( 2156 mTextView.mCursorDrawableRes); 2157 2158 if (mTempRect == null) mTempRect = new Rect(); 2159 mCursorDrawable[cursorIndex].getPadding(mTempRect); 2160 final int width = mCursorDrawable[cursorIndex].getIntrinsicWidth(); 2161 horizontal = Math.max(0.5f, horizontal - 0.5f); 2162 final int left = (int) (horizontal) - mTempRect.left; 2163 mCursorDrawable[cursorIndex].setBounds(left, top - mTempRect.top, left + width, 2164 bottom + mTempRect.bottom); 2165 } 2166 2167 /** 2168 * Called by the framework in response to a text auto-correction (such as fixing a typo using a 2169 * a dictionary) from the current input method, provided by it calling 2170 * {@link InputConnection#commitCorrection} InputConnection.commitCorrection()}. The default 2171 * implementation flashes the background of the corrected word to provide feedback to the user. 2172 * 2173 * @param info The auto correct info about the text that was corrected. 2174 */ 2175 public void onCommitCorrection(CorrectionInfo info) { 2176 if (mCorrectionHighlighter == null) { 2177 mCorrectionHighlighter = new CorrectionHighlighter(); 2178 } else { 2179 mCorrectionHighlighter.invalidate(false); 2180 } 2181 2182 mCorrectionHighlighter.highlight(info); 2183 } 2184 2185 void showSuggestions() { 2186 if (mSuggestionsPopupWindow == null) { 2187 mSuggestionsPopupWindow = new SuggestionsPopupWindow(); 2188 } 2189 hideCursorAndSpanControllers(); 2190 stopTextActionMode(); 2191 mSuggestionsPopupWindow.show(); 2192 } 2193 2194 void onScrollChanged() { 2195 if (mPositionListener != null) { 2196 mPositionListener.onScrollChanged(); 2197 } 2198 if (mTextActionMode != null) { 2199 mTextActionMode.invalidateContentRect(); 2200 } 2201 } 2202 2203 /** 2204 * @return True when the TextView isFocused and has a valid zero-length selection (cursor). 2205 */ 2206 private boolean shouldBlink() { 2207 if (!isCursorVisible() || !mTextView.isFocused()) return false; 2208 2209 final int start = mTextView.getSelectionStart(); 2210 if (start < 0) return false; 2211 2212 final int end = mTextView.getSelectionEnd(); 2213 if (end < 0) return false; 2214 2215 return start == end; 2216 } 2217 2218 void makeBlink() { 2219 if (shouldBlink()) { 2220 mShowCursor = SystemClock.uptimeMillis(); 2221 if (mBlink == null) mBlink = new Blink(); 2222 mTextView.removeCallbacks(mBlink); 2223 mTextView.postDelayed(mBlink, BLINK); 2224 } else { 2225 if (mBlink != null) mTextView.removeCallbacks(mBlink); 2226 } 2227 } 2228 2229 private class Blink implements Runnable { 2230 private boolean mCancelled; 2231 2232 public void run() { 2233 if (mCancelled) { 2234 return; 2235 } 2236 2237 mTextView.removeCallbacks(this); 2238 2239 if (shouldBlink()) { 2240 if (mTextView.getLayout() != null) { 2241 mTextView.invalidateCursorPath(); 2242 } 2243 2244 mTextView.postDelayed(this, BLINK); 2245 } 2246 } 2247 2248 void cancel() { 2249 if (!mCancelled) { 2250 mTextView.removeCallbacks(this); 2251 mCancelled = true; 2252 } 2253 } 2254 2255 void uncancel() { 2256 mCancelled = false; 2257 } 2258 } 2259 2260 private DragShadowBuilder getTextThumbnailBuilder(CharSequence text) { 2261 TextView shadowView = (TextView) View.inflate(mTextView.getContext(), 2262 com.android.internal.R.layout.text_drag_thumbnail, null); 2263 2264 if (shadowView == null) { 2265 throw new IllegalArgumentException("Unable to inflate text drag thumbnail"); 2266 } 2267 2268 if (text.length() > DRAG_SHADOW_MAX_TEXT_LENGTH) { 2269 text = text.subSequence(0, DRAG_SHADOW_MAX_TEXT_LENGTH); 2270 } 2271 shadowView.setText(text); 2272 shadowView.setTextColor(mTextView.getTextColors()); 2273 2274 shadowView.setTextAppearance(R.styleable.Theme_textAppearanceLarge); 2275 shadowView.setGravity(Gravity.CENTER); 2276 2277 shadowView.setLayoutParams(new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2278 ViewGroup.LayoutParams.WRAP_CONTENT)); 2279 2280 final int size = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED); 2281 shadowView.measure(size, size); 2282 2283 shadowView.layout(0, 0, shadowView.getMeasuredWidth(), shadowView.getMeasuredHeight()); 2284 shadowView.invalidate(); 2285 return new DragShadowBuilder(shadowView); 2286 } 2287 2288 private static class DragLocalState { 2289 public TextView sourceTextView; 2290 public int start, end; 2291 2292 public DragLocalState(TextView sourceTextView, int start, int end) { 2293 this.sourceTextView = sourceTextView; 2294 this.start = start; 2295 this.end = end; 2296 } 2297 } 2298 2299 void onDrop(DragEvent event) { 2300 StringBuilder content = new StringBuilder(""); 2301 ClipData clipData = event.getClipData(); 2302 final int itemCount = clipData.getItemCount(); 2303 for (int i=0; i < itemCount; i++) { 2304 Item item = clipData.getItemAt(i); 2305 content.append(item.coerceToStyledText(mTextView.getContext())); 2306 } 2307 2308 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 2309 2310 Object localState = event.getLocalState(); 2311 DragLocalState dragLocalState = null; 2312 if (localState instanceof DragLocalState) { 2313 dragLocalState = (DragLocalState) localState; 2314 } 2315 boolean dragDropIntoItself = dragLocalState != null && 2316 dragLocalState.sourceTextView == mTextView; 2317 2318 if (dragDropIntoItself) { 2319 if (offset >= dragLocalState.start && offset < dragLocalState.end) { 2320 // A drop inside the original selection discards the drop. 2321 return; 2322 } 2323 } 2324 2325 final int originalLength = mTextView.getText().length(); 2326 int min = offset; 2327 int max = offset; 2328 2329 Selection.setSelection((Spannable) mTextView.getText(), max); 2330 mTextView.replaceText_internal(min, max, content); 2331 2332 if (dragDropIntoItself) { 2333 int dragSourceStart = dragLocalState.start; 2334 int dragSourceEnd = dragLocalState.end; 2335 if (max <= dragSourceStart) { 2336 // Inserting text before selection has shifted positions 2337 final int shift = mTextView.getText().length() - originalLength; 2338 dragSourceStart += shift; 2339 dragSourceEnd += shift; 2340 } 2341 2342 // Delete original selection 2343 mTextView.deleteText_internal(dragSourceStart, dragSourceEnd); 2344 2345 // Make sure we do not leave two adjacent spaces. 2346 final int prevCharIdx = Math.max(0, dragSourceStart - 1); 2347 final int nextCharIdx = Math.min(mTextView.getText().length(), dragSourceStart + 1); 2348 if (nextCharIdx > prevCharIdx + 1) { 2349 CharSequence t = mTextView.getTransformedText(prevCharIdx, nextCharIdx); 2350 if (Character.isSpaceChar(t.charAt(0)) && Character.isSpaceChar(t.charAt(1))) { 2351 mTextView.deleteText_internal(prevCharIdx, prevCharIdx + 1); 2352 } 2353 } 2354 } 2355 } 2356 2357 public void addSpanWatchers(Spannable text) { 2358 final int textLength = text.length(); 2359 2360 if (mKeyListener != null) { 2361 text.setSpan(mKeyListener, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 2362 } 2363 2364 if (mSpanController == null) { 2365 mSpanController = new SpanController(); 2366 } 2367 text.setSpan(mSpanController, 0, textLength, Spanned.SPAN_INCLUSIVE_INCLUSIVE); 2368 } 2369 2370 void setContextMenuAnchor(float x, float y) { 2371 mContextMenuAnchorX = x; 2372 mContextMenuAnchorY = y; 2373 } 2374 2375 void onCreateContextMenu(ContextMenu menu) { 2376 if (mIsBeingLongClicked || Float.isNaN(mContextMenuAnchorX) 2377 || Float.isNaN(mContextMenuAnchorY)) { 2378 return; 2379 } 2380 final int offset = mTextView.getOffsetForPosition(mContextMenuAnchorX, mContextMenuAnchorY); 2381 if (offset == -1) { 2382 return; 2383 } 2384 mPreserveDetachedSelection = true; 2385 stopTextActionMode(); 2386 mPreserveDetachedSelection = false; 2387 final boolean isOnSelection = mTextView.hasSelection() 2388 && offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd(); 2389 if (!isOnSelection) { 2390 // Right clicked position is not on the selection. Remove the selection and move the 2391 // cursor to the right clicked position. 2392 stopTextActionMode(); 2393 Selection.setSelection((Spannable) mTextView.getText(), offset); 2394 } 2395 2396 if (shouldOfferToShowSuggestions()) { 2397 if (mSuggestionInfosInContextMenu == null) { 2398 mSuggestionInfosInContextMenu = 2399 new SuggestionInfo[SuggestionSpan.SUGGESTIONS_MAX_SIZE]; 2400 for (int i = 0; i < mSuggestionInfosInContextMenu.length; i++) { 2401 mSuggestionInfosInContextMenu[i] = new SuggestionInfo(); 2402 } 2403 } 2404 final SubMenu subMenu = menu.addSubMenu(Menu.NONE, Menu.NONE, MENU_ITEM_ORDER_REPLACE, 2405 com.android.internal.R.string.replace); 2406 mSuggestionHelper.getSuggestionInfo(mSuggestionInfosInContextMenu); 2407 int i = 0; 2408 for (final SuggestionInfo info : mSuggestionInfosInContextMenu) { 2409 info.mSuggestionEnd = info.mText.length(); 2410 subMenu.add(Menu.NONE, Menu.NONE, i++, info.mText) 2411 .setOnMenuItemClickListener(mOnContextMenuReplaceItemClickListener); 2412 } 2413 } 2414 2415 menu.add(Menu.NONE, TextView.ID_UNDO, MENU_ITEM_ORDER_UNDO, 2416 com.android.internal.R.string.undo) 2417 .setAlphabeticShortcut('z') 2418 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 2419 .setEnabled(mTextView.canUndo()); 2420 menu.add(Menu.NONE, TextView.ID_REDO, MENU_ITEM_ORDER_REDO, 2421 com.android.internal.R.string.redo) 2422 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 2423 .setEnabled(mTextView.canRedo()); 2424 2425 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT, 2426 com.android.internal.R.string.cut) 2427 .setAlphabeticShortcut('x') 2428 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 2429 .setEnabled(mTextView.canCut()); 2430 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY, 2431 com.android.internal.R.string.copy) 2432 .setAlphabeticShortcut('c') 2433 .setOnMenuItemClickListener(mOnContextMenuItemClickListener) 2434 .setEnabled(mTextView.canCopy()); 2435 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE, 2436 com.android.internal.R.string.paste) 2437 .setAlphabeticShortcut('v') 2438 .setEnabled(mTextView.canPaste()) 2439 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 2440 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE_AS_PLAIN_TEXT, 2441 com.android.internal.R.string.paste_as_plain_text) 2442 .setEnabled(mTextView.canPaste()) 2443 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 2444 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE, 2445 com.android.internal.R.string.share) 2446 .setEnabled(mTextView.canShare()) 2447 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 2448 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL, 2449 com.android.internal.R.string.selectAll) 2450 .setAlphabeticShortcut('a') 2451 .setEnabled(mTextView.canSelectAllText()) 2452 .setOnMenuItemClickListener(mOnContextMenuItemClickListener); 2453 2454 mPreserveDetachedSelection = true; 2455 } 2456 2457 private void replaceWithSuggestion(SuggestionInfo suggestionInfo, int spanStart, int spanEnd) { 2458 final Editable editable = (Editable) mTextView.getText(); 2459 final String originalText = TextUtils.substring(editable, spanStart, spanEnd); 2460 // SuggestionSpans are removed by replace: save them before 2461 SuggestionSpan[] suggestionSpans = editable.getSpans(spanStart, spanEnd, 2462 SuggestionSpan.class); 2463 final int length = suggestionSpans.length; 2464 int[] suggestionSpansStarts = new int[length]; 2465 int[] suggestionSpansEnds = new int[length]; 2466 int[] suggestionSpansFlags = new int[length]; 2467 for (int i = 0; i < length; i++) { 2468 final SuggestionSpan suggestionSpan = suggestionSpans[i]; 2469 suggestionSpansStarts[i] = editable.getSpanStart(suggestionSpan); 2470 suggestionSpansEnds[i] = editable.getSpanEnd(suggestionSpan); 2471 suggestionSpansFlags[i] = editable.getSpanFlags(suggestionSpan); 2472 2473 // Remove potential misspelled flags 2474 int suggestionSpanFlags = suggestionSpan.getFlags(); 2475 if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) != 0) { 2476 suggestionSpanFlags &= ~SuggestionSpan.FLAG_MISSPELLED; 2477 suggestionSpanFlags &= ~SuggestionSpan.FLAG_EASY_CORRECT; 2478 suggestionSpan.setFlags(suggestionSpanFlags); 2479 } 2480 } 2481 2482 // Notify source IME of the suggestion pick. Do this before swapping texts. 2483 suggestionInfo.mSuggestionSpan.notifySelection( 2484 mTextView.getContext(), originalText, suggestionInfo.mSuggestionIndex); 2485 2486 // Swap text content between actual text and Suggestion span 2487 final int suggestionStart = suggestionInfo.mSuggestionStart; 2488 final int suggestionEnd = suggestionInfo.mSuggestionEnd; 2489 final String suggestion = suggestionInfo.mText.subSequence( 2490 suggestionStart, suggestionEnd).toString(); 2491 mTextView.replaceText_internal(spanStart, spanEnd, suggestion); 2492 2493 String[] suggestions = suggestionInfo.mSuggestionSpan.getSuggestions(); 2494 suggestions[suggestionInfo.mSuggestionIndex] = originalText; 2495 2496 // Restore previous SuggestionSpans 2497 final int lengthDelta = suggestion.length() - (spanEnd - spanStart); 2498 for (int i = 0; i < length; i++) { 2499 // Only spans that include the modified region make sense after replacement 2500 // Spans partially included in the replaced region are removed, there is no 2501 // way to assign them a valid range after replacement 2502 if (suggestionSpansStarts[i] <= spanStart && suggestionSpansEnds[i] >= spanEnd) { 2503 mTextView.setSpan_internal(suggestionSpans[i], suggestionSpansStarts[i], 2504 suggestionSpansEnds[i] + lengthDelta, suggestionSpansFlags[i]); 2505 } 2506 } 2507 // Move cursor at the end of the replaced word 2508 final int newCursorPosition = spanEnd + lengthDelta; 2509 mTextView.setCursorPosition_internal(newCursorPosition, newCursorPosition); 2510 } 2511 2512 private final MenuItem.OnMenuItemClickListener mOnContextMenuItemClickListener = 2513 new MenuItem.OnMenuItemClickListener() { 2514 @Override 2515 public boolean onMenuItemClick(MenuItem item) { 2516 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { 2517 return true; 2518 } 2519 return mTextView.onTextContextMenuItem(item.getItemId()); 2520 } 2521 }; 2522 2523 private final MenuItem.OnMenuItemClickListener mOnContextMenuReplaceItemClickListener = 2524 new MenuItem.OnMenuItemClickListener() { 2525 @Override 2526 public boolean onMenuItemClick(MenuItem item) { 2527 int index = item.getOrder(); 2528 if (index < 0 || index >= mSuggestionInfosInContextMenu.length) { 2529 clear(); 2530 return false; 2531 } 2532 final Spannable spannable = (Spannable) mTextView.getText(); 2533 final SuggestionSpan suggestionSpan = 2534 mSuggestionInfosInContextMenu[index].mSuggestionSpan; 2535 replaceWithSuggestion(mSuggestionInfosInContextMenu[index], 2536 spannable.getSpanStart(suggestionSpan), spannable.getSpanEnd(suggestionSpan)); 2537 clear(); 2538 return true; 2539 } 2540 2541 private void clear() { 2542 for (final SuggestionInfo info : mSuggestionInfosInContextMenu) { 2543 info.clear(); 2544 } 2545 } 2546 }; 2547 2548 /** 2549 * Controls the {@link EasyEditSpan} monitoring when it is added, and when the related 2550 * pop-up should be displayed. 2551 * Also monitors {@link Selection} to call back to the attached input method. 2552 */ 2553 class SpanController implements SpanWatcher { 2554 2555 private static final int DISPLAY_TIMEOUT_MS = 3000; // 3 secs 2556 2557 private EasyEditPopupWindow mPopupWindow; 2558 2559 private Runnable mHidePopup; 2560 2561 // This function is pure but inner classes can't have static functions 2562 private boolean isNonIntermediateSelectionSpan(final Spannable text, 2563 final Object span) { 2564 return (Selection.SELECTION_START == span || Selection.SELECTION_END == span) 2565 && (text.getSpanFlags(span) & Spanned.SPAN_INTERMEDIATE) == 0; 2566 } 2567 2568 @Override 2569 public void onSpanAdded(Spannable text, Object span, int start, int end) { 2570 if (isNonIntermediateSelectionSpan(text, span)) { 2571 sendUpdateSelection(); 2572 } else if (span instanceof EasyEditSpan) { 2573 if (mPopupWindow == null) { 2574 mPopupWindow = new EasyEditPopupWindow(); 2575 mHidePopup = new Runnable() { 2576 @Override 2577 public void run() { 2578 hide(); 2579 } 2580 }; 2581 } 2582 2583 // Make sure there is only at most one EasyEditSpan in the text 2584 if (mPopupWindow.mEasyEditSpan != null) { 2585 mPopupWindow.mEasyEditSpan.setDeleteEnabled(false); 2586 } 2587 2588 mPopupWindow.setEasyEditSpan((EasyEditSpan) span); 2589 mPopupWindow.setOnDeleteListener(new EasyEditDeleteListener() { 2590 @Override 2591 public void onDeleteClick(EasyEditSpan span) { 2592 Editable editable = (Editable) mTextView.getText(); 2593 int start = editable.getSpanStart(span); 2594 int end = editable.getSpanEnd(span); 2595 if (start >= 0 && end >= 0) { 2596 sendEasySpanNotification(EasyEditSpan.TEXT_DELETED, span); 2597 mTextView.deleteText_internal(start, end); 2598 } 2599 editable.removeSpan(span); 2600 } 2601 }); 2602 2603 if (mTextView.getWindowVisibility() != View.VISIBLE) { 2604 // The window is not visible yet, ignore the text change. 2605 return; 2606 } 2607 2608 if (mTextView.getLayout() == null) { 2609 // The view has not been laid out yet, ignore the text change 2610 return; 2611 } 2612 2613 if (extractedTextModeWillBeStarted()) { 2614 // The input is in extract mode. Do not handle the easy edit in 2615 // the original TextView, as the ExtractEditText will do 2616 return; 2617 } 2618 2619 mPopupWindow.show(); 2620 mTextView.removeCallbacks(mHidePopup); 2621 mTextView.postDelayed(mHidePopup, DISPLAY_TIMEOUT_MS); 2622 } 2623 } 2624 2625 @Override 2626 public void onSpanRemoved(Spannable text, Object span, int start, int end) { 2627 if (isNonIntermediateSelectionSpan(text, span)) { 2628 sendUpdateSelection(); 2629 } else if (mPopupWindow != null && span == mPopupWindow.mEasyEditSpan) { 2630 hide(); 2631 } 2632 } 2633 2634 @Override 2635 public void onSpanChanged(Spannable text, Object span, int previousStart, int previousEnd, 2636 int newStart, int newEnd) { 2637 if (isNonIntermediateSelectionSpan(text, span)) { 2638 sendUpdateSelection(); 2639 } else if (mPopupWindow != null && span instanceof EasyEditSpan) { 2640 EasyEditSpan easyEditSpan = (EasyEditSpan) span; 2641 sendEasySpanNotification(EasyEditSpan.TEXT_MODIFIED, easyEditSpan); 2642 text.removeSpan(easyEditSpan); 2643 } 2644 } 2645 2646 public void hide() { 2647 if (mPopupWindow != null) { 2648 mPopupWindow.hide(); 2649 mTextView.removeCallbacks(mHidePopup); 2650 } 2651 } 2652 2653 private void sendEasySpanNotification(int textChangedType, EasyEditSpan span) { 2654 try { 2655 PendingIntent pendingIntent = span.getPendingIntent(); 2656 if (pendingIntent != null) { 2657 Intent intent = new Intent(); 2658 intent.putExtra(EasyEditSpan.EXTRA_TEXT_CHANGED_TYPE, textChangedType); 2659 pendingIntent.send(mTextView.getContext(), 0, intent); 2660 } 2661 } catch (CanceledException e) { 2662 // This should not happen, as we should try to send the intent only once. 2663 Log.w(TAG, "PendingIntent for notification cannot be sent", e); 2664 } 2665 } 2666 } 2667 2668 /** 2669 * Listens for the delete event triggered by {@link EasyEditPopupWindow}. 2670 */ 2671 private interface EasyEditDeleteListener { 2672 2673 /** 2674 * Clicks the delete pop-up. 2675 */ 2676 void onDeleteClick(EasyEditSpan span); 2677 } 2678 2679 /** 2680 * Displays the actions associated to an {@link EasyEditSpan}. The pop-up is controlled 2681 * by {@link SpanController}. 2682 */ 2683 private class EasyEditPopupWindow extends PinnedPopupWindow 2684 implements OnClickListener { 2685 private static final int POPUP_TEXT_LAYOUT = 2686 com.android.internal.R.layout.text_edit_action_popup_text; 2687 private TextView mDeleteTextView; 2688 private EasyEditSpan mEasyEditSpan; 2689 private EasyEditDeleteListener mOnDeleteListener; 2690 2691 @Override 2692 protected void createPopupWindow() { 2693 mPopupWindow = new PopupWindow(mTextView.getContext(), null, 2694 com.android.internal.R.attr.textSelectHandleWindowStyle); 2695 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 2696 mPopupWindow.setClippingEnabled(true); 2697 } 2698 2699 @Override 2700 protected void initContentView() { 2701 LinearLayout linearLayout = new LinearLayout(mTextView.getContext()); 2702 linearLayout.setOrientation(LinearLayout.HORIZONTAL); 2703 mContentView = linearLayout; 2704 mContentView.setBackgroundResource( 2705 com.android.internal.R.drawable.text_edit_side_paste_window); 2706 2707 LayoutInflater inflater = (LayoutInflater)mTextView.getContext(). 2708 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 2709 2710 LayoutParams wrapContent = new LayoutParams( 2711 ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT); 2712 2713 mDeleteTextView = (TextView) inflater.inflate(POPUP_TEXT_LAYOUT, null); 2714 mDeleteTextView.setLayoutParams(wrapContent); 2715 mDeleteTextView.setText(com.android.internal.R.string.delete); 2716 mDeleteTextView.setOnClickListener(this); 2717 mContentView.addView(mDeleteTextView); 2718 } 2719 2720 public void setEasyEditSpan(EasyEditSpan easyEditSpan) { 2721 mEasyEditSpan = easyEditSpan; 2722 } 2723 2724 private void setOnDeleteListener(EasyEditDeleteListener listener) { 2725 mOnDeleteListener = listener; 2726 } 2727 2728 @Override 2729 public void onClick(View view) { 2730 if (view == mDeleteTextView 2731 && mEasyEditSpan != null && mEasyEditSpan.isDeleteEnabled() 2732 && mOnDeleteListener != null) { 2733 mOnDeleteListener.onDeleteClick(mEasyEditSpan); 2734 } 2735 } 2736 2737 @Override 2738 public void hide() { 2739 if (mEasyEditSpan != null) { 2740 mEasyEditSpan.setDeleteEnabled(false); 2741 } 2742 mOnDeleteListener = null; 2743 super.hide(); 2744 } 2745 2746 @Override 2747 protected int getTextOffset() { 2748 // Place the pop-up at the end of the span 2749 Editable editable = (Editable) mTextView.getText(); 2750 return editable.getSpanEnd(mEasyEditSpan); 2751 } 2752 2753 @Override 2754 protected int getVerticalLocalPosition(int line) { 2755 return mTextView.getLayout().getLineBottom(line); 2756 } 2757 2758 @Override 2759 protected int clipVertically(int positionY) { 2760 // As we display the pop-up below the span, no vertical clipping is required. 2761 return positionY; 2762 } 2763 } 2764 2765 private class PositionListener implements ViewTreeObserver.OnPreDrawListener { 2766 // 3 handles 2767 // 3 ActionPopup [replace, suggestion, easyedit] (suggestionsPopup first hides the others) 2768 // 1 CursorAnchorInfoNotifier 2769 private final int MAXIMUM_NUMBER_OF_LISTENERS = 7; 2770 private TextViewPositionListener[] mPositionListeners = 2771 new TextViewPositionListener[MAXIMUM_NUMBER_OF_LISTENERS]; 2772 private boolean mCanMove[] = new boolean[MAXIMUM_NUMBER_OF_LISTENERS]; 2773 private boolean mPositionHasChanged = true; 2774 // Absolute position of the TextView with respect to its parent window 2775 private int mPositionX, mPositionY; 2776 private int mNumberOfListeners; 2777 private boolean mScrollHasChanged; 2778 final int[] mTempCoords = new int[2]; 2779 2780 public void addSubscriber(TextViewPositionListener positionListener, boolean canMove) { 2781 if (mNumberOfListeners == 0) { 2782 updatePosition(); 2783 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 2784 vto.addOnPreDrawListener(this); 2785 } 2786 2787 int emptySlotIndex = -1; 2788 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2789 TextViewPositionListener listener = mPositionListeners[i]; 2790 if (listener == positionListener) { 2791 return; 2792 } else if (emptySlotIndex < 0 && listener == null) { 2793 emptySlotIndex = i; 2794 } 2795 } 2796 2797 mPositionListeners[emptySlotIndex] = positionListener; 2798 mCanMove[emptySlotIndex] = canMove; 2799 mNumberOfListeners++; 2800 } 2801 2802 public void removeSubscriber(TextViewPositionListener positionListener) { 2803 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2804 if (mPositionListeners[i] == positionListener) { 2805 mPositionListeners[i] = null; 2806 mNumberOfListeners--; 2807 break; 2808 } 2809 } 2810 2811 if (mNumberOfListeners == 0) { 2812 ViewTreeObserver vto = mTextView.getViewTreeObserver(); 2813 vto.removeOnPreDrawListener(this); 2814 } 2815 } 2816 2817 public int getPositionX() { 2818 return mPositionX; 2819 } 2820 2821 public int getPositionY() { 2822 return mPositionY; 2823 } 2824 2825 @Override 2826 public boolean onPreDraw() { 2827 updatePosition(); 2828 2829 for (int i = 0; i < MAXIMUM_NUMBER_OF_LISTENERS; i++) { 2830 if (mPositionHasChanged || mScrollHasChanged || mCanMove[i]) { 2831 TextViewPositionListener positionListener = mPositionListeners[i]; 2832 if (positionListener != null) { 2833 positionListener.updatePosition(mPositionX, mPositionY, 2834 mPositionHasChanged, mScrollHasChanged); 2835 } 2836 } 2837 } 2838 2839 mScrollHasChanged = false; 2840 return true; 2841 } 2842 2843 private void updatePosition() { 2844 mTextView.getLocationInWindow(mTempCoords); 2845 2846 mPositionHasChanged = mTempCoords[0] != mPositionX || mTempCoords[1] != mPositionY; 2847 2848 mPositionX = mTempCoords[0]; 2849 mPositionY = mTempCoords[1]; 2850 } 2851 2852 public void onScrollChanged() { 2853 mScrollHasChanged = true; 2854 } 2855 } 2856 2857 private abstract class PinnedPopupWindow implements TextViewPositionListener { 2858 protected PopupWindow mPopupWindow; 2859 protected ViewGroup mContentView; 2860 int mPositionX, mPositionY; 2861 2862 protected abstract void createPopupWindow(); 2863 protected abstract void initContentView(); 2864 protected abstract int getTextOffset(); 2865 protected abstract int getVerticalLocalPosition(int line); 2866 protected abstract int clipVertically(int positionY); 2867 2868 public PinnedPopupWindow() { 2869 createPopupWindow(); 2870 2871 mPopupWindow.setWindowLayoutType( 2872 WindowManager.LayoutParams.TYPE_APPLICATION_ABOVE_SUB_PANEL); 2873 mPopupWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 2874 mPopupWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 2875 2876 initContentView(); 2877 2878 LayoutParams wrapContent = new LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, 2879 ViewGroup.LayoutParams.WRAP_CONTENT); 2880 mContentView.setLayoutParams(wrapContent); 2881 2882 mPopupWindow.setContentView(mContentView); 2883 } 2884 2885 public void show() { 2886 getPositionListener().addSubscriber(this, false /* offset is fixed */); 2887 2888 computeLocalPosition(); 2889 2890 final PositionListener positionListener = getPositionListener(); 2891 updatePosition(positionListener.getPositionX(), positionListener.getPositionY()); 2892 } 2893 2894 protected void measureContent() { 2895 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2896 mContentView.measure( 2897 View.MeasureSpec.makeMeasureSpec(displayMetrics.widthPixels, 2898 View.MeasureSpec.AT_MOST), 2899 View.MeasureSpec.makeMeasureSpec(displayMetrics.heightPixels, 2900 View.MeasureSpec.AT_MOST)); 2901 } 2902 2903 /* The popup window will be horizontally centered on the getTextOffset() and vertically 2904 * positioned according to viewportToContentHorizontalOffset. 2905 * 2906 * This method assumes that mContentView has properly been measured from its content. */ 2907 private void computeLocalPosition() { 2908 measureContent(); 2909 final int width = mContentView.getMeasuredWidth(); 2910 final int offset = getTextOffset(); 2911 mPositionX = (int) (mTextView.getLayout().getPrimaryHorizontal(offset) - width / 2.0f); 2912 mPositionX += mTextView.viewportToContentHorizontalOffset(); 2913 2914 final int line = mTextView.getLayout().getLineForOffset(offset); 2915 mPositionY = getVerticalLocalPosition(line); 2916 mPositionY += mTextView.viewportToContentVerticalOffset(); 2917 } 2918 2919 private void updatePosition(int parentPositionX, int parentPositionY) { 2920 int positionX = parentPositionX + mPositionX; 2921 int positionY = parentPositionY + mPositionY; 2922 2923 positionY = clipVertically(positionY); 2924 2925 // Horizontal clipping 2926 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 2927 final int width = mContentView.getMeasuredWidth(); 2928 positionX = Math.min(displayMetrics.widthPixels - width, positionX); 2929 positionX = Math.max(0, positionX); 2930 2931 if (isShowing()) { 2932 mPopupWindow.update(positionX, positionY, -1, -1); 2933 } else { 2934 mPopupWindow.showAtLocation(mTextView, Gravity.NO_GRAVITY, 2935 positionX, positionY); 2936 } 2937 } 2938 2939 public void hide() { 2940 if (!isShowing()) { 2941 return; 2942 } 2943 mPopupWindow.dismiss(); 2944 getPositionListener().removeSubscriber(this); 2945 } 2946 2947 @Override 2948 public void updatePosition(int parentPositionX, int parentPositionY, 2949 boolean parentPositionChanged, boolean parentScrolled) { 2950 // Either parentPositionChanged or parentScrolled is true, check if still visible 2951 if (isShowing() && isOffsetVisible(getTextOffset())) { 2952 if (parentScrolled) computeLocalPosition(); 2953 updatePosition(parentPositionX, parentPositionY); 2954 } else { 2955 hide(); 2956 } 2957 } 2958 2959 public boolean isShowing() { 2960 return mPopupWindow.isShowing(); 2961 } 2962 } 2963 2964 private static class SuggestionInfo { 2965 // Range of actual suggestion within text 2966 int mSuggestionStart, mSuggestionEnd; 2967 2968 // The SuggestionSpan that this TextView represents 2969 @Nullable 2970 SuggestionSpan mSuggestionSpan; 2971 2972 // The index of this suggestion inside suggestionSpan 2973 int mSuggestionIndex; 2974 2975 final SpannableStringBuilder mText = new SpannableStringBuilder(); 2976 2977 void clear() { 2978 mSuggestionSpan = null; 2979 mText.clear(); 2980 } 2981 } 2982 2983 private class SuggestionHelper { 2984 private final Comparator<SuggestionSpan> mSuggestionSpanComparator = 2985 new SuggestionSpanComparator(); 2986 private final HashMap<SuggestionSpan, Integer> mSpansLengths = 2987 new HashMap<SuggestionSpan, Integer>(); 2988 2989 private class SuggestionSpanComparator implements Comparator<SuggestionSpan> { 2990 public int compare(SuggestionSpan span1, SuggestionSpan span2) { 2991 final int flag1 = span1.getFlags(); 2992 final int flag2 = span2.getFlags(); 2993 if (flag1 != flag2) { 2994 // The order here should match what is used in updateDrawState 2995 final boolean easy1 = (flag1 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; 2996 final boolean easy2 = (flag2 & SuggestionSpan.FLAG_EASY_CORRECT) != 0; 2997 final boolean misspelled1 = (flag1 & SuggestionSpan.FLAG_MISSPELLED) != 0; 2998 final boolean misspelled2 = (flag2 & SuggestionSpan.FLAG_MISSPELLED) != 0; 2999 if (easy1 && !misspelled1) return -1; 3000 if (easy2 && !misspelled2) return 1; 3001 if (misspelled1) return -1; 3002 if (misspelled2) return 1; 3003 } 3004 3005 return mSpansLengths.get(span1).intValue() - mSpansLengths.get(span2).intValue(); 3006 } 3007 } 3008 3009 /** 3010 * Returns the suggestion spans that cover the current cursor position. The suggestion 3011 * spans are sorted according to the length of text that they are attached to. 3012 */ 3013 private SuggestionSpan[] getSortedSuggestionSpans() { 3014 int pos = mTextView.getSelectionStart(); 3015 Spannable spannable = (Spannable) mTextView.getText(); 3016 SuggestionSpan[] suggestionSpans = spannable.getSpans(pos, pos, SuggestionSpan.class); 3017 3018 mSpansLengths.clear(); 3019 for (SuggestionSpan suggestionSpan : suggestionSpans) { 3020 int start = spannable.getSpanStart(suggestionSpan); 3021 int end = spannable.getSpanEnd(suggestionSpan); 3022 mSpansLengths.put(suggestionSpan, Integer.valueOf(end - start)); 3023 } 3024 3025 // The suggestions are sorted according to their types (easy correction first, then 3026 // misspelled) and to the length of the text that they cover (shorter first). 3027 Arrays.sort(suggestionSpans, mSuggestionSpanComparator); 3028 mSpansLengths.clear(); 3029 3030 return suggestionSpans; 3031 } 3032 3033 /** 3034 * Gets the SuggestionInfo list that contains suggestion information at the current cursor 3035 * position. 3036 * 3037 * @param suggestionInfos SuggestionInfo array the results will be set. 3038 * @return the number of suggestions actually fetched. 3039 */ 3040 public int getSuggestionInfo(SuggestionInfo[] suggestionInfos) { 3041 final Spannable spannable = (Spannable) mTextView.getText(); 3042 final SuggestionSpan[] suggestionSpans = getSortedSuggestionSpans(); 3043 final int nbSpans = suggestionSpans.length; 3044 if (nbSpans == 0) return 0; 3045 3046 int numberOfSuggestions = 0; 3047 for (int spanIndex = 0; spanIndex < nbSpans; spanIndex++) { 3048 final SuggestionSpan suggestionSpan = suggestionSpans[spanIndex]; 3049 final int spanStart = spannable.getSpanStart(suggestionSpan); 3050 final int spanEnd = spannable.getSpanEnd(suggestionSpan); 3051 3052 final String[] suggestions = suggestionSpan.getSuggestions(); 3053 final int nbSuggestions = suggestions.length; 3054 for (int suggestionIndex = 0; suggestionIndex < nbSuggestions; suggestionIndex++) { 3055 final String suggestion = suggestions[suggestionIndex]; 3056 boolean suggestionIsDuplicate = false; 3057 for (int i = 0; i < numberOfSuggestions; i++) { 3058 if (suggestionInfos[i].mText.toString().equals(suggestion)) { 3059 final SuggestionSpan otherSuggestionSpan = 3060 suggestionInfos[i].mSuggestionSpan; 3061 final int otherSpanStart = spannable.getSpanStart(otherSuggestionSpan); 3062 final int otherSpanEnd = spannable.getSpanEnd(otherSuggestionSpan); 3063 if (spanStart == otherSpanStart && spanEnd == otherSpanEnd) { 3064 suggestionIsDuplicate = true; 3065 break; 3066 } 3067 } 3068 } 3069 3070 if (suggestionIsDuplicate) { 3071 continue; 3072 } 3073 SuggestionInfo suggestionInfo = suggestionInfos[numberOfSuggestions]; 3074 suggestionInfo.mSuggestionSpan = suggestionSpan; 3075 suggestionInfo.mSuggestionIndex = suggestionIndex; 3076 suggestionInfo.mSuggestionStart = 0; 3077 suggestionInfo.mSuggestionEnd = suggestion.length(); 3078 suggestionInfo.mText.replace(0, suggestionInfo.mText.length(), suggestion); 3079 numberOfSuggestions++; 3080 if (numberOfSuggestions >= suggestionInfos.length) { 3081 return numberOfSuggestions; 3082 } 3083 } 3084 } 3085 return numberOfSuggestions; 3086 } 3087 } 3088 3089 @VisibleForTesting 3090 public class SuggestionsPopupWindow extends PinnedPopupWindow implements OnItemClickListener { 3091 private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; 3092 3093 // Key of intent extras for inserting new word into user dictionary. 3094 private static final String USER_DICTIONARY_EXTRA_WORD = "word"; 3095 private static final String USER_DICTIONARY_EXTRA_LOCALE = "locale"; 3096 3097 private SuggestionInfo[] mSuggestionInfos; 3098 private int mNumberOfSuggestions; 3099 private boolean mCursorWasVisibleBeforeSuggestions; 3100 private boolean mIsShowingUp = false; 3101 private SuggestionAdapter mSuggestionsAdapter; 3102 private final TextAppearanceSpan mHighlightSpan = new TextAppearanceSpan( 3103 mTextView.getContext(), mTextView.mTextEditSuggestionHighlightStyle); 3104 private TextView mAddToDictionaryButton; 3105 private TextView mDeleteButton; 3106 private SuggestionSpan mMisspelledSpan; 3107 3108 private class CustomPopupWindow extends PopupWindow { 3109 @Override 3110 public void dismiss() { 3111 if (!isShowing()) { 3112 return; 3113 } 3114 super.dismiss(); 3115 getPositionListener().removeSubscriber(SuggestionsPopupWindow.this); 3116 3117 // Safe cast since show() checks that mTextView.getText() is an Editable 3118 ((Spannable) mTextView.getText()).removeSpan(mSuggestionRangeSpan); 3119 3120 mTextView.setCursorVisible(mCursorWasVisibleBeforeSuggestions); 3121 if (hasInsertionController()) { 3122 getInsertionController().show(); 3123 } 3124 } 3125 } 3126 3127 public SuggestionsPopupWindow() { 3128 mCursorWasVisibleBeforeSuggestions = mCursorVisible; 3129 } 3130 3131 @Override 3132 protected void createPopupWindow() { 3133 mPopupWindow = new CustomPopupWindow(); 3134 mPopupWindow.setInputMethodMode(PopupWindow.INPUT_METHOD_NOT_NEEDED); 3135 mPopupWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT)); 3136 mPopupWindow.setFocusable(true); 3137 mPopupWindow.setClippingEnabled(false); 3138 } 3139 3140 @Override 3141 protected void initContentView() { 3142 final LayoutInflater inflater = (LayoutInflater) mTextView.getContext(). 3143 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 3144 final LinearLayout linearLayout = (LinearLayout) inflater.inflate( 3145 mTextView.mTextEditSuggestionContainerLayout, null); 3146 3147 final ListView suggestionListView = (ListView) linearLayout.findViewById( 3148 com.android.internal.R.id.suggestionContainer); 3149 3150 mSuggestionsAdapter = new SuggestionAdapter(); 3151 suggestionListView.setAdapter(mSuggestionsAdapter); 3152 suggestionListView.setOnItemClickListener(this); 3153 3154 // Inflate the suggestion items once and for all. 3155 mSuggestionInfos = new SuggestionInfo[MAX_NUMBER_SUGGESTIONS]; 3156 for (int i = 0; i < mSuggestionInfos.length; i++) { 3157 mSuggestionInfos[i] = new SuggestionInfo(); 3158 } 3159 3160 mContentView = linearLayout; 3161 3162 mAddToDictionaryButton = (TextView) linearLayout.findViewById( 3163 com.android.internal.R.id.addToDictionaryButton); 3164 mAddToDictionaryButton.setOnClickListener(new View.OnClickListener() { 3165 public void onClick(View v) { 3166 final Editable editable = (Editable) mTextView.getText(); 3167 final int spanStart = editable.getSpanStart(mMisspelledSpan); 3168 final int spanEnd = editable.getSpanEnd(mMisspelledSpan); 3169 final String originalText = TextUtils.substring(editable, spanStart, spanEnd); 3170 3171 final Intent intent = new Intent(Settings.ACTION_USER_DICTIONARY_INSERT); 3172 intent.putExtra(USER_DICTIONARY_EXTRA_WORD, originalText); 3173 intent.putExtra(USER_DICTIONARY_EXTRA_LOCALE, 3174 mTextView.getTextServicesLocale().toString()); 3175 intent.setFlags(intent.getFlags() | Intent.FLAG_ACTIVITY_NEW_TASK); 3176 mTextView.getContext().startActivity(intent); 3177 // There is no way to know if the word was indeed added. Re-check. 3178 // TODO The ExtractEditText should remove the span in the original text instead 3179 editable.removeSpan(mMisspelledSpan); 3180 Selection.setSelection(editable, spanEnd); 3181 updateSpellCheckSpans(spanStart, spanEnd, false); 3182 hideWithCleanUp(); 3183 } 3184 }); 3185 3186 mDeleteButton = (TextView) linearLayout.findViewById( 3187 com.android.internal.R.id.deleteButton); 3188 mDeleteButton.setOnClickListener(new View.OnClickListener() { 3189 public void onClick(View v) { 3190 final Editable editable = (Editable) mTextView.getText(); 3191 3192 final int spanUnionStart = editable.getSpanStart(mSuggestionRangeSpan); 3193 int spanUnionEnd = editable.getSpanEnd(mSuggestionRangeSpan); 3194 if (spanUnionStart >= 0 && spanUnionEnd > spanUnionStart) { 3195 // Do not leave two adjacent spaces after deletion, or one at beginning of 3196 // text 3197 if (spanUnionEnd < editable.length() && 3198 Character.isSpaceChar(editable.charAt(spanUnionEnd)) && 3199 (spanUnionStart == 0 || 3200 Character.isSpaceChar(editable.charAt(spanUnionStart - 1)))) { 3201 spanUnionEnd = spanUnionEnd + 1; 3202 } 3203 mTextView.deleteText_internal(spanUnionStart, spanUnionEnd); 3204 } 3205 hideWithCleanUp(); 3206 } 3207 }); 3208 3209 } 3210 3211 public boolean isShowingUp() { 3212 return mIsShowingUp; 3213 } 3214 3215 public void onParentLostFocus() { 3216 mIsShowingUp = false; 3217 } 3218 3219 private class SuggestionAdapter extends BaseAdapter { 3220 private LayoutInflater mInflater = (LayoutInflater) mTextView.getContext(). 3221 getSystemService(Context.LAYOUT_INFLATER_SERVICE); 3222 3223 @Override 3224 public int getCount() { 3225 return mNumberOfSuggestions; 3226 } 3227 3228 @Override 3229 public Object getItem(int position) { 3230 return mSuggestionInfos[position]; 3231 } 3232 3233 @Override 3234 public long getItemId(int position) { 3235 return position; 3236 } 3237 3238 @Override 3239 public View getView(int position, View convertView, ViewGroup parent) { 3240 TextView textView = (TextView) convertView; 3241 3242 if (textView == null) { 3243 textView = (TextView) mInflater.inflate(mTextView.mTextEditSuggestionItemLayout, 3244 parent, false); 3245 } 3246 3247 final SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 3248 textView.setText(suggestionInfo.mText); 3249 return textView; 3250 } 3251 } 3252 3253 @VisibleForTesting 3254 public ViewGroup getContentViewForTesting() { 3255 return mContentView; 3256 } 3257 3258 @Override 3259 public void show() { 3260 if (!(mTextView.getText() instanceof Editable)) return; 3261 3262 if (updateSuggestions()) { 3263 mCursorWasVisibleBeforeSuggestions = mCursorVisible; 3264 mTextView.setCursorVisible(false); 3265 mIsShowingUp = true; 3266 super.show(); 3267 } 3268 } 3269 3270 @Override 3271 protected void measureContent() { 3272 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 3273 final int horizontalMeasure = View.MeasureSpec.makeMeasureSpec( 3274 displayMetrics.widthPixels, View.MeasureSpec.AT_MOST); 3275 final int verticalMeasure = View.MeasureSpec.makeMeasureSpec( 3276 displayMetrics.heightPixels, View.MeasureSpec.AT_MOST); 3277 3278 int width = 0; 3279 View view = null; 3280 for (int i = 0; i < mNumberOfSuggestions; i++) { 3281 view = mSuggestionsAdapter.getView(i, view, mContentView); 3282 view.getLayoutParams().width = LayoutParams.WRAP_CONTENT; 3283 view.measure(horizontalMeasure, verticalMeasure); 3284 width = Math.max(width, view.getMeasuredWidth()); 3285 } 3286 3287 if (mAddToDictionaryButton.getVisibility() != View.GONE) { 3288 mAddToDictionaryButton.measure(horizontalMeasure, verticalMeasure); 3289 width = Math.max(width, mAddToDictionaryButton.getMeasuredWidth()); 3290 } 3291 3292 mDeleteButton.measure(horizontalMeasure, verticalMeasure); 3293 width = Math.max(width, mDeleteButton.getMeasuredWidth()); 3294 3295 // Enforce the width based on actual text widths 3296 mContentView.measure( 3297 View.MeasureSpec.makeMeasureSpec(width, View.MeasureSpec.EXACTLY), 3298 verticalMeasure); 3299 3300 Drawable popupBackground = mPopupWindow.getBackground(); 3301 if (popupBackground != null) { 3302 if (mTempRect == null) mTempRect = new Rect(); 3303 popupBackground.getPadding(mTempRect); 3304 width += mTempRect.left + mTempRect.right; 3305 } 3306 mPopupWindow.setWidth(width); 3307 } 3308 3309 @Override 3310 protected int getTextOffset() { 3311 return mTextView.getSelectionStart(); 3312 } 3313 3314 @Override 3315 protected int getVerticalLocalPosition(int line) { 3316 return mTextView.getLayout().getLineBottom(line); 3317 } 3318 3319 @Override 3320 protected int clipVertically(int positionY) { 3321 final int height = mContentView.getMeasuredHeight(); 3322 final DisplayMetrics displayMetrics = mTextView.getResources().getDisplayMetrics(); 3323 return Math.min(positionY, displayMetrics.heightPixels - height); 3324 } 3325 3326 private void hideWithCleanUp() { 3327 for (final SuggestionInfo info : mSuggestionInfos) { 3328 info.clear(); 3329 } 3330 mMisspelledSpan = null; 3331 hide(); 3332 } 3333 3334 private boolean updateSuggestions() { 3335 Spannable spannable = (Spannable) mTextView.getText(); 3336 mNumberOfSuggestions = 3337 mSuggestionHelper.getSuggestionInfo(mSuggestionInfos); 3338 if (mNumberOfSuggestions == 0) { 3339 return false; 3340 } 3341 3342 int spanUnionStart = mTextView.getText().length(); 3343 int spanUnionEnd = 0; 3344 3345 mMisspelledSpan = null; 3346 for (int i = 0; i < mNumberOfSuggestions; i++) { 3347 final SuggestionInfo suggestionInfo = mSuggestionInfos[i]; 3348 final SuggestionSpan suggestionSpan = suggestionInfo.mSuggestionSpan; 3349 if ((suggestionSpan.getFlags() & SuggestionSpan.FLAG_MISSPELLED) != 0) { 3350 mMisspelledSpan = suggestionSpan; 3351 } 3352 final int spanStart = spannable.getSpanStart(suggestionSpan); 3353 final int spanEnd = spannable.getSpanEnd(suggestionSpan); 3354 spanUnionStart = Math.min(spanUnionStart, spanStart); 3355 spanUnionEnd = Math.max(spanUnionEnd, spanEnd); 3356 } 3357 3358 for (int i = 0; i < mNumberOfSuggestions; i++) { 3359 highlightTextDifferences(mSuggestionInfos[i], spanUnionStart, spanUnionEnd); 3360 } 3361 3362 // Make "Add to dictionary" item visible if there is a span with the misspelled flag 3363 int addToDictionaryButtonVisibility = View.GONE; 3364 if (mMisspelledSpan != null) { 3365 final int misspelledStart = spannable.getSpanStart(mMisspelledSpan); 3366 final int misspelledEnd = spannable.getSpanEnd(mMisspelledSpan); 3367 if (misspelledStart >= 0 && misspelledEnd > misspelledStart) { 3368 addToDictionaryButtonVisibility = View.VISIBLE; 3369 } 3370 } 3371 mAddToDictionaryButton.setVisibility(addToDictionaryButtonVisibility); 3372 3373 if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); 3374 final int underlineColor = mSuggestionInfos[0].mSuggestionSpan.getUnderlineColor(); 3375 if (underlineColor == 0) { 3376 // Fallback on the default highlight color when the first span does not provide one 3377 mSuggestionRangeSpan.setBackgroundColor(mTextView.mHighlightColor); 3378 } else { 3379 final float BACKGROUND_TRANSPARENCY = 0.4f; 3380 final int newAlpha = (int) (Color.alpha(underlineColor) * BACKGROUND_TRANSPARENCY); 3381 mSuggestionRangeSpan.setBackgroundColor( 3382 (underlineColor & 0x00FFFFFF) + (newAlpha << 24)); 3383 } 3384 spannable.setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, 3385 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 3386 3387 mSuggestionsAdapter.notifyDataSetChanged(); 3388 return true; 3389 } 3390 3391 private void highlightTextDifferences(SuggestionInfo suggestionInfo, int unionStart, 3392 int unionEnd) { 3393 final Spannable text = (Spannable) mTextView.getText(); 3394 final int spanStart = text.getSpanStart(suggestionInfo.mSuggestionSpan); 3395 final int spanEnd = text.getSpanEnd(suggestionInfo.mSuggestionSpan); 3396 3397 // Adjust the start/end of the suggestion span 3398 suggestionInfo.mSuggestionStart = spanStart - unionStart; 3399 suggestionInfo.mSuggestionEnd = suggestionInfo.mSuggestionStart 3400 + suggestionInfo.mText.length(); 3401 3402 suggestionInfo.mText.setSpan(mHighlightSpan, 0, suggestionInfo.mText.length(), 3403 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 3404 3405 // Add the text before and after the span. 3406 final String textAsString = text.toString(); 3407 suggestionInfo.mText.insert(0, textAsString.substring(unionStart, spanStart)); 3408 suggestionInfo.mText.append(textAsString.substring(spanEnd, unionEnd)); 3409 } 3410 3411 @Override 3412 public void onItemClick(AdapterView<?> parent, View view, int position, long id) { 3413 Editable editable = (Editable) mTextView.getText(); 3414 SuggestionInfo suggestionInfo = mSuggestionInfos[position]; 3415 3416 final int spanStart = editable.getSpanStart(suggestionInfo.mSuggestionSpan); 3417 final int spanEnd = editable.getSpanEnd(suggestionInfo.mSuggestionSpan); 3418 if (spanStart < 0 || spanEnd <= spanStart) { 3419 // Span has been removed 3420 hideWithCleanUp(); 3421 return; 3422 } 3423 replaceWithSuggestion(suggestionInfo, spanStart, spanEnd); 3424 hideWithCleanUp(); 3425 } 3426 } 3427 3428 /** 3429 * An ActionMode Callback class that is used to provide actions while in text insertion or 3430 * selection mode. 3431 * 3432 * The default callback provides a subset of Select All, Cut, Copy, Paste, Share and Replace 3433 * actions, depending on which of these this TextView supports and the current selection. 3434 */ 3435 private class TextActionModeCallback extends ActionMode.Callback2 { 3436 private final Path mSelectionPath = new Path(); 3437 private final RectF mSelectionBounds = new RectF(); 3438 private final boolean mHasSelection; 3439 3440 private int mHandleHeight; 3441 3442 public TextActionModeCallback(boolean hasSelection) { 3443 mHasSelection = hasSelection; 3444 if (mHasSelection) { 3445 SelectionModifierCursorController selectionController = getSelectionController(); 3446 if (selectionController.mStartHandle == null) { 3447 // As these are for initializing selectionController, hide() must be called. 3448 selectionController.initDrawables(); 3449 selectionController.initHandles(); 3450 selectionController.hide(); 3451 } 3452 mHandleHeight = Math.max( 3453 mSelectHandleLeft.getMinimumHeight(), 3454 mSelectHandleRight.getMinimumHeight()); 3455 } else { 3456 InsertionPointCursorController insertionController = getInsertionController(); 3457 if (insertionController != null) { 3458 insertionController.getHandle(); 3459 mHandleHeight = mSelectHandleCenter.getMinimumHeight(); 3460 } 3461 } 3462 } 3463 3464 @Override 3465 public boolean onCreateActionMode(ActionMode mode, Menu menu) { 3466 mode.setTitle(null); 3467 mode.setSubtitle(null); 3468 mode.setTitleOptionalHint(true); 3469 populateMenuWithItems(menu); 3470 3471 Callback customCallback = getCustomCallback(); 3472 if (customCallback != null) { 3473 if (!customCallback.onCreateActionMode(mode, menu)) { 3474 // The custom mode can choose to cancel the action mode, dismiss selection. 3475 Selection.setSelection((Spannable) mTextView.getText(), 3476 mTextView.getSelectionEnd()); 3477 return false; 3478 } 3479 } 3480 3481 if (mTextView.canProcessText()) { 3482 mProcessTextIntentActionsHandler.onInitializeMenu(menu); 3483 } 3484 3485 if (menu.hasVisibleItems() || mode.getCustomView() != null) { 3486 mTextView.setHasTransientState(true); 3487 return true; 3488 } else { 3489 return false; 3490 } 3491 } 3492 3493 private Callback getCustomCallback() { 3494 return mHasSelection 3495 ? mCustomSelectionActionModeCallback 3496 : mCustomInsertionActionModeCallback; 3497 } 3498 3499 private void populateMenuWithItems(Menu menu) { 3500 if (mTextView.canCut()) { 3501 menu.add(Menu.NONE, TextView.ID_CUT, MENU_ITEM_ORDER_CUT, 3502 com.android.internal.R.string.cut). 3503 setAlphabeticShortcut('x'). 3504 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 3505 } 3506 3507 if (mTextView.canCopy()) { 3508 menu.add(Menu.NONE, TextView.ID_COPY, MENU_ITEM_ORDER_COPY, 3509 com.android.internal.R.string.copy). 3510 setAlphabeticShortcut('c'). 3511 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 3512 } 3513 3514 if (mTextView.canPaste()) { 3515 menu.add(Menu.NONE, TextView.ID_PASTE, MENU_ITEM_ORDER_PASTE, 3516 com.android.internal.R.string.paste). 3517 setAlphabeticShortcut('v'). 3518 setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); 3519 } 3520 3521 if (mTextView.canShare()) { 3522 menu.add(Menu.NONE, TextView.ID_SHARE, MENU_ITEM_ORDER_SHARE, 3523 com.android.internal.R.string.share). 3524 setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 3525 } 3526 3527 updateSelectAllItem(menu); 3528 updateReplaceItem(menu); 3529 } 3530 3531 @Override 3532 public boolean onPrepareActionMode(ActionMode mode, Menu menu) { 3533 updateSelectAllItem(menu); 3534 updateReplaceItem(menu); 3535 3536 Callback customCallback = getCustomCallback(); 3537 if (customCallback != null) { 3538 return customCallback.onPrepareActionMode(mode, menu); 3539 } 3540 return true; 3541 } 3542 3543 private void updateSelectAllItem(Menu menu) { 3544 boolean canSelectAll = mTextView.canSelectAllText(); 3545 boolean selectAllItemExists = menu.findItem(TextView.ID_SELECT_ALL) != null; 3546 if (canSelectAll && !selectAllItemExists) { 3547 menu.add(Menu.NONE, TextView.ID_SELECT_ALL, MENU_ITEM_ORDER_SELECT_ALL, 3548 com.android.internal.R.string.selectAll) 3549 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 3550 } else if (!canSelectAll && selectAllItemExists) { 3551 menu.removeItem(TextView.ID_SELECT_ALL); 3552 } 3553 } 3554 3555 private void updateReplaceItem(Menu menu) { 3556 boolean canReplace = mTextView.isSuggestionsEnabled() && shouldOfferToShowSuggestions() 3557 && !(mTextView.isInExtractedMode() && mTextView.hasSelection()); 3558 boolean replaceItemExists = menu.findItem(TextView.ID_REPLACE) != null; 3559 if (canReplace && !replaceItemExists) { 3560 menu.add(Menu.NONE, TextView.ID_REPLACE, MENU_ITEM_ORDER_REPLACE, 3561 com.android.internal.R.string.replace) 3562 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 3563 } else if (!canReplace && replaceItemExists) { 3564 menu.removeItem(TextView.ID_REPLACE); 3565 } 3566 } 3567 3568 @Override 3569 public boolean onActionItemClicked(ActionMode mode, MenuItem item) { 3570 if (mProcessTextIntentActionsHandler.performMenuItemAction(item)) { 3571 return true; 3572 } 3573 Callback customCallback = getCustomCallback(); 3574 if (customCallback != null && customCallback.onActionItemClicked(mode, item)) { 3575 return true; 3576 } 3577 return mTextView.onTextContextMenuItem(item.getItemId()); 3578 } 3579 3580 @Override 3581 public void onDestroyActionMode(ActionMode mode) { 3582 Callback customCallback = getCustomCallback(); 3583 if (customCallback != null) { 3584 customCallback.onDestroyActionMode(mode); 3585 } 3586 3587 /* 3588 * If we're ending this mode because we're detaching from a window, 3589 * we still have selection state to preserve. Don't clear it, we'll 3590 * bring back the selection mode when (if) we get reattached. 3591 */ 3592 if (!mPreserveDetachedSelection) { 3593 Selection.setSelection((Spannable) mTextView.getText(), 3594 mTextView.getSelectionEnd()); 3595 mTextView.setHasTransientState(false); 3596 } 3597 3598 if (mSelectionModifierCursorController != null) { 3599 mSelectionModifierCursorController.hide(); 3600 } 3601 3602 mTextActionMode = null; 3603 } 3604 3605 @Override 3606 public void onGetContentRect(ActionMode mode, View view, Rect outRect) { 3607 if (!view.equals(mTextView) || mTextView.getLayout() == null) { 3608 super.onGetContentRect(mode, view, outRect); 3609 return; 3610 } 3611 if (mTextView.getSelectionStart() != mTextView.getSelectionEnd()) { 3612 // We have a selection. 3613 mSelectionPath.reset(); 3614 mTextView.getLayout().getSelectionPath( 3615 mTextView.getSelectionStart(), mTextView.getSelectionEnd(), mSelectionPath); 3616 mSelectionPath.computeBounds(mSelectionBounds, true); 3617 mSelectionBounds.bottom += mHandleHeight; 3618 } else if (mCursorCount == 2) { 3619 // We have a split cursor. In this case, we take the rectangle that includes both 3620 // parts of the cursor to ensure we don't obscure either of them. 3621 Rect firstCursorBounds = mCursorDrawable[0].getBounds(); 3622 Rect secondCursorBounds = mCursorDrawable[1].getBounds(); 3623 mSelectionBounds.set( 3624 Math.min(firstCursorBounds.left, secondCursorBounds.left), 3625 Math.min(firstCursorBounds.top, secondCursorBounds.top), 3626 Math.max(firstCursorBounds.right, secondCursorBounds.right), 3627 Math.max(firstCursorBounds.bottom, secondCursorBounds.bottom) 3628 + mHandleHeight); 3629 } else { 3630 // We have a single cursor. 3631 Layout layout = getActiveLayout(); 3632 int line = layout.getLineForOffset(mTextView.getSelectionStart()); 3633 float primaryHorizontal = 3634 layout.getPrimaryHorizontal(mTextView.getSelectionStart()); 3635 mSelectionBounds.set( 3636 primaryHorizontal, 3637 layout.getLineTop(line), 3638 primaryHorizontal, 3639 layout.getLineTop(line + 1) + mHandleHeight); 3640 } 3641 // Take TextView's padding and scroll into account. 3642 int textHorizontalOffset = mTextView.viewportToContentHorizontalOffset(); 3643 int textVerticalOffset = mTextView.viewportToContentVerticalOffset(); 3644 outRect.set( 3645 (int) Math.floor(mSelectionBounds.left + textHorizontalOffset), 3646 (int) Math.floor(mSelectionBounds.top + textVerticalOffset), 3647 (int) Math.ceil(mSelectionBounds.right + textHorizontalOffset), 3648 (int) Math.ceil(mSelectionBounds.bottom + textVerticalOffset)); 3649 } 3650 } 3651 3652 /** 3653 * A listener to call {@link InputMethodManager#updateCursorAnchorInfo(View, CursorAnchorInfo)} 3654 * while the input method is requesting the cursor/anchor position. Does nothing as long as 3655 * {@link InputMethodManager#isWatchingCursor(View)} returns false. 3656 */ 3657 private final class CursorAnchorInfoNotifier implements TextViewPositionListener { 3658 final CursorAnchorInfo.Builder mSelectionInfoBuilder = new CursorAnchorInfo.Builder(); 3659 final int[] mTmpIntOffset = new int[2]; 3660 final Matrix mViewToScreenMatrix = new Matrix(); 3661 3662 @Override 3663 public void updatePosition(int parentPositionX, int parentPositionY, 3664 boolean parentPositionChanged, boolean parentScrolled) { 3665 final InputMethodState ims = mInputMethodState; 3666 if (ims == null || ims.mBatchEditNesting > 0) { 3667 return; 3668 } 3669 final InputMethodManager imm = InputMethodManager.peekInstance(); 3670 if (null == imm) { 3671 return; 3672 } 3673 if (!imm.isActive(mTextView)) { 3674 return; 3675 } 3676 // Skip if the IME has not requested the cursor/anchor position. 3677 if (!imm.isCursorAnchorInfoEnabled()) { 3678 return; 3679 } 3680 Layout layout = mTextView.getLayout(); 3681 if (layout == null) { 3682 return; 3683 } 3684 3685 final CursorAnchorInfo.Builder builder = mSelectionInfoBuilder; 3686 builder.reset(); 3687 3688 final int selectionStart = mTextView.getSelectionStart(); 3689 builder.setSelectionRange(selectionStart, mTextView.getSelectionEnd()); 3690 3691 // Construct transformation matrix from view local coordinates to screen coordinates. 3692 mViewToScreenMatrix.set(mTextView.getMatrix()); 3693 mTextView.getLocationOnScreen(mTmpIntOffset); 3694 mViewToScreenMatrix.postTranslate(mTmpIntOffset[0], mTmpIntOffset[1]); 3695 builder.setMatrix(mViewToScreenMatrix); 3696 3697 final float viewportToContentHorizontalOffset = 3698 mTextView.viewportToContentHorizontalOffset(); 3699 final float viewportToContentVerticalOffset = 3700 mTextView.viewportToContentVerticalOffset(); 3701 3702 final CharSequence text = mTextView.getText(); 3703 if (text instanceof Spannable) { 3704 final Spannable sp = (Spannable) text; 3705 int composingTextStart = EditableInputConnection.getComposingSpanStart(sp); 3706 int composingTextEnd = EditableInputConnection.getComposingSpanEnd(sp); 3707 if (composingTextEnd < composingTextStart) { 3708 final int temp = composingTextEnd; 3709 composingTextEnd = composingTextStart; 3710 composingTextStart = temp; 3711 } 3712 final boolean hasComposingText = 3713 (0 <= composingTextStart) && (composingTextStart < composingTextEnd); 3714 if (hasComposingText) { 3715 final CharSequence composingText = text.subSequence(composingTextStart, 3716 composingTextEnd); 3717 builder.setComposingText(composingTextStart, composingText); 3718 3719 final int minLine = layout.getLineForOffset(composingTextStart); 3720 final int maxLine = layout.getLineForOffset(composingTextEnd - 1); 3721 for (int line = minLine; line <= maxLine; ++line) { 3722 final int lineStart = layout.getLineStart(line); 3723 final int lineEnd = layout.getLineEnd(line); 3724 final int offsetStart = Math.max(lineStart, composingTextStart); 3725 final int offsetEnd = Math.min(lineEnd, composingTextEnd); 3726 final boolean ltrLine = 3727 layout.getParagraphDirection(line) == Layout.DIR_LEFT_TO_RIGHT; 3728 final float[] widths = new float[offsetEnd - offsetStart]; 3729 layout.getPaint().getTextWidths(text, offsetStart, offsetEnd, widths); 3730 final float top = layout.getLineTop(line); 3731 final float bottom = layout.getLineBottom(line); 3732 for (int offset = offsetStart; offset < offsetEnd; ++offset) { 3733 final float charWidth = widths[offset - offsetStart]; 3734 final boolean isRtl = layout.isRtlCharAt(offset); 3735 final float primary = layout.getPrimaryHorizontal(offset); 3736 final float secondary = layout.getSecondaryHorizontal(offset); 3737 // TODO: This doesn't work perfectly for text with custom styles and 3738 // TAB chars. 3739 final float left; 3740 final float right; 3741 if (ltrLine) { 3742 if (isRtl) { 3743 left = secondary - charWidth; 3744 right = secondary; 3745 } else { 3746 left = primary; 3747 right = primary + charWidth; 3748 } 3749 } else { 3750 if (!isRtl) { 3751 left = secondary; 3752 right = secondary + charWidth; 3753 } else { 3754 left = primary - charWidth; 3755 right = primary; 3756 } 3757 } 3758 // TODO: Check top-right and bottom-left as well. 3759 final float localLeft = left + viewportToContentHorizontalOffset; 3760 final float localRight = right + viewportToContentHorizontalOffset; 3761 final float localTop = top + viewportToContentVerticalOffset; 3762 final float localBottom = bottom + viewportToContentVerticalOffset; 3763 final boolean isTopLeftVisible = isPositionVisible(localLeft, localTop); 3764 final boolean isBottomRightVisible = 3765 isPositionVisible(localRight, localBottom); 3766 int characterBoundsFlags = 0; 3767 if (isTopLeftVisible || isBottomRightVisible) { 3768 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; 3769 } 3770 if (!isTopLeftVisible || !isBottomRightVisible) { 3771 characterBoundsFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; 3772 } 3773 if (isRtl) { 3774 characterBoundsFlags |= CursorAnchorInfo.FLAG_IS_RTL; 3775 } 3776 // Here offset is the index in Java chars. 3777 builder.addCharacterBounds(offset, localLeft, localTop, localRight, 3778 localBottom, characterBoundsFlags); 3779 } 3780 } 3781 } 3782 } 3783 3784 // Treat selectionStart as the insertion point. 3785 if (0 <= selectionStart) { 3786 final int offset = selectionStart; 3787 final int line = layout.getLineForOffset(offset); 3788 final float insertionMarkerX = layout.getPrimaryHorizontal(offset) 3789 + viewportToContentHorizontalOffset; 3790 final float insertionMarkerTop = layout.getLineTop(line) 3791 + viewportToContentVerticalOffset; 3792 final float insertionMarkerBaseline = layout.getLineBaseline(line) 3793 + viewportToContentVerticalOffset; 3794 final float insertionMarkerBottom = layout.getLineBottom(line) 3795 + viewportToContentVerticalOffset; 3796 final boolean isTopVisible = 3797 isPositionVisible(insertionMarkerX, insertionMarkerTop); 3798 final boolean isBottomVisible = 3799 isPositionVisible(insertionMarkerX, insertionMarkerBottom); 3800 int insertionMarkerFlags = 0; 3801 if (isTopVisible || isBottomVisible) { 3802 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION; 3803 } 3804 if (!isTopVisible || !isBottomVisible) { 3805 insertionMarkerFlags |= CursorAnchorInfo.FLAG_HAS_INVISIBLE_REGION; 3806 } 3807 if (layout.isRtlCharAt(offset)) { 3808 insertionMarkerFlags |= CursorAnchorInfo.FLAG_IS_RTL; 3809 } 3810 builder.setInsertionMarkerLocation(insertionMarkerX, insertionMarkerTop, 3811 insertionMarkerBaseline, insertionMarkerBottom, insertionMarkerFlags); 3812 } 3813 3814 imm.updateCursorAnchorInfo(mTextView, builder.build()); 3815 } 3816 } 3817 3818 @VisibleForTesting 3819 public abstract class HandleView extends View implements TextViewPositionListener { 3820 protected Drawable mDrawable; 3821 protected Drawable mDrawableLtr; 3822 protected Drawable mDrawableRtl; 3823 private final PopupWindow mContainer; 3824 // Position with respect to the parent TextView 3825 private int mPositionX, mPositionY; 3826 private boolean mIsDragging; 3827 // Offset from touch position to mPosition 3828 private float mTouchToWindowOffsetX, mTouchToWindowOffsetY; 3829 protected int mHotspotX; 3830 protected int mHorizontalGravity; 3831 // Offsets the hotspot point up, so that cursor is not hidden by the finger when moving up 3832 private float mTouchOffsetY; 3833 // Where the touch position should be on the handle to ensure a maximum cursor visibility 3834 private float mIdealVerticalOffset; 3835 // Parent's (TextView) previous position in window 3836 private int mLastParentX, mLastParentY; 3837 // Previous text character offset 3838 protected int mPreviousOffset = -1; 3839 // Previous text character offset 3840 private boolean mPositionHasChanged = true; 3841 // Minimum touch target size for handles 3842 private int mMinSize; 3843 // Indicates the line of text that the handle is on. 3844 protected int mPrevLine = UNSET_LINE; 3845 // Indicates the line of text that the user was touching. This can differ from mPrevLine 3846 // when selecting text when the handles jump to the end / start of words which may be on 3847 // a different line. 3848 protected int mPreviousLineTouched = UNSET_LINE; 3849 3850 private HandleView(Drawable drawableLtr, Drawable drawableRtl, final int id) { 3851 super(mTextView.getContext()); 3852 setId(id); 3853 mContainer = new PopupWindow(mTextView.getContext(), null, 3854 com.android.internal.R.attr.textSelectHandleWindowStyle); 3855 mContainer.setSplitTouchEnabled(true); 3856 mContainer.setClippingEnabled(false); 3857 mContainer.setWindowLayoutType(WindowManager.LayoutParams.TYPE_APPLICATION_SUB_PANEL); 3858 mContainer.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT); 3859 mContainer.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT); 3860 mContainer.setContentView(this); 3861 3862 mDrawableLtr = drawableLtr; 3863 mDrawableRtl = drawableRtl; 3864 mMinSize = mTextView.getContext().getResources().getDimensionPixelSize( 3865 com.android.internal.R.dimen.text_handle_min_size); 3866 3867 updateDrawable(); 3868 3869 final int handleHeight = getPreferredHeight(); 3870 mTouchOffsetY = -0.3f * handleHeight; 3871 mIdealVerticalOffset = 0.7f * handleHeight; 3872 } 3873 3874 public float getIdealVerticalOffset() { 3875 return mIdealVerticalOffset; 3876 } 3877 3878 protected void updateDrawable() { 3879 if (mIsDragging) { 3880 // Don't update drawable during dragging. 3881 return; 3882 } 3883 final int offset = getCurrentCursorOffset(); 3884 final boolean isRtlCharAtOffset = mTextView.getLayout().isRtlCharAt(offset); 3885 final Drawable oldDrawable = mDrawable; 3886 mDrawable = isRtlCharAtOffset ? mDrawableRtl : mDrawableLtr; 3887 mHotspotX = getHotspotX(mDrawable, isRtlCharAtOffset); 3888 mHorizontalGravity = getHorizontalGravity(isRtlCharAtOffset); 3889 final Layout layout = mTextView.getLayout(); 3890 if (layout != null && oldDrawable != mDrawable && isShowing()) { 3891 // Update popup window position. 3892 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX - 3893 getHorizontalOffset() + getCursorOffset()); 3894 mPositionX += mTextView.viewportToContentHorizontalOffset(); 3895 mPositionHasChanged = true; 3896 updatePosition(mLastParentX, mLastParentY, false, false); 3897 postInvalidate(); 3898 } 3899 } 3900 3901 protected abstract int getHotspotX(Drawable drawable, boolean isRtlRun); 3902 protected abstract int getHorizontalGravity(boolean isRtlRun); 3903 3904 // Touch-up filter: number of previous positions remembered 3905 private static final int HISTORY_SIZE = 5; 3906 private static final int TOUCH_UP_FILTER_DELAY_AFTER = 150; 3907 private static final int TOUCH_UP_FILTER_DELAY_BEFORE = 350; 3908 private final long[] mPreviousOffsetsTimes = new long[HISTORY_SIZE]; 3909 private final int[] mPreviousOffsets = new int[HISTORY_SIZE]; 3910 private int mPreviousOffsetIndex = 0; 3911 private int mNumberPreviousOffsets = 0; 3912 3913 private void startTouchUpFilter(int offset) { 3914 mNumberPreviousOffsets = 0; 3915 addPositionToTouchUpFilter(offset); 3916 } 3917 3918 private void addPositionToTouchUpFilter(int offset) { 3919 mPreviousOffsetIndex = (mPreviousOffsetIndex + 1) % HISTORY_SIZE; 3920 mPreviousOffsets[mPreviousOffsetIndex] = offset; 3921 mPreviousOffsetsTimes[mPreviousOffsetIndex] = SystemClock.uptimeMillis(); 3922 mNumberPreviousOffsets++; 3923 } 3924 3925 private void filterOnTouchUp() { 3926 final long now = SystemClock.uptimeMillis(); 3927 int i = 0; 3928 int index = mPreviousOffsetIndex; 3929 final int iMax = Math.min(mNumberPreviousOffsets, HISTORY_SIZE); 3930 while (i < iMax && (now - mPreviousOffsetsTimes[index]) < TOUCH_UP_FILTER_DELAY_AFTER) { 3931 i++; 3932 index = (mPreviousOffsetIndex - i + HISTORY_SIZE) % HISTORY_SIZE; 3933 } 3934 3935 if (i > 0 && i < iMax && 3936 (now - mPreviousOffsetsTimes[index]) > TOUCH_UP_FILTER_DELAY_BEFORE) { 3937 positionAtCursorOffset(mPreviousOffsets[index], false); 3938 } 3939 } 3940 3941 public boolean offsetHasBeenChanged() { 3942 return mNumberPreviousOffsets > 1; 3943 } 3944 3945 @Override 3946 protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 3947 setMeasuredDimension(getPreferredWidth(), getPreferredHeight()); 3948 } 3949 3950 private int getPreferredWidth() { 3951 return Math.max(mDrawable.getIntrinsicWidth(), mMinSize); 3952 } 3953 3954 private int getPreferredHeight() { 3955 return Math.max(mDrawable.getIntrinsicHeight(), mMinSize); 3956 } 3957 3958 public void show() { 3959 if (isShowing()) return; 3960 3961 getPositionListener().addSubscriber(this, true /* local position may change */); 3962 3963 // Make sure the offset is always considered new, even when focusing at same position 3964 mPreviousOffset = -1; 3965 positionAtCursorOffset(getCurrentCursorOffset(), false); 3966 } 3967 3968 protected void dismiss() { 3969 mIsDragging = false; 3970 mContainer.dismiss(); 3971 onDetached(); 3972 } 3973 3974 public void hide() { 3975 dismiss(); 3976 3977 getPositionListener().removeSubscriber(this); 3978 } 3979 3980 public boolean isShowing() { 3981 return mContainer.isShowing(); 3982 } 3983 3984 private boolean isVisible() { 3985 // Always show a dragging handle. 3986 if (mIsDragging) { 3987 return true; 3988 } 3989 3990 if (mTextView.isInBatchEditMode()) { 3991 return false; 3992 } 3993 3994 return isPositionVisible(mPositionX + mHotspotX + getHorizontalOffset(), mPositionY); 3995 } 3996 3997 public abstract int getCurrentCursorOffset(); 3998 3999 protected abstract void updateSelection(int offset); 4000 4001 public abstract void updatePosition(float x, float y); 4002 4003 protected void positionAtCursorOffset(int offset, boolean parentScrolled) { 4004 // A HandleView relies on the layout, which may be nulled by external methods 4005 Layout layout = mTextView.getLayout(); 4006 if (layout == null) { 4007 // Will update controllers' state, hiding them and stopping selection mode if needed 4008 prepareCursorControllers(); 4009 return; 4010 } 4011 layout = getActiveLayout(); 4012 4013 boolean offsetChanged = offset != mPreviousOffset; 4014 if (offsetChanged || parentScrolled) { 4015 if (offsetChanged) { 4016 updateSelection(offset); 4017 addPositionToTouchUpFilter(offset); 4018 } 4019 final int line = layout.getLineForOffset(offset); 4020 mPrevLine = line; 4021 4022 mPositionX = (int) (layout.getPrimaryHorizontal(offset) - 0.5f - mHotspotX - 4023 getHorizontalOffset() + getCursorOffset()); 4024 mPositionY = layout.getLineBottom(line); 4025 4026 // Take TextView's padding and scroll into account. 4027 mPositionX += mTextView.viewportToContentHorizontalOffset(); 4028 mPositionY += mTextView.viewportToContentVerticalOffset(); 4029 4030 mPreviousOffset = offset; 4031 mPositionHasChanged = true; 4032 } 4033 } 4034 4035 public void updatePosition(int parentPositionX, int parentPositionY, 4036 boolean parentPositionChanged, boolean parentScrolled) { 4037 positionAtCursorOffset(getCurrentCursorOffset(), parentScrolled); 4038 if (parentPositionChanged || mPositionHasChanged) { 4039 if (mIsDragging) { 4040 // Update touchToWindow offset in case of parent scrolling while dragging 4041 if (parentPositionX != mLastParentX || parentPositionY != mLastParentY) { 4042 mTouchToWindowOffsetX += parentPositionX - mLastParentX; 4043 mTouchToWindowOffsetY += parentPositionY - mLastParentY; 4044 mLastParentX = parentPositionX; 4045 mLastParentY = parentPositionY; 4046 } 4047 4048 onHandleMoved(); 4049 } 4050 4051 if (isVisible()) { 4052 final int positionX = parentPositionX + mPositionX; 4053 final int positionY = parentPositionY + mPositionY; 4054 if (isShowing()) { 4055 mContainer.update(positionX, positionY, -1, -1); 4056 } else { 4057 mContainer.showAtLocation(mTextView, Gravity.NO_GRAVITY, 4058 positionX, positionY); 4059 } 4060 } else { 4061 if (isShowing()) { 4062 dismiss(); 4063 } 4064 } 4065 4066 mPositionHasChanged = false; 4067 } 4068 } 4069 4070 @Override 4071 protected void onDraw(Canvas c) { 4072 final int drawWidth = mDrawable.getIntrinsicWidth(); 4073 final int left = getHorizontalOffset(); 4074 4075 mDrawable.setBounds(left, 0, left + drawWidth, mDrawable.getIntrinsicHeight()); 4076 mDrawable.draw(c); 4077 } 4078 4079 private int getHorizontalOffset() { 4080 final int width = getPreferredWidth(); 4081 final int drawWidth = mDrawable.getIntrinsicWidth(); 4082 final int left; 4083 switch (mHorizontalGravity) { 4084 case Gravity.LEFT: 4085 left = 0; 4086 break; 4087 default: 4088 case Gravity.CENTER: 4089 left = (width - drawWidth) / 2; 4090 break; 4091 case Gravity.RIGHT: 4092 left = width - drawWidth; 4093 break; 4094 } 4095 return left; 4096 } 4097 4098 protected int getCursorOffset() { 4099 return 0; 4100 } 4101 4102 @Override 4103 public boolean onTouchEvent(MotionEvent ev) { 4104 updateFloatingToolbarVisibility(ev); 4105 4106 switch (ev.getActionMasked()) { 4107 case MotionEvent.ACTION_DOWN: { 4108 startTouchUpFilter(getCurrentCursorOffset()); 4109 mTouchToWindowOffsetX = ev.getRawX() - mPositionX; 4110 mTouchToWindowOffsetY = ev.getRawY() - mPositionY; 4111 4112 final PositionListener positionListener = getPositionListener(); 4113 mLastParentX = positionListener.getPositionX(); 4114 mLastParentY = positionListener.getPositionY(); 4115 mIsDragging = true; 4116 mPreviousLineTouched = UNSET_LINE; 4117 break; 4118 } 4119 4120 case MotionEvent.ACTION_MOVE: { 4121 final float rawX = ev.getRawX(); 4122 final float rawY = ev.getRawY(); 4123 4124 // Vertical hysteresis: vertical down movement tends to snap to ideal offset 4125 final float previousVerticalOffset = mTouchToWindowOffsetY - mLastParentY; 4126 final float currentVerticalOffset = rawY - mPositionY - mLastParentY; 4127 float newVerticalOffset; 4128 if (previousVerticalOffset < mIdealVerticalOffset) { 4129 newVerticalOffset = Math.min(currentVerticalOffset, mIdealVerticalOffset); 4130 newVerticalOffset = Math.max(newVerticalOffset, previousVerticalOffset); 4131 } else { 4132 newVerticalOffset = Math.max(currentVerticalOffset, mIdealVerticalOffset); 4133 newVerticalOffset = Math.min(newVerticalOffset, previousVerticalOffset); 4134 } 4135 mTouchToWindowOffsetY = newVerticalOffset + mLastParentY; 4136 4137 final float newPosX = 4138 rawX - mTouchToWindowOffsetX + mHotspotX + getHorizontalOffset(); 4139 final float newPosY = rawY - mTouchToWindowOffsetY + mTouchOffsetY; 4140 4141 updatePosition(newPosX, newPosY); 4142 break; 4143 } 4144 4145 case MotionEvent.ACTION_UP: 4146 filterOnTouchUp(); 4147 mIsDragging = false; 4148 updateDrawable(); 4149 break; 4150 4151 case MotionEvent.ACTION_CANCEL: 4152 mIsDragging = false; 4153 updateDrawable(); 4154 break; 4155 } 4156 return true; 4157 } 4158 4159 public boolean isDragging() { 4160 return mIsDragging; 4161 } 4162 4163 void onHandleMoved() {} 4164 4165 public void onDetached() {} 4166 } 4167 4168 /** 4169 * Returns the active layout (hint or text layout). Note that the text layout can be null. 4170 */ 4171 private Layout getActiveLayout() { 4172 Layout layout = mTextView.getLayout(); 4173 Layout hintLayout = mTextView.getHintLayout(); 4174 if (TextUtils.isEmpty(layout.getText()) && hintLayout != null && 4175 !TextUtils.isEmpty(hintLayout.getText())) { 4176 layout = hintLayout; 4177 } 4178 return layout; 4179 } 4180 4181 private class InsertionHandleView extends HandleView { 4182 private static final int DELAY_BEFORE_HANDLE_FADES_OUT = 4000; 4183 private static final int RECENT_CUT_COPY_DURATION = 15 * 1000; // seconds 4184 4185 // Used to detect taps on the insertion handle, which will affect the insertion action mode 4186 private float mDownPositionX, mDownPositionY; 4187 private Runnable mHider; 4188 4189 public InsertionHandleView(Drawable drawable) { 4190 super(drawable, drawable, com.android.internal.R.id.insertion_handle); 4191 } 4192 4193 @Override 4194 public void show() { 4195 super.show(); 4196 4197 final long durationSinceCutOrCopy = 4198 SystemClock.uptimeMillis() - TextView.sLastCutCopyOrTextChangedTime; 4199 4200 // Cancel the single tap delayed runnable. 4201 if (mInsertionActionModeRunnable != null 4202 && ((mTapState == TAP_STATE_DOUBLE_TAP) 4203 || (mTapState == TAP_STATE_TRIPLE_CLICK) 4204 || isCursorInsideEasyCorrectionSpan())) { 4205 mTextView.removeCallbacks(mInsertionActionModeRunnable); 4206 } 4207 4208 // Prepare and schedule the single tap runnable to run exactly after the double tap 4209 // timeout has passed. 4210 if ((mTapState != TAP_STATE_DOUBLE_TAP) && (mTapState != TAP_STATE_TRIPLE_CLICK) 4211 && !isCursorInsideEasyCorrectionSpan() 4212 && (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION)) { 4213 if (mTextActionMode == null) { 4214 if (mInsertionActionModeRunnable == null) { 4215 mInsertionActionModeRunnable = new Runnable() { 4216 @Override 4217 public void run() { 4218 startInsertionActionMode(); 4219 } 4220 }; 4221 } 4222 mTextView.postDelayed( 4223 mInsertionActionModeRunnable, 4224 ViewConfiguration.getDoubleTapTimeout() + 1); 4225 } 4226 4227 } 4228 4229 hideAfterDelay(); 4230 } 4231 4232 private void hideAfterDelay() { 4233 if (mHider == null) { 4234 mHider = new Runnable() { 4235 public void run() { 4236 hide(); 4237 } 4238 }; 4239 } else { 4240 removeHiderCallback(); 4241 } 4242 mTextView.postDelayed(mHider, DELAY_BEFORE_HANDLE_FADES_OUT); 4243 } 4244 4245 private void removeHiderCallback() { 4246 if (mHider != null) { 4247 mTextView.removeCallbacks(mHider); 4248 } 4249 } 4250 4251 @Override 4252 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 4253 return drawable.getIntrinsicWidth() / 2; 4254 } 4255 4256 @Override 4257 protected int getHorizontalGravity(boolean isRtlRun) { 4258 return Gravity.CENTER_HORIZONTAL; 4259 } 4260 4261 @Override 4262 protected int getCursorOffset() { 4263 int offset = super.getCursorOffset(); 4264 final Drawable cursor = mCursorCount > 0 ? mCursorDrawable[0] : null; 4265 if (cursor != null) { 4266 cursor.getPadding(mTempRect); 4267 offset += (cursor.getIntrinsicWidth() - mTempRect.left - mTempRect.right) / 2; 4268 } 4269 return offset; 4270 } 4271 4272 @Override 4273 public boolean onTouchEvent(MotionEvent ev) { 4274 final boolean result = super.onTouchEvent(ev); 4275 4276 switch (ev.getActionMasked()) { 4277 case MotionEvent.ACTION_DOWN: 4278 mDownPositionX = ev.getRawX(); 4279 mDownPositionY = ev.getRawY(); 4280 break; 4281 4282 case MotionEvent.ACTION_UP: 4283 if (!offsetHasBeenChanged()) { 4284 final float deltaX = mDownPositionX - ev.getRawX(); 4285 final float deltaY = mDownPositionY - ev.getRawY(); 4286 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 4287 4288 final ViewConfiguration viewConfiguration = ViewConfiguration.get( 4289 mTextView.getContext()); 4290 final int touchSlop = viewConfiguration.getScaledTouchSlop(); 4291 4292 if (distanceSquared < touchSlop * touchSlop) { 4293 // Tapping on the handle toggles the insertion action mode. 4294 if (mTextActionMode != null) { 4295 mTextActionMode.finish(); 4296 } else { 4297 startInsertionActionMode(); 4298 } 4299 } 4300 } else { 4301 if (mTextActionMode != null) { 4302 mTextActionMode.invalidateContentRect(); 4303 } 4304 } 4305 hideAfterDelay(); 4306 break; 4307 4308 case MotionEvent.ACTION_CANCEL: 4309 hideAfterDelay(); 4310 break; 4311 4312 default: 4313 break; 4314 } 4315 4316 return result; 4317 } 4318 4319 @Override 4320 public int getCurrentCursorOffset() { 4321 return mTextView.getSelectionStart(); 4322 } 4323 4324 @Override 4325 public void updateSelection(int offset) { 4326 Selection.setSelection((Spannable) mTextView.getText(), offset); 4327 } 4328 4329 @Override 4330 public void updatePosition(float x, float y) { 4331 Layout layout = mTextView.getLayout(); 4332 int offset; 4333 if (layout != null) { 4334 if (mPreviousLineTouched == UNSET_LINE) { 4335 mPreviousLineTouched = mTextView.getLineAtCoordinate(y); 4336 } 4337 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y); 4338 offset = mTextView.getOffsetAtCoordinate(currLine, x); 4339 mPreviousLineTouched = currLine; 4340 } else { 4341 offset = mTextView.getOffsetForPosition(x, y); 4342 } 4343 positionAtCursorOffset(offset, false); 4344 if (mTextActionMode != null) { 4345 mTextActionMode.invalidate(); 4346 } 4347 } 4348 4349 @Override 4350 void onHandleMoved() { 4351 super.onHandleMoved(); 4352 removeHiderCallback(); 4353 } 4354 4355 @Override 4356 public void onDetached() { 4357 super.onDetached(); 4358 removeHiderCallback(); 4359 } 4360 } 4361 4362 @Retention(RetentionPolicy.SOURCE) 4363 @IntDef({HANDLE_TYPE_SELECTION_START, HANDLE_TYPE_SELECTION_END}) 4364 public @interface HandleType {} 4365 public static final int HANDLE_TYPE_SELECTION_START = 0; 4366 public static final int HANDLE_TYPE_SELECTION_END = 1; 4367 4368 private class SelectionHandleView extends HandleView { 4369 // Indicates the handle type, selection start (HANDLE_TYPE_SELECTION_START) or selection 4370 // end (HANDLE_TYPE_SELECTION_END). 4371 @HandleType 4372 private final int mHandleType; 4373 // Indicates whether the cursor is making adjustments within a word. 4374 private boolean mInWord = false; 4375 // Difference between touch position and word boundary position. 4376 private float mTouchWordDelta; 4377 // X value of the previous updatePosition call. 4378 private float mPrevX; 4379 // Indicates if the handle has moved a boundary between LTR and RTL text. 4380 private boolean mLanguageDirectionChanged = false; 4381 // Distance from edge of horizontally scrolling text view 4382 // to use to switch to character mode. 4383 private final float mTextViewEdgeSlop; 4384 // Used to save text view location. 4385 private final int[] mTextViewLocation = new int[2]; 4386 4387 public SelectionHandleView(Drawable drawableLtr, Drawable drawableRtl, int id, 4388 @HandleType int handleType) { 4389 super(drawableLtr, drawableRtl, id); 4390 mHandleType = handleType; 4391 ViewConfiguration viewConfiguration = ViewConfiguration.get(mTextView.getContext()); 4392 mTextViewEdgeSlop = viewConfiguration.getScaledTouchSlop() * 4; 4393 } 4394 4395 private boolean isStartHandle() { 4396 return mHandleType == HANDLE_TYPE_SELECTION_START; 4397 } 4398 4399 @Override 4400 protected int getHotspotX(Drawable drawable, boolean isRtlRun) { 4401 if (isRtlRun == isStartHandle()) { 4402 return drawable.getIntrinsicWidth() / 4; 4403 } else { 4404 return (drawable.getIntrinsicWidth() * 3) / 4; 4405 } 4406 } 4407 4408 @Override 4409 protected int getHorizontalGravity(boolean isRtlRun) { 4410 return (isRtlRun == isStartHandle()) ? Gravity.LEFT : Gravity.RIGHT; 4411 } 4412 4413 @Override 4414 public int getCurrentCursorOffset() { 4415 return isStartHandle() ? mTextView.getSelectionStart() : mTextView.getSelectionEnd(); 4416 } 4417 4418 @Override 4419 protected void updateSelection(int offset) { 4420 if (isStartHandle()) { 4421 Selection.setSelection((Spannable) mTextView.getText(), offset, 4422 mTextView.getSelectionEnd()); 4423 } else { 4424 Selection.setSelection((Spannable) mTextView.getText(), 4425 mTextView.getSelectionStart(), offset); 4426 } 4427 updateDrawable(); 4428 if (mTextActionMode != null) { 4429 mTextActionMode.invalidate(); 4430 } 4431 } 4432 4433 @Override 4434 public void updatePosition(float x, float y) { 4435 final Layout layout = mTextView.getLayout(); 4436 if (layout == null) { 4437 // HandleView will deal appropriately in positionAtCursorOffset when 4438 // layout is null. 4439 positionAndAdjustForCrossingHandles(mTextView.getOffsetForPosition(x, y)); 4440 return; 4441 } 4442 4443 if (mPreviousLineTouched == UNSET_LINE) { 4444 mPreviousLineTouched = mTextView.getLineAtCoordinate(y); 4445 } 4446 4447 boolean positionCursor = false; 4448 final int anotherHandleOffset = 4449 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart(); 4450 int currLine = getCurrentLineAdjustedForSlop(layout, mPreviousLineTouched, y); 4451 int initialOffset = mTextView.getOffsetAtCoordinate(currLine, x); 4452 4453 if (isStartHandle() && initialOffset >= anotherHandleOffset 4454 || !isStartHandle() && initialOffset <= anotherHandleOffset) { 4455 // Handles have crossed, bound it to the first selected line and 4456 // adjust by word / char as normal. 4457 currLine = layout.getLineForOffset(anotherHandleOffset); 4458 initialOffset = mTextView.getOffsetAtCoordinate(currLine, x); 4459 } 4460 4461 int offset = initialOffset; 4462 final int wordEnd = getWordEnd(offset); 4463 final int wordStart = getWordStart(offset); 4464 4465 if (mPrevX == UNSET_X_VALUE) { 4466 mPrevX = x; 4467 } 4468 4469 final int currentOffset = getCurrentCursorOffset(); 4470 final boolean rtlAtCurrentOffset = layout.isRtlCharAt(currentOffset); 4471 final boolean atRtl = layout.isRtlCharAt(offset); 4472 final boolean isLvlBoundary = layout.isLevelBoundary(offset); 4473 4474 // We can't determine if the user is expanding or shrinking the selection if they're 4475 // on a bi-di boundary, so until they've moved past the boundary we'll just place 4476 // the cursor at the current position. 4477 if (isLvlBoundary || (rtlAtCurrentOffset && !atRtl) || (!rtlAtCurrentOffset && atRtl)) { 4478 // We're on a boundary or this is the first direction change -- just update 4479 // to the current position. 4480 mLanguageDirectionChanged = true; 4481 mTouchWordDelta = 0.0f; 4482 positionAndAdjustForCrossingHandles(offset); 4483 return; 4484 } else if (mLanguageDirectionChanged && !isLvlBoundary) { 4485 // We've just moved past the boundary so update the position. After this we can 4486 // figure out if the user is expanding or shrinking to go by word or character. 4487 positionAndAdjustForCrossingHandles(offset); 4488 mTouchWordDelta = 0.0f; 4489 mLanguageDirectionChanged = false; 4490 return; 4491 } 4492 4493 boolean isExpanding; 4494 final float xDiff = x - mPrevX; 4495 if (isStartHandle()) { 4496 isExpanding = currLine < mPreviousLineTouched; 4497 } else { 4498 isExpanding = currLine > mPreviousLineTouched; 4499 } 4500 if (atRtl == isStartHandle()) { 4501 isExpanding |= xDiff > 0; 4502 } else { 4503 isExpanding |= xDiff < 0; 4504 } 4505 4506 if (mTextView.getHorizontallyScrolling()) { 4507 if (positionNearEdgeOfScrollingView(x, atRtl) 4508 && ((isStartHandle() && mTextView.getScrollX() != 0) 4509 || (!isStartHandle() 4510 && mTextView.canScrollHorizontally(atRtl ? -1 : 1))) 4511 && ((isExpanding && ((isStartHandle() && offset < currentOffset) 4512 || (!isStartHandle() && offset > currentOffset))) 4513 || !isExpanding)) { 4514 // If we're expanding ensure that the offset is actually expanding compared to 4515 // the current offset, if the handle snapped to the word, the finger position 4516 // may be out of sync and we don't want the selection to jump back. 4517 mTouchWordDelta = 0.0f; 4518 final int nextOffset = (atRtl == isStartHandle()) 4519 ? layout.getOffsetToRightOf(mPreviousOffset) 4520 : layout.getOffsetToLeftOf(mPreviousOffset); 4521 positionAndAdjustForCrossingHandles(nextOffset); 4522 return; 4523 } 4524 } 4525 4526 if (isExpanding) { 4527 // User is increasing the selection. 4528 final boolean snapToWord = !mInWord 4529 || (isStartHandle() ? currLine < mPrevLine : currLine > mPrevLine); 4530 if (snapToWord) { 4531 // Sometimes words can be broken across lines (Chinese, hyphenation). 4532 // We still snap to the word boundary but we only use the letters on the 4533 // current line to determine if the user is far enough into the word to snap. 4534 int wordBoundary = isStartHandle() ? wordStart : wordEnd; 4535 if (layout != null && layout.getLineForOffset(wordBoundary) != currLine) { 4536 wordBoundary = isStartHandle() ? 4537 layout.getLineStart(currLine) : layout.getLineEnd(currLine); 4538 } 4539 final int offsetThresholdToSnap = isStartHandle() 4540 ? wordEnd - ((wordEnd - wordBoundary) / 2) 4541 : wordStart + ((wordBoundary - wordStart) / 2); 4542 if (isStartHandle() 4543 && (offset <= offsetThresholdToSnap || currLine < mPrevLine)) { 4544 // User is far enough into the word or on a different line so we expand by 4545 // word. 4546 offset = wordStart; 4547 } else if (!isStartHandle() 4548 && (offset >= offsetThresholdToSnap || currLine > mPrevLine)) { 4549 // User is far enough into the word or on a different line so we expand by 4550 // word. 4551 offset = wordEnd; 4552 } else { 4553 offset = mPreviousOffset; 4554 } 4555 } 4556 if (layout != null && (isStartHandle() && offset < initialOffset) 4557 || (!isStartHandle() && offset > initialOffset)) { 4558 final float adjustedX = layout.getPrimaryHorizontal(offset); 4559 mTouchWordDelta = 4560 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX; 4561 } else { 4562 mTouchWordDelta = 0.0f; 4563 } 4564 positionCursor = true; 4565 } else { 4566 final int adjustedOffset = 4567 mTextView.getOffsetAtCoordinate(currLine, x - mTouchWordDelta); 4568 final boolean shrinking = isStartHandle() 4569 ? adjustedOffset > mPreviousOffset || currLine > mPrevLine 4570 : adjustedOffset < mPreviousOffset || currLine < mPrevLine; 4571 if (shrinking) { 4572 // User is shrinking the selection. 4573 if (currLine != mPrevLine) { 4574 // We're on a different line, so we'll snap to word boundaries. 4575 offset = isStartHandle() ? wordStart : wordEnd; 4576 if (layout != null && (isStartHandle() && offset < initialOffset) 4577 || (!isStartHandle() && offset > initialOffset)) { 4578 final float adjustedX = layout.getPrimaryHorizontal(offset); 4579 mTouchWordDelta = 4580 mTextView.convertToLocalHorizontalCoordinate(x) - adjustedX; 4581 } else { 4582 mTouchWordDelta = 0.0f; 4583 } 4584 } else { 4585 offset = adjustedOffset; 4586 } 4587 positionCursor = true; 4588 } else if ((isStartHandle() && adjustedOffset < mPreviousOffset) 4589 || (!isStartHandle() && adjustedOffset > mPreviousOffset)) { 4590 // Handle has jumped to the word boundary, and the user is moving 4591 // their finger towards the handle, the delta should be updated. 4592 mTouchWordDelta = mTextView.convertToLocalHorizontalCoordinate(x) - 4593 layout.getPrimaryHorizontal(mPreviousOffset); 4594 } 4595 } 4596 4597 if (positionCursor) { 4598 mPreviousLineTouched = currLine; 4599 positionAndAdjustForCrossingHandles(offset); 4600 } 4601 mPrevX = x; 4602 } 4603 4604 /** 4605 * @param offset Cursor offset. Must be in [-1, length]. 4606 * @param parentScrolled If the parent has been scrolled or not. 4607 */ 4608 @Override 4609 protected void positionAtCursorOffset(int offset, boolean parentScrolled) { 4610 super.positionAtCursorOffset(offset, parentScrolled); 4611 mInWord = (offset != -1) && !getWordIteratorWithText().isBoundary(offset); 4612 } 4613 4614 @Override 4615 public boolean onTouchEvent(MotionEvent event) { 4616 boolean superResult = super.onTouchEvent(event); 4617 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 4618 // Reset the touch word offset and x value when the user 4619 // re-engages the handle. 4620 mTouchWordDelta = 0.0f; 4621 mPrevX = UNSET_X_VALUE; 4622 } 4623 return superResult; 4624 } 4625 4626 private void positionAndAdjustForCrossingHandles(int offset) { 4627 final int anotherHandleOffset = 4628 isStartHandle() ? mTextView.getSelectionEnd() : mTextView.getSelectionStart(); 4629 if ((isStartHandle() && offset >= anotherHandleOffset) 4630 || (!isStartHandle() && offset <= anotherHandleOffset)) { 4631 // Handles can not cross and selection is at least one character. 4632 offset = getNextCursorOffset(anotherHandleOffset, !isStartHandle()); 4633 mTouchWordDelta = 0.0f; 4634 } 4635 positionAtCursorOffset(offset, false); 4636 } 4637 4638 private boolean positionNearEdgeOfScrollingView(float x, boolean atRtl) { 4639 mTextView.getLocationOnScreen(mTextViewLocation); 4640 boolean nearEdge; 4641 if (atRtl == isStartHandle()) { 4642 int rightEdge = mTextViewLocation[0] + mTextView.getWidth() 4643 - mTextView.getPaddingRight(); 4644 nearEdge = x > rightEdge - mTextViewEdgeSlop; 4645 } else { 4646 int leftEdge = mTextViewLocation[0] + mTextView.getPaddingLeft(); 4647 nearEdge = x < leftEdge + mTextViewEdgeSlop; 4648 } 4649 return nearEdge; 4650 } 4651 } 4652 4653 private int getCurrentLineAdjustedForSlop(Layout layout, int prevLine, float y) { 4654 final int trueLine = mTextView.getLineAtCoordinate(y); 4655 if (layout == null || prevLine > layout.getLineCount() 4656 || layout.getLineCount() <= 0 || prevLine < 0) { 4657 // Invalid parameters, just return whatever line is at y. 4658 return trueLine; 4659 } 4660 4661 if (Math.abs(trueLine - prevLine) >= 2) { 4662 // Only stick to lines if we're within a line of the previous selection. 4663 return trueLine; 4664 } 4665 4666 final float verticalOffset = mTextView.viewportToContentVerticalOffset(); 4667 final int lineCount = layout.getLineCount(); 4668 final float slop = mTextView.getLineHeight() * LINE_SLOP_MULTIPLIER_FOR_HANDLEVIEWS; 4669 4670 final float firstLineTop = layout.getLineTop(0) + verticalOffset; 4671 final float prevLineTop = layout.getLineTop(prevLine) + verticalOffset; 4672 final float yTopBound = Math.max(prevLineTop - slop, firstLineTop + slop); 4673 4674 final float lastLineBottom = layout.getLineBottom(lineCount - 1) + verticalOffset; 4675 final float prevLineBottom = layout.getLineBottom(prevLine) + verticalOffset; 4676 final float yBottomBound = Math.min(prevLineBottom + slop, lastLineBottom - slop); 4677 4678 // Determine if we've moved lines based on y position and previous line. 4679 int currLine; 4680 if (y <= yTopBound) { 4681 currLine = Math.max(prevLine - 1, 0); 4682 } else if (y >= yBottomBound) { 4683 currLine = Math.min(prevLine + 1, lineCount - 1); 4684 } else { 4685 currLine = prevLine; 4686 } 4687 return currLine; 4688 } 4689 4690 /** 4691 * A CursorController instance can be used to control a cursor in the text. 4692 */ 4693 private interface CursorController extends ViewTreeObserver.OnTouchModeChangeListener { 4694 /** 4695 * Makes the cursor controller visible on screen. 4696 * See also {@link #hide()}. 4697 */ 4698 public void show(); 4699 4700 /** 4701 * Hide the cursor controller from screen. 4702 * See also {@link #show()}. 4703 */ 4704 public void hide(); 4705 4706 /** 4707 * Called when the view is detached from window. Perform house keeping task, such as 4708 * stopping Runnable thread that would otherwise keep a reference on the context, thus 4709 * preventing the activity from being recycled. 4710 */ 4711 public void onDetached(); 4712 } 4713 4714 private class InsertionPointCursorController implements CursorController { 4715 private InsertionHandleView mHandle; 4716 4717 public void show() { 4718 getHandle().show(); 4719 4720 if (mSelectionModifierCursorController != null) { 4721 mSelectionModifierCursorController.hide(); 4722 } 4723 } 4724 4725 public void hide() { 4726 if (mHandle != null) { 4727 mHandle.hide(); 4728 } 4729 } 4730 4731 public void onTouchModeChanged(boolean isInTouchMode) { 4732 if (!isInTouchMode) { 4733 hide(); 4734 } 4735 } 4736 4737 private InsertionHandleView getHandle() { 4738 if (mSelectHandleCenter == null) { 4739 mSelectHandleCenter = mTextView.getContext().getDrawable( 4740 mTextView.mTextSelectHandleRes); 4741 } 4742 if (mHandle == null) { 4743 mHandle = new InsertionHandleView(mSelectHandleCenter); 4744 } 4745 return mHandle; 4746 } 4747 4748 @Override 4749 public void onDetached() { 4750 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 4751 observer.removeOnTouchModeChangeListener(this); 4752 4753 if (mHandle != null) mHandle.onDetached(); 4754 } 4755 } 4756 4757 class SelectionModifierCursorController implements CursorController { 4758 // The cursor controller handles, lazily created when shown. 4759 private SelectionHandleView mStartHandle; 4760 private SelectionHandleView mEndHandle; 4761 // The offsets of that last touch down event. Remembered to start selection there. 4762 private int mMinTouchOffset, mMaxTouchOffset; 4763 4764 private float mDownPositionX, mDownPositionY; 4765 private boolean mGestureStayedInTapRegion; 4766 4767 // Where the user first starts the drag motion. 4768 private int mStartOffset = -1; 4769 4770 private boolean mHaventMovedEnoughToStartDrag; 4771 // The line that a selection happened most recently with the drag accelerator. 4772 private int mLineSelectionIsOn = -1; 4773 // Whether the drag accelerator has selected past the initial line. 4774 private boolean mSwitchedLines = false; 4775 4776 // Indicates the drag accelerator mode that the user is currently using. 4777 private int mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE; 4778 // Drag accelerator is inactive. 4779 private static final int DRAG_ACCELERATOR_MODE_INACTIVE = 0; 4780 // Character based selection by dragging. Only for mouse. 4781 private static final int DRAG_ACCELERATOR_MODE_CHARACTER = 1; 4782 // Word based selection by dragging. Enabled after long pressing or double tapping. 4783 private static final int DRAG_ACCELERATOR_MODE_WORD = 2; 4784 // Paragraph based selection by dragging. Enabled after mouse triple click. 4785 private static final int DRAG_ACCELERATOR_MODE_PARAGRAPH = 3; 4786 4787 SelectionModifierCursorController() { 4788 resetTouchOffsets(); 4789 } 4790 4791 public void show() { 4792 if (mTextView.isInBatchEditMode()) { 4793 return; 4794 } 4795 initDrawables(); 4796 initHandles(); 4797 } 4798 4799 private void initDrawables() { 4800 if (mSelectHandleLeft == null) { 4801 mSelectHandleLeft = mTextView.getContext().getDrawable( 4802 mTextView.mTextSelectHandleLeftRes); 4803 } 4804 if (mSelectHandleRight == null) { 4805 mSelectHandleRight = mTextView.getContext().getDrawable( 4806 mTextView.mTextSelectHandleRightRes); 4807 } 4808 } 4809 4810 private void initHandles() { 4811 // Lazy object creation has to be done before updatePosition() is called. 4812 if (mStartHandle == null) { 4813 mStartHandle = new SelectionHandleView(mSelectHandleLeft, mSelectHandleRight, 4814 com.android.internal.R.id.selection_start_handle, 4815 HANDLE_TYPE_SELECTION_START); 4816 } 4817 if (mEndHandle == null) { 4818 mEndHandle = new SelectionHandleView(mSelectHandleRight, mSelectHandleLeft, 4819 com.android.internal.R.id.selection_end_handle, 4820 HANDLE_TYPE_SELECTION_END); 4821 } 4822 4823 mStartHandle.show(); 4824 mEndHandle.show(); 4825 4826 hideInsertionPointCursorController(); 4827 } 4828 4829 public void hide() { 4830 if (mStartHandle != null) mStartHandle.hide(); 4831 if (mEndHandle != null) mEndHandle.hide(); 4832 } 4833 4834 public void enterDrag(int dragAcceleratorMode) { 4835 // Just need to init the handles / hide insertion cursor. 4836 show(); 4837 mDragAcceleratorMode = dragAcceleratorMode; 4838 // Start location of selection. 4839 mStartOffset = mTextView.getOffsetForPosition(mLastDownPositionX, 4840 mLastDownPositionY); 4841 mLineSelectionIsOn = mTextView.getLineAtCoordinate(mLastDownPositionY); 4842 // Don't show the handles until user has lifted finger. 4843 hide(); 4844 4845 // This stops scrolling parents from intercepting the touch event, allowing 4846 // the user to continue dragging across the screen to select text; TextView will 4847 // scroll as necessary. 4848 mTextView.getParent().requestDisallowInterceptTouchEvent(true); 4849 mTextView.cancelLongPress(); 4850 } 4851 4852 public void onTouchEvent(MotionEvent event) { 4853 // This is done even when the View does not have focus, so that long presses can start 4854 // selection and tap can move cursor from this tap position. 4855 final float eventX = event.getX(); 4856 final float eventY = event.getY(); 4857 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 4858 switch (event.getActionMasked()) { 4859 case MotionEvent.ACTION_DOWN: 4860 if (extractedTextModeWillBeStarted()) { 4861 // Prevent duplicating the selection handles until the mode starts. 4862 hide(); 4863 } else { 4864 // Remember finger down position, to be able to start selection from there. 4865 mMinTouchOffset = mMaxTouchOffset = mTextView.getOffsetForPosition( 4866 eventX, eventY); 4867 4868 // Double tap detection 4869 if (mGestureStayedInTapRegion) { 4870 if (mTapState == TAP_STATE_DOUBLE_TAP 4871 || mTapState == TAP_STATE_TRIPLE_CLICK) { 4872 final float deltaX = eventX - mDownPositionX; 4873 final float deltaY = eventY - mDownPositionY; 4874 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 4875 4876 ViewConfiguration viewConfiguration = ViewConfiguration.get( 4877 mTextView.getContext()); 4878 int doubleTapSlop = viewConfiguration.getScaledDoubleTapSlop(); 4879 boolean stayedInArea = 4880 distanceSquared < doubleTapSlop * doubleTapSlop; 4881 4882 if (stayedInArea && (isMouse || isPositionOnText(eventX, eventY))) { 4883 if (mTapState == TAP_STATE_DOUBLE_TAP) { 4884 selectCurrentWordAndStartDrag(); 4885 } else if (mTapState == TAP_STATE_TRIPLE_CLICK) { 4886 selectCurrentParagraphAndStartDrag(); 4887 } 4888 mDiscardNextActionUp = true; 4889 } 4890 } 4891 } 4892 4893 mDownPositionX = eventX; 4894 mDownPositionY = eventY; 4895 mGestureStayedInTapRegion = true; 4896 mHaventMovedEnoughToStartDrag = true; 4897 } 4898 break; 4899 4900 case MotionEvent.ACTION_POINTER_DOWN: 4901 case MotionEvent.ACTION_POINTER_UP: 4902 // Handle multi-point gestures. Keep min and max offset positions. 4903 // Only activated for devices that correctly handle multi-touch. 4904 if (mTextView.getContext().getPackageManager().hasSystemFeature( 4905 PackageManager.FEATURE_TOUCHSCREEN_MULTITOUCH_DISTINCT)) { 4906 updateMinAndMaxOffsets(event); 4907 } 4908 break; 4909 4910 case MotionEvent.ACTION_MOVE: 4911 final ViewConfiguration viewConfig = ViewConfiguration.get( 4912 mTextView.getContext()); 4913 final int touchSlop = viewConfig.getScaledTouchSlop(); 4914 4915 if (mGestureStayedInTapRegion || mHaventMovedEnoughToStartDrag) { 4916 final float deltaX = eventX - mDownPositionX; 4917 final float deltaY = eventY - mDownPositionY; 4918 final float distanceSquared = deltaX * deltaX + deltaY * deltaY; 4919 4920 if (mGestureStayedInTapRegion) { 4921 int doubleTapTouchSlop = viewConfig.getScaledDoubleTapTouchSlop(); 4922 mGestureStayedInTapRegion = 4923 distanceSquared <= doubleTapTouchSlop * doubleTapTouchSlop; 4924 } 4925 if (mHaventMovedEnoughToStartDrag) { 4926 // We don't start dragging until the user has moved enough. 4927 mHaventMovedEnoughToStartDrag = 4928 distanceSquared <= touchSlop * touchSlop; 4929 } 4930 } 4931 4932 if (isMouse && !isDragAcceleratorActive()) { 4933 final int offset = mTextView.getOffsetForPosition(eventX, eventY); 4934 if (mTextView.hasSelection() 4935 && (!mHaventMovedEnoughToStartDrag || mStartOffset != offset) 4936 && offset >= mTextView.getSelectionStart() 4937 && offset <= mTextView.getSelectionEnd()) { 4938 startDragAndDrop(); 4939 break; 4940 } 4941 4942 if (mStartOffset != offset) { 4943 // Start character based drag accelerator. 4944 if (mTextActionMode != null) { 4945 mTextActionMode.finish(); 4946 } 4947 enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER); 4948 mDiscardNextActionUp = true; 4949 mHaventMovedEnoughToStartDrag = false; 4950 } 4951 } 4952 4953 if (mStartHandle != null && mStartHandle.isShowing()) { 4954 // Don't do the drag if the handles are showing already. 4955 break; 4956 } 4957 4958 updateSelection(event); 4959 break; 4960 4961 case MotionEvent.ACTION_UP: 4962 if (!isDragAcceleratorActive()) { 4963 break; 4964 } 4965 updateSelection(event); 4966 4967 // No longer dragging to select text, let the parent intercept events. 4968 mTextView.getParent().requestDisallowInterceptTouchEvent(false); 4969 4970 int startOffset = mTextView.getSelectionStart(); 4971 int endOffset = mTextView.getSelectionEnd(); 4972 4973 // Since we don't let drag handles pass once they're visible, we need to 4974 // make sure the start / end locations are correct because the user *can* 4975 // switch directions during the initial drag. 4976 if (endOffset < startOffset) { 4977 int tmp = endOffset; 4978 endOffset = startOffset; 4979 startOffset = tmp; 4980 4981 // Also update the selection with the right offsets in this case. 4982 Selection.setSelection((Spannable) mTextView.getText(), 4983 startOffset, endOffset); 4984 } 4985 if (startOffset != endOffset) { 4986 startSelectionActionMode(); 4987 } 4988 4989 // No longer the first dragging motion, reset. 4990 resetDragAcceleratorState(); 4991 break; 4992 } 4993 } 4994 4995 private void updateSelection(MotionEvent event) { 4996 if (mTextView.getLayout() != null) { 4997 switch (mDragAcceleratorMode) { 4998 case DRAG_ACCELERATOR_MODE_CHARACTER: 4999 updateCharacterBasedSelection(event); 5000 break; 5001 case DRAG_ACCELERATOR_MODE_WORD: 5002 updateWordBasedSelection(event); 5003 break; 5004 case DRAG_ACCELERATOR_MODE_PARAGRAPH: 5005 updateParagraphBasedSelection(event); 5006 break; 5007 } 5008 } 5009 } 5010 5011 /** 5012 * If the TextView allows text selection, selects the current paragraph and starts a drag. 5013 * 5014 * @return true if the drag was started. 5015 */ 5016 private boolean selectCurrentParagraphAndStartDrag() { 5017 if (mInsertionActionModeRunnable != null) { 5018 mTextView.removeCallbacks(mInsertionActionModeRunnable); 5019 } 5020 if (mTextActionMode != null) { 5021 mTextActionMode.finish(); 5022 } 5023 if (!selectCurrentParagraph()) { 5024 return false; 5025 } 5026 enterDrag(SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_PARAGRAPH); 5027 return true; 5028 } 5029 5030 private void updateCharacterBasedSelection(MotionEvent event) { 5031 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 5032 Selection.setSelection((Spannable) mTextView.getText(), mStartOffset, offset); 5033 } 5034 5035 private void updateWordBasedSelection(MotionEvent event) { 5036 if (mHaventMovedEnoughToStartDrag) { 5037 return; 5038 } 5039 final boolean isMouse = event.isFromSource(InputDevice.SOURCE_MOUSE); 5040 final ViewConfiguration viewConfig = ViewConfiguration.get( 5041 mTextView.getContext()); 5042 final float eventX = event.getX(); 5043 final float eventY = event.getY(); 5044 final int currLine; 5045 if (isMouse) { 5046 // No need to offset the y coordinate for mouse input. 5047 currLine = mTextView.getLineAtCoordinate(eventY); 5048 } else { 5049 float y = eventY; 5050 if (mSwitchedLines) { 5051 // Offset the finger by the same vertical offset as the handles. 5052 // This improves visibility of the content being selected by 5053 // shifting the finger below the content, this is applied once 5054 // the user has switched lines. 5055 final int touchSlop = viewConfig.getScaledTouchSlop(); 5056 final float fingerOffset = (mStartHandle != null) 5057 ? mStartHandle.getIdealVerticalOffset() 5058 : touchSlop; 5059 y = eventY - fingerOffset; 5060 } 5061 5062 currLine = getCurrentLineAdjustedForSlop(mTextView.getLayout(), mLineSelectionIsOn, 5063 y); 5064 if (!mSwitchedLines && currLine != mLineSelectionIsOn) { 5065 // Break early here, we want to offset the finger position from 5066 // the selection highlight, once the user moved their finger 5067 // to a different line we should apply the offset and *not* switch 5068 // lines until recomputing the position with the finger offset. 5069 mSwitchedLines = true; 5070 return; 5071 } 5072 } 5073 5074 int startOffset; 5075 int offset = mTextView.getOffsetAtCoordinate(currLine, eventX); 5076 // Snap to word boundaries. 5077 if (mStartOffset < offset) { 5078 // Expanding with end handle. 5079 offset = getWordEnd(offset); 5080 startOffset = getWordStart(mStartOffset); 5081 } else { 5082 // Expanding with start handle. 5083 offset = getWordStart(offset); 5084 startOffset = getWordEnd(mStartOffset); 5085 } 5086 mLineSelectionIsOn = currLine; 5087 Selection.setSelection((Spannable) mTextView.getText(), 5088 startOffset, offset); 5089 } 5090 5091 private void updateParagraphBasedSelection(MotionEvent event) { 5092 final int offset = mTextView.getOffsetForPosition(event.getX(), event.getY()); 5093 5094 final int start = Math.min(offset, mStartOffset); 5095 final int end = Math.max(offset, mStartOffset); 5096 final long paragraphsRange = getParagraphsRange(start, end); 5097 final int selectionStart = TextUtils.unpackRangeStartFromLong(paragraphsRange); 5098 final int selectionEnd = TextUtils.unpackRangeEndFromLong(paragraphsRange); 5099 Selection.setSelection((Spannable) mTextView.getText(), selectionStart, selectionEnd); 5100 } 5101 5102 /** 5103 * @param event 5104 */ 5105 private void updateMinAndMaxOffsets(MotionEvent event) { 5106 int pointerCount = event.getPointerCount(); 5107 for (int index = 0; index < pointerCount; index++) { 5108 int offset = mTextView.getOffsetForPosition(event.getX(index), event.getY(index)); 5109 if (offset < mMinTouchOffset) mMinTouchOffset = offset; 5110 if (offset > mMaxTouchOffset) mMaxTouchOffset = offset; 5111 } 5112 } 5113 5114 public int getMinTouchOffset() { 5115 return mMinTouchOffset; 5116 } 5117 5118 public int getMaxTouchOffset() { 5119 return mMaxTouchOffset; 5120 } 5121 5122 public void resetTouchOffsets() { 5123 mMinTouchOffset = mMaxTouchOffset = -1; 5124 resetDragAcceleratorState(); 5125 } 5126 5127 private void resetDragAcceleratorState() { 5128 mStartOffset = -1; 5129 mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE; 5130 mSwitchedLines = false; 5131 } 5132 5133 /** 5134 * @return true iff this controller is currently used to move the selection start. 5135 */ 5136 public boolean isSelectionStartDragged() { 5137 return mStartHandle != null && mStartHandle.isDragging(); 5138 } 5139 5140 /** 5141 * @return true if the user is selecting text using the drag accelerator. 5142 */ 5143 public boolean isDragAcceleratorActive() { 5144 return mDragAcceleratorMode != DRAG_ACCELERATOR_MODE_INACTIVE; 5145 } 5146 5147 public void onTouchModeChanged(boolean isInTouchMode) { 5148 if (!isInTouchMode) { 5149 hide(); 5150 } 5151 } 5152 5153 @Override 5154 public void onDetached() { 5155 final ViewTreeObserver observer = mTextView.getViewTreeObserver(); 5156 observer.removeOnTouchModeChangeListener(this); 5157 5158 if (mStartHandle != null) mStartHandle.onDetached(); 5159 if (mEndHandle != null) mEndHandle.onDetached(); 5160 } 5161 } 5162 5163 private class CorrectionHighlighter { 5164 private final Path mPath = new Path(); 5165 private final Paint mPaint = new Paint(Paint.ANTI_ALIAS_FLAG); 5166 private int mStart, mEnd; 5167 private long mFadingStartTime; 5168 private RectF mTempRectF; 5169 private final static int FADE_OUT_DURATION = 400; 5170 5171 public CorrectionHighlighter() { 5172 mPaint.setCompatibilityScaling(mTextView.getResources().getCompatibilityInfo(). 5173 applicationScale); 5174 mPaint.setStyle(Paint.Style.FILL); 5175 } 5176 5177 public void highlight(CorrectionInfo info) { 5178 mStart = info.getOffset(); 5179 mEnd = mStart + info.getNewText().length(); 5180 mFadingStartTime = SystemClock.uptimeMillis(); 5181 5182 if (mStart < 0 || mEnd < 0) { 5183 stopAnimation(); 5184 } 5185 } 5186 5187 public void draw(Canvas canvas, int cursorOffsetVertical) { 5188 if (updatePath() && updatePaint()) { 5189 if (cursorOffsetVertical != 0) { 5190 canvas.translate(0, cursorOffsetVertical); 5191 } 5192 5193 canvas.drawPath(mPath, mPaint); 5194 5195 if (cursorOffsetVertical != 0) { 5196 canvas.translate(0, -cursorOffsetVertical); 5197 } 5198 invalidate(true); // TODO invalidate cursor region only 5199 } else { 5200 stopAnimation(); 5201 invalidate(false); // TODO invalidate cursor region only 5202 } 5203 } 5204 5205 private boolean updatePaint() { 5206 final long duration = SystemClock.uptimeMillis() - mFadingStartTime; 5207 if (duration > FADE_OUT_DURATION) return false; 5208 5209 final float coef = 1.0f - (float) duration / FADE_OUT_DURATION; 5210 final int highlightColorAlpha = Color.alpha(mTextView.mHighlightColor); 5211 final int color = (mTextView.mHighlightColor & 0x00FFFFFF) + 5212 ((int) (highlightColorAlpha * coef) << 24); 5213 mPaint.setColor(color); 5214 return true; 5215 } 5216 5217 private boolean updatePath() { 5218 final Layout layout = mTextView.getLayout(); 5219 if (layout == null) return false; 5220 5221 // Update in case text is edited while the animation is run 5222 final int length = mTextView.getText().length(); 5223 int start = Math.min(length, mStart); 5224 int end = Math.min(length, mEnd); 5225 5226 mPath.reset(); 5227 layout.getSelectionPath(start, end, mPath); 5228 return true; 5229 } 5230 5231 private void invalidate(boolean delayed) { 5232 if (mTextView.getLayout() == null) return; 5233 5234 if (mTempRectF == null) mTempRectF = new RectF(); 5235 mPath.computeBounds(mTempRectF, false); 5236 5237 int left = mTextView.getCompoundPaddingLeft(); 5238 int top = mTextView.getExtendedPaddingTop() + mTextView.getVerticalOffset(true); 5239 5240 if (delayed) { 5241 mTextView.postInvalidateOnAnimation( 5242 left + (int) mTempRectF.left, top + (int) mTempRectF.top, 5243 left + (int) mTempRectF.right, top + (int) mTempRectF.bottom); 5244 } else { 5245 mTextView.postInvalidate((int) mTempRectF.left, (int) mTempRectF.top, 5246 (int) mTempRectF.right, (int) mTempRectF.bottom); 5247 } 5248 } 5249 5250 private void stopAnimation() { 5251 Editor.this.mCorrectionHighlighter = null; 5252 } 5253 } 5254 5255 private static class ErrorPopup extends PopupWindow { 5256 private boolean mAbove = false; 5257 private final TextView mView; 5258 private int mPopupInlineErrorBackgroundId = 0; 5259 private int mPopupInlineErrorAboveBackgroundId = 0; 5260 5261 ErrorPopup(TextView v, int width, int height) { 5262 super(v, width, height); 5263 mView = v; 5264 // Make sure the TextView has a background set as it will be used the first time it is 5265 // shown and positioned. Initialized with below background, which should have 5266 // dimensions identical to the above version for this to work (and is more likely). 5267 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 5268 com.android.internal.R.styleable.Theme_errorMessageBackground); 5269 mView.setBackgroundResource(mPopupInlineErrorBackgroundId); 5270 } 5271 5272 void fixDirection(boolean above) { 5273 mAbove = above; 5274 5275 if (above) { 5276 mPopupInlineErrorAboveBackgroundId = 5277 getResourceId(mPopupInlineErrorAboveBackgroundId, 5278 com.android.internal.R.styleable.Theme_errorMessageAboveBackground); 5279 } else { 5280 mPopupInlineErrorBackgroundId = getResourceId(mPopupInlineErrorBackgroundId, 5281 com.android.internal.R.styleable.Theme_errorMessageBackground); 5282 } 5283 5284 mView.setBackgroundResource(above ? mPopupInlineErrorAboveBackgroundId : 5285 mPopupInlineErrorBackgroundId); 5286 } 5287 5288 private int getResourceId(int currentId, int index) { 5289 if (currentId == 0) { 5290 TypedArray styledAttributes = mView.getContext().obtainStyledAttributes( 5291 R.styleable.Theme); 5292 currentId = styledAttributes.getResourceId(index, 0); 5293 styledAttributes.recycle(); 5294 } 5295 return currentId; 5296 } 5297 5298 @Override 5299 public void update(int x, int y, int w, int h, boolean force) { 5300 super.update(x, y, w, h, force); 5301 5302 boolean above = isAboveAnchor(); 5303 if (above != mAbove) { 5304 fixDirection(above); 5305 } 5306 } 5307 } 5308 5309 static class InputContentType { 5310 int imeOptions = EditorInfo.IME_NULL; 5311 String privateImeOptions; 5312 CharSequence imeActionLabel; 5313 int imeActionId; 5314 Bundle extras; 5315 OnEditorActionListener onEditorActionListener; 5316 boolean enterDown; 5317 LocaleList imeHintLocales; 5318 } 5319 5320 static class InputMethodState { 5321 ExtractedTextRequest mExtractedTextRequest; 5322 final ExtractedText mExtractedText = new ExtractedText(); 5323 int mBatchEditNesting; 5324 boolean mCursorChanged; 5325 boolean mSelectionModeChanged; 5326 boolean mContentChanged; 5327 int mChangedStart, mChangedEnd, mChangedDelta; 5328 } 5329 5330 /** 5331 * @return True iff (start, end) is a valid range within the text. 5332 */ 5333 private static boolean isValidRange(CharSequence text, int start, int end) { 5334 return 0 <= start && start <= end && end <= text.length(); 5335 } 5336 5337 @VisibleForTesting 5338 public SuggestionsPopupWindow getSuggestionsPopupWindowForTesting() { 5339 return mSuggestionsPopupWindow; 5340 } 5341 5342 /** 5343 * An InputFilter that monitors text input to maintain undo history. It does not modify the 5344 * text being typed (and hence always returns null from the filter() method). 5345 */ 5346 public static class UndoInputFilter implements InputFilter { 5347 private final Editor mEditor; 5348 5349 // Whether the current filter pass is directly caused by an end-user text edit. 5350 private boolean mIsUserEdit; 5351 5352 // Whether the text field is handling an IME composition. Must be parceled in case the user 5353 // rotates the screen during composition. 5354 private boolean mHasComposition; 5355 5356 public UndoInputFilter(Editor editor) { 5357 mEditor = editor; 5358 } 5359 5360 public void saveInstanceState(Parcel parcel) { 5361 parcel.writeInt(mIsUserEdit ? 1 : 0); 5362 parcel.writeInt(mHasComposition ? 1 : 0); 5363 } 5364 5365 public void restoreInstanceState(Parcel parcel) { 5366 mIsUserEdit = parcel.readInt() != 0; 5367 mHasComposition = parcel.readInt() != 0; 5368 } 5369 5370 /** 5371 * Signals that a user-triggered edit is starting. 5372 */ 5373 public void beginBatchEdit() { 5374 if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit"); 5375 mIsUserEdit = true; 5376 } 5377 5378 public void endBatchEdit() { 5379 if (DEBUG_UNDO) Log.d(TAG, "endBatchEdit"); 5380 mIsUserEdit = false; 5381 } 5382 5383 @Override 5384 public CharSequence filter(CharSequence source, int start, int end, 5385 Spanned dest, int dstart, int dend) { 5386 if (DEBUG_UNDO) { 5387 Log.d(TAG, "filter: source=" + source + " (" + start + "-" + end + ") " + 5388 "dest=" + dest + " (" + dstart + "-" + dend + ")"); 5389 } 5390 5391 // Check to see if this edit should be tracked for undo. 5392 if (!canUndoEdit(source, start, end, dest, dstart, dend)) { 5393 return null; 5394 } 5395 5396 // Check for and handle IME composition edits. 5397 if (handleCompositionEdit(source, start, end, dstart)) { 5398 return null; 5399 } 5400 5401 // Handle keyboard edits. 5402 handleKeyboardEdit(source, start, end, dest, dstart, dend); 5403 return null; 5404 } 5405 5406 /** 5407 * Returns true iff the edit was handled, either because it should be ignored or because 5408 * this function created an undo operation for it. 5409 */ 5410 private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) { 5411 // Ignore edits while the user is composing. 5412 if (isComposition(source)) { 5413 mHasComposition = true; 5414 return true; 5415 } 5416 final boolean hadComposition = mHasComposition; 5417 mHasComposition = false; 5418 5419 // Check for the transition out of the composing state. 5420 if (hadComposition) { 5421 // If there was no text the user canceled composition. Ignore the edit. 5422 if (start == end) { 5423 return true; 5424 } 5425 5426 // Otherwise the user inserted the composition. 5427 String newText = TextUtils.substring(source, start, end); 5428 EditOperation edit = new EditOperation(mEditor, "", dstart, newText); 5429 recordEdit(edit, false /* forceMerge */); 5430 return true; 5431 } 5432 5433 // This was neither a composition event nor a transition out of composing. 5434 return false; 5435 } 5436 5437 private void handleKeyboardEdit(CharSequence source, int start, int end, 5438 Spanned dest, int dstart, int dend) { 5439 // An application may install a TextWatcher to provide additional modifications after 5440 // the initial input filters run (e.g. a credit card formatter that adds spaces to a 5441 // string). This results in multiple filter() calls for what the user considers to be 5442 // a single operation. Always undo the whole set of changes in one step. 5443 final boolean forceMerge = isInTextWatcher(); 5444 5445 // Build a new operation with all the information from this edit. 5446 String newText = TextUtils.substring(source, start, end); 5447 String oldText = TextUtils.substring(dest, dstart, dend); 5448 EditOperation edit = new EditOperation(mEditor, oldText, dstart, newText); 5449 recordEdit(edit, forceMerge); 5450 } 5451 5452 /** 5453 * Fetches the last undo operation and checks to see if a new edit should be merged into it. 5454 * If forceMerge is true then the new edit is always merged. 5455 */ 5456 private void recordEdit(EditOperation edit, boolean forceMerge) { 5457 // Fetch the last edit operation and attempt to merge in the new edit. 5458 final UndoManager um = mEditor.mUndoManager; 5459 um.beginUpdate("Edit text"); 5460 EditOperation lastEdit = um.getLastOperation( 5461 EditOperation.class, mEditor.mUndoOwner, UndoManager.MERGE_MODE_UNIQUE); 5462 if (lastEdit == null) { 5463 // Add this as the first edit. 5464 if (DEBUG_UNDO) Log.d(TAG, "filter: adding first op " + edit); 5465 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 5466 } else if (forceMerge) { 5467 // Forced merges take priority because they could be the result of a non-user-edit 5468 // change and this case should not create a new undo operation. 5469 if (DEBUG_UNDO) Log.d(TAG, "filter: force merge " + edit); 5470 lastEdit.forceMergeWith(edit); 5471 } else if (!mIsUserEdit) { 5472 // An application directly modified the Editable outside of a text edit. Treat this 5473 // as a new change and don't attempt to merge. 5474 if (DEBUG_UNDO) Log.d(TAG, "non-user edit, new op " + edit); 5475 um.commitState(mEditor.mUndoOwner); 5476 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 5477 } else if (lastEdit.mergeWith(edit)) { 5478 // Merge succeeded, nothing else to do. 5479 if (DEBUG_UNDO) Log.d(TAG, "filter: merge succeeded, created " + lastEdit); 5480 } else { 5481 // Could not merge with the last edit, so commit the last edit and add this edit. 5482 if (DEBUG_UNDO) Log.d(TAG, "filter: merge failed, adding " + edit); 5483 um.commitState(mEditor.mUndoOwner); 5484 um.addOperation(edit, UndoManager.MERGE_MODE_NONE); 5485 } 5486 um.endUpdate(); 5487 } 5488 5489 private boolean canUndoEdit(CharSequence source, int start, int end, 5490 Spanned dest, int dstart, int dend) { 5491 if (!mEditor.mAllowUndo) { 5492 if (DEBUG_UNDO) Log.d(TAG, "filter: undo is disabled"); 5493 return false; 5494 } 5495 5496 if (mEditor.mUndoManager.isInUndo()) { 5497 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping, currently performing undo/redo"); 5498 return false; 5499 } 5500 5501 // Text filters run before input operations are applied. However, some input operations 5502 // are invalid and will throw exceptions when applied. This is common in tests. Don't 5503 // attempt to undo invalid operations. 5504 if (!isValidRange(source, start, end) || !isValidRange(dest, dstart, dend)) { 5505 if (DEBUG_UNDO) Log.d(TAG, "filter: invalid op"); 5506 return false; 5507 } 5508 5509 // Earlier filters can rewrite input to be a no-op, for example due to a length limit 5510 // on an input field. Skip no-op changes. 5511 if (start == end && dstart == dend) { 5512 if (DEBUG_UNDO) Log.d(TAG, "filter: skipping no-op"); 5513 return false; 5514 } 5515 5516 return true; 5517 } 5518 5519 private boolean isComposition(CharSequence source) { 5520 if (!(source instanceof Spannable)) { 5521 return false; 5522 } 5523 // This is a composition edit if the source has a non-zero-length composing span. 5524 Spannable text = (Spannable) source; 5525 int composeBegin = EditableInputConnection.getComposingSpanStart(text); 5526 int composeEnd = EditableInputConnection.getComposingSpanEnd(text); 5527 return composeBegin < composeEnd; 5528 } 5529 5530 private boolean isInTextWatcher() { 5531 CharSequence text = mEditor.mTextView.getText(); 5532 return (text instanceof SpannableStringBuilder) 5533 && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0; 5534 } 5535 } 5536 5537 /** 5538 * An operation to undo a single "edit" to a text view. 5539 */ 5540 public static class EditOperation extends UndoOperation<Editor> { 5541 private static final int TYPE_INSERT = 0; 5542 private static final int TYPE_DELETE = 1; 5543 private static final int TYPE_REPLACE = 2; 5544 5545 private int mType; 5546 private String mOldText; 5547 private int mOldTextStart; 5548 private String mNewText; 5549 private int mNewTextStart; 5550 5551 private int mOldCursorPos; 5552 private int mNewCursorPos; 5553 5554 /** 5555 * Constructs an edit operation from a text input operation on editor that replaces the 5556 * oldText starting at dstart with newText. 5557 */ 5558 public EditOperation(Editor editor, String oldText, int dstart, String newText) { 5559 super(editor.mUndoOwner); 5560 mOldText = oldText; 5561 mNewText = newText; 5562 5563 // Determine the type of the edit and store where it occurred. Avoid storing 5564 // irrevelant data (e.g. mNewTextStart for a delete) because that makes the 5565 // merging logic more complex (e.g. merging deletes could lead to mNewTextStart being 5566 // outside the bounds of the final text). 5567 if (mNewText.length() > 0 && mOldText.length() == 0) { 5568 mType = TYPE_INSERT; 5569 mNewTextStart = dstart; 5570 } else if (mNewText.length() == 0 && mOldText.length() > 0) { 5571 mType = TYPE_DELETE; 5572 mOldTextStart = dstart; 5573 } else { 5574 mType = TYPE_REPLACE; 5575 mOldTextStart = mNewTextStart = dstart; 5576 } 5577 5578 // Store cursor data. 5579 mOldCursorPos = editor.mTextView.getSelectionStart(); 5580 mNewCursorPos = dstart + mNewText.length(); 5581 } 5582 5583 public EditOperation(Parcel src, ClassLoader loader) { 5584 super(src, loader); 5585 mType = src.readInt(); 5586 mOldText = src.readString(); 5587 mOldTextStart = src.readInt(); 5588 mNewText = src.readString(); 5589 mNewTextStart = src.readInt(); 5590 mOldCursorPos = src.readInt(); 5591 mNewCursorPos = src.readInt(); 5592 } 5593 5594 @Override 5595 public void writeToParcel(Parcel dest, int flags) { 5596 dest.writeInt(mType); 5597 dest.writeString(mOldText); 5598 dest.writeInt(mOldTextStart); 5599 dest.writeString(mNewText); 5600 dest.writeInt(mNewTextStart); 5601 dest.writeInt(mOldCursorPos); 5602 dest.writeInt(mNewCursorPos); 5603 } 5604 5605 private int getNewTextEnd() { 5606 return mNewTextStart + mNewText.length(); 5607 } 5608 5609 private int getOldTextEnd() { 5610 return mOldTextStart + mOldText.length(); 5611 } 5612 5613 @Override 5614 public void commit() { 5615 } 5616 5617 @Override 5618 public void undo() { 5619 if (DEBUG_UNDO) Log.d(TAG, "undo"); 5620 // Remove the new text and insert the old. 5621 Editor editor = getOwnerData(); 5622 Editable text = (Editable) editor.mTextView.getText(); 5623 modifyText(text, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart, 5624 mOldCursorPos); 5625 } 5626 5627 @Override 5628 public void redo() { 5629 if (DEBUG_UNDO) Log.d(TAG, "redo"); 5630 // Remove the old text and insert the new. 5631 Editor editor = getOwnerData(); 5632 Editable text = (Editable) editor.mTextView.getText(); 5633 modifyText(text, mOldTextStart, getOldTextEnd(), mNewText, mNewTextStart, 5634 mNewCursorPos); 5635 } 5636 5637 /** 5638 * Attempts to merge this existing operation with a new edit. 5639 * @param edit The new edit operation. 5640 * @return If the merge succeeded, returns true. Otherwise returns false and leaves this 5641 * object unchanged. 5642 */ 5643 private boolean mergeWith(EditOperation edit) { 5644 if (DEBUG_UNDO) { 5645 Log.d(TAG, "mergeWith old " + this); 5646 Log.d(TAG, "mergeWith new " + edit); 5647 } 5648 switch (mType) { 5649 case TYPE_INSERT: 5650 return mergeInsertWith(edit); 5651 case TYPE_DELETE: 5652 return mergeDeleteWith(edit); 5653 case TYPE_REPLACE: 5654 return mergeReplaceWith(edit); 5655 default: 5656 return false; 5657 } 5658 } 5659 5660 private boolean mergeInsertWith(EditOperation edit) { 5661 // Only merge continuous insertions. 5662 if (edit.mType != TYPE_INSERT) { 5663 return false; 5664 } 5665 // Only merge insertions that are contiguous. 5666 if (getNewTextEnd() != edit.mNewTextStart) { 5667 return false; 5668 } 5669 mNewText += edit.mNewText; 5670 mNewCursorPos = edit.mNewCursorPos; 5671 return true; 5672 } 5673 5674 // TODO: Support forward delete. 5675 private boolean mergeDeleteWith(EditOperation edit) { 5676 // Only merge continuous deletes. 5677 if (edit.mType != TYPE_DELETE) { 5678 return false; 5679 } 5680 // Only merge deletions that are contiguous. 5681 if (mOldTextStart != edit.getOldTextEnd()) { 5682 return false; 5683 } 5684 mOldTextStart = edit.mOldTextStart; 5685 mOldText = edit.mOldText + mOldText; 5686 mNewCursorPos = edit.mNewCursorPos; 5687 return true; 5688 } 5689 5690 private boolean mergeReplaceWith(EditOperation edit) { 5691 // Replacements can merge only with adjacent inserts. 5692 if (edit.mType != TYPE_INSERT || getNewTextEnd() != edit.mNewTextStart) { 5693 return false; 5694 } 5695 mOldText += edit.mOldText; 5696 mNewText += edit.mNewText; 5697 mNewCursorPos = edit.mNewCursorPos; 5698 return true; 5699 } 5700 5701 /** 5702 * Forcibly creates a single merged edit operation by simulating the entire text 5703 * contents being replaced. 5704 */ 5705 public void forceMergeWith(EditOperation edit) { 5706 if (DEBUG_UNDO) Log.d(TAG, "forceMerge"); 5707 Editor editor = getOwnerData(); 5708 5709 // Copy the text of the current field. 5710 // NOTE: Using StringBuilder instead of SpannableStringBuilder would be somewhat faster, 5711 // but would require two parallel implementations of modifyText() because Editable and 5712 // StringBuilder do not share an interface for replace/delete/insert. 5713 Editable editable = (Editable) editor.mTextView.getText(); 5714 Editable originalText = new SpannableStringBuilder(editable.toString()); 5715 5716 // Roll back the last operation. 5717 modifyText(originalText, mNewTextStart, getNewTextEnd(), mOldText, mOldTextStart, 5718 mOldCursorPos); 5719 5720 // Clone the text again and apply the new operation. 5721 Editable finalText = new SpannableStringBuilder(editable.toString()); 5722 modifyText(finalText, edit.mOldTextStart, edit.getOldTextEnd(), edit.mNewText, 5723 edit.mNewTextStart, edit.mNewCursorPos); 5724 5725 // Convert this operation into a non-mergeable replacement of the entire string. 5726 mType = TYPE_REPLACE; 5727 mNewText = finalText.toString(); 5728 mNewTextStart = 0; 5729 mOldText = originalText.toString(); 5730 mOldTextStart = 0; 5731 mNewCursorPos = edit.mNewCursorPos; 5732 // mOldCursorPos is unchanged. 5733 } 5734 5735 private static void modifyText(Editable text, int deleteFrom, int deleteTo, 5736 CharSequence newText, int newTextInsertAt, int newCursorPos) { 5737 // Apply the edit if it is still valid. 5738 if (isValidRange(text, deleteFrom, deleteTo) && 5739 newTextInsertAt <= text.length() - (deleteTo - deleteFrom)) { 5740 if (deleteFrom != deleteTo) { 5741 text.delete(deleteFrom, deleteTo); 5742 } 5743 if (newText.length() != 0) { 5744 text.insert(newTextInsertAt, newText); 5745 } 5746 } 5747 // Restore the cursor position. If there wasn't an old cursor (newCursorPos == -1) then 5748 // don't explicitly set it and rely on SpannableStringBuilder to position it. 5749 // TODO: Select all the text that was undone. 5750 if (0 <= newCursorPos && newCursorPos <= text.length()) { 5751 Selection.setSelection(text, newCursorPos); 5752 } 5753 } 5754 5755 private String getTypeString() { 5756 switch (mType) { 5757 case TYPE_INSERT: 5758 return "insert"; 5759 case TYPE_DELETE: 5760 return "delete"; 5761 case TYPE_REPLACE: 5762 return "replace"; 5763 default: 5764 return ""; 5765 } 5766 } 5767 5768 @Override 5769 public String toString() { 5770 return "[mType=" + getTypeString() + ", " + 5771 "mOldText=" + mOldText + ", " + 5772 "mOldTextStart=" + mOldTextStart + ", " + 5773 "mNewText=" + mNewText + ", " + 5774 "mNewTextStart=" + mNewTextStart + ", " + 5775 "mOldCursorPos=" + mOldCursorPos + ", " + 5776 "mNewCursorPos=" + mNewCursorPos + "]"; 5777 } 5778 5779 public static final Parcelable.ClassLoaderCreator<EditOperation> CREATOR 5780 = new Parcelable.ClassLoaderCreator<EditOperation>() { 5781 @Override 5782 public EditOperation createFromParcel(Parcel in) { 5783 return new EditOperation(in, null); 5784 } 5785 5786 @Override 5787 public EditOperation createFromParcel(Parcel in, ClassLoader loader) { 5788 return new EditOperation(in, loader); 5789 } 5790 5791 @Override 5792 public EditOperation[] newArray(int size) { 5793 return new EditOperation[size]; 5794 } 5795 }; 5796 } 5797 5798 /** 5799 * A helper for enabling and handling "PROCESS_TEXT" menu actions. 5800 * These allow external applications to plug into currently selected text. 5801 */ 5802 static final class ProcessTextIntentActionsHandler { 5803 5804 private final Editor mEditor; 5805 private final TextView mTextView; 5806 private final PackageManager mPackageManager; 5807 private final SparseArray<Intent> mAccessibilityIntents = new SparseArray<Intent>(); 5808 private final SparseArray<AccessibilityNodeInfo.AccessibilityAction> mAccessibilityActions 5809 = new SparseArray<AccessibilityNodeInfo.AccessibilityAction>(); 5810 5811 private ProcessTextIntentActionsHandler(Editor editor) { 5812 mEditor = Preconditions.checkNotNull(editor); 5813 mTextView = Preconditions.checkNotNull(mEditor.mTextView); 5814 mPackageManager = Preconditions.checkNotNull( 5815 mTextView.getContext().getPackageManager()); 5816 } 5817 5818 /** 5819 * Adds "PROCESS_TEXT" menu items to the specified menu. 5820 */ 5821 public void onInitializeMenu(Menu menu) { 5822 int i = 0; 5823 for (ResolveInfo resolveInfo : getSupportedActivities()) { 5824 menu.add(Menu.NONE, Menu.NONE, 5825 Editor.MENU_ITEM_ORDER_PROCESS_TEXT_INTENT_ACTIONS_START + i++, 5826 getLabel(resolveInfo)) 5827 .setIntent(createProcessTextIntentForResolveInfo(resolveInfo)) 5828 .setShowAsAction(MenuItem.SHOW_AS_ACTION_IF_ROOM); 5829 } 5830 } 5831 5832 /** 5833 * Performs a "PROCESS_TEXT" action if there is one associated with the specified 5834 * menu item. 5835 * 5836 * @return True if the action was performed, false otherwise. 5837 */ 5838 public boolean performMenuItemAction(MenuItem item) { 5839 return fireIntent(item.getIntent()); 5840 } 5841 5842 /** 5843 * Initializes and caches "PROCESS_TEXT" accessibility actions. 5844 */ 5845 public void initializeAccessibilityActions() { 5846 mAccessibilityIntents.clear(); 5847 mAccessibilityActions.clear(); 5848 int i = 0; 5849 for (ResolveInfo resolveInfo : getSupportedActivities()) { 5850 int actionId = TextView.ACCESSIBILITY_ACTION_PROCESS_TEXT_START_ID + i++; 5851 mAccessibilityActions.put( 5852 actionId, 5853 new AccessibilityNodeInfo.AccessibilityAction( 5854 actionId, getLabel(resolveInfo))); 5855 mAccessibilityIntents.put( 5856 actionId, createProcessTextIntentForResolveInfo(resolveInfo)); 5857 } 5858 } 5859 5860 /** 5861 * Adds "PROCESS_TEXT" accessibility actions to the specified accessibility node info. 5862 * NOTE: This needs a prior call to {@link #initializeAccessibilityActions()} to make the 5863 * latest accessibility actions available for this call. 5864 */ 5865 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo nodeInfo) { 5866 for (int i = 0; i < mAccessibilityActions.size(); i++) { 5867 nodeInfo.addAction(mAccessibilityActions.valueAt(i)); 5868 } 5869 } 5870 5871 /** 5872 * Performs a "PROCESS_TEXT" action if there is one associated with the specified 5873 * accessibility action id. 5874 * 5875 * @return True if the action was performed, false otherwise. 5876 */ 5877 public boolean performAccessibilityAction(int actionId) { 5878 return fireIntent(mAccessibilityIntents.get(actionId)); 5879 } 5880 5881 private boolean fireIntent(Intent intent) { 5882 if (intent != null && Intent.ACTION_PROCESS_TEXT.equals(intent.getAction())) { 5883 intent.putExtra(Intent.EXTRA_PROCESS_TEXT, mTextView.getSelectedText()); 5884 mEditor.mPreserveDetachedSelection = true; 5885 mTextView.startActivityForResult(intent, TextView.PROCESS_TEXT_REQUEST_CODE); 5886 return true; 5887 } 5888 return false; 5889 } 5890 5891 private List<ResolveInfo> getSupportedActivities() { 5892 PackageManager packageManager = mTextView.getContext().getPackageManager(); 5893 return packageManager.queryIntentActivities(createProcessTextIntent(), 0); 5894 } 5895 5896 private Intent createProcessTextIntentForResolveInfo(ResolveInfo info) { 5897 return createProcessTextIntent() 5898 .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, !mTextView.isTextEditable()) 5899 .setClassName(info.activityInfo.packageName, info.activityInfo.name); 5900 } 5901 5902 private Intent createProcessTextIntent() { 5903 return new Intent() 5904 .setAction(Intent.ACTION_PROCESS_TEXT) 5905 .setType("text/plain"); 5906 } 5907 5908 private CharSequence getLabel(ResolveInfo resolveInfo) { 5909 return resolveInfo.loadLabel(mPackageManager); 5910 } 5911 } 5912} 5913