// Copyright 2015 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
package org.chromium.chrome.browser.omnibox;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.res.Resources;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Rect;
import android.os.StrictMode;
import android.os.SystemClock;
import android.text.Editable;
import android.text.Layout;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextUtils;
import android.text.style.ReplacementSpan;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent;
import android.view.View;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputConnectionWrapper;
import org.chromium.base.ApiCompatibilityUtils;
import org.chromium.base.SysUtils;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.R;
import org.chromium.chrome.browser.metrics.StartupMetrics;
import org.chromium.chrome.browser.omnibox.LocationBarLayout.OmniboxLivenessListener;
import org.chromium.chrome.browser.tab.Tab;
import org.chromium.chrome.browser.util.UrlUtilities;
import org.chromium.chrome.browser.widget.VerticallyFixedEditText;
import org.chromium.content.browser.ContentViewCore;
import org.chromium.ui.UiUtils;
import java.net.MalformedURLException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
/**
* The URL text entry view for the Omnibox.
*/
public class UrlBar extends VerticallyFixedEditText {
private static final String TAG = "UrlBar";
// TextView becomes very slow on long strings, so we limit maximum length
// of what is displayed to the user, see limitDisplayableLength().
private static final int MAX_DISPLAYABLE_LENGTH = 4000;
private static final int MAX_DISPLAYABLE_LENGTH_LOW_END = 1000;
/** The contents of the URL that precede the path/query after being formatted. */
private String mFormattedUrlLocation;
/** The contents of the URL that precede the path/query before formatting. */
private String mOriginalUrlLocation;
/** Overrides the text announced during accessibility events. */
private String mAccessibilityTextOverride;
private boolean mShowKeyboardOnWindowFocus;
private boolean mFirstDrawComplete;
/**
* The text direction of the URL or query: LAYOUT_DIRECTION_LOCALE, LAYOUT_DIRECTION_LTR, or
* LAYOUT_DIRECTION_RTL.
* */
private int mUrlDirection;
private UrlBarDelegate mUrlBarDelegate;
private UrlDirectionListener mUrlDirectionListener;
private final AutocompleteSpan mAutocompleteSpan;
/**
* The gesture detector is used to detect long presses. Long presses require special treatment
* because the URL bar has custom touch event handling. See: {@link #onTouchEvent}.
*/
private final GestureDetector mGestureDetector;
private boolean mFocused;
private boolean mAllowFocus = true;
private final int mDarkHintColor;
private final int mDarkDefaultTextColor;
private final int mDarkHighlightColor;
private final int mLightHintColor;
private final int mLightDefaultTextColor;
private final int mLightHighlightColor;
private Boolean mUseDarkColors;
private AccessibilityManager mAccessibilityManager;
private boolean mDisableTextAccessibilityEvents;
/**
* Whether default TextView scrolling should be disabled because autocomplete has been added.
* This allows the user entered text to be shown instead of the end of the autocomplete.
*/
private boolean mDisableTextScrollingFromAutocomplete;
private OmniboxLivenessListener mOmniboxLivenessListener;
private long mFirstFocusTimeMs;
private boolean mInBatchEditMode;
private int mBeforeBatchEditAutocompleteIndex = -1;
private String mBeforeBatchEditFullText;
private boolean mSelectionChangedInBatchMode;
private boolean mTextDeletedInBatchMode;
private boolean mIsPastedText;
// Used as a hint to indicate the text may contain an ellipsize span. This will be true if an
// ellispize span was applied the last time the text changed. A true value here does not
// guarantee that the text does contain the span currently as newly set text may have cleared
// this (and it the value will only be recalculated after the text has been changed).
private boolean mDidEllipsizeTextHint;
// Set to true when the URL bar text is modified programmatically. Initially set
// to true until the old state has been loaded.
private boolean mIgnoreAutocomplete = true;
private boolean mLastUrlEditWasDelete;
/**
* Implement this to get updates when the direction of the text in the URL bar changes.
* E.g. If the user is typing a URL, then erases it and starts typing a query in Arabic,
* the direction will change from left-to-right to right-to-left.
*/
interface UrlDirectionListener {
/**
* Called whenever the layout direction of the UrlBar changes.
* @param layoutDirection the new direction: android.view.View.LAYOUT_DIRECTION_LTR or
* android.view.View.LAYOUT_DIRECTION_RTL
*/
public void onUrlDirectionChanged(int layoutDirection);
}
/**
* Delegate used to communicate with the content side and the parent layout.
*/
public interface UrlBarDelegate {
/**
* @return The current active {@link Tab}.
*/
Tab getCurrentTab();
/**
* Called when the text state has changed and the autocomplete suggestions should be
* refreshed.
*
* @param textDeleted Whether this change was as a result of text being deleted.
*/
void onTextChangedForAutocomplete(boolean textDeleted);
/**
* @return Whether the light security theme should be used.
*/
boolean shouldEmphasizeHttpsScheme();
}
public UrlBar(Context context, AttributeSet attrs) {
super(context, attrs);
Resources resources = getResources();
mDarkDefaultTextColor =
ApiCompatibilityUtils.getColor(resources, R.color.url_emphasis_default_text);
mDarkHintColor = ApiCompatibilityUtils.getColor(resources,
R.color.locationbar_dark_hint_text);
mDarkHighlightColor = getHighlightColor();
mLightDefaultTextColor =
ApiCompatibilityUtils.getColor(resources, R.color.url_emphasis_light_default_text);
mLightHintColor =
ApiCompatibilityUtils.getColor(resources, R.color.locationbar_light_hint_text);
mLightHighlightColor = ApiCompatibilityUtils.getColor(resources,
R.color.locationbar_light_selection_color);
setUseDarkTextColors(true);
mUrlDirection = LAYOUT_DIRECTION_LOCALE;
mAutocompleteSpan = new AutocompleteSpan();
// The URL Bar is derived from an text edit class, and as such is focusable by
// default. This means that if it is created before the first draw of the UI it
// will (as the only focusable element of the UI) get focus on the first draw.
// We react to this by greying out the tab area and bringing up the keyboard,
// which we don't want to do at startup. Prevent this by disabling focus until
// the first draw.
setFocusable(false);
setFocusableInTouchMode(false);
mGestureDetector = new GestureDetector(
getContext(), new GestureDetector.SimpleOnGestureListener() {
@Override
public void onLongPress(MotionEvent e) {
performLongClick();
}
@Override
public boolean onSingleTapUp(MotionEvent e) {
requestFocus();
return true;
}
});
mGestureDetector.setOnDoubleTapListener(null);
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
/**
* Specifies whether the URL bar should use dark text colors or light colors.
* @param useDarkColors Whether the text colors should be dark (i.e. appropriate for use
* on a light background).
*/
public void setUseDarkTextColors(boolean useDarkColors) {
if (mUseDarkColors != null && mUseDarkColors.booleanValue() == useDarkColors) return;
mUseDarkColors = useDarkColors;
if (mUseDarkColors) {
setTextColor(mDarkDefaultTextColor);
setHighlightColor(mDarkHighlightColor);
} else {
setTextColor(mLightDefaultTextColor);
setHighlightColor(mLightHighlightColor);
}
// Note: Setting the hint text color only takes effect if there is not text in the URL bar.
// To get around this, set the URL to empty before setting the hint color and revert
// back to the previous text after.
boolean hasNonEmptyText = false;
Editable text = getText();
if (!TextUtils.isEmpty(text)) {
setText("");
hasNonEmptyText = true;
}
if (useDarkColors) {
setHintTextColor(mDarkHintColor);
} else {
setHintTextColor(mLightHintColor);
}
if (hasNonEmptyText) setText(text);
if (!hasFocus()) {
deEmphasizeUrl();
emphasizeUrl();
}
}
/**
* Sets whether text changes should trigger autocomplete.
* <p>
* {@link #setDelegate(UrlBarDelegate)} must be called with a non-null instance prior to
* enabling autocomplete.
*
* @param ignoreAutocomplete Whether text changes should be ignored and no auto complete
* triggered.
*/
public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete) {
assert mUrlBarDelegate != null;
mIgnoreAutocomplete = ignoreAutocomplete;
}
/**
* @return The search query text (non-null).
*/
public String getQueryText() {
return getEditableText() != null ? getEditableText().toString() : "";
}
/**
* @return Whether the current cursor position is at the end of the user typed text (i.e.
* at the beginning of the inline autocomplete text if present otherwise the very
* end of the current text).
*/
public boolean isCursorAtEndOfTypedText() {
final int selectionStart = getSelectionStart();
final int selectionEnd = getSelectionEnd();
int expectedSelectionStart = getText().getSpanStart(mAutocompleteSpan);
int expectedSelectionEnd = getText().length();
if (expectedSelectionStart < 0) {
expectedSelectionStart = expectedSelectionEnd;
}
return selectionStart == expectedSelectionStart && selectionEnd == expectedSelectionEnd;
}
/**
* @return Whether the URL is currently in batch edit mode triggered by an IME. No external
* text changes should be triggered while this is true.
*/
// isInBatchEditMode is a package protected method on TextView, so we intentionally chose
// a different name.
public boolean isHandlingBatchInput() {
return mInBatchEditMode;
}
/**
* @return The user text without the autocomplete text.
*/
public String getTextWithoutAutocomplete() {
int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan);
if (autoCompleteIndex < 0) {
return getQueryText();
} else {
return getQueryText().substring(0, autoCompleteIndex);
}
}
/** @return Whether any autocomplete information is specified on the current text. */
@VisibleForTesting
protected boolean hasAutocomplete() {
return getText().getSpanStart(mAutocompleteSpan) >= 0
|| mAutocompleteSpan.mAutocompleteText != null
|| mAutocompleteSpan.mUserText != null;
}
/**
* Whether we want to be showing inline autocomplete results. We don't want to show them as the
* user deletes input. Also if there is a composition (e.g. while using the Japanese IME),
* we must not autocomplete or we'll destroy the composition.
* @return Whether we want to be showing inline autocomplete results.
*/
public boolean shouldAutocomplete() {
if (mLastUrlEditWasDelete) return false;
Editable text = getText();
return isCursorAtEndOfTypedText()
&& !isPastedText()
&& !isHandlingBatchInput()
&& BaseInputConnection.getComposingSpanEnd(text)
== BaseInputConnection.getComposingSpanStart(text);
}
@Override
public void onBeginBatchEdit() {
mBeforeBatchEditAutocompleteIndex = getText().getSpanStart(mAutocompleteSpan);
mBeforeBatchEditFullText = getText().toString();
super.onBeginBatchEdit();
mInBatchEditMode = true;
mTextDeletedInBatchMode = false;
}
@Override
public void onEndBatchEdit() {
super.onEndBatchEdit();
mInBatchEditMode = false;
limitDisplayableLength();
if (mSelectionChangedInBatchMode) {
validateSelection(getSelectionStart(), getSelectionEnd());
mSelectionChangedInBatchMode = false;
}
String newText = getText().toString();
if (!TextUtils.equals(mBeforeBatchEditFullText, newText)
|| getText().getSpanStart(mAutocompleteSpan) != mBeforeBatchEditAutocompleteIndex) {
// If the text being typed is a single character that matches the next character in the
// previously visible autocomplete text, we reapply the autocomplete text to prevent
// a visual flickering when the autocomplete text is cleared and then quickly reapplied
// when the next round of suggestions is received.
if (shouldAutocomplete() && mBeforeBatchEditAutocompleteIndex != -1
&& mBeforeBatchEditFullText != null
&& mBeforeBatchEditFullText.startsWith(newText)
&& !mTextDeletedInBatchMode
&& newText.length() - mBeforeBatchEditAutocompleteIndex == 1) {
setAutocompleteText(newText, mBeforeBatchEditFullText.substring(newText.length()));
}
notifyAutocompleteTextStateChanged(mTextDeletedInBatchMode);
}
mTextDeletedInBatchMode = false;
mBeforeBatchEditAutocompleteIndex = -1;
mBeforeBatchEditFullText = null;
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (!mInBatchEditMode) {
int beforeTextLength = getText().length();
if (validateSelection(selStart, selEnd)) {
boolean textDeleted = getText().length() < beforeTextLength;
notifyAutocompleteTextStateChanged(textDeleted);
}
} else {
mSelectionChangedInBatchMode = true;
}
super.onSelectionChanged(selStart, selEnd);
}
/**
* Validates the selection and clears the autocomplete span if needed. The autocomplete text
* will be deleted if the selection occurs entirely before the autocomplete region.
*
* @param selStart The start of the selection.
* @param selEnd The end of the selection.
* @return Whether the autocomplete span was removed as a result of this validation.
*/
private boolean validateSelection(int selStart, int selEnd) {
int spanStart = getText().getSpanStart(mAutocompleteSpan);
int spanEnd = getText().getSpanEnd(mAutocompleteSpan);
if (spanStart >= 0 && (spanStart != selStart || spanEnd != selEnd)) {
// On selection changes, the autocomplete text has been accepted by the user or needs
// to be deleted below.
mAutocompleteSpan.clearSpan();
// The autocomplete text will be deleted any time the selection occurs entirely before
// the start of the autocomplete text. This is required because certain keyboards will
// insert characters temporarily when starting a key entry gesture (whether it be
// swyping a word or long pressing to get a special character). When this temporary
// character appears, Chrome may decide to append some autocomplete, but the keyboard
// will then remove this temporary character only while leaving the autocomplete text
// alone. See crbug/273763 for more details.
if (selEnd <= spanStart) getText().delete(spanStart, getText().length());
return true;
}
return false;
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
mFocused = focused;
if (!focused) mAutocompleteSpan.clearSpan();
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (focused && mFirstFocusTimeMs == 0) {
mFirstFocusTimeMs = SystemClock.elapsedRealtime();
if (mOmniboxLivenessListener != null) mOmniboxLivenessListener.onOmniboxFocused();
}
if (focused) StartupMetrics.getInstance().recordFocusedOmnibox();
fixupTextDirection();
// Always align to the same as the paragraph direction (LTR = left, RTL = right).
ApiCompatibilityUtils.setTextAlignment(this, TEXT_ALIGNMENT_TEXT_START);
}
/**
* @return The elapsed realtime timestamp in ms of the first time the url bar was focused,
* 0 if never.
*/
public long getFirstFocusTime() {
return mFirstFocusTimeMs;
}
/**
* Sets whether this {@link UrlBar} should be focusable.
*/
public void setAllowFocus(boolean allowFocus) {
mAllowFocus = allowFocus;
if (mFirstDrawComplete) {
setFocusable(allowFocus);
setFocusableInTouchMode(allowFocus);
}
}
/**
* Sets the {@link UrlBar}'s text direction based on focus and contents.
*
* Should be called whenever focus or text contents change.
*/
private void fixupTextDirection() {
// When unfocused, force left-to-right rendering at the paragraph level (which is desired
// for URLs). Right-to-left runs are still rendered RTL, but will not flip the whole URL
// around. This is consistent with OmniboxViewViews on desktop. When focused, render text
// normally (to allow users to make non-URL searches and to avoid showing Android's split
// insertion point when an RTL user enters RTL text). Also render text normally when the
// text field is empty (because then it displays an instruction that is not a URL).
if (mFocused || length() == 0) {
ApiCompatibilityUtils.setTextDirection(this, TEXT_DIRECTION_INHERIT);
} else {
ApiCompatibilityUtils.setTextDirection(this, TEXT_DIRECTION_LTR);
}
}
@Override
protected void onWindowVisibilityChanged(int visibility) {
super.onWindowVisibilityChanged(visibility);
if (visibility == View.GONE && isFocused()) mShowKeyboardOnWindowFocus = true;
}
@Override
public void onWindowFocusChanged(boolean hasWindowFocus) {
super.onWindowFocusChanged(hasWindowFocus);
if (hasWindowFocus) {
if (mShowKeyboardOnWindowFocus && isFocused()) {
// Without the call to post(..), the keyboard was not getting shown when the
// window regained focus despite this being the final call in the view system
// flow.
post(new Runnable() {
@Override
public void run() {
UiUtils.showKeyboard(UrlBar.this);
}
});
}
mShowKeyboardOnWindowFocus = false;
}
}
@Override
public View focusSearch(int direction) {
if (direction == View.FOCUS_BACKWARD
&& mUrlBarDelegate.getCurrentTab().getView() != null) {
return mUrlBarDelegate.getCurrentTab().getView();
} else {
return super.focusSearch(direction);
}
}
@Override
public boolean onTouchEvent(MotionEvent event) {
if (!mFocused) {
mGestureDetector.onTouchEvent(event);
return true;
}
Tab currentTab = mUrlBarDelegate.getCurrentTab();
if (event.getAction() == MotionEvent.ACTION_DOWN && currentTab != null) {
// Make sure to hide the current ContentView ActionBar.
ContentViewCore viewCore = currentTab.getContentViewCore();
if (viewCore != null) viewCore.hideSelectActionMode();
}
return super.onTouchEvent(event);
}
@Override
public boolean bringPointIntoView(int offset) {
if (mDisableTextScrollingFromAutocomplete) return false;
return super.bringPointIntoView(offset);
}
@Override
public boolean onPreDraw() {
boolean retVal = super.onPreDraw();
if (mDisableTextScrollingFromAutocomplete) {
// super.onPreDraw will put the selection at the end of the text selection, but
// in the case of autocomplete we want the last typed character to be shown, which
// is the start of selection.
mDisableTextScrollingFromAutocomplete = false;
bringPointIntoView(getSelectionStart());
retVal = true;
}
return retVal;
}
@Override
public void onDraw(Canvas canvas) {
super.onDraw(canvas);
if (!mFirstDrawComplete) {
mFirstDrawComplete = true;
// We have now avoided the first draw problem (see the comment in
// the constructor) so we want to make the URL bar focusable so that
// touches etc. activate it.
setFocusable(mAllowFocus);
setFocusableInTouchMode(mAllowFocus);
// The URL bar will now react correctly to a focus change event
if (mOmniboxLivenessListener != null) {
mOmniboxLivenessListener.onOmniboxInteractive();
}
}
// Notify listeners if the URL's direction has changed.
updateUrlDirection();
}
/**
* If the direction of the URL has changed, update mUrlDirection and notify the
* UrlDirectionListeners.
*/
private void updateUrlDirection() {
Layout layout = getLayout();
if (layout == null) return;
int urlDirection;
if (length() == 0) {
urlDirection = LAYOUT_DIRECTION_LOCALE;
} else if (layout.getParagraphDirection(0) == Layout.DIR_LEFT_TO_RIGHT) {
urlDirection = LAYOUT_DIRECTION_LTR;
} else {
urlDirection = LAYOUT_DIRECTION_RTL;
}
if (urlDirection != mUrlDirection) {
mUrlDirection = urlDirection;
if (mUrlDirectionListener != null) {
mUrlDirectionListener.onUrlDirectionChanged(urlDirection);
}
}
}
/**
* @return The text direction of the URL, e.g. LAYOUT_DIRECTION_LTR.
*/
public int getUrlDirection() {
return mUrlDirection;
}
/**
* Sets the listener for changes in the url bar's layout direction. Also calls
* onUrlDirectionChanged() immediately on the listener.
*
* @param listener The UrlDirectionListener to receive callbacks when the url direction changes,
* or null to unregister any previously registered listener.
*/
public void setUrlDirectionListener(UrlDirectionListener listener) {
mUrlDirectionListener = listener;
if (mUrlDirectionListener != null) {
mUrlDirectionListener.onUrlDirectionChanged(mUrlDirection);
}
}
/**
* Set the url delegate to handle communication from the {@link UrlBar} to the rest of the UI.
* @param delegate The {@link UrlBarDelegate} to be used.
*/
public void setDelegate(UrlBarDelegate delegate) {
mUrlBarDelegate = delegate;
}
/**
* Set {@link OmniboxLivenessListener} to be used for receiving interaction related messages
* during startup.
* @param listener The listener to use for sending the messages.
*/
@VisibleForTesting
public void setOmniboxLivenessListener(OmniboxLivenessListener listener) {
mOmniboxLivenessListener = listener;
}
/**
* Signal {@link OmniboxLivenessListener} that the omnibox is completely operational now.
*/
@VisibleForTesting
public void onOmniboxFullyFunctional() {
if (mOmniboxLivenessListener != null) mOmniboxLivenessListener.onOmniboxFullyFunctional();
}
@Override
public boolean onTextContextMenuItem(int id) {
if (id == android.R.id.paste) {
ClipboardManager clipboard = (ClipboardManager) getContext()
.getSystemService(Context.CLIPBOARD_SERVICE);
ClipData clipData = clipboard.getPrimaryClip();
if (clipData != null) {
StringBuilder builder = new StringBuilder();
for (int i = 0; i < clipData.getItemCount(); i++) {
builder.append(clipData.getItemAt(i).coerceToText(getContext()));
}
String pasteString = OmniboxViewUtil.sanitizeTextForPaste(builder.toString());
int min = 0;
int max = getText().length();
if (isFocused()) {
final int selStart = getSelectionStart();
final int selEnd = getSelectionEnd();
min = Math.max(0, Math.min(selStart, selEnd));
max = Math.max(0, Math.max(selStart, selEnd));
}
Selection.setSelection(getText(), max);
getText().replace(min, max, pasteString);
mIsPastedText = true;
return true;
}
}
if (mOriginalUrlLocation == null || mFormattedUrlLocation == null) {
return super.onTextContextMenuItem(id);
}
int selectedStartIndex = getSelectionStart();
int selectedEndIndex = getSelectionEnd();
// If we are copying/cutting the full previously formatted URL, reset the URL
// text before initiating the TextViews handling of the context menu.
String currentText = getText().toString();
if (selectedStartIndex == 0
&& (id == android.R.id.cut || id == android.R.id.copy)
&& currentText.startsWith(mFormattedUrlLocation)
&& selectedEndIndex >= mFormattedUrlLocation.length()) {
String newText = mOriginalUrlLocation
+ currentText.substring(mFormattedUrlLocation.length());
selectedEndIndex = selectedEndIndex - mFormattedUrlLocation.length()
+ mOriginalUrlLocation.length();
setIgnoreTextChangesForAutocomplete(true);
setText(newText);
setSelection(0, selectedEndIndex);
boolean retVal = super.onTextContextMenuItem(id);
if (getText().toString().equals(newText)) {
setText(currentText);
setSelection(getText().length());
}
setIgnoreTextChangesForAutocomplete(false);
return retVal;
}
return super.onTextContextMenuItem(id);
}
/**
* Sets the text content of the URL bar.
*
* @param url The original URL (or generic text) that can be used for copy/cut/paste.
* @param formattedUrl Formatted URL for user display. Null if there isn't one.
* @return Whether the visible text has changed.
*/
public boolean setUrl(String url, String formattedUrl) {
if (!TextUtils.isEmpty(formattedUrl)) {
try {
URL javaUrl = new URL(url);
mFormattedUrlLocation =
getUrlContentsPrePath(formattedUrl, javaUrl.getHost());
mOriginalUrlLocation =
getUrlContentsPrePath(url, javaUrl.getHost());
} catch (MalformedURLException mue) {
mOriginalUrlLocation = null;
mFormattedUrlLocation = null;
}
} else {
mOriginalUrlLocation = null;
mFormattedUrlLocation = null;
formattedUrl = url;
}
Editable previousText = getEditableText();
setText(formattedUrl);
if (!isFocused()) scrollToTLD();
return !TextUtils.equals(previousText, getEditableText());
}
/**
* Autocompletes the text on the url bar and selects the text that was not entered by the
* user. Using append() instead of setText() to preserve the soft-keyboard layout.
* @param userText user The text entered by the user.
* @param inlineAutocompleteText The suggested autocompletion for the user's text.
*/
public void setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText) {
boolean emptyAutocomplete = TextUtils.isEmpty(inlineAutocompleteText);
if (!emptyAutocomplete) mDisableTextScrollingFromAutocomplete = true;
int autocompleteIndex = userText.length();
String previousText = getQueryText();
CharSequence newText = TextUtils.concat(userText, inlineAutocompleteText);
setIgnoreTextChangesForAutocomplete(true);
mDisableTextAccessibilityEvents = true;
if (!TextUtils.equals(previousText, newText)) {
// The previous text may also have included autocomplete text, so we only
// append the new autocomplete text that has changed.
if (TextUtils.indexOf(newText, previousText) == 0) {
append(newText.subSequence(previousText.length(), newText.length()));
} else {
setUrl(newText.toString(), null);
}
}
if (getSelectionStart() != autocompleteIndex
|| getSelectionEnd() != getText().length()) {
setSelection(autocompleteIndex, getText().length());
if (inlineAutocompleteText.length() != 0) {
// Sending a TYPE_VIEW_TEXT_SELECTION_CHANGED accessibility event causes the
// previous TYPE_VIEW_TEXT_CHANGED event to be swallowed. As a result the user
// hears the autocomplete text but *not* the text they typed. Instead we send a
// TYPE_ANNOUNCEMENT event, which doesn't swallow the text-changed event.
announceForAccessibility(inlineAutocompleteText);
}
}
if (emptyAutocomplete) {
mAutocompleteSpan.clearSpan();
} else {
mAutocompleteSpan.setSpan(userText, inlineAutocompleteText);
}
setIgnoreTextChangesForAutocomplete(false);
mDisableTextAccessibilityEvents = false;
}
/**
* Returns the length of the autocomplete text currently displayed, zero if none is
* currently displayed.
*/
public int getAutocompleteLength() {
int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan);
if (autoCompleteIndex < 0) return 0;
return getText().length() - autoCompleteIndex;
}
/**
* Overrides the text announced when focusing on the field for accessibility. This value will
* be cleared automatically when the text content changes for this view.
* @param accessibilityOverride The text to be announced instead of the current text value
* (or null if the text content should be read).
*/
public void setAccessibilityTextOverride(String accessibilityOverride) {
mAccessibilityTextOverride = accessibilityOverride;
}
private void scrollToTLD() {
Editable url = getText();
if (url == null || url.length() < 1) return;
String urlString = url.toString();
URL javaUrl;
try {
javaUrl = new URL(urlString);
} catch (MalformedURLException mue) {
return;
}
String host = javaUrl.getHost();
if (host == null || host.isEmpty()) return;
int hostStart = urlString.indexOf(host);
int hostEnd = hostStart + host.length();
setSelection(hostEnd);
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
if (!mInBatchEditMode) {
limitDisplayableLength();
notifyAutocompleteTextStateChanged(lengthAfter == 0);
} else {
mTextDeletedInBatchMode = lengthAfter == 0;
}
mIsPastedText = false;
}
@Override
public void setText(CharSequence text, BufferType type) {
mDisableTextScrollingFromAutocomplete = false;
// Avoid setting the same text to the URL bar as it will mess up the scroll/cursor
// position.
// Setting the text is also quite expensive, so only do it when the text has changed
// (since we apply spans when the URL is not focused, we only optimize this when the
// URL is being edited).
if (!TextUtils.equals(getEditableText(), text)) {
super.setText(text, type);
mAccessibilityTextOverride = null;
}
// Verify the autocomplete is still valid after the text change.
if (mAutocompleteSpan != null
&& mAutocompleteSpan.mUserText != null
&& mAutocompleteSpan.mAutocompleteText != null) {
if (getText().getSpanStart(mAutocompleteSpan) < 0) {
mAutocompleteSpan.clearSpan();
} else {
clearAutocompleteSpanIfInvalid();
}
}
fixupTextDirection();
}
private void clearAutocompleteSpanIfInvalid() {
Editable editableText = getEditableText();
CharSequence previousUserText = mAutocompleteSpan.mUserText;
CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompleteText;
if (editableText.length()
!= (previousUserText.length() + previousAutocompleteText.length())) {
mAutocompleteSpan.clearSpan();
} else if (TextUtils.indexOf(getText(), previousUserText) != 0
|| TextUtils.indexOf(getText(),
previousAutocompleteText, previousUserText.length()) != 0) {
mAutocompleteSpan.clearSpan();
}
}
private void limitDisplayableLength() {
// To limit displayable length we replace middle portion of the string with ellipsis.
// That affects only presentation of the text, and doesn't affect other aspects like
// copying to the clipboard, getting text with getText(), etc.
final int maxLength = SysUtils.isLowEndDevice()
? MAX_DISPLAYABLE_LENGTH_LOW_END : MAX_DISPLAYABLE_LENGTH;
Editable text = getText();
int textLength = text.length();
if (textLength <= maxLength) {
if (mDidEllipsizeTextHint) {
EllipsisSpan[] spans = text.getSpans(0, textLength, EllipsisSpan.class);
if (spans != null && spans.length > 0) {
assert spans.length == 1 : "Should never apply more than a single EllipsisSpan";
for (int i = 0; i < spans.length; i++) {
text.removeSpan(spans[i]);
}
}
}
mDidEllipsizeTextHint = false;
return;
}
mDidEllipsizeTextHint = true;
int spanLeft = text.nextSpanTransition(0, textLength, EllipsisSpan.class);
if (spanLeft != textLength) return;
spanLeft = maxLength / 2;
text.setSpan(EllipsisSpan.INSTANCE, spanLeft, textLength - spanLeft,
Editable.SPAN_INCLUSIVE_EXCLUSIVE);
}
/**
* Returns the portion of the URL that precedes the path/query section of the URL.
*
* @param url The url to be used to find the preceding portion.
* @param host The host to be located in the URL to determine the location of the path.
* @return The URL contents that precede the path (or the passed in URL if the host is
* not found).
*/
private static String getUrlContentsPrePath(String url, String host) {
String urlPrePath = url;
int hostIndex = url.indexOf(host);
if (hostIndex >= 0) {
int pathIndex = url.indexOf('/', hostIndex);
if (pathIndex > 0) {
urlPrePath = url.substring(0, pathIndex);
} else {
urlPrePath = url;
}
}
return urlPrePath;
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
if (mDisableTextAccessibilityEvents) {
if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
|| event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED) {
return;
}
}
super.sendAccessibilityEventUnchecked(event);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
// Certain OEM implementations of onInitializeAccessibilityNodeInfo trigger disk reads
// to access the clipboard. crbug.com/640993
StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads();
try {
super.onInitializeAccessibilityNodeInfo(info);
} finally {
StrictMode.setThreadPolicy(oldPolicy);
}
if (mAccessibilityTextOverride != null) {
info.setText(mAccessibilityTextOverride);
}
}
@VisibleForTesting
InputConnectionWrapper mInputConnection = new InputConnectionWrapper(null, true) {
private final char[] mTempSelectionChar = new char[1];
@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
Editable currentText = getText();
if (currentText == null) return super.commitText(text, newCursorPosition);
int selectionStart = Selection.getSelectionStart(currentText);
int selectionEnd = Selection.getSelectionEnd(currentText);
int autocompleteIndex = currentText.getSpanStart(mAutocompleteSpan);
// If the text being committed is a single character that matches the next character
// in the selection (assumed to be the autocomplete text), we only move the text
// selection instead clearing the autocomplete text causing flickering as the
// autocomplete text will appear once the next suggestions are received.
//
// To be confident that the selection is an autocomplete, we ensure the selection
// is at least one character and the end of the selection is the end of the
// currently entered text.
if (newCursorPosition == 1 && selectionStart > 0 && selectionStart != selectionEnd
&& selectionEnd >= currentText.length()
&& autocompleteIndex == selectionStart
&& text.length() == 1) {
currentText.getChars(selectionStart, selectionStart + 1, mTempSelectionChar, 0);
if (mTempSelectionChar[0] == text.charAt(0)) {
// Since the text isn't changing, TalkBack won't read out the typed characters.
// To work around this, explicitly send an accessibility event. crbug.com/416595
if (mAccessibilityManager != null && mAccessibilityManager.isEnabled()) {
AccessibilityEvent event = AccessibilityEvent.obtain(
AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
event.setFromIndex(selectionStart);
event.setRemovedCount(0);
event.setAddedCount(1);
event.setBeforeText(currentText.toString().substring(0, selectionStart));
sendAccessibilityEventUnchecked(event);
}
setAutocompleteText(
currentText.subSequence(0, selectionStart + 1),
currentText.subSequence(selectionStart + 1, selectionEnd));
if (!mInBatchEditMode) {
notifyAutocompleteTextStateChanged(false);
}
return true;
}
}
boolean retVal = super.commitText(text, newCursorPosition);
// Ensure the autocomplete span is removed if it is no longer valid after committing the
// text.
if (getText().getSpanStart(mAutocompleteSpan) >= 0) clearAutocompleteSpanIfInvalid();
return retVal;
}
@Override
public boolean setComposingText(CharSequence text, int newCursorPosition) {
Editable currentText = getText();
int autoCompleteSpanStart = currentText.getSpanStart(mAutocompleteSpan);
if (autoCompleteSpanStart >= 0) {
int composingEnd = BaseInputConnection.getComposingSpanEnd(currentText);
// On certain device/keyboard combinations, the composing regions are specified
// with a noticeable delay after the initial character is typed, and in certain
// circumstances it does not check that the current state of the text matches the
// expectations of it's composing region.
// For example, you can be typing:
// chrome://f
// Chrome will autocomplete to:
// chrome://f[lags]
// And after the autocomplete has been set, the keyboard will set the composing
// region to the last character and it assumes it is 'f' as it was the last
// character the keyboard sent. If we commit this composition, the text will
// look like:
// chrome://flag[f]
// And if we use the autocomplete clearing logic below, it will look like:
// chrome://f[f]
// To work around this, we see if the composition matches all the characters prior
// to the autocomplete and just readjust the composing region to be that subset.
//
// See crbug.com/366732
if (composingEnd == currentText.length()
&& autoCompleteSpanStart >= text.length()
&& TextUtils.equals(
currentText.subSequence(
autoCompleteSpanStart - text.length(),
autoCompleteSpanStart),
text)) {
setComposingRegion(
autoCompleteSpanStart - text.length(), autoCompleteSpanStart);
}
// Once composing text is being modified, the autocomplete text has been accepted
// or has to be deleted.
mAutocompleteSpan.clearSpan();
Selection.setSelection(currentText, autoCompleteSpanStart);
currentText.delete(autoCompleteSpanStart, currentText.length());
}
return super.setComposingText(text, newCursorPosition);
}
};
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
mInputConnection.setTarget(super.onCreateInputConnection(outAttrs));
return mInputConnection;
}
/**
* Emphasize the TLD and second domain of the URL.
*/
public void emphasizeUrl() {
Editable url = getText();
if (OmniboxUrlEmphasizer.hasEmphasisSpans(url) || hasFocus()) {
return;
}
if (url.length() < 1) {
return;
}
// We retrieve the domain and registry from the full URL (the url bar shows a simplified
// version of the URL).
Tab currentTab = mUrlBarDelegate.getCurrentTab();
if (currentTab == null || currentTab.getProfile() == null) return;
boolean isInternalPage = false;
try {
String tabUrl = currentTab.getUrl();
isInternalPage = UrlUtilities.isInternalScheme(new URI(tabUrl));
} catch (URISyntaxException e) {
// Ignore as this only is for applying color
}
OmniboxUrlEmphasizer.emphasizeUrl(url, getResources(), currentTab.getProfile(),
currentTab.getSecurityLevel(), isInternalPage,
mUseDarkColors, mUrlBarDelegate.shouldEmphasizeHttpsScheme());
}
/**
* Reset the modifications done to emphasize the TLD and second domain of the URL.
*/
public void deEmphasizeUrl() {
OmniboxUrlEmphasizer.deEmphasizeUrl(getText());
}
/**
* @return Whether the current UrlBar input has been pasted from the clipboard.
*/
public boolean isPastedText() {
return mIsPastedText;
}
private void notifyAutocompleteTextStateChanged(boolean textDeleted) {
if (mUrlBarDelegate == null) return;
if (!hasFocus()) return;
if (mIgnoreAutocomplete) return;
mLastUrlEditWasDelete = textDeleted;
mUrlBarDelegate.onTextChangedForAutocomplete(textDeleted);
}
/**
* Simple span used for tracking the current autocomplete state.
*/
private class AutocompleteSpan {
private CharSequence mUserText;
private CharSequence mAutocompleteText;
/**
* Adds the span to the current text.
* @param userText The user entered text.
* @param autocompleteText The autocomplete text being appended.
*/
public void setSpan(CharSequence userText, CharSequence autocompleteText) {
Editable text = getText();
text.removeSpan(this);
mAutocompleteText = autocompleteText;
mUserText = userText;
text.setSpan(
this,
userText.length(),
text.length(),
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
/** Removes this span from the current text and clears the internal state. */
public void clearSpan() {
getText().removeSpan(this);
mAutocompleteText = null;
mUserText = null;
}
}
/**
* Span that displays ellipsis instead of the text. Used to hide portion of
* very large string to get decent performance from TextView.
*/
private static class EllipsisSpan extends ReplacementSpan {
private static final String ELLIPSIS = "...";
public static final EllipsisSpan INSTANCE = new EllipsisSpan();
@Override
public int getSize(Paint paint, CharSequence text,
int start, int end, Paint.FontMetricsInt fm) {
return (int) paint.measureText(ELLIPSIS);
}
@Override
public void draw(Canvas canvas, CharSequence text, int start, int end,
float x, int top, int y, int bottom, Paint paint) {
canvas.drawText(ELLIPSIS, x, y, paint);
}
}
}