Merge "Import translations. DO NOT MERGE"
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index c260434..202ac87 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -141,7 +141,7 @@
     }
 
     public static final int NOT_A_CODE = -1;
-
+    public static final int NOT_A_CURSOR_POSITION = -1;
     public static final int NOT_A_COORDINATE = -1;
     public static final int SUGGESTION_STRIP_COORDINATE = -2;
     public static final int SPELL_CHECKER_COORDINATE = -3;
@@ -154,6 +154,11 @@
     // Must be equal to MAX_WORD_LENGTH in native/jni/src/defines.h
     public static final int DICTIONARY_MAX_WORD_LENGTH = 48;
 
+    // Key events coming any faster than this are long-presses.
+    public static final int LONG_PRESS_MILLISECONDS = 200;
+    // How many continuous deletes at which to start deleting at a higher speed.
+    public static final int DELETE_ACCELERATE_AT = 20;
+
     public static boolean isValidCoordinate(final int coordinate) {
         // Detect {@link NOT_A_COORDINATE}, {@link SUGGESTION_STRIP_COORDINATE},
         // and {@link SPELL_CHECKER_COORDINATE}.
@@ -242,5 +247,4 @@
     private Constants() {
         // This utility class is not publicly instantiable.
     }
-
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 67b5702..ab9b2e5 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -76,6 +76,8 @@
 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.inputlogic.InputLogic;
+import com.android.inputmethod.latin.inputlogic.SpaceState;
 import com.android.inputmethod.latin.personalization.DictionaryDecayBroadcastReciever;
 import com.android.inputmethod.latin.personalization.PersonalizationDictionarySessionRegister;
 import com.android.inputmethod.latin.personalization.UserHistoryDictionary;
@@ -115,15 +117,11 @@
         Suggest.SuggestInitializationListener {
     private static final String TAG = LatinIME.class.getSimpleName();
     private static final boolean TRACE = false;
-    private static boolean DEBUG;
+    // TODO[IL]: Make this private
+    public static boolean DEBUG;
 
     private static final int EXTENDED_TOUCHABLE_REGION_HEIGHT = 100;
 
-    // How many continuous deletes at which to start deleting at a higher speed.
-    private static final int DELETE_ACCELERATE_AT = 20;
-    // Key events coming any faster than this are long-presses.
-    private static final int QUICK_PRESS = 200;
-
     private static final int PENDING_IMS_CALLBACK_DURATION = 800;
 
     private static final int PERIOD_FOR_AUDIO_AND_HAPTIC_FEEDBACK_IN_KEY_REPEAT = 2;
@@ -137,79 +135,38 @@
      */
     private static final String SCHEME_PACKAGE = "package";
 
-    private static final int SPACE_STATE_NONE = 0;
-    // Double space: the state where the user pressed space twice quickly, which LatinIME
-    // resolved as period-space. Undoing this converts the period to a space.
-    private static final int SPACE_STATE_DOUBLE = 1;
-    // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
-    // have just been swapped. Undoing this swaps them back; the space is still considered weak.
-    private static final int SPACE_STATE_SWAP_PUNCTUATION = 2;
-    // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
-    // spaces happen when the user presses space, accepting the current suggestion (whether
-    // it's an auto-correction or not).
-    private static final int SPACE_STATE_WEAK = 3;
-    // Phantom space: a not-yet-inserted space that should get inserted on the next input,
-    // character provided it's not a separator. If it's a separator, the phantom space is dropped.
-    // Phantom spaces happen when a user chooses a word from the suggestion strip.
-    private static final int SPACE_STATE_PHANTOM = 4;
-
-    // Current space state of the input method. This can be any of the above constants.
-    private int mSpaceState;
-
     private final Settings mSettings;
+    private final InputLogic mInputLogic = new InputLogic(this);
 
     private View mExtractArea;
     private View mKeyPreviewBackingView;
     private SuggestionStripView mSuggestionStripView;
-    // Never null
-    private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
-    private Suggest mSuggest;
+
     private CompletionInfo[] mApplicationSpecifiedCompletions;
-    private AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
+    // TODO[IL]: Make this an AsyncResultHolder or a Future in SettingsValues
+    public AppWorkaroundsUtils mAppWorkAroundsUtils = new AppWorkaroundsUtils();
 
     private RichInputMethodManager mRichImm;
     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
     private final SubtypeSwitcher mSubtypeSwitcher;
     private final SubtypeState mSubtypeState = new SubtypeState();
-    // At start, create a default event interpreter that does nothing by passing it no decoder spec.
-    // The event interpreter should never be null.
-    private EventInterpreter mEventInterpreter = new EventInterpreter(this);
 
     private boolean mIsMainDictionaryAvailable;
     private UserBinaryDictionary mUserDictionary;
     private boolean mIsUserDictionaryAvailable;
 
-    private LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-    private final WordComposer mWordComposer = new WordComposer();
-    private final RichInputConnection mConnection = new RichInputConnection(this);
-    private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
-
-    // Keep track of the last selection range to decide if we need to show word alternatives
-    private static final int NOT_A_CURSOR_POSITION = -1;
-    private int mLastSelectionStart = NOT_A_CURSOR_POSITION;
-    private int mLastSelectionEnd = NOT_A_CURSOR_POSITION;
-
-    private int mDeleteCount;
-    private long mLastKeyTime;
-    private final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
     // Personalization debugging params
     private boolean mUseOnlyPersonalizationDictionaryForDebug = false;
     private boolean mBoostPersonalizationDictionaryForDebug = false;
 
-    // Member variables for remembering the current device orientation.
-    private int mDisplayOrientation;
+    // Member variable for remembering the current device orientation.
+    // TODO[IL]: Move this to SettingsValues.
+    public int mDisplayOrientation;
 
     // Object for reacting to adding/removing a dictionary pack.
     private BroadcastReceiver mDictionaryPackInstallReceiver =
             new DictionaryPackInstallBroadcastReceiver(this);
 
-    // Keeps track of most recently inserted text (multi-character key) for reverting
-    private String mEnteredText;
-
-    // TODO: This boolean is persistent state and causes large side effects at unexpected times.
-    // Find a way to remove it for readability.
-    private boolean mIsAutoCorrectionIndicatorOn;
-
     private AlertDialog mOptionsDialog;
 
     private final boolean mIsHardwareAcceleratedDrawingEnabled;
@@ -547,7 +504,7 @@
         initSuggest();
 
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mSuggest);
+            ResearchLogger.getInstance().init(this, mKeyboardSwitcher, mInputLogic.mSuggest);
         }
         mDisplayOrientation = getResources().getConfiguration().orientation;
 
@@ -588,9 +545,10 @@
         // the layout; at this time, we need to skip resetting the contacts dictionary. It will
         // be done later inside {@see #initSuggest()} when the reopenDictionaries message is
         // processed.
-        if (!mHandler.hasPendingReopenDictionaries() && mSuggest != null) {
+        if (!mHandler.hasPendingReopenDictionaries() && mInputLogic.mSuggest != null) {
             // May need to reset dictionaries depending on the user settings.
-            mSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent());
+            mInputLogic.mSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
+                    mSettings.getCurrent());
         }
     }
 
@@ -636,24 +594,26 @@
         mUserDictionary = new UserBinaryDictionary(this, subtypeLocale);
         mIsUserDictionaryAvailable = mUserDictionary.isEnabled();
         newSuggest.setUserDictionary(mUserDictionary);
-        newSuggest.setAdditionalDictionaries(mSuggest /* oldSuggest */, mSettings.getCurrent());
-        final Suggest oldSuggest = mSuggest;
-        mSuggest = newSuggest;
+        newSuggest.setAdditionalDictionaries(mInputLogic.mSuggest /* oldSuggest */,
+                mSettings.getCurrent());
+        final Suggest oldSuggest = mInputLogic.mSuggest;
+        mInputLogic.mSuggest = newSuggest;
         if (oldSuggest != null) oldSuggest.close();
     }
 
     /* package private */ void resetSuggestMainDict() {
         final Locale subtypeLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        mSuggest.resetMainDict(this, subtypeLocale, this /* SuggestInitializationListener */);
+        mInputLogic.mSuggest.resetMainDict(this, subtypeLocale,
+                this /* SuggestInitializationListener */);
         mIsMainDictionaryAvailable = DictionaryFactory.isDictionaryAvailable(this, subtypeLocale);
     }
 
     @Override
     public void onDestroy() {
-        final Suggest suggest = mSuggest;
+        final Suggest suggest = mInputLogic.mSuggest;
         if (suggest != null) {
             suggest.close();
-            mSuggest = null;
+            mInputLogic.mSuggest = null;
         }
         mSettings.onDestroy();
         unregisterReceiver(mReceiver);
@@ -676,10 +636,10 @@
         if (mDisplayOrientation != conf.orientation) {
             mDisplayOrientation = conf.orientation;
             mHandler.startOrientationChanging();
-            mConnection.beginBatchEdit();
-            commitTyped(LastComposedWord.NOT_A_SEPARATOR);
-            mConnection.finishComposingText();
-            mConnection.endBatchEdit();
+            mInputLogic.mConnection.beginBatchEdit();
+            mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+            mInputLogic.mConnection.finishComposingText();
+            mInputLogic.mConnection.endBatchEdit();
             if (isShowingOptionDialog()) {
                 mOptionsDialog.dismiss();
             }
@@ -823,16 +783,16 @@
 
         // The app calling setText() has the effect of clearing the composing
         // span, so we should reset our state unconditionally, even if restarting is true.
-        mEnteredText = null;
-        resetComposingState(true /* alsoResetLastComposedWord */);
-        mDeleteCount = 0;
-        mSpaceState = SPACE_STATE_NONE;
-        mRecapitalizeStatus.deactivate();
-        mCurrentlyPressedHardwareKeys.clear();
+        mInputLogic.mEnteredText = null;
+        mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
+        mInputLogic.mDeleteCount = 0;
+        mInputLogic.mSpaceState = SpaceState.NONE;
+        mInputLogic.mRecapitalizeStatus.deactivate();
+        mInputLogic.mCurrentlyPressedHardwareKeys.clear();
 
         // Note: the following does a round-trip IPC on the main thread: be careful
         final Locale currentLocale = mSubtypeSwitcher.getCurrentSubtypeLocale();
-        final Suggest suggest = mSuggest;
+        final Suggest suggest = mInputLogic.mSuggest;
         if (null != suggest && null != currentLocale && !currentLocale.equals(suggest.mLocale)) {
             initSuggest();
         }
@@ -841,14 +801,15 @@
             // otherwise it will clear the suggestion strip.
             setPunctuationSuggestions();
         }
-        mSuggestedWords = SuggestedWords.EMPTY;
+        mInputLogic.mSuggestedWords = SuggestedWords.EMPTY;
 
         // Sometimes, while rotating, for some reason the framework tells the app we are not
         // connected to it and that means we can't refresh the cache. In this case, schedule a
         // refresh later.
         final boolean canReachInputConnection;
-        if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(editorInfo.initialSelStart,
-                editorInfo.initialSelEnd, false /* shouldFinishComposition */)) {
+        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+                editorInfo.initialSelStart, editorInfo.initialSelEnd,
+                false /* shouldFinishComposition */)) {
             // We try resetting the caches up to 5 times before giving up.
             mHandler.postResetCaches(isDifferentTextField, 5 /* remainingTries */);
             // mLastSelection{Start,End} are reset later in this method, don't need to do it here
@@ -888,8 +849,8 @@
         setSuggestionStripShownInternal(
                 isSuggestionsStripVisible(), /* needsInputViewShown */ false);
 
-        mLastSelectionStart = editorInfo.initialSelStart;
-        mLastSelectionEnd = editorInfo.initialSelEnd;
+        mInputLogic.mLastSelectionStart = editorInfo.initialSelStart;
+        mInputLogic.mLastSelectionEnd = editorInfo.initialSelEnd;
         // In some cases (namely, after rotation of the device) editorInfo.initialSelStart is lying
         // so we try using some heuristics to find out about these and fix them.
         tryFixLyingCursorPosition();
@@ -922,26 +883,29 @@
      * than it really is.
      */
     private void tryFixLyingCursorPosition() {
-        final CharSequence textBeforeCursor =
-                mConnection.getTextBeforeCursor(Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
+        final CharSequence textBeforeCursor = mInputLogic.mConnection.getTextBeforeCursor(
+                Constants.EDITOR_CONTENTS_CACHE_SIZE, 0);
         if (null == textBeforeCursor) {
-            mLastSelectionStart = mLastSelectionEnd = NOT_A_CURSOR_POSITION;
+            mInputLogic.mLastSelectionStart = mInputLogic.mLastSelectionEnd =
+                    Constants.NOT_A_CURSOR_POSITION;
         } else {
             final int textLength = textBeforeCursor.length();
-            if (textLength > mLastSelectionStart
+            if (textLength > mInputLogic.mLastSelectionStart
                     || (textLength < Constants.EDITOR_CONTENTS_CACHE_SIZE
-                            && mLastSelectionStart < Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
+                            && mInputLogic.mLastSelectionStart <
+                                    Constants.EDITOR_CONTENTS_CACHE_SIZE)) {
                 // It should not be possible to have only one of those variables be
                 // NOT_A_CURSOR_POSITION, so if they are equal, either the selection is zero-sized
                 // (simple cursor, no selection) or there is no cursor/we don't know its pos
-                final boolean wasEqual = mLastSelectionStart == mLastSelectionEnd;
-                mLastSelectionStart = textLength;
+                final boolean wasEqual =
+                        mInputLogic.mLastSelectionStart == mInputLogic.mLastSelectionEnd;
+                mInputLogic.mLastSelectionStart = textLength;
                 // We can't figure out the value of mLastSelectionEnd :(
                 // But at least if it's smaller than mLastSelectionStart something is wrong,
                 // and if they used to be equal we also don't want to make it look like there is a
                 // selection.
-                if (wasEqual || mLastSelectionStart > mLastSelectionEnd) {
-                    mLastSelectionEnd = mLastSelectionStart;
+                if (wasEqual || mInputLogic.mLastSelectionStart > mInputLogic.mLastSelectionEnd) {
+                    mInputLogic.mLastSelectionEnd = mInputLogic.mLastSelectionStart;
                 }
             }
         }
@@ -998,12 +962,15 @@
         // Remove pending messages related to update suggestions
         mHandler.cancelUpdateSuggestionStrip();
         // Should do the following in onFinishInputInternal but until JB MR2 it's not called :(
-        if (mWordComposer.isComposingWord()) mConnection.finishComposingText();
-        resetComposingState(true /* alsoResetLastComposedWord */);
+        if (mInputLogic.mWordComposer.isComposingWord()) {
+            mInputLogic.mConnection.finishComposingText();
+        }
+        mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
         // Notify ResearchLogger
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput, mLastSelectionStart,
-                    mLastSelectionEnd, getCurrentInputConnection());
+            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput,
+                    mInputLogic.mLastSelectionStart,
+                    mInputLogic.mLastSelectionEnd, getCurrentInputConnection());
         }
     }
 
@@ -1016,21 +983,22 @@
         if (DEBUG) {
             Log.i(TAG, "onUpdateSelection: oss=" + oldSelStart
                     + ", ose=" + oldSelEnd
-                    + ", lss=" + mLastSelectionStart
-                    + ", lse=" + mLastSelectionEnd
+                    + ", lss=" + mInputLogic.mLastSelectionStart
+                    + ", lse=" + mInputLogic.mLastSelectionEnd
                     + ", nss=" + newSelStart
                     + ", nse=" + newSelEnd
                     + ", cs=" + composingSpanStart
                     + ", ce=" + composingSpanEnd);
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_onUpdateSelection(mLastSelectionStart, mLastSelectionEnd,
+            ResearchLogger.latinIME_onUpdateSelection(mInputLogic.mLastSelectionStart,
+                    mInputLogic.mLastSelectionEnd,
                     oldSelStart, oldSelEnd, newSelStart, newSelEnd, composingSpanStart,
-                    composingSpanEnd, mConnection);
+                    composingSpanEnd, mInputLogic.mConnection);
         }
 
-        final boolean selectionChanged = mLastSelectionStart != newSelStart
-                || mLastSelectionEnd != newSelEnd;
+        final boolean selectionChanged = mInputLogic.mLastSelectionStart != newSelStart
+                || mInputLogic.mLastSelectionEnd != newSelEnd;
 
         // if composingSpanStart and composingSpanEnd are -1, it means there is no composing
         // span in the view - we can use that to narrow down whether the cursor was moved
@@ -1045,24 +1013,25 @@
         // TODO: revisit this when LatinIME supports hardware keyboards.
         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
         // TODO: find a better way to simulate actual execution.
-        if (isInputViewShown() && !mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart,
-                    oldSelEnd, newSelEnd)) {
+        if (isInputViewShown() && !mInputLogic.mConnection.isBelatedExpectedUpdate(oldSelStart,
+                newSelStart, oldSelEnd, newSelEnd)) {
             // TODO: the following is probably better done in resetEntireInputState().
             // it should only happen when the cursor moved, and the very purpose of the
             // test below is to narrow down whether this happened or not. Likewise with
             // the call to updateShiftState.
             // We set this to NONE because after a cursor move, we don't want the space
             // state-related special processing to kick in.
-            mSpaceState = SPACE_STATE_NONE;
+            mInputLogic.mSpaceState = SpaceState.NONE;
 
             // TODO: is it still necessary to test for composingSpan related stuff?
             final boolean selectionChangedOrSafeToReset = selectionChanged
-                    || (!mWordComposer.isComposingWord()) || noComposingSpan;
+                    || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan;
             final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
                     || newSelStart != newSelEnd);
             final int moveAmount = newSelStart - oldSelStart;
             if (selectionChangedOrSafeToReset && (hasOrHadSelection
-                    || !mWordComposer.moveCursorByAndReturnIfInsideComposingWord(moveAmount))) {
+                    || !mInputLogic.mWordComposer.moveCursorByAndReturnIfInsideComposingWord(
+                            moveAmount))) {
                 // If we are composing a word and moving the cursor, we would want to set a
                 // suggestion span for recorrection to work correctly. Unfortunately, that
                 // would involve the keyboard committing some new text, which would move the
@@ -1072,14 +1041,14 @@
                 // Another option would be to send suggestions each time we set the composing
                 // text, but that is probably too expensive to do, so we decided to leave things
                 // as is.
-                resetEntireInputState(newSelStart, newSelEnd);
+                mInputLogic.resetEntireInputState(mSettings.getCurrent(), newSelStart, newSelEnd);
             } else {
                 // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
                 // composition to end.  But in all cases where we don't reset the entire input
                 // state, we still want to tell the rich input connection about the new cursor
                 // position so that it can update its caches.
-                mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
-                        false /* shouldFinishComposition */);
+                mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+                        newSelStart, newSelEnd, false /* shouldFinishComposition */);
             }
 
             // We moved the cursor. If we are touching a word, we need to resume suggestion,
@@ -1088,13 +1057,13 @@
                 mHandler.postResumeSuggestions();
             }
             // Reset the last recapitalization.
-            mRecapitalizeStatus.deactivate();
+            mInputLogic.mRecapitalizeStatus.deactivate();
             mKeyboardSwitcher.updateShiftState();
         }
 
         // Make a note of the cursor position
-        mLastSelectionStart = newSelStart;
-        mLastSelectionEnd = newSelEnd;
+        mInputLogic.mLastSelectionStart = newSelStart;
+        mInputLogic.mLastSelectionEnd = newSelEnd;
         mSubtypeState.currentSubtypeUsed();
     }
 
