Merge "Try to figure out whether d.quotes open or close."
diff --git a/java/res/layout/suggestions_strip.xml b/java/res/layout/suggestions_strip.xml
index 908e305..2ffac17 100644
--- a/java/res/layout/suggestions_strip.xml
+++ b/java/res/layout/suggestions_strip.xml
@@ -25,4 +25,19 @@
         android:orientation="horizontal"
         android:layout_width="match_parent"
         android:layout_height="match_parent" />
+    <LinearLayout
+        android:id="@+id/add_to_dictionary_strip"
+        android:orientation="horizontal"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:visibility="invisible">
+        <include
+            layout="@layout/suggestion_word"
+            android:id="@+id/word_to_save" />
+        <include
+            layout="@layout/suggestion_divider" />
+        <include
+            layout="@layout/hint_add_to_dictionary"
+            android:id="@+id/hint_add_to_dictionary" />
+    </LinearLayout>
 </merge>
diff --git a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
index a8fab88..dec739d 100644
--- a/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/ViewCompatUtils.java
@@ -20,6 +20,9 @@
 
 import java.lang.reflect.Method;
 
+// TODO: Use {@link android.support.v4.view.ViewCompat} instead of this utility class.
+// Currently {@link #getPaddingEnd(View)} and {@link #setPaddingRelative(View,int,int,int,int)}
+// are missing from android-support-v4 static library in KitKat SDK.
 public final class ViewCompatUtils {
     // Note that View.LAYOUT_DIRECTION_LTR and View.LAYOUT_DIRECTION_RTL have been introduced in
     // API level 17 (Build.VERSION_CODE.JELLY_BEAN_MR1).
diff --git a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
index 8dea908..5bde668 100644
--- a/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
+++ b/java/src/com/android/inputmethod/keyboard/EmojiPalettesView.java
@@ -23,12 +23,13 @@
 import android.content.res.ColorStateList;
 import android.content.res.Resources;
 import android.content.res.TypedArray;
+import android.graphics.Color;
 import android.graphics.Rect;
 import android.os.Build;
+import android.os.CountDownTimer;
 import android.preference.PreferenceManager;
 import android.support.v4.view.PagerAdapter;
 import android.support.v4.view.ViewPager;
-import android.text.format.DateUtils;
 import android.util.AttributeSet;
 import android.util.Log;
 import android.util.Pair;
@@ -58,6 +59,7 @@
 import java.util.Comparator;
 import java.util.HashMap;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.TimeUnit;
 
 /**
  * View class to implement Emoji palettes.
@@ -752,9 +754,8 @@
         }
     }
 
-    // TODO: Do the same things done in PointerTracker
     private static class DeleteKeyOnTouchListener implements OnTouchListener {
-        private static final long MAX_REPEAT_COUNT_TIME = 30 * DateUtils.SECOND_IN_MILLIS;
+        private static final long MAX_REPEAT_COUNT_TIME = TimeUnit.SECONDS.toMillis(30);
         private final int mDeleteKeyPressedBackgroundColor;
         private final long mKeyRepeatStartTimeout;
         private final long mKeyRepeatInterval;
@@ -765,61 +766,36 @@
                     res.getColor(R.color.emoji_key_pressed_background_color);
             mKeyRepeatStartTimeout = res.getInteger(R.integer.config_key_repeat_start_timeout);
             mKeyRepeatInterval = res.getInteger(R.integer.config_key_repeat_interval);
+            mTimer = new CountDownTimer(MAX_REPEAT_COUNT_TIME, mKeyRepeatInterval) {
+                @Override
+                public void onTick(long millisUntilFinished) {
+                    final long elapsed = MAX_REPEAT_COUNT_TIME - millisUntilFinished;
+                    if (elapsed < mKeyRepeatStartTimeout) {
+                        return;
+                    }
+                    onKeyRepeat();
+                }
+                @Override
+                public void onFinish() {
+                    onKeyRepeat();
+                }
+            };
         }
 
+        /** Key-repeat state. */
+        private static final int KEY_REPEAT_STATE_INITIALIZED = 0;
+        // The key is touched but auto key-repeat is not started yet.
+        private static final int KEY_REPEAT_STATE_KEY_DOWN = 1;
+        // At least one key-repeat event has already been triggered and the key is not released.
+        private static final int KEY_REPEAT_STATE_KEY_REPEAT = 2;
+
         private KeyboardActionListener mKeyboardActionListener =
                 KeyboardActionListener.EMPTY_LISTENER;
-        private DummyRepeatKeyRepeatTimer mTimer;
 
-        private synchronized void startRepeat() {
-            if (mTimer != null) {
-                abortRepeat();
-            }
-            mTimer = new DummyRepeatKeyRepeatTimer();
-            mTimer.start();
-        }
-
-        private synchronized void abortRepeat() {
-            mTimer.abort();
-            mTimer = null;
-        }
-
-        // TODO: Remove
-        // This function is mimicking the repeat code in PointerTracker.
-        // Specifically referring to PointerTracker#startRepeatKey and PointerTracker#onKeyRepeat.
-        private class DummyRepeatKeyRepeatTimer extends Thread {
-            public boolean mAborted = false;
-
-            @Override
-            public void run() {
-                int repeatCount = 1;
-                int timeCount = 0;
-                while (timeCount < MAX_REPEAT_COUNT_TIME && !mAborted) {
-                    if (timeCount > mKeyRepeatStartTimeout) {
-                        pressDelete(repeatCount);
-                    }
-                    timeCount += mKeyRepeatInterval;
-                    ++repeatCount;
-                    try {
-                        Thread.sleep(mKeyRepeatInterval);
-                    } catch (InterruptedException e) {
-                    }
-                }
-            }
-
-            public void abort() {
-                mAborted = true;
-            }
-        }
-
-        public void pressDelete(final int repeatCount) {
-            mKeyboardActionListener.onPressKey(
-                    Constants.CODE_DELETE, repeatCount, true /* isSinglePointer */);
-            mKeyboardActionListener.onCodeInput(
-                    Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE);
-            mKeyboardActionListener.onReleaseKey(
-                    Constants.CODE_DELETE, false /* withSliding */);
-        }
+        // TODO: Do the same things done in PointerTracker
+        private final CountDownTimer mTimer;
+        private int mState = KEY_REPEAT_STATE_INITIALIZED;
+        private int mRepeatCount = 0;
 
         public void setKeyboardActionListener(final KeyboardActionListener listener) {
             mKeyboardActionListener = listener;
@@ -827,18 +803,80 @@
 
         @Override
         public boolean onTouch(final View v, final MotionEvent event) {
-            switch(event.getAction()) {
+            switch (event.getActionMasked()) {
             case MotionEvent.ACTION_DOWN:
-                v.setBackgroundColor(mDeleteKeyPressedBackgroundColor);
-                pressDelete(0 /* repeatCount */);
-                startRepeat();
+                onTouchDown(v);
                 return true;
+            case MotionEvent.ACTION_MOVE:
+                final float x = event.getX();
+                final float y = event.getY();
+                if (x < 0.0f || v.getWidth() < x || y < 0.0f || v.getHeight() < y) {
+                    // Stop generating key events once the finger moves away from the view area.
+                    onTouchCanceled(v);
+                }
+                return true;
+            case MotionEvent.ACTION_CANCEL:
             case MotionEvent.ACTION_UP:
-                v.setBackgroundColor(0);
-                abortRepeat();
+                onTouchUp(v);
                 return true;
             }
             return false;
         }
+
+        private void handleKeyDown() {
+            mKeyboardActionListener.onPressKey(
+                    Constants.CODE_DELETE, mRepeatCount, true /* isSinglePointer */);
+        }
+
+        private void handleKeyUp() {
+            mKeyboardActionListener.onCodeInput(
+                    Constants.CODE_DELETE, NOT_A_COORDINATE, NOT_A_COORDINATE);
+            mKeyboardActionListener.onReleaseKey(
+                    Constants.CODE_DELETE, false /* withSliding */);
+            ++mRepeatCount;
+        }
+
+        private void onTouchDown(final View v) {
+            mTimer.cancel();
+            mRepeatCount = 0;
+            handleKeyDown();
+            v.setBackgroundColor(mDeleteKeyPressedBackgroundColor);
+            mState = KEY_REPEAT_STATE_KEY_DOWN;
+            mTimer.start();
+        }
+
+        private void onTouchUp(final View v) {
+            mTimer.cancel();
+            if (mState == KEY_REPEAT_STATE_KEY_DOWN) {
+                handleKeyUp();
+            }
+            v.setBackgroundColor(Color.TRANSPARENT);
+            mState = KEY_REPEAT_STATE_INITIALIZED;
+        }
+
+        private void onTouchCanceled(final View v) {
+            mTimer.cancel();
+            v.setBackgroundColor(Color.TRANSPARENT);
+            mState = KEY_REPEAT_STATE_INITIALIZED;
+        }
+
+        // Called by {@link #mTimer} in the UI thread as an auto key-repeat signal.
+        private void onKeyRepeat() {
+            switch (mState) {
+            case KEY_REPEAT_STATE_INITIALIZED:
+                // Basically this should not happen.
+                break;
+            case KEY_REPEAT_STATE_KEY_DOWN:
+                // Do not call {@link #handleKeyDown} here because it has already been called
+                // in {@link #onTouchDown}.
+                handleKeyUp();
+                mState = KEY_REPEAT_STATE_KEY_REPEAT;
+                break;
+            case KEY_REPEAT_STATE_KEY_REPEAT:
+                handleKeyDown();
+                handleKeyUp();
+                break;
+            }
+        }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index b094100..e082735 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -888,10 +888,7 @@
         mInputLogic.finishInput();
         // Notify ResearchLogger
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
-            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput,
-                    // TODO[IL]: mInputLogic.mConnection should be private
-                    mInputLogic.mConnection.getExpectedSelectionStart(),
-                    mInputLogic.mConnection.getExpectedSelectionEnd(), getCurrentInputConnection());
+            ResearchLogger.latinIME_onFinishInputViewInternal(finishingInput);
         }
     }
 
