InputLogic.java revision 8ab4ae4377fca97cb9ef05c615c5a59bc4bc3d8c
1/* 2 * Copyright (C) 2013 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17package com.android.inputmethod.latin.inputlogic; 18 19import android.os.SystemClock; 20import android.text.SpannableString; 21import android.text.TextUtils; 22import android.text.style.SuggestionSpan; 23import android.util.Log; 24import android.view.KeyCharacterMap; 25import android.view.KeyEvent; 26import android.view.inputmethod.CorrectionInfo; 27import android.view.inputmethod.EditorInfo; 28 29import com.android.inputmethod.compat.SuggestionSpanUtils; 30import com.android.inputmethod.event.Event; 31import com.android.inputmethod.event.InputTransaction; 32import com.android.inputmethod.keyboard.KeyboardSwitcher; 33import com.android.inputmethod.latin.Constants; 34import com.android.inputmethod.latin.Dictionary; 35import com.android.inputmethod.latin.DictionaryFacilitatorForSuggest; 36import com.android.inputmethod.latin.InputPointers; 37import com.android.inputmethod.latin.LastComposedWord; 38import com.android.inputmethod.latin.LatinIME; 39import com.android.inputmethod.latin.LatinImeLogger; 40import com.android.inputmethod.latin.RichInputConnection; 41import com.android.inputmethod.latin.Suggest; 42import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; 43import com.android.inputmethod.latin.SuggestedWords; 44import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 45import com.android.inputmethod.latin.WordComposer; 46import com.android.inputmethod.latin.define.ProductionFlag; 47import com.android.inputmethod.latin.settings.SettingsValues; 48import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 49import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; 50import com.android.inputmethod.latin.utils.AsyncResultHolder; 51import com.android.inputmethod.latin.utils.CollectionUtils; 52import com.android.inputmethod.latin.utils.InputTypeUtils; 53import com.android.inputmethod.latin.utils.LatinImeLoggerUtils; 54import com.android.inputmethod.latin.utils.RecapitalizeStatus; 55import com.android.inputmethod.latin.utils.StringUtils; 56import com.android.inputmethod.latin.utils.TextRange; 57import com.android.inputmethod.research.ResearchLogger; 58 59import java.util.ArrayList; 60import java.util.TreeSet; 61import java.util.concurrent.TimeUnit; 62 63/** 64 * This class manages the input logic. 65 */ 66public final class InputLogic { 67 private static final String TAG = InputLogic.class.getSimpleName(); 68 69 // TODO : Remove this member when we can. 70 private final LatinIME mLatinIME; 71 private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; 72 73 // Never null. 74 private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; 75 76 // TODO : make all these fields private as soon as possible. 77 // Current space state of the input method. This can be any of the above constants. 78 private int mSpaceState; 79 // Never null 80 public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY; 81 public final Suggest mSuggest = new Suggest(); 82 83 public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 84 public final WordComposer mWordComposer; 85 public final RichInputConnection mConnection; 86 private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); 87 88 private int mDeleteCount; 89 private long mLastKeyTime; 90 public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet(); 91 92 // Keeps track of most recently inserted text (multi-character key) for reverting 93 private String mEnteredText; 94 95 // TODO: This boolean is persistent state and causes large side effects at unexpected times. 96 // Find a way to remove it for readability. 97 private boolean mIsAutoCorrectionIndicatorOn; 98 private long mDoubleSpacePeriodCountdownStart; 99 100 public InputLogic(final LatinIME latinIME, 101 final SuggestionStripViewAccessor suggestionStripViewAccessor) { 102 mLatinIME = latinIME; 103 mSuggestionStripViewAccessor = suggestionStripViewAccessor; 104 mWordComposer = new WordComposer(); 105 mConnection = new RichInputConnection(latinIME); 106 mInputLogicHandler = InputLogicHandler.NULL_HANDLER; 107 } 108 109 /** 110 * Initializes the input logic for input in an editor. 111 * 112 * Call this when input starts or restarts in some editor (typically, in onStartInputView). 113 * If the input is starting in the same field as before, set `restarting' to true. This allows 114 * the input logic to reset only necessary stuff and save performance. Also, when restarting 115 * some things must not be done (for example, the keyboard should not be reset to the 116 * alphabetic layout), so do not send false to this just in case. 117 * 118 * @param restarting whether input is starting in the same field as before. Unused for now. 119 * @param editorInfo the editorInfo associated with the editor. 120 */ 121 public void startInput(final boolean restarting, final EditorInfo editorInfo) { 122 mEnteredText = null; 123 resetComposingState(true /* alsoResetLastComposedWord */); 124 mDeleteCount = 0; 125 mSpaceState = SpaceState.NONE; 126 mRecapitalizeStatus.deactivate(); 127 mCurrentlyPressedHardwareKeys.clear(); 128 mSuggestedWords = SuggestedWords.EMPTY; 129 // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying 130 // so we try using some heuristics to find out about these and fix them. 131 mConnection.tryFixLyingCursorPosition(); 132 cancelDoubleSpacePeriodCountdown(); 133 if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) { 134 mInputLogicHandler = new InputLogicHandler(mLatinIME, this); 135 } else { 136 mInputLogicHandler.reset(); 137 } 138 } 139 140 /** 141 * Clean up the input logic after input is finished. 142 */ 143 public void finishInput() { 144 if (mWordComposer.isComposingWord()) { 145 mConnection.finishComposingText(); 146 } 147 resetComposingState(true /* alsoResetLastComposedWord */); 148 mInputLogicHandler.reset(); 149 } 150 151 /** 152 * React to a string input. 153 * 154 * This is triggered by keys that input many characters at once, like the ".com" key or 155 * some additional keys for example. 156 * 157 * @param settingsValues the current values of the settings. 158 * @param event the input event containing the data. 159 */ 160 public void onTextInput(final SettingsValues settingsValues, final Event event, 161 // TODO: remove this argument 162 final LatinIME.UIHandler handler) { 163 final String rawText = event.mText.toString(); 164 mConnection.beginBatchEdit(); 165 if (mWordComposer.isComposingWord()) { 166 commitCurrentAutoCorrection(settingsValues, rawText, handler); 167 } else { 168 resetComposingState(true /* alsoResetLastComposedWord */); 169 } 170 handler.postUpdateSuggestionStrip(); 171 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS 172 && ResearchLogger.RESEARCH_KEY_OUTPUT_TEXT.equals(rawText)) { 173 ResearchLogger.getInstance().onResearchKeySelected(mLatinIME); 174 return; 175 } 176 final String text = performSpecificTldProcessingOnTextInput(rawText); 177 if (SpaceState.PHANTOM == mSpaceState) { 178 promotePhantomSpace(settingsValues); 179 } 180 mConnection.commitText(text, 1); 181 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 182 ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */); 183 } 184 mConnection.endBatchEdit(); 185 // Space state must be updated before calling updateShiftState 186 mSpaceState = SpaceState.NONE; 187 mEnteredText = text; 188 } 189 190 /** 191 * A suggestion was picked from the suggestion strip. 192 * @param settingsValues the current values of the settings. 193 * @param index the index of the suggestion. 194 * @param suggestionInfo the suggestion info. 195 * @param keyboardShiftState the shift state of the keyboard, as returned by 196 * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} 197 * @return the complete transaction object 198 */ 199 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 200 // interface 201 public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, 202 final int index, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, 203 // TODO: remove this argument 204 final LatinIME.UIHandler handler) { 205 final SuggestedWords suggestedWords = mSuggestedWords; 206 final String suggestion = suggestionInfo.mWord; 207 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 208 if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { 209 // Word separators are suggested before the user inputs something. 210 // So, LatinImeLogger logs "" as a user's input. 211 LatinImeLogger.logOnManualSuggestion("", suggestion, index, suggestedWords); 212 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 213 final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); 214 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 215 ResearchLogger.latinIME_punctuationSuggestion(index, suggestion, 216 false /* isBatchMode */, suggestedWords.mIsPrediction); 217 } 218 return onCodeInput(settingsValues, event, keyboardShiftState, handler); 219 } 220 221 final Event event = Event.createSuggestionPickedEvent(suggestionInfo); 222 final InputTransaction inputTransaction = new InputTransaction(settingsValues, 223 event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState); 224 mConnection.beginBatchEdit(); 225 if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0 226 // In the batch input mode, a manually picked suggested word should just replace 227 // the current batch input text and there is no need for a phantom space. 228 && !mWordComposer.isBatchMode()) { 229 final int firstChar = Character.codePointAt(suggestion, 0); 230 if (!settingsValues.isWordSeparator(firstChar) 231 || settingsValues.isUsuallyPrecededBySpace(firstChar)) { 232 promotePhantomSpace(settingsValues); 233 } 234 } 235 236 // TODO: We should not need the following branch. We should be able to take the same 237 // code path as for other kinds, use commitChosenWord, and do everything normally. We will 238 // however need to reset the suggestion strip right away, because we know we can't take 239 // the risk of calling commitCompletion twice because we don't know how the app will react. 240 if (SuggestedWordInfo.KIND_APP_DEFINED == suggestionInfo.mKind) { 241 mSuggestedWords = SuggestedWords.EMPTY; 242 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 243 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 244 resetComposingState(true /* alsoResetLastComposedWord */); 245 mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo); 246 mConnection.endBatchEdit(); 247 return inputTransaction; 248 } 249 250 // We need to log before we commit, because the word composer will store away the user 251 // typed word. 252 final String replacedWord = mWordComposer.getTypedWord(); 253 LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords); 254 commitChosenWord(settingsValues, suggestion, 255 LastComposedWord.COMMIT_TYPE_MANUAL_PICK, LastComposedWord.NOT_A_SEPARATOR); 256 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 257 ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion, 258 mWordComposer.isBatchMode(), suggestionInfo.mScore, 259 suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType); 260 } 261 mConnection.endBatchEdit(); 262 // Don't allow cancellation of manual pick 263 mLastComposedWord.deactivate(); 264 // Space state must be updated before calling updateShiftState 265 mSpaceState = SpaceState.PHANTOM; 266 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 267 268 // We should show the "Touch again to save" hint if the user pressed the first entry 269 // AND it's in none of our current dictionaries (main, user or otherwise). 270 final DictionaryFacilitatorForSuggest dictionaryFacilitator = 271 mSuggest.mDictionaryFacilitator; 272 final boolean showingAddToDictionaryHint = 273 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind 274 || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind) 275 && !dictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */); 276 277 if (settingsValues.mIsInternal) { 278 LatinImeLoggerUtils.onSeparator((char)Constants.CODE_SPACE, 279 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 280 } 281 if (showingAddToDictionaryHint && dictionaryFacilitator.isUserDictionaryEnabled()) { 282 mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion); 283 } else { 284 // If we're not showing the "Touch again to save", then update the suggestion strip. 285 handler.postUpdateSuggestionStrip(); 286 } 287 return inputTransaction; 288 } 289 290 /** 291 * Consider an update to the cursor position. Evaluate whether this update has happened as 292 * part of normal typing or whether it was an explicit cursor move by the user. In any case, 293 * do the necessary adjustments. 294 * @param oldSelStart old selection start 295 * @param oldSelEnd old selection end 296 * @param newSelStart new selection start 297 * @param newSelEnd new selection end 298 * @return whether the cursor has moved as a result of user interaction. 299 */ 300 public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, 301 final int newSelStart, final int newSelEnd) { 302 if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { 303 return false; 304 } 305 // TODO: the following is probably better done in resetEntireInputState(). 306 // it should only happen when the cursor moved, and the very purpose of the 307 // test below is to narrow down whether this happened or not. Likewise with 308 // the call to updateShiftState. 309 // We set this to NONE because after a cursor move, we don't want the space 310 // state-related special processing to kick in. 311 mSpaceState = SpaceState.NONE; 312 313 final boolean selectionChangedOrSafeToReset = 314 oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed 315 || !mWordComposer.isComposingWord(); // safe to reset 316 final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd); 317 final int moveAmount = newSelStart - oldSelStart; 318 if (selectionChangedOrSafeToReset && (hasOrHadSelection 319 || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { 320 // If we are composing a word and moving the cursor, we would want to set a 321 // suggestion span for recorrection to work correctly. Unfortunately, that 322 // would involve the keyboard committing some new text, which would move the 323 // cursor back to where it was. Latin IME could then fix the position of the cursor 324 // again, but the asynchronous nature of the calls results in this wreaking havoc 325 // with selection on double tap and the like. 326 // Another option would be to send suggestions each time we set the composing 327 // text, but that is probably too expensive to do, so we decided to leave things 328 // as is. 329 // Also, we're posting a resume suggestions message, and this will update the 330 // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here 331 // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear 332 // it here, which means we'll keep outdated suggestions for a split second but the 333 // visual result is better. 334 resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */); 335 } else { 336 // resetEntireInputState calls resetCachesUponCursorMove, but forcing the 337 // composition to end. But in all cases where we don't reset the entire input 338 // state, we still want to tell the rich input connection about the new cursor 339 // position so that it can update its caches. 340 mConnection.resetCachesUponCursorMoveAndReturnSuccess( 341 newSelStart, newSelEnd, false /* shouldFinishComposition */); 342 } 343 344 // We moved the cursor. If we are touching a word, we need to resume suggestion. 345 mLatinIME.mHandler.postResumeSuggestions(); 346 // Reset the last recapitalization. 347 mRecapitalizeStatus.deactivate(); 348 return true; 349 } 350 351 /** 352 * React to a code input. It may be a code point to insert, or a symbolic value that influences 353 * the keyboard behavior. 354 * 355 * Typically, this is called whenever a key is pressed on the software keyboard. This is not 356 * the entry point for gesture input; see the onBatchInput* family of functions for this. 357 * 358 * @param settingsValues the current settings values. 359 * @param event the event to handle. 360 * @param keyboardShiftMode the current shift mode of the keyboard, as returned by 361 * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} 362 * @return the complete transaction object 363 */ 364 public InputTransaction onCodeInput(final SettingsValues settingsValues, final Event event, 365 final int keyboardShiftMode, 366 // TODO: remove this argument 367 final LatinIME.UIHandler handler) { 368 // TODO: rework the following to not squash the keycode and the code point into the same 369 // var because it's confusing. Instead the switch() should handle this in a readable manner. 370 final int code = 371 Event.NOT_A_CODE_POINT == event.mCodePoint ? event.mKeyCode : event.mCodePoint; 372 final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, 373 SystemClock.uptimeMillis(), mSpaceState, 374 getActualCapsMode(settingsValues, keyboardShiftMode)); 375 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 376 ResearchLogger.latinIME_onCodeInput(code, event.mX, event.mY); 377 } 378 if (event.mKeyCode != Constants.CODE_DELETE 379 || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { 380 mDeleteCount = 0; 381 } 382 mLastKeyTime = inputTransaction.mTimestamp; 383 mConnection.beginBatchEdit(); 384 if (!mWordComposer.isComposingWord()) { 385 mIsAutoCorrectionIndicatorOn = false; 386 } 387 388 // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. 389 if (event.mCodePoint != Constants.CODE_SPACE) { 390 cancelDoubleSpacePeriodCountdown(); 391 } 392 393 boolean didAutoCorrect = false; 394 if (Event.NOT_A_KEY_CODE != event.mKeyCode) { 395 // A special key, like delete, shift, emoji, or the settings key. 396 switch (event.mKeyCode) { 397 case Constants.CODE_DELETE: 398 handleBackspace(inputTransaction); 399 LatinImeLogger.logOnDelete(event.mX, event.mY); 400 break; 401 case Constants.CODE_SHIFT: 402 performRecapitalization(inputTransaction.mSettingsValues); 403 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 404 break; 405 case Constants.CODE_CAPSLOCK: 406 // Note: Changing keyboard to shift lock state is handled in 407 // {@link KeyboardSwitcher#onCodeInput(int)}. 408 break; 409 case Constants.CODE_SYMBOL_SHIFT: 410 // Note: Calling back to the keyboard on the symbol Shift key is handled in 411 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 412 break; 413 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 414 // Note: Calling back to the keyboard on symbol key is handled in 415 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 416 break; 417 case Constants.CODE_SETTINGS: 418 onSettingsKeyPressed(); 419 break; 420 case Constants.CODE_SHORTCUT: 421 // We need to switch to the shortcut IME. This is handled by LatinIME since the 422 // input logic has no business with IME switching. 423 break; 424 case Constants.CODE_ACTION_NEXT: 425 performEditorAction(EditorInfo.IME_ACTION_NEXT); 426 break; 427 case Constants.CODE_ACTION_PREVIOUS: 428 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 429 break; 430 case Constants.CODE_LANGUAGE_SWITCH: 431 handleLanguageSwitchKey(); 432 break; 433 case Constants.CODE_EMOJI: 434 // Note: Switching emoji keyboard is being handled in 435 // {@link KeyboardState#onCodeInput(int,int)}. 436 break; 437 case Constants.CODE_ALPHA_FROM_EMOJI: 438 // Note: Switching back from Emoji keyboard to the main keyboard is being 439 // handled in {@link KeyboardState#onCodeInput(int,int)}. 440 break; 441 case Constants.CODE_SHIFT_ENTER: 442 // TODO: remove this object 443 final InputTransaction tmpTransaction = new InputTransaction( 444 inputTransaction.mSettingsValues, inputTransaction.mEvent, 445 inputTransaction.mTimestamp, inputTransaction.mSpaceState, 446 inputTransaction.mShiftState); 447 didAutoCorrect = handleNonSpecialCharacter(tmpTransaction, handler); 448 break; 449 default: 450 throw new RuntimeException("Unknown key code : " + event.mKeyCode); 451 } 452 } else { 453 switch (event.mCodePoint) { 454 case Constants.CODE_ENTER: 455 final EditorInfo editorInfo = getCurrentInputEditorInfo(); 456 final int imeOptionsActionId = 457 InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); 458 if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { 459 // Either we have an actionLabel and we should performEditorAction with 460 // actionId regardless of its value. 461 performEditorAction(editorInfo.actionId); 462 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { 463 // We didn't have an actionLabel, but we had another action to execute. 464 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, 465 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it 466 // means there should be an action and the app didn't bother to set a specific 467 // code for it - presumably it only handles one. It does not have to be treated 468 // in any specific way: anything that is not IME_ACTION_NONE should be sent to 469 // performEditorAction. 470 performEditorAction(imeOptionsActionId); 471 } else { 472 // No action label, and the action from imeOptions is NONE: this is a regular 473 // enter key that should input a carriage return. 474 didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); 475 } 476 break; 477 default: 478 didAutoCorrect = handleNonSpecialCharacter(inputTransaction, handler); 479 break; 480 } 481 } 482 if (!didAutoCorrect && event.mKeyCode != Constants.CODE_SHIFT 483 && event.mKeyCode != Constants.CODE_CAPSLOCK 484 && event.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) 485 mLastComposedWord.deactivate(); 486 if (Constants.CODE_DELETE != event.mKeyCode) { 487 mEnteredText = null; 488 } 489 mConnection.endBatchEdit(); 490 return inputTransaction; 491 } 492 493 public void onStartBatchInput(final SettingsValues settingsValues, 494 // TODO: remove these arguments 495 final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { 496 mInputLogicHandler.onStartBatchInput(); 497 handler.showGesturePreviewAndSuggestionStrip( 498 SuggestedWords.EMPTY, false /* dismissGestureFloatingPreviewText */); 499 handler.cancelUpdateSuggestionStrip(); 500 ++mAutoCommitSequenceNumber; 501 mConnection.beginBatchEdit(); 502 if (mWordComposer.isComposingWord()) { 503 if (settingsValues.mIsInternal) { 504 if (mWordComposer.isBatchMode()) { 505 LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ", 506 mWordComposer); 507 } 508 } 509 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 510 // If we are in the middle of a recorrection, we need to commit the recorrection 511 // first so that we can insert the batch input at the current cursor position. 512 resetEntireInputState(mConnection.getExpectedSelectionStart(), 513 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 514 } else if (mWordComposer.isSingleLetter()) { 515 // We auto-correct the previous (typed, not gestured) string iff it's one character 516 // long. The reason for this is, even in the middle of gesture typing, you'll still 517 // tap one-letter words and you want them auto-corrected (typically, "i" in English 518 // should become "I"). However for any longer word, we assume that the reason for 519 // tapping probably is that the word you intend to type is not in the dictionary, 520 // so we do not attempt to correct, on the assumption that if that was a dictionary 521 // word, the user would probably have gestured instead. 522 commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR, 523 handler); 524 } else { 525 commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); 526 } 527 } 528 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 529 if (Character.isLetterOrDigit(codePointBeforeCursor) 530 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { 531 final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() != 532 getCurrentAutoCapsState(settingsValues); 533 mSpaceState = SpaceState.PHANTOM; 534 if (!autoShiftHasBeenOverriden) { 535 // When we change the space state, we need to update the shift state of the 536 // keyboard unless it has been overridden manually. This is happening for example 537 // after typing some letters and a period, then gesturing; the keyboard is not in 538 // caps mode yet, but since a gesture is starting, it should go in caps mode, 539 // unless the user explictly said it should not. 540 keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), 541 getCurrentRecapitalizeState()); 542 } 543 } 544 mConnection.endBatchEdit(); 545 mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( 546 getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()), 547 // Prev word is 1st word before cursor 548 getNthPreviousWordForSuggestion( 549 settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */)); 550 } 551 552 /* The sequence number member is only used in onUpdateBatchInput. It is increased each time 553 * auto-commit happens. The reason we need this is, when auto-commit happens we trim the 554 * input pointers that are held in a singleton, and to know how much to trim we rely on the 555 * results of the suggestion process that is held in mSuggestedWords. 556 * However, the suggestion process is asynchronous, and sometimes we may enter the 557 * onUpdateBatchInput method twice without having recomputed suggestions yet, or having 558 * received new suggestions generated from not-yet-trimmed input pointers. In this case, the 559 * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we 560 * remove an unrelated number of pointers (possibly even more than are left in the input 561 * pointers, leading to a crash). 562 * To avoid that, we increase the sequence number each time we auto-commit and trim the 563 * input pointers, and we do not use any suggested words that have been generated with an 564 * earlier sequence number. 565 */ 566 private int mAutoCommitSequenceNumber = 1; 567 public void onUpdateBatchInput(final SettingsValues settingsValues, 568 final InputPointers batchPointers, 569 // TODO: remove these arguments 570 final KeyboardSwitcher keyboardSwitcher) { 571 if (settingsValues.mPhraseGestureEnabled) { 572 final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate(); 573 // If these suggested words have been generated with out of date input pointers, then 574 // we skip auto-commit (see comments above on the mSequenceNumber member). 575 if (null != candidate 576 && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) { 577 if (candidate.mSourceDict.shouldAutoCommit(candidate)) { 578 final String[] commitParts = candidate.mWord.split(" ", 2); 579 batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord); 580 promotePhantomSpace(settingsValues); 581 mConnection.commitText(commitParts[0], 0); 582 mSpaceState = SpaceState.PHANTOM; 583 keyboardSwitcher.requestUpdatingShiftState( 584 getCurrentAutoCapsState(settingsValues), getCurrentRecapitalizeState()); 585 mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( 586 getActualCapsMode(settingsValues, 587 keyboardSwitcher.getKeyboardShiftMode()), commitParts[0]); 588 ++mAutoCommitSequenceNumber; 589 } 590 } 591 } 592 mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); 593 } 594 595 public void onEndBatchInput(final InputPointers batchPointers) { 596 mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber); 597 ++mAutoCommitSequenceNumber; 598 } 599 600 // TODO: remove this argument 601 public void onCancelBatchInput(final LatinIME.UIHandler handler) { 602 mInputLogicHandler.onCancelBatchInput(); 603 handler.showGesturePreviewAndSuggestionStrip( 604 SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */); 605 } 606 607 // TODO: on the long term, this method should become private, but it will be difficult. 608 // Especially, how do we deal with InputMethodService.onDisplayCompletions? 609 public void setSuggestedWords(final SuggestedWords suggestedWords) { 610 mSuggestedWords = suggestedWords; 611 final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; 612 // Put a blue underline to a word in TextView which will be auto-corrected. 613 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 614 && mWordComposer.isComposingWord()) { 615 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 616 final CharSequence textWithUnderline = 617 getTextWithUnderline(mWordComposer.getTypedWord()); 618 // TODO: when called from an updateSuggestionStrip() call that results from a posted 619 // message, this is called outside any batch edit. Potentially, this may result in some 620 // janky flickering of the screen, although the display speed makes it unlikely in 621 // the practice. 622 mConnection.setComposingText(textWithUnderline, 1); 623 } 624 } 625 626 /** 627 * Handle inputting a code point to the editor. 628 * 629 * Non-special keys are those that generate a single code point. 630 * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that 631 * manage keyboard-related stuff like shift, language switch, settings, layout switch, or 632 * any key that results in multiple code points like the ".com" key. 633 * 634 * @param inputTransaction The transaction in progress. 635 * @return whether this caused an auto-correction to happen. 636 */ 637 private boolean handleNonSpecialCharacter(final InputTransaction inputTransaction, 638 // TODO: remove this argument 639 final LatinIME.UIHandler handler) { 640 final int codePoint = inputTransaction.mEvent.mCodePoint; 641 mSpaceState = SpaceState.NONE; 642 final boolean didAutoCorrect; 643 if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) 644 || Character.getType(codePoint) == Character.OTHER_SYMBOL) { 645 didAutoCorrect = handleSeparator(inputTransaction, 646 inputTransaction.mEvent.isSuggestionStripPress(), handler); 647 if (inputTransaction.mSettingsValues.mIsInternal) { 648 LatinImeLoggerUtils.onSeparator((char)codePoint, 649 inputTransaction.mEvent.mX, inputTransaction.mEvent.mY); 650 } 651 } else { 652 didAutoCorrect = false; 653 if (SpaceState.PHANTOM == inputTransaction.mSpaceState) { 654 if (inputTransaction.mSettingsValues.mIsInternal) { 655 if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) { 656 LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ", 657 mWordComposer); 658 } 659 } 660 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 661 // If we are in the middle of a recorrection, we need to commit the recorrection 662 // first so that we can insert the character at the current cursor position. 663 resetEntireInputState(mConnection.getExpectedSelectionStart(), 664 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 665 } else { 666 commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR); 667 } 668 } 669 handleNonSeparator(inputTransaction.mSettingsValues, inputTransaction); 670 } 671 return didAutoCorrect; 672 } 673 674 /** 675 * Handle a non-separator. 676 * @param settingsValues The current settings values. 677 * @param inputTransaction The transaction in progress. 678 */ 679 private void handleNonSeparator(final SettingsValues settingsValues, 680 final InputTransaction inputTransaction) { 681 final int codePoint = inputTransaction.mEvent.mCodePoint; 682 // TODO: refactor this method to stop flipping isComposingWord around all the time, and 683 // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter 684 // which has the same name as other handle* methods but is not the same. 685 boolean isComposingWord = mWordComposer.isComposingWord(); 686 687 // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. 688 // See onStartBatchInput() to see how to do it. 689 if (SpaceState.PHANTOM == inputTransaction.mSpaceState 690 && !settingsValues.isWordConnector(codePoint)) { 691 if (isComposingWord) { 692 // Sanity check 693 throw new RuntimeException("Should not be composing here"); 694 } 695 promotePhantomSpace(settingsValues); 696 } 697 698 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 699 // If we are in the middle of a recorrection, we need to commit the recorrection 700 // first so that we can insert the character at the current cursor position. 701 resetEntireInputState(mConnection.getExpectedSelectionStart(), 702 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 703 isComposingWord = false; 704 } 705 // We want to find out whether to start composing a new word with this character. If so, 706 // we need to reset the composing state and switch isComposingWord. The order of the 707 // tests is important for good performance. 708 // We only start composing if we're not already composing. 709 if (!isComposingWord 710 // We only start composing if this is a word code point. Essentially that means it's a 711 // a letter or a word connector. 712 && settingsValues.isWordCodePoint(codePoint) 713 // We never go into composing state if suggestions are not requested. 714 && settingsValues.isSuggestionsRequested() && 715 // In languages with spaces, we only start composing a word when we are not already 716 // touching a word. In languages without spaces, the above conditions are sufficient. 717 (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations) 718 || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces)) { 719 // Reset entirely the composing state anyway, then start composing a new word unless 720 // the character is a single quote or a dash. The idea here is, single quote and dash 721 // are not separators and they should be treated as normal characters, except in the 722 // first position where they should not start composing a word. 723 isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint 724 && Constants.CODE_DASH != codePoint); 725 // Here we don't need to reset the last composed word. It will be reset 726 // when we commit this one, if we ever do; if on the other hand we backspace 727 // it entirely and resume suggestions on the previous word, we'd like to still 728 // have touch coordinates for it. 729 resetComposingState(false /* alsoResetLastComposedWord */); 730 } 731 if (isComposingWord) { 732 mWordComposer.add(inputTransaction.mEvent); 733 // If it's the first letter, make note of auto-caps state 734 if (mWordComposer.isSingleLetter()) { 735 // We pass 1 to getPreviousWordForSuggestion because we were not composing a word 736 // yet, so the word we want is the 1st word before the cursor. 737 mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( 738 inputTransaction.mShiftState, getNthPreviousWordForSuggestion( 739 settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */)); 740 } 741 mConnection.setComposingText(getTextWithUnderline( 742 mWordComposer.getTypedWord()), 1); 743 } else { 744 final boolean swapWeakSpace = maybeStripSpace(inputTransaction, 745 inputTransaction.mEvent.isSuggestionStripPress()); 746 747 sendKeyCodePoint(settingsValues, codePoint); 748 749 if (swapWeakSpace) { 750 swapSwapperAndSpace(inputTransaction); 751 mSpaceState = SpaceState.WEAK; 752 } 753 // In case the "add to dictionary" hint was still displayed. 754 mSuggestionStripViewAccessor.dismissAddToDictionaryHint(); 755 } 756 inputTransaction.setRequiresUpdateSuggestions(); 757 if (settingsValues.mIsInternal) { 758 LatinImeLoggerUtils.onNonSeparator((char)codePoint, inputTransaction.mEvent.mX, 759 inputTransaction.mEvent.mY); 760 } 761 } 762 763 /** 764 * Handle input of a separator code point. 765 * @param inputTransaction The transaction in progress. 766 * @param isFromSuggestionStrip whether this code point comes from the suggestion strip. 767 * @return whether this caused an auto-correction to happen. 768 */ 769 private boolean handleSeparator(final InputTransaction inputTransaction, 770 final boolean isFromSuggestionStrip, 771 // TODO: remove this argument 772 final LatinIME.UIHandler handler) { 773 final int codePoint = inputTransaction.mEvent.mCodePoint; 774 boolean didAutoCorrect = false; 775 // We avoid sending spaces in languages without spaces if we were composing. 776 final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint 777 && !inputTransaction.mSettingsValues.mSpacingAndPunctuations 778 .mCurrentLanguageHasSpaces 779 && mWordComposer.isComposingWord(); 780 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 781 // If we are in the middle of a recorrection, we need to commit the recorrection 782 // first so that we can insert the separator at the current cursor position. 783 resetEntireInputState(mConnection.getExpectedSelectionStart(), 784 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 785 } 786 // isComposingWord() may have changed since we stored wasComposing 787 if (mWordComposer.isComposingWord()) { 788 if (inputTransaction.mSettingsValues.mCorrectionEnabled) { 789 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR 790 : StringUtils.newSingleCodePointString(codePoint); 791 commitCurrentAutoCorrection(inputTransaction.mSettingsValues, separator, handler); 792 didAutoCorrect = true; 793 } else { 794 commitTyped(inputTransaction.mSettingsValues, 795 StringUtils.newSingleCodePointString(codePoint)); 796 } 797 } 798 799 final boolean swapWeakSpace = maybeStripSpace(inputTransaction, isFromSuggestionStrip); 800 801 final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint 802 && mConnection.isInsideDoubleQuoteOrAfterDigit(); 803 804 final boolean needsPrecedingSpace; 805 if (SpaceState.PHANTOM != inputTransaction.mSpaceState) { 806 needsPrecedingSpace = false; 807 } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { 808 // Double quotes behave like they are usually preceded by space iff we are 809 // not inside a double quote or after a digit. 810 needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit; 811 } else { 812 needsPrecedingSpace = inputTransaction.mSettingsValues.isUsuallyPrecededBySpace( 813 codePoint); 814 } 815 816 if (needsPrecedingSpace) { 817 promotePhantomSpace(inputTransaction.mSettingsValues); 818 } 819 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 820 ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord()); 821 } 822 823 if (!shouldAvoidSendingCode) { 824 sendKeyCodePoint(inputTransaction.mSettingsValues, codePoint); 825 } 826 827 if (Constants.CODE_SPACE == codePoint) { 828 if (inputTransaction.mSettingsValues.isSuggestionsRequested()) { 829 if (maybeDoubleSpacePeriod(inputTransaction)) { 830 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 831 mSpaceState = SpaceState.DOUBLE; 832 } else if (!mSuggestedWords.isPunctuationSuggestions()) { 833 mSpaceState = SpaceState.WEAK; 834 } 835 } 836 837 startDoubleSpacePeriodCountdown(inputTransaction); 838 inputTransaction.setRequiresUpdateSuggestions(); 839 } else { 840 if (swapWeakSpace) { 841 swapSwapperAndSpace(inputTransaction); 842 mSpaceState = SpaceState.SWAP_PUNCTUATION; 843 } else if ((SpaceState.PHANTOM == inputTransaction.mSpaceState 844 && inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) 845 || (Constants.CODE_DOUBLE_QUOTE == codePoint 846 && isInsideDoubleQuoteOrAfterDigit)) { 847 // If we are in phantom space state, and the user presses a separator, we want to 848 // stay in phantom space state so that the next keypress has a chance to add the 849 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 850 // then insert a comma and go on to typing the next word, I want the space to be 851 // inserted automatically before the next word, the same way it is when I don't 852 // input the comma. A double quote behaves like it's usually followed by space if 853 // we're inside a double quote. 854 // The case is a little different if the separator is a space stripper. Such a 855 // separator does not normally need a space on the right (that's the difference 856 // between swappers and strippers), so we should not stay in phantom space state if 857 // the separator is a stripper. Hence the additional test above. 858 mSpaceState = SpaceState.PHANTOM; 859 } 860 861 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 862 // already displayed or not, so it's okay. 863 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 864 } 865 866 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 867 return didAutoCorrect; 868 } 869 870 /** 871 * Handle a press on the backspace key. 872 * @param inputTransaction The transaction in progress. 873 */ 874 private void handleBackspace(final InputTransaction inputTransaction) { 875 mSpaceState = SpaceState.NONE; 876 mDeleteCount++; 877 878 // In many cases, we may have to put the keyboard in auto-shift state again. However 879 // we want to wait a few milliseconds before doing it to avoid the keyboard flashing 880 // during key repeat. 881 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_LATER); 882 883 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 884 // If we are in the middle of a recorrection, we need to commit the recorrection 885 // first so that we can remove the character at the current cursor position. 886 resetEntireInputState(mConnection.getExpectedSelectionStart(), 887 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 888 // When we exit this if-clause, mWordComposer.isComposingWord() will return false. 889 } 890 if (mWordComposer.isComposingWord()) { 891 if (mWordComposer.isBatchMode()) { 892 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 893 final String word = mWordComposer.getTypedWord(); 894 ResearchLogger.latinIME_handleBackspace_batch(word, 1); 895 } 896 final String rejectedSuggestion = mWordComposer.getTypedWord(); 897 mWordComposer.reset(); 898 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); 899 } else { 900 mWordComposer.deleteLast(inputTransaction.mEvent); 901 } 902 mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 903 inputTransaction.setRequiresUpdateSuggestions(); 904 if (!mWordComposer.isComposingWord()) { 905 // If we just removed the last character, auto-caps mode may have changed so we 906 // need to re-evaluate. 907 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 908 } 909 } else { 910 if (mLastComposedWord.canRevertCommit()) { 911 if (inputTransaction.mSettingsValues.mIsInternal) { 912 LatinImeLoggerUtils.onAutoCorrectionCancellation(); 913 } 914 revertCommit(inputTransaction); 915 return; 916 } 917 if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { 918 // Cancel multi-character input: remove the text we just entered. 919 // This is triggered on backspace after a key that inputs multiple characters, 920 // like the smiley key or the .com key. 921 mConnection.deleteSurroundingText(mEnteredText.length(), 0); 922 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 923 ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText); 924 } 925 mEnteredText = null; 926 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 927 // In addition we know that spaceState is false, and that we should not be 928 // reverting any autocorrect at this point. So we can safely return. 929 return; 930 } 931 if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { 932 cancelDoubleSpacePeriodCountdown(); 933 if (mConnection.revertDoubleSpacePeriod()) { 934 // No need to reset mSpaceState, it has already be done (that's why we 935 // receive it as a parameter) 936 return; 937 } 938 } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { 939 if (mConnection.revertSwapPunctuation()) { 940 // Likewise 941 return; 942 } 943 } 944 945 // No cancelling of commit/double space/swap: we have a regular backspace. 946 // We should backspace one char and restart suggestion if at the end of a word. 947 if (mConnection.hasSelection()) { 948 // If there is a selection, remove it. 949 final int numCharsDeleted = mConnection.getExpectedSelectionEnd() 950 - mConnection.getExpectedSelectionStart(); 951 mConnection.setSelection(mConnection.getExpectedSelectionEnd(), 952 mConnection.getExpectedSelectionEnd()); 953 mConnection.deleteSurroundingText(numCharsDeleted, 0); 954 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 955 ResearchLogger.latinIME_handleBackspace(numCharsDeleted, 956 false /* shouldUncommitLogUnit */); 957 } 958 } else { 959 // There is no selection, just delete one character. 960 if (Constants.NOT_A_CURSOR_POSITION == mConnection.getExpectedSelectionEnd()) { 961 // This should never happen. 962 Log.e(TAG, "Backspace when we don't know the selection position"); 963 } 964 if (inputTransaction.mSettingsValues.isBeforeJellyBean() || 965 inputTransaction.mSettingsValues.mInputAttributes.isTypeNull()) { 966 // There are two possible reasons to send a key event: either the field has 967 // type TYPE_NULL, in which case the keyboard should send events, or we are 968 // running in backward compatibility mode. Before Jelly bean, the keyboard 969 // would simulate a hardware keyboard event on pressing enter or delete. This 970 // is bad for many reasons (there are race conditions with commits) but some 971 // applications are relying on this behavior so we continue to support it for 972 // older apps, so we retain this behavior if the app has target SDK < JellyBean. 973 sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); 974 if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { 975 sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); 976 } 977 } else { 978 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 979 if (codePointBeforeCursor == Constants.NOT_A_CODE) { 980 // HACK for backward compatibility with broken apps that haven't realized 981 // yet that hardware keyboards are not the only way of inputting text. 982 // Nothing to delete before the cursor. We should not do anything, but many 983 // broken apps expect something to happen in this case so that they can 984 // catch it and have their broken interface react. If you need the keyboard 985 // to do this, you're doing it wrong -- please fix your app. 986 mConnection.deleteSurroundingText(1, 0); 987 return; 988 } 989 final int lengthToDelete = 990 Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; 991 mConnection.deleteSurroundingText(lengthToDelete, 0); 992 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 993 ResearchLogger.latinIME_handleBackspace(lengthToDelete, 994 true /* shouldUncommitLogUnit */); 995 } 996 if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { 997 final int codePointBeforeCursorToDeleteAgain = 998 mConnection.getCodePointBeforeCursor(); 999 if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { 1000 final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( 1001 codePointBeforeCursorToDeleteAgain) ? 2 : 1; 1002 mConnection.deleteSurroundingText(lengthToDeleteAgain, 0); 1003 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1004 ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain, 1005 true /* shouldUncommitLogUnit */); 1006 } 1007 } 1008 } 1009 } 1010 } 1011 if (inputTransaction.mSettingsValues.isSuggestionStripVisible() 1012 && inputTransaction.mSettingsValues.mSpacingAndPunctuations 1013 .mCurrentLanguageHasSpaces 1014 && !mConnection.isCursorFollowedByWordCharacter( 1015 inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { 1016 restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, 1017 true /* includeResumedWordInSuggestions */); 1018 } 1019 // We just removed at least one character. We need to update the auto-caps state. 1020 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 1021 } 1022 } 1023 1024 /** 1025 * Handle a press on the language switch key (the "globe key") 1026 */ 1027 private void handleLanguageSwitchKey() { 1028 mLatinIME.switchToNextSubtype(); 1029 } 1030 1031 /** 1032 * Swap a space with a space-swapping punctuation sign. 1033 * 1034 * This method will check that there are two characters before the cursor and that the first 1035 * one is a space before it does the actual swapping. 1036 * @param inputTransaction The transaction in progress. 1037 */ 1038 private void swapSwapperAndSpace(final InputTransaction inputTransaction) { 1039 final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0); 1040 // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called. 1041 if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) { 1042 mConnection.deleteSurroundingText(2, 0); 1043 final String text = lastTwo.charAt(1) + " "; 1044 mConnection.commitText(text, 1); 1045 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1046 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text); 1047 } 1048 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 1049 } 1050 } 1051 1052 /* 1053 * Strip a trailing space if necessary and returns whether it's a swap weak space situation. 1054 * @param inputTransaction The transaction in progress. 1055 * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip. 1056 * @return whether we should swap the space instead of removing it. 1057 */ 1058 private boolean maybeStripSpace(final InputTransaction inputTransaction, 1059 final boolean isFromSuggestionStrip) { 1060 final int codePoint = inputTransaction.mEvent.mCodePoint; 1061 if (Constants.CODE_ENTER == codePoint && 1062 SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { 1063 mConnection.removeTrailingSpace(); 1064 return false; 1065 } 1066 if ((SpaceState.WEAK == inputTransaction.mSpaceState 1067 || SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) 1068 && isFromSuggestionStrip) { 1069 if (inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(codePoint)) { 1070 return false; 1071 } 1072 if (inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) { 1073 return true; 1074 } 1075 mConnection.removeTrailingSpace(); 1076 } 1077 return false; 1078 } 1079 1080 public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) { 1081 mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp; 1082 } 1083 1084 public void cancelDoubleSpacePeriodCountdown() { 1085 mDoubleSpacePeriodCountdownStart = 0; 1086 } 1087 1088 public boolean isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction) { 1089 return inputTransaction.mTimestamp - mDoubleSpacePeriodCountdownStart 1090 < inputTransaction.mSettingsValues.mDoubleSpacePeriodTimeout; 1091 } 1092 1093 /** 1094 * Apply the double-space-to-period transformation if applicable. 1095 * 1096 * The double-space-to-period transformation means that we replace two spaces with a 1097 * period-space sequence of characters. This typically happens when the user presses space 1098 * twice in a row quickly. 1099 * This method will check that the double-space-to-period is active in settings, that the 1100 * two spaces have been input close enough together, and that the previous character allows 1101 * for the transformation to take place. If all of these conditions are fulfilled, this 1102 * method applies the transformation and returns true. Otherwise, it does nothing and 1103 * returns false. 1104 * 1105 * @param inputTransaction The transaction in progress. 1106 * @return true if we applied the double-space-to-period transformation, false otherwise. 1107 */ 1108 private boolean maybeDoubleSpacePeriod(final InputTransaction inputTransaction) { 1109 if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod) return false; 1110 if (!isDoubleSpacePeriodCountdownActive(inputTransaction)) return false; 1111 // We only do this when we see two spaces and an accepted code point before the cursor. 1112 // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars. 1113 final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0); 1114 if (null == lastThree) return false; 1115 final int length = lastThree.length(); 1116 if (length < 3) return false; 1117 if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false; 1118 if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false; 1119 // We know there are spaces in pos -1 and -2, and we have at least three chars. 1120 // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space, 1121 // so this is fine. 1122 final int firstCodePoint = 1123 Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ? 1124 Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3); 1125 if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { 1126 cancelDoubleSpacePeriodCountdown(); 1127 mConnection.deleteSurroundingText(2, 0); 1128 final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations 1129 .mSentenceSeparatorAndSpace; 1130 mConnection.commitText(textToInsert, 1); 1131 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1132 ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert, 1133 false /* isBatchMode */); 1134 } 1135 mWordComposer.discardPreviousWordForSuggestion(); 1136 return true; 1137 } 1138 return false; 1139 } 1140 1141 /** 1142 * Returns whether this code point can be followed by the double-space-to-period transformation. 1143 * 1144 * See #maybeDoubleSpaceToPeriod for details. 1145 * Generally, most word characters can be followed by the double-space-to-period transformation, 1146 * while most punctuation can't. Some punctuation however does allow for this to take place 1147 * after them, like the closing parenthesis for example. 1148 * 1149 * @param codePoint the code point after which we may want to apply the transformation 1150 * @return whether it's fine to apply the transformation after this code point. 1151 */ 1152 private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { 1153 // TODO: This should probably be a blacklist rather than a whitelist. 1154 // TODO: This should probably be language-dependant... 1155 return Character.isLetterOrDigit(codePoint) 1156 || codePoint == Constants.CODE_SINGLE_QUOTE 1157 || codePoint == Constants.CODE_DOUBLE_QUOTE 1158 || codePoint == Constants.CODE_CLOSING_PARENTHESIS 1159 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET 1160 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET 1161 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET 1162 || codePoint == Constants.CODE_PLUS 1163 || codePoint == Constants.CODE_PERCENT 1164 || Character.getType(codePoint) == Character.OTHER_SYMBOL; 1165 } 1166 1167 /** 1168 * Performs a recapitalization event. 1169 * @param settingsValues The current settings values. 1170 */ 1171 private void performRecapitalization(final SettingsValues settingsValues) { 1172 if (!mConnection.hasSelection()) { 1173 return; // No selection 1174 } 1175 // If we have a recapitalize in progress, use it; otherwise, create a new one. 1176 if (!mRecapitalizeStatus.isActive() 1177 || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), 1178 mConnection.getExpectedSelectionEnd())) { 1179 final CharSequence selectedText = 1180 mConnection.getSelectedText(0 /* flags, 0 for no styles */); 1181 if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection 1182 mRecapitalizeStatus.initialize(mConnection.getExpectedSelectionStart(), 1183 mConnection.getExpectedSelectionEnd(), selectedText.toString(), 1184 settingsValues.mLocale, 1185 settingsValues.mSpacingAndPunctuations.mSortedWordSeparators); 1186 // We trim leading and trailing whitespace. 1187 mRecapitalizeStatus.trim(); 1188 } 1189 mConnection.finishComposingText(); 1190 mRecapitalizeStatus.rotate(); 1191 final int numCharsDeleted = mConnection.getExpectedSelectionEnd() 1192 - mConnection.getExpectedSelectionStart(); 1193 mConnection.setSelection(mConnection.getExpectedSelectionEnd(), 1194 mConnection.getExpectedSelectionEnd()); 1195 mConnection.deleteSurroundingText(numCharsDeleted, 0); 1196 mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); 1197 mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), 1198 mRecapitalizeStatus.getNewCursorEnd()); 1199 } 1200 1201 private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, 1202 final String suggestion, final String prevWord) { 1203 // If correction is not enabled, we don't add words to the user history dictionary. 1204 // That's to avoid unintended additions in some sensitive fields, or fields that 1205 // expect to receive non-words. 1206 if (!settingsValues.mCorrectionEnabled) return; 1207 1208 if (TextUtils.isEmpty(suggestion)) return; 1209 final boolean wasAutoCapitalized = 1210 mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps(); 1211 final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( 1212 System.currentTimeMillis()); 1213 mSuggest.mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, prevWord, 1214 timeStampInSeconds); 1215 } 1216 1217 public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) { 1218 // Check if we have a suggestion engine attached. 1219 if (!settingsValues.isSuggestionsRequested()) { 1220 if (mWordComposer.isComposingWord()) { 1221 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 1222 + "requested!"); 1223 } 1224 return; 1225 } 1226 1227 if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { 1228 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1229 return; 1230 } 1231 1232 final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<SuggestedWords>(); 1233 mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, 1234 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { 1235 @Override 1236 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1237 final String typedWord = mWordComposer.getTypedWord(); 1238 // Show new suggestions if we have at least one. Otherwise keep the old 1239 // suggestions with the new typed word. Exception: if the length of the 1240 // typed word is <= 1 (after a deletion typically) we clear old suggestions. 1241 if (suggestedWords.size() > 1 || typedWord.length() <= 1) { 1242 holder.set(suggestedWords); 1243 } else { 1244 holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords)); 1245 } 1246 } 1247 } 1248 ); 1249 1250 // This line may cause the current thread to wait. 1251 final SuggestedWords suggestedWords = holder.get(null, 1252 Constants.GET_SUGGESTED_WORDS_TIMEOUT); 1253 if (suggestedWords != null) { 1254 mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); 1255 } 1256 } 1257 1258 /** 1259 * Check if the cursor is touching a word. If so, restart suggestions on this word, else 1260 * do nothing. 1261 * 1262 * @param settingsValues the current values of the settings. 1263 * @param includeResumedWordInSuggestions whether to include the word on which we resume 1264 * suggestions in the suggestion list. 1265 */ 1266 // TODO: make this private. 1267 public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, 1268 final boolean includeResumedWordInSuggestions) { 1269 // HACK: We may want to special-case some apps that exhibit bad behavior in case of 1270 // recorrection. This is a temporary, stopgap measure that will be removed later. 1271 // TODO: remove this. 1272 if (settingsValues.isBrokenByRecorrection() 1273 // Recorrection is not supported in languages without spaces because we don't know 1274 // how to segment them yet. 1275 || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 1276 // If no suggestions are requested, don't try restarting suggestions. 1277 || !settingsValues.isSuggestionsRequested() 1278 // If the cursor is not touching a word, or if there is a selection, return right away. 1279 || mConnection.hasSelection() 1280 // If we don't know the cursor location, return. 1281 || mConnection.getExpectedSelectionStart() < 0) { 1282 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1283 return; 1284 } 1285 final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); 1286 if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations)) { 1287 // Show predictions. 1288 mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime( 1289 WordComposer.CAPS_MODE_OFF, 1290 getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, 1)); 1291 mLatinIME.mHandler.postUpdateSuggestionStrip(); 1292 return; 1293 } 1294 final TextRange range = mConnection.getWordRangeAtCursor( 1295 settingsValues.mSpacingAndPunctuations.mSortedWordSeparators, 1296 0 /* additionalPrecedingWordsCount */); 1297 if (null == range) return; // Happens if we don't have an input connection at all 1298 if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out. 1299 // If for some strange reason (editor bug or so) we measure the text before the cursor as 1300 // longer than what the entire text is supposed to be, the safe thing to do is bail out. 1301 if (range.mHasUrlSpans) return; // If there are links, we don't resume suggestions. Making 1302 // edits to a linkified text through batch commands would ruin the URL spans, and unless 1303 // we take very complicated steps to preserve the whole link, we can't do things right so 1304 // we just do not resume because it's safer. 1305 final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 1306 if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; 1307 final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList(); 1308 final String typedWord = range.mWord.toString(); 1309 if (includeResumedWordInSuggestions) { 1310 suggestions.add(new SuggestedWordInfo(typedWord, 1311 SuggestedWords.MAX_SUGGESTIONS + 1, 1312 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, 1313 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 1314 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */)); 1315 } 1316 if (!isResumableWord(settingsValues, typedWord)) { 1317 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1318 return; 1319 } 1320 int i = 0; 1321 for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { 1322 for (final String s : span.getSuggestions()) { 1323 ++i; 1324 if (!TextUtils.equals(s, typedWord)) { 1325 suggestions.add(new SuggestedWordInfo(s, 1326 SuggestedWords.MAX_SUGGESTIONS - i, 1327 SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, 1328 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 1329 SuggestedWordInfo.NOT_A_CONFIDENCE 1330 /* autoCommitFirstWordConfidence */)); 1331 } 1332 } 1333 } 1334 final int[] codePoints = StringUtils.toCodePointArray(typedWord); 1335 mWordComposer.setComposingWord(codePoints, 1336 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints), 1337 getNthPreviousWordForSuggestion(settingsValues.mSpacingAndPunctuations, 1338 // We want the previous word for suggestion. If we have chars in the word 1339 // before the cursor, then we want the word before that, hence 2; otherwise, 1340 // we want the word immediately before the cursor, hence 1. 1341 0 == numberOfCharsInWordBeforeCursor ? 1 : 2)); 1342 mWordComposer.setCursorPositionWithinWord( 1343 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor)); 1344 mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, 1345 expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); 1346 if (suggestions.isEmpty()) { 1347 // We come here if there weren't any suggestion spans on this word. We will try to 1348 // compute suggestions for it instead. 1349 mInputLogicHandler.getSuggestedWords(Suggest.SESSION_TYPING, 1350 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { 1351 @Override 1352 public void onGetSuggestedWords( 1353 final SuggestedWords suggestedWordsIncludingTypedWord) { 1354 final SuggestedWords suggestedWords; 1355 if (suggestedWordsIncludingTypedWord.size() > 1 1356 && !includeResumedWordInSuggestions) { 1357 // We were able to compute new suggestions for this word. 1358 // Remove the typed word, since we don't want to display it in this 1359 // case. The #getSuggestedWordsExcludingTypedWord() method sets 1360 // willAutoCorrect to false. 1361 suggestedWords = suggestedWordsIncludingTypedWord 1362 .getSuggestedWordsExcludingTypedWord(); 1363 } else { 1364 // No saved suggestions, and we were unable to compute any good one 1365 // either. Rather than displaying an empty suggestion strip, we'll 1366 // display the original word alone in the middle. 1367 // Since there is only one word, willAutoCorrect is false. 1368 suggestedWords = suggestedWordsIncludingTypedWord; 1369 } 1370 mIsAutoCorrectionIndicatorOn = false; 1371 mLatinIME.mHandler.showSuggestionStrip(suggestedWords); 1372 }}); 1373 } else { 1374 // We found suggestion spans in the word. We'll create the SuggestedWords out of 1375 // them, and make willAutoCorrect false. We make typedWordValid false, because the 1376 // color of the word in the suggestion strip changes according to this parameter, 1377 // and false gives the correct color. 1378 final SuggestedWords suggestedWords = new SuggestedWords(suggestions, 1379 null /* rawSuggestions */, typedWord, 1380 false /* typedWordValid */, false /* willAutoCorrect */, 1381 false /* isObsoleteSuggestions */, false /* isPrediction */, 1382 SuggestedWords.NOT_A_SEQUENCE_NUMBER); 1383 mIsAutoCorrectionIndicatorOn = false; 1384 mLatinIME.mHandler.showSuggestionStrip(suggestedWords); 1385 } 1386 } 1387 1388 /** 1389 * Reverts a previous commit with auto-correction. 1390 * 1391 * This is triggered upon pressing backspace just after a commit with auto-correction. 1392 * 1393 * @param inputTransaction The transaction in progress. 1394 */ 1395 private void revertCommit(final InputTransaction inputTransaction) { 1396 final String previousWord = mLastComposedWord.mPrevWord; 1397 final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; 1398 final CharSequence committedWord = mLastComposedWord.mCommittedWord; 1399 final String committedWordString = committedWord.toString(); 1400 final int cancelLength = committedWord.length(); 1401 // We want java chars, not codepoints for the following. 1402 final int separatorLength = mLastComposedWord.mSeparatorString.length(); 1403 // TODO: should we check our saved separator against the actual contents of the text view? 1404 final int deleteLength = cancelLength + separatorLength; 1405 if (LatinImeLogger.sDBG) { 1406 if (mWordComposer.isComposingWord()) { 1407 throw new RuntimeException("revertCommit, but we are composing a word"); 1408 } 1409 final CharSequence wordBeforeCursor = 1410 mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); 1411 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 1412 throw new RuntimeException("revertCommit check failed: we thought we were " 1413 + "reverting \"" + committedWord 1414 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 1415 } 1416 } 1417 mConnection.deleteSurroundingText(deleteLength, 0); 1418 if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) { 1419 mSuggest.mDictionaryFacilitator.cancelAddingUserHistory( 1420 previousWord, committedWordString); 1421 } 1422 final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString; 1423 final SpannableString textToCommit = new SpannableString(stringToCommit); 1424 if (committedWord instanceof SpannableString) { 1425 final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; 1426 final Object[] spans = committedWordWithSuggestionSpans.getSpans(0, 1427 committedWord.length(), Object.class); 1428 final int lastCharIndex = textToCommit.length() - 1; 1429 // We will collect all suggestions in the following array. 1430 final ArrayList<String> suggestions = CollectionUtils.newArrayList(); 1431 // First, add the committed word to the list of suggestions. 1432 suggestions.add(committedWordString); 1433 for (final Object span : spans) { 1434 // If this is a suggestion span, we check that the locale is the right one, and 1435 // that the word is not the committed word. That should mostly be the case. 1436 // Given this, we add it to the list of suggestions, otherwise we discard it. 1437 if (span instanceof SuggestionSpan) { 1438 final SuggestionSpan suggestionSpan = (SuggestionSpan)span; 1439 if (!suggestionSpan.getLocale().equals( 1440 inputTransaction.mSettingsValues.mLocale.toString())) { 1441 continue; 1442 } 1443 for (final String suggestion : suggestionSpan.getSuggestions()) { 1444 if (!suggestion.equals(committedWordString)) { 1445 suggestions.add(suggestion); 1446 } 1447 } 1448 } else { 1449 // If this is not a suggestion span, we just add it as is. 1450 textToCommit.setSpan(span, 0 /* start */, lastCharIndex /* end */, 1451 committedWordWithSuggestionSpans.getSpanFlags(span)); 1452 } 1453 } 1454 // Add the suggestion list to the list of suggestions. 1455 textToCommit.setSpan(new SuggestionSpan(inputTransaction.mSettingsValues.mLocale, 1456 suggestions.toArray(new String[suggestions.size()]), 0 /* flags */), 1457 0 /* start */, lastCharIndex /* end */, 0 /* flags */); 1458 } 1459 if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { 1460 // For languages with spaces, we revert to the typed string, but the cursor is still 1461 // after the separator so we don't resume suggestions. If the user wants to correct 1462 // the word, they have to press backspace again. 1463 mConnection.commitText(textToCommit, 1); 1464 } else { 1465 // For languages without spaces, we revert the typed string but the cursor is flush 1466 // with the typed word, so we need to resume suggestions right away. 1467 final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); 1468 mWordComposer.setComposingWord(codePoints, 1469 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints), previousWord); 1470 mConnection.setComposingText(textToCommit, 1); 1471 } 1472 if (inputTransaction.mSettingsValues.mIsInternal) { 1473 LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString, 1474 Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE); 1475 } 1476 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1477 ResearchLogger.latinIME_revertCommit(committedWord.toString(), 1478 originallyTypedWord.toString(), 1479 mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString); 1480 } 1481 // Don't restart suggestion yet. We'll restart if the user deletes the 1482 // separator. 1483 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1484 // We have a separator between the word and the cursor: we should show predictions. 1485 inputTransaction.setRequiresUpdateSuggestions(); 1486 } 1487 1488 /** 1489 * Factor in auto-caps and manual caps and compute the current caps mode. 1490 * @param settingsValues the current settings values. 1491 * @param keyboardShiftMode the current shift mode of the keyboard. See 1492 * KeyboardSwitcher#getKeyboardShiftMode() for possible values. 1493 * @return the actual caps mode the keyboard is in right now. 1494 */ 1495 private int getActualCapsMode(final SettingsValues settingsValues, 1496 final int keyboardShiftMode) { 1497 if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) { 1498 return keyboardShiftMode; 1499 } 1500 final int auto = getCurrentAutoCapsState(settingsValues); 1501 if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { 1502 return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; 1503 } 1504 if (0 != auto) { 1505 return WordComposer.CAPS_MODE_AUTO_SHIFTED; 1506 } 1507 return WordComposer.CAPS_MODE_OFF; 1508 } 1509 1510 /** 1511 * Gets the current auto-caps state, factoring in the space state. 1512 * 1513 * This method tries its best to do this in the most efficient possible manner. It avoids 1514 * getting text from the editor if possible at all. 1515 * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it 1516 * needs to know auto caps state to display the right layout. 1517 * 1518 * @param settingsValues the relevant settings values 1519 * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF. 1520 */ 1521 public int getCurrentAutoCapsState(final SettingsValues settingsValues) { 1522 if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; 1523 1524 final EditorInfo ei = getCurrentInputEditorInfo(); 1525 if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; 1526 final int inputType = ei.inputType; 1527 // Warning: this depends on mSpaceState, which may not be the most current value. If 1528 // mSpaceState gets updated later, whoever called this may need to be told about it. 1529 return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations, 1530 SpaceState.PHANTOM == mSpaceState); 1531 } 1532 1533 public int getCurrentRecapitalizeState() { 1534 if (!mRecapitalizeStatus.isActive() 1535 || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), 1536 mConnection.getExpectedSelectionEnd())) { 1537 // Not recapitalizing at the moment 1538 return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; 1539 } 1540 return mRecapitalizeStatus.getCurrentMode(); 1541 } 1542 1543 /** 1544 * @return the editor info for the current editor 1545 */ 1546 private EditorInfo getCurrentInputEditorInfo() { 1547 return mLatinIME.getCurrentInputEditorInfo(); 1548 } 1549 1550 /** 1551 * Get the nth previous word before the cursor as context for the suggestion process. 1552 * @param spacingAndPunctuations the current spacing and punctuations settings. 1553 * @param nthPreviousWord reverse index of the word to get (1-indexed) 1554 * @return the nth previous word before the cursor. 1555 */ 1556 // TODO: Make this private 1557 public CharSequence getNthPreviousWordForSuggestion( 1558 final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) { 1559 if (spacingAndPunctuations.mCurrentLanguageHasSpaces) { 1560 // If we are typing in a language with spaces we can just look up the previous 1561 // word from textview. 1562 return mConnection.getNthPreviousWord(spacingAndPunctuations, nthPreviousWord); 1563 } else { 1564 return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null 1565 : mLastComposedWord.mCommittedWord; 1566 } 1567 } 1568 1569 /** 1570 * Tests the passed word for resumability. 1571 * 1572 * We can resume suggestions on words whose first code point is a word code point (with some 1573 * nuances: check the code for details). 1574 * 1575 * @param settings the current values of the settings. 1576 * @param word the word to evaluate. 1577 * @return whether it's fine to resume suggestions on this word. 1578 */ 1579 private static boolean isResumableWord(final SettingsValues settings, final String word) { 1580 final int firstCodePoint = word.codePointAt(0); 1581 return settings.isWordCodePoint(firstCodePoint) 1582 && Constants.CODE_SINGLE_QUOTE != firstCodePoint 1583 && Constants.CODE_DASH != firstCodePoint; 1584 } 1585 1586 /** 1587 * @param actionId the action to perform 1588 */ 1589 private void performEditorAction(final int actionId) { 1590 mConnection.performEditorAction(actionId); 1591 } 1592 1593 /** 1594 * Perform the processing specific to inputting TLDs. 1595 * 1596 * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific 1597 * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type 1598 * of character in onCodeInput, but since this gets inputted as a whole string we need to 1599 * do it here specifically. Then, if the last character before the cursor is a period, then 1600 * we cut the dot at the start of ".com". This is because humans tend to type "www.google." 1601 * and then press the ".com" key and instinctively don't expect to get "www.google..com". 1602 * 1603 * @param text the raw text supplied to onTextInput 1604 * @return the text to actually send to the editor 1605 */ 1606 private String performSpecificTldProcessingOnTextInput(final String text) { 1607 if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD 1608 || !Character.isLetter(text.charAt(1))) { 1609 // Not a tld: do nothing. 1610 return text; 1611 } 1612 // We have a TLD (or something that looks like this): make sure we don't add 1613 // a space even if currently in phantom mode. 1614 mSpaceState = SpaceState.NONE; 1615 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1616 // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT. 1617 if (Constants.CODE_PERIOD == codePointBeforeCursor) { 1618 return text.substring(1); 1619 } else { 1620 return text; 1621 } 1622 } 1623 1624 /** 1625 * Handle a press on the settings key. 1626 */ 1627 private void onSettingsKeyPressed() { 1628 mLatinIME.displaySettingsDialog(); 1629 } 1630 1631 /** 1632 * Resets the whole input state to the starting state. 1633 * 1634 * This will clear the composing word, reset the last composed word, clear the suggestion 1635 * strip and tell the input connection about it so that it can refresh its caches. 1636 * 1637 * @param newSelStart the new selection start, in java characters. 1638 * @param newSelEnd the new selection end, in java characters. 1639 * @param clearSuggestionStrip whether this method should clear the suggestion strip. 1640 */ 1641 // TODO: how is this different from startInput ?! 1642 private void resetEntireInputState(final int newSelStart, final int newSelEnd, 1643 final boolean clearSuggestionStrip) { 1644 final boolean shouldFinishComposition = mWordComposer.isComposingWord(); 1645 resetComposingState(true /* alsoResetLastComposedWord */); 1646 if (clearSuggestionStrip) { 1647 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1648 } 1649 mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, 1650 shouldFinishComposition); 1651 } 1652 1653 /** 1654 * Resets only the composing state. 1655 * 1656 * Compare #resetEntireInputState, which also clears the suggestion strip and resets the 1657 * input connection caches. This only deals with the composing state. 1658 * 1659 * @param alsoResetLastComposedWord whether to also reset the last composed word. 1660 */ 1661 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1662 mWordComposer.reset(); 1663 if (alsoResetLastComposedWord) { 1664 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1665 } 1666 } 1667 1668 /** 1669 * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word 1670 * and obsolete suggestions. 1671 * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( 1672 * String, com.android.inputmethod.latin.SuggestedWords)}. 1673 * @param typedWord The typed word as a string. 1674 * @param previousSuggestedWords The previously suggested words. 1675 * @return Obsolete suggestions with the newly typed word. 1676 */ 1677 private SuggestedWords retrieveOlderSuggestions(final String typedWord, 1678 final SuggestedWords previousSuggestedWords) { 1679 final SuggestedWords oldSuggestedWords = 1680 previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY 1681 : previousSuggestedWords; 1682 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1683 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords); 1684 return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, 1685 false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, 1686 true /* isObsoleteSuggestions */, false /* isPrediction */); 1687 } 1688 1689 /** 1690 * Gets a chunk of text with or the auto-correction indicator underline span as appropriate. 1691 * 1692 * This method looks at the old state of the auto-correction indicator to put or not put 1693 * the underline span as appropriate. It is important to note that this does not correspond 1694 * exactly to whether this word will be auto-corrected to or not: what's important here is 1695 * to keep the same indication as before. 1696 * When we add a new code point to a composing word, we don't know yet if we are going to 1697 * auto-correct it until the suggestions are computed. But in the mean time, we still need 1698 * to display the character and to extend the previous underline. To avoid any flickering, 1699 * the underline should keep the same color it used to have, even if that's not ultimately 1700 * the correct color for this new word. When the suggestions are finished evaluating, we 1701 * will call this method again to fix the color of the underline. 1702 * 1703 * @param text the text on which to maybe apply the span. 1704 * @return the same text, with the auto-correction underline span if that's appropriate. 1705 */ 1706 // TODO: Shouldn't this go in some *Utils class instead? 1707 private CharSequence getTextWithUnderline(final String text) { 1708 return mIsAutoCorrectionIndicatorOn 1709 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text) 1710 : text; 1711 } 1712 1713 /** 1714 * Sends a DOWN key event followed by an UP key event to the editor. 1715 * 1716 * If possible at all, avoid using this method. It causes all sorts of race conditions with 1717 * the text view because it goes through a different, asynchronous binder. Also, batch edits 1718 * are ignored for key events. Use the normal software input methods instead. 1719 * 1720 * @param keyCode the key code to send inside the key event. 1721 */ 1722 private void sendDownUpKeyEvent(final int keyCode) { 1723 final long eventTime = SystemClock.uptimeMillis(); 1724 mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, 1725 KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1726 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1727 mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 1728 KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1729 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1730 } 1731 1732 /** 1733 * Sends a code point to the editor, using the most appropriate method. 1734 * 1735 * Normally we send code points with commitText, but there are some cases (where backward 1736 * compatibility is a concern for example) where we want to use deprecated methods. 1737 * 1738 * @param settingsValues the current values of the settings. 1739 * @param codePoint the code point to send. 1740 */ 1741 // TODO: replace these two parameters with an InputTransaction 1742 private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) { 1743 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1744 ResearchLogger.latinIME_sendKeyCodePoint(codePoint); 1745 } 1746 // TODO: Remove this special handling of digit letters. 1747 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1748 if (codePoint >= '0' && codePoint <= '9') { 1749 sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); 1750 return; 1751 } 1752 1753 // TODO: we should do this also when the editor has TYPE_NULL 1754 if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) { 1755 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 1756 // a hardware keyboard event on pressing enter or delete. This is bad for many 1757 // reasons (there are race conditions with commits) but some applications are 1758 // relying on this behavior so we continue to support it for older apps. 1759 sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); 1760 } else { 1761 mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); 1762 } 1763 } 1764 1765 /** 1766 * Promote a phantom space to an actual space. 1767 * 1768 * This essentially inserts a space, and that's it. It just checks the options and the text 1769 * before the cursor are appropriate before doing it. 1770 * 1771 * @param settingsValues the current values of the settings. 1772 */ 1773 private void promotePhantomSpace(final SettingsValues settingsValues) { 1774 if (settingsValues.shouldInsertSpacesAutomatically() 1775 && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 1776 && !mConnection.textBeforeCursorLooksLikeURL()) { 1777 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1778 ResearchLogger.latinIME_promotePhantomSpace(); 1779 } 1780 sendKeyCodePoint(settingsValues, Constants.CODE_SPACE); 1781 } 1782 } 1783 1784 /** 1785 * Do the final processing after a batch input has ended. This commits the word to the editor. 1786 * @param settingsValues the current values of the settings. 1787 * @param suggestedWords suggestedWords to use. 1788 */ 1789 public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, 1790 final SuggestedWords suggestedWords, 1791 // TODO: remove this argument 1792 final KeyboardSwitcher keyboardSwitcher) { 1793 final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); 1794 if (TextUtils.isEmpty(batchInputText)) { 1795 return; 1796 } 1797 mConnection.beginBatchEdit(); 1798 if (SpaceState.PHANTOM == mSpaceState) { 1799 promotePhantomSpace(settingsValues); 1800 } 1801 final SuggestedWordInfo autoCommitCandidate = mSuggestedWords.getAutoCommitCandidate(); 1802 // Commit except the last word for phrase gesture if the top suggestion is eligible for auto 1803 // commit. 1804 if (settingsValues.mPhraseGestureEnabled && null != autoCommitCandidate) { 1805 // Find the last space 1806 final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1; 1807 if (0 != indexOfLastSpace) { 1808 mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1); 1809 final SuggestedWords suggestedWordsForLastWordOfPhraseGesture = 1810 suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture(); 1811 mLatinIME.showSuggestionStrip(suggestedWordsForLastWordOfPhraseGesture); 1812 } 1813 final String lastWord = batchInputText.substring(indexOfLastSpace); 1814 mWordComposer.setBatchInputWord(lastWord); 1815 mConnection.setComposingText(lastWord, 1); 1816 } else { 1817 mWordComposer.setBatchInputWord(batchInputText); 1818 mConnection.setComposingText(batchInputText, 1); 1819 } 1820 mConnection.endBatchEdit(); 1821 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1822 ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords); 1823 } 1824 // Space state must be updated before calling updateShiftState 1825 mSpaceState = SpaceState.PHANTOM; 1826 keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), 1827 getCurrentRecapitalizeState()); 1828 } 1829 1830 /** 1831 * Commit the typed string to the editor. 1832 * 1833 * This is typically called when we should commit the currently composing word without applying 1834 * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard 1835 * is configured to not do auto-correction at all (because of the settings or the properties of 1836 * the editor). In this case, `separatorString' is set to the separator that was pressed. 1837 * We also come here in a variety of cases with external user action. For example, when the 1838 * cursor is moved while there is a composition, or when the keyboard is closed, or when the 1839 * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected. 1840 * In this case, `separatorString' is set to NOT_A_SEPARATOR. 1841 * 1842 * @param settingsValues the current values of the settings. 1843 * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. 1844 */ 1845 // TODO: Make this private 1846 public void commitTyped(final SettingsValues settingsValues, final String separatorString) { 1847 if (!mWordComposer.isComposingWord()) return; 1848 final String typedWord = mWordComposer.getTypedWord(); 1849 if (typedWord.length() > 0) { 1850 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1851 ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode()); 1852 } 1853 commitChosenWord(settingsValues, typedWord, 1854 LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); 1855 } 1856 } 1857 1858 /** 1859 * Commit the current auto-correction. 1860 * 1861 * This will commit the best guess of the keyboard regarding what the user meant by typing 1862 * the currently composing word. The IME computes suggestions and assigns a confidence score 1863 * to each of them; when it's confident enough in one suggestion, it replaces the typed string 1864 * by this suggestion at commit time. When it's not confident enough, or when it has no 1865 * suggestions, or when the settings or environment does not allow for auto-correction, then 1866 * this method just commits the typed string. 1867 * Note that if suggestions are currently being computed in the background, this method will 1868 * block until the computation returns. This is necessary for consistency (it would be very 1869 * strange if pressing space would commit a different word depending on how fast you press). 1870 * 1871 * @param settingsValues the current value of the settings. 1872 * @param separator the separator that's causing the commit to happen. 1873 */ 1874 private void commitCurrentAutoCorrection(final SettingsValues settingsValues, 1875 final String separator, 1876 // TODO: Remove this argument. 1877 final LatinIME.UIHandler handler) { 1878 // Complete any pending suggestions query first 1879 if (handler.hasPendingUpdateSuggestions()) { 1880 handler.cancelUpdateSuggestionStrip(); 1881 performUpdateSuggestionStripSync(settingsValues); 1882 } 1883 final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull(); 1884 final String typedWord = mWordComposer.getTypedWord(); 1885 final String autoCorrection = (typedAutoCorrection != null) 1886 ? typedAutoCorrection : typedWord; 1887 if (autoCorrection != null) { 1888 if (TextUtils.isEmpty(typedWord)) { 1889 throw new RuntimeException("We have an auto-correction but the typed word " 1890 + "is empty? Impossible! I must commit suicide."); 1891 } 1892 if (settingsValues.mIsInternal) { 1893 LatinImeLoggerUtils.onAutoCorrection( 1894 typedWord, autoCorrection, separator, mWordComposer); 1895 } 1896 if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) { 1897 final SuggestedWords suggestedWords = mSuggestedWords; 1898 ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection, 1899 separator, mWordComposer.isBatchMode(), suggestedWords); 1900 } 1901 commitChosenWord(settingsValues, autoCorrection, 1902 LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); 1903 if (!typedWord.equals(autoCorrection)) { 1904 // This will make the correction flash for a short while as a visual clue 1905 // to the user that auto-correction happened. It has no other effect; in particular 1906 // note that this won't affect the text inside the text field AT ALL: it only makes 1907 // the segment of text starting at the supplied index and running for the length 1908 // of the auto-correction flash. At this moment, the "typedWord" argument is 1909 // ignored by TextView. 1910 mConnection.commitCorrection(new CorrectionInfo( 1911 mConnection.getExpectedSelectionEnd() - autoCorrection.length(), 1912 typedWord, autoCorrection)); 1913 } 1914 } 1915 } 1916 1917 /** 1918 * Commits the chosen word to the text field and saves it for later retrieval. 1919 * 1920 * @param settingsValues the current values of the settings. 1921 * @param chosenWord the word we want to commit. 1922 * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_* 1923 * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. 1924 */ 1925 private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, 1926 final int commitType, final String separatorString) { 1927 final SuggestedWords suggestedWords = mSuggestedWords; 1928 final CharSequence chosenWordWithSuggestions = 1929 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, 1930 suggestedWords); 1931 mConnection.commitText(chosenWordWithSuggestions, 1); 1932 // TODO: we pass 2 here, but would it be better to move this above and pass 1 instead? 1933 final String prevWord = mConnection.getNthPreviousWord( 1934 settingsValues.mSpacingAndPunctuations, 2); 1935 // Add the word to the user history dictionary 1936 performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWord); 1937 // TODO: figure out here if this is an auto-correct or if the best word is actually 1938 // what user typed. Note: currently this is done much later in 1939 // LastComposedWord#didCommitTypedWord by string equality of the remembered 1940 // strings. 1941 mLastComposedWord = mWordComposer.commitWord(commitType, 1942 chosenWordWithSuggestions, separatorString, prevWord); 1943 final boolean shouldDiscardPreviousWordForSuggestion; 1944 if (0 == StringUtils.codePointCount(separatorString)) { 1945 // Separator is 0-length, we can keep the previous word for suggestion. Either this 1946 // was a manual pick or the language has no spaces in which case we want to keep the 1947 // previous word, or it was the keyboard closing or the cursor moving in which case it 1948 // will be reset anyway. 1949 shouldDiscardPreviousWordForSuggestion = false; 1950 } else { 1951 // Otherwise, we discard if the separator contains any non-whitespace. 1952 shouldDiscardPreviousWordForSuggestion = 1953 !StringUtils.containsOnlyWhitespace(separatorString); 1954 } 1955 if (shouldDiscardPreviousWordForSuggestion) { 1956 mWordComposer.discardPreviousWordForSuggestion(); 1957 } 1958 } 1959 1960 /** 1961 * Retry resetting caches in the rich input connection. 1962 * 1963 * When the editor can't be accessed we can't reset the caches, so we schedule a retry. 1964 * This method handles the retry, and re-schedules a new retry if we still can't access. 1965 * We only retry up to 5 times before giving up. 1966 * 1967 * @param settingsValues the current values of the settings. 1968 * @param tryResumeSuggestions Whether we should resume suggestions or not. 1969 * @param remainingTries How many times we may try again before giving up. 1970 * @return whether true if the caches were successfully reset, false otherwise. 1971 */ 1972 // TODO: make this private 1973 public boolean retryResetCachesAndReturnSuccess(final SettingsValues settingsValues, 1974 final boolean tryResumeSuggestions, final int remainingTries, 1975 // TODO: remove these arguments 1976 final LatinIME.UIHandler handler) { 1977 if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( 1978 mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(), 1979 false /* shouldFinishComposition */)) { 1980 if (0 < remainingTries) { 1981 handler.postResetCaches(tryResumeSuggestions, remainingTries - 1); 1982 return false; 1983 } 1984 // If remainingTries is 0, we should stop waiting for new tries, however we'll still 1985 // return true as we need to perform other tasks (for example, loading the keyboard). 1986 } 1987 mConnection.tryFixLyingCursorPosition(); 1988 if (tryResumeSuggestions) { 1989 handler.postResumeSuggestions(); 1990 } 1991 return true; 1992 } 1993} 1994