blob: bec71a454fb26fa0caf90e101fc998d439355f37 [file] [log] [blame]
// Copyright 2017 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.Context;
import android.graphics.Rect;
import android.provider.Settings;
import android.support.annotation.CallSuper;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.view.KeyEvent;
import android.view.accessibility.AccessibilityEvent;
import android.view.accessibility.AccessibilityManager;
import android.view.accessibility.AccessibilityNodeInfo;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
import android.widget.EditText;
import org.chromium.base.Log;
import org.chromium.base.StrictModeContext;
import org.chromium.base.VisibleForTesting;
import org.chromium.chrome.browser.ChromeFeatureList;
import org.chromium.chrome.browser.widget.VerticallyFixedEditText;
/**
* An {@link EditText} that shows autocomplete text at the end.
*/
public class AutocompleteEditText
extends VerticallyFixedEditText implements AutocompleteEditTextModelBase.Delegate {
private static final String TAG = "cr_AutocompleteEdit";
private static final boolean DEBUG = false;
private final AccessibilityManager mAccessibilityManager;
private AutocompleteEditTextModelBase mModel;
private boolean mIgnoreTextChangesForAutocomplete = true;
private boolean mLastEditWasPaste;
/**
* 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 boolean mIgnoreImeForTest;
public AutocompleteEditText(Context context, AttributeSet attrs) {
super(context, attrs);
mAccessibilityManager =
(AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE);
}
@VisibleForTesting
public AccessibilityManager getAccessibilityManagerForTesting() {
return mAccessibilityManager;
}
private void ensureModel() {
if (mModel != null) return;
if (!ChromeFeatureList.isInitialized()
|| ChromeFeatureList.isEnabled(ChromeFeatureList.SPANNABLE_INLINE_AUTOCOMPLETE)) {
Log.w(TAG, "Using spannable model...");
mModel = new SpannableAutocompleteEditTextModel(this);
} else {
Log.w(TAG, "Using non-spannable model...");
mModel = new AutocompleteEditTextModel(this);
}
// Feed initial values.
mModel.setIgnoreTextChangeFromAutocomplete(true);
mModel.onFocusChanged(hasFocus());
mModel.onSetText(getText());
mModel.onTextChanged(getText(), 0, 0, getText().length());
mModel.onSelectionChanged(getSelectionStart(), getSelectionEnd());
if (mLastEditWasPaste) mModel.onPaste();
mModel.setIgnoreTextChangeFromAutocomplete(false);
mModel.setIgnoreTextChangeFromAutocomplete(mIgnoreTextChangesForAutocomplete);
}
/**
* Sets whether text changes should trigger autocomplete.
*
* @param ignoreAutocomplete Whether text changes should be ignored and no auto complete
* triggered.
*/
public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete) {
mIgnoreTextChangesForAutocomplete = ignoreAutocomplete;
if (mModel != null) mModel.setIgnoreTextChangeFromAutocomplete(ignoreAutocomplete);
}
/**
* @return The user text without the autocomplete text.
*/
public String getTextWithoutAutocomplete() {
if (mModel == null) return "";
return mModel.getTextWithoutAutocomplete();
}
/** @return Text that includes autocomplete. */
public String getTextWithAutocomplete() {
if (mModel == null) return "";
return mModel.getTextWithAutocomplete();
}
/** @return Whether any autocomplete information is specified on the current text. */
@VisibleForTesting
public boolean hasAutocomplete() {
if (mModel == null) return false;
return mModel.hasAutocomplete();
}
/**
* 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 (mModel == null) return false;
return mModel.shouldAutocomplete();
}
@Override
protected void onSelectionChanged(int selStart, int selEnd) {
if (mModel != null) mModel.onSelectionChanged(selStart, selEnd);
super.onSelectionChanged(selStart, selEnd);
}
@Override
protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) {
if (mModel != null) mModel.onFocusChanged(focused);
super.onFocusChanged(focused, direction, previouslyFocusedRect);
if (!focused) setCursorVisible(false);
}
@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;
}
/** Call this when text is pasted. */
@CallSuper
public void onPaste() {
mLastEditWasPaste = true;
if (mModel != null) mModel.onPaste();
}
/**
* Autocompletes the text 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;
if (mModel != null) mModel.setAutocompleteText(userText, inlineAutocompleteText);
}
/**
* Returns the length of the autocomplete text currently displayed, zero if none is
* currently displayed.
*/
public int getAutocompleteLength() {
if (mModel == null) return 0;
return mModel.getAutocompleteText().length();
}
@Override
protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) {
super.onTextChanged(text, start, lengthBefore, lengthAfter);
mLastEditWasPaste = false;
if (mModel != null) mModel.onTextChanged(text, start, lengthBefore, lengthAfter);
}
@Override
public void setText(CharSequence text, BufferType type) {
if (DEBUG) Log.i(TAG, "setText -- text: %s", text);
mDisableTextScrollingFromAutocomplete = false;
// Certain OEM implementations of setText trigger disk reads. https://crbug.com/633298
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
super.setText(text, type);
}
if (mModel != null) mModel.onSetText(text);
}
@Override
public void sendAccessibilityEventUnchecked(AccessibilityEvent event) {
if (shouldIgnoreAccessibilityEvent(event)) {
if (DEBUG) Log.i(TAG, "Ignoring accessibility event from autocomplete.");
return;
}
super.sendAccessibilityEventUnchecked(event);
}
@Override
public void onPopulateAccessibilityEvent(AccessibilityEvent event) {
super.onPopulateAccessibilityEvent(event);
if (DEBUG) Log.i(TAG, "onPopulateAccessibilityEvent: " + event);
}
private boolean shouldIgnoreAccessibilityEvent(AccessibilityEvent event) {
return (mIgnoreTextChangesForAutocomplete
|| (mModel != null && mModel.shouldIgnoreAccessibilityEvent()))
&& (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED
|| event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED);
}
@Override
public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) {
// Certain OEM implementations of onInitializeAccessibilityNodeInfo trigger disk reads
// to access the clipboard. crbug.com/640993
try (StrictModeContext unused = StrictModeContext.allowDiskReads()) {
super.onInitializeAccessibilityNodeInfo(info);
}
}
@VisibleForTesting
public InputConnection getInputConnection() {
if (mModel == null) return null;
return mModel.getInputConnection();
}
@VisibleForTesting
public void setIgnoreImeForTest(boolean ignore) {
mIgnoreImeForTest = ignore;
}
@Override
public InputConnection onCreateInputConnection(EditorInfo outAttrs) {
InputConnection target = super.onCreateInputConnection(outAttrs);
// Initially, target is null until View gets the focus.
if (target == null && mModel == null) {
if (DEBUG) Log.i(TAG, "onCreateInputConnection - ignoring null target.");
return null;
}
if (DEBUG) Log.i(TAG, "onCreateInputConnection: " + target);
ensureModel();
InputConnection retVal = mModel.onCreateInputConnection(target);
if (mIgnoreImeForTest) return null;
return retVal;
}
@Override
public boolean dispatchKeyEvent(final KeyEvent event) {
if (mIgnoreImeForTest) return true;
if (mModel == null) return super.dispatchKeyEvent(event);
return mModel.dispatchKeyEvent(event);
}
@Override
public boolean super_dispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
/**
* @return Whether the current UrlBar input has been pasted from the clipboard.
*/
public boolean wasLastEditPaste() {
return mLastEditWasPaste;
}
@Override
public void replaceAllTextFromAutocomplete(String text) {
assert false; // make sure that this method is properly overridden.
}
@Override
public void onAutocompleteTextStateChanged(boolean updateDisplay) {
assert false; // make sure that this method is properly overridden.
}
@Override
public boolean isAccessibilityEnabled() {
return mAccessibilityManager != null && mAccessibilityManager.isEnabled();
}
@Override
public void onUpdateSelectionForTesting(int selStart, int selEnd) {}
@Override
public String getKeyboardPackageName() {
String defaultIme = Settings.Secure.getString(
getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
return defaultIme == null ? "" : defaultIme;
}
}