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