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