@@ -1299,42 +1268,9 @@
         mKeyPreviewBackingView.setVisibility(isFullscreenMode() ? View.GONE : View.VISIBLE);
     }
 
-    // This will reset the whole input state to the starting state. It will clear
-    // the composing word, reset the last composed word, tell the inputconnection about it.
-    private void resetEntireInputState(final int newSelStart, final int newSelEnd) {
-        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
-        resetComposingState(true /* alsoResetLastComposedWord */);
-        final SettingsValues settingsValues = mSettings.getCurrent();
-        if (settingsValues.mBigramPredictionEnabled) {
-            clearSuggestionStrip();
-        } else {
-            setSuggestedWords(settingsValues.mSuggestPuncList, false);
-        }
-        mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
-                shouldFinishComposition);
-    }
-
-    private void resetComposingState(final boolean alsoResetLastComposedWord) {
-        mWordComposer.reset();
-        if (alsoResetLastComposedWord) {
-            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-        }
-    }
-
-    private void commitTyped(final String separatorString) {
-        if (!mWordComposer.isComposingWord()) return;
-        final String typedWord = mWordComposer.getTypedWord();
-        if (typedWord.length() > 0) {
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
-            }
-            commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
-                    separatorString);
-        }
-    }
-
     // Called from the KeyboardSwitcher which needs to know auto caps state to display
     // the right layout.
+    // TODO[IL]: Move this to InputLogic.
     public int getCurrentAutoCapsState() {
         final SettingsValues currentSettingsValues = mSettings.getCurrent();
         if (!currentSettingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
@@ -1344,96 +1280,18 @@
         final int inputType = ei.inputType;
         // Warning: this depends on mSpaceState, which may not be the most current value. If
         // mSpaceState gets updated later, whoever called this may need to be told about it.
-        return mConnection.getCursorCapsMode(inputType, currentSettingsValues,
-                SPACE_STATE_PHANTOM == mSpaceState);
+        return mInputLogic.mConnection.getCursorCapsMode(inputType, currentSettingsValues,
+                SpaceState.PHANTOM == mInputLogic.mSpaceState);
     }
 
     public int getCurrentRecapitalizeState() {
-        if (!mRecapitalizeStatus.isActive()
-                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+        if (!mInputLogic.mRecapitalizeStatus.isActive()
+                || !mInputLogic.mRecapitalizeStatus.isSetAt(mInputLogic.mLastSelectionStart,
+                        mInputLogic.mLastSelectionEnd)) {
             // Not recapitalizing at the moment
             return RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
         }
-        return mRecapitalizeStatus.getCurrentMode();
-    }
-
-    // Factor in auto-caps and manual caps and compute the current caps mode.
-    private int getActualCapsMode() {
-        final int keyboardShiftMode = mKeyboardSwitcher.getKeyboardShiftMode();
-        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
-        final int auto = getCurrentAutoCapsState();
-        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
-            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
-        }
-        if (0 != auto) {
-            return WordComposer.CAPS_MODE_AUTO_SHIFTED;
-        }
-        return WordComposer.CAPS_MODE_OFF;
-    }
-
-    private void swapSwapperAndSpace() {
-        final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
-        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
-        if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
-            mConnection.deleteSurroundingText(2, 0);
-            final String text = lastTwo.charAt(1) + " ";
-            mConnection.commitText(text, 1);
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
-            }
-            mKeyboardSwitcher.updateShiftState();
-        }
-    }
-
-    private boolean maybeDoubleSpacePeriod() {
-        final SettingsValues currentSettingsValues = mSettings.getCurrent();
-        if (!currentSettingsValues.mUseDoubleSpacePeriod) return false;
-        if (!mHandler.isAcceptingDoubleSpacePeriod()) return false;
-        // We only do this when we see two spaces and an accepted code point before the cursor.
-        // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
-        final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
-        if (null == lastThree) return false;
-        final int length = lastThree.length();
-        if (length < 3) return false;
-        if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
-        if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
-        // We know there are spaces in pos -1 and -2, and we have at least three chars.
-        // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
-        // so this is fine.
-        final int firstCodePoint =
-                Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
-                        Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
-        if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
-            mHandler.cancelDoubleSpacePeriodTimer();
-            mConnection.deleteSurroundingText(2, 0);
-            final String textToInsert = new String(
-                    new int[] { currentSettingsValues.mSentenceSeparator, Constants.CODE_SPACE },
-                    0, 2);
-            mConnection.commitText(textToInsert, 1);
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
-                        false /* isBatchMode */);
-            }
-            mWordComposer.discardPreviousWordForSuggestion();
-            mKeyboardSwitcher.updateShiftState();
-            return true;
-        }
-        return false;
-    }
-
-    private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
-        // TODO: Check again whether there really ain't a better way to check this.
-        // TODO: This should probably be language-dependant...
-        return Character.isLetterOrDigit(codePoint)
-                || codePoint == Constants.CODE_SINGLE_QUOTE
-                || codePoint == Constants.CODE_DOUBLE_QUOTE
-                || codePoint == Constants.CODE_CLOSING_PARENTHESIS
-                || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
-                || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
-                || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
-                || codePoint == Constants.CODE_PLUS
-                || codePoint == Constants.CODE_PERCENT
-                || Character.getType(codePoint) == Character.OTHER_SYMBOL;
+        return mInputLogic.mRecapitalizeStatus.getCurrentMode();
     }
 
     // Callback for the {@link SuggestionStripView}, to call when the "add to dictionary" hint is
@@ -1445,7 +1303,7 @@
             return;
         }
         final String wordToEdit;
-        if (CapsModeUtils.isAutoCapsMode(mLastComposedWord.mCapitalizedMode)) {
+        if (CapsModeUtils.isAutoCapsMode(mInputLogic.mLastComposedWord.mCapitalizedMode)) {
             wordToEdit = word.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
         } else {
             wordToEdit = word;
@@ -1453,7 +1311,8 @@
         mUserDictionary.addWordToUserDictionary(wordToEdit);
     }
 
-    private void onSettingsKeyPressed() {
+    // TODO[IL]: Rework the route through which this is called.
+    public void onSettingsKeyPressed() {
         if (isShowingOptionDialog()) return;
         showSubtypeSelectorAndSettings();
     }
@@ -1476,12 +1335,9 @@
         return mOptionsDialog != null && mOptionsDialog.isShowing();
     }
 
-    private void performEditorAction(final int actionId) {
-        mConnection.performEditorAction(actionId);
-    }
-
     // TODO: Revise the language switch key behavior to make it much smarter and more reasonable.
-    private void handleLanguageSwitchKey() {
+    // TODO[IL]: Move a part of this to InputLogic and straighten out the interface for this.
+    public void handleLanguageSwitchKey() {
         final IBinder token = getWindow().getWindow().getAttributes().token;
         if (mSettings.getCurrent().mIncludesOtherImesInLanguageSwitchList) {
             mRichImm.switchToNextInputMethod(token, false /* onlyCurrentIme */);
@@ -1490,197 +1346,21 @@
         mSubtypeState.switchSubtype(token, mRichImm);
     }
 
-    private void sendDownUpKeyEvent(final int code) {
-        final long eventTime = SystemClock.uptimeMillis();
-        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
-                KeyEvent.ACTION_DOWN, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
-                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
-        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
-                KeyEvent.ACTION_UP, code, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
-                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
-    }
-
-    private void sendKeyCodePoint(final int code) {
-        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_sendKeyCodePoint(code);
-        }
-        // TODO: Remove this special handling of digit letters.
-        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
-        if (code >= '0' && code <= '9') {
-            sendDownUpKeyEvent(code - '0' + KeyEvent.KEYCODE_0);
-            return;
-        }
-
-        if (Constants.CODE_ENTER == code && mAppWorkAroundsUtils.isBeforeJellyBean()) {
-            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
-            // a hardware keyboard event on pressing enter or delete. This is bad for many
-            // reasons (there are race conditions with commits) but some applications are
-            // relying on this behavior so we continue to support it for older apps.
-            sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
-        } else {
-            mConnection.commitText(StringUtils.newSingleCodePointString(code), 1);
-        }
-    }
-
     // Implementation of {@link KeyboardActionListener}.
     @Override
     public void onCodeInput(final int primaryCode, final int x, final int y) {
-        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_onCodeInput(primaryCode, x, y);
-        }
-        final long when = SystemClock.uptimeMillis();
-        if (primaryCode != Constants.CODE_DELETE || when > mLastKeyTime + QUICK_PRESS) {
-            mDeleteCount = 0;
-        }
-        mLastKeyTime = when;
-        mConnection.beginBatchEdit();
-        final KeyboardSwitcher switcher = mKeyboardSwitcher;
-        // The space state depends only on the last character pressed and its own previous
-        // state. Here, we revert the space state to neutral if the key is actually modifying
-        // the input contents (any non-shift key), which is what we should do for
-        // all inputs that do not result in a special state. Each character handling is then
-        // free to override the state as they see fit.
-        final int spaceState = mSpaceState;
-        if (!mWordComposer.isComposingWord()) mIsAutoCorrectionIndicatorOn = false;
-
-        // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
-        if (primaryCode != Constants.CODE_SPACE) {
-            mHandler.cancelDoubleSpacePeriodTimer();
-        }
-
-        boolean didAutoCorrect = false;
-        switch (primaryCode) {
-        case Constants.CODE_DELETE:
-            mSpaceState = SPACE_STATE_NONE;
-            handleBackspace(spaceState);
-            LatinImeLogger.logOnDelete(x, y);
-            break;
-        case Constants.CODE_SHIFT:
-            // Note: Calling back to the keyboard on Shift key is handled in
-            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
-            final Keyboard currentKeyboard = switcher.getKeyboard();
-            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
-                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
-                // alphabetic shift and shift while in symbol layout.
-                handleRecapitalize();
-            }
-            break;
-        case Constants.CODE_CAPSLOCK:
-            // Note: Changing keyboard to shift lock state is handled in
-            // {@link KeyboardSwitcher#onCodeInput(int)}.
-            break;
-        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
-            // Note: Calling back to the keyboard on symbol key is handled in
-            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
-            break;
-        case Constants.CODE_SETTINGS:
-            onSettingsKeyPressed();
-            break;
-        case Constants.CODE_SHORTCUT:
-            mSubtypeSwitcher.switchToShortcutIME(this);
-            break;
-        case Constants.CODE_ACTION_NEXT:
-            performEditorAction(EditorInfo.IME_ACTION_NEXT);
-            break;
-        case Constants.CODE_ACTION_PREVIOUS:
-            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
-            break;
-        case Constants.CODE_LANGUAGE_SWITCH:
-            handleLanguageSwitchKey();
-            break;
-        case Constants.CODE_EMOJI:
-            // Note: Switching emoji keyboard is being handled in
-            // {@link KeyboardState#onCodeInput(int,int)}.
-            break;
-        case Constants.CODE_ENTER:
-            final EditorInfo editorInfo = getCurrentInputEditorInfo();
-            final int imeOptionsActionId =
-                    InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
-            if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
-                // Either we have an actionLabel and we should performEditorAction with actionId
-                // regardless of its value.
-                performEditorAction(editorInfo.actionId);
-            } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
-                // We didn't have an actionLabel, but we had another action to execute.
-                // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
-                // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
-                // means there should be an action and the app didn't bother to set a specific
-                // code for it - presumably it only handles one. It does not have to be treated
-                // in any specific way: anything that is not IME_ACTION_NONE should be sent to
-                // performEditorAction.
-                performEditorAction(imeOptionsActionId);
-            } else {
-                // No action label, and the action from imeOptions is NONE: this is a regular
-                // enter key that should input a carriage return.
-                didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
-            }
-            break;
-        case Constants.CODE_SHIFT_ENTER:
-            didAutoCorrect = handleNonSpecialCharacter(Constants.CODE_ENTER, x, y, spaceState);
-            break;
-        default:
-            didAutoCorrect = handleNonSpecialCharacter(primaryCode, x, y, spaceState);
-            break;
-        }
-        switcher.onCodeInput(primaryCode);
-        // Reset after any single keystroke, except shift, capslock, and symbol-shift
-        if (!didAutoCorrect && primaryCode != Constants.CODE_SHIFT
-                && primaryCode != Constants.CODE_CAPSLOCK
-                && primaryCode != Constants.CODE_SWITCH_ALPHA_SYMBOL)
-            mLastComposedWord.deactivate();
-        if (Constants.CODE_DELETE != primaryCode) {
-            mEnteredText = null;
-        }
-        mConnection.endBatchEdit();
-    }
-
-    private boolean handleNonSpecialCharacter(final int primaryCode, final int x, final int y,
-            final int spaceState) {
-        mSpaceState = SPACE_STATE_NONE;
-        final boolean didAutoCorrect;
-        final SettingsValues settingsValues = mSettings.getCurrent();
-        if (settingsValues.isWordSeparator(primaryCode)
-                || Character.getType(primaryCode) == Character.OTHER_SYMBOL) {
-            didAutoCorrect = handleSeparator(primaryCode, x, y, spaceState);
-        } else {
-            didAutoCorrect = false;
-            if (SPACE_STATE_PHANTOM == spaceState) {
-                if (settingsValues.mIsInternal) {
-                    if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
-                        LatinImeLoggerUtils.onAutoCorrection(
-                                "", mWordComposer.getTypedWord(), " ", mWordComposer);
-                    }
-                }
-                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
-                    // If we are in the middle of a recorrection, we need to commit the recorrection
-                    // first so that we can insert the character at the current cursor position.
-                    resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
-                } else {
-                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
-                }
-            }
-            final int keyX, keyY;
-            final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
-            if (keyboard != null && keyboard.hasProximityCharsCorrection(primaryCode)) {
-                keyX = x;
-                keyY = y;
-            } else {
-                keyX = Constants.NOT_A_COORDINATE;
-                keyY = Constants.NOT_A_COORDINATE;
-            }
-            handleCharacter(primaryCode, keyX, keyY, spaceState);
-        }
-        return didAutoCorrect;
+        mInputLogic.onCodeInput(primaryCode, x, y, mHandler, mKeyboardSwitcher, mSubtypeSwitcher);
     }
 
     // Called from PointerTracker through the KeyboardActionListener interface
