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