Merge "Fix: The top left key is a proximity of nonexistent keys."
diff --git a/java/res/values-ka-rGE/strings.xml b/java/res/values-ka-rGE/strings.xml
index 056bc35..8fe415d 100644
--- a/java/res/values-ka-rGE/strings.xml
+++ b/java/res/values-ka-rGE/strings.xml
@@ -167,7 +167,7 @@
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"ინსტალაციისათვის აირჩიეთ ლექსიკონის ფაილი"</string>
     <string name="read_external_dictionary_confirm_install_message" msgid="4782116251651288054">"ნამდვილად გსურთ ამ ფაილის <xliff:g id="LANGUAGE_NAME">%s</xliff:g>-ისთვის ინსტალაცია?"</string>
     <string name="error" msgid="8940763624668513648">"წარმოიშვა შეცდომა"</string>
-    <string name="prefs_dump_contacts_dict" msgid="7227327764402323097">"კონტაქტების საქაღალდის ჩამოწერა"</string>
+    <string name="prefs_dump_contacts_dict" msgid="7227327764402323097">"კონტაქტების საქაღალდის ამონაწერი"</string>
     <string name="prefs_dump_user_dict" msgid="294870685041741951">"პერსონალური საქაღალდის ჩამოწერა"</string>
     <string name="prefs_dump_user_history_dict" msgid="6821075152449554628">"მომხმ. ისტორიის საქაღალდის ჩამოწერა"</string>
     <string name="prefs_dump_personalization_dict" msgid="7558387996151745284">"პერსონალიზაციის საქაღალდის ჩამოწერა"</string>
diff --git a/java/res/values-lo-rLA/strings.xml b/java/res/values-lo-rLA/strings.xml
index 9f28cd1..dae75e3 100644
--- a/java/res/values-lo-rLA/strings.xml
+++ b/java/res/values-lo-rLA/strings.xml
@@ -167,7 +167,7 @@
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"ເລືອກໄຟລ໌ວັດຈະນານຸກົມເພື່ອຕິດຕັ້ງ"</string>
     <string name="read_external_dictionary_confirm_install_message" msgid="4782116251651288054">"ຕິດ​ຕັ້ງ​ໄຟ​ລ໌​ນີ້​ສຳ​ລັບ <xliff:g id="LANGUAGE_NAME">%s</xliff:g> ແທ້ບໍ່??"</string>
     <string name="error" msgid="8940763624668513648">"ມີຂໍ້ຜິດພາດເກີດຂຶ້ນ"</string>
-    <string name="prefs_dump_contacts_dict" msgid="7227327764402323097">"ເທຂໍ້ມູນວັດຈະນານຸກົມລາຍຊື່ຜູ່ຕິດຕໍ່"</string>
+    <string name="prefs_dump_contacts_dict" msgid="7227327764402323097">"​​ດຶງ​ຂໍ້​ມູນ​ວັດ​ຈະ​ນາ​ນຸ​ກົມ​ລາຍ​ຊື່​ຜູ່​ຕິດ​ຕໍ່"</string>
     <string name="prefs_dump_user_dict" msgid="294870685041741951">"ເທຂໍ້ມູນວັດຈະນານຸກົມສ່ວນໂຕ"</string>
     <string name="prefs_dump_user_history_dict" msgid="6821075152449554628">"ເທຂໍ້ມູນວັດຈະນານຸກົມປະຫວັດຜູ່ໃຊ້"</string>
     <string name="prefs_dump_personalization_dict" msgid="7558387996151745284">"ເທຂໍ້ມູນວັດຈະນານຸກົມຄວາມເປັນໂຕຕົນ"</string>
diff --git a/java/src/com/android/inputmethod/event/InputTransaction.java b/java/src/com/android/inputmethod/event/InputTransaction.java
new file mode 100644
index 0000000..5e046a8
--- /dev/null
+++ b/java/src/com/android/inputmethod/event/InputTransaction.java
@@ -0,0 +1,48 @@
+/*
+ * Copyright (C) 2014 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.event;
+
+/**
+ * An object encapsulating a single transaction for input.
+ */
+public class InputTransaction {
+    // UPDATE_LATER is stronger than UPDATE_NOW. The reason for this is, if we have to update later,
+    // it's because something will change that we can't evaluate now, which means that even if we
+    // re-evaluate now we'll have to do it again later. The only case where that wouldn't apply
+    // would be if we needed to update now to find out the new state right away, but then we
+    // can't do it with this deferred mechanism anyway.
+    public static final int SHIFT_NO_UPDATE = 0;
+    public static final int SHIFT_UPDATE_NOW = 1;
+    public static final int SHIFT_UPDATE_LATER = 2;
+
+    // Initial conditions
+    public final int mShiftState;
+
+    // Outputs
+    private int mRequiredShiftUpdate = SHIFT_NO_UPDATE;
+
+    public InputTransaction(final int shiftState) {
+        mShiftState = shiftState;
+    }
+
+    public void requireShiftUpdate(final int updateType) {
+        mRequiredShiftUpdate = Math.max(mRequiredShiftUpdate, updateType);
+    }
+    public int getRequiredShiftUpdate() {
+        return mRequiredShiftUpdate;
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 6c56b8a..3590c48 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -700,12 +700,12 @@
     }
 
     @Override
-    public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
+    public void onCancelMoreKeysPanel() {
         PointerTracker.dismissAllMoreKeysPanels();
     }
 
     @Override
-    public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
+    public void onDismissMoreKeysPanel() {
         dimEntireKeyboard(false /* dimmed */);
         if (isShowingMoreKeysPanel()) {
             mMoreKeysPanel.removeFromParent();
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index 1891dfc..fc33197 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -122,7 +122,7 @@
         onMoveKeyInternal(x, y, pointerId);
         if (hasOldKey && mCurrentKey == null) {
             // If the pointer has moved too far away from any target then cancel the panel.
-            mController.onCancelMoreKeysPanel(this);
+            mController.onCancelMoreKeysPanel();
         }
     }
 
@@ -184,7 +184,7 @@
         if (!isShowingInParent()) {
             return;
         }
-        mController.onDismissMoreKeysPanel(this);
+        mController.onDismissMoreKeysPanel();
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
index b3422d1..7bddd09 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysPanel.java
@@ -29,26 +29,22 @@
 
         /**
          * Remove the current {@link MoreKeysPanel} from the target view.
-         * @param panel the panel to be dismissed.
          */
-        // TODO: Remove unused {@link MoreKeysPanel} argument.
-        public void onDismissMoreKeysPanel(final MoreKeysPanel panel);
+        public void onDismissMoreKeysPanel();
 
         /**
          * Instructs the parent to cancel the panel (e.g., when entering a different input mode).
-         * @param panel the panel to be canceled.
          */
-        // TODO: Remove unused {@link MoreKeysPanel} argument.
-        public void onCancelMoreKeysPanel(final MoreKeysPanel panel);
+        public void onCancelMoreKeysPanel();
     }
 
     public static final Controller EMPTY_CONTROLLER = new Controller() {
         @Override
         public void onShowMoreKeysPanel(final MoreKeysPanel panel) {}
         @Override
-        public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {}
+        public void onDismissMoreKeysPanel() {}
         @Override
-        public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {}
+        public void onCancelMoreKeysPanel() {}
     };
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 851ecc0..05e6ef0 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -40,7 +40,6 @@
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Locale;
-import java.util.Map;
 
 /**
  * Implements a static, compacted, binary dictionary of standard words.
@@ -142,8 +141,6 @@
         JniUtils.loadNativeLibrary();
     }
 
-    private static native boolean createEmptyDictFileNative(String filePath, long dictVersion,
-            String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray);
     private static native long openNative(String sourceDir, long dictOffset, long dictSize,
             boolean isUpdatable);
     private static native void getHeaderInfoNative(long dict, int[] outHeaderSize,
@@ -167,8 +164,6 @@
             int[] suggestOptions, int[] prevWordCodePointArray,
             int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes,
             int[] outputAutoCommitFirstWordConfidence);
-    private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
-    private static native int editDistanceNative(int[] before, int[] after);
     private static native void addUnigramWordNative(long dict, int[] word, int probability,
             int[] shortcutTarget, int shortcutProbability, boolean isNotAWord,
             boolean isBlacklisted, int timestamp);
@@ -179,24 +174,9 @@
             LanguageModelParam[] languageModelParams, int startIndex);
     private static native int calculateProbabilityNative(long dict, int unigramProbability,
             int bigramProbability);
-    private static native int setCurrentTimeForTestNative(int currentTime);
     private static native String getPropertyNative(long dict, String query);
     private static native boolean isCorruptedNative(long dict);
 
-    public static boolean createEmptyDictFile(final String filePath, final long dictVersion,
-            final Locale locale, final Map<String, String> attributeMap) {
-        final String[] keyArray = new String[attributeMap.size()];
-        final String[] valueArray = new String[attributeMap.size()];
-        int index = 0;
-        for (final String key : attributeMap.keySet()) {
-            keyArray[index] = key;
-            valueArray[index] = attributeMap.get(key);
-            index++;
-        }
-        return createEmptyDictFileNative(filePath, dictVersion, locale.toString(), keyArray,
-                valueArray);
-    }
-
     // TODO: Move native dict into session
     private final void loadDictionary(final String path, final long startOffset,
             final long length, final boolean isUpdatable) {
@@ -323,20 +303,6 @@
         return getFormatVersionNative(mNativeDict);
     }
 
-    public static float calcNormalizedScore(final String before, final String after,
-            final int score) {
-        return calcNormalizedScoreNative(StringUtils.toCodePointArray(before),
-                StringUtils.toCodePointArray(after), score);
-    }
-
-    public static int editDistance(final String before, final String after) {
-        if (before == null || after == null) {
-            throw new IllegalArgumentException();
-        }
-        return editDistanceNative(StringUtils.toCodePointArray(before),
-                StringUtils.toCodePointArray(after));
-    }
-
     @Override
     public boolean isValidWord(final String word) {
         return getFrequency(word) != NOT_A_PROBABILITY;
@@ -497,22 +463,6 @@
         return calculateProbabilityNative(mNativeDict, unigramProbability, bigramProbability);
     }
 
-    /**
-     * Control the current time to be used in the native code. If currentTime >= 0, this method sets
-     * the current time and gets into test mode.
-     * In test mode, set timestamp is used as the current time in the native code.
-     * If currentTime < 0, quit the test mode and returns to using time() to get the current time.
-     *
-     * @param currentTime seconds since the unix epoch
-     * @return current time got in the native code.
-     */
-    @UsedForTesting
-    public static int setCurrentTimeForTest(final int currentTime) {
-        final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime);
-        PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp);
-        return currentNativeTimestamp;
-    }
-
     @UsedForTesting
     public String getPropertyForTest(final String query) {
         if (!isValidDictionary()) return "";
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 26545ac..7847738 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -27,6 +27,7 @@
 import com.android.inputmethod.latin.makedict.WordProperty;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.utils.AsyncResultHolder;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.CombinedFormatUtils;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
@@ -228,7 +229,7 @@
     }
 
     private void createBinaryDictionaryLocked() {
-        BinaryDictionary.createEmptyDictFile(mDictFile.getAbsolutePath(),
+        BinaryDictionaryUtils.createEmptyDictFile(mDictFile.getAbsolutePath(),
                 DICTIONARY_FORMAT_VERSION, mLocale, getHeaderAttributeMap());
     }
 
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 4c2454c..44353ba 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -78,7 +78,6 @@
 import com.android.inputmethod.latin.suggestions.SuggestionStripViewAccessor;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
-import com.android.inputmethod.latin.utils.CompletionInfoUtils;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
 import com.android.inputmethod.latin.utils.IntentUtils;
@@ -124,9 +123,6 @@
     private View mKeyPreviewBackingView;
     private SuggestionStripView mSuggestionStripView;
 
-    // TODO[IL]: remove this member completely.
-    public CompletionInfo[] mApplicationSpecifiedCompletions;
-
     private RichInputMethodManager mRichImm;
     @UsedForTesting final KeyboardSwitcher mKeyboardSwitcher;
     private final SubtypeSwitcher mSubtypeSwitcher;
@@ -192,8 +188,9 @@
             final KeyboardSwitcher switcher = latinIme.mKeyboardSwitcher;
             switch (msg.what) {
             case MSG_UPDATE_SUGGESTION_STRIP:
+                cancelUpdateSuggestionStrip();
                 latinIme.mInputLogic.performUpdateSuggestionStripSync(
-                        latinIme.mSettings.getCurrent(), this /* handler */);
+                        latinIme.mSettings.getCurrent());
                 break;
             case MSG_UPDATE_SHIFT_STATE:
                 switcher.updateShiftState();
@@ -554,7 +551,7 @@
             // Create Suggest instance with the new dictionary facilitator.
             replaceSuggest(new Suggest(oldSuggest, dictionaryFacilitator));
         } else if (oldSuggest == null) {
-            initSuggestForLocale(oldSuggest, locale);
+            initSuggest();
         }
     }
 
