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