@@ -915,63 +912,14 @@
                     composingSpanEnd, mInputLogic.mConnection);
         }
 
-        final boolean selectionChanged = oldSelStart != newSelStart || oldSelEnd != 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
-        // by us or not. If we are composing a word but there is no composing span, then
-        // we know for sure the cursor moved while we were composing and we should reset
-        // the state. TODO: rescind this policy: the framework never removes the composing
-        // span on its own accord while editing. This test is useless.
-        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
-
         // If the keyboard is not visible, we don't need to do all the housekeeping work, as it
         // will be reset when the keyboard shows up anyway.
         // 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() && !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.
-            mInputLogic.mSpaceState = SpaceState.NONE;
-
-            // TODO: is it still necessary to test for composingSpan related stuff?
-            final boolean selectionChangedOrSafeToReset = selectionChanged
-                    || (!mInputLogic.mWordComposer.isComposingWord()) || noComposingSpan;
-            final boolean hasOrHadSelection = (oldSelStart != oldSelEnd
-                    || newSelStart != newSelEnd);
-            final int moveAmount = newSelStart - oldSelStart;
-            if (selectionChangedOrSafeToReset && (hasOrHadSelection
-                    || !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
-                // cursor back to where it was. Latin IME could then fix the position of the cursor
-                // again, but the asynchronous nature of the calls results in this wreaking havoc
-                // with selection on double tap and the like.
-                // 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.
-                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.
-                mInputLogic.mConnection.resetCachesUponCursorMoveAndReturnSuccess(
-                        newSelStart, newSelEnd, false /* shouldFinishComposition */);
-            }
-
-            // We moved the cursor. If we are touching a word, we need to resume suggestion.
-            mHandler.postResumeSuggestions();
-            // Reset the last recapitalization.
-            mInputLogic.mRecapitalizeStatus.deactivate();
+        if (isInputViewShown() &&
+                mInputLogic.onUpdateSelection(mSettings.getCurrent(), oldSelStart, oldSelEnd,
+                        newSelStart, newSelEnd, composingSpanStart, composingSpanEnd)) {
             mKeyboardSwitcher.updateShiftState();
         }
 
@@ -1186,7 +1134,7 @@
     // the right layout.
     // TODO[IL]: Remove this, pass the input logic to the keyboard switcher instead?
     public int getCurrentAutoCapsState() {
-        return mInputLogic.getCurrentAutoCapsState(null /* optionalSettingsValues */);
+        return mInputLogic.getCurrentAutoCapsState(mSettings.getCurrent());
     }
 
     // Called from the KeyboardSwitcher which needs to know recaps state to display
@@ -1288,8 +1236,12 @@
         } else {
             codeToSend = codePoint;
         }
