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.os.Bundle; 8 import android.text.Editable; 9 import android.text.Selection; 10 import android.text.SpannableString; 11 import android.text.Spanned; 12 import android.text.style.BackgroundColorSpan; 13 import android.view.KeyEvent; 14 import android.view.accessibility.AccessibilityEvent; 15 import android.view.inputmethod.BaseInputConnection; 16 import android.view.inputmethod.CompletionInfo; 17 import android.view.inputmethod.CorrectionInfo; 18 import android.view.inputmethod.ExtractedText; 19 import android.view.inputmethod.ExtractedTextRequest; 20 import android.view.inputmethod.InputConnection; 21 import android.view.inputmethod.InputConnectionWrapper; 22 import android.view.inputmethod.InputContentInfo; 23 24 import androidx.annotation.VisibleForTesting; 25 26 import org.chromium.base.Log; 27 28 import java.util.Locale; 29 import java.util.regex.Pattern; 30 31 /** 32 * An autocomplete model that appends autocomplete text at the end of query/URL text as 33 * SpannableString. By wrapping all the keyboard related operations in a batch edit, we can 34 * effectively hide the existence of autocomplete text from keyboard. 35 */ 36 public class SpannableAutocompleteEditTextModel implements AutocompleteEditTextModelBase { 37 private static final String TAG = "SpanAutocomplete"; 38 39 private static final boolean DEBUG = false; 40 41 // A pattern that matches strings consisting of English and European character sets, numbers, 42 // punctuations, and a white space. 43 private static final Pattern NON_COMPOSITIONAL_TEXT_PATTERN = Pattern.compile( 44 "[\\p{script=latin}\\p{script=cyrillic}\\p{script=greek}\\p{script=hebrew}\\p{Punct} " 45 + "0-9]*"); 46 47 private final AutocompleteEditTextModelBase.Delegate mDelegate; 48 49 // The current state that reflects EditText view's current state through callbacks such as 50 // onSelectionChanged() and onTextChanged(). It reflects all the latest changes even in a batch 51 // edit. The autocomplete text here is meaningless in the middle of a change. 52 private final AutocompleteState mCurrentState; 53 54 // This keeps track of the state in which previous notification was sent. It prevents redundant 55 // or unnecessary notification. 56 private final AutocompleteState mPreviouslyNotifiedState; 57 58 // This keeps track of the autocompletetext that we need to show (at the end of batch edit if 59 // we are in the middle of it), and also the matching user text that it should be appended to. 60 // Note that this potentially allows the controller to update in a delayed manner. 61 private final AutocompleteState mPreviouslySetState; 62 63 private final SpanCursorController mSpanCursorController; 64 65 private AutocompleteInputConnection mInputConnection; 66 private boolean mLastEditWasTyping = true; 67 private boolean mIgnoreTextChangeFromAutocomplete = true; 68 private int mBatchEditNestCount; 69 private int mDeletePostfixOnNextBeginImeCommand; 70 71 // For testing. 72 private int mLastUpdateSelStart; 73 private int mLastUpdateSelEnd; 74 75 // This controls whether AutocompleteEditText is permitted to pass-through specific 76 // Accessibility announcements, in particular the TEXT_CHANGED and TEXT_SELECTION_CHANGED. 77 // The only events of the above type that are allowed are ones coming from 78 // SpannableAutocompleteEditTextModel. 79 private boolean mDelegateShouldIgnoreAccessibilityEvents = true; 80 SpannableAutocompleteEditTextModel(AutocompleteEditTextModelBase.Delegate delegate)81 public SpannableAutocompleteEditTextModel(AutocompleteEditTextModelBase.Delegate delegate) { 82 if (DEBUG) Log.i(TAG, "constructor"); 83 mDelegate = delegate; 84 mCurrentState = new AutocompleteState(delegate.getText().toString(), "", 85 delegate.getSelectionStart(), delegate.getSelectionEnd()); 86 mPreviouslyNotifiedState = new AutocompleteState(mCurrentState); 87 mPreviouslySetState = new AutocompleteState(mCurrentState); 88 89 mSpanCursorController = new SpanCursorController(delegate); 90 } 91 92 @Override onCreateInputConnection(InputConnection inputConnection)93 public InputConnection onCreateInputConnection(InputConnection inputConnection) { 94 mLastUpdateSelStart = mDelegate.getSelectionStart(); 95 mLastUpdateSelEnd = mDelegate.getSelectionEnd(); 96 mBatchEditNestCount = 0; 97 if (inputConnection == null) { 98 if (DEBUG) Log.i(TAG, "onCreateInputConnection: null"); 99 mInputConnection = null; 100 return null; 101 } 102 if (DEBUG) Log.i(TAG, "onCreateInputConnection"); 103 mInputConnection = new AutocompleteInputConnection(); 104 mInputConnection.setTarget(inputConnection); 105 return mInputConnection; 106 } 107 108 /** 109 * @param editable The editable. 110 * @return Debug string for the given {@Editable}. 111 */ getEditableDebugString(Editable editable)112 private static String getEditableDebugString(Editable editable) { 113 return String.format(Locale.US, "Editable {[%s] SEL[%d %d] COM[%d %d]}", 114 editable.toString(), Selection.getSelectionStart(editable), 115 Selection.getSelectionEnd(editable), 116 BaseInputConnection.getComposingSpanStart(editable), 117 BaseInputConnection.getComposingSpanEnd(editable)); 118 } 119 sendAccessibilityEventForUserTextChange( AutocompleteState oldState, AutocompleteState newState)120 private void sendAccessibilityEventForUserTextChange( 121 AutocompleteState oldState, AutocompleteState newState) { 122 int addedCount = -1; 123 int removedCount = -1; 124 int fromIndex = -1; 125 126 if (newState.isBackwardDeletedFrom(oldState)) { 127 addedCount = 0; 128 removedCount = oldState.getText().length() - newState.getUserText().length(); 129 fromIndex = newState.getUserText().length(); 130 } else if (newState.isForwardTypedFrom(oldState)) { 131 addedCount = newState.getUserText().length() - oldState.getUserText().length(); 132 removedCount = oldState.getAutocompleteText().length(); 133 fromIndex = oldState.getUserText().length(); 134 } else if (newState.getUserText().equals(oldState.getUserText())) { 135 addedCount = 0; 136 removedCount = oldState.getAutocompleteText().length(); 137 fromIndex = oldState.getUserText().length(); 138 } else { 139 // Assume that the whole text has been replaced. 140 addedCount = newState.getText().length(); 141 removedCount = oldState.getUserText().length(); 142 fromIndex = 0; 143 } 144 145 mDelegateShouldIgnoreAccessibilityEvents = false; 146 if (!oldState.getText().equals(newState.getText()) 147 && (addedCount != 0 || removedCount != 0)) { 148 AccessibilityEvent event = 149 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 150 event.setBeforeText(oldState.getText()); 151 event.setFromIndex(fromIndex); 152 event.setRemovedCount(removedCount); 153 event.setAddedCount(addedCount); 154 mDelegate.sendAccessibilityEventUnchecked(event); 155 } 156 157 if (oldState.getSelStart() != newState.getSelStart() 158 || oldState.getSelEnd() != newState.getSelEnd()) { 159 mDelegate.sendAccessibilityEventUnchecked( 160 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED)); 161 } 162 mDelegateShouldIgnoreAccessibilityEvents = true; 163 } 164 sendAccessibilityEventForAppendingAutocomplete(AutocompleteState newState)165 private void sendAccessibilityEventForAppendingAutocomplete(AutocompleteState newState) { 166 if (!newState.hasAutocompleteText()) return; 167 // Note that only text changes and selection does not change. 168 AccessibilityEvent eventTextChanged = 169 AccessibilityEvent.obtain(AccessibilityEvent.TYPE_VIEW_TEXT_CHANGED); 170 eventTextChanged.setBeforeText(newState.getUserText()); 171 eventTextChanged.setFromIndex(newState.getUserText().length()); 172 eventTextChanged.setRemovedCount(0); 173 eventTextChanged.setAddedCount(newState.getAutocompleteText().length()); 174 mDelegateShouldIgnoreAccessibilityEvents = false; 175 mDelegate.sendAccessibilityEventUnchecked(eventTextChanged); 176 mDelegateShouldIgnoreAccessibilityEvents = true; 177 } 178 notifyAccessibilityService()179 private void notifyAccessibilityService() { 180 if (mCurrentState.equals(mPreviouslyNotifiedState)) return; 181 if (!mDelegate.isAccessibilityEnabled()) return; 182 sendAccessibilityEventForUserTextChange(mPreviouslyNotifiedState, mCurrentState); 183 // Read autocomplete text separately. 184 sendAccessibilityEventForAppendingAutocomplete(mCurrentState); 185 } 186 notifyAutocompleteTextStateChanged()187 private void notifyAutocompleteTextStateChanged() { 188 if (DEBUG) { 189 Log.i(TAG, "notifyAutocompleteTextStateChanged PRV[%s] CUR[%s] IGN[%b]", 190 mPreviouslyNotifiedState, mCurrentState, mIgnoreTextChangeFromAutocomplete); 191 } 192 if (mBatchEditNestCount > 0) { 193 // crbug.com/764749 194 Log.w(TAG, "Did not notify - in batch edit."); 195 return; 196 } 197 if (mCurrentState.equals(mPreviouslyNotifiedState)) { 198 // crbug.com/764749 199 Log.w(TAG, "Did not notify - no change."); 200 return; 201 } 202 notifyAccessibilityService(); 203 if (mCurrentState.getUserText().equals(mPreviouslyNotifiedState.getUserText()) 204 && (mCurrentState.hasAutocompleteText() 205 || !mPreviouslyNotifiedState.hasAutocompleteText())) { 206 // Nothing has changed except that autocomplete text has been set or modified. Or 207 // selection change did not affect autocomplete text. Autocomplete text is set by the 208 // controller, so only text change or deletion of autocomplete text should be notified. 209 mPreviouslyNotifiedState.copyFrom(mCurrentState); 210 return; 211 } 212 mPreviouslyNotifiedState.copyFrom(mCurrentState); 213 if (mIgnoreTextChangeFromAutocomplete) { 214 // crbug.com/764749 215 Log.w(TAG, "Did not notify - ignored."); 216 return; 217 } 218 // The current model's mechanism always moves the cursor at the end of user text, so we 219 // don't need to update the display. 220 mDelegate.onAutocompleteTextStateChanged(false /* updateDisplay */); 221 } 222 clearAutocompleteText()223 private void clearAutocompleteText() { 224 if (DEBUG) Log.i(TAG, "clearAutocomplete"); 225 mPreviouslySetState.clearAutocompleteText(); 226 mCurrentState.clearAutocompleteText(); 227 } 228 clearAutocompleteTextAndUpdateSpanCursor()229 private void clearAutocompleteTextAndUpdateSpanCursor() { 230 if (DEBUG) Log.i(TAG, "clearAutocompleteAndUpdateSpanCursor"); 231 clearAutocompleteText(); 232 // Take effect and notify if not already in a batch edit. 233 if (mInputConnection != null) { 234 mInputConnection.onBeginImeCommand(); 235 mInputConnection.onEndImeCommand(); 236 } else { 237 mSpanCursorController.removeSpan(); 238 notifyAutocompleteTextStateChanged(); 239 } 240 } 241 242 @Override dispatchKeyEvent(final KeyEvent event)243 public boolean dispatchKeyEvent(final KeyEvent event) { 244 if (DEBUG) Log.i(TAG, "dispatchKeyEvent"); 245 if (mInputConnection == null) { 246 return mDelegate.super_dispatchKeyEvent(event); 247 } 248 mInputConnection.onBeginImeCommand(); 249 if (event.getKeyCode() == KeyEvent.KEYCODE_ENTER 250 && event.getAction() == KeyEvent.ACTION_DOWN) { 251 mInputConnection.commitAutocomplete(); 252 } 253 boolean retVal = mDelegate.super_dispatchKeyEvent(event); 254 mInputConnection.onEndImeCommand(); 255 return retVal; 256 } 257 258 @Override onSetText(CharSequence text)259 public void onSetText(CharSequence text) { 260 if (DEBUG) Log.i(TAG, "onSetText: " + text); 261 // setText() does not necessarily trigger onTextChanged(). We need to accept the new text 262 // and reset the states. 263 mCurrentState.set(text.toString(), "", text.length(), text.length()); 264 mSpanCursorController.reset(); 265 mPreviouslyNotifiedState.copyFrom(mCurrentState); 266 mPreviouslySetState.copyFrom(mCurrentState); 267 if (mBatchEditNestCount == 0) updateSelectionForTesting(); 268 } 269 270 @Override onSelectionChanged(int selStart, int selEnd)271 public void onSelectionChanged(int selStart, int selEnd) { 272 if (DEBUG) Log.i(TAG, "onSelectionChanged [%d,%d]", selStart, selEnd); 273 if (mCurrentState.getSelStart() == selStart && mCurrentState.getSelEnd() == selEnd) return; 274 275 mCurrentState.setSelection(selStart, selEnd); 276 if (mBatchEditNestCount > 0) return; 277 int len = mCurrentState.getUserText().length(); 278 if (mCurrentState.hasAutocompleteText()) { 279 if (selStart > len || selEnd > len) { 280 if (DEBUG) Log.i(TAG, "Autocomplete text is being touched. Make it real."); 281 if (mInputConnection != null) mInputConnection.commitAutocomplete(); 282 } else { 283 if (DEBUG) Log.i(TAG, "Touching before the cursor removes autocomplete."); 284 clearAutocompleteTextAndUpdateSpanCursor(); 285 } 286 } 287 updateSelectionForTesting(); 288 notifyAutocompleteTextStateChanged(); 289 } 290 291 @Override onFocusChanged(boolean focused)292 public void onFocusChanged(boolean focused) { 293 if (DEBUG) Log.i(TAG, "onFocusChanged: " + focused); 294 295 if (!focused) { 296 // Reset selection now. It will be updated immediately after focus is re-gained. 297 // We do this to ensure the selection changed announcements are advertised by us 298 // since we suppress all TEXT_SELECTION_CHANGED announcements coming from EditText. 299 mPreviouslyNotifiedState.setSelection(-1, -1); 300 mCurrentState.setSelection(-1, -1); 301 } 302 } 303 304 @Override onTextChanged(CharSequence text, int start, int beforeLength, int afterLength)305 public void onTextChanged(CharSequence text, int start, int beforeLength, int afterLength) { 306 if (DEBUG) Log.i(TAG, "onTextChanged: " + text); 307 mSpanCursorController.reflectTextUpdateInState(mCurrentState, text); 308 if (mBatchEditNestCount > 0) return; // let endBatchEdit() handles changes from IME. 309 // An external change such as text paste occurred. 310 mLastEditWasTyping = false; 311 clearAutocompleteTextAndUpdateSpanCursor(); 312 } 313 314 @Override onPaste()315 public void onPaste() { 316 if (DEBUG) Log.i(TAG, "onPaste"); 317 } 318 319 @Override getTextWithAutocomplete()320 public String getTextWithAutocomplete() { 321 String retVal = mCurrentState.getText(); 322 if (DEBUG) Log.i(TAG, "getTextWithAutocomplete: %s", retVal); 323 return retVal; 324 } 325 326 @Override getTextWithoutAutocomplete()327 public String getTextWithoutAutocomplete() { 328 String retVal = mCurrentState.getUserText(); 329 if (DEBUG) Log.i(TAG, "getTextWithoutAutocomplete: " + retVal); 330 return retVal; 331 } 332 333 @Override getAutocompleteText()334 public String getAutocompleteText() { 335 return mCurrentState.getAutocompleteText(); 336 } 337 338 @Override setIgnoreTextChangeFromAutocomplete(boolean ignore)339 public void setIgnoreTextChangeFromAutocomplete(boolean ignore) { 340 if (DEBUG) Log.i(TAG, "setIgnoreText: " + ignore); 341 mIgnoreTextChangeFromAutocomplete = ignore; 342 } 343 344 @Override setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText)345 public void setAutocompleteText(CharSequence userText, CharSequence inlineAutocompleteText) { 346 setAutocompleteTextInternal(userText.toString(), inlineAutocompleteText.toString()); 347 } 348 setAutocompleteTextInternal(String userText, String autocompleteText)349 private void setAutocompleteTextInternal(String userText, String autocompleteText) { 350 if (DEBUG) Log.i(TAG, "setAutocompleteText: %s[%s]", userText, autocompleteText); 351 mPreviouslySetState.set(userText, autocompleteText, userText.length(), userText.length()); 352 // TODO(changwan): avoid any unnecessary removal and addition of autocomplete text when it 353 // is not changed or when it is appended to the existing autocomplete text. 354 if (mInputConnection != null) { 355 mInputConnection.onBeginImeCommand(); 356 mInputConnection.onEndImeCommand(); 357 } 358 } 359 360 @Override shouldAutocomplete()361 public boolean shouldAutocomplete() { 362 boolean retVal = mBatchEditNestCount == 0 && mLastEditWasTyping 363 && mCurrentState.isCursorAtEndOfUserText() && !isKeyboardBlacklisted() 364 && isNonCompositionalText(getTextWithoutAutocomplete()); 365 if (DEBUG) Log.i(TAG, "shouldAutocomplete: " + retVal); 366 return retVal; 367 } 368 isKeyboardBlacklisted()369 private boolean isKeyboardBlacklisted() { 370 String pkgName = mDelegate.getKeyboardPackageName(); 371 return pkgName.contains(".iqqi") // crbug.com/767016 372 || pkgName.contains("omronsoft") || pkgName.contains(".iwnn"); // crbug.com/758443 373 } 374 shouldFinishCompositionOnDeletion()375 private boolean shouldFinishCompositionOnDeletion() { 376 // crbug.com/758443, crbug.com/766888: Japanese keyboard does not finish composition when we 377 // restore the deleted text, and later typing will make Japanese keyboard move before the 378 // restored character. Most keyboards accept finishComposingText and update their internal 379 // states. 380 String pkgName = mDelegate.getKeyboardPackageName(); 381 // One exception is the recent version of Samsung keyboard which works goofily only 382 // when we finish composing text here. Since it is more difficult to blacklist all Japanese 383 // keyboards, instead we call finishComposingText() for all the keyboards except for Samsung 384 // keyboard. 385 return !pkgName.contains("com.sec.android.inputmethod") 386 // crbug.com/1071011: LG keyboard has the same issue. 387 && !pkgName.contains("com.lge.ime"); 388 } 389 390 @VisibleForTesting isNonCompositionalText(String text)391 public static boolean isNonCompositionalText(String text) { 392 // To start with, we are only activating this for English alphabets, European characters, 393 // numbers and URLs to avoid potential bad interactions with more complex IMEs. 394 // The rationale for including character sets with diacritical marks is that backspacing on 395 // a letter with a diacritical mark most likely deletes the whole character instead of 396 // removing the diacritical mark. 397 // TODO(changwan): also scan for other traditionally non-IME charsets. 398 return NON_COMPOSITIONAL_TEXT_PATTERN.matcher(text).matches(); 399 } 400 401 @Override hasAutocomplete()402 public boolean hasAutocomplete() { 403 boolean retVal = mCurrentState.hasAutocompleteText(); 404 if (DEBUG) Log.i(TAG, "hasAutocomplete: " + retVal); 405 return retVal; 406 } 407 408 @Override getInputConnection()409 public InputConnection getInputConnection() { 410 return mInputConnection; 411 } 412 updateSelectionForTesting()413 private void updateSelectionForTesting() { 414 int selStart = mDelegate.getSelectionStart(); 415 int selEnd = mDelegate.getSelectionEnd(); 416 if (selStart == mLastUpdateSelStart && selEnd == mLastUpdateSelEnd) return; 417 418 mLastUpdateSelStart = selStart; 419 mLastUpdateSelEnd = selEnd; 420 mDelegate.onUpdateSelectionForTesting(selStart, selEnd); 421 } 422 423 @Override shouldIgnoreAccessibilityEvent()424 public boolean shouldIgnoreAccessibilityEvent() { 425 return mDelegateShouldIgnoreAccessibilityEvents; 426 } 427 428 /** 429 * A class to set and remove, or do other operations on Span and SpannableString of autocomplete 430 * text that will be appended to the user text. In addition, cursor will be hidden whenever we 431 * are showing span to the user. 432 */ 433 private static class SpanCursorController { 434 private final Delegate mDelegate; 435 private BackgroundColorSpan mSpan; 436 SpanCursorController(Delegate delegate)437 public SpanCursorController(Delegate delegate) { 438 mDelegate = delegate; 439 } 440 setSpan(AutocompleteState state)441 public void setSpan(AutocompleteState state) { 442 int sel = state.getSelStart(); 443 444 if (mSpan == null) mSpan = new BackgroundColorSpan(mDelegate.getHighlightColor()); 445 SpannableString spanString = new SpannableString(state.getAutocompleteText()); 446 // The flag here helps make sure that span does not get spill to other part of the text. 447 spanString.setSpan(mSpan, 0, state.getAutocompleteText().length(), 448 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); 449 Editable editable = mDelegate.getEditableText(); 450 editable.append(spanString); 451 452 // Keep the original selection before adding spannable string. 453 Selection.setSelection(editable, sel, sel); 454 setCursorVisible(false); 455 if (DEBUG) Log.i(TAG, "setSpan: " + getEditableDebugString(editable)); 456 } 457 setCursorVisible(boolean visible)458 private void setCursorVisible(boolean visible) { 459 if (mDelegate.isFocused()) mDelegate.setCursorVisible(visible); 460 } 461 getSpanIndex(Editable editable)462 private int getSpanIndex(Editable editable) { 463 if (editable == null || mSpan == null) return -1; 464 return editable.getSpanStart(mSpan); // returns -1 if mSpan is not attached 465 } 466 reset()467 public void reset() { 468 setCursorVisible(true); 469 Editable editable = mDelegate.getEditableText(); 470 int idx = getSpanIndex(editable); 471 if (idx != -1) { 472 editable.removeSpan(mSpan); 473 } 474 mSpan = null; 475 } 476 removeSpan()477 public boolean removeSpan() { 478 setCursorVisible(true); 479 Editable editable = mDelegate.getEditableText(); 480 int idx = getSpanIndex(editable); 481 if (idx == -1) return false; 482 if (DEBUG) Log.i(TAG, "removeSpan IDX[%d]", idx); 483 editable.removeSpan(mSpan); 484 editable.delete(idx, editable.length()); 485 mSpan = null; 486 if (DEBUG) { 487 Log.i(TAG, "removeSpan - after removal: " + getEditableDebugString(editable)); 488 } 489 return true; 490 } 491 commitSpan()492 public void commitSpan() { 493 mDelegate.getEditableText().removeSpan(mSpan); 494 setCursorVisible(true); 495 } 496 reflectTextUpdateInState(AutocompleteState state, CharSequence text)497 public void reflectTextUpdateInState(AutocompleteState state, CharSequence text) { 498 if (text instanceof Editable) { 499 Editable editable = (Editable) text; 500 int idx = getSpanIndex(editable); 501 if (idx != -1) { 502 // We do not set autocomplete text here as model should solely control it. 503 state.setUserText(editable.subSequence(0, idx).toString()); 504 return; 505 } 506 } 507 state.setUserText(text.toString()); 508 } 509 } 510 511 private class AutocompleteInputConnection extends InputConnectionWrapper { 512 private final AutocompleteState mPreBatchEditState; 513 AutocompleteInputConnection()514 public AutocompleteInputConnection() { 515 super(null, true); 516 mPreBatchEditState = new AutocompleteState(mCurrentState); 517 } 518 incrementBatchEditCount()519 private boolean incrementBatchEditCount() { 520 ++mBatchEditNestCount; 521 // After the outermost super.beginBatchEdit(), EditText will stop selection change 522 // update to the IME app. 523 return super.beginBatchEdit(); 524 } 525 decrementBatchEditCount()526 private boolean decrementBatchEditCount() { 527 --mBatchEditNestCount; 528 boolean retVal = super.endBatchEdit(); 529 if (mBatchEditNestCount == 0) { 530 // At the outermost super.endBatchEdit(), EditText will resume selection change 531 // update to the IME app. 532 updateSelectionForTesting(); 533 } 534 return retVal; 535 } 536 commitAutocomplete()537 public void commitAutocomplete() { 538 if (DEBUG) Log.i(TAG, "commitAutocomplete"); 539 if (!hasAutocomplete()) return; 540 541 String autocompleteText = mCurrentState.getAutocompleteText(); 542 543 mCurrentState.commitAutocompleteText(); 544 // Invalidate mPreviouslySetState. 545 mPreviouslySetState.copyFrom(mCurrentState); 546 mLastEditWasTyping = false; 547 548 if (mBatchEditNestCount == 0) { 549 incrementBatchEditCount(); // avoids additional notifyAutocompleteTextStateChanged() 550 mSpanCursorController.commitSpan(); 551 decrementBatchEditCount(); 552 } else { 553 // We have already removed span in the onBeginImeCommand(), just append the text. 554 mDelegate.append(autocompleteText); 555 } 556 } 557 558 @Override beginBatchEdit()559 public boolean beginBatchEdit() { 560 if (DEBUG) Log.i(TAG, "beginBatchEdit"); 561 onBeginImeCommand(); 562 boolean retVal = incrementBatchEditCount(); 563 onEndImeCommand(); 564 return retVal; 565 } 566 567 /** 568 * Always call this at the beginning of any IME command. Compare this with beginBatchEdit() 569 * which is by itself an IME command. 570 * @return Whether the call was successful. 571 */ onBeginImeCommand()572 public boolean onBeginImeCommand() { 573 if (DEBUG) Log.i(TAG, "onBeginImeCommand: " + mBatchEditNestCount); 574 boolean retVal = incrementBatchEditCount(); 575 if (mBatchEditNestCount == 1) { 576 mPreBatchEditState.copyFrom(mCurrentState); 577 } else if (mDeletePostfixOnNextBeginImeCommand > 0) { 578 int len = mDelegate.getText().length(); 579 mDelegate.getText().delete(len - mDeletePostfixOnNextBeginImeCommand, len); 580 } 581 mDeletePostfixOnNextBeginImeCommand = 0; 582 mSpanCursorController.removeSpan(); 583 return retVal; 584 } 585 restoreBackspacedText(String diff)586 private void restoreBackspacedText(String diff) { 587 if (DEBUG) Log.i(TAG, "restoreBackspacedText. diff: " + diff); 588 589 if (mBatchEditNestCount > 0) { 590 // If batch edit hasn't finished, we will restore backspaced text only for visual 591 // effects. However, for internal operations to work correctly, we need to remove 592 // the restored diff at the beginning of next IME operation. 593 mDeletePostfixOnNextBeginImeCommand = diff.length(); 594 } 595 if (mBatchEditNestCount == 0) { // only at the outermost batch edit 596 if (shouldFinishCompositionOnDeletion()) super.finishComposingText(); 597 } 598 incrementBatchEditCount(); // avoids additional notifyAutocompleteTextStateChanged() 599 Editable editable = mDelegate.getEditableText(); 600 editable.append(diff); 601 decrementBatchEditCount(); 602 } 603 setAutocompleteSpan()604 private boolean setAutocompleteSpan() { 605 mSpanCursorController.removeSpan(); 606 if (DEBUG) { 607 Log.i(TAG, "setAutocompleteSpan. %s->%s", mPreviouslySetState, mCurrentState); 608 } 609 if (!mCurrentState.isCursorAtEndOfUserText()) return false; 610 611 if (mCurrentState.reuseAutocompleteTextIfPrefixExtension(mPreviouslySetState)) { 612 mSpanCursorController.setSpan(mCurrentState); 613 return true; 614 } else { 615 return false; 616 } 617 } 618 619 @Override endBatchEdit()620 public boolean endBatchEdit() { 621 if (DEBUG) Log.i(TAG, "endBatchEdit"); 622 onBeginImeCommand(); 623 boolean retVal = decrementBatchEditCount(); 624 onEndImeCommand(); 625 return retVal; 626 } 627 628 /** 629 * Always call this at the end of an IME command. Compare this with endBatchEdit() 630 * which is by itself an IME command. 631 * @return Whether the call was successful. 632 */ onEndImeCommand()633 public boolean onEndImeCommand() { 634 if (DEBUG) Log.i(TAG, "onEndImeCommand: " + (mBatchEditNestCount - 1)); 635 String diff = mCurrentState.getBackwardDeletedTextFrom(mPreBatchEditState); 636 if (diff != null) { 637 // Update selection first such that keyboard app gets what it expects. 638 boolean retVal = decrementBatchEditCount(); 639 640 if (mPreBatchEditState.hasAutocompleteText()) { 641 // Undo delete to retain the last character and only remove autocomplete text. 642 restoreBackspacedText(diff); 643 } 644 mLastEditWasTyping = false; 645 clearAutocompleteText(); 646 notifyAutocompleteTextStateChanged(); 647 return retVal; 648 } 649 if (!setAutocompleteSpan()) { 650 clearAutocompleteText(); 651 } 652 boolean retVal = decrementBatchEditCount(); 653 // Simply typed some characters or whole text selection has been overridden. 654 if (mCurrentState.isForwardTypedFrom(mPreBatchEditState) 655 || (mPreBatchEditState.isWholeUserTextSelected() 656 && mCurrentState.getUserText().length() > 0 657 && mCurrentState.isCursorAtEndOfUserText())) { 658 mLastEditWasTyping = true; 659 } 660 notifyAutocompleteTextStateChanged(); 661 return retVal; 662 } 663 664 @Override commitText(CharSequence text, int newCursorPosition)665 public boolean commitText(CharSequence text, int newCursorPosition) { 666 if (DEBUG) Log.i(TAG, "commitText: " + text); 667 onBeginImeCommand(); 668 boolean retVal = super.commitText(text, newCursorPosition); 669 onEndImeCommand(); 670 return retVal; 671 } 672 673 @Override setComposingText(CharSequence text, int newCursorPosition)674 public boolean setComposingText(CharSequence text, int newCursorPosition) { 675 if (DEBUG) Log.i(TAG, "setComposingText: " + text); 676 onBeginImeCommand(); 677 boolean retVal = super.setComposingText(text, newCursorPosition); 678 onEndImeCommand(); 679 return retVal; 680 } 681 682 @Override setComposingRegion(int start, int end)683 public boolean setComposingRegion(int start, int end) { 684 if (DEBUG) Log.i(TAG, "setComposingRegion: [%d,%d]", start, end); 685 onBeginImeCommand(); 686 boolean retVal = super.setComposingRegion(start, end); 687 onEndImeCommand(); 688 return retVal; 689 } 690 691 @Override finishComposingText()692 public boolean finishComposingText() { 693 if (DEBUG) Log.i(TAG, "finishComposingText"); 694 onBeginImeCommand(); 695 boolean retVal = super.finishComposingText(); 696 onEndImeCommand(); 697 return retVal; 698 } 699 700 @Override deleteSurroundingText(final int beforeLength, final int afterLength)701 public boolean deleteSurroundingText(final int beforeLength, final int afterLength) { 702 if (DEBUG) Log.i(TAG, "deleteSurroundingText [%d,%d]", beforeLength, afterLength); 703 onBeginImeCommand(); 704 boolean retVal = super.deleteSurroundingText(beforeLength, afterLength); 705 onEndImeCommand(); 706 return retVal; 707 } 708 709 @Override setSelection(final int start, final int end)710 public boolean setSelection(final int start, final int end) { 711 if (DEBUG) Log.i(TAG, "setSelection [%d,%d]", start, end); 712 onBeginImeCommand(); 713 boolean retVal = super.setSelection(start, end); 714 onEndImeCommand(); 715 return retVal; 716 } 717 718 @Override performEditorAction(final int editorAction)719 public boolean performEditorAction(final int editorAction) { 720 if (DEBUG) Log.i(TAG, "performEditorAction: " + editorAction); 721 onBeginImeCommand(); 722 commitAutocomplete(); 723 boolean retVal = super.performEditorAction(editorAction); 724 onEndImeCommand(); 725 return retVal; 726 } 727 728 @Override sendKeyEvent(final KeyEvent event)729 public boolean sendKeyEvent(final KeyEvent event) { 730 if (DEBUG) Log.i(TAG, "sendKeyEvent: " + event.getKeyCode()); 731 onBeginImeCommand(); 732 boolean retVal = super.sendKeyEvent(event); 733 onEndImeCommand(); 734 return retVal; 735 } 736 737 @Override getExtractedText(final ExtractedTextRequest request, final int flags)738 public ExtractedText getExtractedText(final ExtractedTextRequest request, final int flags) { 739 if (DEBUG) Log.i(TAG, "getExtractedText"); 740 onBeginImeCommand(); 741 ExtractedText retVal = super.getExtractedText(request, flags); 742 onEndImeCommand(); 743 return retVal; 744 } 745 746 @Override getTextAfterCursor(final int n, final int flags)747 public CharSequence getTextAfterCursor(final int n, final int flags) { 748 if (DEBUG) Log.i(TAG, "getTextAfterCursor"); 749 onBeginImeCommand(); 750 CharSequence retVal = super.getTextAfterCursor(n, flags); 751 onEndImeCommand(); 752 return retVal; 753 } 754 755 @Override getTextBeforeCursor(final int n, final int flags)756 public CharSequence getTextBeforeCursor(final int n, final int flags) { 757 if (DEBUG) Log.i(TAG, "getTextBeforeCursor"); 758 onBeginImeCommand(); 759 CharSequence retVal = super.getTextBeforeCursor(n, flags); 760 onEndImeCommand(); 761 return retVal; 762 } 763 764 @Override getSelectedText(final int flags)765 public CharSequence getSelectedText(final int flags) { 766 if (DEBUG) Log.i(TAG, "getSelectedText"); 767 onBeginImeCommand(); 768 CharSequence retVal = super.getSelectedText(flags); 769 onEndImeCommand(); 770 return retVal; 771 } 772 773 @Override commitCompletion(CompletionInfo text)774 public boolean commitCompletion(CompletionInfo text) { 775 if (DEBUG) Log.i(TAG, "commitCompletion"); 776 onBeginImeCommand(); 777 boolean retVal = super.commitCompletion(text); 778 onEndImeCommand(); 779 return retVal; 780 } 781 782 @Override commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts)783 public boolean commitContent(InputContentInfo inputContentInfo, int flags, Bundle opts) { 784 if (DEBUG) Log.i(TAG, "commitContent"); 785 onBeginImeCommand(); 786 boolean retVal = super.commitContent(inputContentInfo, flags, opts); 787 onEndImeCommand(); 788 return retVal; 789 } 790 791 @Override commitCorrection(CorrectionInfo correctionInfo)792 public boolean commitCorrection(CorrectionInfo correctionInfo) { 793 if (DEBUG) Log.i(TAG, "commitCorrection"); 794 onBeginImeCommand(); 795 boolean retVal = super.commitCorrection(correctionInfo); 796 onEndImeCommand(); 797 return retVal; 798 } 799 800 @Override deleteSurroundingTextInCodePoints(int beforeLength, int afterLength)801 public boolean deleteSurroundingTextInCodePoints(int beforeLength, int afterLength) { 802 if (DEBUG) Log.i(TAG, "deleteSurroundingTextInCodePoints"); 803 onBeginImeCommand(); 804 boolean retVal = super.deleteSurroundingTextInCodePoints(beforeLength, afterLength); 805 onEndImeCommand(); 806 return retVal; 807 } 808 809 @Override getCursorCapsMode(int reqModes)810 public int getCursorCapsMode(int reqModes) { 811 if (DEBUG) Log.i(TAG, "getCursorCapsMode"); 812 onBeginImeCommand(); 813 int retVal = super.getCursorCapsMode(reqModes); 814 onEndImeCommand(); 815 return retVal; 816 } 817 818 @Override requestCursorUpdates(int cursorUpdateMode)819 public boolean requestCursorUpdates(int cursorUpdateMode) { 820 if (DEBUG) Log.i(TAG, "requestCursorUpdates"); 821 onBeginImeCommand(); 822 boolean retVal = super.requestCursorUpdates(cursorUpdateMode); 823 onEndImeCommand(); 824 return retVal; 825 } 826 827 @Override clearMetaKeyStates(int states)828 public boolean clearMetaKeyStates(int states) { 829 if (DEBUG) Log.i(TAG, "clearMetaKeyStates"); 830 onBeginImeCommand(); 831 boolean retVal = super.clearMetaKeyStates(states); 832 onEndImeCommand(); 833 return retVal; 834 } 835 } 836 } 837