Index: chrome/android/java/src/org/chromium/chrome/browser/omnibox/AutocompleteEditText.java |
diff --git a/chrome/android/java/src/org/chromium/chrome/browser/omnibox/AutocompleteEditText.java b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/AutocompleteEditText.java |
new file mode 100644 |
index 0000000000000000000000000000000000000000..1c02a66a87ab0e9236080534ef8fb132d12ee6eb |
--- /dev/null |
+++ b/chrome/android/java/src/org/chromium/chrome/browser/omnibox/AutocompleteEditText.java |
@@ -0,0 +1,588 @@ |
+// 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.os.StrictMode; |
+import android.text.Editable; |
+import android.text.Selection; |
+import android.text.Spanned; |
+import android.text.TextUtils; |
+import android.util.AttributeSet; |
+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 android.widget.EditText; |
+ |
+import org.chromium.base.Log; |
+import org.chromium.base.VisibleForTesting; |
+import org.chromium.chrome.browser.widget.VerticallyFixedEditText; |
+ |
+/** |
+ * An {@link EditText} that shows autocomplete text at the end. |
+ */ |
+public class AutocompleteEditText extends VerticallyFixedEditText { |
+ private static final String TAG = "cr_AutocompleteEdit"; |
+ |
+ private static final boolean DEBUG = false; |
+ |
+ private final AutocompleteSpan mAutocompleteSpan; |
+ private final AccessibilityManager mAccessibilityManager; |
+ |
+ /** |
+ * 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 mInBatchEditMode; |
+ private int mBeforeBatchEditAutocompleteIndex = -1; |
+ private String mBeforeBatchEditFullText; |
+ private boolean mSelectionChangedInBatchMode; |
+ private boolean mTextDeletedInBatchMode; |
+ |
+ // Set to true when the text is modified programmatically. Initially set to true until the old |
+ // state has been loaded. |
+ private boolean mIgnoreTextChangeFromAutocomplete = true; |
+ private boolean mLastEditWasDelete; |
+ |
+ public AutocompleteEditText(Context context, AttributeSet attrs) { |
+ super(context, attrs); |
+ mAutocompleteSpan = new AutocompleteSpan(); |
+ mAccessibilityManager = |
+ (AccessibilityManager) context.getSystemService(Context.ACCESSIBILITY_SERVICE); |
+ } |
+ |
+ /** |
+ * 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) { |
+ if (DEBUG) Log.i(TAG, "setIgnoreTextChangesForAutocomplete: " + ignoreAutocomplete); |
+ mIgnoreTextChangeFromAutocomplete = ignoreAutocomplete; |
+ } |
+ |
+ /** @return Text that includes autocomplete. */ |
+ public String getTextWithAutocomplete() { |
+ 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). |
+ */ |
+ private 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. |
+ private boolean isHandlingBatchInput() { |
+ return mInBatchEditMode; |
+ } |
+ |
+ /** |
+ * @return The user text without the autocomplete text. |
+ */ |
+ public String getTextWithoutAutocomplete() { |
+ int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan); |
+ if (autoCompleteIndex < 0) { |
+ return getTextWithAutocomplete(); |
+ } else { |
+ return getTextWithAutocomplete().substring(0, autoCompleteIndex); |
+ } |
+ } |
+ |
+ /** @return Whether any autocomplete information is specified on the current text. */ |
+ @VisibleForTesting |
+ public 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 (mLastEditWasDelete) return false; |
+ Editable text = getText(); |
+ |
+ return isCursorAtEndOfTypedText() && !isHandlingBatchInput() |
+ && BaseInputConnection.getComposingSpanEnd(text) |
+ == BaseInputConnection.getComposingSpanStart(text); |
+ } |
+ |
+ @Override |
+ public void onBeginBatchEdit() { |
+ if (DEBUG) Log.i(TAG, "onBeginBatchEdit"); |
+ mBeforeBatchEditAutocompleteIndex = getText().getSpanStart(mAutocompleteSpan); |
+ mBeforeBatchEditFullText = getText().toString(); |
+ |
+ super.onBeginBatchEdit(); |
+ mInBatchEditMode = true; |
+ mTextDeletedInBatchMode = false; |
+ } |
+ |
+ @Override |
+ public void onEndBatchEdit() { |
+ if (DEBUG) Log.i(TAG, "onEndBatchEdit"); |
+ super.onEndBatchEdit(); |
+ mInBatchEditMode = false; |
+ 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, true); |
+ } |
+ |
+ mTextDeletedInBatchMode = false; |
+ mBeforeBatchEditAutocompleteIndex = -1; |
+ mBeforeBatchEditFullText = null; |
+ } |
+ |
+ @Override |
+ protected void onSelectionChanged(int selStart, int selEnd) { |
+ if (DEBUG) Log.i(TAG, "onSelectionChanged -- selStart: %d, selEnd: %d", selStart, selEnd); |
+ if (!mInBatchEditMode) { |
+ int beforeTextLength = getText().length(); |
+ if (validateSelection(selStart, selEnd)) { |
+ boolean textDeleted = getText().length() < beforeTextLength; |
+ notifyAutocompleteTextStateChanged(textDeleted, false); |
+ } |
+ } 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 (DEBUG) { |
+ Log.i(TAG, "validateSelection -- selStart: %d, selEnd: %d, spanStart: %d, spanEnd: %d", |
+ selStart, selEnd, spanStart, spanEnd); |
+ } |
+ |
+ if (spanStart >= 0 && (spanStart != selStart || spanEnd != selEnd)) { |
+ CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompleteText; |
+ |
+ // 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 |
+ && TextUtils.equals(previousAutocompleteText, |
+ getText().subSequence(spanStart, getText().length()))) { |
+ getText().delete(spanStart, getText().length()); |
+ } |
+ return true; |
+ } |
+ return false; |
+ } |
+ |
+ @Override |
+ protected void onFocusChanged(boolean focused, int direction, Rect previouslyFocusedRect) { |
+ if (!focused) mAutocompleteSpan.clearSpan(); |
+ super.onFocusChanged(focused, direction, previouslyFocusedRect); |
+ } |
+ |
+ @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; |
+ } |
+ |
+ /** |
+ * 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) { |
+ if (DEBUG) { |
+ Log.i(TAG, "setAutocompleteText -- userText: %s, inlineAutocompleteText: %s", userText, |
+ inlineAutocompleteText); |
+ } |
+ boolean emptyAutocomplete = TextUtils.isEmpty(inlineAutocompleteText); |
+ |
+ if (!emptyAutocomplete) mDisableTextScrollingFromAutocomplete = true; |
+ |
+ int autocompleteIndex = userText.length(); |
+ |
+ String previousText = getTextWithAutocomplete(); |
+ CharSequence newText = TextUtils.concat(userText, inlineAutocompleteText); |
+ |
+ setIgnoreTextChangesForAutocomplete(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 { |
+ replaceAllTextFromAutocomplete(newText.toString()); |
+ } |
+ } |
+ |
+ 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); |
+ } |
+ |
+ /** |
+ * 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; |
+ } |
+ |
+ @Override |
+ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { |
+ if (DEBUG) { |
+ Log.i(TAG, "onTextChanged -- text: %s, start: %d, lengthBefore: %d, lengthAfter: %d", |
+ text, start, lengthBefore, lengthAfter); |
+ } |
+ |
+ super.onTextChanged(text, start, lengthBefore, lengthAfter); |
+ boolean textDeleted = lengthAfter == 0; |
+ if (!mInBatchEditMode) { |
+ notifyAutocompleteTextStateChanged(textDeleted, true); |
+ } else { |
+ mTextDeletedInBatchMode = textDeleted; |
+ } |
+ } |
+ |
+ @Override |
+ public void setText(CharSequence text, BufferType type) { |
+ if (DEBUG) Log.i(TAG, "setText -- text: %s", text); |
+ |
+ 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)) { |
+ // Certain OEM implementations of setText trigger disk reads. crbug.com/633298 |
+ StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); |
+ try { |
+ super.setText(text, type); |
+ } finally { |
+ StrictMode.setThreadPolicy(oldPolicy); |
+ } |
+ } |
+ |
+ // Verify the autocomplete is still valid after the text change. |
+ // Note: mAutocompleteSpan may be still null here if setText() is called in View |
+ // constructor. |
+ if (mAutocompleteSpan != null && mAutocompleteSpan.mUserText != null |
+ && mAutocompleteSpan.mAutocompleteText != null) { |
+ if (getText().getSpanStart(mAutocompleteSpan) < 0) { |
+ mAutocompleteSpan.clearSpan(); |
+ } else { |
+ clearAutocompleteSpanIfInvalid(); |
+ } |
+ } |
+ } |
+ |
+ 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(); |
+ } |
+ } |
+ |
+ @Override |
+ public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { |
+ if (mIgnoreTextChangeFromAutocomplete) { |
+ 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); |
+ } |
+ } |
+ |
+ @VisibleForTesting |
+ public InputConnectionWrapper getInputConnection() { |
+ return mInputConnection; |
+ } |
+ |
+ private InputConnectionWrapper mInputConnection = new InputConnectionWrapper(null, true) { |
+ private final char[] mTempSelectionChar = new char[1]; |
+ |
+ @Override |
+ public boolean commitText(CharSequence text, int newCursorPosition) { |
+ if (DEBUG) Log.i(TAG, "commitText: [%s]", text); |
+ 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, 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) { |
+ if (DEBUG) Log.i(TAG, "setComposingText: [%s]", text); |
+ 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) { |
+ if (DEBUG) Log.i(TAG, "onCreateInputConnection"); |
+ return createInputConnection(super.onCreateInputConnection(outAttrs)); |
+ } |
+ |
+ @VisibleForTesting |
+ public InputConnection createInputConnection(InputConnection target) { |
+ mInputConnection.setTarget(target); |
+ return mInputConnection; |
+ } |
+ |
+ private void notifyAutocompleteTextStateChanged(boolean textDeleted, boolean updateDisplay) { |
+ if (DEBUG) { |
+ Log.i(TAG, "notifyAutocompleteTextStateChanged: DEL[%b] DIS[%b] IGN[%b]", textDeleted, |
+ updateDisplay, mIgnoreTextChangeFromAutocomplete); |
+ } |
+ if (mIgnoreTextChangeFromAutocomplete) return; |
+ if (!hasFocus()) return; |
+ mLastEditWasDelete = textDeleted; |
+ onAutocompleteTextStateChanged(textDeleted, updateDisplay); |
+ } |
+ |
+ /** |
+ * This is called when autocomplete replaces the whole text. |
+ * |
+ * @param text The text. |
+ */ |
+ protected void replaceAllTextFromAutocomplete(String text) { |
+ setText(text); |
+ } |
+ |
+ /** |
+ * This is called when autocomplete text state changes. |
+ * @param textDeleted True if text is just deleted. |
+ * @param updateDisplay True if string is changed. |
+ */ |
+ public void onAutocompleteTextStateChanged(boolean textDeleted, boolean updateDisplay) {} |
+ |
+ /** |
+ * 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; |
+ } |
+ } |
+} |