-        mInputLogic.onCodeInput(codeToSend, keyX, keyY, mHandler, mKeyboardSwitcher,
-                mSubtypeSwitcher);
+        if (Constants.CODE_SHORTCUT == codePoint) {
+            mSubtypeSwitcher.switchToShortcutIME(this);
+            // Still call the *#onCodeInput methods for readability.
+        }
+        mInputLogic.onCodeInput(codeToSend, keyX, keyY, mSettings.getCurrent(),
+                mHandler, mKeyboardSwitcher);
         mKeyboardSwitcher.onCodeInput(codePoint);
     }
 
@@ -1466,10 +1418,7 @@
             final SuggestedWords punctuationList =
                     mSettings.getCurrent().mSpacingAndPunctuations.mSuggestPuncList;
             final SuggestedWords oldSuggestedWords = previousSuggestedWords == punctuationList
-                            ? SuggestedWords.EMPTY : previousSuggestedWords;
-            if (TextUtils.isEmpty(typedWord)) {
-                return oldSuggestedWords;
-            }
+                    ? SuggestedWords.EMPTY : previousSuggestedWords;
             final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
                     SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
             return new SuggestedWords(typedWordAndPreviousSuggestions,
@@ -1483,27 +1432,27 @@
 
     private void showSuggestionStripWithTypedWord(final SuggestedWords suggestedWords,
             final String typedWord) {
-      if (suggestedWords.isEmpty()) {
-          // No auto-correction is available, clear the cached values.
-          AccessibilityUtils.getInstance().setAutoCorrection(null, null);
-          clearSuggestionStrip();
-          return;
-      }
-      final String autoCorrection;
-      if (suggestedWords.mWillAutoCorrect) {
-          autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
-      } else {
-          // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
-          // because it may differ from mWordComposer.mTypedWord.
-          autoCorrection = typedWord;
-      }
-      mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
-      setSuggestedWords(suggestedWords);
-      setAutoCorrectionIndicator(suggestedWords.mWillAutoCorrect);
-      setSuggestionStripShown(isSuggestionsStripVisible());
-      // An auto-correction is available, cache it in accessibility code so
-      // we can be speak it if the user touches a key that will insert it.
-      AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
+        if (suggestedWords.isEmpty()) {
+            // No auto-correction is available, clear the cached values.
+            AccessibilityUtils.getInstance().setAutoCorrection(null, null);
+            clearSuggestionStrip();
+            return;
+        }
+        final String autoCorrection;
+        if (suggestedWords.mWillAutoCorrect) {
+            autoCorrection = suggestedWords.getWord(SuggestedWords.INDEX_OF_AUTO_CORRECTION);
+        } else {
+            // We can't use suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD)
+            // because it may differ from mWordComposer.mTypedWord.
+            autoCorrection = typedWord;
+        }
+        mInputLogic.mWordComposer.setAutoCorrection(autoCorrection);
+        setSuggestedWords(suggestedWords);
+        setAutoCorrectionIndicator(suggestedWords.mWillAutoCorrect);
+        setSuggestionStripShown(isSuggestionsStripVisible());
+        // An auto-correction is available, cache it in accessibility code so
+        // we can be speak it if the user touches a key that will insert it.
+        AccessibilityUtils.getInstance().setAutoCorrection(suggestedWords, typedWord);
     }
 
     // TODO[IL]: Define a clean interface for this
@@ -1513,7 +1462,7 @@
             return;
         }
         showSuggestionStripWithTypedWord(suggestedWords,
-            suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
+                suggestedWords.getWord(SuggestedWords.INDEX_OF_TYPED_WORD));
     }
 
     // Called from {@link SuggestionStripView} through the {@link SuggestionStripView#Listener}
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index d1b2514..300c7c9 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -36,14 +36,12 @@
 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.Suggest.OnGetSuggestedWordsCallback;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 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.settings.SpacingAndPunctuations;
 import com.android.inputmethod.latin.utils.AsyncResultHolder;
