OLD | NEW |
(Empty) | |
| 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.os.StrictMode; |
| 10 import android.text.Editable; |
| 11 import android.text.Selection; |
| 12 import android.text.Spanned; |
| 13 import android.text.TextUtils; |
| 14 import android.util.AttributeSet; |
| 15 import android.view.accessibility.AccessibilityEvent; |
| 16 import android.view.accessibility.AccessibilityManager; |
| 17 import android.view.accessibility.AccessibilityNodeInfo; |
| 18 import android.view.inputmethod.BaseInputConnection; |
| 19 import android.view.inputmethod.EditorInfo; |
| 20 import android.view.inputmethod.InputConnection; |
| 21 import android.view.inputmethod.InputConnectionWrapper; |
| 22 import android.widget.EditText; |
| 23 |
| 24 import org.chromium.base.Log; |
| 25 import org.chromium.base.VisibleForTesting; |
| 26 import org.chromium.chrome.browser.widget.VerticallyFixedEditText; |
| 27 |
| 28 /** |
| 29 * An {@link EditText} that shows autocomplete text at the end. |
| 30 */ |
| 31 public class AutocompleteEditText extends VerticallyFixedEditText { |
| 32 private static final String TAG = "cr_AutocompleteEdit"; |
| 33 |
| 34 private static final boolean DEBUG = false; |
| 35 |
| 36 private final AutocompleteSpan mAutocompleteSpan; |
| 37 private final AccessibilityManager mAccessibilityManager; |
| 38 |
| 39 /** |
| 40 * Whether default TextView scrolling should be disabled because autocomplet
e has been added. |
| 41 * This allows the user entered text to be shown instead of the end of the a
utocomplete. |
| 42 */ |
| 43 private boolean mDisableTextScrollingFromAutocomplete; |
| 44 |
| 45 private boolean mInBatchEditMode; |
| 46 private int mBeforeBatchEditAutocompleteIndex = -1; |
| 47 private String mBeforeBatchEditFullText; |
| 48 private boolean mSelectionChangedInBatchMode; |
| 49 private boolean mTextDeletedInBatchMode; |
| 50 |
| 51 // Set to true when the text is modified programmatically. Initially set to
true until the old |
| 52 // state has been loaded. |
| 53 private boolean mIgnoreTextChangeFromAutocomplete = true; |
| 54 private boolean mLastEditWasDelete; |
| 55 |
| 56 public AutocompleteEditText(Context context, AttributeSet attrs) { |
| 57 super(context, attrs); |
| 58 mAutocompleteSpan = new AutocompleteSpan(); |
| 59 mAccessibilityManager = |
| 60 (AccessibilityManager) context.getSystemService(Context.ACCESSIB
ILITY_SERVICE); |
| 61 } |
| 62 |
| 63 /** |
| 64 * Sets whether text changes should trigger autocomplete. |
| 65 * |
| 66 * @param ignoreAutocomplete Whether text changes should be ignored and no a
uto complete |
| 67 * triggered. |
| 68 */ |
| 69 public void setIgnoreTextChangesForAutocomplete(boolean ignoreAutocomplete)
{ |
| 70 if (DEBUG) Log.i(TAG, "setIgnoreTextChangesForAutocomplete: " + ignoreAu
tocomplete); |
| 71 mIgnoreTextChangeFromAutocomplete = ignoreAutocomplete; |
| 72 } |
| 73 |
| 74 /** @return Text that includes autocomplete. */ |
| 75 public String getTextWithAutocomplete() { |
| 76 return getEditableText() != null ? getEditableText().toString() : ""; |
| 77 } |
| 78 |
| 79 /** |
| 80 * @return Whether the current cursor position is at the end of the user typ
ed text (i.e. |
| 81 * at the beginning of the inline autocomplete text if present other
wise the very |
| 82 * end of the current text). |
| 83 */ |
| 84 private boolean isCursorAtEndOfTypedText() { |
| 85 final int selectionStart = getSelectionStart(); |
| 86 final int selectionEnd = getSelectionEnd(); |
| 87 |
| 88 int expectedSelectionStart = getText().getSpanStart(mAutocompleteSpan); |
| 89 int expectedSelectionEnd = getText().length(); |
| 90 if (expectedSelectionStart < 0) { |
| 91 expectedSelectionStart = expectedSelectionEnd; |
| 92 } |
| 93 |
| 94 return selectionStart == expectedSelectionStart && selectionEnd == expec
tedSelectionEnd; |
| 95 } |
| 96 |
| 97 /** |
| 98 * @return Whether the URL is currently in batch edit mode triggered by an I
ME. No external |
| 99 * text changes should be triggered while this is true. |
| 100 */ |
| 101 // isInBatchEditMode is a package protected method on TextView, so we intent
ionally chose |
| 102 // a different name. |
| 103 private boolean isHandlingBatchInput() { |
| 104 return mInBatchEditMode; |
| 105 } |
| 106 |
| 107 /** |
| 108 * @return The user text without the autocomplete text. |
| 109 */ |
| 110 public String getTextWithoutAutocomplete() { |
| 111 int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan); |
| 112 if (autoCompleteIndex < 0) { |
| 113 return getTextWithAutocomplete(); |
| 114 } else { |
| 115 return getTextWithAutocomplete().substring(0, autoCompleteIndex); |
| 116 } |
| 117 } |
| 118 |
| 119 /** @return Whether any autocomplete information is specified on the current
text. */ |
| 120 @VisibleForTesting |
| 121 public boolean hasAutocomplete() { |
| 122 return getText().getSpanStart(mAutocompleteSpan) >= 0 |
| 123 || mAutocompleteSpan.mAutocompleteText != null |
| 124 || mAutocompleteSpan.mUserText != null; |
| 125 } |
| 126 |
| 127 /** |
| 128 * Whether we want to be showing inline autocomplete results. We don't want
to show them as the |
| 129 * user deletes input. Also if there is a composition (e.g. while using the
Japanese IME), |
| 130 * we must not autocomplete or we'll destroy the composition. |
| 131 * @return Whether we want to be showing inline autocomplete results. |
| 132 */ |
| 133 public boolean shouldAutocomplete() { |
| 134 if (mLastEditWasDelete) return false; |
| 135 Editable text = getText(); |
| 136 |
| 137 return isCursorAtEndOfTypedText() && !isHandlingBatchInput() |
| 138 && BaseInputConnection.getComposingSpanEnd(text) |
| 139 == BaseInputConnection.getComposingSpanStart(text); |
| 140 } |
| 141 |
| 142 @Override |
| 143 public void onBeginBatchEdit() { |
| 144 if (DEBUG) Log.i(TAG, "onBeginBatchEdit"); |
| 145 mBeforeBatchEditAutocompleteIndex = getText().getSpanStart(mAutocomplete
Span); |
| 146 mBeforeBatchEditFullText = getText().toString(); |
| 147 |
| 148 super.onBeginBatchEdit(); |
| 149 mInBatchEditMode = true; |
| 150 mTextDeletedInBatchMode = false; |
| 151 } |
| 152 |
| 153 @Override |
| 154 public void onEndBatchEdit() { |
| 155 if (DEBUG) Log.i(TAG, "onEndBatchEdit"); |
| 156 super.onEndBatchEdit(); |
| 157 mInBatchEditMode = false; |
| 158 if (mSelectionChangedInBatchMode) { |
| 159 validateSelection(getSelectionStart(), getSelectionEnd()); |
| 160 mSelectionChangedInBatchMode = false; |
| 161 } |
| 162 |
| 163 String newText = getText().toString(); |
| 164 if (!TextUtils.equals(mBeforeBatchEditFullText, newText) |
| 165 || getText().getSpanStart(mAutocompleteSpan) != mBeforeBatchEdit
AutocompleteIndex) { |
| 166 // If the text being typed is a single character that matches the ne
xt character in the |
| 167 // previously visible autocomplete text, we reapply the autocomplete
text to prevent |
| 168 // a visual flickering when the autocomplete text is cleared and the
n quickly reapplied |
| 169 // when the next round of suggestions is received. |
| 170 if (shouldAutocomplete() && mBeforeBatchEditAutocompleteIndex != -1 |
| 171 && mBeforeBatchEditFullText != null |
| 172 && mBeforeBatchEditFullText.startsWith(newText) && !mTextDel
etedInBatchMode |
| 173 && newText.length() - mBeforeBatchEditAutocompleteIndex == 1
) { |
| 174 setAutocompleteText(newText, mBeforeBatchEditFullText.substring(
newText.length())); |
| 175 } |
| 176 notifyAutocompleteTextStateChanged(mTextDeletedInBatchMode, true); |
| 177 } |
| 178 |
| 179 mTextDeletedInBatchMode = false; |
| 180 mBeforeBatchEditAutocompleteIndex = -1; |
| 181 mBeforeBatchEditFullText = null; |
| 182 } |
| 183 |
| 184 @Override |
| 185 protected void onSelectionChanged(int selStart, int selEnd) { |
| 186 if (DEBUG) Log.i(TAG, "onSelectionChanged -- selStart: %d, selEnd: %d",
selStart, selEnd); |
| 187 if (!mInBatchEditMode) { |
| 188 int beforeTextLength = getText().length(); |
| 189 if (validateSelection(selStart, selEnd)) { |
| 190 boolean textDeleted = getText().length() < beforeTextLength; |
| 191 notifyAutocompleteTextStateChanged(textDeleted, false); |
| 192 } |
| 193 } else { |
| 194 mSelectionChangedInBatchMode = true; |
| 195 } |
| 196 super.onSelectionChanged(selStart, selEnd); |
| 197 } |
| 198 |
| 199 /** |
| 200 * Validates the selection and clears the autocomplete span if needed. The
autocomplete text |
| 201 * will be deleted if the selection occurs entirely before the autocomplete
region. |
| 202 * |
| 203 * @param selStart The start of the selection. |
| 204 * @param selEnd The end of the selection. |
| 205 * @return Whether the autocomplete span was removed as a result of this val
idation. |
| 206 */ |
| 207 private boolean validateSelection(int selStart, int selEnd) { |
| 208 int spanStart = getText().getSpanStart(mAutocompleteSpan); |
| 209 int spanEnd = getText().getSpanEnd(mAutocompleteSpan); |
| 210 |
| 211 if (DEBUG) { |
| 212 Log.i(TAG, "validateSelection -- selStart: %d, selEnd: %d, spanStart
: %d, spanEnd: %d", |
| 213 selStart, selEnd, spanStart, spanEnd); |
| 214 } |
| 215 |
| 216 if (spanStart >= 0 && (spanStart != selStart || spanEnd != selEnd)) { |
| 217 CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompl
eteText; |
| 218 |
| 219 // On selection changes, the autocomplete text has been accepted by
the user or needs |
| 220 // to be deleted below. |
| 221 mAutocompleteSpan.clearSpan(); |
| 222 |
| 223 // The autocomplete text will be deleted any time the selection occu
rs entirely before |
| 224 // the start of the autocomplete text. This is required because cer
tain keyboards will |
| 225 // insert characters temporarily when starting a key entry gesture (
whether it be |
| 226 // swyping a word or long pressing to get a special character). Whe
n this temporary |
| 227 // character appears, Chrome may decide to append some autocomplete,
but the keyboard |
| 228 // will then remove this temporary character only while leaving the
autocomplete text |
| 229 // alone. See crbug/273763 for more details. |
| 230 if (selEnd <= spanStart |
| 231 && TextUtils.equals(previousAutocompleteText, |
| 232 getText().subSequence(spanStart, getText().length
()))) { |
| 233 getText().delete(spanStart, getText().length()); |
| 234 } |
| 235 return true; |
| 236 } |
| 237 return false; |
| 238 } |
| 239 |
| 240 @Override |
| 241 protected void onFocusChanged(boolean focused, int direction, Rect previousl
yFocusedRect) { |
| 242 if (!focused) mAutocompleteSpan.clearSpan(); |
| 243 super.onFocusChanged(focused, direction, previouslyFocusedRect); |
| 244 } |
| 245 |
| 246 @Override |
| 247 public boolean bringPointIntoView(int offset) { |
| 248 if (mDisableTextScrollingFromAutocomplete) return false; |
| 249 return super.bringPointIntoView(offset); |
| 250 } |
| 251 |
| 252 @Override |
| 253 public boolean onPreDraw() { |
| 254 boolean retVal = super.onPreDraw(); |
| 255 if (mDisableTextScrollingFromAutocomplete) { |
| 256 // super.onPreDraw will put the selection at the end of the text sel
ection, but |
| 257 // in the case of autocomplete we want the last typed character to b
e shown, which |
| 258 // is the start of selection. |
| 259 mDisableTextScrollingFromAutocomplete = false; |
| 260 bringPointIntoView(getSelectionStart()); |
| 261 retVal = true; |
| 262 } |
| 263 return retVal; |
| 264 } |
| 265 |
| 266 /** |
| 267 * Autocompletes the text on the url bar and selects the text that was not e
ntered by the |
| 268 * user. Using append() instead of setText() to preserve the soft-keyboard l
ayout. |
| 269 * @param userText user The text entered by the user. |
| 270 * @param inlineAutocompleteText The suggested autocompletion for the user's
text. |
| 271 */ |
| 272 public void setAutocompleteText(CharSequence userText, CharSequence inlineAu
tocompleteText) { |
| 273 if (DEBUG) { |
| 274 Log.i(TAG, "setAutocompleteText -- userText: %s, inlineAutocompleteT
ext: %s", userText, |
| 275 inlineAutocompleteText); |
| 276 } |
| 277 boolean emptyAutocomplete = TextUtils.isEmpty(inlineAutocompleteText); |
| 278 |
| 279 if (!emptyAutocomplete) mDisableTextScrollingFromAutocomplete = true; |
| 280 |
| 281 int autocompleteIndex = userText.length(); |
| 282 |
| 283 String previousText = getTextWithAutocomplete(); |
| 284 CharSequence newText = TextUtils.concat(userText, inlineAutocompleteText
); |
| 285 |
| 286 setIgnoreTextChangesForAutocomplete(true); |
| 287 |
| 288 if (!TextUtils.equals(previousText, newText)) { |
| 289 // The previous text may also have included autocomplete text, so we
only |
| 290 // append the new autocomplete text that has changed. |
| 291 if (TextUtils.indexOf(newText, previousText) == 0) { |
| 292 append(newText.subSequence(previousText.length(), newText.length
())); |
| 293 } else { |
| 294 replaceAllTextFromAutocomplete(newText.toString()); |
| 295 } |
| 296 } |
| 297 |
| 298 if (getSelectionStart() != autocompleteIndex || getSelectionEnd() != get
Text().length()) { |
| 299 setSelection(autocompleteIndex, getText().length()); |
| 300 |
| 301 if (inlineAutocompleteText.length() != 0) { |
| 302 // Sending a TYPE_VIEW_TEXT_SELECTION_CHANGED accessibility even
t causes the |
| 303 // previous TYPE_VIEW_TEXT_CHANGED event to be swallowed. As a r
esult the user |
| 304 // hears the autocomplete text but *not* the text they typed. In
stead we send a |
| 305 // TYPE_ANNOUNCEMENT event, which doesn't swallow the text-chang
ed event. |
| 306 announceForAccessibility(inlineAutocompleteText); |
| 307 } |
| 308 } |
| 309 |
| 310 if (emptyAutocomplete) { |
| 311 mAutocompleteSpan.clearSpan(); |
| 312 } else { |
| 313 mAutocompleteSpan.setSpan(userText, inlineAutocompleteText); |
| 314 } |
| 315 |
| 316 setIgnoreTextChangesForAutocomplete(false); |
| 317 } |
| 318 |
| 319 /** |
| 320 * Returns the length of the autocomplete text currently displayed, zero if
none is |
| 321 * currently displayed. |
| 322 */ |
| 323 public int getAutocompleteLength() { |
| 324 int autoCompleteIndex = getText().getSpanStart(mAutocompleteSpan); |
| 325 if (autoCompleteIndex < 0) return 0; |
| 326 return getText().length() - autoCompleteIndex; |
| 327 } |
| 328 |
| 329 @Override |
| 330 protected void onTextChanged(CharSequence text, int start, int lengthBefore,
int lengthAfter) { |
| 331 if (DEBUG) { |
| 332 Log.i(TAG, "onTextChanged -- text: %s, start: %d, lengthBefore: %d,
lengthAfter: %d", |
| 333 text, start, lengthBefore, lengthAfter); |
| 334 } |
| 335 |
| 336 super.onTextChanged(text, start, lengthBefore, lengthAfter); |
| 337 boolean textDeleted = lengthAfter == 0; |
| 338 if (!mInBatchEditMode) { |
| 339 notifyAutocompleteTextStateChanged(textDeleted, true); |
| 340 } else { |
| 341 mTextDeletedInBatchMode = textDeleted; |
| 342 } |
| 343 } |
| 344 |
| 345 @Override |
| 346 public void setText(CharSequence text, BufferType type) { |
| 347 if (DEBUG) Log.i(TAG, "setText -- text: %s", text); |
| 348 |
| 349 mDisableTextScrollingFromAutocomplete = false; |
| 350 |
| 351 // Avoid setting the same text to the URL bar as it will mess up the scr
oll/cursor |
| 352 // position. |
| 353 // Setting the text is also quite expensive, so only do it when the text
has changed |
| 354 // (since we apply spans when the URL is not focused, we only optimize t
his when the |
| 355 // URL is being edited). |
| 356 if (!TextUtils.equals(getEditableText(), text)) { |
| 357 // Certain OEM implementations of setText trigger disk reads. crbug.
com/633298 |
| 358 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(
); |
| 359 try { |
| 360 super.setText(text, type); |
| 361 } finally { |
| 362 StrictMode.setThreadPolicy(oldPolicy); |
| 363 } |
| 364 } |
| 365 |
| 366 // Verify the autocomplete is still valid after the text change. |
| 367 // Note: mAutocompleteSpan may be still null here if setText() is called
in View |
| 368 // constructor. |
| 369 if (mAutocompleteSpan != null && mAutocompleteSpan.mUserText != null |
| 370 && mAutocompleteSpan.mAutocompleteText != null) { |
| 371 if (getText().getSpanStart(mAutocompleteSpan) < 0) { |
| 372 mAutocompleteSpan.clearSpan(); |
| 373 } else { |
| 374 clearAutocompleteSpanIfInvalid(); |
| 375 } |
| 376 } |
| 377 } |
| 378 |
| 379 private void clearAutocompleteSpanIfInvalid() { |
| 380 Editable editableText = getEditableText(); |
| 381 CharSequence previousUserText = mAutocompleteSpan.mUserText; |
| 382 CharSequence previousAutocompleteText = mAutocompleteSpan.mAutocompleteT
ext; |
| 383 if (editableText.length() |
| 384 != (previousUserText.length() + previousAutocompleteText.length(
))) { |
| 385 mAutocompleteSpan.clearSpan(); |
| 386 } else if (TextUtils.indexOf(getText(), previousUserText) != 0 |
| 387 || TextUtils.indexOf(getText(), previousAutocompleteText, previo
usUserText.length()) |
| 388 != 0) { |
| 389 mAutocompleteSpan.clearSpan(); |
| 390 } |
| 391 } |
| 392 |
| 393 @Override |
| 394 public void sendAccessibilityEventUnchecked(AccessibilityEvent event) { |
| 395 if (mIgnoreTextChangeFromAutocomplete) { |
| 396 if (event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT_SELECT
ION_CHANGED |
| 397 || event.getEventType() == AccessibilityEvent.TYPE_VIEW_TEXT
_CHANGED) { |
| 398 return; |
| 399 } |
| 400 } |
| 401 super.sendAccessibilityEventUnchecked(event); |
| 402 } |
| 403 |
| 404 @Override |
| 405 public void onInitializeAccessibilityNodeInfo(AccessibilityNodeInfo info) { |
| 406 // Certain OEM implementations of onInitializeAccessibilityNodeInfo trig
ger disk reads |
| 407 // to access the clipboard. crbug.com/640993 |
| 408 StrictMode.ThreadPolicy oldPolicy = StrictMode.allowThreadDiskReads(); |
| 409 try { |
| 410 super.onInitializeAccessibilityNodeInfo(info); |
| 411 } finally { |
| 412 StrictMode.setThreadPolicy(oldPolicy); |
| 413 } |
| 414 } |
| 415 |
| 416 @VisibleForTesting |
| 417 public InputConnectionWrapper getInputConnection() { |
| 418 return mInputConnection; |
| 419 } |
| 420 |
| 421 private InputConnectionWrapper mInputConnection = new InputConnectionWrapper
(null, true) { |
| 422 private final char[] mTempSelectionChar = new char[1]; |
| 423 |
| 424 @Override |
| 425 public boolean commitText(CharSequence text, int newCursorPosition) { |
| 426 if (DEBUG) Log.i(TAG, "commitText: [%s]", text); |
| 427 Editable currentText = getText(); |
| 428 if (currentText == null) return super.commitText(text, newCursorPosi
tion); |
| 429 |
| 430 int selectionStart = Selection.getSelectionStart(currentText); |
| 431 int selectionEnd = Selection.getSelectionEnd(currentText); |
| 432 int autocompleteIndex = currentText.getSpanStart(mAutocompleteSpan); |
| 433 // If the text being committed is a single character that matches th
e next character |
| 434 // in the selection (assumed to be the autocomplete text), we only m
ove the text |
| 435 // selection instead clearing the autocomplete text causing flickeri
ng as the |
| 436 // autocomplete text will appear once the next suggestions are recei
ved. |
| 437 // |
| 438 // To be confident that the selection is an autocomplete, we ensure
the selection |
| 439 // is at least one character and the end of the selection is the end
of the |
| 440 // currently entered text. |
| 441 if (newCursorPosition == 1 && selectionStart > 0 && selectionStart !
= selectionEnd |
| 442 && selectionEnd >= currentText.length() && autocompleteIndex
== selectionStart |
| 443 && text.length() == 1) { |
| 444 currentText.getChars(selectionStart, selectionStart + 1, mTempSe
lectionChar, 0); |
| 445 if (mTempSelectionChar[0] == text.charAt(0)) { |
| 446 // Since the text isn't changing, TalkBack won't read out th
e typed characters. |
| 447 // To work around this, explicitly send an accessibility eve
nt. crbug.com/416595 |
| 448 if (mAccessibilityManager != null && mAccessibilityManager.i
sEnabled()) { |
| 449 AccessibilityEvent event = AccessibilityEvent.obtain( |
| 450 AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); |
| 451 event.setFromIndex(selectionStart); |
| 452 event.setRemovedCount(0); |
| 453 event.setAddedCount(1); |
| 454 event.setBeforeText(currentText.toString().substring(0,
selectionStart)); |
| 455 sendAccessibilityEventUnchecked(event); |
| 456 } |
| 457 |
| 458 setAutocompleteText(currentText.subSequence(0, selectionStar
t + 1), |
| 459 currentText.subSequence(selectionStart + 1, selectio
nEnd)); |
| 460 if (!mInBatchEditMode) { |
| 461 notifyAutocompleteTextStateChanged(false, false); |
| 462 } |
| 463 return true; |
| 464 } |
| 465 } |
| 466 |
| 467 boolean retVal = super.commitText(text, newCursorPosition); |
| 468 |
| 469 // Ensure the autocomplete span is removed if it is no longer valid
after committing the |
| 470 // text. |
| 471 if (getText().getSpanStart(mAutocompleteSpan) >= 0) clearAutocomplet
eSpanIfInvalid(); |
| 472 |
| 473 return retVal; |
| 474 } |
| 475 |
| 476 @Override |
| 477 public boolean setComposingText(CharSequence text, int newCursorPosition
) { |
| 478 if (DEBUG) Log.i(TAG, "setComposingText: [%s]", text); |
| 479 Editable currentText = getText(); |
| 480 int autoCompleteSpanStart = currentText.getSpanStart(mAutocompleteSp
an); |
| 481 if (autoCompleteSpanStart >= 0) { |
| 482 int composingEnd = BaseInputConnection.getComposingSpanEnd(curre
ntText); |
| 483 |
| 484 // On certain device/keyboard combinations, the composing region
s are specified |
| 485 // with a noticeable delay after the initial character is typed,
and in certain |
| 486 // circumstances it does not check that the current state of the
text matches the |
| 487 // expectations of it's composing region. |
| 488 // For example, you can be typing: |
| 489 // chrome://f |
| 490 // Chrome will autocomplete to: |
| 491 // chrome://f[lags] |
| 492 // And after the autocomplete has been set, the keyboard will se
t the composing |
| 493 // region to the last character and it assumes it is 'f' as it w
as the last |
| 494 // character the keyboard sent. If we commit this composition,
the text will |
| 495 // look like: |
| 496 // chrome://flag[f] |
| 497 // And if we use the autocomplete clearing logic below, it will
look like: |
| 498 // chrome://f[f] |
| 499 // To work around this, we see if the composition matches all th
e characters prior |
| 500 // to the autocomplete and just readjust the composing region to
be that subset. |
| 501 // |
| 502 // See crbug.com/366732 |
| 503 if (composingEnd == currentText.length() && autoCompleteSpanStar
t >= text.length() |
| 504 && TextUtils.equals( |
| 505 currentText.subSequence(autoCompleteSpanStart
- text.length(), |
| 506 autoCompleteSpanStart), |
| 507 text)) { |
| 508 setComposingRegion( |
| 509 autoCompleteSpanStart - text.length(), autoCompleteS
panStart); |
| 510 } |
| 511 |
| 512 // Once composing text is being modified, the autocomplete text
has been accepted |
| 513 // or has to be deleted. |
| 514 mAutocompleteSpan.clearSpan(); |
| 515 Selection.setSelection(currentText, autoCompleteSpanStart); |
| 516 currentText.delete(autoCompleteSpanStart, currentText.length()); |
| 517 } |
| 518 return super.setComposingText(text, newCursorPosition); |
| 519 } |
| 520 }; |
| 521 |
| 522 @Override |
| 523 public InputConnection onCreateInputConnection(EditorInfo outAttrs) { |
| 524 if (DEBUG) Log.i(TAG, "onCreateInputConnection"); |
| 525 return createInputConnection(super.onCreateInputConnection(outAttrs)); |
| 526 } |
| 527 |
| 528 @VisibleForTesting |
| 529 public InputConnection createInputConnection(InputConnection target) { |
| 530 mInputConnection.setTarget(target); |
| 531 return mInputConnection; |
| 532 } |
| 533 |
| 534 private void notifyAutocompleteTextStateChanged(boolean textDeleted, boolean
updateDisplay) { |
| 535 if (DEBUG) { |
| 536 Log.i(TAG, "notifyAutocompleteTextStateChanged: DEL[%b] DIS[%b] IGN[
%b]", textDeleted, |
| 537 updateDisplay, mIgnoreTextChangeFromAutocomplete); |
| 538 } |
| 539 if (mIgnoreTextChangeFromAutocomplete) return; |
| 540 if (!hasFocus()) return; |
| 541 mLastEditWasDelete = textDeleted; |
| 542 onAutocompleteTextStateChanged(textDeleted, updateDisplay); |
| 543 } |
| 544 |
| 545 /** |
| 546 * This is called when autocomplete replaces the whole text. |
| 547 * |
| 548 * @param text The text. |
| 549 */ |
| 550 protected void replaceAllTextFromAutocomplete(String text) { |
| 551 setText(text); |
| 552 } |
| 553 |
| 554 /** |
| 555 * This is called when autocomplete text state changes. |
| 556 * @param textDeleted True if text is just deleted. |
| 557 * @param updateDisplay True if string is changed. |
| 558 */ |
| 559 public void onAutocompleteTextStateChanged(boolean textDeleted, boolean upda
teDisplay) {} |
| 560 |
| 561 /** |
| 562 * Simple span used for tracking the current autocomplete state. |
| 563 */ |
| 564 private class AutocompleteSpan { |
| 565 private CharSequence mUserText; |
| 566 private CharSequence mAutocompleteText; |
| 567 |
| 568 /** |
| 569 * Adds the span to the current text. |
| 570 * @param userText The user entered text. |
| 571 * @param autocompleteText The autocomplete text being appended. |
| 572 */ |
| 573 public void setSpan(CharSequence userText, CharSequence autocompleteText
) { |
| 574 Editable text = getText(); |
| 575 text.removeSpan(this); |
| 576 mAutocompleteText = autocompleteText; |
| 577 mUserText = userText; |
| 578 text.setSpan(this, userText.length(), text.length(), Spanned.SPAN_EX
CLUSIVE_EXCLUSIVE); |
| 579 } |
| 580 |
| 581 /** Removes this span from the current text and clears the internal stat
e. */ |
| 582 public void clearSpan() { |
| 583 getText().removeSpan(this); |
| 584 mAutocompleteText = null; |
| 585 mUserText = null; |
| 586 } |
| 587 } |
| 588 } |
OLD | NEW |