+    // TODO[IL]: Move this to InputLogic
     @Override
     public void onTextInput(final String rawText) {
-        mConnection.beginBatchEdit();
-        if (mWordComposer.isComposingWord()) {
-            commitCurrentAutoCorrection(rawText);
+        mInputLogic.mConnection.beginBatchEdit();
+        if (mInputLogic.mWordComposer.isComposingWord()) {
+            mInputLogic.commitCurrentAutoCorrection(mSettings.getCurrent(), rawText, mHandler);
         } else {
-            resetComposingState(true /* alsoResetLastComposedWord */);
+            mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
         }
         mHandler.postUpdateSuggestionStrip();
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS
@@ -1689,40 +1369,42 @@
             return;
         }
         final String text = specificTldProcessingOnTextInput(rawText);
-        if (SPACE_STATE_PHANTOM == mSpaceState) {
-            promotePhantomSpace();
+        if (SpaceState.PHANTOM == mInputLogic.mSpaceState) {
+            mInputLogic.promotePhantomSpace(mSettings.getCurrent());
         }
-        mConnection.commitText(text, 1);
+        mInputLogic.mConnection.commitText(text, 1);
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_onTextInput(text, false /* isBatchMode */);
         }
-        mConnection.endBatchEdit();
+        mInputLogic.mConnection.endBatchEdit();
         // Space state must be updated before calling updateShiftState
-        mSpaceState = SPACE_STATE_NONE;
+        mInputLogic.mSpaceState = SpaceState.NONE;
         mKeyboardSwitcher.updateShiftState();
         mKeyboardSwitcher.onCodeInput(Constants.CODE_OUTPUT_TEXT);
-        mEnteredText = text;
+        mInputLogic.mEnteredText = text;
     }
 
     @Override
     public void onStartBatchInput() {
         mInputUpdater.onStartBatchInput();
         mHandler.cancelUpdateSuggestionStrip();
-        mConnection.beginBatchEdit();
+        mInputLogic.mConnection.beginBatchEdit();
         final SettingsValues currentSettingsValues = mSettings.getCurrent();
-        if (mWordComposer.isComposingWord()) {
+        if (mInputLogic.mWordComposer.isComposingWord()) {
             if (currentSettingsValues.mIsInternal) {
-                if (mWordComposer.isBatchMode()) {
-                    LatinImeLoggerUtils.onAutoCorrection(
-                            "", mWordComposer.getTypedWord(), " ", mWordComposer);
+                if (mInputLogic.mWordComposer.isBatchMode()) {
+                    LatinImeLoggerUtils.onAutoCorrection("",
+                            mInputLogic.mWordComposer.getTypedWord(), " ",
+                            mInputLogic.mWordComposer);
                 }
             }
-            final int wordComposerSize = mWordComposer.size();
+            final int wordComposerSize = mInputLogic.mWordComposer.size();
             // Since isComposingWord() is true, the size is at least 1.
-            if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            if (mInputLogic.mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
                 // If we are in the middle of a recorrection, we need to commit the recorrection
                 // first so that we can insert the batch input at the current cursor position.
-                resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
+                mInputLogic.resetEntireInputState(currentSettingsValues,
+                        mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd);
             } else if (wordComposerSize <= 1) {
                 // We auto-correct the previous (typed, not gestured) string iff it's one character
                 // long. The reason for this is, even in the middle of gesture typing, you'll still
@@ -1731,17 +1413,18 @@
                 // tapping probably is that the word you intend to type is not in the dictionary,
                 // so we do not attempt to correct, on the assumption that if that was a dictionary
                 // word, the user would probably have gestured instead.
-                commitCurrentAutoCorrection(LastComposedWord.NOT_A_SEPARATOR);
+                mInputLogic.commitCurrentAutoCorrection(currentSettingsValues,
+                        LastComposedWord.NOT_A_SEPARATOR, mHandler);
             } else {
-                commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
             }
         }
-        final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+        final int codePointBeforeCursor = mInputLogic.mConnection.getCodePointBeforeCursor();
         if (Character.isLetterOrDigit(codePointBeforeCursor)
                 || currentSettingsValues.isUsuallyFollowedBySpace(codePointBeforeCursor)) {
             final boolean autoShiftHasBeenOverriden = mKeyboardSwitcher.getKeyboardShiftMode() !=
                     getCurrentAutoCapsState();
-            mSpaceState = SPACE_STATE_PHANTOM;
+            mInputLogic.mSpaceState = SpaceState.PHANTOM;
             if (!autoShiftHasBeenOverriden) {
                 // When we change the space state, we need to update the shift state of the
                 // keyboard unless it has been overridden manually. This is happening for example
@@ -1751,10 +1434,12 @@
                 mKeyboardSwitcher.updateShiftState();
             }
         }
-        mConnection.endBatchEdit();
-        mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(getActualCapsMode(),
+        mInputLogic.mConnection.endBatchEdit();
+        mInputLogic.mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                mInputLogic.getActualCapsMode(mKeyboardSwitcher),
                 // Prev word is 1st word before cursor
-                getNthPreviousWordForSuggestion(currentSettingsValues, 1 /* nthPreviousWord */));
+                mInputLogic.getNthPreviousWordForSuggestion(currentSettingsValues,
+                        1 /* nthPreviousWord */));
     }
 
     static final class InputUpdater implements Handler.Callback {
@@ -1857,7 +1542,7 @@
         // be synchronized.
         private void getSuggestedWordsGestureLocked(final InputPointers batchPointers,
                 final int sequenceNumber, final OnGetSuggestedWordsCallback callback) {
-            mLatinIme.mWordComposer.setBatchInputPointers(batchPointers);
+            mLatinIme.mInputLogic.mWordComposer.setBatchInputPointers(batchPointers);
             mLatinIme.getSuggestedWordsOrOlderSuggestionsAsync(Suggest.SESSION_GESTURE,
                     sequenceNumber, new OnGetSuggestedWordsCallback() {
                 @Override
@@ -1916,19 +1601,22 @@
     @Override
     public void onUpdateBatchInput(final InputPointers batchPointers) {
         if (mSettings.getCurrent().mPhraseGestureEnabled) {
-            final SuggestedWordInfo candidate = mSuggestedWords.getAutoCommitCandidate();
+            final SuggestedWordInfo candidate =
+                    mInputLogic.mSuggestedWords.getAutoCommitCandidate();
             // If these suggested words have been generated with out of date input pointers, then
             // we skip auto-commit (see comments above on the mSequenceNumber member).
-            if (null != candidate && mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
+            if (null != candidate
+                    && mInputLogic.mSuggestedWords.mSequenceNumber >= mAutoCommitSequenceNumber) {
                 if (candidate.mSourceDict.shouldAutoCommit(candidate)) {
                     final String[] commitParts = candidate.mWord.split(" ", 2);
                     batchPointers.shift(candidate.mIndexOfTouchPointOfSecondWord);
-                    promotePhantomSpace();
-                    mConnection.commitText(commitParts[0], 0);
-                    mSpaceState = SPACE_STATE_PHANTOM;
+                    mInputLogic.promotePhantomSpace(mSettings.getCurrent());
+                    mInputLogic.mConnection.commitText(commitParts[0], 0);
+                    mInputLogic.mSpaceState = SpaceState.PHANTOM;
                     mKeyboardSwitcher.updateShiftState();
-                    mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
-                            getActualCapsMode(), commitParts[0]);
+                    mInputLogic.mWordComposer.
+                            setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                            mInputLogic.getActualCapsMode(mKeyboardSwitcher), commitParts[0]);
                     ++mAutoCommitSequenceNumber;
                 }
             }
@@ -1942,30 +1630,31 @@
         if (TextUtils.isEmpty(batchInputText)) {
             return;
         }
-        mConnection.beginBatchEdit();
-        if (SPACE_STATE_PHANTOM == mSpaceState) {
-            promotePhantomSpace();
+        mInputLogic.mConnection.beginBatchEdit();
+        if (SpaceState.PHANTOM == mInputLogic.mSpaceState) {
+            mInputLogic.promotePhantomSpace(mSettings.getCurrent());
         }
         if (mSettings.getCurrent().mPhraseGestureEnabled) {
             // Find the last space
             final int indexOfLastSpace = batchInputText.lastIndexOf(Constants.CODE_SPACE) + 1;
             if (0 != indexOfLastSpace) {
-                mConnection.commitText(batchInputText.substring(0, indexOfLastSpace), 1);
+                mInputLogic.mConnection.commitText(batchInputText.substring(0, indexOfLastSpace),
+                        1);
                 showSuggestionStrip(suggestedWords.getSuggestedWordsForLastWordOfPhraseGesture());
             }
             final String lastWord = batchInputText.substring(indexOfLastSpace);
-            mWordComposer.setBatchInputWord(lastWord);
-            mConnection.setComposingText(lastWord, 1);
+            mInputLogic.mWordComposer.setBatchInputWord(lastWord);
+            mInputLogic.mConnection.setComposingText(lastWord, 1);
         } else {
-            mWordComposer.setBatchInputWord(batchInputText);
-            mConnection.setComposingText(batchInputText, 1);
+            mInputLogic.mWordComposer.setBatchInputWord(batchInputText);
+            mInputLogic.mConnection.setComposingText(batchInputText, 1);
         }
-        mConnection.endBatchEdit();
+        mInputLogic.mConnection.endBatchEdit();
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_onEndBatchInput(batchInputText, 0, suggestedWords);
         }
         // Space state must be updated before calling updateShiftState
-        mSpaceState = SPACE_STATE_PHANTOM;
+        mInputLogic.mSpaceState = SpaceState.PHANTOM;
         mKeyboardSwitcher.updateShiftState();
     }
 
@@ -1982,9 +1671,9 @@
         }
         // We have a TLD (or something that looks like this): make sure we don't add
         // a space even if currently in phantom mode.
-        mSpaceState = SPACE_STATE_NONE;
+        mInputLogic.mSpaceState = SpaceState.NONE;
         // TODO: use getCodePointBeforeCursor instead to improve performance and simplify the code
-        final CharSequence lastOne = mConnection.getTextBeforeCursor(1, 0);
+        final CharSequence lastOne = mInputLogic.mConnection.getTextBeforeCursor(1, 0);
         if (lastOne != null && lastOne.length() == 1
                 && lastOne.charAt(0) == Constants.CODE_PERIOD) {
             return text.substring(1);
@@ -2012,378 +1701,10 @@
         mInputUpdater.onCancelBatchInput();
     }
 
-    private void handleBackspace(final int spaceState) {
-        mDeleteCount++;
-
-        // In many cases, we may have to put the keyboard in auto-shift state again. However
-        // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
-        // during key repeat.
-        mHandler.postUpdateShiftState();
-
-        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
-            // If we are in the middle of a recorrection, we need to commit the recorrection
-            // first so that we can remove the character at the current cursor position.
-            resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
-            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
-        }
-        if (mWordComposer.isComposingWord()) {
-            if (mWordComposer.isBatchMode()) {
-                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                    final String word = mWordComposer.getTypedWord();
-                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
-                }
-                final String rejectedSuggestion = mWordComposer.getTypedWord();
-                mWordComposer.reset();
-                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
-            } else {
-                mWordComposer.deleteLast();
-            }
-            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-            mHandler.postUpdateSuggestionStrip();
-            if (!mWordComposer.isComposingWord()) {
-                // If we just removed the last character, auto-caps mode may have changed so we
-                // need to re-evaluate.
-                mKeyboardSwitcher.updateShiftState();
-            }
-        } else {
-            final SettingsValues currentSettings = mSettings.getCurrent();
-            if (mLastComposedWord.canRevertCommit()) {
-                if (currentSettings.mIsInternal) {
-                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
-                }
-                revertCommit();
-                return;
-            }
-            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
-                // Cancel multi-character input: remove the text we just entered.
-                // This is triggered on backspace after a key that inputs multiple characters,
-                // like the smiley key or the .com key.
-                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
-                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                    ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
-                }
-                mEnteredText = null;
-                // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
-                // In addition we know that spaceState is false, and that we should not be
-                // reverting any autocorrect at this point. So we can safely return.
-                return;
-            }
-            if (SPACE_STATE_DOUBLE == spaceState) {
-                mHandler.cancelDoubleSpacePeriodTimer();
-                if (mConnection.revertDoubleSpacePeriod()) {
-                    // No need to reset mSpaceState, it has already be done (that's why we
-                    // receive it as a parameter)
-                    return;
-                }
-            } else if (SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-                if (mConnection.revertSwapPunctuation()) {
-                    // Likewise
-                    return;
-                }
-            }
-
-            // No cancelling of commit/double space/swap: we have a regular backspace.
-            // We should backspace one char and restart suggestion if at the end of a word.
-            if (mLastSelectionStart != mLastSelectionEnd) {
-                // If there is a selection, remove it.
-                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
-                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
-                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
-                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
-                // but we want to set it right away to avoid it being used with the wrong values
-                // later (typically, in a subsequent press on backspace).
-                mLastSelectionEnd = mLastSelectionStart;
-                mConnection.deleteSurroundingText(numCharsDeleted, 0);
-                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                    ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
-                            false /* shouldUncommitLogUnit */);
-                }
-            } else {
-                // There is no selection, just delete one character.
-                if (NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
-                    // This should never happen.
-                    Log.e(TAG, "Backspace when we don't know the selection position");
-                }
-                if (mAppWorkAroundsUtils.isBeforeJellyBean() ||
-                        currentSettings.mInputAttributes.isTypeNull()) {
-                    // There are two possible reasons to send a key event: either the field has
-                    // type TYPE_NULL, in which case the keyboard should send events, or we are
-                    // running in backward compatibility mode. Before Jelly bean, the keyboard
-                    // would simulate a hardware keyboard event on pressing enter or delete. This
-                    // is bad for many reasons (there are race conditions with commits) but some
-                    // applications are relying on this behavior so we continue to support it for
-                    // older apps, so we retain this behavior if the app has target SDK < JellyBean.
-                    sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
-                    if (mDeleteCount > DELETE_ACCELERATE_AT) {
-                        sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
-                    }
-                } else {
-                    final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
-                    if (codePointBeforeCursor == Constants.NOT_A_CODE) {
-                        // Nothing to delete before the cursor.
-                        return;
-                    }
-                    final int lengthToDelete =
-                            Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
-                    mConnection.deleteSurroundingText(lengthToDelete, 0);
-                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                        ResearchLogger.latinIME_handleBackspace(lengthToDelete,
-                                true /* shouldUncommitLogUnit */);
-                    }
-                    if (mDeleteCount > DELETE_ACCELERATE_AT) {
-                        final int codePointBeforeCursorToDeleteAgain =
-                                mConnection.getCodePointBeforeCursor();
-                        if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
-                            final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
-                                    codePointBeforeCursorToDeleteAgain) ? 2 : 1;
-                            mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
-                            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                                ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
-                                        true /* shouldUncommitLogUnit */);
-                            }
-                        }
-                    }
-                }
-            }
-            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)
-                    && currentSettings.mCurrentLanguageHasSpaces) {
-                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord();
-            }
-            // We just removed a character. We need to update the auto-caps state.
-            mKeyboardSwitcher.updateShiftState();
-        }
-    }
-
-    /*
-     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
-     */
-    private boolean maybeStripSpace(final int code, final int spaceState,
-            final boolean isFromSuggestionStrip) {
-        if (Constants.CODE_ENTER == code && SPACE_STATE_SWAP_PUNCTUATION == spaceState) {
-            mConnection.removeTrailingSpace();
-            return false;
-        }
-        if ((SPACE_STATE_WEAK == spaceState || SPACE_STATE_SWAP_PUNCTUATION == spaceState)
-                && isFromSuggestionStrip) {
-            final SettingsValues currentSettings = mSettings.getCurrent();
-            if (currentSettings.isUsuallyPrecededBySpace(code)) return false;
-            if (currentSettings.isUsuallyFollowedBySpace(code)) return true;
-            mConnection.removeTrailingSpace();
-        }
-        return false;
-    }
-
-    private void handleCharacter(final int primaryCode, final int x, final int y,
-            final int spaceState) {
-        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
-        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
-        // which has the same name as other handle* methods but is not the same.
-        boolean isComposingWord = mWordComposer.isComposingWord();
-
-        // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
-        // See onStartBatchInput() to see how to do it.
-        final SettingsValues currentSettings = mSettings.getCurrent();
-        if (SPACE_STATE_PHANTOM == spaceState && !currentSettings.isWordConnector(primaryCode)) {
-            if (isComposingWord) {
-                // Sanity check
-                throw new RuntimeException("Should not be composing here");
-            }
-            promotePhantomSpace();
-        }
-
-        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
-            // If we are in the middle of a recorrection, we need to commit the recorrection
-            // first so that we can insert the character at the current cursor position.
-            resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
-            isComposingWord = false;
-        }
-        // We want to find out whether to start composing a new word with this character. If so,
-        // we need to reset the composing state and switch isComposingWord. The order of the
-        // tests is important for good performance.
-        // We only start composing if we're not already composing.
-        if (!isComposingWord
-        // We only start composing if this is a word code point. Essentially that means it's a
-        // a letter or a word connector.
-                && currentSettings.isWordCodePoint(primaryCode)
-        // We never go into composing state if suggestions are not requested.
-                && currentSettings.isSuggestionsRequested(mDisplayOrientation) &&
-        // In languages with spaces, we only start composing a word when we are not already
-        // touching a word. In languages without spaces, the above conditions are sufficient.
-                (!mConnection.isCursorTouchingWord(currentSettings)
-                        || !currentSettings.mCurrentLanguageHasSpaces)) {
-            // Reset entirely the composing state anyway, then start composing a new word unless
-            // the character is a single quote or a dash. The idea here is, single quote and dash
-            // are not separators and they should be treated as normal characters, except in the
-            // first position where they should not start composing a word.
-            isComposingWord = (Constants.CODE_SINGLE_QUOTE != primaryCode
-                    && Constants.CODE_DASH != primaryCode);
-            // Here we don't need to reset the last composed word. It will be reset
-            // when we commit this one, if we ever do; if on the other hand we backspace
-            // it entirely and resume suggestions on the previous word, we'd like to still
-            // have touch coordinates for it.
-            resetComposingState(false /* alsoResetLastComposedWord */);
-        }
-        if (isComposingWord) {
-            final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
-            // TODO: We should reconsider which coordinate system should be used to represent
-            // keyboard event.
-            final int keyX = mainKeyboardView.getKeyX(x);
-            final int keyY = mainKeyboardView.getKeyY(y);
-            mWordComposer.add(primaryCode, keyX, keyY);
-            // If it's the first letter, make note of auto-caps state
-            if (mWordComposer.size() == 1) {
-                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
-                // yet, so the word we want is the 1st word before the cursor.
-                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
-                        getActualCapsMode(),
-                        getNthPreviousWordForSuggestion(currentSettings, 1 /* nthPreviousWord */));
-            }
-            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
-        } else {
-            final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
-                    Constants.SUGGESTION_STRIP_COORDINATE == x);
-
-            sendKeyCodePoint(primaryCode);
-
-            if (swapWeakSpace) {
-                swapSwapperAndSpace();
-                mSpaceState = SPACE_STATE_WEAK;
-            }
-            // In case the "add to dictionary" hint was still displayed.
-            if (null != mSuggestionStripView) mSuggestionStripView.dismissAddToDictionaryHint();
-        }
-        mHandler.postUpdateSuggestionStrip();
-        if (currentSettings.mIsInternal) {
-            LatinImeLoggerUtils.onNonSeparator((char)primaryCode, x, y);
-        }
-    }
-
-    private void handleRecapitalize() {
-        if (mLastSelectionStart == mLastSelectionEnd) return; // No selection
-        // If we have a recapitalize in progress, use it; otherwise, create a new one.
-        if (!mRecapitalizeStatus.isActive()
-                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
-            final CharSequence selectedText =
-                    mConnection.getSelectedText(0 /* flags, 0 for no styles */);
-            if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
-            final SettingsValues currentSettings = mSettings.getCurrent();
-            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
-                    selectedText.toString(), currentSettings.mLocale,
-                    currentSettings.mWordSeparators);
-            // We trim leading and trailing whitespace.
-            mRecapitalizeStatus.trim();
-            // Trimming the object may have changed the length of the string, and we need to
-            // reposition the selection handles accordingly. As this result in an IPC call,
-            // only do it if it's actually necessary, in other words if the recapitalize status
-            // is not set at the same place as before.
-            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
-                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
-                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
-            }
-        }
-        mConnection.finishComposingText();
-        mRecapitalizeStatus.rotate();
-        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
-        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
-        mConnection.deleteSurroundingText(numCharsDeleted, 0);
-        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
-        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
-        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
-        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
-        // Match the keyboard to the new state.
-        mKeyboardSwitcher.updateShiftState();
-    }
-
-    // Returns true if we do an autocorrection, false otherwise.
-    private boolean handleSeparator(final int primaryCode, final int x, final int y,
-            final int spaceState) {
-        boolean didAutoCorrect = false;
-        final SettingsValues currentSettings = mSettings.getCurrent();
-        // We avoid sending spaces in languages without spaces if we were composing.
-        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == primaryCode
-                && !currentSettings.mCurrentLanguageHasSpaces && mWordComposer.isComposingWord();
-        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
-            // If we are in the middle of a recorrection, we need to commit the recorrection
-            // first so that we can insert the separator at the current cursor position.
-            resetEntireInputState(mLastSelectionStart, mLastSelectionEnd);
-        }
-        if (mWordComposer.isComposingWord()) { // May have changed since we stored wasComposing
-            if (currentSettings.mCorrectionEnabled) {
-                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
-                        : StringUtils.newSingleCodePointString(primaryCode);
-                commitCurrentAutoCorrection(separator);
-                didAutoCorrect = true;
-            } else {
-                commitTyped(StringUtils.newSingleCodePointString(primaryCode));
-            }
-        }
-
-        final boolean swapWeakSpace = maybeStripSpace(primaryCode, spaceState,
-                Constants.SUGGESTION_STRIP_COORDINATE == x);
-
-        if (SPACE_STATE_PHANTOM == spaceState &&
-                currentSettings.isUsuallyPrecededBySpace(primaryCode)) {
-            promotePhantomSpace();
-        }
-        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_handleSeparator(primaryCode, mWordComposer.isComposingWord());
-        }
-
-        if (!shouldAvoidSendingCode) {
-            sendKeyCodePoint(primaryCode);
-        }
-
-        if (Constants.CODE_SPACE == primaryCode) {
-            if (currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
-                if (maybeDoubleSpacePeriod()) {
-                    mSpaceState = SPACE_STATE_DOUBLE;
-                } else if (!isShowingPunctuationList()) {
-                    mSpaceState = SPACE_STATE_WEAK;
-                }
-            }
-
-            mHandler.startDoubleSpacePeriodTimer();
-            mHandler.postUpdateSuggestionStrip();
-        } else {
-            if (swapWeakSpace) {
-                swapSwapperAndSpace();
-                mSpaceState = SPACE_STATE_SWAP_PUNCTUATION;
-            } else if (SPACE_STATE_PHANTOM == spaceState
-                    && currentSettings.isUsuallyFollowedBySpace(primaryCode)) {
-                // If we are in phantom space state, and the user presses a separator, we want to
-                // stay in phantom space state so that the next keypress has a chance to add the
-                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
-                // then insert a comma and go on to typing the next word, I want the space to be
-                // inserted automatically before the next word, the same way it is when I don't
-                // input the comma.
-                // The case is a little different if the separator is a space stripper. Such a
-                // separator does not normally need a space on the right (that's the difference
-                // between swappers and strippers), so we should not stay in phantom space state if
-                // the separator is a stripper. Hence the additional test above.
-                mSpaceState = SPACE_STATE_PHANTOM;
-            }
-
-            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
-            // already displayed or not, so it's okay.
-            setPunctuationSuggestions();
-        }
-        if (currentSettings.mIsInternal) {
-            LatinImeLoggerUtils.onSeparator((char)primaryCode, x, y);
-        }
-
-        mKeyboardSwitcher.updateShiftState();
-        return didAutoCorrect;
-    }
-
-    private CharSequence getTextWithUnderline(final String text) {
-        return mIsAutoCorrectionIndicatorOn
-                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(this, text)
-                : text;
-    }
-
+    // TODO[IL]: Rename this to avoid using handle*
     private void handleClose() {
         // TODO: Verify that words are logged properly when IME is closed.
-        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+        mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
         requestHideSelf(0);
         final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
         if (mainKeyboardView != null) {
@@ -2391,12 +1712,12 @@
         }
     }
 
-    // TODO: make this private
+    // TODO[IL]: Move this to InputLogic and make it private
     // Outside LatinIME, only used by the test suite.
     @UsedForTesting
