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