1 // Copyright 2015 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 package org.chromium.chrome.browser.omnibox; 6 7 import android.content.Context; 8 import android.graphics.Canvas; 9 import android.graphics.Paint; 10 import android.graphics.Rect; 11 import android.os.Build; 12 import android.provider.Settings; 13 import android.text.Editable; 14 import android.text.InputType; 15 import android.text.Layout; 16 import android.text.Selection; 17 import android.text.SpannableStringBuilder; 18 import android.text.TextUtils; 19 import android.text.TextWatcher; 20 import android.text.style.ReplacementSpan; 21 import android.util.AttributeSet; 22 import android.view.GestureDetector; 23 import android.view.KeyEvent; 24 import android.view.MotionEvent; 25 import android.view.View; 26 import android.view.inputmethod.EditorInfo; 27 import android.view.inputmethod.InputConnection; 28 import android.widget.TextView; 29 30 import androidx.annotation.IntDef; 31 import androidx.annotation.NonNull; 32 import androidx.annotation.Nullable; 33 import androidx.core.text.BidiFormatter; 34 import androidx.core.util.ObjectsCompat; 35 import androidx.core.view.inputmethod.EditorInfoCompat; 36 37 import org.chromium.base.ApiCompatibilityUtils; 38 import org.chromium.base.compat.ApiHelperForO; 39 import org.chromium.base.Log; 40 import org.chromium.base.SysUtils; 41 import org.chromium.base.ThreadUtils; 42 import org.chromium.base.metrics.RecordUserAction; 43 import org.chromium.chrome.browser.WindowDelegate; 44 import org.chromium.ui.KeyboardVisibilityDelegate; 45 46 import java.lang.annotation.Retention; 47 import java.lang.annotation.RetentionPolicy; 48 49 /** 50 * The URL text entry view for the Omnibox. 51 */ 52 public abstract class UrlBar extends AutocompleteEditText { 53 private static final String TAG = "UrlBar"; 54 55 private static final boolean DEBUG = false; 56 57 // TextView becomes very slow on long strings, so we limit maximum length 58 // of what is displayed to the user, see limitDisplayableLength(). 59 private static final int MAX_DISPLAYABLE_LENGTH = 4000; 60 private static final int MAX_DISPLAYABLE_LENGTH_LOW_END = 1000; 61 62 private boolean mFirstDrawComplete; 63 64 /** 65 * The text direction of the URL or query: LAYOUT_DIRECTION_LOCALE, LAYOUT_DIRECTION_LTR, or 66 * LAYOUT_DIRECTION_RTL. 67 * */ 68 private int mUrlDirection; 69 70 private UrlBarDelegate mUrlBarDelegate; 71 private UrlTextChangeListener mUrlTextChangeListener; 72 private TextWatcher mTextChangedListener; 73 private UrlBarTextContextMenuDelegate mTextContextMenuDelegate; 74 private UrlDirectionListener mUrlDirectionListener; 75 76 /** 77 * The gesture detector is used to detect long presses. Long presses require special treatment 78 * because the URL bar has custom touch event handling. See: {@link #onTouchEvent}. 79 */ 80 private final GestureDetector mGestureDetector; 81 82 private final KeyboardHideHelper mKeyboardHideHelper; 83 84 private boolean mFocused; 85 private boolean mSuppressingTouchMoveEventsForThisTouch; 86 private MotionEvent mSuppressedTouchDownEvent; 87 private boolean mAllowFocus = true; 88 89 private boolean mPendingScroll; 90 private int mPreviousWidth; 91 92 @ScrollType 93 private int mPreviousScrollType; 94 private String mPreviousScrollText; 95 private int mPreviousScrollViewWidth; 96 private int mPreviousScrollResultXPosition; 97 private float mPreviousScrollFontSize; 98 private boolean mPreviousScrollWasRtl; 99 100 // Used as a hint to indicate the text may contain an ellipsize span. This will be true if an 101 // ellispize span was applied the last time the text changed. A true value here does not 102 // guarantee that the text does contain the span currently as newly set text may have cleared 103 // this (and it the value will only be recalculated after the text has been changed). 104 private boolean mDidEllipsizeTextHint; 105 106 /** A cached point for getting this view's location in the window. */ 107 private final int[] mCachedLocation = new int[2]; 108 109 /** The location of this view on the last ACTION_DOWN event. */ 110 private float mDownEventViewTop; 111 112 /** 113 * The character index in the displayed text where the origin ends. This is required to 114 * ensure that the end of the origin is not scrolled out of view for long hostnames. 115 */ 116 private int mOriginEndIndex; 117 118 @ScrollType 119 private int mScrollType; 120 121 /** What scrolling action should be taken after the URL bar text changes. **/ 122 @IntDef({ScrollType.NO_SCROLL, ScrollType.SCROLL_TO_TLD, ScrollType.SCROLL_TO_BEGINNING}) 123 @Retention(RetentionPolicy.SOURCE) 124 public @interface ScrollType { 125 int NO_SCROLL = 0; 126 int SCROLL_TO_TLD = 1; 127 int SCROLL_TO_BEGINNING = 2; 128 } 129 130 /** 131 * An optional string to use with AccessibilityNodeInfo to report text content. 132 * This is particularly important for auto-fill applications, such as password managers, that 133 * rely on AccessibilityNodeInfo data to apply related form-fill data. 134 */ 135 private CharSequence mTextForAutofillServices; 136 protected boolean mRequestingAutofillStructure; 137 138 /** 139 * Implement this to get updates when the direction of the text in the URL bar changes. 140 * E.g. If the user is typing a URL, then erases it and starts typing a query in Arabic, 141 * the direction will change from left-to-right to right-to-left. 142 */ 143 interface UrlDirectionListener { 144 /** 145 * Called whenever the layout direction of the UrlBar changes. 146 * @param layoutDirection the new direction: android.view.View.LAYOUT_DIRECTION_LTR or 147 * android.view.View.LAYOUT_DIRECTION_RTL 148 */ onUrlDirectionChanged(int layoutDirection)149 public void onUrlDirectionChanged(int layoutDirection); 150 } 151 152 /** 153 * Delegate used to communicate with the content side and the parent layout. 154 */ 155 public interface UrlBarDelegate { 156 /** 157 * @return The view to be focused on a backward focus traversal. 158 */ 159 @Nullable getViewForUrlBackFocus()160 View getViewForUrlBackFocus(); 161 162 /** 163 * @return Whether the keyboard should be allowed to learn from the user input. 164 */ allowKeyboardLearning()165 boolean allowKeyboardLearning(); 166 167 /** 168 * Called to notify that back key has been pressed while the URL bar has focus. 169 */ backKeyPressed()170 void backKeyPressed(); 171 172 /** 173 * Called to notify that a tap or long press gesture has been detected. 174 * @param isLongPress Whether or not is a long press gesture. 175 */ gestureDetected(boolean isLongPress)176 void gestureDetected(boolean isLongPress); 177 } 178 179 /** Provides updates about the URL text changes. */ 180 public interface UrlTextChangeListener { 181 /** 182 * Called when the text state has changed. 183 * @param textWithoutAutocomplete The url bar text without autocompletion. 184 * @param textWithAutocomplete The url bar text with autocompletion. 185 */ 186 // TODO(crbug.com/1003080): Consider splitting these into two different callbacks. onTextChanged(String textWithoutAutocomplete, String textWithAutocomplete)187 void onTextChanged(String textWithoutAutocomplete, String textWithAutocomplete); 188 } 189 190 /** Delegate that provides the additional functionality to the textual context menus. */ 191 interface UrlBarTextContextMenuDelegate { 192 /** @return The text to be pasted into the UrlBar. */ 193 @NonNull getTextToPaste()194 String getTextToPaste(); 195 196 /** 197 * Gets potential replacement text to be used instead of the current selected text for 198 * cut/copy actions. If null is returned, the existing text will be cut or copied. 199 * 200 * @param currentText The current displayed text. 201 * @param selectionStart The selection start in the display text. 202 * @param selectionEnd The selection end in the display text. 203 * @return The text to be cut/copied instead of the currently selected text. 204 */ 205 @Nullable getReplacementCutCopyText(String currentText, int selectionStart, int selectionEnd)206 String getReplacementCutCopyText(String currentText, int selectionStart, int selectionEnd); 207 } 208 UrlBar(Context context, AttributeSet attrs)209 public UrlBar(Context context, AttributeSet attrs) { 210 super(context, attrs); 211 mUrlDirection = LAYOUT_DIRECTION_LOCALE; 212 213 // The URL Bar is derived from an text edit class, and as such is focusable by 214 // default. This means that if it is created before the first draw of the UI it 215 // will (as the only focusable element of the UI) get focus on the first draw. 216 // We react to this by greying out the tab area and bringing up the keyboard, 217 // which we don't want to do at startup. Prevent this by disabling focus until 218 // the first draw. 219 setFocusable(false); 220 setFocusableInTouchMode(false); 221 222 // The HTC Sense IME will attempt to autocomplete words in the Omnibox when Prediction is 223 // enabled. We want to disable this feature and rely on the Omnibox's implementation. 224 // Their IME does not respect ~TYPE_TEXT_FLAG_AUTO_COMPLETE nor any of the other InputType 225 // options I tried, but setting the filter variation prevents it. Sadly, it also removes 226 // the .com button, but the prediction was buggy as it would autocomplete words even when 227 // typing at the beginning of the omnibox text when other content was present (messing up 228 // what was previously there). See bug: http://b/issue?id=6200071 229 String defaultIme = Settings.Secure.getString( 230 getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); 231 if (defaultIme != null && defaultIme.contains("com.htc.android.htcime")) { 232 setInputType(getInputType() | InputType.TYPE_TEXT_VARIATION_FILTER); 233 } 234 235 mGestureDetector = 236 new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() { 237 @Override 238 public void onLongPress(MotionEvent e) { 239 mUrlBarDelegate.gestureDetected(true); 240 performLongClick(); 241 } 242 243 @Override 244 public boolean onSingleTapUp(MotionEvent e) { 245 requestFocus(); 246 mUrlBarDelegate.gestureDetected(false); 247 return true; 248 } 249 }, ThreadUtils.getUiThreadHandler()); 250 mGestureDetector.setOnDoubleTapListener(null); 251 mKeyboardHideHelper = new KeyboardHideHelper(this, new Runnable() { 252 @Override 253 public void run() { 254 if (mUrlBarDelegate != null) mUrlBarDelegate.backKeyPressed(); 255 } 256 }); 257 258 ApiCompatibilityUtils.disableSmartSelectionTextClassifier(this); 259 } 260 261 /** 262 * Initialize the delegate that allows interaction with the Window. 263 */ setWindowDelegate(WindowDelegate windowDelegate)264 public void setWindowDelegate(WindowDelegate windowDelegate) { 265 mKeyboardHideHelper.setWindowDelegate(windowDelegate); 266 } 267 268 /** 269 * Set the delegate to be used for text context menu actions. 270 */ setTextContextMenuDelegate(UrlBarTextContextMenuDelegate delegate)271 public void setTextContextMenuDelegate(UrlBarTextContextMenuDelegate delegate) { 272 mTextContextMenuDelegate = delegate; 273 } 274 275 @Override onKeyPreIme(int keyCode, KeyEvent event)276 public boolean onKeyPreIme(int keyCode, KeyEvent event) { 277 if (KeyEvent.KEYCODE_BACK == keyCode && event.getAction() == KeyEvent.ACTION_UP) { 278 mKeyboardHideHelper.monitorForKeyboardHidden(); 279 } 280 return super.onKeyPreIme(keyCode, event); 281 } 282 283 /** 284 * See {@link AutocompleteEditText#setIgnoreTextChangesForAutocomplete(boolean)}. 285 * <p> 286 * {@link #setDelegate(UrlBarDelegate)} must be called with a non-null instance prior to 287 * enabling autocomplete. 288 */ 289 @Override setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete)290 public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete) { 291 assert mUrlBarDelegate != null; 292 super.setIgnoreTextChangesForAutocomplete(ignoreAutocomplete); 293 } 294 295 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)296 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 297 mFocused = focused; 298 super.onFocusChanged(focused, direction, previouslyFocusedRect); 299 300 if (focused) { 301 mPendingScroll = false; 302 } 303 fixupTextDirection(); 304 305 if (!focused && Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { 306 // https://crbug.com/1103555: On Android Q+, the URL bar can trigger augmented 307 // Autofill, which disables ordinary Autofill requests for the duration of the 308 // Autofill session. This is worked around by canceling the session when the user 309 // focuses another view. 310 ApiHelperForO.cancelAutofillSession(); 311 } 312 } 313 314 /** 315 * Sets whether this {@link UrlBar} should be focusable. 316 */ setAllowFocus(boolean allowFocus)317 public void setAllowFocus(boolean allowFocus) { 318 mAllowFocus = allowFocus; 319 setFocusable(allowFocus); 320 setFocusableInTouchMode(allowFocus); 321 } 322 323 /** 324 * Sets the {@link UrlBar}'s text direction based on focus and contents. 325 * 326 * Should be called whenever focus or text contents change. 327 */ fixupTextDirection()328 private void fixupTextDirection() { 329 // When unfocused, force left-to-right rendering at the paragraph level (which is desired 330 // for URLs). Right-to-left runs are still rendered RTL, but will not flip the whole URL 331 // around. This is consistent with OmniboxViewViews on desktop. When focused, render text 332 // normally (to allow users to make non-URL searches and to avoid showing Android's split 333 // insertion point when an RTL user enters RTL text). Also render text normally when the 334 // text field is empty (because then it displays an instruction that is not a URL). 335 if (mFocused || length() == 0) { 336 setTextDirection(TEXT_DIRECTION_INHERIT); 337 } else { 338 setTextDirection(TEXT_DIRECTION_LTR); 339 } 340 // Always align to the same as the paragraph direction (LTR = left, RTL = right). 341 setTextAlignment(TEXT_ALIGNMENT_TEXT_START); 342 } 343 344 @Override onWindowFocusChanged(boolean hasWindowFocus)345 public void onWindowFocusChanged(boolean hasWindowFocus) { 346 super.onWindowFocusChanged(hasWindowFocus); 347 if (DEBUG) Log.i(TAG, "onWindowFocusChanged: " + hasWindowFocus); 348 if (hasWindowFocus) { 349 if (isFocused()) { 350 // Without the call to post(..), the keyboard was not getting shown when the 351 // window regained focus despite this being the final call in the view system 352 // flow. 353 post(new Runnable() { 354 @Override 355 public void run() { 356 KeyboardVisibilityDelegate.getInstance().showKeyboard(UrlBar.this); 357 } 358 }); 359 } 360 } 361 } 362 363 @Override focusSearch(int direction)364 public View focusSearch(int direction) { 365 if (direction == View.FOCUS_BACKWARD && mUrlBarDelegate.getViewForUrlBackFocus() != null) { 366 return mUrlBarDelegate.getViewForUrlBackFocus(); 367 } else { 368 return super.focusSearch(direction); 369 } 370 } 371 372 @Override onTouchEvent(MotionEvent event)373 public boolean onTouchEvent(MotionEvent event) { 374 // This method contains special logic to enable long presses to be handled correctly. 375 376 // One piece of the logic is to suppress all ACTION_DOWN events received while the UrlBar is 377 // not focused, and only pass them to super.onTouchEvent() if it turns out we're about to 378 // perform a long press. Long pressing will not behave properly without sending this event, 379 // but if we always send it immediately, it will cause the keyboard to show immediately, 380 // whereas we want to wait to show it until after the URL focus animation finishes, to avoid 381 // performance issues on slow devices. 382 383 // The other piece of the logic is to suppress ACTION_MOVE events received after an 384 // ACTION_DOWN received while the UrlBar is not focused. This is because the UrlBar moves to 385 // the side as it's focusing, and a finger held still on the screen would therefore be 386 // interpreted as a drag selection. 387 388 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 389 getLocationInWindow(mCachedLocation); 390 mDownEventViewTop = mCachedLocation[1]; 391 mSuppressingTouchMoveEventsForThisTouch = !mFocused; 392 } 393 394 if (!mFocused) { 395 if (event.getActionMasked() == MotionEvent.ACTION_DOWN) { 396 mSuppressedTouchDownEvent = MotionEvent.obtain(event); 397 } 398 mGestureDetector.onTouchEvent(event); 399 return true; 400 } 401 402 if (event.getActionMasked() == MotionEvent.ACTION_UP 403 || event.getActionMasked() == MotionEvent.ACTION_CANCEL) { 404 // Minor optimization to avoid unnecessarily holding onto a MotionEvent after the touch 405 // finishes. 406 mSuppressedTouchDownEvent = null; 407 } 408 409 if (mSuppressingTouchMoveEventsForThisTouch 410 && event.getActionMasked() == MotionEvent.ACTION_MOVE) { 411 return true; 412 } 413 414 try { 415 return super.onTouchEvent(event); 416 } catch (NullPointerException e) { 417 // Working around a platform bug (b/25562038) that was fixed in N that can throw an 418 // exception during text selection. We just swallow the exception. The outcome is that 419 // the text selection handle doesn't show. 420 421 // If this happens on N or later, there's a different issue here that we might want to 422 // know about. 423 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) throw e; 424 425 Log.w(TAG, "Ignoring NPE in UrlBar#onTouchEvent.", e); 426 return true; 427 } catch (IndexOutOfBoundsException e) { 428 // Work around crash of unknown origin (https://crbug.com/837419). 429 Log.w(TAG, "Ignoring IndexOutOfBoundsException in UrlBar#onTouchEvent.", e); 430 return true; 431 } 432 } 433 434 @Override performLongClick()435 public boolean performLongClick() { 436 if (!shouldPerformLongClick()) return false; 437 438 releaseSuppressedTouchDownEvent(); 439 return super.performLongClick(); 440 } 441 442 /** 443 * @return Whether or not a long click should be performed. 444 */ shouldPerformLongClick()445 private boolean shouldPerformLongClick() { 446 getLocationInWindow(mCachedLocation); 447 448 // If the view moved between the last down event, block the long-press. 449 return mDownEventViewTop == mCachedLocation[1]; 450 } 451 releaseSuppressedTouchDownEvent()452 private void releaseSuppressedTouchDownEvent() { 453 if (mSuppressedTouchDownEvent != null) { 454 super.onTouchEvent(mSuppressedTouchDownEvent); 455 mSuppressedTouchDownEvent = null; 456 } 457 } 458 459 @Override onDraw(Canvas canvas)460 public void onDraw(Canvas canvas) { 461 super.onDraw(canvas); 462 463 if (!mFirstDrawComplete) { 464 mFirstDrawComplete = true; 465 466 // We have now avoided the first draw problem (see the comment in 467 // the constructor) so we want to make the URL bar focusable so that 468 // touches etc. activate it. 469 setFocusable(mAllowFocus); 470 setFocusableInTouchMode(mAllowFocus); 471 } 472 473 // Notify listeners if the URL's direction has changed. 474 updateUrlDirection(); 475 } 476 477 /** 478 * If the direction of the URL has changed, update mUrlDirection and notify the 479 * UrlDirectionListeners. 480 */ updateUrlDirection()481 private void updateUrlDirection() { 482 Layout layout = getLayout(); 483 if (layout == null) return; 484 485 int urlDirection; 486 if (length() == 0) { 487 urlDirection = LAYOUT_DIRECTION_LOCALE; 488 } else if (layout.getParagraphDirection(0) == Layout.DIR_LEFT_TO_RIGHT) { 489 urlDirection = LAYOUT_DIRECTION_LTR; 490 } else { 491 urlDirection = LAYOUT_DIRECTION_RTL; 492 } 493 494 if (urlDirection != mUrlDirection) { 495 mUrlDirection = urlDirection; 496 if (mUrlDirectionListener != null) { 497 mUrlDirectionListener.onUrlDirectionChanged(urlDirection); 498 } 499 500 // Ensure the display text is visible after updating the URL direction. 501 scrollDisplayText(); 502 } 503 } 504 505 /** 506 * @return The text direction of the URL, e.g. LAYOUT_DIRECTION_LTR. 507 */ getUrlDirection()508 public int getUrlDirection() { 509 return mUrlDirection; 510 } 511 512 /** 513 * Sets the listener for changes in the url bar's layout direction. Also calls 514 * onUrlDirectionChanged() immediately on the listener. 515 * 516 * @param listener The UrlDirectionListener to receive callbacks when the url direction changes, 517 * or null to unregister any previously registered listener. 518 */ setUrlDirectionListener(UrlDirectionListener listener)519 public void setUrlDirectionListener(UrlDirectionListener listener) { 520 mUrlDirectionListener = listener; 521 if (mUrlDirectionListener != null) { 522 mUrlDirectionListener.onUrlDirectionChanged(mUrlDirection); 523 } 524 } 525 526 /** 527 * Set the url delegate to handle communication from the {@link UrlBar} to the rest of the UI. 528 * @param delegate The {@link UrlBarDelegate} to be used. 529 */ setDelegate(UrlBarDelegate delegate)530 public void setDelegate(UrlBarDelegate delegate) { 531 mUrlBarDelegate = delegate; 532 } 533 534 /** 535 * Set the listener to be notified when the URL text has changed. 536 * @param listener The listener to be notified. 537 */ setUrlTextChangeListener(UrlTextChangeListener listener)538 public void setUrlTextChangeListener(UrlTextChangeListener listener) { 539 mUrlTextChangeListener = listener; 540 } 541 542 /** 543 * Set the listener to be notified when the view's text has changed. 544 * @param textChangedListener The listener to be notified. 545 */ setTextChangedListener(TextWatcher textChangedListener)546 public void setTextChangedListener(TextWatcher textChangedListener) { 547 if (ObjectsCompat.equals(mTextChangedListener, textChangedListener)) { 548 return; 549 } else if (mTextChangedListener != null) { 550 removeTextChangedListener(mTextChangedListener); 551 } 552 553 mTextChangedListener = textChangedListener; 554 addTextChangedListener(mTextChangedListener); 555 } 556 557 /** 558 * Set the text to report to Autofill services upon call to onProvideAutofillStructure. 559 */ setTextForAutofillServices(CharSequence text)560 public void setTextForAutofillServices(CharSequence text) { 561 mTextForAutofillServices = text; 562 } 563 564 @Override onTextContextMenuItem(int id)565 public boolean onTextContextMenuItem(int id) { 566 if (mTextContextMenuDelegate == null) return super.onTextContextMenuItem(id); 567 568 if (id == android.R.id.paste) { 569 String pasteString = mTextContextMenuDelegate.getTextToPaste(); 570 if (pasteString != null) { 571 int min = 0; 572 int max = getText().length(); 573 574 if (isFocused()) { 575 final int selStart = getSelectionStart(); 576 final int selEnd = getSelectionEnd(); 577 578 min = Math.max(0, Math.min(selStart, selEnd)); 579 max = Math.max(0, Math.max(selStart, selEnd)); 580 } 581 582 Selection.setSelection(getText(), max); 583 getText().replace(min, max, pasteString); 584 onPaste(); 585 } 586 return true; 587 } 588 589 if ((id == android.R.id.cut || id == android.R.id.copy)) { 590 if (id == android.R.id.cut) { 591 RecordUserAction.record("Omnibox.LongPress.Cut"); 592 } else { 593 RecordUserAction.record("Omnibox.LongPress.Copy"); 594 } 595 String currentText = getText().toString(); 596 String replacementCutCopyText = mTextContextMenuDelegate.getReplacementCutCopyText( 597 currentText, getSelectionStart(), getSelectionEnd()); 598 if (replacementCutCopyText == null) return super.onTextContextMenuItem(id); 599 600 setIgnoreTextChangesForAutocomplete(true); 601 setText(replacementCutCopyText); 602 setSelection(0, replacementCutCopyText.length()); 603 setIgnoreTextChangesForAutocomplete(false); 604 605 boolean retVal = super.onTextContextMenuItem(id); 606 607 if (TextUtils.equals(getText(), replacementCutCopyText)) { 608 // Restore the old text if the operation did modify the text. 609 setIgnoreTextChangesForAutocomplete(true); 610 setText(currentText); 611 612 // Move the cursor to the end. 613 setSelection(getText().length()); 614 setIgnoreTextChangesForAutocomplete(false); 615 } 616 617 return retVal; 618 } 619 620 if (id == android.R.id.shareText) { 621 RecordUserAction.record("Omnibox.LongPress.Share"); 622 } 623 624 return super.onTextContextMenuItem(id); 625 } 626 627 /** 628 * Specified how text should be scrolled within the UrlBar. 629 * 630 * @param scrollType What type of scroll should be applied to the text. 631 * @param scrollToIndex The index that should be scrolled to, which only applies to 632 * {@link ScrollType#SCROLL_TO_TLD}. 633 */ setScrollState(@crollType int scrollType, int scrollToIndex)634 public void setScrollState(@ScrollType int scrollType, int scrollToIndex) { 635 if (scrollType == ScrollType.SCROLL_TO_TLD) { 636 mOriginEndIndex = scrollToIndex; 637 } else { 638 mOriginEndIndex = 0; 639 } 640 mScrollType = scrollType; 641 scrollDisplayText(); 642 } 643 644 /** 645 * Scrolls the omnibox text to a position determined by the current scroll type. 646 * 647 * @see #setScrollState(int, int) 648 */ scrollDisplayText()649 private void scrollDisplayText() { 650 if (isLayoutRequested()) { 651 mPendingScroll = mScrollType != ScrollType.NO_SCROLL; 652 return; 653 } 654 scrollDisplayTextInternal(mScrollType); 655 } 656 657 /** 658 * Scrolls the omnibox text to the position specified, based on the {@link ScrollType}. 659 * 660 * @param scrollType What type of scroll to perform. 661 * SCROLL_TO_TLD: Scrolls the omnibox text to bring the TLD into view. 662 * SCROLL_TO_BEGINNING: Scrolls text that's too long to fit in the omnibox 663 * to the beginning so we can see the first character. 664 */ scrollDisplayTextInternal(@crollType int scrollType)665 private void scrollDisplayTextInternal(@ScrollType int scrollType) { 666 mPendingScroll = false; 667 668 if (mFocused) return; 669 670 Editable text = getText(); 671 if (TextUtils.isEmpty(text)) scrollType = ScrollType.SCROLL_TO_BEGINNING; 672 673 // Ensure any selection from the focus state is cleared. 674 setSelection(0); 675 676 float currentTextSize = getTextSize(); 677 boolean currentIsRtl = getLayoutDirection() == LAYOUT_DIRECTION_RTL; 678 679 int measuredWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight()); 680 if (scrollType == mPreviousScrollType && TextUtils.equals(text, mPreviousScrollText) 681 && measuredWidth == mPreviousScrollViewWidth 682 // Font size is float but it changes in discrete range (eg small font, big font), 683 // therefore false negative using regular equality is unlikely. 684 && currentTextSize == mPreviousScrollFontSize 685 && currentIsRtl == mPreviousScrollWasRtl) { 686 scrollTo(mPreviousScrollResultXPosition, getScrollY()); 687 return; 688 } 689 690 switch (scrollType) { 691 case ScrollType.SCROLL_TO_TLD: 692 scrollToTLD(); 693 break; 694 case ScrollType.SCROLL_TO_BEGINNING: 695 scrollToBeginning(); 696 break; 697 default: 698 // Intentional return to avoid clearing scroll state when no scroll was applied. 699 return; 700 } 701 702 mPreviousScrollType = scrollType; 703 mPreviousScrollText = text.toString(); 704 mPreviousScrollViewWidth = measuredWidth; 705 mPreviousScrollFontSize = currentTextSize; 706 mPreviousScrollResultXPosition = getScrollX(); 707 mPreviousScrollWasRtl = currentIsRtl; 708 } 709 710 /** 711 * Scrolls the omnibox text to show the very beginning of the text entered. 712 */ scrollToBeginning()713 private void scrollToBeginning() { 714 Editable text = getText(); 715 float scrollPos = 0f; 716 if (TextUtils.isEmpty(text)) { 717 if (getLayoutDirection() == LAYOUT_DIRECTION_RTL 718 && BidiFormatter.getInstance().isRtl(getHint())) { 719 // Compared to below that uses getPrimaryHorizontal(1) due to 0 returning an 720 // invalid value, if the text is empty, getPrimaryHorizontal(0) returns the actual 721 // max scroll amount. 722 scrollPos = (int) getLayout().getPrimaryHorizontal(0) - getMeasuredWidth(); 723 } 724 } else if (BidiFormatter.getInstance().isRtl(text)) { 725 // RTL. 726 float endPointX = getLayout().getPrimaryHorizontal(text.length()); 727 int measuredWidth = getMeasuredWidth(); 728 float width = getLayout().getPaint().measureText(text.toString()); 729 scrollPos = Math.max(0, endPointX - measuredWidth + width); 730 } 731 scrollTo((int) scrollPos, getScrollY()); 732 } 733 734 /** 735 * Scrolls the omnibox text to bring the TLD into view. 736 */ scrollToTLD()737 private void scrollToTLD() { 738 Editable url = getText(); 739 int measuredWidth = getMeasuredWidth() - (getPaddingLeft() + getPaddingRight()); 740 741 Layout textLayout = getLayout(); 742 assert getLayout().getLineCount() == 1; 743 final int originEndIndex = Math.min(mOriginEndIndex, url.length()); 744 if (mOriginEndIndex > url.length()) { 745 // If discovered locally, please update crbug.com/859219 with the steps to reproduce. 746 assert false : "Attempting to scroll past the end of the URL: " + url + ", end index: " 747 + mOriginEndIndex; 748 } 749 float endPointX = textLayout.getPrimaryHorizontal(originEndIndex); 750 // Compare the position offset of the last character and the character prior to determine 751 // the LTR-ness of the final component of the URL. 752 float priorToEndPointX = url.length() == 1 753 ? 0 754 : textLayout.getPrimaryHorizontal(Math.max(0, originEndIndex - 1)); 755 756 float scrollPos; 757 if (priorToEndPointX < endPointX) { 758 // LTR 759 scrollPos = Math.max(0, endPointX - measuredWidth); 760 } else { 761 // RTL 762 763 // To handle BiDirectional text, search backward from the two existing offsets to find 764 // the first LTR character. Ensure the final RTL component of the domain is visible 765 // above any of the prior LTR pieces. 766 int rtlStartIndexForEndingRun = originEndIndex - 1; 767 for (int i = originEndIndex - 2; i >= 0; i--) { 768 float indexOffsetDrawPosition = textLayout.getPrimaryHorizontal(i); 769 if (indexOffsetDrawPosition > endPointX) { 770 rtlStartIndexForEndingRun = i; 771 } else { 772 // getPrimaryHorizontal determines the index position for the next character 773 // based on the previous characters. In bi-directional text, the first RTL 774 // character following LTR text will have an LTR-appearing horizontal offset 775 // as it is based on the preceding LTR text. Thus, the start of the RTL 776 // character run will be after and including the first LTR horizontal index. 777 rtlStartIndexForEndingRun = Math.max(0, rtlStartIndexForEndingRun - 1); 778 break; 779 } 780 } 781 float width = textLayout.getPaint().measureText( 782 url.subSequence(rtlStartIndexForEndingRun, originEndIndex).toString()); 783 if (width < measuredWidth) { 784 scrollPos = Math.max(0, endPointX + width - measuredWidth); 785 } else { 786 scrollPos = endPointX + measuredWidth; 787 } 788 } 789 scrollTo((int) scrollPos, getScrollY()); 790 } 791 792 @Override onLayout(boolean changed, int left, int top, int right, int bottom)793 protected void onLayout(boolean changed, int left, int top, int right, int bottom) { 794 super.onLayout(changed, left, top, right, bottom); 795 796 if (mPendingScroll) { 797 scrollDisplayTextInternal(mScrollType); 798 } else if (mPreviousWidth != (right - left)) { 799 scrollDisplayTextInternal(mScrollType); 800 mPreviousWidth = right - left; 801 } 802 } 803 804 @Override bringPointIntoView(int offset)805 public boolean bringPointIntoView(int offset) { 806 // TextView internally attempts to keep the selection visible, but in the unfocused state 807 // this class ensures that the TLD is visible. 808 if (!mFocused) return false; 809 assert !mPendingScroll; 810 811 return super.bringPointIntoView(offset); 812 } 813 814 @Override onCreateInputConnection(EditorInfo outAttrs)815 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 816 InputConnection connection = super.onCreateInputConnection(outAttrs); 817 if (mUrlBarDelegate == null || !mUrlBarDelegate.allowKeyboardLearning()) { 818 outAttrs.imeOptions |= EditorInfoCompat.IME_FLAG_NO_PERSONALIZED_LEARNING; 819 } 820 return connection; 821 } 822 823 @Override setText(CharSequence text, BufferType type)824 public void setText(CharSequence text, BufferType type) { 825 if (DEBUG) Log.i(TAG, "setText -- text: %s", text); 826 super.setText(text, type); 827 fixupTextDirection(); 828 } 829 limitDisplayableLength()830 private void limitDisplayableLength() { 831 // To limit displayable length we replace middle portion of the string with ellipsis. 832 // That affects only presentation of the text, and doesn't affect other aspects like 833 // copying to the clipboard, getting text with getText(), etc. 834 final int maxLength = 835 SysUtils.isLowEndDevice() ? MAX_DISPLAYABLE_LENGTH_LOW_END : MAX_DISPLAYABLE_LENGTH; 836 837 Editable text = getText(); 838 int textLength = text.length(); 839 if (textLength <= maxLength) { 840 if (mDidEllipsizeTextHint) { 841 EllipsisSpan[] spans = text.getSpans(0, textLength, EllipsisSpan.class); 842 if (spans != null && spans.length > 0) { 843 assert spans.length == 1 : "Should never apply more than a single EllipsisSpan"; 844 for (int i = 0; i < spans.length; i++) { 845 text.removeSpan(spans[i]); 846 } 847 } 848 } 849 mDidEllipsizeTextHint = false; 850 return; 851 } 852 853 mDidEllipsizeTextHint = true; 854 855 int spanLeft = text.nextSpanTransition(0, textLength, EllipsisSpan.class); 856 if (spanLeft != textLength) return; 857 858 spanLeft = maxLength / 2; 859 text.setSpan(EllipsisSpan.INSTANCE, spanLeft, textLength - spanLeft, 860 Editable.SPAN_INCLUSIVE_EXCLUSIVE); 861 } 862 863 @Override getText()864 public Editable getText() { 865 if (mRequestingAutofillStructure) { 866 // crbug.com/1109186: mTextForAutofillServices must not be null here, but Autofill 867 // requests can be triggered before it is initialized. 868 return new SpannableStringBuilder( 869 mTextForAutofillServices != null ? mTextForAutofillServices : ""); 870 } 871 return super.getText(); 872 } 873 874 @Override getAccessibilityClassName()875 public CharSequence getAccessibilityClassName() { 876 // When UrlBar is used as a read-only TextView, force Talkback to pronounce it like 877 // TextView. Otherwise Talkback will say "Edit box, http://...". crbug.com/636988 878 if (isEnabled()) { 879 return super.getAccessibilityClassName(); 880 } else { 881 return TextView.class.getName(); 882 } 883 } 884 885 @Override replaceAllTextFromAutocomplete(String text)886 public void replaceAllTextFromAutocomplete(String text) { 887 if (DEBUG) Log.i(TAG, "replaceAllTextFromAutocomplete: " + text); 888 setText(text); 889 } 890 891 @Override onAutocompleteTextStateChanged(boolean updateDisplay)892 public void onAutocompleteTextStateChanged(boolean updateDisplay) { 893 if (DEBUG) { 894 Log.i(TAG, "onAutocompleteTextStateChanged: DIS[%b]", updateDisplay); 895 } 896 if (mUrlTextChangeListener == null) return; 897 if (updateDisplay) limitDisplayableLength(); 898 // crbug.com/764749 899 Log.w(TAG, "Text change observed, triggering autocomplete."); 900 901 mUrlTextChangeListener.onTextChanged( 902 getTextWithoutAutocomplete(), getTextWithAutocomplete()); 903 } 904 905 /** 906 * Span that displays ellipsis instead of the text. Used to hide portion of 907 * very large string to get decent performance from TextView. 908 */ 909 private static class EllipsisSpan extends ReplacementSpan { 910 private static final String ELLIPSIS = "..."; 911 912 public static final EllipsisSpan INSTANCE = new EllipsisSpan(); 913 914 @Override getSize( Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm)915 public int getSize( 916 Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) { 917 return (int) paint.measureText(ELLIPSIS); 918 } 919 920 @Override draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, Paint paint)921 public void draw(Canvas canvas, CharSequence text, int start, int end, float x, int top, 922 int y, int bottom, Paint paint) { 923 canvas.drawText(ELLIPSIS, x, y, paint); 924 } 925 } 926 } 927