@@ -812,7 +809,6 @@
         // The EditorInfo might have a flag that affects fullscreen mode.
         // Note: This call should be done by InputMethodService?
         updateFullscreenMode();
-        mApplicationSpecifiedCompletions = null;
 
         // The app calling setText() has the effect of clearing the composing
         // span, so we should reset our state unconditionally, even if restarting is true.
@@ -879,7 +875,7 @@
         }
         // This will set the punctuation suggestions if next word suggestion is off;
         // otherwise it will clear the suggestion strip.
-        setNeutralSuggestionStripInternal();
+        setNeutralSuggestionStrip();
 
         mHandler.cancelUpdateSuggestionStrip();
         mHandler.cancelDoubleSpacePeriodTimer();
@@ -954,8 +950,7 @@
         // NOTE: the test harness subclasses LatinIME and overrides isInputViewShown().
         // TODO: find a better way to simulate actual execution.
         if (isInputViewShown() &&
-                mInputLogic.onUpdateSelection(mSettings.getCurrent(), oldSelStart, oldSelEnd,
-                        newSelStart, newSelEnd, composingSpanStart, composingSpanEnd)) {
+                mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd)) {
             mKeyboardSwitcher.updateShiftState();
         }
 
@@ -1034,8 +1029,6 @@
             }
             return;
         }
-        mApplicationSpecifiedCompletions =
-                CompletionInfoUtils.removeNulls(applicationSpecifiedCompletions);
 
         final ArrayList<SuggestedWords.SuggestedWordInfo> applicationSuggestedWords =
                 SuggestedWords.getFromApplicationSpecifiedCompletions(
@@ -1050,18 +1043,6 @@
         }
     }
 
-    private void setSuggestionStripShownInternal(final boolean isSuggestionStripVisible) {
-        // TODO: Modify this if we support suggestions with hard keyboard
-        if (!onEvaluateInputViewShown() || !hasSuggestionStripView()) {
-            return;
-        }
-        if (isSuggestionStripVisible) {
-            mSuggestionStripView.setVisibility(View.VISIBLE);
-        } else {
-            mSuggestionStripView.setVisibility(isFullscreenMode() ? View.GONE : View.INVISIBLE);
-        }
-    }
-
     private int getAdjustedBackingViewHeight() {
         final int currentHeight = mKeyPreviewBackingView.getHeight();
         if (currentHeight > 0) {
@@ -1299,7 +1280,7 @@
 
     @Override
     public void onStartBatchInput() {
-        mInputLogic.onStartBatchInput(mSettings.getCurrent(),  mKeyboardSwitcher, mHandler);
+        mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
     }
 
     @Override
@@ -1309,7 +1290,7 @@
 
     @Override
     public void onEndBatchInput(final InputPointers batchPointers) {
-        mInputLogic.onEndBatchInput(mSettings.getCurrent(), batchPointers);
+        mInputLogic.onEndBatchInput(batchPointers);
     }
 
     @Override
@@ -1387,13 +1368,23 @@
     public void setSuggestedWords(final SuggestedWords suggestedWords,
             final boolean isSuggestionStripVisible) {
         mInputLogic.setSuggestedWords(suggestedWords);
+        // TODO: Modify this when we support suggestions with hard keyboard
         if (!hasSuggestionStripView()) {
             return;
         }
+        mKeyboardSwitcher.onAutoCorrectionStateChanged(suggestedWords.mWillAutoCorrect);
+        if (!onEvaluateInputViewShown()) {
+            return;
+        }
+        if (!isSuggestionStripVisible) {
+            mSuggestionStripView.setVisibility(isFullscreenMode() ? View.GONE : View.INVISIBLE);
+            return;
+        }
+        mSuggestionStripView.setVisibility(View.VISIBLE);
+
         final SettingsValues currentSettings = mSettings.getCurrent();
         final boolean showSuggestions;
-        if (SuggestedWords.EMPTY == suggestedWords
-                || suggestedWords.isPunctuationSuggestions()
+        if (SuggestedWords.EMPTY == suggestedWords || suggestedWords.isPunctuationSuggestions()
                 || !currentSettings.isSuggestionsRequested()) {
             showSuggestions = !mSuggestionStripView.maybeShowImportantNoticeTitle(
                     currentSettings.mInputAttributes);
@@ -1404,8 +1395,6 @@
             mSuggestionStripView.setSuggestions(suggestedWords,
                     SubtypeLocaleUtils.isRtlLanguage(mSubtypeSwitcher.getCurrentSubtype()));
         }
-        mKeyboardSwitcher.onAutoCorrectionStateChanged(suggestedWords.mWillAutoCorrect);
-        setSuggestionStripShownInternal(isSuggestionStripVisible);
     }
 
     // TODO[IL]: Move this out of LatinIME.
@@ -1449,32 +1438,6 @@
                 sequenceNumber, callback);
     }
 
-    // TODO[IL]: Move this to InputLogic
-    public SuggestedWords maybeRetrieveOlderSuggestions(final String typedWord,
-            final SuggestedWords suggestedWords, final SuggestedWords previousSuggestedWords) {
-        // TODO: consolidate this into getSuggestedWords
-        // We update the suggestion strip only when we have some suggestions to show, i.e. when
-        // the suggestion count is > 1; else, we leave the old suggestions, with the typed word
-        // replaced with the new one. However, when the length of the typed word is 1 or 0 (after
-        // a deletion typically), we do want to remove the old suggestions. Also, if we are showing
-        // the "add to dictionary" hint, we need to revert to suggestions - although it is unclear
-        // how we can come here if it's displayed.
-        if (suggestedWords.size() > 1 || typedWord.length() <= 1
-                || !hasSuggestionStripView() || isShowingAddToDictionaryHint()) {
-            return suggestedWords;
-        } else {
-            final SuggestedWords punctuationList =
-                    mSettings.getCurrent().mSpacingAndPunctuations.mSuggestPuncList;
-            final SuggestedWords oldSuggestedWords = previousSuggestedWords == punctuationList
-                    ? SuggestedWords.EMPTY : previousSuggestedWords;
-            final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
-                    SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
-            return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */,
-                    false /* typedWordValid */, false /* hasAutoCorrectionCandidate */,
-                    true /* isObsoleteSuggestions */, false /* isPrediction */);
-        }
-    }
-
     @Override
     public void showSuggestionStrip(final SuggestedWords sourceSuggestedWords) {
         final SuggestedWords suggestedWords =
@@ -1515,15 +1478,10 @@
         mSuggestionStripView.showAddToDictionaryHint(word);
     }
 