-    boolean isShowingPunctuationList() {
-        if (mSuggestedWords == null) return false;
-        return mSettings.getCurrent().mSuggestPuncList == mSuggestedWords;
+    public boolean isShowingPunctuationList() {
+        if (mInputLogic.mSuggestedWords == null) return false;
+        return mSettings.getCurrent().mSuggestPuncList == mInputLogic.mSuggestedWords;
     }
 
     private boolean isSuggestionsStripVisible() {
@@ -2414,13 +1735,21 @@
         return currentSettings.isSuggestionsRequested(mDisplayOrientation);
     }
 
-    private void clearSuggestionStrip() {
+    public void dismissAddToDictionaryHint() {
+        if (null != mSuggestionStripView) {
+            mSuggestionStripView.dismissAddToDictionaryHint();
+        }
+    }
+
+    // TODO[IL]: Define a clear interface for this
+    public void clearSuggestionStrip() {
         setSuggestedWords(SuggestedWords.EMPTY, false);
         setAutoCorrectionIndicator(false);
     }
 
-    private void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
-        mSuggestedWords = words;
+    // TODO[IL]: Define a clear interface for this
+    public void setSuggestedWords(final SuggestedWords words, final boolean isAutoCorrection) {
+        mInputLogic.mSuggestedWords = words;
         if (mSuggestionStripView != null) {
             mSuggestionStripView.setSuggestions(words);
             mKeyboardSwitcher.onAutoCorrectionStateChanged(isAutoCorrection);
@@ -2429,34 +1758,36 @@
 
     private void setAutoCorrectionIndicator(final boolean newAutoCorrectionIndicator) {
         // Put a blue underline to a word in TextView which will be auto-corrected.
-        if (mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
-                && mWordComposer.isComposingWord()) {
-            mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
+        if (mInputLogic.mIsAutoCorrectionIndicatorOn != newAutoCorrectionIndicator
+                && mInputLogic.mWordComposer.isComposingWord()) {
+            mInputLogic.mIsAutoCorrectionIndicatorOn = newAutoCorrectionIndicator;
             final CharSequence textWithUnderline =
-                    getTextWithUnderline(mWordComposer.getTypedWord());
+                    mInputLogic.getTextWithUnderline(mInputLogic.mWordComposer.getTypedWord());
             // TODO: when called from an updateSuggestionStrip() call that results from a posted
             // message, this is called outside any batch edit. Potentially, this may result in some
             // janky flickering of the screen, although the display speed makes it unlikely in
             // the practice.
-            mConnection.setComposingText(textWithUnderline, 1);
+            mInputLogic.mConnection.setComposingText(textWithUnderline, 1);
         }
     }
 
-    private void updateSuggestionStrip() {
+    // TODO[IL]: Move this to InputLogic and make private again
+    public void updateSuggestionStrip() {
         mHandler.cancelUpdateSuggestionStrip();
         final SettingsValues currentSettings = mSettings.getCurrent();
 
         // Check if we have a suggestion engine attached.
-        if (mSuggest == null
+        if (mInputLogic.mSuggest == null
                 || !currentSettings.isSuggestionsRequested(mDisplayOrientation)) {
-            if (mWordComposer.isComposingWord()) {
+            if (mInputLogic.mWordComposer.isComposingWord()) {
                 Log.w(TAG, "Called updateSuggestionsOrPredictions but suggestions were not "
                         + "requested!");
             }
             return;
         }
 
-        if (!mWordComposer.isComposingWord() && !currentSettings.mBigramPredictionEnabled) {
+        if (!mInputLogic.mWordComposer.isComposingWord()
+                && !currentSettings.mBigramPredictionEnabled) {
             setPunctuationSuggestions();
             return;
         }
@@ -2478,28 +1809,10 @@
         }
     }
 
-    /**
-     * Get the nth previous word before the cursor as context for the suggestion process.
-     * @param currentSettings the current settings values.
-     * @param nthPreviousWord reverse index of the word to get (1-indexed)
-     * @return the nth previous word before the cursor.
-     */
-    private String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
-            final int nthPreviousWord) {
-        if (currentSettings.mCurrentLanguageHasSpaces) {
-            // If we are typing in a language with spaces we can just look up the previous
-            // word from textview.
-            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
-        } else {
-            return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
-                    : mLastComposedWord.mCommittedWord;
-        }
-    }
-
     private void getSuggestedWords(final int sessionId, final int sequenceNumber,
             final OnGetSuggestedWordsCallback callback) {
         final Keyboard keyboard = mKeyboardSwitcher.getKeyboard();
-        final Suggest suggest = mSuggest;
+        final Suggest suggest = mInputLogic.mSuggest;
         if (keyboard == null || suggest == null) {
             callback.onGetSuggestedWords(SuggestedWords.EMPTY);
             return;
@@ -2511,22 +1824,25 @@
         final int[] additionalFeaturesOptions = currentSettings.mAdditionalFeaturesSettingValues;
 
         if (DEBUG) {
-            if (mWordComposer.isComposingWord() || mWordComposer.isBatchMode()) {
-                final String previousWord = mWordComposer.getPreviousWordForSuggestion();
+            if (mInputLogic.mWordComposer.isComposingWord()
+                    || mInputLogic.mWordComposer.isBatchMode()) {
+                final String previousWord
+                        = mInputLogic.mWordComposer.getPreviousWordForSuggestion();
                 // TODO: this is for checking consistency with older versions. Remove this when
                 // we are confident this is stable.
                 // We're checking the previous word in the text field against the memorized previous
                 // word. If we are composing a word we should have the second word before the cursor
                 // memorized, otherwise we should have the first.
-                final String rereadPrevWord = getNthPreviousWordForSuggestion(currentSettings,
-                        mWordComposer.isComposingWord() ? 2 : 1);
+                final String rereadPrevWord = mInputLogic.getNthPreviousWordForSuggestion(
+                        currentSettings, mInputLogic.mWordComposer.isComposingWord() ? 2 : 1);
                 if (!TextUtils.equals(previousWord, rereadPrevWord)) {
                     throw new RuntimeException("Unexpected previous word: "
                             + previousWord + " <> " + rereadPrevWord);
                 }
             }
         }
-        suggest.getSuggestedWords(mWordComposer, mWordComposer.getPreviousWordForSuggestion(),
+        suggest.getSuggestedWords(mInputLogic.mWordComposer,
+                mInputLogic.mWordComposer.getPreviousWordForSuggestion(),
                 keyboard.getProximityInfo(),
                 currentSettings.mBlockPotentiallyOffensive, currentSettings.mCorrectionEnabled,
                 additionalFeaturesOptions, sessionId, sequenceNumber, callback);
@@ -2539,7 +1855,7 @@
                     @Override
                     public void onGetSuggestedWords(SuggestedWords suggestedWords) {
                         callback.onGetSuggestedWords(maybeRetrieveOlderSuggestions(
-                                mWordComposer.getTypedWord(), suggestedWords));
+                                mInputLogic.mWordComposer.getTypedWord(), suggestedWords));
                     }
                 });
     }
@@ -2563,7 +1879,7 @@
     }
 
     private SuggestedWords getOlderSuggestions(final String typedWord) {
-        SuggestedWords previousSuggestedWords = mSuggestedWords;
+        SuggestedWords previousSuggestedWords = mInputLogic.mSuggestedWords;
         if (previousSuggestedWords == mSettings.getCurrent().mSuggestPuncList) {
             previousSuggestedWords = SuggestedWords.EMPTY;
         }
@@ -2591,7 +1907,7 @@
             // because it may differ from mWordComposer.mTypedWord.
             autoCorrection = typedWord;
         }
-        mWordComposer.setAutoCorrection(autoCorrection);
+        mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
     }
 
     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
@@ -2621,50 +1937,11 @@
             suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
     }
 
-    private void commitCurrentAutoCorrection(final String separator) {
-        // Complete any pending suggestions query first
-        if (mHandler.hasPendingUpdateSuggestions()) {
-            updateSuggestionStrip();
-        }
-        final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
-        final String typedWord = mWordComposer.getTypedWord();
-        final String autoCorrection = (typedAutoCorrection != null)
-                ? typedAutoCorrection : typedWord;
-        if (autoCorrection != null) {
-            if (TextUtils.isEmpty(typedWord)) {
-                throw new RuntimeException("We have an auto-correction but the typed word "
-                        + "is empty? Impossible! I must commit suicide.");
-            }
-            if (mSettings.isInternal()) {
-                LatinImeLoggerUtils.onAutoCorrection(
-                        typedWord, autoCorrection, separator, mWordComposer);
-            }
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                final SuggestedWords suggestedWords = mSuggestedWords;
-                ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
-                        separator, mWordComposer.isBatchMode(), suggestedWords);
-            }
-            commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
-                    separator);
-            if (!typedWord.equals(autoCorrection)) {
-                // This will make the correction flash for a short while as a visual clue
-                // to the user that auto-correction happened. It has no other effect; in particular
-                // note that this won't affect the text inside the text field AT ALL: it only makes
-                // the segment of text starting at the supplied index and running for the length
-                // of the auto-correction flash. At this moment, the "typedWord" argument is
-                // ignored by TextView.
-                mConnection.commitCorrection(
-                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
-                        typedWord, autoCorrection));
-            }
-        }
-    }
-
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
     // interface
     @Override
     public void pickSuggestionManually(final int index, final SuggestedWordInfo suggestionInfo) {
-        final SuggestedWords suggestedWords = mSuggestedWords;
+        final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords;
         final String suggestion = suggestionInfo.mWord;
         // If this is a punctuation picked from the suggestion strip, pass it to onCodeInput
         if (suggestion.length() == 1 && isShowingPunctuationList()) {
@@ -2682,57 +1959,57 @@
             return;
         }
 
-        mConnection.beginBatchEdit();
+        mInputLogic.mConnection.beginBatchEdit();
         final SettingsValues currentSettings = mSettings.getCurrent();
-        if (SPACE_STATE_PHANTOM == mSpaceState && suggestion.length() > 0
+        if (SpaceState.PHANTOM == mInputLogic.mSpaceState && suggestion.length() > 0
                 // In the batch input mode, a manually picked suggested word should just replace
                 // the current batch input text and there is no need for a phantom space.
-                && !mWordComposer.isBatchMode()) {
+                && !mInputLogic.mWordComposer.isBatchMode()) {
             final int firstChar = Character.codePointAt(suggestion, 0);
             if (!currentSettings.isWordSeparator(firstChar)
                     || currentSettings.isUsuallyPrecededBySpace(firstChar)) {
-                promotePhantomSpace();
+                mInputLogic.promotePhantomSpace(currentSettings);
             }
         }
 
         if (currentSettings.isApplicationSpecifiedCompletionsOn()
                 && mApplicationSpecifiedCompletions != null
                 && index >= 0 && index < mApplicationSpecifiedCompletions.length) {
-            mSuggestedWords = SuggestedWords.EMPTY;
+            mInputLogic.mSuggestedWords = SuggestedWords.EMPTY;
             if (mSuggestionStripView != null) {
                 mSuggestionStripView.clear();
             }
             mKeyboardSwitcher.updateShiftState();
-            resetComposingState(true /* alsoResetLastComposedWord */);
+            mInputLogic.resetComposingState(true /* alsoResetLastComposedWord */);
             final CompletionInfo completionInfo = mApplicationSpecifiedCompletions[index];
-            mConnection.commitCompletion(completionInfo);
-            mConnection.endBatchEdit();
+            mInputLogic.mConnection.commitCompletion(completionInfo);
+            mInputLogic.mConnection.endBatchEdit();
             return;
         }
 
         // We need to log before we commit, because the word composer will store away the user
         // typed word.
-        final String replacedWord = mWordComposer.getTypedWord();
+        final String replacedWord = mInputLogic.mWordComposer.getTypedWord();
         LatinImeLogger.logOnManualSuggestion(replacedWord, suggestion, index, suggestedWords);
         commitChosenWord(suggestion, LastComposedWord.COMMIT_TYPE_MANUAL_PICK,
                 LastComposedWord.NOT_A_SEPARATOR);
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.latinIME_pickSuggestionManually(replacedWord, index, suggestion,
-                    mWordComposer.isBatchMode(), suggestionInfo.mScore, suggestionInfo.mKind,
-                    suggestionInfo.mSourceDict.mDictType);
+                    mInputLogic.mWordComposer.isBatchMode(), suggestionInfo.mScore,
+                    suggestionInfo.mKind, suggestionInfo.mSourceDict.mDictType);
         }
-        mConnection.endBatchEdit();
+        mInputLogic.mConnection.endBatchEdit();
         // Don't allow cancellation of manual pick
-        mLastComposedWord.deactivate();
+        mInputLogic.mLastComposedWord.deactivate();
         // Space state must be updated before calling updateShiftState
-        mSpaceState = SPACE_STATE_PHANTOM;
+        mInputLogic.mSpaceState = SpaceState.PHANTOM;
         mKeyboardSwitcher.updateShiftState();
 
         // We should show the "Touch again to save" hint if the user pressed the first entry
         // AND it's in none of our current dictionaries (main, user or otherwise).
         // Please note that if mSuggest is null, it means that everything is off: suggestion
         // and correction, so we shouldn't try to show the hint
