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