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