-        final Suggest suggest = mSuggest;
+        final Suggest suggest = mInputLogic.mSuggest;
         final boolean showingAddToDictionaryHint =
                 (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
                         || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
@@ -2756,10 +2033,11 @@
     /**
      * Commits the chosen word to the text field and saves it for later retrieval.
      */
-    private void commitChosenWord(final String chosenWord, final int commitType,
+    // TODO[IL]: Move to InputLogic and make public again
+    public void commitChosenWord(final String chosenWord, final int commitType,
             final String separatorString) {
-        final SuggestedWords suggestedWords = mSuggestedWords;
-        mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
+        final SuggestedWords suggestedWords = mInputLogic.mSuggestedWords;
+        mInputLogic.mConnection.commitText(SuggestionSpanUtils.getTextWithSuggestionSpan(
                 this, chosenWord, suggestedWords, mIsMainDictionaryAvailable), 1);
         // Add the word to the user history dictionary
         final String prevWord = addToUserHistoryDictionary(chosenWord);
@@ -2767,8 +2045,8 @@
         // what user typed. Note: currently this is done much later in
         // LastComposedWord#didCommitTypedWord by string equality of the remembered
         // strings.
-        mLastComposedWord = mWordComposer.commitWord(commitType, chosenWord, separatorString,
-                prevWord);
+        mInputLogic.mLastComposedWord = mInputLogic.mWordComposer.commitWord(commitType,
+                chosenWord, separatorString, prevWord);
         final boolean shouldDiscardPreviousWordForSuggestion;
         if (0 == StringUtils.codePointCount(separatorString)) {
             // Separator is 0-length. Discard the word only if the current language has spaces.
@@ -2780,11 +2058,12 @@
                     !StringUtils.containsOnlyWhitespace(separatorString);
         }
         if (shouldDiscardPreviousWordForSuggestion) {
-            mWordComposer.discardPreviousWordForSuggestion();
+            mInputLogic.mWordComposer.discardPreviousWordForSuggestion();
         }
     }
 
-    private void setPunctuationSuggestions() {
+    // TODO[IL]: Define a clean interface for this
+    public void setPunctuationSuggestions() {
         final SettingsValues currentSettings = mSettings.getCurrent();
         if (currentSettings.mBigramPredictionEnabled) {
             clearSuggestionStrip();
@@ -2797,7 +2076,7 @@
 
     private String addToUserHistoryDictionary(final String suggestion) {
         if (TextUtils.isEmpty(suggestion)) return null;
-        final Suggest suggest = mSuggest;
+        final Suggest suggest = mInputLogic.mSuggest;
         if (suggest == null) return null;
 
         // If correction is not enabled, we don't add words to the user history dictionary.
@@ -2809,9 +2088,10 @@
         final UserHistoryDictionary userHistoryDictionary = suggest.getUserHistoryDictionary();
         if (userHistoryDictionary == null) return null;
 
-        final String prevWord = mConnection.getNthPreviousWord(currentSettings, 2);
+        final String prevWord = mInputLogic.mConnection.getNthPreviousWord(currentSettings, 2);
         final String secondWord;
-        if (mWordComposer.wasAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+        if (mInputLogic.mWordComposer.wasAutoCapitalized()
+                && !mInputLogic.mWordComposer.isMostlyCaps()) {
             secondWord = suggestion.toLowerCase(mSubtypeSwitcher.getCurrentSubtypeLocale());
         } else {
             secondWord = suggestion;
@@ -2848,19 +2128,19 @@
         // how to segment them yet.
         if (!mSettings.getCurrent().mCurrentLanguageHasSpaces) return;
         // If the cursor is not touching a word, or if there is a selection, return right away.
-        if (mLastSelectionStart != mLastSelectionEnd) return;
+        if (mInputLogic.mLastSelectionStart != mInputLogic.mLastSelectionEnd) return;
         // If we don't know the cursor location, return.
-        if (mLastSelectionStart < 0) return;
+        if (mInputLogic.mLastSelectionStart < 0) return;
         final SettingsValues currentSettings = mSettings.getCurrent();
-        if (!mConnection.isCursorTouchingWord(currentSettings)) return;
-        final TextRange range = mConnection.getWordRangeAtCursor(currentSettings.mWordSeparators,
-                0 /* additionalPrecedingWordsCount */);
+        if (!mInputLogic.mConnection.isCursorTouchingWord(currentSettings)) return;
+        final TextRange range = mInputLogic.mConnection.getWordRangeAtCursor(
+                currentSettings.mWordSeparators, 0 /* additionalPrecedingWordsCount */);
         if (null == range) return; // Happens if we don't have an input connection at all
         if (range.length() <= 0) return; // Race condition. No text to resume on, so bail out.
         // If for some strange reason (editor bug or so) we measure the text before the cursor as
         // longer than what the entire text is supposed to be, the safe thing to do is bail out.
         final int numberOfCharsInWordBeforeCursor = range.getNumberOfCharsInWordBeforeCursor();
-        if (numberOfCharsInWordBeforeCursor > mLastSelectionStart) return;
+        if (numberOfCharsInWordBeforeCursor > mInputLogic.mLastSelectionStart) return;
         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         final String typedWord = range.mWord.toString();
         if (!isResumableWord(typedWord, currentSettings)) return;
@@ -2878,18 +2158,18 @@
                 }
             }
         }
-        mWordComposer.setComposingWord(typedWord,
-                getNthPreviousWordForSuggestion(currentSettings,
+        mInputLogic.mWordComposer.setComposingWord(typedWord,
+                mInputLogic.getNthPreviousWordForSuggestion(currentSettings,
                         // We want the previous word for suggestion. If we have chars in the word
                         // before the cursor, then we want the word before that, hence 2; otherwise,
                         // we want the word immediately before the cursor, hence 1.
                         0 == numberOfCharsInWordBeforeCursor ? 1 : 2),
                 mKeyboardSwitcher.getKeyboard());
-        mWordComposer.setCursorPositionWithinWord(
+        mInputLogic.mWordComposer.setCursorPositionWithinWord(
                 typedWord.codePointCount(0, numberOfCharsInWordBeforeCursor));
-        mConnection.setComposingRegion(
-                mLastSelectionStart - numberOfCharsInWordBeforeCursor,
-                mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
+        mInputLogic.mConnection.setComposingRegion(
+                mInputLogic.mLastSelectionStart - numberOfCharsInWordBeforeCursor,
+                mInputLogic.mLastSelectionEnd + range.getNumberOfCharsInWordAfterCursor());
         if (suggestions.isEmpty()) {
             // We come here if there weren't any suggestion spans on this word. We will try to
             // compute suggestions for it instead.
@@ -2938,42 +2218,11 @@
         // We also need to unset mIsAutoCorrectionIndicatorOn to avoid showSuggestionStrip touching
         // the text to adapt it.
         // TODO: remove mIsAutoCorrectionIndicatorOn (see comment on definition)
-        mIsAutoCorrectionIndicatorOn = false;
+        mInputLogic.mIsAutoCorrectionIndicatorOn = false;
         mHandler.showSuggestionStripWithTypedWord(suggestedWords, typedWord);
     }
 
     /**
-     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
-     * word, else do nothing.
-     */
-    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord() {
-        final CharSequence word =
-                mConnection.getWordBeforeCursorIfAtEndOfWord(mSettings.getCurrent());
-        if (null != word) {
-            final String wordString = word.toString();
-            restartSuggestionsOnWordBeforeCursor(wordString);
-            // TODO: Handle the case where the user manually moves the cursor and then backs up over
-            // a separator.  In that case, the current log unit should not be uncommitted.
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
-                        true /* dumpCurrentLogUnit */);
-            }
-        }
-    }
-
-    private void restartSuggestionsOnWordBeforeCursor(final String word) {
-        mWordComposer.setComposingWord(word,
-                // Previous word is the 2nd word before cursor because we are restarting on the
-                // 1st word before cursor.
-                getNthPreviousWordForSuggestion(mSettings.getCurrent(), 2 /* nthPreviousWord */),
-                mKeyboardSwitcher.getKeyboard());
-        final int length = word.length();
-        mConnection.deleteSurroundingText(length, 0);
-        mConnection.setComposingText(word, 1);
-        mHandler.postUpdateSuggestionStrip();
-    }
-
-    /**
      * Retry resetting caches in the rich input connection.
      *
      * When the editor can't be accessed we can't reset the caches, so we schedule a retry.
@@ -2984,8 +2233,8 @@
      * @param remainingTries How many times we may try again before giving up.
      */
     private void retryResetCaches(final boolean tryResumeSuggestions, final int remainingTries) {
-        if (!mConnection.resetCachesUponCursorMoveAndReturnSuccess(mLastSelectionStart,
-                    mLastSelectionEnd, false)) {
+        if (!mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
+                mInputLogic.mLastSelectionStart, mInputLogic.mLastSelectionEnd, false)) {
             if (0 < remainingTries) {
                 mHandler.postResetCaches(tryResumeSuggestions, remainingTries - 1);
                 return;
@@ -2998,74 +2247,6 @@
         if (tryResumeSuggestions) mHandler.postResumeSuggestions();
     }
 
-    private void revertCommit() {
-        final String previousWord = mLastComposedWord.mPrevWord;
-        final String originallyTypedWord = mLastComposedWord.mTypedWord;
-        final String committedWord = mLastComposedWord.mCommittedWord;
-        final int cancelLength = committedWord.length();
-        // We want java chars, not codepoints for the following.
-        final int separatorLength = mLastComposedWord.mSeparatorString.length();
-        // TODO: should we check our saved separator against the actual contents of the text view?
-        final int deleteLength = cancelLength + separatorLength;
-        if (DEBUG) {
-            if (mWordComposer.isComposingWord()) {
-                throw new RuntimeException("revertCommit, but we are composing a word");
-            }
-            final CharSequence wordBeforeCursor =
-                    mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength);
-            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
-                throw new RuntimeException("revertCommit check failed: we thought we were "
-                        + "reverting \"" + committedWord
-                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
-            }
-        }
-        mConnection.deleteSurroundingText(deleteLength, 0);
-        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
-            if (mSuggest != null) {
-                mSuggest.cancelAddingUserHistory(previousWord, committedWord);
-            }
-        }
-        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
-        if (mSettings.getCurrent().mCurrentLanguageHasSpaces) {
-            // For languages with spaces, we revert to the typed string, but the cursor is still
-            // after the separator so we don't resume suggestions. If the user wants to correct
-            // the word, they have to press backspace again.
-            mConnection.commitText(stringToCommit, 1);
-        } else {
-            // For languages without spaces, we revert the typed string but the cursor is flush
-            // with the typed word, so we need to resume suggestions right away.
-            mWordComposer.setComposingWord(stringToCommit, previousWord,
-                    mKeyboardSwitcher.getKeyboard());
-            mConnection.setComposingText(stringToCommit, 1);
-        }
-        if (mSettings.isInternal()) {
-            LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
-                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
-        }
-        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
-                    mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
-        }
-        // Don't restart suggestion yet. We'll restart if the user deletes the
-        // separator.
-        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
-        // We have a separator between the word and the cursor: we should show predictions.
-        mHandler.postUpdateSuggestionStrip();
-    }
-
-    // This essentially inserts a space, and that's it.
-    public void promotePhantomSpace() {
-        final SettingsValues currentSettings = mSettings.getCurrent();
-        if (currentSettings.shouldInsertSpacesAutomatically()
-                && currentSettings.mCurrentLanguageHasSpaces
-                && !mConnection.textBeforeCursorLooksLikeURL()) {
-            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-                ResearchLogger.latinIME_promotePhantomSpace();
-            }
-            sendKeyCodePoint(Constants.CODE_SPACE);
-        }
-    }
-
     // TODO: Make this private
     // Outside LatinIME, only used by the {@link InputTestsBase} test suite.
     @UsedForTesting
@@ -3090,7 +2271,7 @@
             return;
         }
         if (repeatCount > 0) {
-            if (code == Constants.CODE_DELETE && !mConnection.canDeleteCharacters()) {
+            if (code == Constants.CODE_DELETE && !mInputLogic.mConnection.canDeleteCharacters()) {
                 // No need to feedback when repeat delete key will have no effect.
                 return;
             }
@@ -3144,9 +2325,9 @@
         // onHardwareKeyEvent, like onKeyDown returns true if it handled the event, false if
         // it doesn't know what to do with it and leave it to the application. For example,
         // hardware key events for adjusting the screen's brightness are passed as is.
-        if (mEventInterpreter.onHardwareKeyEvent(event)) {
+        if (mInputLogic.mEventInterpreter.onHardwareKeyEvent(event)) {
             final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
-            mCurrentlyPressedHardwareKeys.add(keyIdentifier);
+            mInputLogic.mCurrentlyPressedHardwareKeys.add(keyIdentifier);
             return true;
         }
         return super.onKeyDown(keyCode, event);
@@ -3155,7 +2336,7 @@
     @Override
     public boolean onKeyUp(final int keyCode, final KeyEvent event) {
         final long keyIdentifier = event.getDeviceId() << 32 + event.getKeyCode();
-        if (mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
+        if (mInputLogic.mCurrentlyPressedHardwareKeys.remove(keyIdentifier)) {
             return true;
         }
         return super.onKeyUp(keyCode, event);
@@ -3187,7 +2368,7 @@
     public void launchKeyboardedDialogActivity(final Class<? extends Activity> activityClass) {
         // Put the text in the attached EditText into a safe, saved state before switching to a
         // new activity that will also use the soft keyboard.
-        commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+        mInputLogic.commitTyped(LastComposedWord.NOT_A_SEPARATOR);
         launchSubActivity(activityClass);
     }
 
@@ -3253,26 +2434,21 @@
 
     // TODO: can this be removed somehow without breaking the tests?
     @UsedForTesting
-    /* package for test */ String getFirstSuggestedWord() {
-        return mSuggestedWords.size() > 0 ? mSuggestedWords.getWord(0) : null;
+    /* package for test */ SuggestedWords getSuggestedWords() {
+        // You may not use this method for anything else than debug
+        return DEBUG ? mInputLogic.mSuggestedWords : null;
     }
 
     // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
     @UsedForTesting
     /* package for test */ boolean isCurrentlyWaitingForMainDictionary() {
-        return mSuggest.isCurrentlyWaitingForMainDictionary();
-    }
-
-    // DO NOT USE THIS for any other purpose than testing. This is information private to LatinIME.
-    @UsedForTesting
-    /* package for test */ boolean hasMainDictionary() {
-        return mSuggest.hasMainDictionary();
+        return mInputLogic.mSuggest.isCurrentlyWaitingForMainDictionary();
     }
 
     // DO NOT USE THIS for any other purpose than testing. This can break the keyboard badly.
     @UsedForTesting
     /* package for test */ void replaceMainDictionaryForTest(final Locale locale) {
-        mSuggest.resetMainDict(this, locale, null);
+        mInputLogic.mSuggest.resetMainDict(this, locale, null);
     }
 
     public void debugDumpStateAndCrashWithException(final String context) {
@@ -3297,7 +2473,7 @@
         p.println("  mIsSuggestionsRequested = "
                 + settingsValues.isSuggestionsRequested(mDisplayOrientation));
         p.println("  mCorrectionEnabled=" + settingsValues.mCorrectionEnabled);
-        p.println("  isComposingWord=" + mWordComposer.isComposingWord());
+        p.println("  isComposingWord=" + mInputLogic.mWordComposer.isComposingWord());
         p.println("  mSoundOn=" + settingsValues.mSoundOn);
         p.println("  mVibrateOn=" + settingsValues.mVibrateOn);
         p.println("  mKeyPreviewPopupOn=" + settingsValues.mKeyPreviewPopupOn);
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
new file mode 100644
index 0000000..2f92210
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -0,0 +1,1190 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.inputlogic;
+
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.KeyCharacterMap;
+import android.view.KeyEvent;
+import android.view.inputmethod.CorrectionInfo;
+import android.view.inputmethod.EditorInfo;
+
+import com.android.inputmethod.compat.SuggestionSpanUtils;
+import com.android.inputmethod.event.EventInterpreter;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LastComposedWord;
+import com.android.inputmethod.latin.LatinIME;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.RichInputConnection;
+import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.Suggest;
+import com.android.inputmethod.latin.SuggestedWords;
+import com.android.inputmethod.latin.WordComposer;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.latin.settings.Settings;
+import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.utils.InputTypeUtils;
+import com.android.inputmethod.latin.utils.LatinImeLoggerUtils;
+import com.android.inputmethod.latin.utils.RecapitalizeStatus;
+import com.android.inputmethod.latin.utils.StringUtils;
+import com.android.inputmethod.research.ResearchLogger;
+
+import java.util.TreeSet;
+
+/**
+ * This class manages the input logic.
+ */
+public final class InputLogic {
+    private static final String TAG = InputLogic.class.getSimpleName();
+
+    // TODO : Remove this member when we can.
+    private final LatinIME mLatinIME;
+
+    // TODO : make all these fields private as soon as possible.
+    // Current space state of the input method. This can be any of the above constants.
+    public int mSpaceState;
+    // Never null
+    public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
+    public Suggest mSuggest;
+    // The event interpreter should never be null.
+    public EventInterpreter mEventInterpreter;
+
+    public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+    public final WordComposer mWordComposer;
+    public final RichInputConnection mConnection;
+    public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
+
+    // Keep track of the last selection range to decide if we need to show word alternatives
+    public int mLastSelectionStart = Constants.NOT_A_CURSOR_POSITION;
+    public int mLastSelectionEnd = Constants.NOT_A_CURSOR_POSITION;
+
+    public int mDeleteCount;
+    private long mLastKeyTime;
+    public final TreeSet<Long> mCurrentlyPressedHardwareKeys = CollectionUtils.newTreeSet();
+
+    // Keeps track of most recently inserted text (multi-character key) for reverting
+    public String mEnteredText;
+
+    // TODO: This boolean is persistent state and causes large side effects at unexpected times.
+    // Find a way to remove it for readability.
+    public boolean mIsAutoCorrectionIndicatorOn;
+
+    public InputLogic(final LatinIME latinIME) {
+        mLatinIME = latinIME;
+        mWordComposer = new WordComposer();
+        mEventInterpreter = new EventInterpreter(latinIME);
+        mConnection = new RichInputConnection(latinIME);
+    }
+
+    /**
+     * Initializes the input logic for input in an editor.
+     *
+     * Call this when input starts or restarts in some editor (typically, in onStartInputView).
+     * If the input is starting in the same field as before, set `restarting' to true. This allows
+     * the input logic to reset only necessary stuff and save performance. Also, when restarting
+     * some things must not be done (for example, the keyboard should not be reset to the
+     * alphabetic layout), so do not send false to this just in case.
+     *
+     * @param restarting whether input is starting in the same field as before.
+     */
+    public void startInput(final boolean restarting) {
+    }
+
+    /**
+     * Clean up the input logic after input is finished.
+     */
+    public void finishInput() {
+    }
+
+    /**
+     * React to a code input. It may be a code point to insert, or a symbolic value that influences
+     * the keyboard behavior.
+     *
+     * Typically, this is called whenever a key is pressed on the software keyboard. This is not
+     * the entry point for gesture input; see the onBatchInput* family of functions for this.
+     *
+     * @param code the code to handle. It may be a code point, or an internal key code.
+     * @param x the x-coordinate where the user pressed the key, or NOT_A_COORDINATE.
+     * @param y the y-coordinate where the user pressed the key, or NOT_A_COORDINATE.
+     */
+    public void onCodeInput(final int code, final int x, final int y,
+            // TODO: remove these three arguments
+            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher,
+            final SubtypeSwitcher subtypeSwitcher) {
+        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+            ResearchLogger.latinIME_onCodeInput(code, x, y);
+        }
+        final SettingsValues settingsValues = Settings.getInstance().getCurrent();
+        final long when = SystemClock.uptimeMillis();
+        if (code != Constants.CODE_DELETE
+                || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
+            mDeleteCount = 0;
+        }
+        mLastKeyTime = when;
+        mConnection.beginBatchEdit();
+        final KeyboardSwitcher switcher = keyboardSwitcher;
+        // The space state depends only on the last character pressed and its own previous
+        // state. Here, we revert the space state to neutral if the key is actually modifying
+        // the input contents (any non-shift key), which is what we should do for
+        // all inputs that do not result in a special state. Each character handling is then
+        // free to override the state as they see fit.
+        final int spaceState = mSpaceState;
+        if (!mWordComposer.isComposingWord()) {
+            mIsAutoCorrectionIndicatorOn = false;
+        }
+
+        // TODO: Consolidate the double-space period timer, mLastKeyTime, and the space state.
+        if (code != Constants.CODE_SPACE) {
+            handler.cancelDoubleSpacePeriodTimer();
+        }
+
+        boolean didAutoCorrect = false;
+        switch (code) {
+        case Constants.CODE_DELETE:
+            handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher);
+            LatinImeLogger.logOnDelete(x, y);
+            break;
+        case Constants.CODE_SHIFT:
+            // Note: Calling back to the keyboard on Shift key is handled in
+            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+            final Keyboard currentKeyboard = switcher.getKeyboard();
+            if (null != currentKeyboard && currentKeyboard.mId.isAlphabetKeyboard()) {
+                // TODO: Instead of checking for alphabetic keyboard here, separate keycodes for
+                // alphabetic shift and shift while in symbol layout.
+                performRecapitalization(settingsValues, keyboardSwitcher);
+            }
+            break;
+        case Constants.CODE_CAPSLOCK:
+            // Note: Changing keyboard to shift lock state is handled in
+            // {@link KeyboardSwitcher#onCodeInput(int)}.
+            break;
+        case Constants.CODE_SWITCH_ALPHA_SYMBOL:
+            // Note: Calling back to the keyboard on symbol key is handled in
+            // {@link #onPressKey(int,int,boolean)} and {@link #onReleaseKey(int,boolean)}.
+            break;
+        case Constants.CODE_SETTINGS:
+            onSettingsKeyPressed();
+            break;
+        case Constants.CODE_SHORTCUT:
+            subtypeSwitcher.switchToShortcutIME(mLatinIME);
+            break;
+        case Constants.CODE_ACTION_NEXT:
+            performEditorAction(EditorInfo.IME_ACTION_NEXT);
+            break;
+        case Constants.CODE_ACTION_PREVIOUS:
+            performEditorAction(EditorInfo.IME_ACTION_PREVIOUS);
+            break;
+        case Constants.CODE_LANGUAGE_SWITCH:
+            handleLanguageSwitchKey();
+            break;
+        case Constants.CODE_EMOJI:
+            // Note: Switching emoji keyboard is being handled in
+            // {@link KeyboardState#onCodeInput(int,int)}.
+            break;
+        case Constants.CODE_ENTER:
+            final EditorInfo editorInfo = getCurrentInputEditorInfo();
+            final int imeOptionsActionId =
+                    InputTypeUtils.getImeOptionsActionIdFromEditorInfo(editorInfo);
+            if (InputTypeUtils.IME_ACTION_CUSTOM_LABEL == imeOptionsActionId) {
+                // Either we have an actionLabel and we should performEditorAction with actionId
+                // regardless of its value.
+                performEditorAction(editorInfo.actionId);
+            } else if (EditorInfo.IME_ACTION_NONE != imeOptionsActionId) {
+                // We didn't have an actionLabel, but we had another action to execute.
+                // EditorInfo.IME_ACTION_NONE explicitly means no action. In contrast,
+                // EditorInfo.IME_ACTION_UNSPECIFIED is the default value for an action, so it
+                // means there should be an action and the app didn't bother to set a specific
+                // code for it - presumably it only handles one. It does not have to be treated
+                // in any specific way: anything that is not IME_ACTION_NONE should be sent to
+                // performEditorAction.
+                performEditorAction(imeOptionsActionId);
+            } else {
+                // No action label, and the action from imeOptions is NONE: this is a regular
+                // enter key that should input a carriage return.
+                didAutoCorrect = handleNonSpecialCharacter(settingsValues,
+                        Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
+            }
+            break;
+        case Constants.CODE_SHIFT_ENTER:
+            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
+                    Constants.CODE_ENTER, x, y, spaceState, keyboardSwitcher, handler);
+            break;
+        default:
+            didAutoCorrect = handleNonSpecialCharacter(settingsValues,
+                    code, x, y, spaceState, keyboardSwitcher, handler);
+            break;
+        }
+        switcher.onCodeInput(code);
+        // Reset after any single keystroke, except shift, capslock, and symbol-shift
+        if (!didAutoCorrect && code != Constants.CODE_SHIFT
+                && code != Constants.CODE_CAPSLOCK
+                && code != Constants.CODE_SWITCH_ALPHA_SYMBOL)
+            mLastComposedWord.deactivate();
+        if (Constants.CODE_DELETE != code) {
+            mEnteredText = null;
+        }
+        mConnection.endBatchEdit();
+    }
+
+    /**
+     * Handle inputting a code point to the editor.
+     *
+     * Non-special keys are those that generate a single code point.
+     * This includes all letters, digits, punctuation, separators, emoji. It excludes keys that
+     * manage keyboard-related stuff like shift, language switch, settings, layout switch, or
+     * any key that results in multiple code points like the ".com" key.
+     *
+     * @param settingsValues The current settings values.
+     * @param codePoint the code point associated with the key.
+     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param spaceState the space state at start of the batch input.
+     * @return whether this caused an auto-correction to happen.
+     */
+    private boolean handleNonSpecialCharacter(final SettingsValues settingsValues,
+            final int codePoint, final int x, final int y, final int spaceState,
+            // TODO: remove these arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        mSpaceState = SpaceState.NONE;
+        final boolean didAutoCorrect;
+        if (settingsValues.isWordSeparator(codePoint)
+                || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
+            didAutoCorrect = handleSeparator(settingsValues, codePoint, x, y, spaceState,
+                    keyboardSwitcher, handler);
+        } else {
+            didAutoCorrect = false;
+            if (SpaceState.PHANTOM == spaceState) {
+                if (settingsValues.mIsInternal) {
+                    if (mWordComposer.isComposingWord() && mWordComposer.isBatchMode()) {
+                        LatinImeLoggerUtils.onAutoCorrection("", mWordComposer.getTypedWord(), " ",
+                                mWordComposer);
+                    }
+                }
+                if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+                    // If we are in the middle of a recorrection, we need to commit the recorrection
+                    // first so that we can insert the character at the current cursor position.
+                    resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+                } else {
+                    commitTyped(LastComposedWord.NOT_A_SEPARATOR);
+                }
+            }
+            final int keyX, keyY;
+            final Keyboard keyboard = keyboardSwitcher.getKeyboard();
+            if (keyboard != null && keyboard.hasProximityCharsCorrection(codePoint)) {
+                keyX = x;
+                keyY = y;
+            } else {
+                keyX = Constants.NOT_A_COORDINATE;
+                keyY = Constants.NOT_A_COORDINATE;
+            }
+            handleNonSeparator(settingsValues, codePoint, keyX, keyY, spaceState,
+                    keyboardSwitcher, handler);
+        }
+        return didAutoCorrect;
+    }
+
+    /**
+     * Handle a non-separator.
+     * @param settingsValues The current settings values.
+     * @param codePoint the code point associated with the key.
+     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param spaceState the space state at start of the batch input.
+     */
+    private void handleNonSeparator(final SettingsValues settingsValues,
+            final int codePoint, final int x, final int y, final int spaceState,
+            // TODO: Remove these arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        // TODO: refactor this method to stop flipping isComposingWord around all the time, and
+        // make it shorter (possibly cut into several pieces). Also factor handleNonSpecialCharacter
+        // which has the same name as other handle* methods but is not the same.
+        boolean isComposingWord = mWordComposer.isComposingWord();
+
+        // TODO: remove isWordConnector() and use isUsuallyFollowedBySpace() instead.
+        // See onStartBatchInput() to see how to do it.
+        if (SpaceState.PHANTOM == spaceState && !settingsValues.isWordConnector(codePoint)) {
+            if (isComposingWord) {
+                // Sanity check
+                throw new RuntimeException("Should not be composing here");
+            }
+            promotePhantomSpace(settingsValues);
+        }
+
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the character at the current cursor position.
+            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+            isComposingWord = false;
+        }
+        // We want to find out whether to start composing a new word with this character. If so,
+        // we need to reset the composing state and switch isComposingWord. The order of the
+        // tests is important for good performance.
+        // We only start composing if we're not already composing.
+        if (!isComposingWord
+        // We only start composing if this is a word code point. Essentially that means it's a
+        // a letter or a word connector.
+                && settingsValues.isWordCodePoint(codePoint)
+        // We never go into composing state if suggestions are not requested.
+                && settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation) &&
+        // In languages with spaces, we only start composing a word when we are not already
+        // touching a word. In languages without spaces, the above conditions are sufficient.
+                (!mConnection.isCursorTouchingWord(settingsValues)
+                        || !settingsValues.mCurrentLanguageHasSpaces)) {
+            // Reset entirely the composing state anyway, then start composing a new word unless
+            // the character is a single quote or a dash. The idea here is, single quote and dash
+            // are not separators and they should be treated as normal characters, except in the
+            // first position where they should not start composing a word.
+            isComposingWord = (Constants.CODE_SINGLE_QUOTE != codePoint
+                    && Constants.CODE_DASH != codePoint);
+            // Here we don't need to reset the last composed word. It will be reset
+            // when we commit this one, if we ever do; if on the other hand we backspace
+            // it entirely and resume suggestions on the previous word, we'd like to still
+            // have touch coordinates for it.
+            resetComposingState(false /* alsoResetLastComposedWord */);
+        }
+        if (isComposingWord) {
+            final MainKeyboardView mainKeyboardView = keyboardSwitcher.getMainKeyboardView();
+            // TODO: We should reconsider which coordinate system should be used to represent
+            // keyboard event.
+            final int keyX = mainKeyboardView.getKeyX(x);
+            final int keyY = mainKeyboardView.getKeyY(y);
+            mWordComposer.add(codePoint, keyX, keyY);
+            // If it's the first letter, make note of auto-caps state
+            if (mWordComposer.size() == 1) {
+                // We pass 1 to getPreviousWordForSuggestion because we were not composing a word
+                // yet, so the word we want is the 1st word before the cursor.
+                mWordComposer.setCapitalizedModeAndPreviousWordAtStartComposingTime(
+                        getActualCapsMode(keyboardSwitcher),
+                        getNthPreviousWordForSuggestion(settingsValues, 1 /* nthPreviousWord */));
+            }
+            mConnection.setComposingText(getTextWithUnderline(
+                    mWordComposer.getTypedWord()), 1);
+        } else {
+            final boolean swapWeakSpace = maybeStripSpace(settingsValues,
+                    codePoint, spaceState, Constants.SUGGESTION_STRIP_COORDINATE == x);
+
+            sendKeyCodePoint(codePoint);
+
+            if (swapWeakSpace) {
+                swapSwapperAndSpace(keyboardSwitcher);
+                mSpaceState = SpaceState.WEAK;
+            }
+            // In case the "add to dictionary" hint was still displayed.
+            mLatinIME.dismissAddToDictionaryHint();
+        }
+        handler.postUpdateSuggestionStrip();
+        if (settingsValues.mIsInternal) {
+            LatinImeLoggerUtils.onNonSeparator((char)codePoint, x, y);
+        }
+    }
+
+    /**
+     * Handle input of a separator code point.
+     * @param settingsValues The current settings values.
+     * @param codePoint the code point associated with the key.
+     * @param x the x-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param y the y-coordinate of the key press, or Contants.NOT_A_COORDINATE if not applicable.
+     * @param spaceState the space state at start of the batch input.
+     * @return whether this caused an auto-correction to happen.
+     */
+    private boolean handleSeparator(final SettingsValues settingsValues,
+            final int codePoint, final int x, final int y, final int spaceState,
+            // TODO: remove these arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        boolean didAutoCorrect = false;
+        // We avoid sending spaces in languages without spaces if we were composing.
+        final boolean shouldAvoidSendingCode = Constants.CODE_SPACE == codePoint
+                && !settingsValues.mCurrentLanguageHasSpaces
+                && mWordComposer.isComposingWord();
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can insert the separator at the current cursor position.
+            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+        }
+        // isComposingWord() may have changed since we stored wasComposing
+        if (mWordComposer.isComposingWord()) {
+            if (settingsValues.mCorrectionEnabled) {
+                final String separator = shouldAvoidSendingCode ? LastComposedWord.NOT_A_SEPARATOR
+                        : StringUtils.newSingleCodePointString(codePoint);
+                commitCurrentAutoCorrection(settingsValues, separator, handler);
+                didAutoCorrect = true;
+            } else {
+                commitTyped(StringUtils.newSingleCodePointString(codePoint));
+            }
+        }
+
+        final boolean swapWeakSpace = maybeStripSpace(settingsValues, codePoint, spaceState,
+                Constants.SUGGESTION_STRIP_COORDINATE == x);
+
+        if (SpaceState.PHANTOM == spaceState &&
+                settingsValues.isUsuallyPrecededBySpace(codePoint)) {
+            promotePhantomSpace(settingsValues);
+        }
+        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+            ResearchLogger.latinIME_handleSeparator(codePoint, mWordComposer.isComposingWord());
+        }
+
+        if (!shouldAvoidSendingCode) {
+            sendKeyCodePoint(codePoint);
+        }
+
+        if (Constants.CODE_SPACE == codePoint) {
+            if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation)) {
+                if (maybeDoubleSpacePeriod(settingsValues, keyboardSwitcher, handler)) {
+                    mSpaceState = SpaceState.DOUBLE;
+                } else if (!mLatinIME.isShowingPunctuationList()) {
+                    mSpaceState = SpaceState.WEAK;
+                }
+            }
+
+            handler.startDoubleSpacePeriodTimer();
+            handler.postUpdateSuggestionStrip();
+        } else {
+            if (swapWeakSpace) {
+                swapSwapperAndSpace(keyboardSwitcher);
+                mSpaceState = SpaceState.SWAP_PUNCTUATION;
+            } else if (SpaceState.PHANTOM == spaceState
+                    && settingsValues.isUsuallyFollowedBySpace(codePoint)) {
+                // If we are in phantom space state, and the user presses a separator, we want to
+                // stay in phantom space state so that the next keypress has a chance to add the
+                // space. For example, if I type "Good dat", pick "day" from the suggestion strip
+                // then insert a comma and go on to typing the next word, I want the space to be
+                // inserted automatically before the next word, the same way it is when I don't
+                // input the comma.
+                // The case is a little different if the separator is a space stripper. Such a
+                // separator does not normally need a space on the right (that's the difference
+                // between swappers and strippers), so we should not stay in phantom space state if
+                // the separator is a stripper. Hence the additional test above.
+                mSpaceState = SpaceState.PHANTOM;
+            }
+
+            // Set punctuation right away. onUpdateSelection will fire but tests whether it is
+            // already displayed or not, so it's okay.
+            mLatinIME.setPunctuationSuggestions();
+        }
+        if (settingsValues.mIsInternal) {
+            LatinImeLoggerUtils.onSeparator((char)codePoint, x, y);
+        }
+
+        keyboardSwitcher.updateShiftState();
+        return didAutoCorrect;
+    }
+
+    /**
+     * Handle a press on the backspace key.
+     * @param settingsValues The current settings values.
+     * @param spaceState The space state at start of this batch edit.
+     */
+    private void handleBackspace(final SettingsValues settingsValues, final int spaceState,
+            // TODO: remove these arguments
+            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
+        mSpaceState = SpaceState.NONE;
+        mDeleteCount++;
+
+        // In many cases, we may have to put the keyboard in auto-shift state again. However
+        // we want to wait a few milliseconds before doing it to avoid the keyboard flashing
+        // during key repeat.
+        handler.postUpdateShiftState();
+
+        if (mWordComposer.isCursorFrontOrMiddleOfComposingWord()) {
+            // If we are in the middle of a recorrection, we need to commit the recorrection
+            // first so that we can remove the character at the current cursor position.
+            resetEntireInputState(settingsValues, mLastSelectionStart, mLastSelectionEnd);
+            // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
+        }
+        if (mWordComposer.isComposingWord()) {
+            if (mWordComposer.isBatchMode()) {
+                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                    final String word = mWordComposer.getTypedWord();
+                    ResearchLogger.latinIME_handleBackspace_batch(word, 1);
+                }
+                final String rejectedSuggestion = mWordComposer.getTypedWord();
+                mWordComposer.reset();
+                mWordComposer.setRejectedBatchModeSuggestion(rejectedSuggestion);
+            } else {
+                mWordComposer.deleteLast();
+            }
+            mConnection.setComposingText(getTextWithUnderline(mWordComposer.getTypedWord()), 1);
+            handler.postUpdateSuggestionStrip();
+            if (!mWordComposer.isComposingWord()) {
+                // If we just removed the last character, auto-caps mode may have changed so we
+                // need to re-evaluate.
+                keyboardSwitcher.updateShiftState();
+            }
+        } else {
+            if (mLastComposedWord.canRevertCommit()) {
+                if (settingsValues.mIsInternal) {
+                    LatinImeLoggerUtils.onAutoCorrectionCancellation();
+                }
+                revertCommit(settingsValues, keyboardSwitcher, handler);
+                return;
+            }
+            if (mEnteredText != null && mConnection.sameAsTextBeforeCursor(mEnteredText)) {
+                // Cancel multi-character input: remove the text we just entered.
+                // This is triggered on backspace after a key that inputs multiple characters,
+                // like the smiley key or the .com key.
+                mConnection.deleteSurroundingText(mEnteredText.length(), 0);
+                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                    ResearchLogger.latinIME_handleBackspace_cancelTextInput(mEnteredText);
+                }
+                mEnteredText = null;
+                // If we have mEnteredText, then we know that mHasUncommittedTypedChars == false.
+                // In addition we know that spaceState is false, and that we should not be
+                // reverting any autocorrect at this point. So we can safely return.
+                return;
+            }
+            if (SpaceState.DOUBLE == spaceState) {
+                handler.cancelDoubleSpacePeriodTimer();
+                if (mConnection.revertDoubleSpacePeriod()) {
+                    // No need to reset mSpaceState, it has already be done (that's why we
+                    // receive it as a parameter)
+                    return;
+                }
+            } else if (SpaceState.SWAP_PUNCTUATION == spaceState) {
+                if (mConnection.revertSwapPunctuation()) {
+                    // Likewise
+                    return;
+                }
+            }
+
+            // No cancelling of commit/double space/swap: we have a regular backspace.
+            // We should backspace one char and restart suggestion if at the end of a word.
+            if (mLastSelectionStart != mLastSelectionEnd) {
+                // If there is a selection, remove it.
+                final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+                mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+                // Reset mLastSelectionEnd to mLastSelectionStart. This is what is supposed to
+                // happen, and if it's wrong, the next call to onUpdateSelection will correct it,
+                // but we want to set it right away to avoid it being used with the wrong values
+                // later (typically, in a subsequent press on backspace).
+                mLastSelectionEnd = mLastSelectionStart;
+                mConnection.deleteSurroundingText(numCharsDeleted, 0);
+                if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                    ResearchLogger.latinIME_handleBackspace(numCharsDeleted,
+                            false /* shouldUncommitLogUnit */);
+                }
+            } else {
+                // There is no selection, just delete one character.
+                if (Constants.NOT_A_CURSOR_POSITION == mLastSelectionEnd) {
+                    // This should never happen.
+                    Log.e(TAG, "Backspace when we don't know the selection position");
+                }
+                if (mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean() ||
+                        settingsValues.mInputAttributes.isTypeNull()) {
+                    // There are two possible reasons to send a key event: either the field has
+                    // type TYPE_NULL, in which case the keyboard should send events, or we are
+                    // running in backward compatibility mode. Before Jelly bean, the keyboard
+                    // would simulate a hardware keyboard event on pressing enter or delete. This
+                    // is bad for many reasons (there are race conditions with commits) but some
+                    // applications are relying on this behavior so we continue to support it for
+                    // older apps, so we retain this behavior if the app has target SDK < JellyBean.
+                    sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
+                        sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
+                    }
+                } else {
+                    final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
+                    if (codePointBeforeCursor == Constants.NOT_A_CODE) {
+                        // Nothing to delete before the cursor.
+                        return;
+                    }
+                    final int lengthToDelete =
+                            Character.isSupplementaryCodePoint(codePointBeforeCursor) ? 2 : 1;
+                    mConnection.deleteSurroundingText(lengthToDelete, 0);
+                    if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                        ResearchLogger.latinIME_handleBackspace(lengthToDelete,
+                                true /* shouldUncommitLogUnit */);
+                    }
+                    if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
+                        final int codePointBeforeCursorToDeleteAgain =
+                                mConnection.getCodePointBeforeCursor();
+                        if (codePointBeforeCursorToDeleteAgain != Constants.NOT_A_CODE) {
+                            final int lengthToDeleteAgain = Character.isSupplementaryCodePoint(
+                                    codePointBeforeCursorToDeleteAgain) ? 2 : 1;
+                            mConnection.deleteSurroundingText(lengthToDeleteAgain, 0);
+                            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                                ResearchLogger.latinIME_handleBackspace(lengthToDeleteAgain,
+                                        true /* shouldUncommitLogUnit */);
+                            }
+                        }
+                    }
+                }
+            }
+            // TODO: move mDisplayOrientation to CurrentSettings.
+            if (settingsValues.isSuggestionsRequested(mLatinIME.mDisplayOrientation)
+                    && settingsValues.mCurrentLanguageHasSpaces) {
+                restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(settingsValues, keyboardSwitcher,
+                        handler);
+            }
+            // We just removed a character. We need to update the auto-caps state.
+            keyboardSwitcher.updateShiftState();
+        }
+    }
+
+    /**
+     * Handle a press on the language switch key (the "globe key")
+     */
+    private void handleLanguageSwitchKey() {
+        mLatinIME.handleLanguageSwitchKey();
+    }
+
+    /**
+     * Swap a space with a space-swapping punctuation sign.
+     *
+     * This method will check that there are two characters before the cursor and that the first
+     * one is a space before it does the actual swapping.
+     */
+    // TODO: Remove this argument
+    private void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) {
+        final CharSequence lastTwo = mConnection.getTextBeforeCursor(2, 0);
+        // It is guaranteed lastTwo.charAt(1) is a swapper - else this method is not called.
+        if (lastTwo != null && lastTwo.length() == 2 && lastTwo.charAt(0) == Constants.CODE_SPACE) {
+            mConnection.deleteSurroundingText(2, 0);
+            final String text = lastTwo.charAt(1) + " ";
+            mConnection.commitText(text, 1);
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
+            }
+            keyboardSwitcher.updateShiftState();
+        }
+    }
+
+    /*
+     * Strip a trailing space if necessary and returns whether it's a swap weak space situation.
+     * @param settingsValues The current settings values.
+     * @param codePoint The code point that is about to be inserted.
+     * @param spaceState The space state at start of this batch edit.
+     * @param isFromSuggestionStrip Whether this code point is coming from the suggestion strip.
+     * @return whether we should swap the space instead of removing it.
+     */
+    private boolean maybeStripSpace(final SettingsValues settingsValues,
+            final int code, final int spaceState, final boolean isFromSuggestionStrip) {
+        if (Constants.CODE_ENTER == code && SpaceState.SWAP_PUNCTUATION == spaceState) {
+            mConnection.removeTrailingSpace();
+            return false;
+        }
+        if ((SpaceState.WEAK == spaceState || SpaceState.SWAP_PUNCTUATION == spaceState)
+                && isFromSuggestionStrip) {
+            if (settingsValues.isUsuallyPrecededBySpace(code)) return false;
+            if (settingsValues.isUsuallyFollowedBySpace(code)) return true;
+            mConnection.removeTrailingSpace();
+        }
+        return false;
+    }
+
+    /**
+     * Apply the double-space-to-period transformation if applicable.
+     *
+     * The double-space-to-period transformation means that we replace two spaces with a
+     * period-space sequence of characters. This typically happens when the user presses space
+     * twice in a row quickly.
+     * This method will check that the double-space-to-period is active in settings, that the
+     * two spaces have been input close enough together, and that the previous character allows
+     * for the transformation to take place. If all of these conditions are fulfilled, this
+     * method applies the transformation and returns true. Otherwise, it does nothing and
+     * returns false.
+     *
+     * @param settingsValues the current values of the settings.
+     * @return true if we applied the double-space-to-period transformation, false otherwise.
+     */
+    private boolean maybeDoubleSpacePeriod(final SettingsValues settingsValues,
+            // TODO: remove these arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        if (!settingsValues.mUseDoubleSpacePeriod) return false;
+        if (!handler.isAcceptingDoubleSpacePeriod()) return false;
+        // We only do this when we see two spaces and an accepted code point before the cursor.
+        // The code point may be a surrogate pair but the two spaces may not, so we need 4 chars.
+        final CharSequence lastThree = mConnection.getTextBeforeCursor(4, 0);
+        if (null == lastThree) return false;
+        final int length = lastThree.length();
+        if (length < 3) return false;
+        if (lastThree.charAt(length - 1) != Constants.CODE_SPACE) return false;
+        if (lastThree.charAt(length - 2) != Constants.CODE_SPACE) return false;
+        // We know there are spaces in pos -1 and -2, and we have at least three chars.
+        // If we have only three chars, isSurrogatePairs can't return true as charAt(1) is a space,
+        // so this is fine.
+        final int firstCodePoint =
+                Character.isSurrogatePair(lastThree.charAt(0), lastThree.charAt(1)) ?
+                        Character.codePointAt(lastThree, 0) : lastThree.charAt(length - 3);
+        if (canBeFollowedByDoubleSpacePeriod(firstCodePoint)) {
+            handler.cancelDoubleSpacePeriodTimer();
+            mConnection.deleteSurroundingText(2, 0);
+            final String textToInsert = new String(
+                    new int[] { settingsValues.mSentenceSeparator, Constants.CODE_SPACE }, 0, 2);
+            mConnection.commitText(textToInsert, 1);
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                ResearchLogger.latinIME_maybeDoubleSpacePeriod(textToInsert,
+                        false /* isBatchMode */);
+            }
+            mWordComposer.discardPreviousWordForSuggestion();
+            keyboardSwitcher.updateShiftState();
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * Returns whether this code point can be followed by the double-space-to-period transformation.
+     *
+     * See #maybeDoubleSpaceToPeriod for details.
+     * Generally, most word characters can be followed by the double-space-to-period transformation,
+     * while most punctuation can't. Some punctuation however does allow for this to take place
+     * after them, like the closing parenthesis for example.
+     *
+     * @param codePoint the code point after which we may want to apply the transformation
+     * @return whether it's fine to apply the transformation after this code point.
+     */
+    private static boolean canBeFollowedByDoubleSpacePeriod(final int codePoint) {
+        // TODO: This should probably be a blacklist rather than a whitelist.
+        // TODO: This should probably be language-dependant...
+        return Character.isLetterOrDigit(codePoint)
+                || codePoint == Constants.CODE_SINGLE_QUOTE
+                || codePoint == Constants.CODE_DOUBLE_QUOTE
+                || codePoint == Constants.CODE_CLOSING_PARENTHESIS
+                || codePoint == Constants.CODE_CLOSING_SQUARE_BRACKET
+                || codePoint == Constants.CODE_CLOSING_CURLY_BRACKET
+                || codePoint == Constants.CODE_CLOSING_ANGLE_BRACKET
+                || codePoint == Constants.CODE_PLUS
+                || codePoint == Constants.CODE_PERCENT
+                || Character.getType(codePoint) == Character.OTHER_SYMBOL;
+    }
+
+    /**
+     * Performs a recapitalization event.
+     * @param settingsValues The current settings values.
+     */
+    private void performRecapitalization(final SettingsValues settingsValues,
+            // TODO: remove this argument.
+            final KeyboardSwitcher keyboardSwitcher) {
+        if (mLastSelectionStart == mLastSelectionEnd) {
+            return; // No selection
+        }
+        // If we have a recapitalize in progress, use it; otherwise, create a new one.
+        if (!mRecapitalizeStatus.isActive()
+                || !mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+            final CharSequence selectedText =
+                    mConnection.getSelectedText(0 /* flags, 0 for no styles */);
+            if (TextUtils.isEmpty(selectedText)) return; // Race condition with the input connection
+            mRecapitalizeStatus.initialize(mLastSelectionStart, mLastSelectionEnd,
+                    selectedText.toString(),
+                    settingsValues.mLocale, settingsValues.mWordSeparators);
+            // We trim leading and trailing whitespace.
+            mRecapitalizeStatus.trim();
+            // Trimming the object may have changed the length of the string, and we need to
+            // reposition the selection handles accordingly. As this result in an IPC call,
+            // only do it if it's actually necessary, in other words if the recapitalize status
+            // is not set at the same place as before.
+            if (!mRecapitalizeStatus.isSetAt(mLastSelectionStart, mLastSelectionEnd)) {
+                mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+                mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+            }
+        }
+        mConnection.finishComposingText();
+        mRecapitalizeStatus.rotate();
+        final int numCharsDeleted = mLastSelectionEnd - mLastSelectionStart;
+        mConnection.setSelection(mLastSelectionEnd, mLastSelectionEnd);
+        mConnection.deleteSurroundingText(numCharsDeleted, 0);
+        mConnection.commitText(mRecapitalizeStatus.getRecapitalizedString(), 0);
+        mLastSelectionStart = mRecapitalizeStatus.getNewCursorStart();
+        mLastSelectionEnd = mRecapitalizeStatus.getNewCursorEnd();
+        mConnection.setSelection(mLastSelectionStart, mLastSelectionEnd);
+        // Match the keyboard to the new state.
+        keyboardSwitcher.updateShiftState();
+    }
+
+    /**
+     * Check if the cursor is actually at the end of a word. If so, restart suggestions on this
+     * word, otherwise do nothing.
+     * @param settingsValues the current values of the settings.
+     */
+    private void restartSuggestionsOnWordBeforeCursorIfAtEndOfWord(
+            final SettingsValues settingsValues,
+            // TODO: remove these two arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        final CharSequence word = mConnection.getWordBeforeCursorIfAtEndOfWord(settingsValues);
+        if (null != word) {
+            final String wordString = word.toString();
+            mWordComposer.setComposingWord(word,
+                    // Previous word is the 2nd word before cursor because we are restarting on the
+                    // 1st word before cursor.
+                    getNthPreviousWordForSuggestion(settingsValues, 2 /* nthPreviousWord */),
+                    keyboardSwitcher.getKeyboard());
+            final int length = word.length();
+            mConnection.deleteSurroundingText(length, 0);
+            mConnection.setComposingText(word, 1);
+            handler.postUpdateSuggestionStrip();
+            // TODO: Handle the case where the user manually moves the cursor and then backs up over
+            // a separator. In that case, the current log unit should not be uncommitted.
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                ResearchLogger.getInstance().uncommitCurrentLogUnit(wordString,
+                        true /* dumpCurrentLogUnit */);
+            }
+        }
+    }
+
+    /**
+     * Reverts a previous commit with auto-correction.
+     *
+     * This is triggered upon pressing backspace just after a commit with auto-correction.
+     *
+     * @param settingsValues the current settings values.
+     */
+    private void revertCommit(final SettingsValues settingsValues,
+            // TODO: remove these arguments
+            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+        final String previousWord = mLastComposedWord.mPrevWord;
+        final String originallyTypedWord = mLastComposedWord.mTypedWord;
+        final String committedWord = mLastComposedWord.mCommittedWord;
+        final int cancelLength = committedWord.length();
+        // We want java chars, not codepoints for the following.
+        final int separatorLength = mLastComposedWord.mSeparatorString.length();
+        // TODO: should we check our saved separator against the actual contents of the text view?
+        final int deleteLength = cancelLength + separatorLength;
+        if (LatinIME.DEBUG) {
+            if (mWordComposer.isComposingWord()) {
+                throw new RuntimeException("revertCommit, but we are composing a word");
+            }
+            final CharSequence wordBeforeCursor =
+                    mConnection.getTextBeforeCursor(deleteLength, 0).subSequence(0, cancelLength);
+            if (!TextUtils.equals(committedWord, wordBeforeCursor)) {
+                throw new RuntimeException("revertCommit check failed: we thought we were "
+                        + "reverting \"" + committedWord
+                        + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
+            }
+        }
+        mConnection.deleteSurroundingText(deleteLength, 0);
+        if (!TextUtils.isEmpty(previousWord) && !TextUtils.isEmpty(committedWord)) {
+            if (mSuggest != null) {
+                mSuggest.cancelAddingUserHistory(previousWord, committedWord);
+            }
+        }
+        final String stringToCommit = originallyTypedWord + mLastComposedWord.mSeparatorString;
+        if (settingsValues.mCurrentLanguageHasSpaces) {
+            // For languages with spaces, we revert to the typed string, but the cursor is still
+            // after the separator so we don't resume suggestions. If the user wants to correct
+            // the word, they have to press backspace again.
+            mConnection.commitText(stringToCommit, 1);
+        } else {
+            // For languages without spaces, we revert the typed string but the cursor is flush
+            // with the typed word, so we need to resume suggestions right away.
+            mWordComposer.setComposingWord(stringToCommit, previousWord,
+                    keyboardSwitcher.getKeyboard());
+            mConnection.setComposingText(stringToCommit, 1);
+        }
+        if (settingsValues.mIsInternal) {
+            LatinImeLoggerUtils.onSeparator(mLastComposedWord.mSeparatorString,
+                    Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
+        }
+        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+            ResearchLogger.latinIME_revertCommit(committedWord, originallyTypedWord,
+                    mWordComposer.isBatchMode(), mLastComposedWord.mSeparatorString);
+        }
+        // Don't restart suggestion yet. We'll restart if the user deletes the
+        // separator.
+        mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+        // We have a separator between the word and the cursor: we should show predictions.
+        handler.postUpdateSuggestionStrip();
+    }
+
+    /**
+     * Factor in auto-caps and manual caps and compute the current caps mode.
+     * @param keyboardSwitcher the keyboard switcher. Caps mode depends on its mode.
+     * @return the actual caps mode the keyboard is in right now.
+     */
+    // TODO: Make this private
+    public int getActualCapsMode(final KeyboardSwitcher keyboardSwitcher) {
+        final int keyboardShiftMode = keyboardSwitcher.getKeyboardShiftMode();
+        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
+        final int auto = mLatinIME.getCurrentAutoCapsState();
+        if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
+            return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
+        }
+        if (0 != auto) {
+            return WordComposer.CAPS_MODE_AUTO_SHIFTED;
+        }
+        return WordComposer.CAPS_MODE_OFF;
+    }
+
+    /**
+     * @return the editor info for the current editor
+     */
+    private EditorInfo getCurrentInputEditorInfo() {
+        return mLatinIME.getCurrentInputEditorInfo();
+    }
+
+    /**
+     * Get the nth previous word before the cursor as context for the suggestion process.
+     * @param currentSettings the current settings values.
+     * @param nthPreviousWord reverse index of the word to get (1-indexed)
+     * @return the nth previous word before the cursor.
+     */
+    // TODO: Make this private
+    public String getNthPreviousWordForSuggestion(final SettingsValues currentSettings,
+            final int nthPreviousWord) {
+        if (currentSettings.mCurrentLanguageHasSpaces) {
+            // If we are typing in a language with spaces we can just look up the previous
+            // word from textview.
+            return mConnection.getNthPreviousWord(currentSettings, nthPreviousWord);
+        } else {
+            return LastComposedWord.NOT_A_COMPOSED_WORD == mLastComposedWord ? null
+                    : mLastComposedWord.mCommittedWord;
+        }
+    }
+
+    /**
+     * @param actionId the action to perform
+     */
+    private void performEditorAction(final int actionId) {
+        mConnection.performEditorAction(actionId);
+    }
+
+    /**
+     * Handle a press on the settings key.
+     */
+    private void onSettingsKeyPressed() {
+        mLatinIME.onSettingsKeyPressed();
+    }
+
+    /**
+     * Resets the whole input state to the starting state.
+     *
+     * This will clear the composing word, reset the last composed word, clear the suggestion
+     * strip and tell the input connection about it so that it can refresh its caches.
+     *
+     * @param settingsValues the current values of the settings.
+     * @param newSelStart the new selection start, in java characters.
+     * @param newSelEnd the new selection end, in java characters.
+     */
+    // TODO: how is this different from startInput ?!
+    // TODO: remove all references to this in LatinIME and make this private
+    public void resetEntireInputState(final SettingsValues settingsValues,
+            final int newSelStart, final int newSelEnd) {
+        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
+        resetComposingState(true /* alsoResetLastComposedWord */);
+        if (settingsValues.mBigramPredictionEnabled) {
+            mLatinIME.clearSuggestionStrip();
+        } else {
+            mLatinIME.setSuggestedWords(settingsValues.mSuggestPuncList, false);
+        }
+        mConnection.resetCachesUponCursorMoveAndReturnSuccess(newSelStart, newSelEnd,
+                shouldFinishComposition);
+    }
+
+    /**
+     * Resets only the composing state.
+     *
+     * Compare #resetEntireInputState, which also clears the suggestion strip and resets the
+     * input connection caches. This only deals with the composing state.
+     *
+     * @param alsoResetLastComposedWord whether to also reset the last composed word.
+     */
+    // TODO: remove all references to this in LatinIME and make this private.
+    public void resetComposingState(final boolean alsoResetLastComposedWord) {
+        mWordComposer.reset();
+        if (alsoResetLastComposedWord) {
+            mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
+        }
+    }
+
+    /**
+     * Gets a chunk of text with or the auto-correction indicator underline span as appropriate.
+     *
+     * This method looks at the old state of the auto-correction indicator to put or not put
+     * the underline span as appropriate. It is important to note that this does not correspond
+     * exactly to whether this word will be auto-corrected to or not: what's important here is
+     * to keep the same indication as before.
+     * When we add a new code point to a composing word, we don't know yet if we are going to
+     * auto-correct it until the suggestions are computed. But in the mean time, we still need
+     * to display the character and to extend the previous underline. To avoid any flickering,
+     * the underline should keep the same color it used to have, even if that's not ultimately
+     * the correct color for this new word. When the suggestions are finished evaluating, we
+     * will call this method again to fix the color of the underline.
+     *
+     * @param text the text on which to maybe apply the span.
+     * @return the same text, with the auto-correction underline span if that's appropriate.
+     */
+    // TODO: remove all references to this in LatinIME and make this private. Also, shouldn't
+    // this go in some *Utils class instead?
+    public CharSequence getTextWithUnderline(final String text) {
+        return mIsAutoCorrectionIndicatorOn
+                ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
+                : text;
+    }
+
+    /**
+     * Sends a DOWN key event followed by an UP key event to the editor.
+     *
+     * If possible at all, avoid using this method. It causes all sorts of race conditions with
+     * the text view because it goes through a different, asynchronous binder. Also, batch edits
+     * are ignored for key events. Use the normal software input methods instead.
+     *
+     * @param keyCode the key code to send inside the key event.
+     */
+    private void sendDownUpKeyEvent(final int keyCode) {
+        final long eventTime = SystemClock.uptimeMillis();
+        mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
+                KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+        mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
+                KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
+                KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
+    }
+
+    /**
+     * Sends a code point to the editor, using the most appropriate method.
+     *
+     * Normally we send code points with commitText, but there are some cases (where backward
+     * compatibility is a concern for example) where we want to use deprecated methods.
+     *
+     * @param codePoint the code point to send.
+     */
+    private void sendKeyCodePoint(final int codePoint) {
+        if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+            ResearchLogger.latinIME_sendKeyCodePoint(codePoint);
+        }
+        // TODO: Remove this special handling of digit letters.
+        // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
+        if (codePoint >= '0' && codePoint <= '9') {
+            sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
+            return;
+        }
+
+        // TODO: we should do this also when the editor has TYPE_NULL
+        if (Constants.CODE_ENTER == codePoint
+                && mLatinIME.mAppWorkAroundsUtils.isBeforeJellyBean()) {
+            // Backward compatibility mode. Before Jelly bean, the keyboard would simulate
+            // a hardware keyboard event on pressing enter or delete. This is bad for many
+            // reasons (there are race conditions with commits) but some applications are
+            // relying on this behavior so we continue to support it for older apps.
+            sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
+        } else {
+            mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
+        }
+    }
+
+    /**
+     * Promote a phantom space to an actual space.
+     *
+     * This essentially inserts a space, and that's it. It just checks the options and the text
+     * before the cursor are appropriate before doing it.
+     *
+     * @param settingsValues the current values of the settings.
+     */
+    // TODO: Make this private.
+    public void promotePhantomSpace(final SettingsValues settingsValues) {
+        if (settingsValues.shouldInsertSpacesAutomatically()
+                && settingsValues.mCurrentLanguageHasSpaces
+                && !mConnection.textBeforeCursorLooksLikeURL()) {
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                ResearchLogger.latinIME_promotePhantomSpace();
+            }
+            sendKeyCodePoint(Constants.CODE_SPACE);
+        }
+    }
+
+    /**
+     * Commit the typed string to the editor.
+     *
+     * This is typically called when we should commit the currently composing word without applying
+     * auto-correction to it. Typically, we come here upon pressing a separator when the keyboard
+     * is configured to not do auto-correction at all (because of the settings or the properties of
+     * the editor). In this case, `separatorString' is set to the separator that was pressed.
+     * We also come here in a variety of cases with external user action. For example, when the
+     * cursor is moved while there is a composition, or when the keyboard is closed, or when the
+     * user presses the Send button for an SMS, we don't auto-correct as that would be unexpected.
+     * In this case, `separatorString' is set to NOT_A_SEPARATOR.
+     *
+     * @param separatorString the separator string that's causing the commit, or NOT_A_SEPARATOR if
+     *   none.
+     */
+    // TODO: Make this private
+    public void commitTyped(final String separatorString) {
+        if (!mWordComposer.isComposingWord()) return;
+        final String typedWord = mWordComposer.getTypedWord();
+        if (typedWord.length() > 0) {
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                ResearchLogger.getInstance().onWordFinished(typedWord, mWordComposer.isBatchMode());
+            }
+            mLatinIME.commitChosenWord(typedWord, LastComposedWord.COMMIT_TYPE_USER_TYPED_WORD,
+                    separatorString);
+        }
+    }
+
+    /**
+     * Commit the current auto-correction.
+     *
+     * This will commit the best guess of the keyboard regarding what the user meant by typing
+     * the currently composing word. The IME computes suggestions and assigns a confidence score
+     * to each of them; when it's confident enough in one suggestion, it replaces the typed string
+     * by this suggestion at commit time. When it's not confident enough, or when it has no
+     * suggestions, or when the settings or environment does not allow for auto-correction, then
+     * this method just commits the typed string.
+     * Note that if suggestions are currently being computed in the background, this method will
+     * block until the computation returns. This is necessary for consistency (it would be very
+     * strange if pressing space would commit a different word depending on how fast you press).
+     *
+     * @param settingsValues the current value of the settings.
+     * @param separator the separator that's causing the commit to happen.
+     */
+    // TODO: Make this private
+    public void commitCurrentAutoCorrection(final SettingsValues settingsValues,
+            final String separator,
+            // TODO: Remove this argument.
+            final LatinIME.UIHandler handler) {
+        // Complete any pending suggestions query first
+        if (handler.hasPendingUpdateSuggestions()) {
+            mLatinIME.updateSuggestionStrip();
+        }
+        final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
+        final String typedWord = mWordComposer.getTypedWord();
+        final String autoCorrection = (typedAutoCorrection != null)
+                ? typedAutoCorrection : typedWord;
+        if (autoCorrection != null) {
+            if (TextUtils.isEmpty(typedWord)) {
+                throw new RuntimeException("We have an auto-correction but the typed word "
+                        + "is empty? Impossible! I must commit suicide.");
+            }
+            if (settingsValues.mIsInternal) {
+                LatinImeLoggerUtils.onAutoCorrection(
+                        typedWord, autoCorrection, separator, mWordComposer);
+            }
+            if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
+                final SuggestedWords suggestedWords = mSuggestedWords;
+                ResearchLogger.latinIme_commitCurrentAutoCorrection(typedWord, autoCorrection,
+                        separator, mWordComposer.isBatchMode(), suggestedWords);
+            }
+            mLatinIME.commitChosenWord(autoCorrection, LastComposedWord.COMMIT_TYPE_DECIDED_WORD,
+                    separator);
+            if (!typedWord.equals(autoCorrection)) {
+                // This will make the correction flash for a short while as a visual clue
+                // to the user that auto-correction happened. It has no other effect; in particular
+                // note that this won't affect the text inside the text field AT ALL: it only makes
+                // the segment of text starting at the supplied index and running for the length
+                // of the auto-correction flash. At this moment, the "typedWord" argument is
+                // ignored by TextView.
+                mConnection.commitCorrection(
+                        new CorrectionInfo(mLastSelectionEnd - typedWord.length(),
+                        typedWord, autoCorrection));
+            }
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java
new file mode 100644
index 0000000..ce80c00
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/inputlogic/SpaceState.java
@@ -0,0 +1,54 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.inputlogic;
+
+/**
+ * Class for managing space states.
+ *
+ * At any given time, the input logic is in one of five possible space states. Depending on the
+ * current space state, some behavior will change; the prime example of this is the PHANTOM state,
+ * in which any subsequent letter input will input a space before the letter. Read on the
+ * description inside this class for each of the space states.
+ */
+public class SpaceState {
+    // None: the state where all the keyboard behavior is the most "standard" and no automatic
+    // input is added or removed. In this state, all self-inserting keys only insert themselves,
+    // and backspace removes one character.
+    public static final int NONE = 0;
+    // Double space: the state where the user pressed space twice quickly, which LatinIME
+    // resolved as period-space. In this state, pressing backspace will undo the
+    // double-space-to-period insertion: it will replace ". " with "  ".
+    public static final int DOUBLE = 1;
+    // Swap punctuation: the state where a weak space and a punctuation from the suggestion strip
+    // have just been swapped. In this state, pressing backspace will undo the swap: the
+    // characters will be swapped back back, and the space state will go to WEAK.
+    public static final int SWAP_PUNCTUATION = 2;
+    // Weak space: a space that should be swapped only by suggestion strip punctuation. Weak
+    // spaces happen when the user presses space, accepting the current suggestion (whether
+    // it's an auto-correction or not). In this state, pressing a punctuation from the suggestion
+    // strip inserts it before the space (while it inserts it after the space in the NONE state).
+    public static final int WEAK = 3;
+    // Phantom space: a not-yet-inserted space that should get inserted on the next input,
+    // character provided it's not a separator. If it's a separator, the phantom space is dropped.
+    // Phantom spaces happen when a user chooses a word from the suggestion strip. In this state,
+    // non-separators insert a space before they get inserted.
+    public static final int PHANTOM = 4;
+
+    private SpaceState() {
+        // This class is not publicly instantiable.
+    }
+}
diff --git a/native/jni/Android.mk b/native/jni/Android.mk
index 7827db3..3b3da96 100644
--- a/native/jni/Android.mk
+++ b/native/jni/Android.mk
@@ -28,7 +28,8 @@
 LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_SRC_DIR)
 
 LOCAL_CFLAGS += -Werror -Wall -Wextra -Weffc++ -Wformat=2 -Wcast-qual -Wcast-align \
