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