1 // Copyright 2017 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.Rect; 9 import android.provider.Settings; 10 import android.text.TextUtils; 11 import android.util.AttributeSet; 12 import android.view.KeyEvent; 13 import android.view.accessibility.AccessibilityEvent; 14 import android.view.accessibility.AccessibilityManager; 15 import android.view.accessibility.AccessibilityNodeInfo; 16 import android.view.inputmethod.EditorInfo; 17 import android.view.inputmethod.InputConnection; 18 import android.widget.EditText; 19 20 import androidx.annotation.CallSuper; 21 import androidx.annotation.VisibleForTesting; 22 23 import org.chromium.base.Log; 24 import org.chromium.base.StrictModeContext; 25 import org.chromium.chrome.browser.flags.ChromeFeatureList; 26 import org.chromium.components.browser_ui.widget.text.VerticallyFixedEditText; 27 28 /** 29 * An {@link EditText} that shows autocomplete text at the end. 30 */ 31 public class AutocompleteEditText 32 extends VerticallyFixedEditText implements AutocompleteEditTextModelBase.Delegate { 33 private static final String TAG = "AutocompleteEdit"; 34 35 private static final boolean DEBUG = false; 36 37 private final AccessibilityManager mAccessibilityManager; 38 39 private AutocompleteEditTextModelBase mModel; 40 private boolean mIgnoreTextChangesForAutocomplete = true; 41 private boolean mLastEditWasPaste; 42 43 /** 44 * Whether default TextView scrolling should be disabled because autocomplete has been added. 45 * This allows the user entered text to be shown instead of the end of the autocomplete. 46 */ 47 private boolean mDisableTextScrollingFromAutocomplete; 48 49 private boolean mIgnoreImeForTest; 50 AutocompleteEditText(Context context, AttributeSet attrs)51 public AutocompleteEditText(Context context, AttributeSet attrs) { 52 super(context, attrs); 53 mAccessibilityManager = 54 (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); 55 } 56 57 @VisibleForTesting getAccessibilityManagerForTesting()58 public AccessibilityManager getAccessibilityManagerForTesting() { 59 return mAccessibilityManager; 60 } 61 ensureModel()62 private void ensureModel() { 63 if (mModel != null) return; 64 65 if (!ChromeFeatureList.isInitialized() 66 || ChromeFeatureList.isEnabled(ChromeFeatureList.SPANNABLE_INLINE_AUTOCOMPLETE)) { 67 Log.w(TAG, "Using spannable model..."); 68 mModel = new SpannableAutocompleteEditTextModel(this); 69 } else { 70 Log.w(TAG, "Using non-spannable model..."); 71 mModel = new AutocompleteEditTextModel(this); 72 } 73 // Feed initial values. 74 mModel.setIgnoreTextChangeFromAutocomplete(true); 75 mModel.onFocusChanged(hasFocus()); 76 mModel.onSetText(getText()); 77 mModel.onTextChanged(getText(), 0, 0, getText().length()); 78 mModel.onSelectionChanged(getSelectionStart(), getSelectionEnd()); 79 if (mLastEditWasPaste) mModel.onPaste(); 80 mModel.setIgnoreTextChangeFromAutocomplete(false); 81 mModel.setIgnoreTextChangeFromAutocomplete(mIgnoreTextChangesForAutocomplete); 82 } 83 84 /** 85 * Sets whether text changes should trigger autocomplete. 86 * 87 * @param ignoreAutocomplete Whether text changes should be ignored and no auto complete 88 * triggered. 89 */ setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete)90 public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete) { 91 mIgnoreTextChangesForAutocomplete = ignoreAutocomplete; 92 if (mModel != null) mModel.setIgnoreTextChangeFromAutocomplete(ignoreAutocomplete); 93 } 94 95 /** 96 * @return The user text without the autocomplete text. 97 */ getTextWithoutAutocomplete()98 public String getTextWithoutAutocomplete() { 99 if (mModel == null) return ""; 100 return mModel.getTextWithoutAutocomplete(); 101 } 102 103 /** @return Text that includes autocomplete. */ getTextWithAutocomplete()104 public String getTextWithAutocomplete() { 105 if (mModel == null) return ""; 106 return mModel.getTextWithAutocomplete(); 107 } 108 109 /** @return Whether any autocomplete information is specified on the current text. */ 110 @VisibleForTesting hasAutocomplete()111 public boolean hasAutocomplete() { 112 if (mModel == null) return false; 113 return mModel.hasAutocomplete(); 114 } 115 116 /** 117 * Whether we want to be showing inline autocomplete results. We don't want to show them as the 118 * user deletes input. Also if there is a composition (e.g. while using the Japanese IME), 119 * we must not autocomplete or we'll destroy the composition. 120 * @return Whether we want to be showing inline autocomplete results. 121 */ shouldAutocomplete()122 public boolean shouldAutocomplete() { 123 if (mModel == null) return false; 124 return mModel.shouldAutocomplete(); 125 } 126 127 @Override onSelectionChanged(int selStart, int selEnd)128 protected void onSelectionChanged(int selStart, int selEnd) { 129 if (mModel != null) mModel.onSelectionChanged(selStart, selEnd); 130 super.onSelectionChanged(selStart, selEnd); 131 } 132 133 @Override onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect)134 protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { 135 if (mModel != null) mModel.onFocusChanged(focused); 136 super.onFocusChanged(focused, direction, previouslyFocusedRect); 137 if (!focused) setCursorVisible(false); 138 } 139 140 @Override bringPointIntoView(int offset)141 public boolean bringPointIntoView(int offset) { 142 if (mDisableTextScrollingFromAutocomplete) return false; 143 return super.bringPointIntoView(offset); 144 } 145 146 @Override onPreDraw()147 public boolean onPreDraw() { 148 boolean retVal = super.onPreDraw(); 149 if (mDisableTextScrollingFromAutocomplete) { 150 // super.onPreDraw will put the selection at the end of the text selection, but 151 // in the case of autocomplete we want the last typed character to be shown, which 152 // is the start of selection. 153 mDisableTextScrollingFromAutocomplete = false; 154 bringPointIntoView(getSelectionStart()); 155 retVal = true; 156 } 157 return retVal; 158 } 159 160 /** Call this when text is pasted. */ 161 @CallSuper onPaste()162 public void onPaste() { 163 mLastEditWasPaste = true; 164 if (mModel != null) mModel.onPaste(); 165 } 166 167 /** 168 * Autocompletes the text and selects the text that was not entered by the user. Using append() 169 * instead of setText() to preserve the soft-keyboard layout. 170 * @param userText user The text entered by the user. 171 * @param inlineAutocompleteText The suggested autocompletion for the user's text. 172 */ setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText)173 public void setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText) { 174 boolean emptyAutocomplete = TextUtils.isEmpty(inlineAutocompleteText); 175 if (!emptyAutocomplete) mDisableTextScrollingFromAutocomplete = true; 176 if (mModel != null) mModel.setAutocompleteText(userText, inlineAutocompleteText); 177 } 178 179 /** 180 * Returns the length of the autocomplete text currently displayed, zero if none is 181 * currently displayed. 182 */ getAutocompleteLength()183 public int getAutocompleteLength() { 184 if (mModel == null) return 0; 185 return mModel.getAutocompleteText().length(); 186 } 187 188 @Override onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter)189 protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { 190 super.onTextChanged(text, start, lengthBefore, lengthAfter); 191 mLastEditWasPaste = false; 192 if (mModel != null) mModel.onTextChanged(text, start, lengthBefore, lengthAfter); 193 } 194 195 @Override setText(CharSequence text, BufferType type)196 public void setText(CharSequence text, BufferType type) { 197 if (DEBUG) Log.i(TAG, "setText -- text: %s", text); 198 mDisableTextScrollingFromAutocomplete = false; 199 200 // Certain OEM implementations of setText trigger disk reads. https://crbug.com/633298 201 try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { 202 super.setText(text, type); 203 } 204 if (mModel != null) mModel.onSetText(text); 205 } 206 207 @Override sendAccessibilityEventUnchecked(AccessibilityEvent event)208 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { 209 if (shouldIgnoreAccessibilityEvent(event)) { 210 if (DEBUG) Log.i(TAG, "Ignoring accessibility event from autocomplete."); 211 return; 212 } 213 super.sendAccessibilityEventUnchecked(event); 214 } 215 216 @Override onPopulateAccessibilityEvent(AccessibilityEvent event)217 public void onPopulateAccessibilityEvent(AccessibilityEvent event) { 218 super.onPopulateAccessibilityEvent(event); 219 if (DEBUG) Log.i(TAG, "onPopulateAccessibilityEvent: " + event); 220 } 221 shouldIgnoreAccessibilityEvent(AccessibilityEvent event)222 private boolean shouldIgnoreAccessibilityEvent(AccessibilityEvent event) { 223 return (mIgnoreTextChangesForAutocomplete 224 || (mModel != null && mModel.shouldIgnoreAccessibilityEvent())) 225 && (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED 226 || event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 227 } 228 229 @Override onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info)230 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { 231 // Certain OEM implementations of onInitializeAccessibilityNodeInfo trigger disk reads 232 // to access the clipboard. crbug.com/640993 233 try (StrictModeContext ignored = StrictModeContext.allowDiskReads()) { 234 super.onInitializeAccessibilityNodeInfo(info); 235 } 236 } 237 238 @VisibleForTesting getInputConnection()239 public InputConnection getInputConnection() { 240 if (mModel == null) return null; 241 return mModel.getInputConnection(); 242 } 243 244 @VisibleForTesting setIgnoreImeForTest(boolean ignore)245 public void setIgnoreImeForTest(boolean ignore) { 246 mIgnoreImeForTest = ignore; 247 } 248 249 @Override onCreateInputConnection(EditorInfo outAttrs)250 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { 251 InputConnection target = super.onCreateInputConnection(outAttrs); 252 // Initially, target is null until View gets the focus. 253 if (target == null && mModel == null) { 254 if (DEBUG) Log.i(TAG, "onCreateInputConnection - ignoring null target."); 255 return null; 256 } 257 if (DEBUG) Log.i(TAG, "onCreateInputConnection: " + target); 258 ensureModel(); 259 InputConnection retVal = mModel.onCreateInputConnection(target); 260 if (mIgnoreImeForTest) return null; 261 return retVal; 262 } 263 264 @Override dispatchKeyEvent(final KeyEvent event)265 public boolean dispatchKeyEvent(final KeyEvent event) { 266 if (mIgnoreImeForTest) return true; 267 if (mModel == null) return super.dispatchKeyEvent(event); 268 return mModel.dispatchKeyEvent(event); 269 } 270 271 @Override super_dispatchKeyEvent(KeyEvent event)272 public boolean super_dispatchKeyEvent(KeyEvent event) { 273 return super.dispatchKeyEvent(event); 274 } 275 276 /** 277 * @return Whether the current UrlBar input has been pasted from the clipboard. 278 */ wasLastEditPaste()279 public boolean wasLastEditPaste() { 280 return mLastEditWasPaste; 281 } 282 283 @Override replaceAllTextFromAutocomplete(String text)284 public void replaceAllTextFromAutocomplete(String text) { 285 assert false; // make sure that this method is properly overridden. 286 } 287 288 @Override onAutocompleteTextStateChanged(boolean updateDisplay)289 public void onAutocompleteTextStateChanged(boolean updateDisplay) { 290 assert false; // make sure that this method is properly overridden. 291 } 292 293 @Override isAccessibilityEnabled()294 public boolean isAccessibilityEnabled() { 295 return mAccessibilityManager != null && mAccessibilityManager.isEnabled(); 296 } 297 298 @Override onUpdateSelectionForTesting(int selStart, int selEnd)299 public void onUpdateSelectionForTesting(int selStart, int selEnd) {} 300 301 @Override getKeyboardPackageName()302 public String getKeyboardPackageName() { 303 String defaultIme = Settings.Secure.getString( 304 getContext().getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD); 305 return defaultIme == null ? "" : defaultIme; 306 } 307 } 308