-    -Wwrite-strings -Wfloat-equal -Wpointer-arith -Winit-self -Wredundant-decls -Wno-system-headers
+    -Wwrite-strings -Wfloat-equal -Wpointer-arith -Winit-self -Wredundant-decls \
+    -Woverloaded-virtual -Wstrict-null-sentinel -Wsign-promo -Wno-system-headers
 
 ifeq ($(TARGET_ARCH), arm)
 ifeq ($(TARGET_GCC_VERSION), 4.6)
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index 069852d..558667e 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -23,6 +23,7 @@
 #include "suggest/core/dicnode/internal/dic_node_state.h"
 #include "suggest/core/dicnode/internal/dic_node_properties.h"
 #include "suggest/core/dictionary/digraph_utils.h"
+#include "suggest/core/dictionary/error_type_utils.h"
 #include "utils/char_utils.h"
 
 #if DEBUG_DICT
@@ -493,8 +494,8 @@
         mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
     }
 
-    bool isExactMatch() const {
-        return mDicNodeState.mDicNodeStateScoring.isExactMatch();
+    ErrorTypeUtils::ErrorType getContainedErrorTypes() const {
+        return mDicNodeState.mDicNodeStateScoring.getContainedErrorTypes();
     }
 
     bool isBlacklistedOrNotAWord() const {
@@ -535,8 +536,8 @@
             return false;
         }
         // Promote exact matches to prevent them from being pruned.
-        const bool leftExactMatch = isExactMatch();
-        const bool rightExactMatch = right->isExactMatch();
+        const bool leftExactMatch = ErrorTypeUtils::isExactMatch(getContainedErrorTypes());
+        const bool rightExactMatch = ErrorTypeUtils::isExactMatch(right->getContainedErrorTypes());
         if (leftExactMatch != rightExactMatch) {
             return leftExactMatch;
         }
diff --git a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
index 74f9eee..11c201e 100644
--- a/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
+++ b/native/jni/src/suggest/core/dicnode/internal/dic_node_state_scoring.h
@@ -32,7 +32,7 @@
               mDigraphIndex(DigraphUtils::NOT_A_DIGRAPH_INDEX),
               mEditCorrectionCount(0), mProximityCorrectionCount(0),
               mNormalizedCompoundDistance(0.0f), mSpatialDistance(0.0f), mLanguageDistance(0.0f),
-              mRawLength(0.0f), mContainingErrorTypes(ErrorTypeUtils::NOT_AN_ERROR),
+              mRawLength(0.0f), mContainedErrorTypes(ErrorTypeUtils::NOT_AN_ERROR),
               mNormalizedCompoundDistanceAfterFirstWord(MAX_VALUE_FOR_WEIGHTING) {
     }
 
@@ -48,7 +48,7 @@
         mDoubleLetterLevel = NOT_A_DOUBLE_LETTER;
         mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
         mNormalizedCompoundDistanceAfterFirstWord = MAX_VALUE_FOR_WEIGHTING;
-        mContainingErrorTypes = ErrorTypeUtils::NOT_AN_ERROR;
+        mContainedErrorTypes = ErrorTypeUtils::NOT_AN_ERROR;
     }
 
     AK_FORCE_INLINE void init(const DicNodeStateScoring *const scoring) {
@@ -60,7 +60,7 @@
         mRawLength = scoring->mRawLength;
         mDoubleLetterLevel = scoring->mDoubleLetterLevel;
         mDigraphIndex = scoring->mDigraphIndex;
-        mContainingErrorTypes = scoring->mContainingErrorTypes;
+        mContainedErrorTypes = scoring->mContainedErrorTypes;
         mNormalizedCompoundDistanceAfterFirstWord =
                 scoring->mNormalizedCompoundDistanceAfterFirstWord;
     }