@@ -181,6 +179,76 @@
     }
 
     /**
+     * Consider an update to the cursor position. Evaluate whether this update has happened as
+     * part of normal typing or whether it was an explicit cursor move by the user. In any case,
+     * do the necessary adjustments.
+     * @param settingsValues the current settings
+     * @param oldSelStart old selection start
+     * @param oldSelEnd old selection end
+     * @param newSelStart new selection start
+     * @param newSelEnd new selection end
+     * @param composingSpanStart composing span start
+     * @param composingSpanEnd composing span end
+     * @return whether the cursor has moved as a result of user interaction.
+     */
+    public boolean onUpdateSelection(final SettingsValues settingsValues,
+            final int oldSelStart, final int oldSelEnd,
+            final int newSelStart, final int newSelEnd,
+            final int composingSpanStart, final int composingSpanEnd) {
+        if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) {
+            return false;
+        }
+        // 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 = SpaceState.NONE;
+
+        // 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
+        // by us or not. If we are composing a word but there is no composing span, then
+        // we know for sure the cursor moved while we were composing and we should reset
+        // the state. TODO: rescind this policy: the framework never removes the composing
+        // span on its own accord while editing. This test is useless.
+        final boolean noComposingSpan = composingSpanStart == -1 && composingSpanEnd == -1;
+        final boolean selectionChanged = oldSelStart != newSelStart || oldSelEnd != newSelEnd;
+
+        // TODO: is it still necessary to test for composingSpan related stuff?
+        final boolean selectionChangedOrSafeToReset = selectionChanged
+                || (!mWordComposer.isComposingWord()) || noComposingSpan;
+        final boolean hasOrHadSelection = (oldSelStart != oldSelEnd || newSelStart != newSelEnd);
+        final int moveAmount = newSelStart - oldSelStart;
+        if (selectionChangedOrSafeToReset && (hasOrHadSelection
+                || !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
+            // cursor back to where it was. Latin IME could then fix the position of the cursor
+            // again, but the asynchronous nature of the calls results in this wreaking havoc
+            // with selection on double tap and the like.
+            // 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(settingsValues, 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 */);
+        }
+
+        // We moved the cursor. If we are touching a word, we need to resume suggestion.
+        mLatinIME.mHandler.postResumeSuggestions();
+        // Reset the last recapitalization.
+        mRecapitalizeStatus.deactivate();
+        return true;
+    }
+
+    /**
      * React to a code input. It may be a code point to insert, or a symbolic value that influences
      * the keyboard behavior.
      *
@@ -192,13 +260,12 @@
      * @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) {
+            final SettingsValues settingsValues,
+            // TODO: remove these two arguments
+            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
         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) {
@@ -247,7 +314,8 @@
             onSettingsKeyPressed();
             break;
         case Constants.CODE_SHORTCUT:
-            subtypeSwitcher.switchToShortcutIME(mLatinIME);
+            // We need to switch to the shortcut IME. This is handled by LatinIME since the
+            // input logic has no business with IME switching.
             break;
         case Constants.CODE_ACTION_NEXT:
             performEditorAction(EditorInfo.IME_ACTION_NEXT);
@@ -1272,15 +1340,10 @@
      * This is called from the KeyboardSwitcher (through a trampoline in LatinIME) because it
      * needs to know auto caps state to display the right layout.
      *
-     * @param optionalSettingsValues settings values, or null if we should just get the current ones
-     *   from the singleton.
+     * @param settingsValues the relevant settings values
      * @return a caps mode from TextUtils.CAP_MODE_* or Constants.TextUtils.CAP_MODE_OFF.
      */
