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 17 package com.android.inputmethod.latin.inputlogic; 18 19 import android.graphics.Color; 20 import android.os.SystemClock; 21 import android.text.SpannableString; 22 import android.text.Spanned; 23 import android.text.TextUtils; 24 import android.text.style.BackgroundColorSpan; 25 import android.text.style.SuggestionSpan; 26 import android.util.Log; 27 import android.view.KeyCharacterMap; 28 import android.view.KeyEvent; 29 import android.view.inputmethod.CorrectionInfo; 30 import android.view.inputmethod.EditorInfo; 31 32 import com.android.inputmethod.compat.SuggestionSpanUtils; 33 import com.android.inputmethod.event.Event; 34 import com.android.inputmethod.event.InputTransaction; 35 import com.android.inputmethod.keyboard.Keyboard; 36 import com.android.inputmethod.keyboard.KeyboardSwitcher; 37 import com.android.inputmethod.latin.Dictionary; 38 import com.android.inputmethod.latin.DictionaryFacilitator; 39 import com.android.inputmethod.latin.LastComposedWord; 40 import com.android.inputmethod.latin.LatinIME; 41 import com.android.inputmethod.latin.NgramContext; 42 import com.android.inputmethod.latin.RichInputConnection; 43 import com.android.inputmethod.latin.Suggest; 44 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback; 45 import com.android.inputmethod.latin.SuggestedWords; 46 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo; 47 import com.android.inputmethod.latin.WordComposer; 48 import com.android.inputmethod.latin.common.Constants; 49 import com.android.inputmethod.latin.common.InputPointers; 50 import com.android.inputmethod.latin.common.StringUtils; 51 import com.android.inputmethod.latin.define.DebugFlags; 52 import com.android.inputmethod.latin.settings.SettingsValues; 53 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion; 54 import com.android.inputmethod.latin.settings.SpacingAndPunctuations; 55 import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor; 56 import com.android.inputmethod.latin.utils.AsyncResultHolder; 57 import com.android.inputmethod.latin.utils.InputTypeUtils; 58 import com.android.inputmethod.latin.utils.RecapitalizeStatus; 59 import com.android.inputmethod.latin.utils.StatsUtils; 60 import com.android.inputmethod.latin.utils.TextRange; 61 62 import java.util.ArrayList; 63 import java.util.Locale; 64 import java.util.TreeSet; 65 import java.util.concurrent.TimeUnit; 66 67 import javax.annotation.Nonnull; 68 69 /** 70 * This class manages the input logic. 71 */ 72 public final class InputLogic { 73 private static final String TAG = InputLogic.class.getSimpleName(); 74 75 // TODO : Remove this member when we can. 76 final LatinIME mLatinIME; 77 private final SuggestionStripViewAccessor mSuggestionStripViewAccessor; 78 79 // Never null. 80 private InputLogicHandler mInputLogicHandler = InputLogicHandler.NULL_HANDLER; 81 82 // TODO : make all these fields private as soon as possible. 83 // Current space state of the input method. This can be any of the above constants. 84 private int mSpaceState; 85 // Never null 86 public SuggestedWords mSuggestedWords = SuggestedWords.getEmptyInstance(); 87 public final Suggest mSuggest; 88 private final DictionaryFacilitator mDictionaryFacilitator; 89 90 public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 91 // This has package visibility so it can be accessed from InputLogicHandler. 92 /* package */ final WordComposer mWordComposer; 93 public final RichInputConnection mConnection; 94 private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus(); 95 96 private int mDeleteCount; 97 private long mLastKeyTime; 98 public final TreeSet<Long> mCurrentlyPressedHardwareKeys = new TreeSet<>(); 99 100 // Keeps track of most recently inserted text (multi-character key) for reverting 101 private String mEnteredText; 102 103 // TODO: This boolean is persistent state and causes large side effects at unexpected times. 104 // Find a way to remove it for readability. 105 private boolean mIsAutoCorrectionIndicatorOn; 106 private long mDoubleSpacePeriodCountdownStart; 107 108 // The word being corrected while the cursor is in the middle of the word. 109 // Note: This does not have a composing span, so it must be handled separately. 110 private String mWordBeingCorrectedByCursor = null; 111 112 /** 113 * Create a new instance of the input logic. 114 * @param latinIME the instance of the parent LatinIME. We should remove this when we can. 115 * @param suggestionStripViewAccessor an object to access the suggestion strip view. 116 * @param dictionaryFacilitator facilitator for getting suggestions and updating user history 117 * dictionary. 118 */ InputLogic(final LatinIME latinIME, final SuggestionStripViewAccessor suggestionStripViewAccessor, final DictionaryFacilitator dictionaryFacilitator)119 public InputLogic(final LatinIME latinIME, 120 final SuggestionStripViewAccessor suggestionStripViewAccessor, 121 final DictionaryFacilitator dictionaryFacilitator) { 122 mLatinIME = latinIME; 123 mSuggestionStripViewAccessor = suggestionStripViewAccessor; 124 mWordComposer = new WordComposer(); 125 mConnection = new RichInputConnection(latinIME); 126 mInputLogicHandler = InputLogicHandler.NULL_HANDLER; 127 mSuggest = new Suggest(dictionaryFacilitator); 128 mDictionaryFacilitator = dictionaryFacilitator; 129 } 130 131 /** 132 * Initializes the input logic for input in an editor. 133 * 134 * Call this when input starts or restarts in some editor (typically, in onStartInputView). 135 * 136 * @param combiningSpec the combining spec string for this subtype 137 * @param settingsValues the current settings values 138 */ startInput(final String combiningSpec, final SettingsValues settingsValues)139 public void startInput(final String combiningSpec, final SettingsValues settingsValues) { 140 mEnteredText = null; 141 mWordBeingCorrectedByCursor = null; 142 mConnection.onStartInput(); 143 if (!mWordComposer.getTypedWord().isEmpty()) { 144 // For messaging apps that offer send button, the IME does not get the opportunity 145 // to capture the last word. This block should capture those uncommitted words. 146 // The timestamp at which it is captured is not accurate but close enough. 147 StatsUtils.onWordCommitUserTyped( 148 mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); 149 } 150 mWordComposer.restartCombining(combiningSpec); 151 resetComposingState(true /* alsoResetLastComposedWord */); 152 mDeleteCount = 0; 153 mSpaceState = SpaceState.NONE; 154 mRecapitalizeStatus.disable(); // Do not perform recapitalize until the cursor is moved once 155 mCurrentlyPressedHardwareKeys.clear(); 156 mSuggestedWords = SuggestedWords.getEmptyInstance(); 157 // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying 158 // so we try using some heuristics to find out about these and fix them. 159 mConnection.tryFixLyingCursorPosition(); 160 cancelDoubleSpacePeriodCountdown(); 161 if (InputLogicHandler.NULL_HANDLER == mInputLogicHandler) { 162 mInputLogicHandler = new InputLogicHandler(mLatinIME, this); 163 } else { 164 mInputLogicHandler.reset(); 165 } 166 167 if (settingsValues.mShouldShowLxxSuggestionUi) { 168 mConnection.requestCursorUpdates(true /* enableMonitor */, 169 true /* requestImmediateCallback */); 170 } 171 } 172 173 /** 174 * Call this when the subtype changes. 175 * @param combiningSpec the spec string for the combining rules 176 * @param settingsValues the current settings values 177 */ onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues)178 public void onSubtypeChanged(final String combiningSpec, final SettingsValues settingsValues) { 179 finishInput(); 180 startInput(combiningSpec, settingsValues); 181 } 182 183 /** 184 * Call this when the orientation changes. 185 * @param settingsValues the current values of the settings. 186 */ onOrientationChange(final SettingsValues settingsValues)187 public void onOrientationChange(final SettingsValues settingsValues) { 188 // If !isComposingWord, #commitTyped() is a no-op, but still, it's better to avoid 189 // the useless IPC of {begin,end}BatchEdit. 190 if (mWordComposer.isComposingWord()) { 191 mConnection.beginBatchEdit(); 192 // If we had a composition in progress, we need to commit the word so that the 193 // suggestionsSpan will be added. This will allow resuming on the same suggestions 194 // after rotation is finished. 195 commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); 196 mConnection.endBatchEdit(); 197 } 198 } 199 200 /** 201 * Clean up the input logic after input is finished. 202 */ finishInput()203 public void finishInput() { 204 if (mWordComposer.isComposingWord()) { 205 mConnection.finishComposingText(); 206 StatsUtils.onWordCommitUserTyped( 207 mWordComposer.getTypedWord(), mWordComposer.isBatchMode()); 208 } 209 resetComposingState(true /* alsoResetLastComposedWord */); 210 mInputLogicHandler.reset(); 211 } 212 213 // Normally this class just gets out of scope after the process ends, but in unit tests, we 214 // create several instances of LatinIME in the same process, which results in several 215 // instances of InputLogic. This cleans up the associated handler so that tests don't leak 216 // handlers. recycle()217 public void recycle() { 218 final InputLogicHandler inputLogicHandler = mInputLogicHandler; 219 mInputLogicHandler = InputLogicHandler.NULL_HANDLER; 220 inputLogicHandler.destroy(); 221 mDictionaryFacilitator.closeDictionaries(); 222 } 223 224 /** 225 * React to a string input. 226 * 227 * This is triggered by keys that input many characters at once, like the ".com" key or 228 * some additional keys for example. 229 * 230 * @param settingsValues the current values of the settings. 231 * @param event the input event containing the data. 232 * @return the complete transaction object 233 */ onTextInput(final SettingsValues settingsValues, final Event event, final int keyboardShiftMode, final LatinIME.UIHandler handler)234 public InputTransaction onTextInput(final SettingsValues settingsValues, final Event event, 235 final int keyboardShiftMode, final LatinIME.UIHandler handler) { 236 final String rawText = event.getTextToCommit().toString(); 237 final InputTransaction inputTransaction = new InputTransaction(settingsValues, event, 238 SystemClock.uptimeMillis(), mSpaceState, 239 getActualCapsMode(settingsValues, keyboardShiftMode)); 240 mConnection.beginBatchEdit(); 241 if (mWordComposer.isComposingWord()) { 242 commitCurrentAutoCorrection(settingsValues, rawText, handler); 243 } else { 244 resetComposingState(true /* alsoResetLastComposedWord */); 245 } 246 handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_TYPING); 247 final String text = performSpecificTldProcessingOnTextInput(rawText); 248 if (SpaceState.PHANTOM == mSpaceState) { 249 insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); 250 } 251 mConnection.commitText(text, 1); 252 StatsUtils.onWordCommitUserTyped(mEnteredText, mWordComposer.isBatchMode()); 253 mConnection.endBatchEdit(); 254 // Space state must be updated before calling updateShiftState 255 mSpaceState = SpaceState.NONE; 256 mEnteredText = text; 257 mWordBeingCorrectedByCursor = null; 258 inputTransaction.setDidAffectContents(); 259 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 260 return inputTransaction; 261 } 262 263 /** 264 * A suggestion was picked from the suggestion strip. 265 * @param settingsValues the current values of the settings. 266 * @param suggestionInfo the suggestion info. 267 * @param keyboardShiftState the shift state of the keyboard, as returned by 268 * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} 269 * @return the complete transaction object 270 */ 271 // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener} 272 // interface onPickSuggestionManually(final SettingsValues settingsValues, final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)273 public InputTransaction onPickSuggestionManually(final SettingsValues settingsValues, 274 final SuggestedWordInfo suggestionInfo, final int keyboardShiftState, 275 final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { 276 final SuggestedWords suggestedWords = mSuggestedWords; 277 final String suggestion = suggestionInfo.mWord; 278 // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput 279 if (suggestion.length() == 1 && suggestedWords.isPunctuationSuggestions()) { 280 // We still want to log a suggestion click. 281 StatsUtils.onPickSuggestionManually( 282 mSuggestedWords, suggestionInfo, mDictionaryFacilitator); 283 // Word separators are suggested before the user inputs something. 284 // Rely on onCodeInput to do the complicated swapping/stripping logic consistently. 285 final Event event = Event.createPunctuationSuggestionPickedEvent(suggestionInfo); 286 return onCodeInput(settingsValues, event, keyboardShiftState, 287 currentKeyboardScriptId, handler); 288 } 289 290 final Event event = Event.createSuggestionPickedEvent(suggestionInfo); 291 final InputTransaction inputTransaction = new InputTransaction(settingsValues, 292 event, SystemClock.uptimeMillis(), mSpaceState, keyboardShiftState); 293 // Manual pick affects the contents of the editor, so we take note of this. It's important 294 // for the sequence of language switching. 295 inputTransaction.setDidAffectContents(); 296 mConnection.beginBatchEdit(); 297 if (SpaceState.PHANTOM == mSpaceState && suggestion.length() > 0 298 // In the batch input mode, a manually picked suggested word should just replace 299 // the current batch input text and there is no need for a phantom space. 300 && !mWordComposer.isBatchMode()) { 301 final int firstChar = Character.codePointAt(suggestion, 0); 302 if (!settingsValues.isWordSeparator(firstChar) 303 || settingsValues.isUsuallyPrecededBySpace(firstChar)) { 304 insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); 305 } 306 } 307 308 // TODO: We should not need the following branch. We should be able to take the same 309 // code path as for other kinds, use commitChosenWord, and do everything normally. We will 310 // however need to reset the suggestion strip right away, because we know we can't take 311 // the risk of calling commitCompletion twice because we don't know how the app will react. 312 if (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_APP_DEFINED)) { 313 mSuggestedWords = SuggestedWords.getEmptyInstance(); 314 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 315 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 316 resetComposingState(true /* alsoResetLastComposedWord */); 317 mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo); 318 mConnection.endBatchEdit(); 319 return inputTransaction; 320 } 321 322 commitChosenWord(settingsValues, suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK, 323 LastComposedWord.NOT_A_SEPARATOR); 324 mConnection.endBatchEdit(); 325 // Don't allow cancellation of manual pick 326 mLastComposedWord.deactivate(); 327 // Space state must be updated before calling updateShiftState 328 mSpaceState = SpaceState.PHANTOM; 329 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 330 331 // If we're not showing the "Touch again to save", then update the suggestion strip. 332 // That's going to be predictions (or punctuation suggestions), so INPUT_STYLE_NONE. 333 handler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_NONE); 334 335 StatsUtils.onPickSuggestionManually( 336 mSuggestedWords, suggestionInfo, mDictionaryFacilitator); 337 StatsUtils.onWordCommitSuggestionPickedManually( 338 suggestionInfo.mWord, mWordComposer.isBatchMode()); 339 return inputTransaction; 340 } 341 342 /** 343 * Consider an update to the cursor position. Evaluate whether this update has happened as 344 * part of normal typing or whether it was an explicit cursor move by the user. In any case, 345 * do the necessary adjustments. 346 * @param oldSelStart old selection start 347 * @param oldSelEnd old selection end 348 * @param newSelStart new selection start 349 * @param newSelEnd new selection end 350 * @param settingsValues the current values of the settings. 351 * @return whether the cursor has moved as a result of user interaction. 352 */ onUpdateSelection(final int oldSelStart, final int oldSelEnd, final int newSelStart, final int newSelEnd, final SettingsValues settingsValues)353 public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd, 354 final int newSelStart, final int newSelEnd, final SettingsValues settingsValues) { 355 if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) { 356 return false; 357 } 358 // TODO: the following is probably better done in resetEntireInputState(). 359 // it should only happen when the cursor moved, and the very purpose of the 360 // test below is to narrow down whether this happened or not. Likewise with 361 // the call to updateShiftState. 362 // We set this to NONE because after a cursor move, we don't want the space 363 // state-related special processing to kick in. 364 mSpaceState = SpaceState.NONE; 365 366 final boolean selectionChangedOrSafeToReset = 367 oldSelStart != newSelStart || oldSelEnd != newSelEnd // selection changed 368 || !mWordComposer.isComposingWord(); // safe to reset 369 final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd); 370 final int moveAmount = newSelStart - oldSelStart; 371 // As an added small gift from the framework, it happens upon rotation when there 372 // is a selection that we get a wrong cursor position delivered to startInput() that 373 // does not get reflected in the oldSel{Start,End} parameters to the next call to 374 // onUpdateSelection. In this case, we may have set a composition, and when we're here 375 // we realize we shouldn't have. In theory, in this case, selectionChangedOrSafeToReset 376 // should be true, but that is if the framework had taken that wrong cursor position 377 // into account, which means we have to reset the entire composing state whenever there 378 // is or was a selection regardless of whether it changed or not. 379 if (hasOrHadSelection || !settingsValues.needsToLookupSuggestions() 380 || (selectionChangedOrSafeToReset 381 && !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) { 382 // If we are composing a word and moving the cursor, we would want to set a 383 // suggestion span for recorrection to work correctly. Unfortunately, that 384 // would involve the keyboard committing some new text, which would move the 385 // cursor back to where it was. Latin IME could then fix the position of the cursor 386 // again, but the asynchronous nature of the calls results in this wreaking havoc 387 // with selection on double tap and the like. 388 // Another option would be to send suggestions each time we set the composing 389 // text, but that is probably too expensive to do, so we decided to leave things 390 // as is. 391 // Also, we're posting a resume suggestions message, and this will update the 392 // suggestions strip in a few milliseconds, so if we cleared the suggestion strip here 393 // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear 394 // it here, which means we'll keep outdated suggestions for a split second but the 395 // visual result is better. 396 resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */); 397 // If the user is in the middle of correcting a word, we should learn it before moving 398 // the cursor away. 399 if (!TextUtils.isEmpty(mWordBeingCorrectedByCursor)) { 400 final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( 401 System.currentTimeMillis()); 402 performAdditionToUserHistoryDictionary(settingsValues, mWordBeingCorrectedByCursor, 403 NgramContext.EMPTY_PREV_WORDS_INFO); 404 } 405 } else { 406 // resetEntireInputState calls resetCachesUponCursorMove, but forcing the 407 // composition to end. But in all cases where we don't reset the entire input 408 // state, we still want to tell the rich input connection about the new cursor 409 // position so that it can update its caches. 410 mConnection.resetCachesUponCursorMoveAndReturnSuccess( 411 newSelStart, newSelEnd, false /* shouldFinishComposition */); 412 } 413 414 // The cursor has been moved : we now accept to perform recapitalization 415 mRecapitalizeStatus.enable(); 416 // We moved the cursor. If we are touching a word, we need to resume suggestion. 417 mLatinIME.mHandler.postResumeSuggestions(true /* shouldDelay */); 418 // Stop the last recapitalization, if started. 419 mRecapitalizeStatus.stop(); 420 mWordBeingCorrectedByCursor = null; 421 return true; 422 } 423 424 /** 425 * React to a code input. It may be a code point to insert, or a symbolic value that influences 426 * the keyboard behavior. 427 * 428 * Typically, this is called whenever a key is pressed on the software keyboard. This is not 429 * the entry point for gesture input; see the onBatchInput* family of functions for this. 430 * 431 * @param settingsValues the current settings values. 432 * @param event the event to handle. 433 * @param keyboardShiftMode the current shift mode of the keyboard, as returned by 434 * {@link com.android.inputmethod.keyboard.KeyboardSwitcher#getKeyboardShiftMode()} 435 * @return the complete transaction object 436 */ onCodeInput(final SettingsValues settingsValues, @Nonnull final Event event, final int keyboardShiftMode, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)437 public InputTransaction onCodeInput(final SettingsValues settingsValues, 438 @Nonnull final Event event, final int keyboardShiftMode, 439 final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { 440 mWordBeingCorrectedByCursor = null; 441 final Event processedEvent = mWordComposer.processEvent(event); 442 final InputTransaction inputTransaction = new InputTransaction(settingsValues, 443 processedEvent, SystemClock.uptimeMillis(), mSpaceState, 444 getActualCapsMode(settingsValues, keyboardShiftMode)); 445 if (processedEvent.mKeyCode != Constants.CODE_DELETE 446 || inputTransaction.mTimestamp > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) { 447 mDeleteCount = 0; 448 } 449 mLastKeyTime = inputTransaction.mTimestamp; 450 mConnection.beginBatchEdit(); 451 if (!mWordComposer.isComposingWord()) { 452 // TODO: is this useful? It doesn't look like it should be done here, but rather after 453 // a word is committed. 454 mIsAutoCorrectionIndicatorOn = false; 455 } 456 457 // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state. 458 if (processedEvent.mCodePoint != Constants.CODE_SPACE) { 459 cancelDoubleSpacePeriodCountdown(); 460 } 461 462 Event currentEvent = processedEvent; 463 while (null != currentEvent) { 464 if (currentEvent.isConsumed()) { 465 handleConsumedEvent(currentEvent, inputTransaction); 466 } else if (currentEvent.isFunctionalKeyEvent()) { 467 handleFunctionalEvent(currentEvent, inputTransaction, currentKeyboardScriptId, 468 handler); 469 } else { 470 handleNonFunctionalEvent(currentEvent, inputTransaction, handler); 471 } 472 currentEvent = currentEvent.mNextEvent; 473 } 474 // Try to record the word being corrected when the user enters a word character or 475 // the backspace key. 476 if (!mConnection.hasSlowInputConnection() && !mWordComposer.isComposingWord() 477 && (settingsValues.isWordCodePoint(processedEvent.mCodePoint) || 478 processedEvent.mKeyCode == Constants.CODE_DELETE)) { 479 mWordBeingCorrectedByCursor = getWordAtCursor( 480 settingsValues, currentKeyboardScriptId); 481 } 482 if (!inputTransaction.didAutoCorrect() && processedEvent.mKeyCode != Constants.CODE_SHIFT 483 && processedEvent.mKeyCode != Constants.CODE_CAPSLOCK 484 && processedEvent.mKeyCode != Constants.CODE_SWITCH_ALPHA_SYMBOL) 485 mLastComposedWord.deactivate(); 486 if (Constants.CODE_DELETE != processedEvent.mKeyCode) { 487 mEnteredText = null; 488 } 489 mConnection.endBatchEdit(); 490 return inputTransaction; 491 } 492 onStartBatchInput(final SettingsValues settingsValues, final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler)493 public void onStartBatchInput(final SettingsValues settingsValues, 494 final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) { 495 mWordBeingCorrectedByCursor = null; 496 mInputLogicHandler.onStartBatchInput(); 497 handler.showGesturePreviewAndSuggestionStrip( 498 SuggestedWords.getEmptyInstance(), false /* dismissGestureFloatingPreviewText */); 499 handler.cancelUpdateSuggestionStrip(); 500 ++mAutoCommitSequenceNumber; 501 mConnection.beginBatchEdit(); 502 if (mWordComposer.isComposingWord()) { 503 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 504 // If we are in the middle of a recorrection, we need to commit the recorrection 505 // first so that we can insert the batch input at the current cursor position. 506 // We also need to unlearn the original word that is now being corrected. 507 unlearnWord(mWordComposer.getTypedWord(), settingsValues, 508 Constants.EVENT_BACKSPACE); 509 resetEntireInputState(mConnection.getExpectedSelectionStart(), 510 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 511 } else if (mWordComposer.isSingleLetter()) { 512 // We auto-correct the previous (typed, not gestured) string iff it's one character 513 // long. The reason for this is, even in the middle of gesture typing, you'll still 514 // tap one-letter words and you want them auto-corrected (typically, "i" in English 515 // should become "I"). However for any longer word, we assume that the reason for 516 // tapping probably is that the word you intend to type is not in the dictionary, 517 // so we do not attempt to correct, on the assumption that if that was a dictionary 518 // word, the user would probably have gestured instead. 519 commitCurrentAutoCorrection(settingsValues, LastComposedWord.NOT_A_SEPARATOR, 520 handler); 521 } else { 522 commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR); 523 } 524 } 525 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 526 if (Character.isLetterOrDigit(codePointBeforeCursor) 527 || settingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) { 528 final boolean autoShiftHasBeenOverriden = keyboardSwitcher.getKeyboardShiftMode() != 529 getCurrentAutoCapsState(settingsValues); 530 mSpaceState = SpaceState.PHANTOM; 531 if (!autoShiftHasBeenOverriden) { 532 // When we change the space state, we need to update the shift state of the 533 // keyboard unless it has been overridden manually. This is happening for example 534 // after typing some letters and a period, then gesturing; the keyboard is not in 535 // caps mode yet, but since a gesture is starting, it should go in caps mode, 536 // unless the user explictly said it should not. 537 keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), 538 getCurrentRecapitalizeState()); 539 } 540 } 541 mConnection.endBatchEdit(); 542 mWordComposer.setCapitalizedModeAtStartComposingTime( 543 getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode())); 544 } 545 546 /* The sequence number member is only used in onUpdateBatchInput. It is increased each time 547 * auto-commit happens. The reason we need this is, when auto-commit happens we trim the 548 * input pointers that are held in a singleton, and to know how much to trim we rely on the 549 * results of the suggestion process that is held in mSuggestedWords. 550 * However, the suggestion process is asynchronous, and sometimes we may enter the 551 * onUpdateBatchInput method twice without having recomputed suggestions yet, or having 552 * received new suggestions generated from not-yet-trimmed input pointers. In this case, the 553 * mIndexOfTouchPointOfSecondWords member will be out of date, and we must not use it lest we 554 * remove an unrelated number of pointers (possibly even more than are left in the input 555 * pointers, leading to a crash). 556 * To avoid that, we increase the sequence number each time we auto-commit and trim the 557 * input pointers, and we do not use any suggested words that have been generated with an 558 * earlier sequence number. 559 */ 560 private int mAutoCommitSequenceNumber = 1; onUpdateBatchInput(final InputPointers batchPointers)561 public void onUpdateBatchInput(final InputPointers batchPointers) { 562 mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber); 563 } 564 onEndBatchInput(final InputPointers batchPointers)565 public void onEndBatchInput(final InputPointers batchPointers) { 566 mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber); 567 ++mAutoCommitSequenceNumber; 568 } 569 onCancelBatchInput(final LatinIME.UIHandler handler)570 public void onCancelBatchInput(final LatinIME.UIHandler handler) { 571 mInputLogicHandler.onCancelBatchInput(); 572 handler.showGesturePreviewAndSuggestionStrip( 573 SuggestedWords.getEmptyInstance(), true /* dismissGestureFloatingPreviewText */); 574 } 575 576 // TODO: on the long term, this method should become private, but it will be difficult. 577 // Especially, how do we deal with InputMethodService.onDisplayCompletions? setSuggestedWords(final SuggestedWords suggestedWords)578 public void setSuggestedWords(final SuggestedWords suggestedWords) { 579 if (!suggestedWords.isEmpty()) { 580 final SuggestedWordInfo suggestedWordInfo; 581 if (suggestedWords.mWillAutoCorrect) { 582 suggestedWordInfo = suggestedWords.getInfo(SuggestedWords.INDEX_OF_AUTO_CORRECTION); 583 } else { 584 // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD) 585 // because it may differ from mWordComposer.mTypedWord. 586 suggestedWordInfo = suggestedWords.mTypedWordInfo; 587 } 588 mWordComposer.setAutoCorrection(suggestedWordInfo); 589 } 590 mSuggestedWords = suggestedWords; 591 final boolean newAutoCorrectionIndicator = suggestedWords.mWillAutoCorrect; 592 593 // Put a blue underline to a word in TextView which will be auto-corrected. 594 if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator 595 && mWordComposer.isComposingWord()) { 596 mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator; 597 final CharSequence textWithUnderline = 598 getTextWithUnderline(mWordComposer.getTypedWord()); 599 // TODO: when called from an updateSuggestionStrip() call that results from a posted 600 // message, this is called outside any batch edit. Potentially, this may result in some 601 // janky flickering of the screen, although the display speed makes it unlikely in 602 // the practice. 603 setComposingTextInternal(textWithUnderline, 1); 604 } 605 } 606 607 /** 608 * Handle a consumed event. 609 * 610 * Consumed events represent events that have already been consumed, typically by the 611 * combining chain. 612 * 613 * @param event The event to handle. 614 * @param inputTransaction The transaction in progress. 615 */ handleConsumedEvent(final Event event, final InputTransaction inputTransaction)616 private void handleConsumedEvent(final Event event, final InputTransaction inputTransaction) { 617 // A consumed event may have text to commit and an update to the composing state, so 618 // we evaluate both. With some combiners, it's possible than an event contains both 619 // and we enter both of the following if clauses. 620 final CharSequence textToCommit = event.getTextToCommit(); 621 if (!TextUtils.isEmpty(textToCommit)) { 622 mConnection.commitText(textToCommit, 1); 623 inputTransaction.setDidAffectContents(); 624 } 625 if (mWordComposer.isComposingWord()) { 626 setComposingTextInternal(mWordComposer.getTypedWord(), 1); 627 inputTransaction.setDidAffectContents(); 628 inputTransaction.setRequiresUpdateSuggestions(); 629 } 630 } 631 632 /** 633 * Handle a functional key event. 634 * 635 * A functional event is a special key, like delete, shift, emoji, or the settings key. 636 * Non-special keys are those that generate a single code point. 637 * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that 638 * manage keyboard-related stuff like shift, language switch, settings, layout switch, or 639 * any key that results in multiple code points like the ".com" key. 640 * 641 * @param event The event to handle. 642 * @param inputTransaction The transaction in progress. 643 */ handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, final int currentKeyboardScriptId, final LatinIME.UIHandler handler)644 private void handleFunctionalEvent(final Event event, final InputTransaction inputTransaction, 645 final int currentKeyboardScriptId, final LatinIME.UIHandler handler) { 646 switch (event.mKeyCode) { 647 case Constants.CODE_DELETE: 648 handleBackspaceEvent(event, inputTransaction, currentKeyboardScriptId); 649 // Backspace is a functional key, but it affects the contents of the editor. 650 inputTransaction.setDidAffectContents(); 651 break; 652 case Constants.CODE_SHIFT: 653 performRecapitalization(inputTransaction.mSettingsValues); 654 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 655 if (mSuggestedWords.isPrediction()) { 656 inputTransaction.setRequiresUpdateSuggestions(); 657 } 658 break; 659 case Constants.CODE_CAPSLOCK: 660 // Note: Changing keyboard to shift lock state is handled in 661 // {@link KeyboardSwitcher#onEvent(Event)}. 662 break; 663 case Constants.CODE_SYMBOL_SHIFT: 664 // Note: Calling back to the keyboard on the symbol Shift key is handled in 665 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 666 break; 667 case Constants.CODE_SWITCH_ALPHA_SYMBOL: 668 // Note: Calling back to the keyboard on symbol key is handled in 669 // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}. 670 break; 671 case Constants.CODE_SETTINGS: 672 onSettingsKeyPressed(); 673 break; 674 case Constants.CODE_SHORTCUT: 675 // We need to switch to the shortcut IME. This is handled by LatinIME since the 676 // input logic has no business with IME switching. 677 break; 678 case Constants.CODE_ACTION_NEXT: 679 performEditorAction(EditorInfo.IME_ACTION_NEXT); 680 break; 681 case Constants.CODE_ACTION_PREVIOUS: 682 performEditorAction(EditorInfo.IME_ACTION_PREVIOUS); 683 break; 684 case Constants.CODE_LANGUAGE_SWITCH: 685 handleLanguageSwitchKey(); 686 break; 687 case Constants.CODE_EMOJI: 688 // Note: Switching emoji keyboard is being handled in 689 // {@link KeyboardState#onEvent(Event,int)}. 690 break; 691 case Constants.CODE_ALPHA_FROM_EMOJI: 692 // Note: Switching back from Emoji keyboard to the main keyboard is being 693 // handled in {@link KeyboardState#onEvent(Event,int)}. 694 break; 695 case Constants.CODE_SHIFT_ENTER: 696 final Event tmpEvent = Event.createSoftwareKeypressEvent(Constants.CODE_ENTER, 697 event.mKeyCode, event.mX, event.mY, event.isKeyRepeat()); 698 handleNonSpecialCharacterEvent(tmpEvent, inputTransaction, handler); 699 // Shift + Enter is treated as a functional key but it results in adding a new 700 // line, so that does affect the contents of the editor. 701 inputTransaction.setDidAffectContents(); 702 break; 703 default: 704 throw new RuntimeException("Unknown key code : " + event.mKeyCode); 705 } 706 } 707 708 /** 709 * Handle an event that is not a functional event. 710 * 711 * These events are generally events that cause input, but in some cases they may do other 712 * things like trigger an editor action. 713 * 714 * @param event The event to handle. 715 * @param inputTransaction The transaction in progress. 716 */ handleNonFunctionalEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)717 private void handleNonFunctionalEvent(final Event event, 718 final InputTransaction inputTransaction, 719 final LatinIME.UIHandler handler) { 720 inputTransaction.setDidAffectContents(); 721 switch (event.mCodePoint) { 722 case Constants.CODE_ENTER: 723 final EditorInfo editorInfo = getCurrentInputEditorInfo(); 724 final int imeOptionsActionId = 725 InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo); 726 if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) { 727 // Either we have an actionLabel and we should performEditorAction with 728 // actionId regardless of its value. 729 performEditorAction(editorInfo.actionId); 730 } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) { 731 // We didn't have an actionLabel, but we had another action to execute. 732 // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast, 733 // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it 734 // means there should be an action and the app didn't bother to set a specific 735 // code for it - presumably it only handles one. It does not have to be treated 736 // in any specific way: anything that is not IME_ACTION_NONE should be sent to 737 // performEditorAction. 738 performEditorAction(imeOptionsActionId); 739 } else { 740 // No action label, and the action from imeOptions is NONE: this is a regular 741 // enter key that should input a carriage return. 742 handleNonSpecialCharacterEvent(event, inputTransaction, handler); 743 } 744 break; 745 default: 746 handleNonSpecialCharacterEvent(event, inputTransaction, handler); 747 break; 748 } 749 } 750 751 /** 752 * Handle inputting a code point to the editor. 753 * 754 * Non-special keys are those that generate a single code point. 755 * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that 756 * manage keyboard-related stuff like shift, language switch, settings, layout switch, or 757 * any key that results in multiple code points like the ".com" key. 758 * 759 * @param event The event to handle. 760 * @param inputTransaction The transaction in progress. 761 */ handleNonSpecialCharacterEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)762 private void handleNonSpecialCharacterEvent(final Event event, 763 final InputTransaction inputTransaction, 764 final LatinIME.UIHandler handler) { 765 final int codePoint = event.mCodePoint; 766 mSpaceState = SpaceState.NONE; 767 if (inputTransaction.mSettingsValues.isWordSeparator(codePoint) 768 || Character.getType(codePoint) == Character.OTHER_SYMBOL) { 769 handleSeparatorEvent(event, inputTransaction, handler); 770 } else { 771 if (SpaceState.PHANTOM == inputTransaction.mSpaceState) { 772 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 773 // If we are in the middle of a recorrection, we need to commit the recorrection 774 // first so that we can insert the character at the current cursor position. 775 // We also need to unlearn the original word that is now being corrected. 776 unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues, 777 Constants.EVENT_BACKSPACE); 778 resetEntireInputState(mConnection.getExpectedSelectionStart(), 779 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 780 } else { 781 commitTyped(inputTransaction.mSettingsValues, LastComposedWord.NOT_A_SEPARATOR); 782 } 783 } 784 handleNonSeparatorEvent(event, inputTransaction.mSettingsValues, inputTransaction); 785 } 786 } 787 788 /** 789 * Handle a non-separator. 790 * @param event The event to handle. 791 * @param settingsValues The current settings values. 792 * @param inputTransaction The transaction in progress. 793 */ handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues, final InputTransaction inputTransaction)794 private void handleNonSeparatorEvent(final Event event, final SettingsValues settingsValues, 795 final InputTransaction inputTransaction) { 796 final int codePoint = event.mCodePoint; 797 // TODO: refactor this method to stop flipping isComposingWord around all the time, and 798 // make it shorter (possibly cut into several pieces). Also factor 799 // handleNonSpecialCharacterEvent which has the same name as other handle* methods but is 800 // not the same. 801 boolean isComposingWord = mWordComposer.isComposingWord(); 802 803 // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead. 804 // See onStartBatchInput() to see how to do it. 805 if (SpaceState.PHANTOM == inputTransaction.mSpaceState 806 && !settingsValues.isWordConnector(codePoint)) { 807 if (isComposingWord) { 808 // Sanity check 809 throw new RuntimeException("Should not be composing here"); 810 } 811 insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); 812 } 813 814 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 815 // If we are in the middle of a recorrection, we need to commit the recorrection 816 // first so that we can insert the character at the current cursor position. 817 // We also need to unlearn the original word that is now being corrected. 818 unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues, 819 Constants.EVENT_BACKSPACE); 820 resetEntireInputState(mConnection.getExpectedSelectionStart(), 821 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 822 isComposingWord = false; 823 } 824 // We want to find out whether to start composing a new word with this character. If so, 825 // we need to reset the composing state and switch isComposingWord. The order of the 826 // tests is important for good performance. 827 // We only start composing if we're not already composing. 828 if (!isComposingWord 829 // We only start composing if this is a word code point. Essentially that means it's a 830 // a letter or a word connector. 831 && settingsValues.isWordCodePoint(codePoint) 832 // We never go into composing state if suggestions are not requested. 833 && settingsValues.needsToLookupSuggestions() && 834 // In languages with spaces, we only start composing a word when we are not already 835 // touching a word. In languages without spaces, the above conditions are sufficient. 836 // NOTE: If the InputConnection is slow, we skip the text-after-cursor check since it 837 // can incur a very expensive getTextAfterCursor() lookup, potentially making the 838 // keyboard UI slow and non-responsive. 839 // TODO: Cache the text after the cursor so we don't need to go to the InputConnection 840 // each time. We are already doing this for getTextBeforeCursor(). 841 (!settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 842 || !mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, 843 !mConnection.hasSlowInputConnection() /* checkTextAfter */))) { 844 // Reset entirely the composing state anyway, then start composing a new word unless 845 // the character is a word connector. The idea here is, word connectors are not 846 // separators and they should be treated as normal characters, except in the first 847 // position where they should not start composing a word. 848 isComposingWord = !settingsValues.mSpacingAndPunctuations.isWordConnector(codePoint); 849 // Here we don't need to reset the last composed word. It will be reset 850 // when we commit this one, if we ever do; if on the other hand we backspace 851 // it entirely and resume suggestions on the previous word, we'd like to still 852 // have touch coordinates for it. 853 resetComposingState(false /* alsoResetLastComposedWord */); 854 } 855 if (isComposingWord) { 856 mWordComposer.applyProcessedEvent(event); 857 // If it's the first letter, make note of auto-caps state 858 if (mWordComposer.isSingleLetter()) { 859 mWordComposer.setCapitalizedModeAtStartComposingTime(inputTransaction.mShiftState); 860 } 861 setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 862 } else { 863 final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, 864 inputTransaction); 865 866 if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { 867 mSpaceState = SpaceState.WEAK; 868 } else { 869 sendKeyCodePoint(settingsValues, codePoint); 870 } 871 } 872 inputTransaction.setRequiresUpdateSuggestions(); 873 } 874 875 /** 876 * Handle input of a separator code point. 877 * @param event The event to handle. 878 * @param inputTransaction The transaction in progress. 879 */ handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, final LatinIME.UIHandler handler)880 private void handleSeparatorEvent(final Event event, final InputTransaction inputTransaction, 881 final LatinIME.UIHandler handler) { 882 final int codePoint = event.mCodePoint; 883 final SettingsValues settingsValues = inputTransaction.mSettingsValues; 884 final boolean wasComposingWord = mWordComposer.isComposingWord(); 885 // We avoid sending spaces in languages without spaces if we were composing. 886 final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint 887 && !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 888 && wasComposingWord; 889 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 890 // If we are in the middle of a recorrection, we need to commit the recorrection 891 // first so that we can insert the separator at the current cursor position. 892 // We also need to unlearn the original word that is now being corrected. 893 unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues, 894 Constants.EVENT_BACKSPACE); 895 resetEntireInputState(mConnection.getExpectedSelectionStart(), 896 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 897 } 898 // isComposingWord() may have changed since we stored wasComposing 899 if (mWordComposer.isComposingWord()) { 900 if (settingsValues.mAutoCorrectionEnabledPerUserSettings) { 901 final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR 902 : StringUtils.newSingleCodePointString(codePoint); 903 commitCurrentAutoCorrection(settingsValues, separator, handler); 904 inputTransaction.setDidAutoCorrect(); 905 } else { 906 commitTyped(settingsValues, 907 StringUtils.newSingleCodePointString(codePoint)); 908 } 909 } 910 911 final boolean swapWeakSpace = tryStripSpaceAndReturnWhetherShouldSwapInstead(event, 912 inputTransaction); 913 914 final boolean isInsideDoubleQuoteOrAfterDigit = Constants.CODE_DOUBLE_QUOTE == codePoint 915 && mConnection.isInsideDoubleQuoteOrAfterDigit(); 916 917 final boolean needsPrecedingSpace; 918 if (SpaceState.PHANTOM != inputTransaction.mSpaceState) { 919 needsPrecedingSpace = false; 920 } else if (Constants.CODE_DOUBLE_QUOTE == codePoint) { 921 // Double quotes behave like they are usually preceded by space iff we are 922 // not inside a double quote or after a digit. 923 needsPrecedingSpace = !isInsideDoubleQuoteOrAfterDigit; 924 } else if (settingsValues.mSpacingAndPunctuations.isClusteringSymbol(codePoint) 925 && settingsValues.mSpacingAndPunctuations.isClusteringSymbol( 926 mConnection.getCodePointBeforeCursor())) { 927 needsPrecedingSpace = false; 928 } else { 929 needsPrecedingSpace = settingsValues.isUsuallyPrecededBySpace(codePoint); 930 } 931 932 if (needsPrecedingSpace) { 933 insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); 934 } 935 936 if (tryPerformDoubleSpacePeriod(event, inputTransaction)) { 937 mSpaceState = SpaceState.DOUBLE; 938 inputTransaction.setRequiresUpdateSuggestions(); 939 StatsUtils.onDoubleSpacePeriod(); 940 } else if (swapWeakSpace && trySwapSwapperAndSpace(event, inputTransaction)) { 941 mSpaceState = SpaceState.SWAP_PUNCTUATION; 942 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 943 } else if (Constants.CODE_SPACE == codePoint) { 944 if (!mSuggestedWords.isPunctuationSuggestions()) { 945 mSpaceState = SpaceState.WEAK; 946 } 947 948 startDoubleSpacePeriodCountdown(inputTransaction); 949 if (wasComposingWord || mSuggestedWords.isEmpty()) { 950 inputTransaction.setRequiresUpdateSuggestions(); 951 } 952 953 if (!shouldAvoidSendingCode) { 954 sendKeyCodePoint(settingsValues, codePoint); 955 } 956 } else { 957 if ((SpaceState.PHANTOM == inputTransaction.mSpaceState 958 && settingsValues.isUsuallyFollowedBySpace(codePoint)) 959 || (Constants.CODE_DOUBLE_QUOTE == codePoint 960 && isInsideDoubleQuoteOrAfterDigit)) { 961 // If we are in phantom space state, and the user presses a separator, we want to 962 // stay in phantom space state so that the next keypress has a chance to add the 963 // space. For example, if I type "Good dat", pick "day" from the suggestion strip 964 // then insert a comma and go on to typing the next word, I want the space to be 965 // inserted automatically before the next word, the same way it is when I don't 966 // input the comma. A double quote behaves like it's usually followed by space if 967 // we're inside a double quote. 968 // The case is a little different if the separator is a space stripper. Such a 969 // separator does not normally need a space on the right (that's the difference 970 // between swappers and strippers), so we should not stay in phantom space state if 971 // the separator is a stripper. Hence the additional test above. 972 mSpaceState = SpaceState.PHANTOM; 973 } 974 975 sendKeyCodePoint(settingsValues, codePoint); 976 977 // Set punctuation right away. onUpdateSelection will fire but tests whether it is 978 // already displayed or not, so it's okay. 979 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 980 } 981 982 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 983 } 984 985 /** 986 * Handle a press on the backspace key. 987 * @param event The event to handle. 988 * @param inputTransaction The transaction in progress. 989 */ handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, final int currentKeyboardScriptId)990 private void handleBackspaceEvent(final Event event, final InputTransaction inputTransaction, 991 final int currentKeyboardScriptId) { 992 mSpaceState = SpaceState.NONE; 993 mDeleteCount++; 994 995 // In many cases after backspace, we need to update the shift state. Normally we need 996 // to do this right away to avoid the shift state being out of date in case the user types 997 // backspace then some other character very fast. However, in the case of backspace key 998 // repeat, this can lead to flashiness when the cursor flies over positions where the 999 // shift state should be updated, so if this is a key repeat, we update after a small delay. 1000 // Then again, even in the case of a key repeat, if the cursor is at start of text, it 1001 // can't go any further back, so we can update right away even if it's a key repeat. 1002 final int shiftUpdateKind = 1003 event.isKeyRepeat() && mConnection.getExpectedSelectionStart() > 0 1004 ? InputTransaction.SHIFT_UPDATE_LATER : InputTransaction.SHIFT_UPDATE_NOW; 1005 inputTransaction.requireShiftUpdate(shiftUpdateKind); 1006 1007 if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) { 1008 // If we are in the middle of a recorrection, we need to commit the recorrection 1009 // first so that we can remove the character at the current cursor position. 1010 // We also need to unlearn the original word that is now being corrected. 1011 unlearnWord(mWordComposer.getTypedWord(), inputTransaction.mSettingsValues, 1012 Constants.EVENT_BACKSPACE); 1013 resetEntireInputState(mConnection.getExpectedSelectionStart(), 1014 mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */); 1015 // When we exit this if-clause, mWordComposer.isComposingWord() will return false. 1016 } 1017 if (mWordComposer.isComposingWord()) { 1018 if (mWordComposer.isBatchMode()) { 1019 final String rejectedSuggestion = mWordComposer.getTypedWord(); 1020 mWordComposer.reset(); 1021 mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion); 1022 if (!TextUtils.isEmpty(rejectedSuggestion)) { 1023 unlearnWord(rejectedSuggestion, inputTransaction.mSettingsValues, 1024 Constants.EVENT_REJECTION); 1025 } 1026 StatsUtils.onBackspaceWordDelete(rejectedSuggestion.length()); 1027 } else { 1028 mWordComposer.applyProcessedEvent(event); 1029 StatsUtils.onBackspacePressed(1); 1030 } 1031 if (mWordComposer.isComposingWord()) { 1032 setComposingTextInternal(getTextWithUnderline(mWordComposer.getTypedWord()), 1); 1033 } else { 1034 mConnection.commitText("", 1); 1035 } 1036 inputTransaction.setRequiresUpdateSuggestions(); 1037 } else { 1038 if (mLastComposedWord.canRevertCommit()) { 1039 final String lastComposedWord = mLastComposedWord.mTypedWord; 1040 revertCommit(inputTransaction, inputTransaction.mSettingsValues); 1041 StatsUtils.onRevertAutoCorrect(); 1042 StatsUtils.onWordCommitUserTyped(lastComposedWord, mWordComposer.isBatchMode()); 1043 // Restart suggestions when backspacing into a reverted word. This is required for 1044 // the final corrected word to be learned, as learning only occurs when suggestions 1045 // are active. 1046 // 1047 // Note: restartSuggestionsOnWordTouchedByCursor is already called for normal 1048 // (non-revert) backspace handling. 1049 if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() 1050 && inputTransaction.mSettingsValues.mSpacingAndPunctuations 1051 .mCurrentLanguageHasSpaces 1052 && !mConnection.isCursorFollowedByWordCharacter( 1053 inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { 1054 restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, 1055 false /* forStartInput */, currentKeyboardScriptId); 1056 } 1057 return; 1058 } 1059 if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) { 1060 // Cancel multi-character input: remove the text we just entered. 1061 // This is triggered on backspace after a key that inputs multiple characters, 1062 // like the smiley key or the .com key. 1063 mConnection.deleteTextBeforeCursor(mEnteredText.length()); 1064 StatsUtils.onDeleteMultiCharInput(mEnteredText.length()); 1065 mEnteredText = null; 1066 // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false. 1067 // In addition we know that spaceState is false, and that we should not be 1068 // reverting any autocorrect at this point. So we can safely return. 1069 return; 1070 } 1071 if (SpaceState.DOUBLE == inputTransaction.mSpaceState) { 1072 cancelDoubleSpacePeriodCountdown(); 1073 if (mConnection.revertDoubleSpacePeriod( 1074 inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { 1075 // No need to reset mSpaceState, it has already be done (that's why we 1076 // receive it as a parameter) 1077 inputTransaction.setRequiresUpdateSuggestions(); 1078 mWordComposer.setCapitalizedModeAtStartComposingTime( 1079 WordComposer.CAPS_MODE_OFF); 1080 StatsUtils.onRevertDoubleSpacePeriod(); 1081 return; 1082 } 1083 } else if (SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { 1084 if (mConnection.revertSwapPunctuation()) { 1085 StatsUtils.onRevertSwapPunctuation(); 1086 // Likewise 1087 return; 1088 } 1089 } 1090 1091 boolean hasUnlearnedWordBeingDeleted = false; 1092 1093 // No cancelling of commit/double space/swap: we have a regular backspace. 1094 // We should backspace one char and restart suggestion if at the end of a word. 1095 if (mConnection.hasSelection()) { 1096 // If there is a selection, remove it. 1097 // We also need to unlearn the selected text. 1098 final CharSequence selection = mConnection.getSelectedText(0 /* 0 for no styles */); 1099 if (!TextUtils.isEmpty(selection)) { 1100 unlearnWord(selection.toString(), inputTransaction.mSettingsValues, 1101 Constants.EVENT_BACKSPACE); 1102 hasUnlearnedWordBeingDeleted = true; 1103 } 1104 final int numCharsDeleted = mConnection.getExpectedSelectionEnd() 1105 - mConnection.getExpectedSelectionStart(); 1106 mConnection.setSelection(mConnection.getExpectedSelectionEnd(), 1107 mConnection.getExpectedSelectionEnd()); 1108 mConnection.deleteTextBeforeCursor(numCharsDeleted); 1109 StatsUtils.onBackspaceSelectedText(numCharsDeleted); 1110 } else { 1111 // There is no selection, just delete one character. 1112 if (inputTransaction.mSettingsValues.isBeforeJellyBean() 1113 || inputTransaction.mSettingsValues.mInputAttributes.isTypeNull() 1114 || Constants.NOT_A_CURSOR_POSITION 1115 == mConnection.getExpectedSelectionEnd()) { 1116 // There are three possible reasons to send a key event: either the field has 1117 // type TYPE_NULL, in which case the keyboard should send events, or we are 1118 // running in backward compatibility mode, or we don't know the cursor position. 1119 // Before Jelly bean, the keyboard would simulate a hardware keyboard event on 1120 // pressing enter or delete. This is bad for many reasons (there are race 1121 // conditions with commits) but some applications are relying on this behavior 1122 // so we continue to support it for older apps, so we retain this behavior if 1123 // the app has target SDK < JellyBean. 1124 // As for the case where we don't know the cursor position, it can happen 1125 // because of bugs in the framework. But the framework should know, so the next 1126 // best thing is to leave it to whatever it thinks is best. 1127 sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); 1128 int totalDeletedLength = 1; 1129 if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { 1130 // If this is an accelerated (i.e., double) deletion, then we need to 1131 // consider unlearning here because we may have already reached 1132 // the previous word, and will lose it after next deletion. 1133 hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( 1134 inputTransaction.mSettingsValues, currentKeyboardScriptId); 1135 sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); 1136 totalDeletedLength++; 1137 } 1138 StatsUtils.onBackspacePressed(totalDeletedLength); 1139 } else { 1140 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1141 if (codePointBeforeCursor == Constants.NOT_A_CODE) { 1142 // HACK for backward compatibility with broken apps that haven't realized 1143 // yet that hardware keyboards are not the only way of inputting text. 1144 // Nothing to delete before the cursor. We should not do anything, but many 1145 // broken apps expect something to happen in this case so that they can 1146 // catch it and have their broken interface react. If you need the keyboard 1147 // to do this, you're doing it wrong -- please fix your app. 1148 mConnection.deleteTextBeforeCursor(1); 1149 // TODO: Add a new StatsUtils method onBackspaceWhenNoText() 1150 return; 1151 } 1152 final int lengthToDelete = 1153 Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1; 1154 mConnection.deleteTextBeforeCursor(lengthToDelete); 1155 int totalDeletedLength = lengthToDelete; 1156 if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { 1157 // If this is an accelerated (i.e., double) deletion, then we need to 1158 // consider unlearning here because we may have already reached 1159 // the previous word, and will lose it after next deletion. 1160 hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( 1161 inputTransaction.mSettingsValues, currentKeyboardScriptId); 1162 final int codePointBeforeCursorToDeleteAgain = 1163 mConnection.getCodePointBeforeCursor(); 1164 if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) { 1165 final int lengthToDeleteAgain = Character.isSupplementaryCodePoint( 1166 codePointBeforeCursorToDeleteAgain) ? 2 : 1; 1167 mConnection.deleteTextBeforeCursor(lengthToDeleteAgain); 1168 totalDeletedLength += lengthToDeleteAgain; 1169 } 1170 } 1171 StatsUtils.onBackspacePressed(totalDeletedLength); 1172 } 1173 } 1174 if (!hasUnlearnedWordBeingDeleted) { 1175 // Consider unlearning the word being deleted (if we have not done so already). 1176 unlearnWordBeingDeleted( 1177 inputTransaction.mSettingsValues, currentKeyboardScriptId); 1178 } 1179 if (mConnection.hasSlowInputConnection()) { 1180 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1181 } else if (inputTransaction.mSettingsValues.isSuggestionsEnabledPerUserSettings() 1182 && inputTransaction.mSettingsValues.mSpacingAndPunctuations 1183 .mCurrentLanguageHasSpaces 1184 && !mConnection.isCursorFollowedByWordCharacter( 1185 inputTransaction.mSettingsValues.mSpacingAndPunctuations)) { 1186 restartSuggestionsOnWordTouchedByCursor(inputTransaction.mSettingsValues, 1187 false /* forStartInput */, currentKeyboardScriptId); 1188 } 1189 } 1190 } 1191 getWordAtCursor(final SettingsValues settingsValues, final int currentKeyboardScriptId)1192 String getWordAtCursor(final SettingsValues settingsValues, final int currentKeyboardScriptId) { 1193 if (!mConnection.hasSelection() 1194 && settingsValues.isSuggestionsEnabledPerUserSettings() 1195 && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { 1196 final TextRange range = mConnection.getWordRangeAtCursor( 1197 settingsValues.mSpacingAndPunctuations, 1198 currentKeyboardScriptId); 1199 if (range != null) { 1200 return range.mWord.toString(); 1201 } 1202 } 1203 return ""; 1204 } 1205 unlearnWordBeingDeleted( final SettingsValues settingsValues, final int currentKeyboardScriptId)1206 boolean unlearnWordBeingDeleted( 1207 final SettingsValues settingsValues, final int currentKeyboardScriptId) { 1208 if (mConnection.hasSlowInputConnection()) { 1209 // TODO: Refactor unlearning so that it does not incur any extra calls 1210 // to the InputConnection. That way it can still be performed on a slow 1211 // InputConnection. 1212 Log.w(TAG, "Skipping unlearning due to slow InputConnection."); 1213 return false; 1214 } 1215 // If we just started backspacing to delete a previous word (but have not 1216 // entered the composing state yet), unlearn the word. 1217 // TODO: Consider tracking whether or not this word was typed by the user. 1218 if (!mConnection.isCursorFollowedByWordCharacter(settingsValues.mSpacingAndPunctuations)) { 1219 final String wordBeingDeleted = getWordAtCursor( 1220 settingsValues, currentKeyboardScriptId); 1221 if (!TextUtils.isEmpty(wordBeingDeleted)) { 1222 unlearnWord(wordBeingDeleted, settingsValues, Constants.EVENT_BACKSPACE); 1223 return true; 1224 } 1225 } 1226 return false; 1227 } 1228 unlearnWord(final String word, final SettingsValues settingsValues, final int eventType)1229 void unlearnWord(final String word, final SettingsValues settingsValues, final int eventType) { 1230 final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( 1231 settingsValues.mSpacingAndPunctuations, 2); 1232 final long timeStampInSeconds = TimeUnit.MILLISECONDS.toSeconds( 1233 System.currentTimeMillis()); 1234 mDictionaryFacilitator.unlearnFromUserHistory( 1235 word, ngramContext, timeStampInSeconds, eventType); 1236 } 1237 1238 /** 1239 * Handle a press on the language switch key (the "globe key") 1240 */ handleLanguageSwitchKey()1241 private void handleLanguageSwitchKey() { 1242 mLatinIME.switchToNextSubtype(); 1243 } 1244 1245 /** 1246 * Swap a space with a space-swapping punctuation sign. 1247 * 1248 * This method will check that there are two characters before the cursor and that the first 1249 * one is a space before it does the actual swapping. 1250 * @param event The event to handle. 1251 * @param inputTransaction The transaction in progress. 1252 * @return true if the swap has been performed, false if it was prevented by preliminary checks. 1253 */ trySwapSwapperAndSpace(final Event event, final InputTransaction inputTransaction)1254 private boolean trySwapSwapperAndSpace(final Event event, 1255 final InputTransaction inputTransaction) { 1256 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1257 if (Constants.CODE_SPACE != codePointBeforeCursor) { 1258 return false; 1259 } 1260 mConnection.deleteTextBeforeCursor(1); 1261 final String text = event.getTextToCommit() + " "; 1262 mConnection.commitText(text, 1); 1263 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 1264 return true; 1265 } 1266 1267 /* 1268 * Strip a trailing space if necessary and returns whether it's a swap weak space situation. 1269 * @param event The event to handle. 1270 * @param inputTransaction The transaction in progress. 1271 * @return whether we should swap the space instead of removing it. 1272 */ tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event, final InputTransaction inputTransaction)1273 private boolean tryStripSpaceAndReturnWhetherShouldSwapInstead(final Event event, 1274 final InputTransaction inputTransaction) { 1275 final int codePoint = event.mCodePoint; 1276 final boolean isFromSuggestionStrip = event.isSuggestionStripPress(); 1277 if (Constants.CODE_ENTER == codePoint && 1278 SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) { 1279 mConnection.removeTrailingSpace(); 1280 return false; 1281 } 1282 if ((SpaceState.WEAK == inputTransaction.mSpaceState 1283 || SpaceState.SWAP_PUNCTUATION == inputTransaction.mSpaceState) 1284 && isFromSuggestionStrip) { 1285 if (inputTransaction.mSettingsValues.isUsuallyPrecededBySpace(codePoint)) { 1286 return false; 1287 } 1288 if (inputTransaction.mSettingsValues.isUsuallyFollowedBySpace(codePoint)) { 1289 return true; 1290 } 1291 mConnection.removeTrailingSpace(); 1292 } 1293 return false; 1294 } 1295 startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction)1296 public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) { 1297 mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp; 1298 } 1299 cancelDoubleSpacePeriodCountdown()1300 public void cancelDoubleSpacePeriodCountdown() { 1301 mDoubleSpacePeriodCountdownStart = 0; 1302 } 1303 isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction)1304 public boolean isDoubleSpacePeriodCountdownActive(final InputTransaction inputTransaction) { 1305 return inputTransaction.mTimestamp - mDoubleSpacePeriodCountdownStart 1306 < inputTransaction.mSettingsValues.mDoubleSpacePeriodTimeout; 1307 } 1308 1309 /** 1310 * Apply the double-space-to-period transformation if applicable. 1311 * 1312 * The double-space-to-period transformation means that we replace two spaces with a 1313 * period-space sequence of characters. This typically happens when the user presses space 1314 * twice in a row quickly. 1315 * This method will check that the double-space-to-period is active in settings, that the 1316 * two spaces have been input close enough together, that the typed character is a space 1317 * and that the previous character allows for the transformation to take place. If all of 1318 * these conditions are fulfilled, this method applies the transformation and returns true. 1319 * Otherwise, it does nothing and returns false. 1320 * 1321 * @param event The event to handle. 1322 * @param inputTransaction The transaction in progress. 1323 * @return true if we applied the double-space-to-period transformation, false otherwise. 1324 */ tryPerformDoubleSpacePeriod(final Event event, final InputTransaction inputTransaction)1325 private boolean tryPerformDoubleSpacePeriod(final Event event, 1326 final InputTransaction inputTransaction) { 1327 // Check the setting, the typed character and the countdown. If any of the conditions is 1328 // not fulfilled, return false. 1329 if (!inputTransaction.mSettingsValues.mUseDoubleSpacePeriod 1330 || Constants.CODE_SPACE != event.mCodePoint 1331 || !isDoubleSpacePeriodCountdownActive(inputTransaction)) { 1332 return false; 1333 } 1334 // We only do this when we see one space and an accepted code point before the cursor. 1335 // The code point may be a surrogate pair but the space may not, so we need 3 chars. 1336 final CharSequence lastTwo = mConnection.getTextBeforeCursor(3, 0); 1337 if (null == lastTwo) return false; 1338 final int length = lastTwo.length(); 1339 if (length < 2) return false; 1340 if (lastTwo.charAt(length - 1) != Constants.CODE_SPACE) { 1341 return false; 1342 } 1343 // We know there is a space in pos -1, and we have at least two chars. If we have only two 1344 // chars, isSurrogatePairs can't return true as charAt(1) is a space, so this is fine. 1345 final int firstCodePoint = 1346 Character.isSurrogatePair(lastTwo.charAt(0), lastTwo.charAt(1)) ? 1347 Character.codePointAt(lastTwo, length - 3) : lastTwo.charAt(length - 2); 1348 if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) { 1349 cancelDoubleSpacePeriodCountdown(); 1350 mConnection.deleteTextBeforeCursor(1); 1351 final String textToInsert = inputTransaction.mSettingsValues.mSpacingAndPunctuations 1352 .mSentenceSeparatorAndSpace; 1353 mConnection.commitText(textToInsert, 1); 1354 inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW); 1355 inputTransaction.setRequiresUpdateSuggestions(); 1356 return true; 1357 } 1358 return false; 1359 } 1360 1361 /** 1362 * Returns whether this code point can be followed by the double-space-to-period transformation. 1363 * 1364 * See #maybeDoubleSpaceToPeriod for details. 1365 * Generally, most word characters can be followed by the double-space-to-period transformation, 1366 * while most punctuation can't. Some punctuation however does allow for this to take place 1367 * after them, like the closing parenthesis for example. 1368 * 1369 * @param codePoint the code point after which we may want to apply the transformation 1370 * @return whether it's fine to apply the transformation after this code point. 1371 */ canBeFollowedByDoubleSpacePeriod(final int codePoint)1372 private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) { 1373 // TODO: This should probably be a blacklist rather than a whitelist. 1374 // TODO: This should probably be language-dependant... 1375 return Character.isLetterOrDigit(codePoint) 1376 || codePoint == Constants.CODE_SINGLE_QUOTE 1377 || codePoint == Constants.CODE_DOUBLE_QUOTE 1378 || codePoint == Constants.CODE_CLOSING_PARENTHESIS 1379 || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET 1380 || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET 1381 || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET 1382 || codePoint == Constants.CODE_PLUS 1383 || codePoint == Constants.CODE_PERCENT 1384 || Character.getType(codePoint) == Character.OTHER_SYMBOL; 1385 } 1386 1387 /** 1388 * Performs a recapitalization event. 1389 * @param settingsValues The current settings values. 1390 */ performRecapitalization(final SettingsValues settingsValues)1391 private void performRecapitalization(final SettingsValues settingsValues) { 1392 if (!mConnection.hasSelection() || !mRecapitalizeStatus.mIsEnabled()) { 1393 return; // No selection or recapitalize is disabled for now 1394 } 1395 final int selectionStart = mConnection.getExpectedSelectionStart(); 1396 final int selectionEnd = mConnection.getExpectedSelectionEnd(); 1397 final int numCharsSelected = selectionEnd - selectionStart; 1398 if (numCharsSelected > Constants.MAX_CHARACTERS_FOR_RECAPITALIZATION) { 1399 // We bail out if we have too many characters for performance reasons. We don't want 1400 // to suck possibly multiple-megabyte data. 1401 return; 1402 } 1403 // If we have a recapitalize in progress, use it; otherwise, start a new one. 1404 if (!mRecapitalizeStatus.isStarted() 1405 || !mRecapitalizeStatus.isSetAt(selectionStart, selectionEnd)) { 1406 final CharSequence selectedText = 1407 mConnection.getSelectedText(0 /* flags, 0 for no styles */); 1408 if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection 1409 mRecapitalizeStatus.start(selectionStart, selectionEnd, selectedText.toString(), 1410 settingsValues.mLocale, 1411 settingsValues.mSpacingAndPunctuations.mSortedWordSeparators); 1412 // We trim leading and trailing whitespace. 1413 mRecapitalizeStatus.trim(); 1414 } 1415 mConnection.finishComposingText(); 1416 mRecapitalizeStatus.rotate(); 1417 mConnection.setSelection(selectionEnd, selectionEnd); 1418 mConnection.deleteTextBeforeCursor(numCharsSelected); 1419 mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0); 1420 mConnection.setSelection(mRecapitalizeStatus.getNewCursorStart(), 1421 mRecapitalizeStatus.getNewCursorEnd()); 1422 } 1423 performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, final String suggestion, @Nonnull final NgramContext ngramContext)1424 private void performAdditionToUserHistoryDictionary(final SettingsValues settingsValues, 1425 final String suggestion, @Nonnull final NgramContext ngramContext) { 1426 // If correction is not enabled, we don't add words to the user history dictionary. 1427 // That's to avoid unintended additions in some sensitive fields, or fields that 1428 // expect to receive non-words. 1429 if (!settingsValues.mAutoCorrectionEnabledPerUserSettings) return; 1430 if (mConnection.hasSlowInputConnection()) { 1431 // Since we don't unlearn when the user backspaces on a slow InputConnection, 1432 // turn off learning to guard against adding typos that the user later deletes. 1433 Log.w(TAG, "Skipping learning due to slow InputConnection."); 1434 return; 1435 } 1436 1437 if (TextUtils.isEmpty(suggestion)) return; 1438 final boolean wasAutoCapitalized = 1439 mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps(); 1440 final int timeStampInSeconds = (int)TimeUnit.MILLISECONDS.toSeconds( 1441 System.currentTimeMillis()); 1442 mDictionaryFacilitator.addToUserHistory(suggestion, wasAutoCapitalized, 1443 ngramContext, timeStampInSeconds, settingsValues.mBlockPotentiallyOffensive); 1444 } 1445 performUpdateSuggestionStripSync(final SettingsValues settingsValues, final int inputStyle)1446 public void performUpdateSuggestionStripSync(final SettingsValues settingsValues, 1447 final int inputStyle) { 1448 long startTimeMillis = 0; 1449 if (DebugFlags.DEBUG_ENABLED) { 1450 startTimeMillis = System.currentTimeMillis(); 1451 Log.d(TAG, "performUpdateSuggestionStripSync()"); 1452 } 1453 // Check if we have a suggestion engine attached. 1454 if (!settingsValues.needsToLookupSuggestions()) { 1455 if (mWordComposer.isComposingWord()) { 1456 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not " 1457 + "requested!"); 1458 } 1459 // Clear the suggestions strip. 1460 mSuggestionStripViewAccessor.showSuggestionStrip(SuggestedWords.getEmptyInstance()); 1461 return; 1462 } 1463 1464 if (!mWordComposer.isComposingWord() && !settingsValues.mBigramPredictionEnabled) { 1465 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1466 return; 1467 } 1468 1469 final AsyncResultHolder<SuggestedWords> holder = new AsyncResultHolder<>("Suggest"); 1470 mInputLogicHandler.getSuggestedWords(inputStyle, SuggestedWords.NOT_A_SEQUENCE_NUMBER, 1471 new OnGetSuggestedWordsCallback() { 1472 @Override 1473 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1474 final String typedWordString = mWordComposer.getTypedWord(); 1475 final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo( 1476 typedWordString, "" /* prevWordsContext */, 1477 SuggestedWordInfo.MAX_SCORE, 1478 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, 1479 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 1480 SuggestedWordInfo.NOT_A_CONFIDENCE); 1481 // Show new suggestions if we have at least one. Otherwise keep the old 1482 // suggestions with the new typed word. Exception: if the length of the 1483 // typed word is <= 1 (after a deletion typically) we clear old suggestions. 1484 if (suggestedWords.size() > 1 || typedWordString.length() <= 1) { 1485 holder.set(suggestedWords); 1486 } else { 1487 holder.set(retrieveOlderSuggestions(typedWordInfo, mSuggestedWords)); 1488 } 1489 } 1490 } 1491 ); 1492 1493 // This line may cause the current thread to wait. 1494 final SuggestedWords suggestedWords = holder.get(null, 1495 Constants.GET_SUGGESTED_WORDS_TIMEOUT); 1496 if (suggestedWords != null) { 1497 mSuggestionStripViewAccessor.showSuggestionStrip(suggestedWords); 1498 } 1499 if (DebugFlags.DEBUG_ENABLED) { 1500 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 1501 Log.d(TAG, "performUpdateSuggestionStripSync() : " + runTimeMillis + " ms to finish"); 1502 } 1503 } 1504 1505 /** 1506 * Check if the cursor is touching a word. If so, restart suggestions on this word, else 1507 * do nothing. 1508 * 1509 * @param settingsValues the current values of the settings. 1510 * @param forStartInput whether we're doing this in answer to starting the input (as opposed 1511 * to a cursor move, for example). In ICS, there is a platform bug that we need to work 1512 * around only when we come here at input start time. 1513 */ restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, final boolean forStartInput, final int currentKeyboardScriptId)1514 public void restartSuggestionsOnWordTouchedByCursor(final SettingsValues settingsValues, 1515 final boolean forStartInput, 1516 // TODO: remove this argument, put it into settingsValues 1517 final int currentKeyboardScriptId) { 1518 // HACK: We may want to special-case some apps that exhibit bad behavior in case of 1519 // recorrection. This is a temporary, stopgap measure that will be removed later. 1520 // TODO: remove this. 1521 if (settingsValues.isBrokenByRecorrection() 1522 // Recorrection is not supported in languages without spaces because we don't know 1523 // how to segment them yet. 1524 || !settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 1525 // If no suggestions are requested, don't try restarting suggestions. 1526 || !settingsValues.needsToLookupSuggestions() 1527 // If we are currently in a batch input, we must not resume suggestions, or the result 1528 // of the batch input will replace the new composition. This may happen in the corner case 1529 // that the app moves the cursor on its own accord during a batch input. 1530 || mInputLogicHandler.isInBatchInput() 1531 // If the cursor is not touching a word, or if there is a selection, return right away. 1532 || mConnection.hasSelection() 1533 // If we don't know the cursor location, return. 1534 || mConnection.getExpectedSelectionStart() < 0) { 1535 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1536 return; 1537 } 1538 final int expectedCursorPosition = mConnection.getExpectedSelectionStart(); 1539 if (!mConnection.isCursorTouchingWord(settingsValues.mSpacingAndPunctuations, 1540 true /* checkTextAfter */)) { 1541 // Show predictions. 1542 mWordComposer.setCapitalizedModeAtStartComposingTime(WordComposer.CAPS_MODE_OFF); 1543 mLatinIME.mHandler.postUpdateSuggestionStrip(SuggestedWords.INPUT_STYLE_RECORRECTION); 1544 return; 1545 } 1546 final TextRange range = mConnection.getWordRangeAtCursor( 1547 settingsValues.mSpacingAndPunctuations, currentKeyboardScriptId); 1548 if (null == range) return; // Happens if we don't have an input connection at all 1549 if (range.length() <= 0) { 1550 // Race condition, or touching a word in a non-supported script. 1551 mLatinIME.setNeutralSuggestionStrip(); 1552 return; 1553 } 1554 // If for some strange reason (editor bug or so) we measure the text before the cursor as 1555 // longer than what the entire text is supposed to be, the safe thing to do is bail out. 1556 if (range.mHasUrlSpans) return; // If there are links, we don't resume suggestions. Making 1557 // edits to a linkified text through batch commands would ruin the URL spans, and unless 1558 // we take very complicated steps to preserve the whole link, we can't do things right so 1559 // we just do not resume because it's safer. 1560 final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor(); 1561 if (numberOfCharsInWordBeforeCursor > expectedCursorPosition) return; 1562 final ArrayList<SuggestedWordInfo> suggestions = new ArrayList<>(); 1563 final String typedWordString = range.mWord.toString(); 1564 final SuggestedWordInfo typedWordInfo = new SuggestedWordInfo(typedWordString, 1565 "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS + 1, 1566 SuggestedWordInfo.KIND_TYPED, Dictionary.DICTIONARY_USER_TYPED, 1567 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 1568 SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */); 1569 suggestions.add(typedWordInfo); 1570 if (!isResumableWord(settingsValues, typedWordString)) { 1571 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1572 return; 1573 } 1574 int i = 0; 1575 for (final SuggestionSpan span : range.getSuggestionSpansAtWord()) { 1576 for (final String s : span.getSuggestions()) { 1577 ++i; 1578 if (!TextUtils.equals(s, typedWordString)) { 1579 suggestions.add(new SuggestedWordInfo(s, 1580 "" /* prevWordsContext */, SuggestedWords.MAX_SUGGESTIONS - i, 1581 SuggestedWordInfo.KIND_RESUMED, Dictionary.DICTIONARY_RESUMED, 1582 SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */, 1583 SuggestedWordInfo.NOT_A_CONFIDENCE 1584 /* autoCommitFirstWordConfidence */)); 1585 } 1586 } 1587 } 1588 final int[] codePoints = StringUtils.toCodePointArray(typedWordString); 1589 mWordComposer.setComposingWord(codePoints, 1590 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); 1591 mWordComposer.setCursorPositionWithinWord( 1592 typedWordString.codePointCount(0, numberOfCharsInWordBeforeCursor)); 1593 if (forStartInput) { 1594 mConnection.maybeMoveTheCursorAroundAndRestoreToWorkaroundABug(); 1595 } 1596 mConnection.setComposingRegion(expectedCursorPosition - numberOfCharsInWordBeforeCursor, 1597 expectedCursorPosition + range.getNumberOfCharsInWordAfterCursor()); 1598 if (suggestions.size() <= 1) { 1599 // If there weren't any suggestion spans on this word, suggestions#size() will be 1 1600 // if shouldIncludeResumedWordInSuggestions is true, 0 otherwise. In this case, we 1601 // have no useful suggestions, so we will try to compute some for it instead. 1602 mInputLogicHandler.getSuggestedWords(Suggest.SESSION_ID_TYPING, 1603 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() { 1604 @Override 1605 public void onGetSuggestedWords(final SuggestedWords suggestedWords) { 1606 doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords); 1607 }}); 1608 } else { 1609 // We found suggestion spans in the word. We'll create the SuggestedWords out of 1610 // them, and make willAutoCorrect false. We make typedWordValid false, because the 1611 // color of the word in the suggestion strip changes according to this parameter, 1612 // and false gives the correct color. 1613 final SuggestedWords suggestedWords = new SuggestedWords(suggestions, 1614 null /* rawSuggestions */, typedWordInfo, false /* typedWordValid */, 1615 false /* willAutoCorrect */, false /* isObsoleteSuggestions */, 1616 SuggestedWords.INPUT_STYLE_RECORRECTION, SuggestedWords.NOT_A_SEQUENCE_NUMBER); 1617 doShowSuggestionsAndClearAutoCorrectionIndicator(suggestedWords); 1618 } 1619 } 1620 doShowSuggestionsAndClearAutoCorrectionIndicator(final SuggestedWords suggestedWords)1621 void doShowSuggestionsAndClearAutoCorrectionIndicator(final SuggestedWords suggestedWords) { 1622 mIsAutoCorrectionIndicatorOn = false; 1623 mLatinIME.mHandler.showSuggestionStrip(suggestedWords); 1624 } 1625 1626 /** 1627 * Reverts a previous commit with auto-correction. 1628 * 1629 * This is triggered upon pressing backspace just after a commit with auto-correction. 1630 * 1631 * @param inputTransaction The transaction in progress. 1632 * @param settingsValues the current values of the settings. 1633 */ revertCommit(final InputTransaction inputTransaction, final SettingsValues settingsValues)1634 private void revertCommit(final InputTransaction inputTransaction, 1635 final SettingsValues settingsValues) { 1636 final CharSequence originallyTypedWord = mLastComposedWord.mTypedWord; 1637 final String originallyTypedWordString = 1638 originallyTypedWord != null ? originallyTypedWord.toString() : ""; 1639 final CharSequence committedWord = mLastComposedWord.mCommittedWord; 1640 final String committedWordString = committedWord.toString(); 1641 final int cancelLength = committedWord.length(); 1642 final String separatorString = mLastComposedWord.mSeparatorString; 1643 // If our separator is a space, we won't actually commit it, 1644 // but set the space state to PHANTOM so that a space will be inserted 1645 // on the next keypress 1646 final boolean usePhantomSpace = separatorString.equals(Constants.STRING_SPACE); 1647 // We want java chars, not codepoints for the following. 1648 final int separatorLength = separatorString.length(); 1649 // TODO: should we check our saved separator against the actual contents of the text view? 1650 final int deleteLength = cancelLength + separatorLength; 1651 if (DebugFlags.DEBUG_ENABLED) { 1652 if (mWordComposer.isComposingWord()) { 1653 throw new RuntimeException("revertCommit, but we are composing a word"); 1654 } 1655 final CharSequence wordBeforeCursor = 1656 mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength); 1657 if (!TextUtils.equals(committedWord, wordBeforeCursor)) { 1658 throw new RuntimeException("revertCommit check failed: we thought we were " 1659 + "reverting \"" + committedWord 1660 + "\", but before the cursor we found \"" + wordBeforeCursor + "\""); 1661 } 1662 } 1663 mConnection.deleteTextBeforeCursor(deleteLength); 1664 if (!TextUtils.isEmpty(committedWord)) { 1665 unlearnWord(committedWordString, inputTransaction.mSettingsValues, 1666 Constants.EVENT_REVERT); 1667 } 1668 final String stringToCommit = originallyTypedWord + 1669 (usePhantomSpace ? "" : separatorString); 1670 final SpannableString textToCommit = new SpannableString(stringToCommit); 1671 if (committedWord instanceof SpannableString) { 1672 final SpannableString committedWordWithSuggestionSpans = (SpannableString)committedWord; 1673 final Object[] spans = committedWordWithSuggestionSpans.getSpans(0, 1674 committedWord.length(), Object.class); 1675 final int lastCharIndex = textToCommit.length() - 1; 1676 // We will collect all suggestions in the following array. 1677 final ArrayList<String> suggestions = new ArrayList<>(); 1678 // First, add the committed word to the list of suggestions. 1679 suggestions.add(committedWordString); 1680 for (final Object span : spans) { 1681 // If this is a suggestion span, we check that the word is not the committed word. 1682 // That should mostly be the case. 1683 // Given this, we add it to the list of suggestions, otherwise we discard it. 1684 if (span instanceof SuggestionSpan) { 1685 final SuggestionSpan suggestionSpan = (SuggestionSpan)span; 1686 for (final String suggestion : suggestionSpan.getSuggestions()) { 1687 if (!suggestion.equals(committedWordString)) { 1688 suggestions.add(suggestion); 1689 } 1690 } 1691 } else { 1692 // If this is not a suggestion span, we just add it as is. 1693 textToCommit.setSpan(span, 0 /* start */, lastCharIndex /* end */, 1694 committedWordWithSuggestionSpans.getSpanFlags(span)); 1695 } 1696 } 1697 // Add the suggestion list to the list of suggestions. 1698 textToCommit.setSpan(new SuggestionSpan(mLatinIME /* context */, 1699 inputTransaction.mSettingsValues.mLocale, 1700 suggestions.toArray(new String[suggestions.size()]), 0 /* flags */, 1701 null /* notificationTargetClass */), 1702 0 /* start */, lastCharIndex /* end */, 0 /* flags */); 1703 } 1704 1705 if (inputTransaction.mSettingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces) { 1706 mConnection.commitText(textToCommit, 1); 1707 if (usePhantomSpace) { 1708 mSpaceState = SpaceState.PHANTOM; 1709 } 1710 } else { 1711 // For languages without spaces, we revert the typed string but the cursor is flush 1712 // with the typed word, so we need to resume suggestions right away. 1713 final int[] codePoints = StringUtils.toCodePointArray(stringToCommit); 1714 mWordComposer.setComposingWord(codePoints, 1715 mLatinIME.getCoordinatesForCurrentKeyboard(codePoints)); 1716 setComposingTextInternal(textToCommit, 1); 1717 } 1718 // Don't restart suggestion yet. We'll restart if the user deletes the separator. 1719 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1720 1721 // We have a separator between the word and the cursor: we should show predictions. 1722 inputTransaction.setRequiresUpdateSuggestions(); 1723 } 1724 1725 /** 1726 * Factor in auto-caps and manual caps and compute the current caps mode. 1727 * @param settingsValues the current settings values. 1728 * @param keyboardShiftMode the current shift mode of the keyboard. See 1729 * KeyboardSwitcher#getKeyboardShiftMode() for possible values. 1730 * @return the actual caps mode the keyboard is in right now. 1731 */ getActualCapsMode(final SettingsValues settingsValues, final int keyboardShiftMode)1732 private int getActualCapsMode(final SettingsValues settingsValues, 1733 final int keyboardShiftMode) { 1734 if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) { 1735 return keyboardShiftMode; 1736 } 1737 final int auto = getCurrentAutoCapsState(settingsValues); 1738 if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) { 1739 return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED; 1740 } 1741 if (0 != auto) { 1742 return WordComposer.CAPS_MODE_AUTO_SHIFTED; 1743 } 1744 return WordComposer.CAPS_MODE_OFF; 1745 } 1746 1747 /** 1748 * Gets the current auto-caps state, factoring in the space state. 1749 * 1750 * This method tries its best to do this in the most efficient possible manner. It avoids 1751 * getting text from the editor if possible at all. 1752 * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it 1753 * needs to know auto caps state to display the right layout. 1754 * 1755 * @param settingsValues the relevant settings values 1756 * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF. 1757 */ getCurrentAutoCapsState(final SettingsValues settingsValues)1758 public int getCurrentAutoCapsState(final SettingsValues settingsValues) { 1759 if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF; 1760 1761 final EditorInfo ei = getCurrentInputEditorInfo(); 1762 if (ei == null) return Constants.TextUtils.CAP_MODE_OFF; 1763 final int inputType = ei.inputType; 1764 // Warning: this depends on mSpaceState, which may not be the most current value. If 1765 // mSpaceState gets updated later, whoever called this may need to be told about it. 1766 return mConnection.getCursorCapsMode(inputType, settingsValues.mSpacingAndPunctuations, 1767 SpaceState.PHANTOM == mSpaceState); 1768 } 1769 getCurrentRecapitalizeState()1770 public int getCurrentRecapitalizeState() { 1771 if (!mRecapitalizeStatus.isStarted() 1772 || !mRecapitalizeStatus.isSetAt(mConnection.getExpectedSelectionStart(), 1773 mConnection.getExpectedSelectionEnd())) { 1774 // Not recapitalizing at the moment 1775 return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE; 1776 } 1777 return mRecapitalizeStatus.getCurrentMode(); 1778 } 1779 1780 /** 1781 * @return the editor info for the current editor 1782 */ getCurrentInputEditorInfo()1783 private EditorInfo getCurrentInputEditorInfo() { 1784 return mLatinIME.getCurrentInputEditorInfo(); 1785 } 1786 1787 /** 1788 * Get n-gram context from the nth previous word before the cursor as context 1789 * for the suggestion process. 1790 * @param spacingAndPunctuations the current spacing and punctuations settings. 1791 * @param nthPreviousWord reverse index of the word to get (1-indexed) 1792 * @return the information of previous words 1793 */ getNgramContextFromNthPreviousWordForSuggestion( final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord)1794 public NgramContext getNgramContextFromNthPreviousWordForSuggestion( 1795 final SpacingAndPunctuations spacingAndPunctuations, final int nthPreviousWord) { 1796 if (spacingAndPunctuations.mCurrentLanguageHasSpaces) { 1797 // If we are typing in a language with spaces we can just look up the previous 1798 // word information from textview. 1799 return mConnection.getNgramContextFromNthPreviousWord( 1800 spacingAndPunctuations, nthPreviousWord); 1801 } 1802 if (LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord) { 1803 return NgramContext.BEGINNING_OF_SENTENCE; 1804 } 1805 return new NgramContext(new NgramContext.WordInfo( 1806 mLastComposedWord.mCommittedWord.toString())); 1807 } 1808 1809 /** 1810 * Tests the passed word for resumability. 1811 * 1812 * We can resume suggestions on words whose first code point is a word code point (with some 1813 * nuances: check the code for details). 1814 * 1815 * @param settings the current values of the settings. 1816 * @param word the word to evaluate. 1817 * @return whether it's fine to resume suggestions on this word. 1818 */ isResumableWord(final SettingsValues settings, final String word)1819 private static boolean isResumableWord(final SettingsValues settings, final String word) { 1820 final int firstCodePoint = word.codePointAt(0); 1821 return settings.isWordCodePoint(firstCodePoint) 1822 && Constants.CODE_SINGLE_QUOTE != firstCodePoint 1823 && Constants.CODE_DASH != firstCodePoint; 1824 } 1825 1826 /** 1827 * @param actionId the action to perform 1828 */ performEditorAction(final int actionId)1829 private void performEditorAction(final int actionId) { 1830 mConnection.performEditorAction(actionId); 1831 } 1832 1833 /** 1834 * Perform the processing specific to inputting TLDs. 1835 * 1836 * Some keys input a TLD (specifically, the ".com" key) and this warrants some specific 1837 * processing. First, if this is a TLD, we ignore PHANTOM spaces -- this is done by type 1838 * of character in onCodeInput, but since this gets inputted as a whole string we need to 1839 * do it here specifically. Then, if the last character before the cursor is a period, then 1840 * we cut the dot at the start of ".com". This is because humans tend to type "www.google." 1841 * and then press the ".com" key and instinctively don't expect to get "www.google..com". 1842 * 1843 * @param text the raw text supplied to onTextInput 1844 * @return the text to actually send to the editor 1845 */ performSpecificTldProcessingOnTextInput(final String text)1846 private String performSpecificTldProcessingOnTextInput(final String text) { 1847 if (text.length() <= 1 || text.charAt(0) != Constants.CODE_PERIOD 1848 || !Character.isLetter(text.charAt(1))) { 1849 // Not a tld: do nothing. 1850 return text; 1851 } 1852 // We have a TLD (or something that looks like this): make sure we don't add 1853 // a space even if currently in phantom mode. 1854 mSpaceState = SpaceState.NONE; 1855 final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor(); 1856 // If no code point, #getCodePointBeforeCursor returns NOT_A_CODE_POINT. 1857 if (Constants.CODE_PERIOD == codePointBeforeCursor) { 1858 return text.substring(1); 1859 } 1860 return text; 1861 } 1862 1863 /** 1864 * Handle a press on the settings key. 1865 */ onSettingsKeyPressed()1866 private void onSettingsKeyPressed() { 1867 mLatinIME.displaySettingsDialog(); 1868 } 1869 1870 /** 1871 * Resets the whole input state to the starting state. 1872 * 1873 * This will clear the composing word, reset the last composed word, clear the suggestion 1874 * strip and tell the input connection about it so that it can refresh its caches. 1875 * 1876 * @param newSelStart the new selection start, in java characters. 1877 * @param newSelEnd the new selection end, in java characters. 1878 * @param clearSuggestionStrip whether this method should clear the suggestion strip. 1879 */ 1880 // TODO: how is this different from startInput ?! resetEntireInputState(final int newSelStart, final int newSelEnd, final boolean clearSuggestionStrip)1881 private void resetEntireInputState(final int newSelStart, final int newSelEnd, 1882 final boolean clearSuggestionStrip) { 1883 final boolean shouldFinishComposition = mWordComposer.isComposingWord(); 1884 resetComposingState(true /* alsoResetLastComposedWord */); 1885 if (clearSuggestionStrip) { 1886 mSuggestionStripViewAccessor.setNeutralSuggestionStrip(); 1887 } 1888 mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd, 1889 shouldFinishComposition); 1890 } 1891 1892 /** 1893 * Resets only the composing state. 1894 * 1895 * Compare #resetEntireInputState, which also clears the suggestion strip and resets the 1896 * input connection caches. This only deals with the composing state. 1897 * 1898 * @param alsoResetLastComposedWord whether to also reset the last composed word. 1899 */ resetComposingState(final boolean alsoResetLastComposedWord)1900 private void resetComposingState(final boolean alsoResetLastComposedWord) { 1901 mWordComposer.reset(); 1902 if (alsoResetLastComposedWord) { 1903 mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD; 1904 } 1905 } 1906 1907 /** 1908 * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word 1909 * and obsolete suggestions. 1910 * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions( 1911 * SuggestedWordInfo, com.android.inputmethod.latin.SuggestedWords)}. 1912 * @param typedWordInfo The typed word as a SuggestedWordInfo. 1913 * @param previousSuggestedWords The previously suggested words. 1914 * @return Obsolete suggestions with the newly typed word. 1915 */ retrieveOlderSuggestions(final SuggestedWordInfo typedWordInfo, final SuggestedWords previousSuggestedWords)1916 static SuggestedWords retrieveOlderSuggestions(final SuggestedWordInfo typedWordInfo, 1917 final SuggestedWords previousSuggestedWords) { 1918 final SuggestedWords oldSuggestedWords = previousSuggestedWords.isPunctuationSuggestions() 1919 ? SuggestedWords.getEmptyInstance() : previousSuggestedWords; 1920 final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions = 1921 SuggestedWords.getTypedWordAndPreviousSuggestions(typedWordInfo, oldSuggestedWords); 1922 return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */, 1923 typedWordInfo, false /* typedWordValid */, false /* hasAutoCorrectionCandidate */, 1924 true /* isObsoleteSuggestions */, oldSuggestedWords.mInputStyle, 1925 SuggestedWords.NOT_A_SEQUENCE_NUMBER); 1926 } 1927 1928 /** 1929 * @return the {@link Locale} of the {@link #mDictionaryFacilitator} if available. Otherwise 1930 * {@link Locale#ROOT}. 1931 */ 1932 @Nonnull getDictionaryFacilitatorLocale()1933 private Locale getDictionaryFacilitatorLocale() { 1934 return mDictionaryFacilitator != null ? mDictionaryFacilitator.getLocale() : Locale.ROOT; 1935 } 1936 1937 /** 1938 * Gets a chunk of text with or the auto-correction indicator underline span as appropriate. 1939 * 1940 * This method looks at the old state of the auto-correction indicator to put or not put 1941 * the underline span as appropriate. It is important to note that this does not correspond 1942 * exactly to whether this word will be auto-corrected to or not: what's important here is 1943 * to keep the same indication as before. 1944 * When we add a new code point to a composing word, we don't know yet if we are going to 1945 * auto-correct it until the suggestions are computed. But in the mean time, we still need 1946 * to display the character and to extend the previous underline. To avoid any flickering, 1947 * the underline should keep the same color it used to have, even if that's not ultimately 1948 * the correct color for this new word. When the suggestions are finished evaluating, we 1949 * will call this method again to fix the color of the underline. 1950 * 1951 * @param text the text on which to maybe apply the span. 1952 * @return the same text, with the auto-correction underline span if that's appropriate. 1953 */ 1954 // TODO: Shouldn't this go in some *Utils class instead? getTextWithUnderline(final String text)1955 private CharSequence getTextWithUnderline(final String text) { 1956 // TODO: Locale should be determined based on context and the text given. 1957 return mIsAutoCorrectionIndicatorOn 1958 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline( 1959 mLatinIME, text, getDictionaryFacilitatorLocale()) 1960 : text; 1961 } 1962 1963 /** 1964 * Sends a DOWN key event followed by an UP key event to the editor. 1965 * 1966 * If possible at all, avoid using this method. It causes all sorts of race conditions with 1967 * the text view because it goes through a different, asynchronous binder. Also, batch edits 1968 * are ignored for key events. Use the normal software input methods instead. 1969 * 1970 * @param keyCode the key code to send inside the key event. 1971 */ sendDownUpKeyEvent(final int keyCode)1972 private void sendDownUpKeyEvent(final int keyCode) { 1973 final long eventTime = SystemClock.uptimeMillis(); 1974 mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, 1975 KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1976 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1977 mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, 1978 KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, 1979 KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); 1980 } 1981 1982 /** 1983 * Sends a code point to the editor, using the most appropriate method. 1984 * 1985 * Normally we send code points with commitText, but there are some cases (where backward 1986 * compatibility is a concern for example) where we want to use deprecated methods. 1987 * 1988 * @param settingsValues the current values of the settings. 1989 * @param codePoint the code point to send. 1990 */ 1991 // TODO: replace these two parameters with an InputTransaction sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint)1992 private void sendKeyCodePoint(final SettingsValues settingsValues, final int codePoint) { 1993 // TODO: Remove this special handling of digit letters. 1994 // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. 1995 if (codePoint >= '0' && codePoint <= '9') { 1996 sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); 1997 return; 1998 } 1999 2000 // TODO: we should do this also when the editor has TYPE_NULL 2001 if (Constants.CODE_ENTER == codePoint && settingsValues.isBeforeJellyBean()) { 2002 // Backward compatibility mode. Before Jelly bean, the keyboard would simulate 2003 // a hardware keyboard event on pressing enter or delete. This is bad for many 2004 // reasons (there are race conditions with commits) but some applications are 2005 // relying on this behavior so we continue to support it for older apps. 2006 sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); 2007 } else { 2008 mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); 2009 } 2010 } 2011 2012 /** 2013 * Insert an automatic space, if the options allow it. 2014 * 2015 * This checks the options and the text before the cursor are appropriate before inserting 2016 * an automatic space. 2017 * 2018 * @param settingsValues the current values of the settings. 2019 */ insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues)2020 private void insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues) { 2021 if (settingsValues.shouldInsertSpacesAutomatically() 2022 && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces 2023 && !mConnection.textBeforeCursorLooksLikeURL()) { 2024 sendKeyCodePoint(settingsValues, Constants.CODE_SPACE); 2025 } 2026 } 2027 2028 /** 2029 * Do the final processing after a batch input has ended. This commits the word to the editor. 2030 * @param settingsValues the current values of the settings. 2031 * @param suggestedWords suggestedWords to use. 2032 */ onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher)2033 public void onUpdateTailBatchInputCompleted(final SettingsValues settingsValues, 2034 final SuggestedWords suggestedWords, final KeyboardSwitcher keyboardSwitcher) { 2035 final String batchInputText = suggestedWords.isEmpty() ? null : suggestedWords.getWord(0); 2036 if (TextUtils.isEmpty(batchInputText)) { 2037 return; 2038 } 2039 mConnection.beginBatchEdit(); 2040 if (SpaceState.PHANTOM == mSpaceState) { 2041 insertAutomaticSpaceIfOptionsAndTextAllow(settingsValues); 2042 } 2043 mWordComposer.setBatchInputWord(batchInputText); 2044 setComposingTextInternal(batchInputText, 1); 2045 mConnection.endBatchEdit(); 2046 // Space state must be updated before calling updateShiftState 2047 mSpaceState = SpaceState.PHANTOM; 2048 keyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(settingsValues), 2049 getCurrentRecapitalizeState()); 2050 } 2051 2052 /** 2053 * Commit the typed string to the editor. 2054 * 2055 * This is typically called when we should commit the currently composing word without applying 2056 * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard 2057 * is configured to not do auto-correction at all (because of the settings or the properties of 2058 * the editor). In this case, `separatorString' is set to the separator that was pressed. 2059 * We also come here in a variety of cases with external user action. For example, when the 2060 * cursor is moved while there is a composition, or when the keyboard is closed, or when the 2061 * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected. 2062 * In this case, `separatorString' is set to NOT_A_SEPARATOR. 2063 * 2064 * @param settingsValues the current values of the settings. 2065 * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. 2066 */ commitTyped(final SettingsValues settingsValues, final String separatorString)2067 public void commitTyped(final SettingsValues settingsValues, final String separatorString) { 2068 if (!mWordComposer.isComposingWord()) return; 2069 final String typedWord = mWordComposer.getTypedWord(); 2070 if (typedWord.length() > 0) { 2071 final boolean isBatchMode = mWordComposer.isBatchMode(); 2072 commitChosenWord(settingsValues, typedWord, 2073 LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD, separatorString); 2074 StatsUtils.onWordCommitUserTyped(typedWord, isBatchMode); 2075 } 2076 } 2077 2078 /** 2079 * Commit the current auto-correction. 2080 * 2081 * This will commit the best guess of the keyboard regarding what the user meant by typing 2082 * the currently composing word. The IME computes suggestions and assigns a confidence score 2083 * to each of them; when it's confident enough in one suggestion, it replaces the typed string 2084 * by this suggestion at commit time. When it's not confident enough, or when it has no 2085 * suggestions, or when the settings or environment does not allow for auto-correction, then 2086 * this method just commits the typed string. 2087 * Note that if suggestions are currently being computed in the background, this method will 2088 * block until the computation returns. This is necessary for consistency (it would be very 2089 * strange if pressing space would commit a different word depending on how fast you press). 2090 * 2091 * @param settingsValues the current value of the settings. 2092 * @param separator the separator that's causing the commit to happen. 2093 */ commitCurrentAutoCorrection(final SettingsValues settingsValues, final String separator, final LatinIME.UIHandler handler)2094 private void commitCurrentAutoCorrection(final SettingsValues settingsValues, 2095 final String separator, final LatinIME.UIHandler handler) { 2096 // Complete any pending suggestions query first 2097 if (handler.hasPendingUpdateSuggestions()) { 2098 handler.cancelUpdateSuggestionStrip(); 2099 // To know the input style here, we should retrieve the in-flight "update suggestions" 2100 // message and read its arg1 member here. However, the Handler class does not let 2101 // us retrieve this message, so we can't do that. But in fact, we notice that 2102 // we only ever come here when the input style was typing. In the case of batch 2103 // input, we update the suggestions synchronously when the tail batch comes. Likewise 2104 // for application-specified completions. As for recorrections, we never auto-correct, 2105 // so we don't come here either. Hence, the input style is necessarily 2106 // INPUT_STYLE_TYPING. 2107 performUpdateSuggestionStripSync(settingsValues, SuggestedWords.INPUT_STYLE_TYPING); 2108 } 2109 final SuggestedWordInfo autoCorrectionOrNull = mWordComposer.getAutoCorrectionOrNull(); 2110 final String typedWord = mWordComposer.getTypedWord(); 2111 final String stringToCommit = (autoCorrectionOrNull != null) 2112 ? autoCorrectionOrNull.mWord : typedWord; 2113 if (stringToCommit != null) { 2114 if (TextUtils.isEmpty(typedWord)) { 2115 throw new RuntimeException("We have an auto-correction but the typed word " 2116 + "is empty? Impossible! I must commit suicide."); 2117 } 2118 final boolean isBatchMode = mWordComposer.isBatchMode(); 2119 commitChosenWord(settingsValues, stringToCommit, 2120 LastComposedWord.COMMIT_TYPE_DECIDED_WORD, separator); 2121 if (!typedWord.equals(stringToCommit)) { 2122 // This will make the correction flash for a short while as a visual clue 2123 // to the user that auto-correction happened. It has no other effect; in particular 2124 // note that this won't affect the text inside the text field AT ALL: it only makes 2125 // the segment of text starting at the supplied index and running for the length 2126 // of the auto-correction flash. At this moment, the "typedWord" argument is 2127 // ignored by TextView. 2128 mConnection.commitCorrection(new CorrectionInfo( 2129 mConnection.getExpectedSelectionEnd() - stringToCommit.length(), 2130 typedWord, stringToCommit)); 2131 String prevWordsContext = (autoCorrectionOrNull != null) 2132 ? autoCorrectionOrNull.mPrevWordsContext 2133 : ""; 2134 StatsUtils.onAutoCorrection(typedWord, stringToCommit, isBatchMode, 2135 mDictionaryFacilitator, prevWordsContext); 2136 StatsUtils.onWordCommitAutoCorrect(stringToCommit, isBatchMode); 2137 } else { 2138 StatsUtils.onWordCommitUserTyped(stringToCommit, isBatchMode); 2139 } 2140 } 2141 } 2142 2143 /** 2144 * Commits the chosen word to the text field and saves it for later retrieval. 2145 * 2146 * @param settingsValues the current values of the settings. 2147 * @param chosenWord the word we want to commit. 2148 * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_* 2149 * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none. 2150 */ commitChosenWord(final SettingsValues settingsValues, final String chosenWord, final int commitType, final String separatorString)2151 private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord, 2152 final int commitType, final String separatorString) { 2153 long startTimeMillis = 0; 2154 if (DebugFlags.DEBUG_ENABLED) { 2155 startTimeMillis = System.currentTimeMillis(); 2156 Log.d(TAG, "commitChosenWord() : [" + chosenWord + "]"); 2157 } 2158 final SuggestedWords suggestedWords = mSuggestedWords; 2159 // TODO: Locale should be determined based on context and the text given. 2160 final Locale locale = getDictionaryFacilitatorLocale(); 2161 final CharSequence chosenWordWithSuggestions = chosenWord; 2162 // b/21926256 2163 // SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord, 2164 // suggestedWords, locale); 2165 if (DebugFlags.DEBUG_ENABLED) { 2166 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 2167 Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 2168 + "SuggestionSpanUtils.getTextWithSuggestionSpan()"); 2169 startTimeMillis = System.currentTimeMillis(); 2170 } 2171 // When we are composing word, get n-gram context from the 2nd previous word because the 2172 // 1st previous word is the word to be committed. Otherwise get n-gram context from the 1st 2173 // previous word. 2174 final NgramContext ngramContext = mConnection.getNgramContextFromNthPreviousWord( 2175 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1); 2176 if (DebugFlags.DEBUG_ENABLED) { 2177 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 2178 Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 2179 + "Connection.getNgramContextFromNthPreviousWord()"); 2180 Log.d(TAG, "commitChosenWord() : NgramContext = " + ngramContext); 2181 startTimeMillis = System.currentTimeMillis(); 2182 } 2183 mConnection.commitText(chosenWordWithSuggestions, 1); 2184 if (DebugFlags.DEBUG_ENABLED) { 2185 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 2186 Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 2187 + "Connection.commitText"); 2188 startTimeMillis = System.currentTimeMillis(); 2189 } 2190 // Add the word to the user history dictionary 2191 performAdditionToUserHistoryDictionary(settingsValues, chosenWord, ngramContext); 2192 if (DebugFlags.DEBUG_ENABLED) { 2193 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 2194 Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 2195 + "performAdditionToUserHistoryDictionary()"); 2196 startTimeMillis = System.currentTimeMillis(); 2197 } 2198 // TODO: figure out here if this is an auto-correct or if the best word is actually 2199 // what user typed. Note: currently this is done much later in 2200 // LastComposedWord#didCommitTypedWord by string equality of the remembered 2201 // strings. 2202 mLastComposedWord = mWordComposer.commitWord(commitType, 2203 chosenWordWithSuggestions, separatorString, ngramContext); 2204 if (DebugFlags.DEBUG_ENABLED) { 2205 long runTimeMillis = System.currentTimeMillis() - startTimeMillis; 2206 Log.d(TAG, "commitChosenWord() : " + runTimeMillis + " ms to run " 2207 + "WordComposer.commitWord()"); 2208 startTimeMillis = System.currentTimeMillis(); 2209 } 2210 } 2211 2212 /** 2213 * Retry resetting caches in the rich input connection. 2214 * 2215 * When the editor can't be accessed we can't reset the caches, so we schedule a retry. 2216 * This method handles the retry, and re-schedules a new retry if we still can't access. 2217 * We only retry up to 5 times before giving up. 2218 * 2219 * @param tryResumeSuggestions Whether we should resume suggestions or not. 2220 * @param remainingTries How many times we may try again before giving up. 2221 * @return whether true if the caches were successfully reset, false otherwise. 2222 */ retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, final int remainingTries, final LatinIME.UIHandler handler)2223 public boolean retryResetCachesAndReturnSuccess(final boolean tryResumeSuggestions, 2224 final int remainingTries, final LatinIME.UIHandler handler) { 2225 final boolean shouldFinishComposition = mConnection.hasSelection() 2226 || !mConnection.isCursorPositionKnown(); 2227 if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess( 2228 mConnection.getExpectedSelectionStart(), mConnection.getExpectedSelectionEnd(), 2229 shouldFinishComposition)) { 2230 if (0 < remainingTries) { 2231 handler.postResetCaches(tryResumeSuggestions, remainingTries - 1); 2232 return false; 2233 } 2234 // If remainingTries is 0, we should stop waiting for new tries, however we'll still 2235 // return true as we need to perform other tasks (for example, loading the keyboard). 2236 } 2237 mConnection.tryFixLyingCursorPosition(); 2238 if (tryResumeSuggestions) { 2239 handler.postResumeSuggestions(true /* shouldDelay */); 2240 } 2241 return true; 2242 } 2243 getSuggestedWords(final SettingsValues settingsValues, final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle, final int sequenceNumber, final OnGetSuggestedWordsCallback callback)2244 public void getSuggestedWords(final SettingsValues settingsValues, 2245 final Keyboard keyboard, final int keyboardShiftMode, final int inputStyle, 2246 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) { 2247 mWordComposer.adviseCapitalizedModeBeforeFetchingSuggestions( 2248 getActualCapsMode(settingsValues, keyboardShiftMode)); 2249 mSuggest.getSuggestedWords(mWordComposer, 2250 getNgramContextFromNthPreviousWordForSuggestion( 2251 settingsValues.mSpacingAndPunctuations, 2252 // Get the word on which we should search the bigrams. If we are composing 2253 // a word, it's whatever is *before* the half-committed word in the buffer, 2254 // hence 2; if we aren't, we should just skip whitespace if any, so 1. 2255 mWordComposer.isComposingWord() ? 2 : 1), 2256 keyboard, 2257 new SettingsValuesForSuggestion(settingsValues.mBlockPotentiallyOffensive), 2258 settingsValues.mAutoCorrectionEnabledPerUserSettings, 2259 inputStyle, sequenceNumber, callback); 2260 } 2261 2262 /** 2263 * Used as an injection point for each call of 2264 * {@link RichInputConnection#setComposingText(CharSequence, int)}. 2265 * 2266 * <p>Currently using this method is optional and you can still directly call 2267 * {@link RichInputConnection#setComposingText(CharSequence, int)}, but it is recommended to 2268 * use this method whenever possible.<p> 2269 * <p>TODO: Should we move this mechanism to {@link RichInputConnection}?</p> 2270 * 2271 * @param newComposingText the composing text to be set 2272 * @param newCursorPosition the new cursor position 2273 */ setComposingTextInternal(final CharSequence newComposingText, final int newCursorPosition)2274 private void setComposingTextInternal(final CharSequence newComposingText, 2275 final int newCursorPosition) { 2276 setComposingTextInternalWithBackgroundColor(newComposingText, newCursorPosition, 2277 Color.TRANSPARENT, newComposingText.length()); 2278 } 2279 2280 /** 2281 * Equivalent to {@link #setComposingTextInternal(CharSequence, int)} except that this method 2282 * allows to set {@link BackgroundColorSpan} to the composing text with the given color. 2283 * 2284 * <p>TODO: Currently the background color is exclusive with the black underline, which is 2285 * automatically added by the framework. We need to change the framework if we need to have both 2286 * of them at the same time.</p> 2287 * <p>TODO: Should we move this method to {@link RichInputConnection}?</p> 2288 * 2289 * @param newComposingText the composing text to be set 2290 * @param newCursorPosition the new cursor position 2291 * @param backgroundColor the background color to be set to the composing text. Set 2292 * {@link Color#TRANSPARENT} to disable the background color. 2293 * @param coloredTextLength the length of text, in Java chars, which should be rendered with 2294 * the given background color. 2295 */ setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, final int newCursorPosition, final int backgroundColor, final int coloredTextLength)2296 private void setComposingTextInternalWithBackgroundColor(final CharSequence newComposingText, 2297 final int newCursorPosition, final int backgroundColor, final int coloredTextLength) { 2298 final CharSequence composingTextToBeSet; 2299 if (backgroundColor == Color.TRANSPARENT) { 2300 composingTextToBeSet = newComposingText; 2301 } else { 2302 final SpannableString spannable = new SpannableString(newComposingText); 2303 final BackgroundColorSpan backgroundColorSpan = 2304 new BackgroundColorSpan(backgroundColor); 2305 final int spanLength = Math.min(coloredTextLength, spannable.length()); 2306 spannable.setSpan(backgroundColorSpan, 0, spanLength, 2307 Spanned.SPAN_EXCLUSIVE_EXCLUSIVE | Spanned.SPAN_COMPOSING); 2308 composingTextToBeSet = spannable; 2309 } 2310 mConnection.setComposingText(composingTextToBeSet, newCursorPosition); 2311 } 2312 2313 /** 2314 * Gets an object allowing private IME commands to be sent to the 2315 * underlying editor. 2316 * @return An object for sending private commands to the underlying editor. 2317 */ getPrivateCommandPerformer()2318 public PrivateCommandPerformer getPrivateCommandPerformer() { 2319 return mConnection; 2320 } 2321 2322 /** 2323 * Gets the expected index of the first char of the composing span within the editor's text. 2324 * Returns a negative value in case there appears to be no valid composing span. 2325 * 2326 * @see #getComposingLength() 2327 * @see RichInputConnection#hasSelection() 2328 * @see RichInputConnection#isCursorPositionKnown() 2329 * @see RichInputConnection#getExpectedSelectionStart() 2330 * @see RichInputConnection#getExpectedSelectionEnd() 2331 * @return The expected index in Java chars of the first char of the composing span. 2332 */ 2333 // TODO: try and see if we can get rid of this method. Ideally the users of this class should 2334 // never need to know this. getComposingStart()2335 public int getComposingStart() { 2336 if (!mConnection.isCursorPositionKnown() || mConnection.hasSelection()) { 2337 return -1; 2338 } 2339 return mConnection.getExpectedSelectionStart() - mWordComposer.size(); 2340 } 2341 2342 /** 2343 * Gets the expected length in Java chars of the composing span. 2344 * May be 0 if there is no valid composing span. 2345 * @see #getComposingStart() 2346 * @return The expected length of the composing span. 2347 */ 2348 // TODO: try and see if we can get rid of this method. Ideally the users of this class should 2349 // never need to know this. getComposingLength()2350 public int getComposingLength() { 2351 return mWordComposer.size(); 2352 } 2353 } 2354