Home | History | Annotate | Download | only in inputlogic
      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      */
    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      */
    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      */
    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      */
    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      */
    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.
    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      */
    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
    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      */
    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      */
    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 
    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;
    561     public void onUpdateBatchInput(final InputPointers batchPointers) {
    562         mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
    563     }
    564 
    565     public void onEndBatchInput(final InputPointers batchPointers) {
    566         mInputLogicHandler.updateTailBatchInput(batchPointers, mAutoCommitSequenceNumber);
    567         ++mAutoCommitSequenceNumber;
    568     }
    569 
    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?
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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      */
    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 
   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 
   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 
   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      */
   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      */
   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      */
   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 
   1296     public void startDoubleSpacePeriodCountdown(final InputTransaction inputTransaction) {
   1297         mDoubleSpacePeriodCountdownStart = inputTransaction.mTimestamp;
   1298     }
   1299 
   1300     public void cancelDoubleSpacePeriodCountdown() {
   1301         mDoubleSpacePeriodCountdownStart = 0;
   1302     }
   1303 
   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      */
   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      */
   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      */
   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 
   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 
   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      */
   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 
   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      */
   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      */
   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      */
   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 
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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 ?!
   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      */
   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      */
   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
   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?
   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      */
   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
   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      */
   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      */
   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      */
   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      */
   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      */
   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      */
   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 
   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      */
   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      */
   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      */
   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.
   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.
   2350     public int getComposingLength() {
   2351         return mWordComposer.size();
   2352     }
   2353 }
   2354