@@ -69,7 +69,7 @@
             const int inputSize, const int totalInputIndex,
             const ErrorTypeUtils::ErrorType errorType) {
         addDistance(spatialCost, languageCost, doNormalization, inputSize, totalInputIndex);
-        mContainingErrorTypes = mContainingErrorTypes | errorType;
+        mContainedErrorTypes = mContainedErrorTypes | errorType;
         if (ErrorTypeUtils::isEditCorrectionError(errorType)) {
             ++mEditCorrectionCount;
         }
@@ -169,8 +169,8 @@
         }
     }
 
-    bool isExactMatch() const {
-        return ErrorTypeUtils::isExactMatch(mContainingErrorTypes);
+    ErrorTypeUtils::ErrorType getContainedErrorTypes() const {
+        return mContainedErrorTypes;
     }
 
  private:
@@ -188,7 +188,7 @@
     float mLanguageDistance;
     float mRawLength;
     // All accumulated error types so far
-    ErrorTypeUtils::ErrorType mContainingErrorTypes;
+    ErrorTypeUtils::ErrorType mContainedErrorTypes;
     float mNormalizedCompoundDistanceAfterFirstWord;
 
     AK_FORCE_INLINE void addDistance(float spatialDistance, float languageDistance,
diff --git a/native/jni/src/suggest/core/dictionary/error_type_utils.h b/native/jni/src/suggest/core/dictionary/error_type_utils.h
index ab4a65e..1122291 100644
--- a/native/jni/src/suggest/core/dictionary/error_type_utils.h
+++ b/native/jni/src/suggest/core/dictionary/error_type_utils.h
@@ -47,9 +47,8 @@
     // A new word error should be an edit correction error or a proximity correction error.
     static const ErrorType NEW_WORD;
 
-    // TODO: Differentiate errors.
-    static bool isExactMatch(const ErrorType containingErrors) {
-        return (containingErrors & ~ERRORS_TREATED_AS_AN_EXACT_MATCH) == 0;
+    static bool isExactMatch(const ErrorType containedErrorTypes) {
+        return (containedErrorTypes & ~ERRORS_TREATED_AS_AN_EXACT_MATCH) == 0;
     }
 
     static bool isEditCorrectionError(const ErrorType errorType) {
diff --git a/native/jni/src/suggest/core/dictionary/suggestions_output_utils.cpp b/native/jni/src/suggest/core/dictionary/suggestions_output_utils.cpp
index a3ca7e7..d219757 100644
--- a/native/jni/src/suggest/core/dictionary/suggestions_output_utils.cpp
+++ b/native/jni/src/suggest/core/dictionary/suggestions_output_utils.cpp
@@ -18,8 +18,9 @@
 
 #include "suggest/core/dicnode/dic_node.h"
 #include "suggest/core/dicnode/dic_node_utils.h"
-#include "suggest/core/dictionary/dictionary.h"
 #include "suggest/core/dictionary/binary_dictionary_shortcut_iterator.h"
+#include "suggest/core/dictionary/dictionary.h"
+#include "suggest/core/dictionary/error_type_utils.h"
 #include "suggest/core/policy/scoring.h"
 #include "suggest/core/session/dic_traverse_session.h"
 
@@ -98,7 +99,8 @@
         const bool isPossiblyOffensiveWord =
                 traverseSession->getDictionaryStructurePolicy()->getProbability(
                         terminalDicNode->getProbability(), NOT_A_PROBABILITY) <= 0;
-        const bool isExactMatch = terminalDicNode->isExactMatch();
+        const bool isExactMatch =
+                ErrorTypeUtils::isExactMatch(terminalDicNode->getContainedErrorTypes());
         const bool isFirstCharUppercase = terminalDicNode->isFirstCharUppercase();
         // Heuristic: We exclude freq=0 first-char-uppercase words from exact match.
         // (e.g. "AMD" and "and")
@@ -115,9 +117,9 @@
         // TODO: Better integration with java side autocorrection logic.
         const int finalScore = scoringPolicy->calculateFinalScore(
                 compoundDistance, traverseSession->getInputSize(),
-                terminalDicNode->isExactMatch()
-                        || (forceCommitMultiWords && terminalDicNode->hasMultipleWords())
-                                || (isValidWord && scoringPolicy->doesAutoCorrectValidWord()));
+                terminalDicNode->getContainedErrorTypes(),
+                (forceCommitMultiWords && terminalDicNode->hasMultipleWords())
+                         || (isValidWord && scoringPolicy->doesAutoCorrectValidWord()));
         if (maxScore < finalScore && isValidWord) {
             maxScore = finalScore;
         }
@@ -149,7 +151,9 @@
             const bool sameAsTyped = scoringPolicy->sameAsTyped(traverseSession, terminalDicNode);
             const int shortcutBaseScore = scoringPolicy->doesAutoCorrectValidWord() ?
                      scoringPolicy->calculateFinalScore(compoundDistance,
-                             traverseSession->getInputSize(), true /* forceCommit */) : finalScore;
+                             traverseSession->getInputSize(),
+                             terminalDicNode->getContainedErrorTypes(),
+                             true /* forceCommit */) : finalScore;
             const int updatedOutputWordIndex = outputShortcuts(&shortcutIt,
                     outputWordIndex, shortcutBaseScore, outputCodePoints, frequencies, outputTypes,
                     sameAsTyped);
diff --git a/native/jni/src/suggest/core/policy/scoring.h b/native/jni/src/suggest/core/policy/scoring.h
index a164129..5ae3d21 100644
--- a/native/jni/src/suggest/core/policy/scoring.h
+++ b/native/jni/src/suggest/core/policy/scoring.h
@@ -28,7 +28,7 @@
 class Scoring {
  public:
     virtual int calculateFinalScore(const float compoundDistance, const int inputSize,
-            const bool forceCommit) const = 0;
+            const ErrorTypeUtils::ErrorType containedErrorTypes, const bool forceCommit) const = 0;
     virtual bool getMostProbableString(const DicTraverseSession *const traverseSession,
             const int terminalSize, const float languageWeight, int *const outputCodePoints,
             int *const type, int *const freq) const = 0;
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
index 104eb2a..7b33206 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
@@ -22,6 +22,12 @@
 const int ScoringParams::THRESHOLD_NEXT_WORD_PROBABILITY = 40;
 const int ScoringParams::THRESHOLD_NEXT_WORD_PROBABILITY_FOR_CAPPED = 120;
 const float ScoringParams::AUTOCORRECT_OUTPUT_THRESHOLD = 1.0f;
+
+const float ScoringParams::EXACT_MATCH_PROMOTION = 1.1f;
+const float ScoringParams::CASE_ERROR_PENALTY_FOR_EXACT_MATCH = 0.01f;
+const float ScoringParams::ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH = 0.02f;
+const float ScoringParams::DIGRAPH_PENALTY_FOR_EXACT_MATCH = 0.03f;
+
 // TODO: Unlimit max cache dic node size
 const int ScoringParams::MAX_CACHE_DIC_NODE_SIZE = 170;
 const int ScoringParams::MAX_CACHE_DIC_NODE_SIZE_FOR_SINGLE_POINT = 310;
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.h b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
index 7d4b5c3..de7410d 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.h
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
@@ -32,6 +32,11 @@
     static const int MAX_CACHE_DIC_NODE_SIZE_FOR_SINGLE_POINT;
     static const int THRESHOLD_SHORT_WORD_LENGTH;
 
+    static const float EXACT_MATCH_PROMOTION;
+    static const float CASE_ERROR_PENALTY_FOR_EXACT_MATCH;
+    static const float ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH;
+    static const float DIGRAPH_PENALTY_FOR_EXACT_MATCH;
+
     // Numerically optimized parameters (currently for tap typing only).
     // TODO: add ability to modify these constants programmatically.
     // TODO: explore optimization of gesture parameters.
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
index b1337c6..186e3ba 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
@@ -18,6 +18,7 @@
 #define LATINIME_TYPING_SCORING_H
 
 #include "defines.h"
+#include "suggest/core/dictionary/error_type_utils.h"
 #include "suggest/core/policy/scoring.h"
 #include "suggest/core/session/dic_traverse_session.h"
 #include "suggest/policyimpl/typing/scoring_params.h"
@@ -53,12 +54,26 @@
     }
 
     AK_FORCE_INLINE int calculateFinalScore(const float compoundDistance,
-            const int inputSize, const bool forceCommit) const {
+            const int inputSize, const ErrorTypeUtils::ErrorType containedErrorTypes,
+            const bool forceCommit) const {
         const float maxDistance = ScoringParams::DISTANCE_WEIGHT_LANGUAGE
                 + static_cast<float>(inputSize) * ScoringParams::TYPING_MAX_OUTPUT_SCORE_PER_INPUT;
-        const float score = ScoringParams::TYPING_BASE_OUTPUT_SCORE
-                - compoundDistance / maxDistance
-                + (forceCommit ? ScoringParams::AUTOCORRECT_OUTPUT_THRESHOLD : 0.0f);
+        float score = ScoringParams::TYPING_BASE_OUTPUT_SCORE - compoundDistance / maxDistance;
+        if (forceCommit) {
+            score += ScoringParams::AUTOCORRECT_OUTPUT_THRESHOLD;
+        }
+        if (ErrorTypeUtils::isExactMatch(containedErrorTypes)) {
+            score += ScoringParams::EXACT_MATCH_PROMOTION;
+            if ((ErrorTypeUtils::MATCH_WITH_CASE_ERROR & containedErrorTypes) != 0) {
+                score -= ScoringParams::CASE_ERROR_PENALTY_FOR_EXACT_MATCH;
+            }
+            if ((ErrorTypeUtils::MATCH_WITH_ACCENT_ERROR & containedErrorTypes) != 0) {
+                score -= ScoringParams::ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH;
+            }
+            if ((ErrorTypeUtils::MATCH_WITH_DIGRAPH & containedErrorTypes) != 0) {
+                score -= ScoringParams::DIGRAPH_PENALTY_FOR_EXACT_MATCH;
+            }
+        }
         return static_cast<int>(score * SUGGEST_INTERFACE_OUTPUT_SCALE);
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java
index 0f0ebaf..89021b4 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsLanguageWithoutSpaces.java
@@ -99,7 +99,8 @@
         assertEquals("predictions in lang without spaces", "Barack",
                 mEditText.getText().toString());
         // Test the first prediction is displayed
+        final SuggestedWords suggestedWords = mLatinIME.getSuggestedWords();
         assertEquals("predictions in lang without spaces", "Obama",
-                mLatinIME.getFirstSuggestedWord());
+                suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
index 2d736e3..a474c6a 100644
--- a/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTestsNonEnglish.java
@@ -84,8 +84,9 @@
             type(WORD_TO_TYPE);
             sleep(DELAY_TO_WAIT_FOR_UNDERLINE);
             runMessages();
+            final SuggestedWords suggestedWords = mLatinIME.getSuggestedWords();
             assertEquals("type word then type space yields predictions for French",
-                    EXPECTED_RESULT, mLatinIME.getFirstSuggestedWord());
+                    EXPECTED_RESULT, suggestedWords.size() > 0 ? suggestedWords.getWord(0) : null);
         } finally {
             setBooleanPreference(NEXT_WORD_PREDICTION_OPTION, previousNextWordPredictionOption,
                     defaultNextWordPredictionOption);
diff --git a/tests/src/com/android/inputmethod/latin/PunctuationTests.java b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
index 84ff6b30..d5c06e2 100644
--- a/tests/src/com/android/inputmethod/latin/PunctuationTests.java
+++ b/tests/src/com/android/inputmethod/latin/PunctuationTests.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin;
 
+import android.provider.Settings.Secure;
 import android.test.suitebuilder.annotation.LargeTest;
 
 import com.android.inputmethod.latin.R;
@@ -153,7 +154,9 @@
         final String WORD_TO_TYPE = "you'f ";
         final String EXPECTED_RESULT = "you'd ";
         type(WORD_TO_TYPE);
-        assertEquals("auto-correction with single quote inside",
+        assertEquals("auto-correction with single quote inside. ID = "
+                + Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID)
+                + " ; Suggestions = " + mLatinIME.getSuggestedWords(),
                 EXPECTED_RESULT, mEditText.getText().toString());
     }
 
@@ -161,7 +164,9 @@
         final String WORD_TO_TYPE = "'tgis' ";
         final String EXPECTED_RESULT = "'this' ";
         type(WORD_TO_TYPE);
-        assertEquals("auto-correction with single quotes around",
+        assertEquals("auto-correction with single quotes around. ID = "
+                + Secure.getString(getContext().getContentResolver(), Secure.ANDROID_ID)
+                + " ; Suggestions = " + mLatinIME.getSuggestedWords(),
                 EXPECTED_RESULT, mEditText.getText().toString());
     }
 }