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