-    // TODO[IL]: Define a clean interface for this
     // This will show either an empty suggestion strip (if prediction is enabled) or
     // punctuation suggestions (if it's disabled).
     @Override
     public void setNeutralSuggestionStrip() {
-        setNeutralSuggestionStripInternal();
-    }
-
-    private void setNeutralSuggestionStripInternal() {
         final SettingsValues currentSettings = mSettings.getCurrent();
         final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
                 ? SuggestedWords.EMPTY : currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 1747eee..ba64028 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
 import com.android.inputmethod.latin.utils.AutoCorrectionUtils;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.BoundedTreeSet;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
@@ -309,7 +310,7 @@
         // than i because we added the typed word to mSuggestions without touching mScores.
         for (int i = 0; i < suggestionsSize - 1; ++i) {
             final SuggestedWordInfo cur = suggestions.get(i + 1);
-            final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
                     typedWord, cur.toString(), cur.mScore);
             final String scoreInfoString;
             if (normalizedScore > 0) {
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index 06bc90c..dc2c9fd 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -167,15 +167,10 @@
             final CompletionInfo[] infos) {
         final ArrayList<SuggestedWordInfo> result = CollectionUtils.newArrayList();
         for (final CompletionInfo info : infos) {
-            if (info == null) continue;
-            final CharSequence text = info.getText();
-            if (null == text) continue;
-            final SuggestedWordInfo suggestedWordInfo = new SuggestedWordInfo(text.toString(),
-                    SuggestedWordInfo.MAX_SCORE, SuggestedWordInfo.KIND_APP_DEFINED,
-                    Dictionary.DICTIONARY_APPLICATION_DEFINED,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
-            result.add(suggestedWordInfo);
+            if (null == info || null == info.getText()) {
+                continue;
+            }
+            result.add(new SuggestedWordInfo(info));
         }
         return result;
     }
@@ -234,6 +229,9 @@
         public static final int KIND_FLAG_EXACT_MATCH = 0x40000000;
 
         public final String mWord;
+        // The completion info from the application. Null for suggestions that don't come from
+        // the application (including keyboard-computed ones, so this is almost always null)
+        public final CompletionInfo mApplicationSpecifiedCompletionInfo;
         public final int mScore;
         public final int mKind; // one of the KIND_* constants above
         public final int mCodePointCount;
@@ -260,6 +258,7 @@
                 final Dictionary sourceDict, final int indexOfTouchPointOfSecondWord,
                 final int autoCommitFirstWordConfidence) {
             mWord = word;
+            mApplicationSpecifiedCompletionInfo = null;
             mScore = score;
             mKind = kind;
             mSourceDict = sourceDict;
@@ -268,6 +267,22 @@
             mAutoCommitFirstWordConfidence = autoCommitFirstWordConfidence;
         }
 
+        /**
+         * Create a new suggested word info from an application-specified completion.
+         * If the passed argument or its contained text is null, this throws a NPE.
+         * @param applicationSpecifiedCompletion The application-specified completion info.
+         */
+        public SuggestedWordInfo(final CompletionInfo applicationSpecifiedCompletion) {
+            mWord = applicationSpecifiedCompletion.getText().toString();
+            mApplicationSpecifiedCompletionInfo = applicationSpecifiedCompletion;
+            mScore = SuggestedWordInfo.MAX_SCORE;
+            mKind = SuggestedWordInfo.KIND_APP_DEFINED;
+            mSourceDict = Dictionary.DICTIONARY_APPLICATION_DEFINED;
+            mCodePointCount = StringUtils.codePointCount(mWord);
+            mIndexOfTouchPointOfSecondWord = SuggestedWordInfo.NOT_AN_INDEX;
+            mAutoCommitFirstWordConfidence = SuggestedWordInfo.NOT_A_CONFIDENCE;
+        }
+
         public boolean isEligibleForAutoCommit() {
             return (KIND_CORRECTION == mKind && NOT_AN_INDEX != mIndexOfTouchPointOfSecondWord);
         }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index f2f9f1e..daaac59 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -23,12 +23,12 @@
 import android.util.Log;
 import android.view.KeyCharacterMap;
 import android.view.KeyEvent;
-import android.view.inputmethod.CompletionInfo;
 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.event.InputTransaction;
 import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
@@ -74,7 +74,7 @@
 
     // 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;
+    private int mSpaceState;
     // Never null
     public SuggestedWords mSuggestedWords = SuggestedWords.EMPTY;
     // TODO: mSuggest should be touched by a single thread.
@@ -85,7 +85,7 @@
     public LastComposedWord mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
     public final WordComposer mWordComposer;
     public final RichInputConnection mConnection;
-    public final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
+    private final RecapitalizeStatus mRecapitalizeStatus = new RecapitalizeStatus();
 
     private int mDeleteCount;
     private long mLastKeyTime;
@@ -96,7 +96,7 @@
 
     // 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;
+    private boolean mIsAutoCorrectionIndicatorOn;
 
     public InputLogic(final LatinIME latinIME,
             final SuggestionStripViewAccessor suggestionStripViewAccessor) {
@@ -227,17 +227,16 @@
             }
         }
 
-        // TODO: stop relying on mApplicationSpecifiedCompletions. The SuggestionInfo object
-        // should contain a reference to the CompletionInfo instead.
-        if (settingsValues.isApplicationSpecifiedCompletionsOn()
-                && mLatinIME.mApplicationSpecifiedCompletions != null
-                && index >= 0 && index < mLatinIME.mApplicationSpecifiedCompletions.length) {
+        // TODO: We should not need the following branch. We should be able to take the same
+        // code path as for other kinds, use commitChosenWord, and do everything normally. We will
+        // however need to reset the suggestion strip right away, because we know we can't take
+        // the risk of calling commitCompletion twice because we don't know how the app will react.
+        if (SuggestedWordInfo.KIND_APP_DEFINED == suggestionInfo.mKind) {
             mSuggestedWords = SuggestedWords.EMPTY;
             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
             keyboardSwitcher.updateShiftState();
             resetComposingState(true /* alsoResetLastComposedWord */);
-            final CompletionInfo completionInfo = mLatinIME.mApplicationSpecifiedCompletions[index];
-            mConnection.commitCompletion(completionInfo);
+            mConnection.commitCompletion(suggestionInfo.mApplicationSpecifiedCompletionInfo);
             mConnection.endBatchEdit();
             return;
         }
@@ -290,19 +289,14 @@
      * 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) {
+    public boolean onUpdateSelection(final int oldSelStart, final int oldSelEnd,
+            final int newSelStart, final int newSelEnd) {
         if (mConnection.isBelatedExpectedUpdate(oldSelStart, newSelStart, oldSelEnd, newSelEnd)) {
             return false;
         }
@@ -335,8 +329,7 @@
             // we'd have the suggestion strip noticeably janky. To avoid that, we don't clear
             // it here, which means we'll keep outdated suggestions for a split second but the
             // visual result is better.
-            resetEntireInputState(settingsValues, newSelStart, newSelEnd,
-                    false /* clearSuggestionStrip */);
+            resetEntireInputState(newSelStart, newSelEnd, false /* clearSuggestionStrip */);
         } else {
             // resetEntireInputState calls resetCachesUponCursorMove, but forcing the
             // composition to end. But in all cases where we don't reset the entire input
@@ -372,6 +365,8 @@
             ResearchLogger.latinIME_onCodeInput(code, x, y);
         }
         final long when = SystemClock.uptimeMillis();
+        final InputTransaction inputTransaction = new InputTransaction(
+                getActualCapsMode(settingsValues, keyboardSwitcher.getKeyboardShiftMode()));
         if (code != Constants.CODE_DELETE
                 || when > mLastKeyTime + Constants.LONG_PRESS_MILLISECONDS) {
             mDeleteCount = 0;
@@ -396,12 +391,12 @@
         boolean didAutoCorrect = false;
         switch (code) {
         case Constants.CODE_DELETE:
-            handleBackspace(settingsValues, spaceState, handler, keyboardSwitcher);
+            handleBackspace(settingsValues, spaceState, inputTransaction, handler);
             LatinImeLogger.logOnDelete(x, y);
             break;
         case Constants.CODE_SHIFT:
             performRecapitalization(settingsValues);
-            keyboardSwitcher.updateShiftState();
+            inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
             break;
         case Constants.CODE_CAPSLOCK:
             // Note: Changing keyboard to shift lock state is handled in
@@ -456,12 +451,12 @@
                 // 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);
+                        x, y, spaceState, inputTransaction, handler);
             }
             break;
         case Constants.CODE_SHIFT_ENTER:
             didAutoCorrect = handleNonSpecialCharacter(settingsValues, Constants.CODE_ENTER,
-                    x, y, spaceState, keyboardSwitcher, handler);
+                    x, y, spaceState, inputTransaction, handler);
             break;
         case Constants.CODE_ALPHA_FROM_EMOJI:
             // Note: Switching back from Emoji keyboard to the main keyboard is being handled in
@@ -469,7 +464,7 @@
             break;
         default:
             didAutoCorrect = handleNonSpecialCharacter(settingsValues,
-                    code, x, y, spaceState, keyboardSwitcher, handler);
+                    code, x, y, spaceState, inputTransaction, handler);
             break;
         }
         // Reset after any single keystroke, except shift, capslock, and symbol-shift
@@ -481,6 +476,15 @@
             mEnteredText = null;
         }
         mConnection.endBatchEdit();
