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