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