+        switch (inputTransaction.getRequiredShiftUpdate()) {
+        case InputTransaction.SHIFT_UPDATE_LATER:
+            mLatinIME.mHandler.postUpdateShiftState();
+            break;
+        case InputTransaction.SHIFT_UPDATE_NOW:
+            keyboardSwitcher.updateShiftState();
+            break;
+        default: // SHIFT_NO_UPDATE
+        }
     }
 
     public void onStartBatchInput(final SettingsValues settingsValues,
@@ -504,7 +508,7 @@
             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 batch input at the current cursor position.
-                resetEntireInputState(settingsValues, mConnection.getExpectedSelectionStart(),
+                resetEntireInputState(mConnection.getExpectedSelectionStart(),
                         mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             } else if (wordComposerSize <= 1) {
                 // We auto-correct the previous (typed, not gestured) string iff it's one character
@@ -585,8 +589,7 @@
         mInputLogicHandler.onUpdateBatchInput(batchPointers, mAutoCommitSequenceNumber);
     }
 
-    public void onEndBatchInput(final SettingsValues settingValues,
-            final InputPointers batchPointers) {
+    public void onEndBatchInput(final InputPointers batchPointers) {
         mInputLogicHandler.onEndBatchInput(batchPointers, mAutoCommitSequenceNumber);
         ++mAutoCommitSequenceNumber;
     }
@@ -630,18 +633,20 @@
      * @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.
+     * @param inputTransaction The transaction in progress.
      * @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) {
+            final InputTransaction inputTransaction,
+            // TODO: remove this argument
+            final LatinIME.UIHandler handler) {
         mSpaceState = SpaceState.NONE;
         final boolean didAutoCorrect;
         if (settingsValues.isWordSeparator(codePoint)
                 || Character.getType(codePoint) == Character.OTHER_SYMBOL) {
             didAutoCorrect = handleSeparator(settingsValues, codePoint,
-                    Constants.SUGGESTION_STRIP_COORDINATE == x, spaceState, keyboardSwitcher,
+                    Constants.SUGGESTION_STRIP_COORDINATE == x, spaceState, inputTransaction,
                     handler);
             if (settingsValues.mIsInternal) {
                 LatinImeLoggerUtils.onSeparator((char)codePoint, x, y);
@@ -658,14 +663,14 @@
                 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, mConnection.getExpectedSelectionStart(),
+                    resetEntireInputState(mConnection.getExpectedSelectionStart(),
                             mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
                 } else {
                     commitTyped(settingsValues, LastComposedWord.NOT_A_SEPARATOR);
                 }
             }
             handleNonSeparator(settingsValues, codePoint, x, y, spaceState,
-                    keyboardSwitcher, handler);
+                    inputTransaction, handler);
         }
         return didAutoCorrect;
     }
@@ -677,11 +682,13 @@
      * @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.
+     * @param inputTransaction The transaction in progress.
      */
     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) {
+            final InputTransaction inputTransaction,
+            // TODO: Remove this argument
+            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.
@@ -700,7 +707,7 @@
         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, mConnection.getExpectedSelectionStart(),
+            resetEntireInputState(mConnection.getExpectedSelectionStart(),
                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             isComposingWord = false;
         }
@@ -737,8 +744,7 @@
                 // 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(settingsValues, keyboardSwitcher.getKeyboardShiftMode()),
-                        getNthPreviousWordForSuggestion(
+                        inputTransaction.mShiftState, getNthPreviousWordForSuggestion(
                                 settingsValues.mSpacingAndPunctuations, 1 /* nthPreviousWord */));
             }
             mConnection.setComposingText(getTextWithUnderline(
@@ -750,7 +756,7 @@
             sendKeyCodePoint(settingsValues, codePoint);
 
             if (swapWeakSpace) {
-                swapSwapperAndSpace(keyboardSwitcher);
+                swapSwapperAndSpace(inputTransaction);
                 mSpaceState = SpaceState.WEAK;
             }
             // In case the "add to dictionary" hint was still displayed.
@@ -768,12 +774,14 @@
      * @param codePoint the code point associated with the key.
      * @param isFromSuggestionStrip whether this code point comes from the suggestion strip.
      * @param spaceState the space state at start of the batch input.
+     * @param inputTransaction The transaction in progress.
      * @return whether this caused an auto-correction to happen.
      */
     private boolean handleSeparator(final SettingsValues settingsValues,
             final int codePoint, final boolean isFromSuggestionStrip, final int spaceState,
-            // TODO: remove these arguments
-            final KeyboardSwitcher keyboardSwitcher, final LatinIME.UIHandler handler) {
+            final InputTransaction inputTransaction,
+            // TODO: remove this argument
+            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
@@ -782,7 +790,7 @@
         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, mConnection.getExpectedSelectionStart(),
+            resetEntireInputState(mConnection.getExpectedSelectionStart(),
                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
         }
         // isComposingWord() may have changed since we stored wasComposing
@@ -828,7 +836,7 @@
         if (Constants.CODE_SPACE == codePoint) {
             if (settingsValues.isSuggestionsRequested()) {
                 if (maybeDoubleSpacePeriod(settingsValues, handler)) {
-                    keyboardSwitcher.updateShiftState();
+                    inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
                     mSpaceState = SpaceState.DOUBLE;
                 } else if (!mSuggestedWords.isPunctuationSuggestions()) {
                     mSpaceState = SpaceState.WEAK;
@@ -839,7 +847,7 @@
             handler.postUpdateSuggestionStrip();
         } else {
             if (swapWeakSpace) {
-                swapSwapperAndSpace(keyboardSwitcher);
+                swapSwapperAndSpace(inputTransaction);
                 mSpaceState = SpaceState.SWAP_PUNCTUATION;
             } else if ((SpaceState.PHANTOM == spaceState
                     && settingsValues.isUsuallyFollowedBySpace(codePoint))
@@ -864,7 +872,7 @@
             mSuggestionStripViewAccessor.setNeutralSuggestionStrip();
         }
 
-        keyboardSwitcher.updateShiftState();
+        inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
         return didAutoCorrect;
     }
 
@@ -872,23 +880,24 @@
      * Handle a press on the backspace key.
      * @param settingsValues The current settings values.
      * @param spaceState The space state at start of this batch edit.
+     * @param inputTransaction The transaction in progress.
      */
     private void handleBackspace(final SettingsValues settingsValues, final int spaceState,
-            // TODO: remove these arguments
-            final LatinIME.UIHandler handler, final KeyboardSwitcher keyboardSwitcher) {
+            final InputTransaction inputTransaction,
+            // TODO: remove this argument
+            final LatinIME.UIHandler handler) {
         mSpaceState = SpaceState.NONE;
-        final int deleteCountAtStart = mDeleteCount;
         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();
+        inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_LATER);
 
         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, mConnection.getExpectedSelectionStart(),
+            resetEntireInputState(mConnection.getExpectedSelectionStart(),
                     mConnection.getExpectedSelectionEnd(), true /* clearSuggestionStrip */);
             // When we exit this if-clause, mWordComposer.isComposingWord() will return false.
         }
@@ -909,7 +918,7 @@
             if (!mWordComposer.isComposingWord()) {
                 // If we just removed the last character, auto-caps mode may have changed so we
                 // need to re-evaluate.
-                keyboardSwitcher.updateShiftState();
+                inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
             }
         } else {
             if (mLastComposedWord.canRevertCommit()) {
@@ -1021,7 +1030,7 @@
                         true /* includeResumedWordInSuggestions */);
             }
             // We just removed at least one character. We need to update the auto-caps state.
-            keyboardSwitcher.updateShiftState();
+            inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
         }
     }
 
@@ -1037,9 +1046,9 @@
      *
      * 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.
+     * @param inputTransaction The transaction in progress.
      */
-    // TODO: Remove this argument
-    private void swapSwapperAndSpace(final KeyboardSwitcher keyboardSwitcher) {
+    private void swapSwapperAndSpace(final InputTransaction inputTransaction) {
         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) {
@@ -1049,7 +1058,7 @@
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.latinIME_swapSwapperAndSpace(lastTwo, text);
             }
-            keyboardSwitcher.updateShiftState();
+            inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
         }
     }
 
@@ -1205,11 +1214,7 @@
                 timeStampInSeconds);
     }
 