-    public int getCurrentAutoCapsState(final SettingsValues optionalSettingsValues) {
-        // If we are in a batch edit, we need to use the same settings values as the outside
-        // code, that will pass it to us. Otherwise, we can just take the current values.
-        final SettingsValues settingsValues = null != optionalSettingsValues
-                ? optionalSettingsValues : Settings.getInstance().getCurrent();
+    public int getCurrentAutoCapsState(final SettingsValues settingsValues) {
         if (!settingsValues.mAutoCap) return Constants.TextUtils.CAP_MODE_OFF;
 
         final EditorInfo ei = getCurrentInputEditorInfo();
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index f836e61..af04de4 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -38,7 +38,6 @@
 import android.text.style.UnderlineSpan;
 import android.util.AttributeSet;
 import android.view.Gravity;
-import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
 import android.view.ViewGroup;
@@ -100,10 +99,6 @@
     private static final int AUTO_CORRECT_UNDERLINE = 0x02;
     private static final int VALID_TYPED_WORD_BOLD = 0x04;
 
-    private final TextView mWordToSaveView;
-    private final TextView mLeftwardsArrowView;
-    private final TextView mHintToSaveView;
-
     public SuggestionStripLayoutHelper(final Context context, final AttributeSet attrs,
             final int defStyle, final ArrayList<TextView> wordViews,
             final ArrayList<View> dividerViews, final ArrayList<TextView> debugInfoViews) {
@@ -157,11 +152,6 @@
                 R.dimen.config_more_suggestions_bottom_gap);
         mMoreSuggestionsRowHeight = res.getDimensionPixelSize(
                 R.dimen.config_more_suggestions_row_height);
-
-        final LayoutInflater inflater = LayoutInflater.from(context);
-        mWordToSaveView = (TextView)inflater.inflate(R.layout.suggestion_word, null);
-        mLeftwardsArrowView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
-        mHintToSaveView = (TextView)inflater.inflate(R.layout.hint_add_to_dictionary, null);
     }
 
     public int getMaxMoreSuggestionsRow() {
@@ -466,54 +456,30 @@
         mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
     }
 
-    public void layoutAddToDictionaryHint(final String word, final ViewGroup stripView,
-            final int stripWidth, final CharSequence hintText, final OnClickListener listener) {
+    public void layoutAddToDictionaryHint(final String word, final ViewGroup addToDictionaryStrip,
+            final int stripWidth, final CharSequence hintText) {
         final int width = stripWidth - mDividerWidth - mPadding * 2;
 
-        final TextView wordView = mWordToSaveView;
+        final TextView wordView = (TextView)addToDictionaryStrip.findViewById(R.id.word_to_save);
         wordView.setTextColor(mColorTypedWord);
         final int wordWidth = (int)(width * mCenterSuggestionWeight);
-        final CharSequence text = getEllipsizedText(word, wordWidth, wordView.getPaint());
+        final CharSequence wordToSave = getEllipsizedText(word, wordWidth, wordView.getPaint());
         final float wordScaleX = wordView.getTextScaleX();
-        // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
-        // will be extracted at {@link #getAddToDictionaryWord()}.
-        wordView.setTag(word);
-        wordView.setText(text);
+        wordView.setText(wordToSave);
         wordView.setTextScaleX(wordScaleX);
-        stripView.addView(wordView);
         setLayoutWeight(wordView, mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
 
-        stripView.addView(mDividerViews.get(0));
-
-        final TextView leftArrowView = mLeftwardsArrowView;
-        leftArrowView.setTextColor(mColorAutoCorrect);
-        leftArrowView.setText(LEFTWARDS_ARROW);
-        stripView.addView(leftArrowView);
-
-        final TextView hintView = mHintToSaveView;
+        final TextView hintView = (TextView)addToDictionaryStrip.findViewById(
+                R.id.hint_add_to_dictionary);
         hintView.setGravity(Gravity.LEFT | Gravity.CENTER_VERTICAL);
         hintView.setTextColor(mColorAutoCorrect);
-        final int hintWidth = width - wordWidth - leftArrowView.getWidth();
-        final float hintScaleX = getTextScaleX(hintText, hintWidth, hintView.getPaint());
-        hintView.setText(hintText);
+        final int hintWidth = width - wordWidth;
+        final String hintWithArrow = LEFTWARDS_ARROW + hintText;
+        final float hintScaleX = getTextScaleX(hintWithArrow, hintWidth, hintView.getPaint());
+        hintView.setText(hintWithArrow);
         hintView.setTextScaleX(hintScaleX);
-        stripView.addView(hintView);
         setLayoutWeight(
                 hintView, 1.0f - mCenterSuggestionWeight, ViewGroup.LayoutParams.MATCH_PARENT);
-
-        wordView.setOnClickListener(listener);
-        leftArrowView.setOnClickListener(listener);
-        hintView.setOnClickListener(listener);
-    }
-
-    public String getAddToDictionaryWord() {
-        // String tag is set at
-        // {@link #layoutAddToDictionaryHint(String,ViewGroup,int,CharSequence,OnClickListener}.
-        return (String)mWordToSaveView.getTag();
-    }
-
-    public boolean isAddToDictionaryShowing(final View v) {
-        return v == mWordToSaveView || v == mHintToSaveView || v == mLeftwardsArrowView;
     }
 
     private static void setLayoutWeight(final View v, final float weight, final int height) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index 88b57f9..32552eb 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -56,6 +56,7 @@
     static final boolean DBG = LatinImeLogger.sDBG;
 
     private final ViewGroup mSuggestionsStrip;
+    private final ViewGroup mAddToDictionaryStrip;
     MainKeyboardView mMainKeyboardView;
 
     private final View mMoreSuggestionsContainer;
@@ -70,6 +71,32 @@
     private SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
 
     private final SuggestionStripLayoutHelper mLayoutHelper;
+    private final StripVisibilityGroup mStripVisibilityGroup;
+
+    private static class StripVisibilityGroup {
+        private final View mSuggestionsStrip;
+        private final View mAddToDictionaryStrip;
+
+        public StripVisibilityGroup(final View suggestionsStrip, final View addToDictionaryStrip) {
+            mSuggestionsStrip = suggestionsStrip;
+            mAddToDictionaryStrip = addToDictionaryStrip;
+            showSuggestionsStrip();
+        }
+
+        public void showSuggestionsStrip() {
+            mSuggestionsStrip.setVisibility(VISIBLE);
+            mAddToDictionaryStrip.setVisibility(INVISIBLE);
+        }
+
+        public void showAddToDictionaryStrip() {
+            mSuggestionsStrip.setVisibility(INVISIBLE);
+            mAddToDictionaryStrip.setVisibility(VISIBLE);
+        }
+
+        public boolean isShowingAddToDictionaryStrip() {
+            return mAddToDictionaryStrip.getVisibility() == VISIBLE;
+        }
+    }
 
     /**
      * Construct a {@link SuggestionStripView} for showing suggestions to be picked by the user.
@@ -88,6 +115,9 @@
         inflater.inflate(R.layout.suggestions_strip, this);
 
         mSuggestionsStrip = (ViewGroup)findViewById(R.id.suggestions_strip);
+        mAddToDictionaryStrip = (ViewGroup)findViewById(R.id.add_to_dictionary_strip);
+        mStripVisibilityGroup = new StripVisibilityGroup(mSuggestionsStrip, mAddToDictionaryStrip);
+
         for (int pos = 0; pos < SuggestedWords.MAX_SUGGESTIONS; pos++) {
             final TextView word = (TextView)inflater.inflate(R.layout.suggestion_word, null);
             word.setOnClickListener(this);
@@ -137,14 +167,16 @@
     }
 
     public boolean isShowingAddToDictionaryHint() {
-        return mSuggestionsStrip.getChildCount() > 0
-                && mLayoutHelper.isAddToDictionaryShowing(mSuggestionsStrip.getChildAt(0));
+        return mStripVisibilityGroup.isShowingAddToDictionaryStrip();
     }
 
     public void showAddToDictionaryHint(final String word, final CharSequence hintText) {
-        clear();
-        mLayoutHelper.layoutAddToDictionaryHint(
-                word, mSuggestionsStrip, getWidth(), hintText, this);
+        mLayoutHelper.layoutAddToDictionaryHint(word, mAddToDictionaryStrip, getWidth(), hintText);
+        // {@link TextView#setTag()} is used to hold the word to be added to dictionary. The word
+        // will be extracted at {@link #onClick(View)}.
+        mAddToDictionaryStrip.setTag(word);
+        mAddToDictionaryStrip.setOnClickListener(this);
+        mStripVisibilityGroup.showAddToDictionaryStrip();
     }
 
     public boolean dismissAddToDictionaryHint() {
@@ -157,8 +189,7 @@
 
     public void clear() {
         mSuggestionsStrip.removeAllViews();
-        removeAllViews();
-        addView(mSuggestionsStrip);
+        mStripVisibilityGroup.showSuggestionsStrip();
         dismissMoreSuggestionsPanel();
     }
 
@@ -302,26 +333,26 @@
 
     @Override
     public void onClick(final View view) {
-        if (mLayoutHelper.isAddToDictionaryShowing(view)) {
-            mListener.addWordToUserDictionary(mLayoutHelper.getAddToDictionaryWord());
+        final Object tag = view.getTag();
+        // {@link String} tag is set at {@link #showAddToDictionaryHint(String,CharSequence)}.
+        if (tag instanceof String) {
+            final String wordToSave = (String)tag;
+            mListener.addWordToUserDictionary(wordToSave);
             clear();
             return;
         }
 
-        final Object tag = view.getTag();
-        // Integer tag is set at
+        // {@link Integer} tag is set at
         // {@link SuggestionStripLayoutHelper#setupWordViewsTextAndColor(SuggestedWords,int)} and
         // {@link SuggestionStripLayoutHelper#layoutPunctuationSuggestions(SuggestedWords,ViewGroup}
-        if (!(tag instanceof Integer)) {
-            return;
+        if (tag instanceof Integer) {
+            final int index = (Integer) tag;
+            if (index >= mSuggestedWords.size()) {
+                return;
+            }
+            final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
+            mListener.pickSuggestionManually(index, wordInfo);
         }
-        final int index = (Integer) tag;
-        if (index >= mSuggestedWords.size()) {
-            return;
-        }
-
-        final SuggestedWordInfo wordInfo = mSuggestedWords.getInfo(index);
-        mListener.pickSuggestionManually(index, wordInfo);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 2a8148d..b1f54c0 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -1117,14 +1117,13 @@
     private static final LogStatement LOGSTATEMENT_LATINIME_ONFINISHINPUTVIEWINTERNAL =
             new LogStatement("LatinIMEOnFinishInputViewInternal", false, false, "isTextTruncated",
                     "text");
-    public static void latinIME_onFinishInputViewInternal(final boolean finishingInput,
-            final int savedSelectionStart, final int savedSelectionEnd, final InputConnection ic) {
+    public static void latinIME_onFinishInputViewInternal(final boolean finishingInput) {
         // The finishingInput flag is set in InputMethodService.  It is true if called from
         // doFinishInput(), which can be called as part of doStartInput().  This can happen at times
         // when the IME is not closing, such as when powering up.  The finishinInput flag is false
         // if called from finishViews(), which is called from hideWindow() and onDestroy().  These
         // are the situations in which we want to finish up the researchLog.
-        if (ic != null && !finishingInput) {
+        if (!finishingInput) {
             final ResearchLogger researchLogger = getInstance();
             // Assume that OUTPUT_ENTIRE_BUFFER is only true when we don't care about privacy (e.g.
             // during a live user test), so the normal isPotentiallyPrivate and
diff --git a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
index ccdd567..939dedb 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
@@ -20,9 +20,9 @@
 import android.content.res.Resources;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
+import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
 
 import java.util.ArrayList;
@@ -30,7 +30,7 @@
 
 @SmallTest
 public class SubtypeLocaleUtilsTests extends AndroidTestCase {
-    // Locale to subtypes list.
+    // All input method subtypes of LatinIME.
     private final ArrayList<InputMethodSubtype> mSubtypesList = CollectionUtils.newArrayList();
 
     private RichInputMethodManager mRichImm;
@@ -62,6 +62,13 @@
         mRes = context.getResources();
         SubtypeLocaleUtils.init(context);
 
+        final InputMethodInfo imi = mRichImm.getInputMethodInfoOfThisIme();
+        final int subtypeCount = imi.getSubtypeCount();
+        for (int index = 0; index < subtypeCount; index++) {
+            final InputMethodSubtype subtype = imi.getSubtypeAt(index);
+            mSubtypesList.add(subtype);
+        }
+
         EN_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 Locale.US.toString(), "qwerty");
         EN_GB = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
@@ -99,14 +106,15 @@
 
     public void testAllFullDisplayName() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
-            final String subtypeName =
-                    SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+            final String subtypeName = SubtypeLocaleUtils
+                    .getSubtypeDisplayNameInSystemLocale(subtype);
             if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
-                final String noLanguage = mRes.getString(R.string.subtype_no_language);
-                assertTrue(subtypeName, subtypeName.contains(noLanguage));
+                final String layoutName = SubtypeLocaleUtils
+                        .getKeyboardLayoutSetDisplayName(subtype);
+                assertTrue(subtypeName, subtypeName.contains(layoutName));
             } else {
-                final String languageName =
-                        SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale());
+                final String languageName = SubtypeLocaleUtils
+                        .getSubtypeLocaleDisplayNameInSystemLocale(subtype.getLocale());
                 assertTrue(subtypeName, subtypeName.contains(languageName));
             }
         }
@@ -266,11 +274,11 @@
 
     public void testAllFullDisplayNameForSpacebar() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
-            final String subtypeName =
-                    SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+            final String subtypeName = SubtypeLocaleUtils
+                    .getSubtypeDisplayNameInSystemLocale(subtype);
             final String spacebarText = SubtypeLocaleUtils.getFullDisplayName(subtype);
-            final String languageName =
-                    SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale());
+            final String languageName = SubtypeLocaleUtils
+                    .getSubtypeLocaleDisplayName(subtype.getLocale());
             if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
                 assertFalse(subtypeName, spacebarText.contains(languageName));
             } else {
@@ -281,15 +289,16 @@
 
    public void testAllMiddleDisplayNameForSpacebar() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
-            final String subtypeName =
-                    SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+            final String subtypeName = SubtypeLocaleUtils
+                    .getSubtypeDisplayNameInSystemLocale(subtype);
             final String spacebarText = SubtypeLocaleUtils.getMiddleDisplayName(subtype);
             if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
                 assertEquals(subtypeName,
-                        SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype), spacebarText);
+                        SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype), spacebarText);
             } else {
+                final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
                 assertEquals(subtypeName,
-                        SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale()),
+                        SubtypeLocaleUtils.getSubtypeLocaleDisplayName(locale.getLanguage()),
                         spacebarText);
             }
         }
@@ -297,8 +306,8 @@
 
     public void testAllShortDisplayNameForSpacebar() {
         for (final InputMethodSubtype subtype : mSubtypesList) {
-            final String subtypeName =
-                    SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(subtype);
+            final String subtypeName = SubtypeLocaleUtils
+                    .getSubtypeDisplayNameInSystemLocale(subtype);
             final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
             final String spacebarText = SubtypeLocaleUtils.getShortDisplayName(subtype);
             final String languageCode = StringUtils.capitalizeFirstCodePoint(