-    public void performUpdateSuggestionStripSync(final SettingsValues settingsValues,
-            // TODO: Remove this argument
-            final LatinIME.UIHandler handler) {
-        handler.cancelUpdateSuggestionStrip();
-
+    public void performUpdateSuggestionStripSync(final SettingsValues settingsValues) {
         // Check if we have a suggestion engine attached.
         if (mSuggest == null || !settingsValues.isSuggestionsRequested()) {
             if (mWordComposer.isComposingWord()) {
@@ -1229,11 +1234,15 @@
                 SuggestedWords.NOT_A_SEQUENCE_NUMBER, new OnGetSuggestedWordsCallback() {
                     @Override
                     public void onGetSuggestedWords(final SuggestedWords suggestedWords) {
-                        final SuggestedWords suggestedWordsWithMaybeOlderSuggestions =
-                                mLatinIME.maybeRetrieveOlderSuggestions(
-                                        mWordComposer.getTypedWord(), suggestedWords,
-                                        mSuggestedWords);
-                        holder.set(suggestedWordsWithMaybeOlderSuggestions);
+                        final String typedWord = mWordComposer.getTypedWord();
+                        // Show new suggestions if we have at least one. Otherwise keep the old
+                        // suggestions with the new typed word. Exception: if the length of the
+                        // typed word is <= 1 (after a deletion typically) we clear old suggestions.
+                        if (suggestedWords.size() > 1 || typedWord.length() <= 1) {
+                            holder.set(suggestedWords);
+                        } else {
+                            holder.set(retrieveOlderSuggestions(typedWord, mSuggestedWords));
+                        }
                     }
                 }
         );
@@ -1485,7 +1494,9 @@
      */
     private int getActualCapsMode(final SettingsValues settingsValues,
             final int keyboardShiftMode) {
-        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) return keyboardShiftMode;
+        if (keyboardShiftMode != WordComposer.CAPS_MODE_AUTO_SHIFTED) {
+            return keyboardShiftMode;
+        }
         final int auto = getCurrentAutoCapsState(settingsValues);
         if (0 != (auto & TextUtils.CAP_MODE_CHARACTERS)) {
             return WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED;
@@ -1623,15 +1634,13 @@
      * 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.
      * @param clearSuggestionStrip whether this method should clear the suggestion strip.
      */
     // 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 clearSuggestionStrip) {
+    private void resetEntireInputState(final int newSelStart, final int newSelEnd,
+            final boolean clearSuggestionStrip) {
         final boolean shouldFinishComposition = mWordComposer.isComposingWord();
         resetComposingState(true /* alsoResetLastComposedWord */);
         if (clearSuggestionStrip) {
@@ -1649,8 +1658,7 @@
      *
      * @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) {
+    private void resetComposingState(final boolean alsoResetLastComposedWord) {
         mWordComposer.reset();
         if (alsoResetLastComposedWord) {
             mLastComposedWord = LastComposedWord.NOT_A_COMPOSED_WORD;
@@ -1658,6 +1666,27 @@
     }
 
     /**
+     * Make a {@link com.android.inputmethod.latin.SuggestedWords} object containing a typed word
+     * and obsolete suggestions.
+     * See {@link com.android.inputmethod.latin.SuggestedWords#getTypedWordAndPreviousSuggestions(
+     *      String, com.android.inputmethod.latin.SuggestedWords)}.
+     * @param typedWord The typed word as a string.
+     * @param previousSuggestedWords The previously suggested words.
+     * @return Obsolete suggestions with the newly typed word.
+     */
+    private SuggestedWords retrieveOlderSuggestions(final String typedWord,
+            final SuggestedWords previousSuggestedWords) {
+        final SuggestedWords oldSuggestedWords =
+                previousSuggestedWords.isPunctuationSuggestions() ? SuggestedWords.EMPTY
+                        : previousSuggestedWords;
+        final ArrayList<SuggestedWords.SuggestedWordInfo> typedWordAndPreviousSuggestions =
+                SuggestedWords.getTypedWordAndPreviousSuggestions(typedWord, oldSuggestedWords);
+        return new SuggestedWords(typedWordAndPreviousSuggestions, null /* rawSuggestions */,
+                false /* typedWordValid */, false /* hasAutoCorrectionCandidate */,
+                true /* isObsoleteSuggestions */, false /* isPrediction */);
+    }
+
+    /**
      * 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
@@ -1674,9 +1703,8 @@
      * @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) {
+    // TODO: Shouldn't this go in some *Utils class instead?
+    private CharSequence getTextWithUnderline(final String text) {
         return mIsAutoCorrectionIndicatorOn
                 ? SuggestionSpanUtils.getTextWithAutoCorrectionIndicatorUnderline(mLatinIME, text)
                 : text;
@@ -1741,8 +1769,7 @@
      *
      * @param settingsValues the current values of the settings.
      */
-    // TODO: Make this private.
-    public void promotePhantomSpace(final SettingsValues settingsValues) {
+    private void promotePhantomSpace(final SettingsValues settingsValues) {
         if (settingsValues.shouldInsertSpacesAutomatically()
                 && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
                 && !mConnection.textBeforeCursorLooksLikeURL()) {
@@ -1848,7 +1875,8 @@
             final LatinIME.UIHandler handler) {
         // Complete any pending suggestions query first
         if (handler.hasPendingUpdateSuggestions()) {
-            performUpdateSuggestionStripSync(settingsValues, handler);
+            handler.cancelUpdateSuggestionStrip();
+            performUpdateSuggestionStripSync(settingsValues);
         }
         final String typedAutoCorrection = mWordComposer.getAutoCorrectionOrNull();
         final String typedWord = mWordComposer.getTypedWord();
@@ -1892,8 +1920,7 @@
      * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
      * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
      */
-    // TODO: Make this private
-    public void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
+    private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
             final int commitType, final String separatorString) {
         final SuggestedWords suggestedWords = mSuggestedWords;
         final CharSequence chosenWordWithSuggestions =
diff --git a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index 1050d1b..a50bad9 100644
--- a/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/java/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -22,6 +22,7 @@
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.io.File;
@@ -54,7 +55,7 @@
         if (!mDictPlacedDir.isDirectory()) {
             throw new UnsupportedFormatException("Given path is not a directory.");
         }
-        if (!BinaryDictionary.createEmptyDictFile(mDictPlacedDir.getAbsolutePath(),
+        if (!BinaryDictionaryUtils.createEmptyDictFile(mDictPlacedDir.getAbsolutePath(),
                 FormatSpec.VERSION4, LocaleUtils.constructLocaleFromString(
                 dict.mOptions.mAttributes.get(DictionaryHeader.DICTIONARY_LOCALE_KEY)),
                 dict.mOptions.mAttributes)) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index dae36f7..a07e8eb 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -37,6 +37,7 @@
 import com.android.inputmethod.latin.SynchronouslyLoadedUserBinaryDictionary;
 import com.android.inputmethod.latin.UserBinaryDictionary;
 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
@@ -320,7 +321,7 @@
                     hasRecommendedSuggestions = false;
                 } else {
                     gatheredSuggestions = EMPTY_STRING_ARRAY;
-                    final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+                    final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
                             mOriginalText, mBestSuggestion, mBestScore);
                     hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
                 }
@@ -355,7 +356,7 @@
                 final int bestScore = mScores[mLength - 1];
                 final String bestSuggestion = mSuggestions.get(0);
                 final float normalizedScore =
-                        BinaryDictionary.calcNormalizedScore(
+                        BinaryDictionaryUtils.calcNormalizedScore(
                                 mOriginalText, bestSuggestion.toString(), bestScore);
                 hasRecommendedSuggestions = (normalizedScore > mRecommendedThreshold);
                 if (DBG) {
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index be457ca..43cb11b 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -280,8 +280,8 @@
     private final MoreKeysPanel.Controller mMoreSuggestionsController =
             new MoreKeysPanel.Controller() {
         @Override
-        public void onDismissMoreKeysPanel(final MoreKeysPanel panel) {
-            mMainKeyboardView.onDismissMoreKeysPanel(panel);
+        public void onDismissMoreKeysPanel() {
+            mMainKeyboardView.onDismissMoreKeysPanel();
         }
 
         @Override
@@ -290,7 +290,7 @@
         }
 
         @Override
-        public void onCancelMoreKeysPanel(final MoreKeysPanel panel) {
+        public void onCancelMoreKeysPanel() {
             dismissMoreSuggestionsPanel();
         }
     };
@@ -321,8 +321,10 @@
         }
         // Dismiss another {@link MoreKeysPanel} that may be being showed, for example
         // {@link MoreKeysKeyboardView}.
-        // TODO: Remove unused null argument.
-        mMainKeyboardView.onDismissMoreKeysPanel(null /* unused */);
+        mMainKeyboardView.onDismissMoreKeysPanel();
+        // Dismiss all key previews and sliding key input preview that may be being showed.
+        mMainKeyboardView.dismissAllKeyPreviews();
+        mMainKeyboardView.dismissSlidingKeyInputPreview();
         final int stripWidth = getWidth();
         final View container = mMoreSuggestionsContainer;
         final int maxWidth = stripWidth - container.getPaddingLeft() - container.getPaddingRight();
diff --git a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
index 37c173f..22b9b77 100644
--- a/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AutoCorrectionUtils.java
@@ -40,7 +40,7 @@
             final int autoCorrectionSuggestionScore = suggestion.mScore;
             // TODO: when the normalized score of the first suggestion is nearly equals to
             //       the normalized score of the second suggestion, behave less aggressive.
-            final float normalizedScore = BinaryDictionary.calcNormalizedScore(
+            final float normalizedScore = BinaryDictionaryUtils.calcNormalizedScore(
                     consideredWord, suggestion.mWord, autoCorrectionSuggestionScore);
             if (DBG) {
                 Log.d(TAG, "Normalized " + consideredWord + "," + suggestion + ","
@@ -71,9 +71,8 @@
         if (typedWordLength < MINIMUM_SAFETY_NET_CHAR_LENGTH) {
             return false;
         }
-        final int maxEditDistanceOfNativeDictionary =
-                (typedWordLength < 5 ? 2 : typedWordLength / 2) + 1;
-        final int distance = BinaryDictionary.editDistance(typedWord, suggestion);
+        final int maxEditDistanceOfNativeDictionary = (typedWordLength / 2) + 1;
+        final int distance = BinaryDictionaryUtils.editDistance(typedWord, suggestion);
         if (DBG) {
             Log.d(TAG, "Autocorrected edit distance = " + distance
                     + ", " + maxEditDistanceOfNativeDictionary);
diff --git a/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
new file mode 100644
index 0000000..6872285
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtils.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2014 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.utils;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.personalization.PersonalizationHelper;
+
+import java.util.Locale;
+import java.util.Map;
+
+public final class BinaryDictionaryUtils {
+    private static final String TAG = BinaryDictionaryUtils.class.getSimpleName();
+
+    private BinaryDictionaryUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    static {
+        JniUtils.loadNativeLibrary();
+    }
+
+    private static native boolean createEmptyDictFileNative(String filePath, long dictVersion,
+            String locale, String[] attributeKeyStringArray, String[] attributeValueStringArray);
+    private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
+    private static native int editDistanceNative(int[] before, int[] after);
+    private static native int setCurrentTimeForTestNative(int currentTime);
+
+    public static boolean createEmptyDictFile(final String filePath, final long dictVersion,
+            final Locale locale, final Map<String, String> attributeMap) {
+        final String[] keyArray = new String[attributeMap.size()];
+        final String[] valueArray = new String[attributeMap.size()];
+        int index = 0;
+        for (final String key : attributeMap.keySet()) {
+            keyArray[index] = key;
+            valueArray[index] = attributeMap.get(key);
+            index++;
+        }
+        return createEmptyDictFileNative(filePath, dictVersion, locale.toString(), keyArray,
+                valueArray);
+    }
+
+    public static float calcNormalizedScore(final String before, final String after,
+            final int score) {
+        return calcNormalizedScoreNative(StringUtils.toCodePointArray(before),
+                StringUtils.toCodePointArray(after), score);
+    }
+
+    public static int editDistance(final String before, final String after) {
+        if (before == null || after == null) {
+            throw new IllegalArgumentException();
+        }
+        return editDistanceNative(StringUtils.toCodePointArray(before),
+                StringUtils.toCodePointArray(after));
+    }
+
+    /**
+     * Control the current time to be used in the native code. If currentTime >= 0, this method sets
+     * the current time and gets into test mode.
+     * In test mode, set timestamp is used as the current time in the native code.
+     * If currentTime < 0, quit the test mode and returns to using time() to get the current time.
+     *
+     * @param currentTime seconds since the unix epoch
+     * @return current time got in the native code.
+     */
+    @UsedForTesting
+    public static int setCurrentTimeForTest(final int currentTime) {
+        final int currentNativeTimestamp = setCurrentTimeForTestNative(currentTime);
+        PersonalizationHelper.currentTimeChangedForTesting(currentNativeTimestamp);
+        return currentNativeTimestamp;
+    }
+}
diff --git a/native/jni/NativeFileList.mk b/native/jni/NativeFileList.mk
index eb24df6..82237dc 100644
--- a/native/jni/NativeFileList.mk
+++ b/native/jni/NativeFileList.mk
@@ -15,6 +15,7 @@
 LATIN_IME_JNI_SRC_FILES := \
     com_android_inputmethod_keyboard_ProximityInfo.cpp \
     com_android_inputmethod_latin_BinaryDictionary.cpp \
+    com_android_inputmethod_latin_BinaryDictionaryUtils.cpp \
     com_android_inputmethod_latin_DicTraverseSession.cpp \
     jni_common.cpp
 
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index bb54cbd..7a7816d 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -27,8 +27,6 @@
 #include "suggest/core/dictionary/word_property.h"
 #include "suggest/core/suggest_options.h"
 #include "suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.h"
-#include "suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h"
-#include "utils/autocorrection_threshold_utils.h"
 #include "utils/char_utils.h"
 #include "utils/time_keeper.h"
 
@@ -36,49 +34,6 @@
 
 class ProximityInfo;
 
-// TODO: Move to makedict.
-static jboolean latinime_BinaryDictionary_createEmptyDictFile(JNIEnv *env, jclass clazz,
-        jstring filePath, jlong dictVersion, jstring locale, jobjectArray attributeKeyStringArray,
-        jobjectArray attributeValueStringArray) {
-    const jsize filePathUtf8Length = env->GetStringUTFLength(filePath);
-    char filePathChars[filePathUtf8Length + 1];
-    env->GetStringUTFRegion(filePath, 0, env->GetStringLength(filePath), filePathChars);
-    filePathChars[filePathUtf8Length] = '\0';
-    jsize localeLength = env->GetStringLength(locale);
-    jchar localeCodePoints[localeLength];
-    env->GetStringRegion(locale, 0, localeLength, localeCodePoints);
-    const int keyCount = env->GetArrayLength(attributeKeyStringArray);
-    const int valueCount = env->GetArrayLength(attributeValueStringArray);
-    if (keyCount != valueCount) {
-        return false;
-    }
-
-    DictionaryHeaderStructurePolicy::AttributeMap attributeMap;
-    for (int i = 0; i < keyCount; i++) {
-        jstring keyString = static_cast<jstring>(
-                env->GetObjectArrayElement(attributeKeyStringArray, i));
-        const jsize keyUtf8Length = env->GetStringUTFLength(keyString);
-        char keyChars[keyUtf8Length + 1];
-        env->GetStringUTFRegion(keyString, 0, env->GetStringLength(keyString), keyChars);
-        keyChars[keyUtf8Length] = '\0';
-        DictionaryHeaderStructurePolicy::AttributeMap::key_type key;
-        HeaderReadWriteUtils::insertCharactersIntoVector(keyChars, &key);
-
-        jstring valueString = static_cast<jstring>(
-                env->GetObjectArrayElement(attributeValueStringArray, i));
-        const jsize valueUtf8Length = env->GetStringUTFLength(valueString);
-        char valueChars[valueUtf8Length + 1];
-        env->GetStringUTFRegion(valueString, 0, env->GetStringLength(valueString), valueChars);
-        valueChars[valueUtf8Length] = '\0';
-        DictionaryHeaderStructurePolicy::AttributeMap::mapped_type value;
-        HeaderReadWriteUtils::insertCharactersIntoVector(valueChars, &value);
-        attributeMap[key] = value;
-    }
-
-    return DictFileWritingUtils::createEmptyDictFile(filePathChars, static_cast<int>(dictVersion),
-            CharUtils::convertShortArrayToIntVector(localeCodePoints, localeLength), &attributeMap);
-}
-
 static jlong latinime_BinaryDictionary_open(JNIEnv *env, jclass clazz, jstring sourceDir,
         jlong dictOffset, jlong dictSize, jboolean isUpdatable) {
     PROF_OPEN;
@@ -335,30 +290,6 @@
             outShortcutProbabilities);
 }
 
-static jfloat latinime_BinaryDictionary_calcNormalizedScore(JNIEnv *env, jclass clazz,
-        jintArray before, jintArray after, jint score) {
-    jsize beforeLength = env->GetArrayLength(before);
-    jsize afterLength = env->GetArrayLength(after);
-    int beforeCodePoints[beforeLength];
-    int afterCodePoints[afterLength];
-    env->GetIntArrayRegion(before, 0, beforeLength, beforeCodePoints);
-    env->GetIntArrayRegion(after, 0, afterLength, afterCodePoints);
-    return AutocorrectionThresholdUtils::calcNormalizedScore(beforeCodePoints, beforeLength,
-            afterCodePoints, afterLength, score);
-}
-
-static jint latinime_BinaryDictionary_editDistance(JNIEnv *env, jclass clazz, jintArray before,
-        jintArray after) {
-    jsize beforeLength = env->GetArrayLength(before);
-    jsize afterLength = env->GetArrayLength(after);
-    int beforeCodePoints[beforeLength];
-    int afterCodePoints[afterLength];
-    env->GetIntArrayRegion(before, 0, beforeLength, beforeCodePoints);
-    env->GetIntArrayRegion(after, 0, afterLength, afterCodePoints);
-    return AutocorrectionThresholdUtils::editDistance(beforeCodePoints, beforeLength,
-            afterCodePoints, afterLength);
-}
-
 static void latinime_BinaryDictionary_addUnigramWord(JNIEnv *env, jclass clazz, jlong dict,
         jintArray word, jint probability, jintArray shortcutTarget, jint shortuctProbability,
         jboolean isNotAWord, jboolean isBlacklisted, jint timestamp) {
@@ -518,17 +449,6 @@
     return env->NewStringUTF(resultChars);
 }
 
-static int latinime_BinaryDictionary_setCurrentTimeForTest(JNIEnv *env, jclass clazz,
-        jint currentTime) {
-    if (currentTime >= 0) {
-        TimeKeeper::startTestModeWithForceCurrentTime(currentTime);
-    } else {
-        TimeKeeper::stopTestMode();
-    }
-    TimeKeeper::setCurrentTime();
-    return TimeKeeper::peekCurrentTime();
-}
-
 static bool latinime_BinaryDictionary_isCorruptedNative(JNIEnv *env, jclass clazz, jlong dict) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) {
@@ -539,12 +459,6 @@
 
 static const JNINativeMethod sMethods[] = {
     {
-        const_cast<char *>("createEmptyDictFileNative"),
-        const_cast<char *>(
-                "(Ljava/lang/String;JLjava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)Z"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_createEmptyDictFile)
-    },
-    {
         const_cast<char *>("openNative"),
         const_cast<char *>("(Ljava/lang/String;JJZ)J"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_open)
@@ -606,16 +520,6 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_getNextWord)
     },
     {
-        const_cast<char *>("calcNormalizedScoreNative"),
-        const_cast<char *>("([I[II)F"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_calcNormalizedScore)
-    },
-    {
-        const_cast<char *>("editDistanceNative"),
-        const_cast<char *>("([I[I)I"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_editDistance)
-    },
-    {
         const_cast<char *>("addUnigramWordNative"),
         const_cast<char *>("(J[II[IIZZI)V"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_addUnigramWord)
@@ -642,11 +546,6 @@
         reinterpret_cast<void *>(latinime_BinaryDictionary_calculateProbabilityNative)
     },
     {
-        const_cast<char *>("setCurrentTimeForTestNative"),
-        const_cast<char *>("(I)I"),
-        reinterpret_cast<void *>(latinime_BinaryDictionary_setCurrentTimeForTest)
-    },
-    {
         const_cast<char *>("getPropertyNative"),
         const_cast<char *>("(JLjava/lang/String;)Ljava/lang/String;"),
         reinterpret_cast<void *>(latinime_BinaryDictionary_getProperty)
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.cpp
new file mode 100644
index 0000000..f723664
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.cpp
@@ -0,0 +1,136 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#define LOG_TAG "LatinIME: jni: BinaryDictionaryUtils"
+
+#include "com_android_inputmethod_latin_BinaryDictionaryUtils.h"
+
+#include "defines.h"
+#include "jni.h"
+#include "jni_common.h"
+#include "suggest/policyimpl/dictionary/utils/dict_file_writing_utils.h"
+#include "utils/autocorrection_threshold_utils.h"
+#include "utils/char_utils.h"
+#include "utils/time_keeper.h"
+
+namespace latinime {
+
+static jboolean latinime_BinaryDictionaryUtils_createEmptyDictFile(JNIEnv *env, jclass clazz,
+        jstring filePath, jlong dictVersion, jstring locale, jobjectArray attributeKeyStringArray,
+        jobjectArray attributeValueStringArray) {
+    const jsize filePathUtf8Length = env->GetStringUTFLength(filePath);
+    char filePathChars[filePathUtf8Length + 1];
+    env->GetStringUTFRegion(filePath, 0, env->GetStringLength(filePath), filePathChars);
+    filePathChars[filePathUtf8Length] = '\0';
+    jsize localeLength = env->GetStringLength(locale);
+    jchar localeCodePoints[localeLength];
+    env->GetStringRegion(locale, 0, localeLength, localeCodePoints);
+    const int keyCount = env->GetArrayLength(attributeKeyStringArray);
+    const int valueCount = env->GetArrayLength(attributeValueStringArray);
+    if (keyCount != valueCount) {
+        return false;
+    }
+
+    DictionaryHeaderStructurePolicy::AttributeMap attributeMap;
+    for (int i = 0; i < keyCount; i++) {
+        jstring keyString = static_cast<jstring>(
+                env->GetObjectArrayElement(attributeKeyStringArray, i));
+        const jsize keyUtf8Length = env->GetStringUTFLength(keyString);
+        char keyChars[keyUtf8Length + 1];
+        env->GetStringUTFRegion(keyString, 0, env->GetStringLength(keyString), keyChars);
+        keyChars[keyUtf8Length] = '\0';
+        DictionaryHeaderStructurePolicy::AttributeMap::key_type key;
+        HeaderReadWriteUtils::insertCharactersIntoVector(keyChars, &key);
+
+        jstring valueString = static_cast<jstring>(
+                env->GetObjectArrayElement(attributeValueStringArray, i));
+        const jsize valueUtf8Length = env->GetStringUTFLength(valueString);
+        char valueChars[valueUtf8Length + 1];
+        env->GetStringUTFRegion(valueString, 0, env->GetStringLength(valueString), valueChars);
+        valueChars[valueUtf8Length] = '\0';
+        DictionaryHeaderStructurePolicy::AttributeMap::mapped_type value;
+        HeaderReadWriteUtils::insertCharactersIntoVector(valueChars, &value);
+        attributeMap[key] = value;
+    }
+
+    return DictFileWritingUtils::createEmptyDictFile(filePathChars, static_cast<int>(dictVersion),
+            CharUtils::convertShortArrayToIntVector(localeCodePoints, localeLength), &attributeMap);
+}
+
+static jfloat latinime_BinaryDictionaryUtils_calcNormalizedScore(JNIEnv *env, jclass clazz,
+        jintArray before, jintArray after, jint score) {
+    jsize beforeLength = env->GetArrayLength(before);
+    jsize afterLength = env->GetArrayLength(after);
+    int beforeCodePoints[beforeLength];
+    int afterCodePoints[afterLength];
+    env->GetIntArrayRegion(before, 0, beforeLength, beforeCodePoints);
+    env->GetIntArrayRegion(after, 0, afterLength, afterCodePoints);
+    return AutocorrectionThresholdUtils::calcNormalizedScore(beforeCodePoints, beforeLength,
+            afterCodePoints, afterLength, score);
+}
+
+static jint latinime_BinaryDictionaryUtils_editDistance(JNIEnv *env, jclass clazz, jintArray before,
+        jintArray after) {
+    jsize beforeLength = env->GetArrayLength(before);
+    jsize afterLength = env->GetArrayLength(after);
+    int beforeCodePoints[beforeLength];
+    int afterCodePoints[afterLength];
+    env->GetIntArrayRegion(before, 0, beforeLength, beforeCodePoints);
+    env->GetIntArrayRegion(after, 0, afterLength, afterCodePoints);
+    return AutocorrectionThresholdUtils::editDistance(beforeCodePoints, beforeLength,
+            afterCodePoints, afterLength);
+}
+
+static int latinime_BinaryDictionaryUtils_setCurrentTimeForTest(JNIEnv *env, jclass clazz,
+        jint currentTime) {
+    if (currentTime >= 0) {
+        TimeKeeper::startTestModeWithForceCurrentTime(currentTime);
+    } else {
+        TimeKeeper::stopTestMode();
+    }
+    TimeKeeper::setCurrentTime();
+    return TimeKeeper::peekCurrentTime();
+}
+
+static const JNINativeMethod sMethods[] = {
+    {
+        const_cast<char *>("createEmptyDictFileNative"),
+        const_cast<char *>(
+                "(Ljava/lang/String;JLjava/lang/String;[Ljava/lang/String;[Ljava/lang/String;)Z"),
+        reinterpret_cast<void *>(latinime_BinaryDictionaryUtils_createEmptyDictFile)
+    },
+    {
+        const_cast<char *>("calcNormalizedScoreNative"),
+        const_cast<char *>("([I[II)F"),
+        reinterpret_cast<void *>(latinime_BinaryDictionaryUtils_calcNormalizedScore)
+    },
+    {
+        const_cast<char *>("editDistanceNative"),
+        const_cast<char *>("([I[I)I"),
+        reinterpret_cast<void *>(latinime_BinaryDictionaryUtils_editDistance)
+    },
+    {
+        const_cast<char *>("setCurrentTimeForTestNative"),
+        const_cast<char *>("(I)I"),
+        reinterpret_cast<void *>(latinime_BinaryDictionaryUtils_setCurrentTimeForTest)
+    }
+};
+
+int register_BinaryDictionaryUtils(JNIEnv *env) {
+    const char *const kClassPathName = "com/android/inputmethod/latin/utils/BinaryDictionaryUtils";
+    return registerNativeMethods(env, kClassPathName, sMethods, NELEMS(sMethods));
+}
+} // namespace latinime
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.h b/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.h
new file mode 100644
index 0000000..38edcd2
--- /dev/null
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionaryUtils.h
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2014 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.
+ */
+
+#ifndef _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARYUTILS_H
+#define _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARYUTILS_H
+
+#include "jni.h"
+
+namespace latinime {
+int register_BinaryDictionaryUtils(JNIEnv *env);
+} // namespace latinime
+#endif // _COM_ANDROID_INPUTMETHOD_LATIN_BINARYDICTIONARYUTILS_H
diff --git a/native/jni/jni_common.cpp b/native/jni/jni_common.cpp
index f2867d7..9fa7ef8 100644
--- a/native/jni/jni_common.cpp
+++ b/native/jni/jni_common.cpp
@@ -20,6 +20,7 @@
 
 #include "com_android_inputmethod_keyboard_ProximityInfo.h"
 #include "com_android_inputmethod_latin_BinaryDictionary.h"
+#include "com_android_inputmethod_latin_BinaryDictionaryUtils.h"
 #include "com_android_inputmethod_latin_DicTraverseSession.h"
 #include "defines.h"
 
@@ -42,6 +43,10 @@
         AKLOGE("ERROR: BinaryDictionary native registration failed");
         return -1;
     }
+    if (!latinime::register_BinaryDictionaryUtils(env)) {
+        AKLOGE("ERROR: BinaryDictionaryUtils native registration failed");
+        return -1;
+    }
     if (!latinime::register_DicTraverseSession(env)) {
         AKLOGE("ERROR: DicTraverseSession native registration failed");
         return -1;
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelTests.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelTests.java
new file mode 100644
index 0000000..06139b8
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetActionLabelTests.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2014 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.keyboard;
+
+import android.content.res.Resources;
+import android.test.suitebuilder.annotation.MediumTest;
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
+import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.utils.RunInLocale;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+@MediumTest
+public final class KeyboardLayoutSetActionLabelTests extends KeyboardLayoutSetTestsBase {
+    private static void doTestActionKey(final String tag, final KeyboardLayoutSet layoutSet,
+            final int elementId, final String label, final int iconId) {
+        final Keyboard keyboard = layoutSet.getKeyboard(elementId);
+        final Key enterKey = keyboard.getKey(Constants.CODE_ENTER);
+        assertNotNull(tag + " enter key on " + keyboard.mId, enterKey);
+        assertEquals(tag + " enter label " + enterKey, label, enterKey.getLabel());
+        assertEquals(tag + " enter icon " + enterKey, iconId, enterKey.getIconId());
+    }
+
+    private void doTestActionLabel(final String tag, final InputMethodSubtype subtype,
+            final int actionId, final int labelResId) {
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.imeOptions = actionId;
+        final RunInLocale<String> job = new RunInLocale<String>() {
+            @Override
+            protected String job(final Resources res) {
+                return res.getString(labelResId);
+            }
+        };
+        final Resources res = getContext().getResources();
+        final String label;
+        if (subtype.getLocale().equals(SubtypeLocaleUtils.NO_LANGUAGE)) {
+            // Using system locale.
+            label = res.getString(labelResId);
+        } else {
+            label = job.runInLocale(res, SubtypeLocaleUtils.getSubtypeLocale(subtype));
+        }
+        // Test text layouts.
+        editorInfo.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        final KeyboardLayoutSet layoutSet = createKeyboardLayoutSet(subtype, editorInfo);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_ALPHABET,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_SYMBOLS,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_SYMBOLS_SHIFTED,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        // Test phone number layouts.
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_PHONE,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_PHONE_SYMBOLS,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        // Test normal number layout.
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_NUMBER,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+        // Test number password layouts.
+        editorInfo.inputType =
+                InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD;
+        final KeyboardLayoutSet passwordSet = createKeyboardLayoutSet(subtype, editorInfo);
+        doTestActionKey(tag, passwordSet, KeyboardId.ELEMENT_NUMBER,
+                label, KeyboardIconsSet.ICON_UNDEFINED);
+    }
+
+    private void doTestActionKeyIcon(final String tag, final InputMethodSubtype subtype,
+            final int actionId, final String iconName) {
+        final int iconId = KeyboardIconsSet.getIconId(iconName);
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.imeOptions = actionId;
+        // Test text layouts.
+        editorInfo.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL;
+        final KeyboardLayoutSet layoutSet = createKeyboardLayoutSet(subtype, editorInfo);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_ALPHABET, null /* label */, iconId);
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_SYMBOLS, null /* label */, iconId);
+        doTestActionKey(
+                tag, layoutSet, KeyboardId.ELEMENT_SYMBOLS_SHIFTED, null /* label */, iconId);
+        // Test phone number layouts.
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_PHONE, null /* label */, iconId);
+        doTestActionKey(
+                tag, layoutSet, KeyboardId.ELEMENT_PHONE_SYMBOLS, null /* label */, iconId);
+        // Test normal number layout.
+        doTestActionKey(tag, layoutSet, KeyboardId.ELEMENT_NUMBER, null /* label */, iconId);
+        // Test number password layout.
+        editorInfo.inputType =
+                InputType.TYPE_CLASS_NUMBER | InputType.TYPE_NUMBER_VARIATION_PASSWORD;
+        final KeyboardLayoutSet passwordSet = createKeyboardLayoutSet(subtype, editorInfo);
+        doTestActionKey(tag, passwordSet, KeyboardId.ELEMENT_NUMBER, null /* label */, iconId);
+    }
+
+    public void testActionUnspecified() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "unspecifiled "
+                    + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionKeyIcon(tag, subtype, EditorInfo.IME_ACTION_UNSPECIFIED, "enter_key");
+        }
+    }
+
+    public void testActionNone() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "none " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionKeyIcon(tag, subtype, EditorInfo.IME_ACTION_NONE, "enter_key");
+        }
+    }
+
+    public void testActionGo() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "go " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionLabel(tag, subtype, EditorInfo.IME_ACTION_GO, R.string.label_go_key);
+        }
+    }
+
+    public void testActionSearch() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "search " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionKeyIcon(tag, subtype, EditorInfo.IME_ACTION_SEARCH, "search_key");
+        }
+    }
+
+    public void testActionSend() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "send " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionLabel(tag, subtype, EditorInfo.IME_ACTION_SEND, R.string.label_send_key);
+        }
+    }
+
+    public void testActionNext() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "next " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionLabel(tag, subtype, EditorInfo.IME_ACTION_NEXT, R.string.label_next_key);
+        }
+    }
+
+    public void testActionDone() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "done " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionLabel(tag, subtype, EditorInfo.IME_ACTION_DONE, R.string.label_done_key);
+        }
+    }
+
+    public void testActionPrevious() {
+        for (final InputMethodSubtype subtype : getAllSubtypesList()) {
+            final String tag = "previous " + SubtypeLocaleUtils.getSubtypeNameForLogging(subtype);
+            doTestActionLabel(
+                    tag, subtype, EditorInfo.IME_ACTION_PREVIOUS, R.string.label_previous_key);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetSubtypesCountTests.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetSubtypesCountTests.java
new file mode 100644
index 0000000..e691639
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetSubtypesCountTests.java
@@ -0,0 +1,60 @@
+/*
+ * Copyright (C) 2014 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.keyboard;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.ArrayList;
+
+@SmallTest
+public class KeyboardLayoutSetSubtypesCountTests extends KeyboardLayoutSetTestsBase {
+    private static final int NUMBER_OF_SUBTYPES = 63;
+    private static final int NUMBER_OF_ASCII_CAPABLE_SUBTYPES = 40;
+    private static final int NUMBER_OF_PREDEFINED_ADDITIONAL_SUBTYPES = 2;
+
+    private static String toString(final ArrayList<InputMethodSubtype> subtypeList) {
+        final StringBuilder sb = new StringBuilder();
+        for (int index = 0; index < subtypeList.size(); index++) {
+            final InputMethodSubtype subtype = subtypeList.get(index);
+            sb.append(index + ": ");
+            sb.append(SubtypeLocaleUtils.getSubtypeNameForLogging(subtype));
+            sb.append("\n");
+        }
+        return sb.toString();
+    }
+
+    public final void testAllSubtypesCount() {
+        final ArrayList<InputMethodSubtype> allSubtypesList = getAllSubtypesList();
+        assertEquals(toString(allSubtypesList), NUMBER_OF_SUBTYPES, allSubtypesList.size());
+    }
+
+    public final void testAsciiCapableSubtypesCount() {
+        final ArrayList<InputMethodSubtype> asciiCapableSubtypesList =
+                getAsciiCapableSubtypesList();
+        assertEquals(toString(asciiCapableSubtypesList),
+                NUMBER_OF_ASCII_CAPABLE_SUBTYPES, asciiCapableSubtypesList.size());
+    }
+
+    public final void testAdditionalSubtypesCount() {
+        final ArrayList<InputMethodSubtype> additionalSubtypesList = getAdditionalSubtypesList();
+        assertEquals(toString(additionalSubtypesList),
+                NUMBER_OF_PREDEFINED_ADDITIONAL_SUBTYPES, additionalSubtypesList.size());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
index cd97fc9..0993c4b 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
@@ -41,10 +41,6 @@
 
 @SmallTest
 public class KeyboardLayoutSetTestsBase extends AndroidTestCase {
-    private static final int NUMBER_OF_SUBTYPES = 63;
-    private static final int NUMBER_OF_ASCII_CAPABLE_SUBTYPES = 40;
-    private static final int NUMBER_OF_PREDEFINED_ADDITIONAL_SUBTYPES = 2;
-
     private static final KeyboardTheme DEFAULT_KEYBOARD_THEME =
             KeyboardSwitcher.KEYBOARD_THEMES[KeyboardSwitcher.THEME_INDEX_DEFAULT];
 
@@ -82,37 +78,23 @@
         }
     }
 
+    protected final ArrayList<InputMethodSubtype> getAllSubtypesList() {
+        return mAllSubtypesList;
+    }
+
+    protected final ArrayList<InputMethodSubtype> getAsciiCapableSubtypesList() {
+        return mAsciiCapableSubtypesList;
+    }
+
+    protected final ArrayList<InputMethodSubtype> getAdditionalSubtypesList() {
+        return mAdditionalSubtypesList;
+    }
+
     protected final boolean isPhone() {
         return mScreenMetrics == Constants.SCREEN_METRICS_SMALL_PHONE
                 || mScreenMetrics == Constants.SCREEN_METRICS_LARGE_PHONE;
     }
 
-    private static String toString(final ArrayList<InputMethodSubtype> subtypeList) {
-        final StringBuilder sb = new StringBuilder();
-        for (int index = 0; index < subtypeList.size(); index++) {
-            final InputMethodSubtype subtype = subtypeList.get(index);
-            sb.append(index + ": ");
-            sb.append(SubtypeLocaleUtils.getSubtypeNameForLogging(subtype));
-            sb.append("\n");
-        }
-        return sb.toString();
-    }
-
-    public final void testAllSubtypesCount() {
-        assertEquals(toString(mAllSubtypesList),
-                NUMBER_OF_SUBTYPES, mAllSubtypesList.size());
-    }
-
-    public final void testAsciiCapableSubtypesCount() {
-        assertEquals(toString(mAsciiCapableSubtypesList),
-                NUMBER_OF_ASCII_CAPABLE_SUBTYPES, mAsciiCapableSubtypesList.size());
-    }
-
-    public final void testAdditionalSubtypesCount() {
-        assertEquals(toString(mAdditionalSubtypesList),
-                NUMBER_OF_PREDEFINED_ADDITIONAL_SUBTYPES, mAdditionalSubtypesList.size());
-    }
-
     protected final InputMethodSubtype getSubtype(final Locale locale,
             final String keyboardLayout) {
         for (final InputMethodSubtype subtype : mAllSubtypesList) {
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index fa51236..69420d6 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -27,6 +27,7 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.LocaleUtils;
 
@@ -111,7 +112,7 @@
                 DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
         attributeMap.put(DictionaryHeader.HAS_HISTORICAL_INFO_KEY,
                 DictionaryHeader.ATTRIBUTE_VALUE_TRUE);
-        if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), FormatSpec.VERSION4,
+        if (BinaryDictionaryUtils.createEmptyDictFile(file.getAbsolutePath(), FormatSpec.VERSION4,
                 LocaleUtils.constructLocaleFromString(TEST_LOCALE), attributeMap)) {
             return file;
         } else {
@@ -121,11 +122,11 @@
     }
 
     private static int setCurrentTimeForTestMode(final int currentTime) {
-        return BinaryDictionary.setCurrentTimeForTest(currentTime);
+        return BinaryDictionaryUtils.setCurrentTimeForTest(currentTime);
     }
 
     private static int stopTestModeInNativeCode() {
-        return BinaryDictionary.setCurrentTimeForTest(-1);
+        return BinaryDictionaryUtils.setCurrentTimeForTest(-1);
     }
 
     public void testReadDictInJavaSide() {
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index c1adf65..4f9245c 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -25,6 +25,7 @@
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 import com.android.inputmethod.latin.makedict.WordProperty;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.LanguageModelParam;
 
@@ -59,7 +60,7 @@
         file.delete();
         file.mkdir();
         Map<String, String> attributeMap = new HashMap<String, String>();
-        if (BinaryDictionary.createEmptyDictFile(file.getAbsolutePath(), FormatSpec.VERSION4,
+        if (BinaryDictionaryUtils.createEmptyDictFile(file.getAbsolutePath(), FormatSpec.VERSION4,
                 Locale.ENGLISH, attributeMap)) {
             return file;
         } else {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index 0ee0fb5..80978df 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -30,6 +30,7 @@
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNodeArray;
 import com.android.inputmethod.latin.makedict.FusionDictionary.WeightedString;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.ByteArrayDictBuffer;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
@@ -77,7 +78,7 @@
 
     public BinaryDictDecoderEncoderTests(final long seed, final int maxUnigrams) {
         super();
-        BinaryDictionary.setCurrentTimeForTest(0);
+        BinaryDictionaryUtils.setCurrentTimeForTest(0);
         Log.e(TAG, "Testing dictionary: seed is " + seed);
         final Random random = new Random(seed);
         sWords.clear();
@@ -112,13 +113,13 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
-        BinaryDictionary.setCurrentTimeForTest(0);
+        BinaryDictionaryUtils.setCurrentTimeForTest(0);
     }
 
     @Override
     protected void tearDown() throws Exception {
         // Quit test mode.
-        BinaryDictionary.setCurrentTimeForTest(-1);
+        BinaryDictionaryUtils.setCurrentTimeForTest(-1);
         super.tearDown();
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index 6ace2de..04840d6 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -20,8 +20,8 @@
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
 
-import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
+import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.FileUtils;
 
@@ -79,11 +79,11 @@
     }
 
     private static int setCurrentTimeForTestMode(final int currentTime) {
-        return BinaryDictionary.setCurrentTimeForTest(currentTime);
+        return BinaryDictionaryUtils.setCurrentTimeForTest(currentTime);
     }
 
     private static int stopTestModeInNativeCode() {
-        return BinaryDictionary.setCurrentTimeForTest(-1);
+        return BinaryDictionaryUtils.setCurrentTimeForTest(-1);
     }
 
     /**
diff --git a/tests/src/com/android/inputmethod/latin/EditDistanceTests.java b/tests/src/com/android/inputmethod/latin/utils/EditDistanceTests.java
similarity index 79%
rename from tests/src/com/android/inputmethod/latin/EditDistanceTests.java
rename to tests/src/com/android/inputmethod/latin/utils/EditDistanceTests.java
index ffec20a..5831226 100644
--- a/tests/src/com/android/inputmethod/latin/EditDistanceTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/EditDistanceTests.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin;
+package com.android.inputmethod.latin.utils;
 
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
@@ -29,7 +29,7 @@
      * sitting
      */
     public void testExample1() {
-        final int dist = BinaryDictionary.editDistance("kitten", "sitting");
+        final int dist = BinaryDictionaryUtils.editDistance("kitten", "sitting");
         assertEquals("edit distance between 'kitten' and 'sitting' is 3",
                 3, dist);
     }
@@ -42,26 +42,26 @@
      * S--unday
      */
     public void testExample2() {
-        final int dist = BinaryDictionary.editDistance("Saturday", "Sunday");
+        final int dist = BinaryDictionaryUtils.editDistance("Saturday", "Sunday");
         assertEquals("edit distance between 'Saturday' and 'Sunday' is 3",
                 3, dist);
     }
 
     public void testBothEmpty() {
-        final int dist = BinaryDictionary.editDistance("", "");
+        final int dist = BinaryDictionaryUtils.editDistance("", "");
         assertEquals("when both string are empty, no edits are needed",
                 0, dist);
     }
 
     public void testFirstArgIsEmpty() {
-        final int dist = BinaryDictionary.editDistance("", "aaaa");
+        final int dist = BinaryDictionaryUtils.editDistance("", "aaaa");
         assertEquals("when only one string of the arguments is empty,"
                  + " the edit distance is the length of the other.",
                  4, dist);
     }
 
     public void testSecoondArgIsEmpty() {
-        final int dist = BinaryDictionary.editDistance("aaaa", "");
+        final int dist = BinaryDictionaryUtils.editDistance("aaaa", "");
         assertEquals("when only one string of the arguments is empty,"
                  + " the edit distance is the length of the other.",
                  4, dist);
@@ -70,27 +70,27 @@
     public void testSameStrings() {
         final String arg1 = "The quick brown fox jumps over the lazy dog.";
         final String arg2 = "The quick brown fox jumps over the lazy dog.";
-        final int dist = BinaryDictionary.editDistance(arg1, arg2);
+        final int dist = BinaryDictionaryUtils.editDistance(arg1, arg2);
         assertEquals("when same strings are passed, distance equals 0.",
                 0, dist);
     }
 
     public void testSameReference() {
         final String arg = "The quick brown fox jumps over the lazy dog.";
-        final int dist = BinaryDictionary.editDistance(arg, arg);
+        final int dist = BinaryDictionaryUtils.editDistance(arg, arg);
         assertEquals("when same string references are passed, the distance equals 0.",
                 0, dist);
     }
 
     public void testNullArg() {
         try {
-            BinaryDictionary.editDistance(null, "aaa");
+            BinaryDictionaryUtils.editDistance(null, "aaa");
             fail("IllegalArgumentException should be thrown.");
         } catch (Exception e) {
             assertTrue(e instanceof IllegalArgumentException);
         }
         try {
-            BinaryDictionary.editDistance("aaa", null);
+            BinaryDictionaryUtils.editDistance("aaa", null);
             fail("IllegalArgumentException should be thrown.");
         } catch (Exception e) {
             assertTrue(e instanceof IllegalArgumentException);
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
index b83ce57..b1dd7f6 100644
--- a/tools/dicttool/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -41,6 +41,7 @@
         $(LATINIME_CORE_SOURCE_DIRECTORY)/SuggestedWords.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/WordComposer.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/settings/NativeSuggestOptions.java \
+        $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/BinaryDictionaryUtils.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/ByteArrayDictBuffer.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/CollectionUtils.java \
         $(LATINIME_CORE_SOURCE_DIRECTORY)/utils/CombinedFormatUtils.java \