Merge "Remove deprecated Canvas.clipRegion(Region) call"
diff --git a/common/Android.mk b/common/Android.mk
index 085543f..132a223 100644
--- a/common/Android.mk
+++ b/common/Android.mk
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-LOCAL_PATH:= $(call my-dir)
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 LOCAL_MODULE := latinime-common
 LOCAL_SRC_FILES := $(call all-java-files-under, src)
diff --git a/common/src/com/android/inputmethod/latin/common/CodePointUtils.java b/common/src/com/android/inputmethod/latin/common/CodePointUtils.java
index 592da5c..ec59de8 100644
--- a/common/src/com/android/inputmethod/latin/common/CodePointUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/CodePointUtils.java
@@ -20,6 +20,8 @@
 
 import java.util.Random;
 
+import javax.annotation.Nonnull;
+
 // Utility methods related with code points used for tests.
 // TODO: Figure out where this class should be.
 @UsedForTesting
@@ -65,17 +67,23 @@
     };
 
     @UsedForTesting
-    public static int[] generateCodePointSet(final int codePointSetSize, final Random random) {
+    @Nonnull
+    public static int[] generateCodePointSet(final int codePointSetSize,
+            @Nonnull final Random random) {
         final int[] codePointSet = new int[codePointSetSize];
         for (int i = codePointSet.length - 1; i >= 0; ) {
             final int r = Math.abs(random.nextInt());
-            if (r < 0) continue;
+            if (r < 0) {
+                continue;
+            }
             // Don't insert 0~0x20, but insert any other code point.
             // Code points are in the range 0~0x10FFFF.
             final int candidateCodePoint = 0x20 + r % (Character.MAX_CODE_POINT - 0x20);
             // Code points between MIN_ and MAX_SURROGATE are not valid on their own.
             if (candidateCodePoint >= Character.MIN_SURROGATE
-                    && candidateCodePoint <= Character.MAX_SURROGATE) continue;
+                    && candidateCodePoint <= Character.MAX_SURROGATE) {
+                continue;
+            }
             codePointSet[i] = candidateCodePoint;
             --i;
         }
@@ -86,8 +94,10 @@
      * Generates a random word.
      */
     @UsedForTesting
-    public static String generateWord(final Random random, final int[] codePointSet) {
-        StringBuilder builder = new StringBuilder();
+    @Nonnull
+    public static String generateWord(@Nonnull final Random random,
+            @Nonnull final int[] codePointSet) {
+        final StringBuilder builder = new StringBuilder();
         // 8 * 4 = 32 chars max, but we do it the following way so as to bias the random toward
         // longer words. This should be closer to natural language, and more importantly, it will
         // exercise the algorithms in dicttool much more.
diff --git a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
similarity index 71%
rename from java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
rename to common/src/com/android/inputmethod/latin/common/CollectionUtils.java
index f9839eb..f7ba693 100644
--- a/java/src/com/android/inputmethod/latin/utils/CollectionUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/CollectionUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.utils;
+package com.android.inputmethod.latin.common;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -22,16 +22,27 @@
 import javax.annotation.Nonnull;
 import javax.annotation.Nullable;
 
+/**
+ * Utility methods for working with collections.
+ */
 public final class CollectionUtils {
     private CollectionUtils() {
         // This utility class is not publicly instantiable.
     }
 
+    /**
+     * Converts a sub-range of the given array to an ArrayList of the appropriate type.
+     * @param array Array to be converted.
+     * @param start First index inclusive to be converted.
+     * @param end Last index exclusive to be converted.
+     * @throws IllegalArgumentException if start or end are out of range or start &gt; end.
+     */
     @Nonnull
     public static <E> ArrayList<E> arrayAsList(@Nonnull final E[] array, final int start,
             final int end) {
         if (start < 0 || start > end || end > array.length) {
-            throw new IllegalArgumentException();
+            throw new IllegalArgumentException("Invalid start: " + start + " end: " + end
+                    + " with array.length: " + array.length);
         }
 
         final ArrayList<E> list = new ArrayList<>(end - start);
diff --git a/common/src/com/android/inputmethod/latin/common/ComposedData.java b/common/src/com/android/inputmethod/latin/common/ComposedData.java
new file mode 100644
index 0000000..7f09660
--- /dev/null
+++ b/common/src/com/android/inputmethod/latin/common/ComposedData.java
@@ -0,0 +1,66 @@
+/*
+ * 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.common;
+
+import javax.annotation.Nonnull;
+
+/**
+ * An immutable class that encapsulates a snapshot of word composition data.
+ */
+public class ComposedData {
+    @Nonnull
+    public final InputPointers mInputPointers;
+    public final boolean mIsBatchMode;
+    @Nonnull
+    public final String mTypedWord;
+
+    public ComposedData(@Nonnull final InputPointers inputPointers, final boolean isBatchMode,
+            @Nonnull final String typedWord) {
+        mInputPointers = inputPointers;
+        mIsBatchMode = isBatchMode;
+        mTypedWord = typedWord;
+    }
+
+    /**
+     * Copy the code points in the typed word to a destination array of ints.
+     *
+     * If the array is too small to hold the code points in the typed word, nothing is copied and
+     * -1 is returned.
+     *
+     * @param destination the array of ints.
+     * @return the number of copied code points.
+     */
+    public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+            @Nonnull final int[] destination) {
+        // lastIndex is exclusive
+        final int lastIndex = mTypedWord.length()
+                - StringUtils.getTrailingSingleQuotesCount(mTypedWord);
+        if (lastIndex <= 0) {
+            // The string is empty or contains only single quotes.
+            return 0;
+        }
+
+        // The following function counts the number of code points in the text range which begins
+        // at index 0 and extends to the character at lastIndex.
+        final int codePointSize = Character.codePointCount(mTypedWord, 0, lastIndex);
+        if (codePointSize > destination.length) {
+            return -1;
+        }
+        return StringUtils.copyCodePointsAndReturnCodePointCount(destination, mTypedWord, 0,
+                lastIndex, true /* downCase */);
+    }
+}
diff --git a/common/src/com/android/inputmethod/latin/common/Constants.java b/common/src/com/android/inputmethod/latin/common/Constants.java
index 8f4a1e5..abc377a 100644
--- a/common/src/com/android/inputmethod/latin/common/Constants.java
+++ b/common/src/com/android/inputmethod/latin/common/Constants.java
@@ -18,6 +18,8 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 
+import javax.annotation.Nonnull;
+
 public final class Constants {
     public static final class Color {
         /**
@@ -259,6 +261,7 @@
         return code >= CODE_SPACE;
     }
 
+    @Nonnull
     public static String printableCode(final int code) {
         switch (code) {
         case CODE_SHIFT: return "shift";
@@ -286,7 +289,8 @@
         }
     }
 
-    public static String printableCodes(final int[] codes) {
+    @Nonnull
+    public static String printableCodes(@Nonnull final int[] codes) {
         final StringBuilder sb = new StringBuilder();
         boolean addDelimiter = false;
         for (final int code : codes) {
diff --git a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java b/common/src/com/android/inputmethod/latin/common/CoordinateUtils.java
similarity index 98%
rename from java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
rename to common/src/com/android/inputmethod/latin/common/CoordinateUtils.java
index 3a97059..0316624 100644
--- a/java/src/com/android/inputmethod/latin/utils/CoordinateUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/CoordinateUtils.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.utils;
+package com.android.inputmethod.latin.common;
 
 import javax.annotation.Nonnull;
 
diff --git a/java/src/com/android/inputmethod/latin/utils/FileUtils.java b/common/src/com/android/inputmethod/latin/common/FileUtils.java
similarity index 96%
rename from java/src/com/android/inputmethod/latin/utils/FileUtils.java
rename to common/src/com/android/inputmethod/latin/common/FileUtils.java
index f1106a6..6768458 100644
--- a/java/src/com/android/inputmethod/latin/utils/FileUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/FileUtils.java
@@ -14,7 +14,7 @@
  * the License.
  */
 
-package com.android.inputmethod.latin.utils;
+package com.android.inputmethod.latin.common;
 
 import java.io.File;
 import java.io.FilenameFilter;
diff --git a/common/src/com/android/inputmethod/latin/common/InputPointers.java b/common/src/com/android/inputmethod/latin/common/InputPointers.java
index 40131ac..7beee15 100644
--- a/common/src/com/android/inputmethod/latin/common/InputPointers.java
+++ b/common/src/com/android/inputmethod/latin/common/InputPointers.java
@@ -18,6 +18,8 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 
+import javax.annotation.Nonnull;
+
 // TODO: This class is not thread-safe.
 public final class InputPointers {
     private static final boolean DEBUG_TIME = false;
@@ -28,7 +30,7 @@
     private final ResizableIntArray mPointerIds;
     private final ResizableIntArray mTimes;
 
-    public InputPointers(int defaultCapacity) {
+    public InputPointers(final int defaultCapacity) {
         mDefaultCapacity = defaultCapacity;
         mXCoordinates = new ResizableIntArray(defaultCapacity);
         mYCoordinates = new ResizableIntArray(defaultCapacity);
@@ -51,7 +53,8 @@
         mTimes.fill(lastTime, fromIndex, fillLength);
     }
 
-    public void addPointerAt(int index, int x, int y, int pointerId, int time) {
+    public void addPointerAt(final int index, final int x, final int y, final int pointerId,
+            final int time) {
         mXCoordinates.addAt(index, x);
         mYCoordinates.addAt(index, y);
         mPointerIds.addAt(index, pointerId);
@@ -62,21 +65,21 @@
     }
 
     @UsedForTesting
-    public void addPointer(int x, int y, int pointerId, int time) {
+    public void addPointer(final int x, final int y, final int pointerId, final int time) {
         mXCoordinates.add(x);
         mYCoordinates.add(y);
         mPointerIds.add(pointerId);
         mTimes.add(time);
     }
 
-    public void set(InputPointers ip) {
+    public void set(@Nonnull final InputPointers ip) {
         mXCoordinates.set(ip.mXCoordinates);
         mYCoordinates.set(ip.mYCoordinates);
         mPointerIds.set(ip.mPointerIds);
         mTimes.set(ip.mTimes);
     }
 
-    public void copy(InputPointers ip) {
+    public void copy(@Nonnull final InputPointers ip) {
         mXCoordinates.copy(ip.mXCoordinates);
         mYCoordinates.copy(ip.mYCoordinates);
         mPointerIds.copy(ip.mPointerIds);
@@ -93,8 +96,9 @@
      * @param startPos the starting index of the data in {@code times} and etc.
      * @param length the number of data to be appended.
      */
-    public void append(int pointerId, ResizableIntArray times, ResizableIntArray xCoordinates,
-            ResizableIntArray yCoordinates, int startPos, int length) {
+    public void append(final int pointerId, @Nonnull final ResizableIntArray times,
+            @Nonnull final ResizableIntArray xCoordinates,
+            @Nonnull final ResizableIntArray yCoordinates, final int startPos, final int length) {
         if (length == 0) {
             return;
         }
@@ -127,14 +131,17 @@
         return mXCoordinates.getLength();
     }
 
+    @Nonnull
     public int[] getXCoordinates() {
         return mXCoordinates.getPrimitiveArray();
     }
 
+    @Nonnull
     public int[] getYCoordinates() {
         return mYCoordinates.getPrimitiveArray();
     }
 
+    @Nonnull
     public int[] getPointerIds() {
         return mPointerIds.getPrimitiveArray();
     }
@@ -145,6 +152,7 @@
      * @return The time each point was registered, in milliseconds, relative to the first event in
      * the sequence.
      */
+    @Nonnull
     public int[] getTimes() {
         return mTimes.getPrimitiveArray();
     }
diff --git a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java b/common/src/com/android/inputmethod/latin/common/LocaleUtils.java
similarity index 79%
rename from java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
rename to common/src/com/android/inputmethod/latin/common/LocaleUtils.java
index 4f0805c..7f2333b 100644
--- a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
+++ b/common/src/com/android/inputmethod/latin/common/LocaleUtils.java
@@ -14,15 +14,15 @@
  * the License.
  */
 
-package com.android.inputmethod.dictionarypack;
-
-import android.content.res.Configuration;
-import android.content.res.Resources;
-import android.text.TextUtils;
+package com.android.inputmethod.latin.common;
 
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.Locale;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 /**
  * A class to help with handling Locales in string form.
  *
@@ -105,8 +105,8 @@
      * @return a constant that measures how well the tested locale matches the reference locale.
      */
     public static int getMatchLevel(final String referenceLocale, final String testedLocale) {
-        if (TextUtils.isEmpty(referenceLocale)) {
-            return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
+        if (StringUtils.isEmpty(referenceLocale)) {
+            return StringUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
         }
         if (null == testedLocale) return LOCALE_NO_MATCH;
         final String[] referenceParams = referenceLocale.split("_", 3);
@@ -160,45 +160,53 @@
         return LOCALE_MATCH <= level;
     }
 
-    /**
-     * Sets the system locale for this process.
-     *
-     * @param res the resources to use. Pass current resources.
-     * @param newLocale the locale to change to.
-     * @return the old locale.
-     */
-    public static Locale setSystemLocale(final Resources res, final Locale newLocale) {
-        final Configuration conf = res.getConfiguration();
-        final Locale saveLocale = conf.locale;
-        conf.locale = newLocale;
-        res.updateConfiguration(conf, res.getDisplayMetrics());
-        return saveLocale;
-    }
-
     private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();
 
     /**
      * Creates a locale from a string specification.
+     * @param localeString a string specification of a locale, in a format of "ll_cc_variant" where
+     * "ll" is a language code, "cc" is a country code.
      */
-    public static Locale constructLocaleFromString(final String localeStr) {
-        if (localeStr == null)
+    @Nullable
+    public static Locale constructLocaleFromString(@Nullable final String localeString) {
+        if (localeString == null) {
             return null;
-        synchronized (sLocaleCache) {
-            if (sLocaleCache.containsKey(localeStr))
-                return sLocaleCache.get(localeStr);
-            Locale retval = null;
-            String[] localeParams = localeStr.split("_", 3);
-            if (localeParams.length == 1) {
-                retval = new Locale(localeParams[0]);
-            } else if (localeParams.length == 2) {
-                retval = new Locale(localeParams[0], localeParams[1]);
-            } else if (localeParams.length == 3) {
-                retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
-            }
-            if (retval != null) {
-                sLocaleCache.put(localeStr, retval);
-            }
-            return retval;
         }
+        synchronized (sLocaleCache) {
+            if (sLocaleCache.containsKey(localeString)) {
+                return sLocaleCache.get(localeString);
+            }
+            final String[] elements = localeString.split("_", 3);
+            final Locale locale;
+            if (elements.length == 1) {
+                locale = new Locale(elements[0] /* language */);
+            } else if (elements.length == 2) {
+                locale = new Locale(elements[0] /* language */, elements[1] /* country */);
+            } else { // localeParams.length == 3
+                locale = new Locale(elements[0] /* language */, elements[1] /* country */,
+                        elements[2] /* variant */);
+            }
+            sLocaleCache.put(localeString, locale);
+            return locale;
+        }
+    }
+
+    // TODO: Get this information from the framework instead of maintaining here by ourselves.
+    private static final HashSet<String> sRtlLanguageCodes = new HashSet<>();
+    static {
+        // List of known Right-To-Left language codes.
+        sRtlLanguageCodes.add("ar"); // Arabic
+        sRtlLanguageCodes.add("fa"); // Persian
+        sRtlLanguageCodes.add("iw"); // Hebrew
+        sRtlLanguageCodes.add("ku"); // Kurdish
+        sRtlLanguageCodes.add("ps"); // Pashto
+        sRtlLanguageCodes.add("sd"); // Sindhi
+        sRtlLanguageCodes.add("ug"); // Uyghur
+        sRtlLanguageCodes.add("ur"); // Urdu
+        sRtlLanguageCodes.add("yi"); // Yiddish
+    }
+
+    public static boolean isRtlLanguage(@Nonnull final Locale locale) {
+        return sRtlLanguageCodes.contains(locale.getLanguage());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java b/common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java
similarity index 90%
rename from java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
rename to common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java
index 7603dbb..7ef741c 100644
--- a/java/src/com/android/inputmethod/latin/settings/NativeSuggestOptions.java
+++ b/common/src/com/android/inputmethod/latin/common/NativeSuggestOptions.java
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.settings;
+package com.android.inputmethod.latin.common;
 
 public class NativeSuggestOptions {
     // Need to update suggest_options.h when you add, remove or reorder options.
@@ -25,8 +25,11 @@
     private static final int WEIGHT_FOR_LOCALE_IN_THOUSANDS = 4;
     private static final int OPTIONS_SIZE = 5;
 
-    private final int[] mOptions = new int[OPTIONS_SIZE
-            + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
+    private final int[] mOptions;
+
+    public NativeSuggestOptions(final int additionalFeaturesSettingsSize) {
+        mOptions = new int[OPTIONS_SIZE + additionalFeaturesSettingsSize];
+    }
 
     public void setIsGesture(final boolean value) {
         setBooleanOption(IS_GESTURE, value);
diff --git a/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java b/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java
index ea23d8a..340abb2 100644
--- a/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java
+++ b/common/src/com/android/inputmethod/latin/common/ResizableIntArray.java
@@ -18,8 +18,11 @@
 
 import java.util.Arrays;
 
+import javax.annotation.Nonnull;
+
 // TODO: This class is not thread-safe.
 public final class ResizableIntArray {
+    @Nonnull
     private int[] mArray;
     private int mLength;
 
@@ -89,17 +92,18 @@
         mLength = 0;
     }
 
+    @Nonnull
     public int[] getPrimitiveArray() {
         return mArray;
     }
 
-    public void set(final ResizableIntArray ip) {
+    public void set(@Nonnull final ResizableIntArray ip) {
         // TODO: Implement primitive array pool.
         mArray = ip.mArray;
         mLength = ip.mLength;
     }
 
-    public void copy(final ResizableIntArray ip) {
+    public void copy(@Nonnull final ResizableIntArray ip) {
         final int newCapacity = calculateCapacity(ip.mLength);
         if (newCapacity > 0) {
             // TODO: Implement primitive array pool.
@@ -109,7 +113,7 @@
         mLength = ip.mLength;
     }
 
-    public void append(final ResizableIntArray src, final int startPos, final int length) {
+    public void append(@Nonnull final ResizableIntArray src, final int startPos, final int length) {
         if (length == 0) {
             return;
         }
diff --git a/dictionaries/de_wordlist.combined.gz b/dictionaries/de_wordlist.combined.gz
index 803211c..92c9554 100644
--- a/dictionaries/de_wordlist.combined.gz
+++ b/dictionaries/de_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_GB_wordlist.combined.gz b/dictionaries/en_GB_wordlist.combined.gz
index 1fa9b85..217660f 100644
--- a/dictionaries/en_GB_wordlist.combined.gz
+++ b/dictionaries/en_GB_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_US_wordlist.combined.gz b/dictionaries/en_US_wordlist.combined.gz
index 2e039ff..8aed9c5 100644
--- a/dictionaries/en_US_wordlist.combined.gz
+++ b/dictionaries/en_US_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/en_wordlist.combined.gz b/dictionaries/en_wordlist.combined.gz
index e845346..7fe6618 100644
--- a/dictionaries/en_wordlist.combined.gz
+++ b/dictionaries/en_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/es_wordlist.combined.gz b/dictionaries/es_wordlist.combined.gz
index 3391e64..71e7309 100644
--- a/dictionaries/es_wordlist.combined.gz
+++ b/dictionaries/es_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/fr_wordlist.combined.gz b/dictionaries/fr_wordlist.combined.gz
index 1b9fd73..afe44a6 100644
--- a/dictionaries/fr_wordlist.combined.gz
+++ b/dictionaries/fr_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/it_wordlist.combined.gz b/dictionaries/it_wordlist.combined.gz
index 5a5cbdc..ed58a12 100644
--- a/dictionaries/it_wordlist.combined.gz
+++ b/dictionaries/it_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/nl_wordlist.combined.gz b/dictionaries/nl_wordlist.combined.gz
index 37ba8ab..19c3a7e 100644
--- a/dictionaries/nl_wordlist.combined.gz
+++ b/dictionaries/nl_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pl_wordlist.combined.gz b/dictionaries/pl_wordlist.combined.gz
index ba71a55..2b84eec 100644
--- a/dictionaries/pl_wordlist.combined.gz
+++ b/dictionaries/pl_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pt_BR_wordlist.combined.gz b/dictionaries/pt_BR_wordlist.combined.gz
index 02df1c1..7aac61e 100644
--- a/dictionaries/pt_BR_wordlist.combined.gz
+++ b/dictionaries/pt_BR_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/pt_PT_wordlist.combined.gz b/dictionaries/pt_PT_wordlist.combined.gz
index bcd50ab..5bf9a60 100644
--- a/dictionaries/pt_PT_wordlist.combined.gz
+++ b/dictionaries/pt_PT_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/ru_wordlist.combined.gz b/dictionaries/ru_wordlist.combined.gz
index 401ad08..5e92662 100644
--- a/dictionaries/ru_wordlist.combined.gz
+++ b/dictionaries/ru_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/sv_wordlist.combined.gz b/dictionaries/sv_wordlist.combined.gz
index b6ebab3..db44ae4 100644
--- a/dictionaries/sv_wordlist.combined.gz
+++ b/dictionaries/sv_wordlist.combined.gz
Binary files differ
diff --git a/dictionaries/tr_wordlist.combined.gz b/dictionaries/tr_wordlist.combined.gz
index 306cea1..d3c8825 100644
--- a/dictionaries/tr_wordlist.combined.gz
+++ b/dictionaries/tr_wordlist.combined.gz
Binary files differ
diff --git a/java-overridable/src/com/android/inputmethod/latin/define/ProductionFlags.java b/java-overridable/src/com/android/inputmethod/latin/define/ProductionFlags.java
index f806256..4367568 100644
--- a/java-overridable/src/com/android/inputmethod/latin/define/ProductionFlags.java
+++ b/java-overridable/src/com/android/inputmethod/latin/define/ProductionFlags.java
@@ -48,4 +48,10 @@
      * When {@code true}, personal dictionary sync feature is ready to be enabled.
      */
     public static final boolean ENABLE_PERSONAL_DICTIONARY_SYNC = ENABLE_ACCOUNT_SIGN_IN && false;
+
+    /**
+     * When {@code true}, the IME maintains per account {@link UserHistoryDictionary}.
+     */
+    public static final boolean ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY =
+            ENABLE_ACCOUNT_SIGN_IN && false;
 }
diff --git a/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java b/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
index bd54238..4e8a10b 100644
--- a/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
+++ b/java-overridable/src/com/android/inputmethod/latin/settings/AdditionalFeaturesSettingUtils.java
@@ -47,6 +47,7 @@
         // do nothing.
     }
 
+    @Nonnull
     public static RichInputMethodSubtype createRichInputMethodSubtype(
             @Nonnull final RichInputMethodManager imm,
             @Nonnull final InputMethodSubtype subtype,
diff --git a/java/Android.mk b/java/Android.mk
index a2c5697..b84b347 100644
--- a/java/Android.mk
+++ b/java/Android.mk
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-LOCAL_PATH:= $(call my-dir)
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
 LOCAL_MODULE_TAGS := optional
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index ee1cef6..88e867f 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -37,7 +37,6 @@
 
     <application android:label="@string/english_ime_name"
             android:icon="@drawable/ic_launcher_keyboard"
-            android:killAfterRestore="false"
             android:supportsRtl="true"
             android:allowBackup="true">
 
diff --git a/java/res/drawable/btn_keyboard_key_ics.xml b/java/res/drawable/btn_keyboard_key_ics.xml
index 0bb098d..bacd5d7 100644
--- a/java/res/drawable/btn_keyboard_key_ics.xml
+++ b/java/res/drawable/btn_keyboard_key_ics.xml
@@ -32,6 +32,8 @@
           android:drawable="@drawable/btn_keyboard_key_normal_off_holo_dark" />
 
     <!-- Empty background keys. -->
+    <item android:state_empty="true" android:state_pressed="true"
+          android:drawable="@drawable/btn_keyboard_key_pressed_ics_light" />
     <item android:state_empty="true"
           android:drawable="@android:color/transparent" />
 
diff --git a/java/res/drawable/btn_keyboard_key_klp.xml b/java/res/drawable/btn_keyboard_key_klp.xml
index 2a202a1..e2f2085 100644
--- a/java/res/drawable/btn_keyboard_key_klp.xml
+++ b/java/res/drawable/btn_keyboard_key_klp.xml
@@ -32,6 +32,8 @@
           android:drawable="@drawable/btn_keyboard_key_normal_off_holo_dark" />
 
     <!-- Empty background keys. -->
+    <item android:state_empty="true" android:state_pressed="true"
+          android:drawable="@drawable/btn_keyboard_key_pressed_klp_light" />
     <item android:state_empty="true"
           android:drawable="@android:color/transparent" />
 
diff --git a/java/res/drawable/btn_keyboard_key_lxx_dark.xml b/java/res/drawable/btn_keyboard_key_lxx_dark.xml
index bb1789a..161592d 100644
--- a/java/res/drawable/btn_keyboard_key_lxx_dark.xml
+++ b/java/res/drawable/btn_keyboard_key_lxx_dark.xml
@@ -32,8 +32,10 @@
           android:drawable="@drawable/btn_keyboard_key_normal_off_lxx_dark" />
 
     <!-- Empty background keys. -->
+    <item android:state_empty="true" android:state_pressed="true"
+          android:drawable="@color/key_background_pressed_lxx_dark" />
     <item android:state_empty="true"
-          android:drawable="@color/key_background_lxx_dark" />
+          android:drawable="@android:color/transparent" />
 
     <!-- Normal keys. -->
     <item android:state_pressed="true"
diff --git a/java/res/drawable/btn_keyboard_key_lxx_light.xml b/java/res/drawable/btn_keyboard_key_lxx_light.xml
index 60fe02d..0154d75 100644
--- a/java/res/drawable/btn_keyboard_key_lxx_light.xml
+++ b/java/res/drawable/btn_keyboard_key_lxx_light.xml
@@ -32,8 +32,10 @@
           android:drawable="@drawable/btn_keyboard_key_normal_off_lxx_light" />
 
     <!-- Empty background keys. -->
+    <item android:state_empty="true" android:state_pressed="true"
+          android:drawable="@color/key_background_pressed_lxx_light" />
     <item android:state_empty="true"
-          android:drawable="@color/key_background_lxx_light" />
+          android:drawable="@android:color/transparent" />
 
     <!-- Normal keys. -->
     <item android:state_pressed="true"
diff --git a/java/res/raw/main_de.dict b/java/res/raw/main_de.dict
index 45b2883..c3c2cbe 100644
--- a/java/res/raw/main_de.dict
+++ b/java/res/raw/main_de.dict
Binary files differ
diff --git a/java/res/raw/main_en.dict b/java/res/raw/main_en.dict
index 5bbb857..b9e5bc7 100644
--- a/java/res/raw/main_en.dict
+++ b/java/res/raw/main_en.dict
Binary files differ
diff --git a/java/res/raw/main_es.dict b/java/res/raw/main_es.dict
index fae1318..076d5aa 100644
--- a/java/res/raw/main_es.dict
+++ b/java/res/raw/main_es.dict
Binary files differ
diff --git a/java/res/raw/main_fr.dict b/java/res/raw/main_fr.dict
index 19532d9..0e86860 100644
--- a/java/res/raw/main_fr.dict
+++ b/java/res/raw/main_fr.dict
Binary files differ
diff --git a/java/res/raw/main_it.dict b/java/res/raw/main_it.dict
index ff11b97..609ef13 100644
--- a/java/res/raw/main_it.dict
+++ b/java/res/raw/main_it.dict
Binary files differ
diff --git a/java/res/raw/main_pt_br.dict b/java/res/raw/main_pt_br.dict
index 9fa5044..c338651 100644
--- a/java/res/raw/main_pt_br.dict
+++ b/java/res/raw/main_pt_br.dict
Binary files differ
diff --git a/java/res/raw/main_ru.dict b/java/res/raw/main_ru.dict
index 76b5f80..d0af707 100644
--- a/java/res/raw/main_ru.dict
+++ b/java/res/raw/main_ru.dict
Binary files differ
diff --git a/java/res/values-af/strings-emoji-descriptions.xml b/java/res/values-af/strings-emoji-descriptions.xml
index 26bfc5f..5ed066a 100644
--- a/java/res/values-af/strings-emoji-descriptions.xml
+++ b/java/res/values-af/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Koekie"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Staaf sjokolade"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Lekkergoed"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Suiglekker"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Vla"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Heuningpot"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Broskoek"</string>
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index ca8b816..f8be8f1 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serwies (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Tradisioneel)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Kompak)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Geen taal nie (alfabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabet (QWERTZ)"</string>
diff --git a/java/res/values-ar/strings-emoji-descriptions.xml b/java/res/values-ar/strings-emoji-descriptions.xml
index 875f626..c1decd3 100644
--- a/java/res/values-ar/strings-emoji-descriptions.xml
+++ b/java/res/values-ar/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"كعكة"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"بار شيكولاتة"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"حلوى"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"مصاصة"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"كاسترد"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"جرة عسل"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"كعكة بسمن وسكر"</string>
diff --git a/java/res/values-ar/strings.xml b/java/res/values-ar/strings.xml
index 62d5f3d..da414a6 100644
--- a/java/res/values-ar/strings.xml
+++ b/java/res/values-ar/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"هنجليزية (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"الصربية (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (التقليدية)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (مكثفة)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"بدون لغة (أبجدية)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"‏الأبجدية (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"‏الأبجدية (QWERTZ)"</string>
diff --git a/java/res/values-az-rAZ/strings.xml b/java/res/values-az-rAZ/strings.xml
index b13c905..b1a192c 100644
--- a/java/res/values-az-rAZ/strings.xml
+++ b/java/res/values-az-rAZ/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hingilis (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serb (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Ənənəvi)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Kompakt)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Dil yoxdur (Əlifba)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Əlifba (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Əlifba (QWERTZ)"</string>
diff --git a/java/res/values-bg/strings.xml b/java/res/values-bg/strings.xml
index 805014b..5d7e6b4 100644
--- a/java/res/values-bg/strings.xml
+++ b/java/res/values-bg/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Хинглиш (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Сръбска (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (традиционна клавиатура)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (компактна)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Без език (латиница)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Латиница (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Латиница (QWERTZ)"</string>
diff --git a/java/res/values-bn-rBD/strings.xml b/java/res/values-bn-rBD/strings.xml
index 5487bb5..8d77be4 100644
--- a/java/res/values-bn-rBD/strings.xml
+++ b/java/res/values-bn-rBD/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"হিংলিশ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"সার্বিয়ান (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ঐতিহ্যবাহি)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (কম্প্যাক্ট)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"কোনো ভাষা নয় (বর্ণমালা)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"বর্ণমালা (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"বর্ণমালা (QWERTZ)"</string>
diff --git a/java/res/values-ca/strings.xml b/java/res/values-ca/strings.xml
index ef792b8..3f0e447 100644
--- a/java/res/values-ca/strings.xml
+++ b/java/res/values-ca/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbi (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (compacte)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Cap idioma (alfabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabet (QWERTZ)"</string>
diff --git a/java/res/values-cs/strings-emoji-descriptions.xml b/java/res/values-cs/strings-emoji-descriptions.xml
index b664202..b7f359c 100644
--- a/java/res/values-cs/strings-emoji-descriptions.xml
+++ b/java/res/values-cs/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Sušenka"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Tabulka čokolády"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Bonbon"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lízátko"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Pudink"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Hrnek medu"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Dort"</string>
diff --git a/java/res/values-da/strings.xml b/java/res/values-da/strings.xml
index 0e92e47..39c51e9 100644
--- a/java/res/values-da/strings.xml
+++ b/java/res/values-da/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbisk (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (traditionelt)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (kompakt)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Intet sprog (Alfabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabet (QWERTZ)"</string>
diff --git a/java/res/values-el/strings-emoji-descriptions.xml b/java/res/values-el/strings-emoji-descriptions.xml
index aa8966e..a15d0fd 100644
--- a/java/res/values-el/strings-emoji-descriptions.xml
+++ b/java/res/values-el/strings-emoji-descriptions.xml
@@ -269,7 +269,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Κούκι"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Σοκολάτα"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Γλυκά"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Γλειφιτζούρι"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Κρέμα"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Βάζο με μέλι"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Μπισκότο"</string>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 3ed2826..1778cde 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbian (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Traditional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"No language (Alphabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alphabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alphabet (QWERTZ)"</string>
diff --git a/java/res/values-en-rIN/strings.xml b/java/res/values-en-rIN/strings.xml
index 3ed2826..1778cde 100644
--- a/java/res/values-en-rIN/strings.xml
+++ b/java/res/values-en-rIN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbian (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Traditional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"No language (Alphabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alphabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alphabet (QWERTZ)"</string>
diff --git a/java/res/values-es-rUS/strings.xml b/java/res/values-es-rUS/strings.xml
index 8142b1d..7b9e3ba 100644
--- a/java/res/values-es-rUS/strings.xml
+++ b/java/res/values-es-rUS/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbio (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (compacto)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ningún idioma (alfabeto)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabeto (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabeto (QWERTZ)"</string>
diff --git a/java/res/values-eu-rES/strings-emoji-descriptions.xml b/java/res/values-eu-rES/strings-emoji-descriptions.xml
index 2faec96..c774ae1 100644
--- a/java/res/values-eu-rES/strings-emoji-descriptions.xml
+++ b/java/res/values-eu-rES/strings-emoji-descriptions.xml
@@ -846,6 +846,6 @@
     <string name="spoken_emoji_1F6C1" msgid="2845056048320031158">"Bainuontzia"</string>
     <string name="spoken_emoji_1F6C2" msgid="8117262514698011877">"Pasaporte-kontrola"</string>
     <string name="spoken_emoji_1F6C3" msgid="1176342001834630675">"Aduana"</string>
-    <string name="spoken_emoji_1F6C4" msgid="1477622834179978886">"Maleta-erreklamazioa"</string>
+    <string name="spoken_emoji_1F6C4" msgid="1477622834179978886">"Ekipaje-erreklamazioa"</string>
     <string name="spoken_emoji_1F6C5" msgid="2495834050856617451">"Ahaztutako maletak"</string>
 </resources>
diff --git a/java/res/values-eu-rES/strings.xml b/java/res/values-eu-rES/strings.xml
index 9496d95..23d1593 100644
--- a/java/res/values-eu-rES/strings.xml
+++ b/java/res/values-eu-rES/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglisha (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbiarra (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradizionala)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (trinkoa)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ez dago hizkuntzarik (alfabetoa)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabetoa (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabetoa (QWERTZ)"</string>
diff --git a/java/res/values-fa/strings-emoji-descriptions.xml b/java/res/values-fa/strings-emoji-descriptions.xml
index cc670ee..8adb530 100644
--- a/java/res/values-fa/strings-emoji-descriptions.xml
+++ b/java/res/values-fa/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"کلوچه"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"تخته شکلات"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"آبنبات"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"آبنبات چوبی"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"آب‌نبات چوبی"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"کاستارد"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"ظرف عسل"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"کیک روغنی"</string>
diff --git a/java/res/values-fa/strings.xml b/java/res/values-fa/strings.xml
index a0390df..16a6f87 100644
--- a/java/res/values-fa/strings.xml
+++ b/java/res/values-fa/strings.xml
@@ -84,7 +84,7 @@
     <string name="hint_add_to_dictionary_without_word" msgid="3040385779511255101">"برای ذخیره اینجا را لمس کنید"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"دیکشنری موجود است"</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"طرح زمینه صفحه‌کلید"</string>
-    <string name="switch_accounts" msgid="3321216593719006162">"جابجایی بین حساب‌ها"</string>
+    <string name="switch_accounts" msgid="3321216593719006162">"جابه‌جایی بین حساب‌ها"</string>
     <string name="no_accounts_selected" msgid="2073821619103904330">"هیچ حسابی انتخاب نشده است"</string>
     <string name="account_selected" msgid="2846876462199625974">"در حال حاضر در حال استفاده از <xliff:g id="EMAIL_ADDRESS">%1$s</xliff:g>"</string>
     <string name="account_select_ok" msgid="9141195141763227797">"تأیید"</string>
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"هندی انگلیسی (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"صربی (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (سنتی)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (فشرده)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"بدون زبان (حروف الفبا)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"‏حروف الفبا (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"‏حروف الفبا (QWERTZ)"</string>
diff --git a/java/res/values-fi/strings-emoji-descriptions.xml b/java/res/values-fi/strings-emoji-descriptions.xml
index ad08bbd..7d6a9f9 100644
--- a/java/res/values-fi/strings-emoji-descriptions.xml
+++ b/java/res/values-fi/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Pikkuleipä"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Suklaapatukka"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Karamelli"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Tikkukaramelli"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Tikkari"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Vanukas"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Hunajapurkki"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Kakkuviipale"</string>
diff --git a/java/res/values-fi/strings.xml b/java/res/values-fi/strings.xml
index b133cc9..0308d18 100644
--- a/java/res/values-fi/strings.xml
+++ b/java/res/values-fi/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hindienglanti (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"serbialainen (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (perinteinen)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tiivis)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ei kieltä (aakkoset)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Aakkoset (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Aakkoset (QWERTZ)"</string>
diff --git a/java/res/values-fr-rCA/strings.xml b/java/res/values-fr-rCA/strings.xml
index ee52205..6730a79 100644
--- a/java/res/values-fr-rCA/strings.xml
+++ b/java/res/values-fr-rCA/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbe (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (traditionnel)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Aucune langue (alphabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alphabet latin (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alphabet latin (QWERTZ)"</string>
diff --git a/java/res/values-fr/strings.xml b/java/res/values-fr/strings.xml
index 2a93c99..3349f4a 100644
--- a/java/res/values-fr/strings.xml
+++ b/java/res/values-fr/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hindi/Anglais (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbe (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (traditionnel)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Aucune langue (latin)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alphabet latin (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alphabet latin (QWERTZ)"</string>
diff --git a/java/res/values-gl-rES/strings.xml b/java/res/values-gl-rES/strings.xml
index d266ad5..d72bcb8 100644
--- a/java/res/values-gl-rES/strings.xml
+++ b/java/res/values-gl-rES/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbio (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (compacto)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ningún idioma (alfabeto)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabeto (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabeto (QWERTZ)"</string>
diff --git a/java/res/values-hi/strings.xml b/java/res/values-hi/strings.xml
index 9fc08ac..a4f2dd3 100644
--- a/java/res/values-hi/strings.xml
+++ b/java/res/values-hi/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"हिंग्लिश (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"सर्बियाई (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (पारंपरिक)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (संक्षिप्त)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"भाषा उपलब्ध नहीं है (लैटिन वर्णाक्षर)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"वर्णाक्षर (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"वर्णाक्षर (QWERTZ)"</string>
diff --git a/java/res/values-hr/strings.xml b/java/res/values-hr/strings.xml
index 7a5a665..e189371 100644
--- a/java/res/values-hr/strings.xml
+++ b/java/res/values-hr/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Srpski (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicionalni)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (kompaktna)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Nema jezika (abeceda)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Abeceda (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Abeceda (QWERTZ)"</string>
diff --git a/java/res/values-hu/strings-emoji-descriptions.xml b/java/res/values-hu/strings-emoji-descriptions.xml
index 4f3d01c..b72f298 100644
--- a/java/res/values-hu/strings-emoji-descriptions.xml
+++ b/java/res/values-hu/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Sütemény"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Csokoládé"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Cukorka"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Nyalóka"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Sodó"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Mézesbödön"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Tortaszelet"</string>
diff --git a/java/res/values-hy-rAM/strings-emoji-descriptions.xml b/java/res/values-hy-rAM/strings-emoji-descriptions.xml
index f41f2fc..dcc718e 100644
--- a/java/res/values-hy-rAM/strings-emoji-descriptions.xml
+++ b/java/res/values-hy-rAM/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Թխվածքաբլիթ"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Շոկոլադի սալիկ"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Կոնֆետ"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Սառնաշաքար կոնֆետ"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Շաքարաքլոր"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Եփովի կրեմ"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Մեղրանոթ"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Տորթի կտոր"</string>
diff --git a/java/res/values-hy-rAM/strings.xml b/java/res/values-hy-rAM/strings.xml
index 7d30119..8d7c5c8 100644
--- a/java/res/values-hy-rAM/strings.xml
+++ b/java/res/values-hy-rAM/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Հինգլիշ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Սերբերեն (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ավանդական)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (սեղմ)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ոչ մի լեզվով (Այբուբեն)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Այբուբեն (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Այբուբեն (QWERTZ)"</string>
diff --git a/java/res/values-is-rIS/strings.xml b/java/res/values-is-rIS/strings.xml
index 2bc3bfb..8a6927a 100644
--- a/java/res/values-is-rIS/strings.xml
+++ b/java/res/values-is-rIS/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbneskt (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (hefðbundið)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (lítið)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Ekkert tungumál (stafróf)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Stafróf (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Stafróf (QWERTZ)"</string>
diff --git a/java/res/values-iw/strings-emoji-descriptions.xml b/java/res/values-iw/strings-emoji-descriptions.xml
index ab31403..fc4435a 100644
--- a/java/res/values-iw/strings-emoji-descriptions.xml
+++ b/java/res/values-iw/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"עוגייה"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"חפיסת שוקולד"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"סוכרייה"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"סוכרייה על מקל"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"רפרפת"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"סיר דבש"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"עוגת פירות"</string>
diff --git a/java/res/values-ja/strings.xml b/java/res/values-ja/strings.xml
index 4caaae3..53ef577 100644
--- a/java/res/values-ja/strings.xml
+++ b/java/res/values-ja/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ヒングリッシュ(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"セルビア語(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(伝統言語)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(コンパクト)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"言語なし(アルファベット)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"アルファベット(QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"アルファベット(QWERTZ)"</string>
diff --git a/java/res/values-ka-rGE/strings.xml b/java/res/values-ka-rGE/strings.xml
index dc2308c..d4457a5 100644
--- a/java/res/values-ka-rGE/strings.xml
+++ b/java/res/values-ka-rGE/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ჰინგლისური (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"სერბული (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ტრადიციული)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (კომპაქტური)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"ენის გარეშე (ანბანი)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"ანბანი (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"ანბანი (QWERTZ)"</string>
diff --git a/java/res/values-kk-rKZ/strings.xml b/java/res/values-kk-rKZ/strings.xml
index 838a07a..58f8f30 100644
--- a/java/res/values-kk-rKZ/strings.xml
+++ b/java/res/values-kk-rKZ/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Хинглиш (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Серб (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (дәстүрлі)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (шағын)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Тіл жоқ (әліпби)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Әліпби (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Әліпби (QWERTZ)"</string>
diff --git a/java/res/values-km-rKH/strings-emoji-descriptions.xml b/java/res/values-km-rKH/strings-emoji-descriptions.xml
index 757df50..e9b8780 100644
--- a/java/res/values-km-rKH/strings-emoji-descriptions.xml
+++ b/java/res/values-km-rKH/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"ខូគី"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"​​សូកូឡា"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"ស្ករគ្រាប់"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"ស្ករ​គ្រាប់​មាន​ដង​​កាន់"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"សង់ខ្យា"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"ថូ"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"នំ​ខេក"</string>
diff --git a/java/res/values-kn-rIN/strings-emoji-descriptions.xml b/java/res/values-kn-rIN/strings-emoji-descriptions.xml
index 4e6d5ce..a013c27 100644
--- a/java/res/values-kn-rIN/strings-emoji-descriptions.xml
+++ b/java/res/values-kn-rIN/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"ಕುಕಿ"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"ಚಾಕೊಲೇಟ್ ಬಾರ್"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"ಕ್ಯಾಂಡಿ"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"ಲಾಲಿಪಪ್"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"ಕಸ್ಟರ್ಡ್"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"ಜೇನಿನ ಮಡಕೆ"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"ಶಾರ್ಟ್‌ಕೇಕ್"</string>
diff --git a/java/res/values-ko/strings.xml b/java/res/values-ko/strings.xml
index e393517..68195f0 100644
--- a/java/res/values-ko/strings.xml
+++ b/java/res/values-ko/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"인도 영어(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"세르비아어(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(번체)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(컴팩트)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"언어 없음(알파벳)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"알파벳(QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"알파벳(QWERTZ)"</string>
diff --git a/java/res/values-ky-rKG/strings.xml b/java/res/values-ky-rKG/strings.xml
index 90c2db3..6b46785 100644
--- a/java/res/values-ky-rKG/strings.xml
+++ b/java/res/values-ky-rKG/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Хинглиш (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Сербче (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Салттык)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Чакан)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Тил жок (Алфавит)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Алфавит (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Алфавит (QWERTZ)"</string>
diff --git a/java/res/values-lo-rLA/strings-emoji-descriptions.xml b/java/res/values-lo-rLA/strings-emoji-descriptions.xml
index 0747fa6..84b9d05 100644
--- a/java/res/values-lo-rLA/strings-emoji-descriptions.xml
+++ b/java/res/values-lo-rLA/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"​ຄຸ​ກ​ກີ້"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"​ຊັອກ​ໂກ​ແລັດບາ"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"ແຄນດີ້"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"​ໂລ​ລິ​ປັອບ"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"​ຄັ​ສ​ຕາດ"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"ໝໍ້​ນ້ຳ​ເຜິ້ງ"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"ຊັອດ​ເຄັກ"</string>
diff --git a/java/res/values-lo-rLA/strings.xml b/java/res/values-lo-rLA/strings.xml
index b23eb31..efc09be 100644
--- a/java/res/values-lo-rLA/strings.xml
+++ b/java/res/values-lo-rLA/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ຮິງ​ລິສ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"ເຊີ​ບຽນ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ດັ້ງ​ເດີມ)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ກະ​ທັດ​ຮັດ)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"ບໍ່ມີພາສາ (ໂຕອັກສອນ)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"ໂຕອັກສອນ (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"ໂຕອັກສອນ (QWERTZ)"</string>
diff --git a/java/res/values-lt/strings-emoji-descriptions.xml b/java/res/values-lt/strings-emoji-descriptions.xml
index fa81dbb..afe9ac0 100644
--- a/java/res/values-lt/strings-emoji-descriptions.xml
+++ b/java/res/values-lt/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Sausainis"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Šokolado plytelė"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Saldainis"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Ledinukas ant pagaliuko"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Saldus kremas"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Medaus puodynė"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Trapus pyragas"</string>
diff --git a/java/res/values-lv/strings-emoji-descriptions.xml b/java/res/values-lv/strings-emoji-descriptions.xml
index a51991b..525a1a0 100644
--- a/java/res/values-lv/strings-emoji-descriptions.xml
+++ b/java/res/values-lv/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Cepums"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Šokolādes tāfelīte"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Konfekte"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Cukurgailītis"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Olu krēms"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Medus pods"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Smilšu torte ar augļu pildījumu"</string>
diff --git a/java/res/values-lv/strings.xml b/java/res/values-lv/strings.xml
index ed7e9d4..833839b 100644
--- a/java/res/values-lv/strings.xml
+++ b/java/res/values-lv/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hindi–angļu valoda (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbu (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicionālā)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (kompaktā)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Nav valodas (alfabēts)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabēts (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabēts (QWERTZ)"</string>
diff --git a/java/res/values-ml-rIN/strings.xml b/java/res/values-ml-rIN/strings.xml
index acf00c2..a34de64 100644
--- a/java/res/values-ml-rIN/strings.xml
+++ b/java/res/values-ml-rIN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ഹിംഗ്ലീഷ് (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"സെർബിയൻ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (പരമ്പരാഗതം)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (കോം‌പാക്‌ട്)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"ഭാഷയില്ല (അക്ഷരമാല)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"അക്ഷരമാല (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"അക്ഷരമാല (QWERTZ)"</string>
diff --git a/java/res/values-mr-rIN/strings.xml b/java/res/values-mr-rIN/strings.xml
index 492226a..2c6746e 100644
--- a/java/res/values-mr-rIN/strings.xml
+++ b/java/res/values-mr-rIN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"हिंग्लिश (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"सर्बियन (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (पारंपारिक)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (संक्षिप्त)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"भाषा नाही (वर्णमाला)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"वर्णमाला (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"वर्णमाला (QWERTZ)"</string>
diff --git a/java/res/values-ms-rMY/strings.xml b/java/res/values-ms-rMY/strings.xml
index f33bdb4..3136c3b 100644
--- a/java/res/values-ms-rMY/strings.xml
+++ b/java/res/values-ms-rMY/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Bahasa Serbia (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Tradisional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Sarat)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Tiada bahasa (Abjad)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Abjad (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Abjad (QWERTZ)"</string>
diff --git a/java/res/values-my-rMM/strings.xml b/java/res/values-my-rMM/strings.xml
index 3e5a491..ab40378 100644
--- a/java/res/values-my-rMM/strings.xml
+++ b/java/res/values-my-rMM/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ဟင်ဂလိပ် (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"ဆားဘီယား (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ရိုးရာ)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ကျစ်လစ်သော)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"ဘာသာစကားမရှိ (ဗျည်းအက္ခရာ)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"ဗျည်းအက္ခရာ (ကွာတီ)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"ဗျည်းအက္ခရာ (ကွာတီ)"</string>
diff --git a/java/res/values-nb/strings-emoji-descriptions.xml b/java/res/values-nb/strings-emoji-descriptions.xml
index fa655f1..6811188 100644
--- a/java/res/values-nb/strings-emoji-descriptions.xml
+++ b/java/res/values-nb/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Kjeks"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Sjokoladeplate"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Godteri"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Kjærlighet på pinne"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Pudding"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Honningkrukke"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Kakestykke"</string>
diff --git a/java/res/values-ne-rNP/strings-emoji-descriptions.xml b/java/res/values-ne-rNP/strings-emoji-descriptions.xml
index 43f4d89..39e5bc0 100644
--- a/java/res/values-ne-rNP/strings-emoji-descriptions.xml
+++ b/java/res/values-ne-rNP/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"कुकीज"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"चकलेट बार"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"क्यान्डी"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"लालीपप"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"कस्तार्ड"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"महदानी"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"सर्टकेक"</string>
diff --git a/java/res/values-ne-rNP/strings.xml b/java/res/values-ne-rNP/strings.xml
index ec17c4e..40c2d4f 100644
--- a/java/res/values-ne-rNP/strings.xml
+++ b/java/res/values-ne-rNP/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"हिङ्लिस (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"सर्बियाई (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (परम्परागत)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (संकुचित)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"कुनै भाषा होइन (वर्णमाला)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"वर्णमाला (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"वर्णमाला (QWERTZ)"</string>
diff --git a/java/res/values-pt/strings.xml b/java/res/values-pt/strings.xml
index 2e0cd3b..2cc86e1 100644
--- a/java/res/values-pt/strings.xml
+++ b/java/res/values-pt/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Híndi-inglês (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Sérvio (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (compacto)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Nenhum idioma (alfabeto)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabeto (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabeto (QWERTZ)"</string>
diff --git a/java/res/values-ro/strings-emoji-descriptions.xml b/java/res/values-ro/strings-emoji-descriptions.xml
index f44a0b9..2ac84e5 100644
--- a/java/res/values-ro/strings-emoji-descriptions.xml
+++ b/java/res/values-ro/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Biscuit"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Ciocolată"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Bomboane"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Acadea"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Budincă"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Oală de miere"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Prăjitură"</string>
diff --git a/java/res/values-ro/strings.xml b/java/res/values-ro/strings.xml
index 8e3ebf2..0339873 100644
--- a/java/res/values-ro/strings.xml
+++ b/java/res/values-ro/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Sârbă (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradițională)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Nicio limbă (alfabet)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabet (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabet (QWERTZ)"</string>
diff --git a/java/res/values-ru/strings.xml b/java/res/values-ru/strings.xml
index bd45c68..e5aaf8b 100644
--- a/java/res/values-ru/strings.xml
+++ b/java/res/values-ru/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Хинглиш (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Сербский (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (классическая)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (компактная раскладка)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Язык не определен (латиница)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Латиница (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Латиница (QWERTZ)"</string>
diff --git a/java/res/values-si-rLK/strings.xml b/java/res/values-si-rLK/strings.xml
index 2e5f3d9..f8fa5e7 100644
--- a/java/res/values-si-rLK/strings.xml
+++ b/java/res/values-si-rLK/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"හින්ග්ලිෂ් (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"සර්බියානු (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (සාම්ප්‍රදායික)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (සංයුක්ත)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"භාෂාවක් නැත (අකාරාදිය)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"අකාරාදිය (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"අකාරාදිය (QWERTZ)"</string>
diff --git a/java/res/values-sk/strings.xml b/java/res/values-sk/strings.xml
index 80c91c8..0d0e69f 100644
--- a/java/res/values-sk/strings.xml
+++ b/java/res/values-sk/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"srbčina (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradičná)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (kompaktná)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Žiadny jazyk (latinka)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Latinka (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Latinka (QWERTZ)"</string>
diff --git a/java/res/values-sl/strings.xml b/java/res/values-sl/strings.xml
index 396334c..e1f86c3 100644
--- a/java/res/values-sl/strings.xml
+++ b/java/res/values-sl/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hindujska angleščina (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Srbščina (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (tradicionalna)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (kompaktna)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Brez jezika (latinice)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Latinica (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Latinica (QWERTZ)"</string>
diff --git a/java/res/values-sr/strings.xml b/java/res/values-sr/strings.xml
index 28f6cae..f8c4ff6 100644
--- a/java/res/values-sr/strings.xml
+++ b/java/res/values-sr/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"хенглески (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"српски (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (традиционални)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (компактна)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Нема језика (абецеда)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Абецеда (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Абецеда (QWERTZ)"</string>
diff --git a/java/res/values-sv/strings-emoji-descriptions.xml b/java/res/values-sv/strings-emoji-descriptions.xml
index 879de0b..e49c73c 100644
--- a/java/res/values-sv/strings-emoji-descriptions.xml
+++ b/java/res/values-sv/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Småkaka"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Chokladkaka"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Godis"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Klubba"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Vaniljkräm"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Honungsburk"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Sockerkaka"</string>
diff --git a/java/res/values-sw/strings-emoji-descriptions.xml b/java/res/values-sw/strings-emoji-descriptions.xml
index 5716387..329bf39 100644
--- a/java/res/values-sw/strings-emoji-descriptions.xml
+++ b/java/res/values-sw/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Biskuti"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Mchi wa chokoleti"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Peremende"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Peremende ya kijiti"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Faluda au Kastadi"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Emoji ya chungu cha asali"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Keki tamu yenye vitandamlo inayofanana na biskuti"</string>
diff --git a/java/res/values-sw/strings.xml b/java/res/values-sw/strings.xml
index ae1a3a8..04c4f2b 100644
--- a/java/res/values-sw/strings.xml
+++ b/java/res/values-sw/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Kiserbia (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (cha Jadi)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Thabiti)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Hakuna lugha (Alfabeti)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabeti (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabeti (QWERTZ)"</string>
diff --git a/java/res/values-ta-rIN/strings.xml b/java/res/values-ta-rIN/strings.xml
index 0a2d138..64c573b 100644
--- a/java/res/values-ta-rIN/strings.xml
+++ b/java/res/values-ta-rIN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ஹிங்கிலிஷ் (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"செர்பியன் (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (பாரம்பரியமானது)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (வசதியான)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"மொழியில்லை (அகரவரிசை)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"அகரவரிசை (க்வெர்டி)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"அகரவரிசை (க்வெர்ட்ச்)"</string>
diff --git a/java/res/values-te-rIN/strings.xml b/java/res/values-te-rIN/strings.xml
index 15e9aaf..0fb55b4 100644
--- a/java/res/values-te-rIN/strings.xml
+++ b/java/res/values-te-rIN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"హింగ్లీష్ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"సెర్బియన్ (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (సాంప్రదాయకం)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (కాంపాక్ట్)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"భాష లేదు (ఆల్ఫాబెట్)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"ఆల్ఫాబెట్ (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"ఆల్ఫాబెట్ (QWERTZ)"</string>
diff --git a/java/res/values-th/strings.xml b/java/res/values-th/strings.xml
index 5c239db..fcc76d9 100644
--- a/java/res/values-th/strings.xml
+++ b/java/res/values-th/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ภาษาอังกฤษผสมกับฮินดู (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"เซอร์เบีย (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ดั้งเดิม)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (แบบกะทัดรัด)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"ไม่มีภาษา (ตัวอักษรละติน)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"ตัวอักษร (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"ตัวอักษร (QWERTZ)"</string>
diff --git a/java/res/values-tl/strings.xml b/java/res/values-tl/strings.xml
index 8d755e4..82617b8 100644
--- a/java/res/values-tl/strings.xml
+++ b/java/res/values-tl/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serbian (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Traditional)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Compact)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Walang wika (Alpabeto)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alpabeto (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alpabeto (QWERTZ)"</string>
diff --git a/java/res/values-tr/strings.xml b/java/res/values-tr/strings.xml
index 86426fa..359e0d1 100644
--- a/java/res/values-tr/strings.xml
+++ b/java/res/values-tr/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hingilizce (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Sırpça (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Geleneksel)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Kompakt)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Dil yok (Alfabe)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Alfabe (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Alfabe (QWERTZ)"</string>
diff --git a/java/res/values-uk/strings.xml b/java/res/values-uk/strings.xml
index 33ab584..dd625cb 100644
--- a/java/res/values-uk/strings.xml
+++ b/java/res/values-uk/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Хінґліш (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"сербська (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (традиційна)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (компактна)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Стандартна (латиниця)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Латиниця (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Латиниця (QWERTZ)"</string>
diff --git a/java/res/values-ur-rPK/strings.xml b/java/res/values-ur-rPK/strings.xml
index f00e503..4b1f03b 100644
--- a/java/res/values-ur-rPK/strings.xml
+++ b/java/res/values-ur-rPK/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"ہنگلش (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"سربیائی (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (روایتی)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (کمپیکٹ)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"کوئی زبان نہیں (الفابیٹ)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"‏حروف تہجی (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"‏حروف تہجی (QWERTZ)"</string>
diff --git a/java/res/values-uz-rUZ/strings.xml b/java/res/values-uz-rUZ/strings.xml
index 62143d7..59aba60 100644
--- a/java/res/values-uz-rUZ/strings.xml
+++ b/java/res/values-uz-rUZ/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Hinglish (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Serb (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (an’anaviy)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (ixcham)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Til aniqlanmadi (lotin)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Lotin (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Lotin (QWERTZ)"</string>
diff --git a/java/res/values-vi/strings-emoji-descriptions.xml b/java/res/values-vi/strings-emoji-descriptions.xml
index 8b44dcf..492c726 100644
--- a/java/res/values-vi/strings-emoji-descriptions.xml
+++ b/java/res/values-vi/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"Bánh quy"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"Thanh sôcôla"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"Kẹo"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Kẹo que"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"Món sữa trứng"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"Mắt ong"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"Bánh bơ giòn"</string>
diff --git a/java/res/values-vi/strings.xml b/java/res/values-vi/strings.xml
index fa0e11b..ec0deb1 100644
--- a/java/res/values-vi/strings.xml
+++ b/java/res/values-vi/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"Tiếng Anh-Hindi (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"Tiếng Serbia (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Truyền thống)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (Viết tắt)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"Không ngôn ngữ nào (Bảng chữ cái)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"Bảng chữ cái (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"Bảng chữ cái (QWERTZ)"</string>
diff --git a/java/res/values-zh-rCN/strings.xml b/java/res/values-zh-rCN/strings.xml
index 4562c4b..52b0dac 100644
--- a/java/res/values-zh-rCN/strings.xml
+++ b/java/res/values-zh-rCN/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"印地英语(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"塞尔维亚语(<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>布局)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(传统)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g>(紧凑型)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"无语言(字母)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"字母 (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"字母 (QWERTZ)"</string>
diff --git a/java/res/values-zh-rHK/strings.xml b/java/res/values-zh-rHK/strings.xml
index e584a88..840f333 100644
--- a/java/res/values-zh-rHK/strings.xml
+++ b/java/res/values-zh-rHK/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"印度英文 (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"塞爾維亞文 (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (傳統)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (精簡版)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"無語言 (字母)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"字母 (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"字母 (QWERTZ)"</string>
diff --git a/java/res/values-zh-rTW/strings-emoji-descriptions.xml b/java/res/values-zh-rTW/strings-emoji-descriptions.xml
index 06e260a..b5c7230 100644
--- a/java/res/values-zh-rTW/strings-emoji-descriptions.xml
+++ b/java/res/values-zh-rTW/strings-emoji-descriptions.xml
@@ -267,7 +267,7 @@
     <string name="spoken_emoji_1F36A" msgid="2726271795913042295">"餅乾"</string>
     <string name="spoken_emoji_1F36B" msgid="6342163604299875931">"巧克力棒"</string>
     <string name="spoken_emoji_1F36C" msgid="2168934753998218790">"糖果"</string>
-    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"棒棒糖"</string>
+    <string name="spoken_emoji_1F36D" msgid="3671507903799975792">"Lollipop"</string>
     <string name="spoken_emoji_1F36E" msgid="4630541402785165902">"卡士達"</string>
     <string name="spoken_emoji_1F36F" msgid="5577915387425169439">"蜂蜜罐"</string>
     <string name="spoken_emoji_1F370" msgid="7243244547866114951">"水果蛋糕"</string>
diff --git a/java/res/values-zh-rTW/strings.xml b/java/res/values-zh-rTW/strings.xml
index aa534c8..8fac310 100644
--- a/java/res/values-zh-rTW/strings.xml
+++ b/java/res/values-zh-rTW/strings.xml
@@ -102,8 +102,7 @@
     <string name="subtype_with_layout_hi_ZZ" msgid="6827402953860547044">"印度英文 (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_with_layout_sr_ZZ" msgid="2859024772719772407">"塞爾維亞文 (<xliff:g id="KEYBOARD_LAYOUT">%s</xliff:g>)"</string>
     <string name="subtype_generic_traditional" msgid="8584594350973800586">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (傳統)"</string>
-    <!-- no translation found for subtype_generic_compact (3353673321203202922) -->
-    <skip />
+    <string name="subtype_generic_compact" msgid="3353673321203202922">"<xliff:g id="LANGUAGE_NAME">%s</xliff:g> (精簡)"</string>
     <string name="subtype_no_language" msgid="7137390094240139495">"無語言 (字母)"</string>
     <string name="subtype_no_language_qwerty" msgid="244337630616742604">"字母 (QWERTY)"</string>
     <string name="subtype_no_language_qwertz" msgid="443066912507547976">"字母 (QWERTZ)"</string>
diff --git a/java/res/xml-sw600dp/key_space_3kw.xml b/java/res/xml-sw600dp/key_space_3kw.xml
index 9932d34..8cc3a38 100644
--- a/java/res/xml-sw600dp/key_space_3kw.xml
+++ b/java/res/xml-sw600dp/key_space_3kw.xml
@@ -22,12 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <switch>
-        <!-- fa: Perisan
-             kn: Kannada
-             ne: Nepali
-             te: Telugu -->
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="true"
         >
             <Key
@@ -39,7 +35,7 @@
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="false"
         >
             <Key
diff --git a/java/res/xml-sw600dp/key_space_7kw.xml b/java/res/xml-sw600dp/key_space_7kw.xml
index 3311f81..61e0765 100644
--- a/java/res/xml-sw600dp/key_space_7kw.xml
+++ b/java/res/xml-sw600dp/key_space_7kw.xml
@@ -22,12 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <switch>
-        <!-- fa: Perisan
-             kn: Kannada
-             ne: Nepali
-             te: Telugu -->
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="true"
         >
             <Key
@@ -39,7 +35,7 @@
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="false"
         >
             <Key
diff --git a/java/res/xml-sw600dp/rows_number_normal.xml b/java/res/xml-sw600dp/rows_number_normal.xml
index 7a4700d..e6fdf73 100644
--- a/java/res/xml-sw600dp/rows_number_normal.xml
+++ b/java/res/xml-sw600dp/rows_number_normal.xml
@@ -24,17 +24,17 @@
     <Row>
         <Key
             latin:keySpec="-"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="+"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="."
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
@@ -53,13 +53,15 @@
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
+        <!-- U+FF0A: "*" FULLWIDTH ASTERISK -->
         <Key
-            latin:keyStyle="numStarKeyStyle"
+            latin:keySpec="&#xFF0A;|*"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="/"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <switch>
@@ -70,14 +72,14 @@
                     latin:keySpec=","
                     latin:keyLabelFlags="hasPopupHint"
                     latin:moreKeys="!text/morekeys_am_pm"
-                    latin:keyStyle="numKeyStyle"
+                    latin:keyStyle="numSymbolKeyStyle"
                     latin:keyWidth="10%p"
                     latin:backgroundType="functional" />
             </case>
             <default>
                 <Key
                     latin:keySpec=","
-                    latin:keyStyle="numKeyStyle"
+                    latin:keyStyle="numSymbolKeyStyle"
                     latin:keyWidth="10%p"
                     latin:backgroundType="functional" />
             </default>
@@ -100,12 +102,12 @@
     <Row>
         <Key
             latin:keySpec="("
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec=")"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <switch>
@@ -114,14 +116,14 @@
             >
                 <Key
                     latin:keySpec=":"
-                    latin:keyStyle="numKeyStyle"
+                    latin:keyStyle="numSymbolKeyStyle"
                     latin:keyWidth="10%p"
                     latin:backgroundType="functional" />
             </case>
             <default>
                 <Key
                     latin:keySpec="="
-                    latin:keyStyle="numKeyStyle"
+                    latin:keyStyle="numSymbolKeyStyle"
                     latin:keyWidth="10%p"
                     latin:backgroundType="functional" />
             </default>
@@ -143,8 +145,10 @@
         <Key
             latin:keyStyle="tabletNumSpaceKeyStyle"
             latin:keyWidth="30%p" />
+        <!-- U+FF0A: "*" FULLWIDTH ASTERISK -->
         <Key
-            latin:keyStyle="numStarKeyStyle"
+            latin:keySpec="&#xFF0A;|*"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyXPos="31%p" />
         <Key
             latin:keySpec="0"
diff --git a/java/res/xml-sw600dp/rows_number_password.xml b/java/res/xml-sw600dp/rows_number_password.xml
index 6c3855a..37e6338 100644
--- a/java/res/xml-sw600dp/rows_number_password.xml
+++ b/java/res/xml-sw600dp/rows_number_password.xml
@@ -70,7 +70,8 @@
         <Key
             latin:keyStyle="deleteKeyStyle" />
         <Key
-            latin:keyStyle="num0KeyStyle" />
+            latin:keyStyle="num0KeyStyle"
+            latin:keyHintLabel="+" />
         <Key
             latin:keyStyle="enterKeyStyle" />
         <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
diff --git a/java/res/xml-sw600dp/rows_phone.xml b/java/res/xml-sw600dp/rows_phone.xml
index 612397a..fc86a76 100644
--- a/java/res/xml-sw600dp/rows_phone.xml
+++ b/java/res/xml-sw600dp/rows_phone.xml
@@ -28,16 +28,18 @@
     <Row>
         <Key
             latin:keySpec="-"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="+"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
-            latin:keyStyle="numPauseKeyStyle"
+            latin:keySpec="!string/label_pause_key|,"
+            latin:keyLabelFlags="followKeyLabelRatio|autoXScale"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
@@ -55,16 +57,18 @@
     <Row>
         <Key
             latin:keySpec=","
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="."
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
-            latin:keyStyle="numWaitKeyStyle"
+            latin:keySpec="!string/label_wait_key|;"
+            latin:keyLabelFlags="followKeyLabelRatio|autoXScale"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
@@ -82,17 +86,17 @@
     <Row>
         <Key
             latin:keySpec="("
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec=")"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
             latin:keySpec="N"
-            latin:keyStyle="numKeyStyle"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyWidth="10%p"
             latin:backgroundType="functional" />
         <Key
@@ -109,13 +113,16 @@
         <Key
             latin:keyStyle="tabletNumSpaceKeyStyle"
             latin:keyWidth="30%p" />
+        <!-- U+FF0A: "*" FULLWIDTH ASTERISK -->
         <Key
-            latin:keyStyle="numStarKeyStyle"
+            latin:keySpec="&#xFF0A;|*"
+            latin:keyStyle="numSymbolKeyStyle"
             latin:keyXPos="31%p" />
         <Key
-            latin:keyStyle="num0KeyStyle" />
+            latin:keyStyle="num0KeyStyle"
+            latin:keyHintLabel="+" />
         <Key
             latin:keySpec="\#"
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
     </Row>
 </merge>
diff --git a/java/res/xml/key_space_5kw.xml b/java/res/xml/key_space_5kw.xml
index b1fe0bb..692c245 100644
--- a/java/res/xml/key_space_5kw.xml
+++ b/java/res/xml/key_space_5kw.xml
@@ -22,12 +22,8 @@
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
     <switch>
-        <!-- fa: Perisan
-             kn: Kannada
-             ne: Nepali
-             te: Telugu -->
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="true"
         >
             <Key
@@ -39,7 +35,7 @@
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
         <case
-            latin:languageCode="fa|kn|ne|te"
+            latin:keyboardLayoutSet="bengali_akkhor|farsi|kannada|nepali_romanized|nepali_traditional|telugu"
             latin:languageSwitchKeyEnabled="false"
         >
             <Key
diff --git a/java/res/xml/key_styles_number.xml b/java/res/xml/key_styles_number.xml
index 847b436..911c276 100644
--- a/java/res/xml/key_styles_number.xml
+++ b/java/res/xml/key_styles_number.xml
@@ -33,9 +33,7 @@
         latin:keyLabelFlags="fontNormal|followKeyLetterRatio|followFunctionalTextColor"
         latin:parentStyle="numKeyBaseStyle" />
     <key-style
-        latin:styleName="numFunctionalKeyStyle"
-        latin:keyLabelFlags="followKeyLargeLetterRatio"
-        latin:backgroundType="functional"
+        latin:styleName="numSymbolKeyStyle"
         latin:parentStyle="numKeyBaseStyle" />
     <key-style
         latin:styleName="numberKeyStyle"
@@ -44,7 +42,6 @@
     <key-style
         latin:styleName="num0KeyStyle"
         latin:keySpec="0"
-        latin:keyHintLabel="+"
         latin:parentStyle="numberKeyStyle" />
     <key-style
         latin:styleName="num1KeyStyle"
@@ -90,11 +87,6 @@
         latin:keySpec="9"
         latin:keyHintLabel="WXYZ"
         latin:parentStyle="numberKeyStyle" />
-    <!-- U+FF0A: "*" FULLWIDTH ASTERISK -->
-    <key-style
-        latin:styleName="numStarKeyStyle"
-        latin:keySpec="&#xFF0A;|*"
-        latin:parentStyle="numKeyStyle" />
     <!-- Only for non-tablet device -->
     <key-style
         latin:styleName="numPhoneToSymbolKeyStyle"
@@ -105,16 +97,6 @@
         latin:keySpec="!text/keylabel_to_phone_numeric|!code/key_switch_alpha_symbol"
         latin:parentStyle="numModeKeyStyle" />
     <key-style
-        latin:styleName="numPauseKeyStyle"
-        latin:keySpec="!text/label_pause_key|,"
-        latin:keyLabelFlags="followKeyHintLabelRatio|autoXScale"
-        latin:parentStyle="numKeyBaseStyle" />
-    <key-style
-        latin:styleName="numWaitKeyStyle"
-        latin:keySpec="!text/label_wait_key|;"
-        latin:keyLabelFlags="followKeyHintLabelRatio|autoXScale"
-        latin:parentStyle="numKeyBaseStyle" />
-    <key-style
         latin:styleName="numTabKeyStyle"
         latin:keyActionFlags="noKeyPreview"
         latin:parentStyle="tabKeyStyle" />
diff --git a/java/res/xml/keyboard_layout_set_bengali_akkhor.xml b/java/res/xml/keyboard_layout_set_bengali_akkhor.xml
index b2b09b2..267064d 100644
--- a/java/res/xml/keyboard_layout_set_bengali_akkhor.xml
+++ b/java/res/xml/keyboard_layout_set_bengali_akkhor.xml
@@ -19,7 +19,7 @@
 -->
 
 <KeyboardLayoutSet xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin" >
-    <Feature latin:supportedScript="devanagari" />
+    <Feature latin:supportedScript="bengali" />
     <Element
         latin:elementKeyboard="@xml/kbd_bengali_akkhor"
         latin:elementName="alphabet"
diff --git a/java/res/xml/rows_number_normal.xml b/java/res/xml/rows_number_normal.xml
index d8d1508..0f92ac6 100644
--- a/java/res/xml/rows_number_normal.xml
+++ b/java/res/xml/rows_number_normal.xml
@@ -35,7 +35,8 @@
             latin:keySpec="-"
             latin:moreKeys="+"
             latin:keyLabelFlags="hasPopupHint"
-            latin:keyStyle="numFunctionalKeyStyle"
+            latin:keyStyle="numKeyStyle"
+            latin:backgroundType="functional"
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
@@ -54,7 +55,8 @@
             >
                 <Key
                     latin:keySpec="."
-                    latin:keyStyle="numFunctionalKeyStyle"
+                    latin:keyStyle="numKeyStyle"
+                    latin:backgroundType="functional"
                     latin:keyWidth="fillRight" />
             </case>
             <case
@@ -62,15 +64,17 @@
             >
                 <Key
                     latin:keySpec="."
-                    latin:keyLabelFlags="hasPopupHint"
                     latin:moreKeys="!text/morekeys_am_pm"
-                    latin:keyStyle="numFunctionalKeyStyle"
+                    latin:keyLabelFlags="hasPopupHint"
+                    latin:keyStyle="numKeyStyle"
+                    latin:backgroundType="functional"
                     latin:keyWidth="fillRight" />
             </case>
             <default>
                 <Key
                     latin:keySpec=","
-                    latin:keyStyle="numFunctionalKeyStyle"
+                    latin:keyStyle="numKeyStyle"
+                    latin:backgroundType="functional"
                     latin:keyWidth="fillRight" />
             </default>
         </switch>
diff --git a/java/res/xml/rows_number_password.xml b/java/res/xml/rows_number_password.xml
index 2e61a08..65736c4 100644
--- a/java/res/xml/rows_number_password.xml
+++ b/java/res/xml/rows_number_password.xml
@@ -70,7 +70,8 @@
         <Key
             latin:keyStyle="deleteKeyStyle" />
         <Key
-            latin:keyStyle="num0KeyStyle" />
+            latin:keyStyle="num0KeyStyle"
+            latin:keyHintLabel="+" />
         <Key
             latin:keyStyle="enterKeyStyle" />
         <!-- Note: This Spacer prevents the above key from being marked as a right edge key. -->
diff --git a/java/res/xml/rows_phone.xml b/java/res/xml/rows_phone.xml
index 03e4541..bb5590d 100644
--- a/java/res/xml/rows_phone.xml
+++ b/java/res/xml/rows_phone.xml
@@ -36,7 +36,8 @@
             latin:keySpec="-"
             latin:moreKeys="+"
             latin:keyLabelFlags="hasPopupHint"
-            latin:keyStyle="numFunctionalKeyStyle"
+            latin:keyStyle="numKeyStyle"
+            latin:backgroundType="functional"
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
@@ -48,7 +49,8 @@
             latin:keyStyle="num6KeyStyle" />
         <Key
             latin:keySpec="."
-            latin:keyStyle="numFunctionalKeyStyle"
+            latin:keyStyle="numKeyStyle"
+            latin:backgroundType="functional"
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
diff --git a/java/res/xml/rows_phone_symbols.xml b/java/res/xml/rows_phone_symbols.xml
index 983bfb5..195a183 100644
--- a/java/res/xml/rows_phone_symbols.xml
+++ b/java/res/xml/rows_phone_symbols.xml
@@ -28,45 +28,53 @@
     <Row>
         <Key
             latin:keySpec="("
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec="/"
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec=")"
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec="-"
             latin:moreKeys="+"
             latin:keyLabelFlags="hasPopupHint"
-            latin:keyStyle="numFunctionalKeyStyle"
+            latin:keyStyle="numKeyStyle"
+            latin:backgroundType="functional"
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
         <Key
             latin:keySpec="N"
-            latin:keyStyle="numKeyBaseStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <!-- Pause is a comma. Check PhoneNumberUtils.java to see if this
             has changed. -->
         <Key
-            latin:keyStyle="numPauseKeyStyle" />
+            latin:keySpec="!string/label_pause_key|,"
+            latin:keyLabelFlags="followKeyLabelRatio|autoXScale"
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec=","
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec="."
-            latin:keyStyle="numFunctionalKeyStyle"
+            latin:keyStyle="numKeyStyle"
+            latin:backgroundType="functional"
             latin:keyWidth="fillRight" />
     </Row>
     <Row>
+        <!-- U+FF0A: "*" FULLWIDTH ASTERISK -->
         <Key
-            latin:keyStyle="numStarKeyStyle" />
+            latin:keySpec="&#xFF0A;|*"
+            latin:keyStyle="numSymbolKeyStyle" />
         <!-- Wait is a semicolon. -->
         <Key
-            latin:keyStyle="numWaitKeyStyle" />
+            latin:keySpec="!string/label_wait_key|;"
+            latin:keyLabelFlags="followKeyLabelRatio|autoXScale"
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keySpec="\#"
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keyStyle="deleteKeyStyle"
             latin:keyWidth="fillRight" />
@@ -76,7 +84,7 @@
             latin:keyStyle="numPhoneToNumericKeyStyle" />
         <Key
             latin:keySpec="+"
-            latin:keyStyle="numKeyStyle" />
+            latin:keyStyle="numSymbolKeyStyle" />
         <Key
             latin:keyStyle="numSpaceKeyStyle" />
         <Key
diff --git a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java
index 7fc1e9d..2de71ce 100644
--- a/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java
+++ b/java/src/com/android/inputmethod/accessibility/KeyboardAccessibilityNodeProvider.java
@@ -31,9 +31,9 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardView;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.settings.SettingsValues;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 
 import java.util.List;
 
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
index 3a27c57..58ad4bd 100644
--- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
@@ -26,6 +26,8 @@
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
 
+import javax.annotation.Nonnull;
+
 public final class InputMethodSubtypeCompatUtils {
     private static final String TAG = InputMethodSubtypeCompatUtils.class.getSimpleName();
     // Note that InputMethodSubtype(int nameId, int iconId, String locale, String mode,
@@ -53,6 +55,7 @@
     }
 
     @SuppressWarnings("deprecation")
+    @Nonnull
     public static InputMethodSubtype newInputMethodSubtype(int nameId, int iconId, String locale,
             String mode, String extraValue, boolean isAuxiliary,
             boolean overridesImplicitlyEnabledSubtype, int id) {
diff --git a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
index d3e24e3..be07443 100644
--- a/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
+++ b/java/src/com/android/inputmethod/compat/SuggestionSpanUtils.java
@@ -27,8 +27,8 @@
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.SuggestionSpanPickedNotificationReceiver;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
index 37fa76b..659fe5c 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryProvider.java
@@ -31,6 +31,7 @@
 import android.util.Log;
 
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
 
 import java.io.File;
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
index e9b634e..e6acb8f 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionaryService.java
@@ -25,6 +25,7 @@
 import android.widget.Toast;
 
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.LocaleUtils;
 
 import java.util.Locale;
 import java.util.Random;
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index c2dc879..14e0050 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.dictionarypack;
 
+import com.android.inputmethod.latin.common.LocaleUtils;
+
 import android.app.Activity;
 import android.content.BroadcastReceiver;
 import android.content.ContentResolver;
diff --git a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java
index f1633ff..50b3c72 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DownloadOverMeteredDialog.java
@@ -26,6 +26,7 @@
 
 import com.android.inputmethod.annotations.ExternallyReferenced;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.LocaleUtils;
 
 import java.util.Locale;
 
diff --git a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
index db4315f..ce23eb7 100644
--- a/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
+++ b/java/src/com/android/inputmethod/dictionarypack/MetadataDbHelper.java
@@ -47,7 +47,8 @@
     // used to identify the versions for upgrades. This should never change going forward.
     private static final int METADATA_DATABASE_VERSION_WITH_CLIENTID = 6;
     // The current database version.
-    private static final int CURRENT_METADATA_DATABASE_VERSION = 10;
+    // This MUST be increased every time the dictionary pack metadata URL changes.
+    private static final int CURRENT_METADATA_DATABASE_VERSION = 11;
 
     private final static long NOT_A_DOWNLOAD_ID = -1;
 
diff --git a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
index d59b7a5..aeb6667 100644
--- a/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
+++ b/java/src/com/android/inputmethod/dictionarypack/UpdateHandler.java
@@ -39,6 +39,8 @@
 import com.android.inputmethod.compat.DownloadManagerCompatUtils;
 import com.android.inputmethod.compat.NotificationCompatUtils;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.LocaleUtils;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.DebugLogUtils;
 
@@ -78,7 +80,8 @@
     // DownloadManager uses as an ID numbers returned out of an AUTOINCREMENT column
     // in SQLite, so it should never return anything < 0.
     public static final int NOT_AN_ID = -1;
-    public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION = 2;
+    public static final int MAXIMUM_SUPPORTED_FORMAT_VERSION =
+            FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION;
 
     // Arbitrary. Probably good if it's a power of 2, and a couple thousand bytes long.
     private static final int FILE_COPY_BUFFER_SIZE = 8192;
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 3c90a04..619b801 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -22,7 +22,7 @@
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.internal.KeyboardParams;
 import com.android.inputmethod.latin.common.Constants;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index b674359..b105138 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -38,7 +38,6 @@
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodSubtype;
-import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
 import com.android.inputmethod.latin.utils.ScriptUtils;
@@ -52,6 +51,9 @@
 import java.lang.ref.SoftReference;
 import java.util.HashMap;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
 /**
  * This class represents a set of keyboard layouts. Each of them represents a different keyboard
  * specific to a keyboard state, such as alphabet, symbols, and so on.  Layouts in the same
@@ -83,6 +85,8 @@
     private static final HashMap<KeyboardId, SoftReference<Keyboard>> sKeyboardCache =
             new HashMap<>();
     private static final KeysCache sKeysCache = new KeysCache();
+    private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
+            new HashMap<>();
 
     @SuppressWarnings("serial")
     public static final class KeyboardLayoutSetException extends RuntimeException {
@@ -141,6 +145,16 @@
         sKeysCache.clear();
     }
 
+    public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) {
+        final Integer value = sScriptIdsForSubtypes.get(subtype);
+        if (null == value) {
+            final int scriptId = Builder.readScriptId(resources, subtype);
+            sScriptIdsForSubtypes.put(subtype, scriptId);
+            return scriptId;
+        }
+        return value;
+    }
+
     KeyboardLayoutSet(final Context context, final Params params) {
         mContext = context;
         mParams = params;
@@ -245,7 +259,7 @@
 
         private static final EditorInfo EMPTY_EDITOR_INFO = new EditorInfo();
 
-        public Builder(final Context context, final EditorInfo ei) {
+        public Builder(final Context context, @Nullable final EditorInfo ei) {
             mContext = context;
             mPackageName = context.getPackageName();
             mResources = context.getResources();
@@ -266,7 +280,7 @@
             return this;
         }
 
-        public Builder setSubtype(final RichInputMethodSubtype subtype) {
+        public Builder setSubtype(@Nonnull final RichInputMethodSubtype subtype) {
             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
             // TODO: Consolidate with {@link InputAttributes}.
             @SuppressWarnings("deprecation")
@@ -276,11 +290,11 @@
                     mParams.mEditorInfo.imeOptions)
                     || deprecatedForceAscii;
             final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
-                    ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
+                    ? RichInputMethodSubtype.getNoLanguageSubtype()
                     : subtype;
             mParams.mSubtype = keyboardSubtype;
             mParams.mKeyboardLayoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
-                    + SubtypeLocaleUtils.getKeyboardLayoutSetName(keyboardSubtype);
+                    + keyboardSubtype.getKeyboardLayoutSetName();
             return this;
         }
 
@@ -304,31 +318,13 @@
             return this;
         }
 
-        public Builder setScriptId(final int scriptId) {
-            mParams.mScriptId = scriptId;
-            return this;
-        }
-
         public Builder setSplitLayoutEnabledByUser(final boolean enabled) {
             mParams.mIsSplitLayoutEnabledByUser = enabled;
             return this;
         }
 
-        private final static HashMap<InputMethodSubtype, Integer> sScriptIdsForSubtypes =
-                new HashMap<>();
-        public static int getScriptId(final Resources resources, final InputMethodSubtype subtype) {
-            final Integer value = sScriptIdsForSubtypes.get(subtype);
-            if (null == value) {
-                final int scriptId = readScriptId(resources, subtype);
-                sScriptIdsForSubtypes.put(subtype, scriptId);
-                return scriptId;
-            }
-            return value;
-        }
-
         // Super redux version of reading the script ID for some subtype from Xml.
-        private static int readScriptId(final Resources resources,
-                final InputMethodSubtype subtype) {
+        static int readScriptId(final Resources resources, final InputMethodSubtype subtype) {
             final String layoutSetName = KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX
                     + SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
             final int xmlId = getXmlId(resources, layoutSetName);
@@ -359,7 +355,7 @@
             try {
                 final int scriptId =
                         featureAttr.getInt(R.styleable.KeyboardLayoutSet_Feature_supportedScript,
-                        ScriptUtils.SCRIPT_UNKNOWN);
+                                ScriptUtils.SCRIPT_UNKNOWN);
                 XmlParseUtils.checkEndTag(TAG_FEATURE, parser);
                 return scriptId;
             } finally {
@@ -415,7 +411,7 @@
                     if (TAG_ELEMENT.equals(tag)) {
                         parseKeyboardLayoutSetElement(parser);
                     } else if (TAG_FEATURE.equals(tag)) {
-                        parseKeyboardLayoutSetFeature(parser);
+                        mParams.mScriptId = readScriptIdFromTagFeature(mResources, parser);
                     } else {
                         throw new XmlParseUtils.IllegalStartTag(parser, tag, TAG_KEYBOARD_SET);
                     }
@@ -460,12 +456,6 @@
             }
         }
 
-        private void parseKeyboardLayoutSetFeature(final XmlPullParser parser)
-                throws XmlPullParserException, IOException {
-            final int scriptId = readScriptIdFromTagFeature(mResources, parser);
-            setScriptId(scriptId);
-        }
-
         private static int getKeyboardMode(final EditorInfo editorInfo) {
             final int inputType = editorInfo.inputType;
             final int variation = inputType & InputType.TYPE_MASK_VARIATION;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 89acc3c..5e3a5f1 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -39,6 +39,8 @@
 import com.android.inputmethod.latin.define.ProductionFlags;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.settings.SettingsValues;
+import com.android.inputmethod.latin.utils.CapsModeUtils;
+import com.android.inputmethod.latin.utils.RecapitalizeStatus;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 import com.android.inputmethod.latin.utils.ScriptUtils;
 
@@ -112,7 +114,7 @@
         final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
         final int keyboardHeight = ResourceUtils.getKeyboardHeight(res, settingsValues);
         builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
-        builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
+        builder.setSubtype(RichInputMethodManager.getInstance().getCurrentSubtype());
         builder.setVoiceInputKeyEnabled(settingsValues.mShowsVoiceInputKey);
         builder.setLanguageSwitchKeyEnabled(mLatinIME.shouldShowLanguageSwitchKey());
         builder.setSplitLayoutEnabledByUser(ProductionFlags.IS_SPLIT_KEYBOARD_SUPPORTED
@@ -121,7 +123,8 @@
         try {
             mState.onLoadKeyboard(currentAutoCapsState, currentRecapitalizeState);
             // TODO: revisit this for multi-lingual input
-            mKeyboardTextsSet.setLocale(mSubtypeSwitcher.getCurrentSubtypeLocales()[0],
+            mKeyboardTextsSet.setLocale(
+                    RichInputMethodManager.getInstance().getCurrentSubtypeLocales()[0],
                     mThemeContext);
         } catch (KeyboardLayoutSetException e) {
             Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
@@ -161,7 +164,7 @@
                 currentSettingsValues.mKeyPreviewDismissEndXScale,
                 currentSettingsValues.mKeyPreviewDismissEndYScale,
                 currentSettingsValues.mKeyPreviewDismissDuration);
-        keyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
+        keyboardView.updateShortcutKey(RichInputMethodManager.getInstance().isShortcutImeReady());
         final boolean subtypeChanged = (oldKeyboard == null)
                 || !keyboard.mId.mSubtype.equals(oldKeyboard.mId.mSubtype);
         final int languageOnSpacebarFormatType = mSubtypeSwitcher.getLanguageOnSpacebarFormatType(
@@ -204,42 +207,64 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setAlphabetKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setAlphabetKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET));
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setAlphabetManualShiftedKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setAlphabetManualShiftedKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED));
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setAlphabetAutomaticShiftedKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setAlphabetAutomaticShiftedKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED));
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setAlphabetShiftLockedKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setAlphabetShiftLockedKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCKED));
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setAlphabetShiftLockShiftedKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setAlphabetShiftLockShiftedKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_SHIFT_LOCK_SHIFTED));
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setSymbolsKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setSymbolsKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS));
     }
 
     private void setMainKeyboardFrame(final SettingsValues settingsValues) {
-        mMainKeyboardFrame.setVisibility(
-                settingsValues.mHasHardwareKeyboard ? View.GONE : View.VISIBLE);
+        final int visibility = settingsValues.mHasHardwareKeyboard ? View.GONE : View.VISIBLE;
+        mKeyboardView.setVisibility(visibility);
+        // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
+        // @see #getVisibleKeyboardView() and
+        // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
+        mMainKeyboardFrame.setVisibility(visibility);
         mEmojiPalettesView.setVisibility(View.GONE);
         mEmojiPalettesView.stopEmojiPalettes();
     }
@@ -247,8 +272,15 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setEmojiKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setEmojiKeyboard");
+        }
         final Keyboard keyboard = mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
         mMainKeyboardFrame.setVisibility(View.GONE);
+        // The visibility of {@link #mKeyboardView} must be aligned with {@link #MainKeyboardFrame}.
+        // @see #getVisibleKeyboardView() and
+        // @see LatinIME#onComputeInset(android.inputmethodservice.InputMethodService.Insets)
+        mKeyboardView.setVisibility(View.GONE);
         mEmojiPalettesView.startEmojiPalettes(
                 mKeyboardTextsSet.getText(KeyboardTextsSet.SWITCH_TO_ALPHA_KEY_LABEL),
                 mKeyboardView.getKeyVisualAttribute(), keyboard.mIconsSet);
@@ -269,19 +301,29 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void setSymbolsShiftedKeyboard() {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "setSymbolsShiftedKeyboard");
+        }
         setKeyboard(mKeyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_SYMBOLS_SHIFTED));
     }
 
     // Future method for requesting an updating to the shift state.
     @Override
-    public void requestUpdatingShiftState(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
-        mState.onUpdateShiftState(currentAutoCapsState, currentRecapitalizeState);
+    public void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode) {
+        if (DEBUG_ACTION) {
+            Log.d(TAG, "requestUpdatingShiftState: "
+                    + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
+                    + " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode));
+        }
+        mState.onUpdateShiftState(autoCapsFlags, recapitalizeMode);
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void startDoubleTapShiftKeyTimer() {
+        if (DEBUG_TIMER_ACTION) {
+            Log.d(TAG, "startDoubleTapShiftKeyTimer");
+        }
         final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             keyboardView.startDoubleTapShiftKeyTimer();
@@ -291,6 +333,9 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public void cancelDoubleTapShiftKeyTimer() {
+        if (DEBUG_TIMER_ACTION) {
+            Log.d(TAG, "setAlphabetKeyboard");
+        }
         final MainKeyboardView keyboardView = getMainKeyboardView();
         if (keyboardView != null) {
             keyboardView.cancelDoubleTapShiftKeyTimer();
@@ -300,6 +345,9 @@
     // Implements {@link KeyboardState.SwitchActions}.
     @Override
     public boolean isInDoubleTapShiftKeyTimeout() {
+        if (DEBUG_TIMER_ACTION) {
+            Log.d(TAG, "isInDoubleTapShiftKeyTimeout");
+        }
         final MainKeyboardView keyboardView = getMainKeyboardView();
         return keyboardView != null && keyboardView.isInDoubleTapShiftKeyTimeout();
     }
@@ -367,9 +415,10 @@
     }
 
     public void onNetworkStateChanged() {
-        if (mKeyboardView != null) {
-            mKeyboardView.updateShortcutKey(mSubtypeSwitcher.isShortcutImeReady());
+        if (mKeyboardView == null) {
+            return;
         }
+        mKeyboardView.updateShortcutKey(RichInputMethodManager.getInstance().isShortcutImeReady());
     }
 
     public int getKeyboardShiftMode() {
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index f23db04..cba7ff2 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -39,8 +39,8 @@
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.accessibility.MainKeyboardAccessibilityDelegate;
 import com.android.inputmethod.annotations.ExternallyReferenced;
-import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
 import com.android.inputmethod.keyboard.internal.DrawingPreviewPlacerView;
+import com.android.inputmethod.keyboard.internal.DrawingProxy;
 import com.android.inputmethod.keyboard.internal.GestureFloatingTextDrawingPreview;
 import com.android.inputmethod.keyboard.internal.GestureTrailsDrawingPreview;
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
@@ -56,9 +56,9 @@
 import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.settings.DebugSettings;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.TypefaceUtils;
 
 import java.util.Locale;
@@ -110,7 +110,7 @@
  * @attr ref R.styleable#MainKeyboardView_gestureRecognitionSpeedThreshold
  * @attr ref R.styleable#MainKeyboardView_suppressKeyPreviewAfterBatchInputDuration
  */
-public final class MainKeyboardView extends KeyboardView implements PointerTracker.DrawingProxy,
+public final class MainKeyboardView extends KeyboardView implements DrawingProxy,
         MoreKeysPanel.Controller {
     private static final String TAG = MainKeyboardView.class.getSimpleName();
 
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
index 0152253..3acc11b 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboardView.java
@@ -31,7 +31,7 @@
 import com.android.inputmethod.keyboard.internal.KeyDrawParams;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.common.Constants;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 /**
  * A view that renders a virtual {@link MoreKeysKeyboard}. It handles rendering of keys and
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 467f515..7902ce8 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -25,18 +25,20 @@
 import com.android.inputmethod.keyboard.internal.BatchInputArbiter;
 import com.android.inputmethod.keyboard.internal.BatchInputArbiter.BatchInputArbiterListener;
 import com.android.inputmethod.keyboard.internal.BogusMoveEventDetector;
+import com.android.inputmethod.keyboard.internal.DrawingProxy;
 import com.android.inputmethod.keyboard.internal.GestureEnabler;
 import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingParams;
 import com.android.inputmethod.keyboard.internal.GestureStrokeDrawingPoints;
 import com.android.inputmethod.keyboard.internal.GestureStrokeRecognitionParams;
 import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
+import com.android.inputmethod.keyboard.internal.TimerProxy;
 import com.android.inputmethod.keyboard.internal.TypingTimeRecorder;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.common.InputPointers;
 import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.settings.Settings;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 
 import java.util.ArrayList;
@@ -52,66 +54,6 @@
     private static final boolean DEBUG_LISTENER = false;
     private static boolean DEBUG_MODE = DebugFlags.DEBUG_ENABLED || DEBUG_EVENT;
 
-    public interface DrawingProxy {
-        public void invalidateKey(@Nullable Key key);
-        public void showKeyPreview(@Nonnull Key key);
-        public void dismissKeyPreview(@Nonnull Key key);
-        public void dismissKeyPreviewWithoutDelay(@Nonnull Key key);
-        public void onLongPress(@Nonnull PointerTracker tracker);
-        public static final int FADE_IN = 0;
-        public static final int FADE_OUT = 1;
-        public void startWhileTypingAnimation(final int fadeInOrOut);
-        public void showSlidingKeyInputPreview(@Nullable PointerTracker tracker);
-        public void showGestureTrail(@Nonnull PointerTracker tracker,
-                boolean showsFloatingPreviewText);
-        public void dismissGestureFloatingPreviewTextWithoutDelay();
-    }
-
-    public interface TimerProxy {
-        public void startTypingStateTimer(Key typedKey);
-        public boolean isTypingState();
-        public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay);
-        public void startLongPressTimerOf(PointerTracker tracker, int delay);
-        public void cancelLongPressTimerOf(PointerTracker tracker);
-        public void cancelLongPressShiftKeyTimers();
-        public void cancelKeyTimersOf(PointerTracker tracker);
-        public void startDoubleTapShiftKeyTimer();
-        public void cancelDoubleTapShiftKeyTimer();
-        public boolean isInDoubleTapShiftKeyTimeout();
-        public void startUpdateBatchInputTimer(PointerTracker tracker);
-        public void cancelUpdateBatchInputTimer(PointerTracker tracker);
-        public void cancelAllUpdateBatchInputTimers();
-
-        public static class Adapter implements TimerProxy {
-            @Override
-            public void startTypingStateTimer(Key typedKey) {}
-            @Override
-            public boolean isTypingState() { return false; }
-            @Override
-            public void startKeyRepeatTimerOf(PointerTracker tracker, int repeatCount, int delay) {}
-            @Override
-            public void startLongPressTimerOf(PointerTracker tracker, int delay) {}
-            @Override
-            public void cancelLongPressTimerOf(PointerTracker tracker) {}
-            @Override
-            public void cancelLongPressShiftKeyTimers() {}
-            @Override
-            public void cancelKeyTimersOf(PointerTracker tracker) {}
-            @Override
-            public void startDoubleTapShiftKeyTimer() {}
-            @Override
-            public void cancelDoubleTapShiftKeyTimer() {}
-            @Override
-            public boolean isInDoubleTapShiftKeyTimeout() { return false; }
-            @Override
-            public void startUpdateBatchInputTimer(PointerTracker tracker) {}
-            @Override
-            public void cancelUpdateBatchInputTimer(PointerTracker tracker) {}
-            @Override
-            public void cancelAllUpdateBatchInputTimers() {}
-        }
-    }
-
     static final class PointerTrackerParams {
         public final boolean mKeySelectionByDraggingFinger;
         public final int mTouchNoiseThresholdTime;
@@ -586,7 +528,7 @@
         }
         sListener.onStartBatchInput();
         dismissAllMoreKeysPanels();
-        sTimerProxy.cancelLongPressTimerOf(this);
+        sTimerProxy.cancelLongPressTimersOf(this);
     }
 
     private void showGestureTrail() {
@@ -1094,7 +1036,7 @@
     }
 
     public void cancelLongPressTimer() {
-        sTimerProxy.cancelLongPressTimerOf(this);
+        sTimerProxy.cancelLongPressTimersOf(this);
     }
 
     public void onLongPressed() {
@@ -1163,7 +1105,7 @@
     private void startLongPressTimer(final Key key) {
         // Note that we need to cancel all active long press shift key timers if any whenever we
         // start a new long press timer for both non-shift and shift keys.
-        sTimerProxy.cancelLongPressShiftKeyTimers();
+        sTimerProxy.cancelLongPressShiftKeyTimer();
         if (sInGesture) return;
         if (key == null) return;
         if (!key.isLongPressEnabled()) return;
diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
index 54d3e3b..09313f8 100644
--- a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPageKeyboardView.java
@@ -148,7 +148,7 @@
 
     void callListenerOnPressKey(final Key pressedKey) {
         mPendingKeyDown = null;
-        pressedKey.onReleased();
+        pressedKey.onPressed();
         invalidateKey(pressedKey);
         mListener.onPressKey(pressedKey);
     }
diff --git a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java
index 06184f8..cf4dd3d 100644
--- a/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java
+++ b/java/src/com/android/inputmethod/keyboard/emoji/EmojiPalettesView.java
@@ -48,7 +48,7 @@
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.common.Constants;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 
@@ -113,7 +113,7 @@
                 context, null /* editorInfo */);
         final Resources res = context.getResources();
         mEmojiLayoutParams = new EmojiLayoutParams(res);
-        builder.setSubtype(SubtypeSwitcher.getInstance().getEmojiSubtype());
+        builder.setSubtype(RichInputMethodSubtype.getEmojiSubtype());
         builder.setKeyboardGeometry(ResourceUtils.getDefaultKeyboardWidth(res),
                 mEmojiLayoutParams.mEmojiKeyboardHeight);
         final KeyboardLayoutSet layoutSet = builder.build();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java
index a5d47ad..9c0d743 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingPreviewPlacerView.java
@@ -24,7 +24,7 @@
 import android.util.AttributeSet;
 import android.widget.RelativeLayout;
 
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 import java.util.ArrayList;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/DrawingProxy.java b/java/src/com/android/inputmethod/keyboard/internal/DrawingProxy.java
new file mode 100644
index 0000000..7fc586a
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/DrawingProxy.java
@@ -0,0 +1,72 @@
+/*
+ * 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.internal;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.PointerTracker;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+public interface DrawingProxy {
+    // TODO: Remove this method.
+    public void invalidateKey(@Nullable Key key);
+
+    // TODO: Rename this method to onKeyPressed.
+    public void showKeyPreview(@Nonnull Key key);
+
+    // TODO: Rename this method to onKeyReleased.
+    public void dismissKeyPreview(@Nonnull Key key);
+
+    /**
+     * Dismiss a key preview visual without delay.
+     * @param key the key whose preview visual should be dismissed.
+     */
+    public void dismissKeyPreviewWithoutDelay(@Nonnull Key key);
+
+    // TODO: Rename this method to onKeyLongPressed.
+    public void onLongPress(@Nonnull PointerTracker tracker);
+
+    /**
+     * Start a while-typing-animation.
+     * @param fadeInOrOut {@link #FADE_IN} starts while-typing-fade-in animation.
+     * {@link #FADE_OUT} starts while-typing-fade-out animation.
+     */
+    public void startWhileTypingAnimation(int fadeInOrOut);
+    public static final int FADE_IN = 0;
+    public static final int FADE_OUT = 1;
+
+    /**
+     * Show sliding-key input preview.
+     * @param tracker the {@link PointerTracker} that is currently doing the sliding-key input.
+     * null to dismiss the sliding-key input preview.
+     */
+    public void showSlidingKeyInputPreview(@Nullable PointerTracker tracker);
+
+    /**
+     * Show gesture trails.
+     * @param tracker the {@link PointerTracker} whose gesture trail will be shown.
+     * @param showsFloatingPreviewText when true, a gesture floating preview text will be shown
+     * with this <code>tracker</code>'s trail.
+     */
+    public void showGestureTrail(@Nonnull PointerTracker tracker, boolean showsFloatingPreviewText);
+
+    /**
+     * Dismiss a gesture floating preview text without delay.
+     */
+    public void dismissGestureFloatingPreviewTextWithoutDelay();
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
index 330ec52..5443c2a 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureFloatingTextDrawingPreview.java
@@ -27,7 +27,7 @@
 import com.android.inputmethod.keyboard.PointerTracker;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 import javax.annotation.Nonnull;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
index d376487..448f1b4 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyPreviewChoreographer.java
@@ -23,7 +23,7 @@
 import android.view.ViewGroup;
 
 import com.android.inputmethod.keyboard.Key;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.utils.ViewLayoutUtils;
 
 import java.util.ArrayDeque;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index c739bf3..51f89c1 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -36,7 +36,6 @@
 import com.android.inputmethod.latin.common.Constants;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.utils.ResourceUtils;
-import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 import com.android.inputmethod.latin.utils.XmlParseUtils;
 import com.android.inputmethod.latin.utils.XmlParseUtils.ParseException;
 
@@ -648,7 +647,7 @@
         try {
             final boolean keyboardLayoutSetMatched = matchString(caseAttr,
                     R.styleable.Keyboard_Case_keyboardLayoutSet,
-                    SubtypeLocaleUtils.getKeyboardLayoutSetName(id.mSubtype));
+                    id.mSubtype.getKeyboardLayoutSetName());
             final boolean keyboardLayoutSetElementMatched = matchTypedValue(caseAttr,
                     R.styleable.Keyboard_Case_keyboardLayoutSetElement, id.mElementId,
                     KeyboardId.elementIdToName(id.mElementId));
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index cc28e7a..70e1167 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -21,6 +21,7 @@
 
 import com.android.inputmethod.event.Event;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.utils.CapsModeUtils;
 import com.android.inputmethod.latin.utils.RecapitalizeStatus;
 
 /**
@@ -38,9 +39,11 @@
 public final class KeyboardState {
     private static final String TAG = KeyboardState.class.getSimpleName();
     private static final boolean DEBUG_EVENT = false;
-    private static final boolean DEBUG_ACTION = false;
+    private static final boolean DEBUG_INTERNAL_ACTION = false;
 
     public interface SwitchActions {
+        public static final boolean DEBUG_ACTION = false;
+
         public void setAlphabetKeyboard();
         public void setAlphabetManualShiftedKeyboard();
         public void setAlphabetAutomaticShiftedKeyboard();
@@ -53,8 +56,9 @@
         /**
          * Request to call back {@link KeyboardState#onUpdateShiftState(int, int)}.
          */
-        public void requestUpdatingShiftState(final int currentAutoCapsState,
-                final int currentRecapitalizeState);
+        public void requestUpdatingShiftState(final int autoCapsFlags, final int recapitalizeMode);
+
+        public static final boolean DEBUG_TIMER_ACTION = false;
 
         public void startDoubleTapShiftKeyTimer();
         public boolean isInDoubleTapShiftKeyTimeout();
@@ -119,10 +123,9 @@
         mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
     }
 
-    public void onLoadKeyboard(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    public void onLoadKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onLoadKeyboard: " + this);
+            Log.d(TAG, "onLoadKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         // Reset alphabet shift state.
         mAlphabetShiftState.setShiftLocked(false);
@@ -130,7 +133,7 @@
         mPrevSymbolsKeyboardWasShifted = false;
         mShiftKeyState.onRelease();
         mSymbolKeyState.onRelease();
-        onRestoreKeyboardState(currentAutoCapsState, currentRecapitalizeState);
+        onRestoreKeyboardState(autoCapsFlags, recapitalizeMode);
     }
 
     private static final int UNSHIFT = 0;
@@ -156,14 +159,14 @@
         }
     }
 
-    private void onRestoreKeyboardState(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    private void onRestoreKeyboardState(final int autoCapsFlags, final int recapitalizeMode) {
         final SavedKeyboardState state = mSavedKeyboardState;
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onRestoreKeyboardState: saved=" + state + " " + this);
+            Log.d(TAG, "onRestoreKeyboardState: saved=" + state
+                    + " " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         if (!state.mIsValid || state.mIsAlphabetMode) {
-            setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState);
+            setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
         } else if (state.mIsEmojiMode) {
             setEmojiKeyboard();
         } else {
@@ -188,7 +191,7 @@
     }
 
     private void setShifted(final int shiftMode) {
-        if (DEBUG_ACTION) {
+        if (DEBUG_INTERNAL_ACTION) {
             Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode) + " " + this);
         }
         if (!mIsAlphabetMode) return;
@@ -227,7 +230,7 @@
     }
 
     private void setShiftLocked(final boolean shiftLocked) {
-        if (DEBUG_ACTION) {
+        if (DEBUG_INTERNAL_ACTION) {
             Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked + " " + this);
         }
         if (!mIsAlphabetMode) return;
@@ -241,10 +244,10 @@
         mAlphabetShiftState.setShiftLocked(shiftLocked);
     }
 
-    private void toggleAlphabetAndSymbols(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
-        if (DEBUG_ACTION) {
-            Log.d(TAG, "toggleAlphabetAndSymbols: " + this);
+    private void toggleAlphabetAndSymbols(final int autoCapsFlags, final int recapitalizeMode) {
+        if (DEBUG_INTERNAL_ACTION) {
+            Log.d(TAG, "toggleAlphabetAndSymbols: "
+                    + stateToString(autoCapsFlags, recapitalizeMode));
         }
         if (mIsAlphabetMode) {
             mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
@@ -256,7 +259,7 @@
             mPrevSymbolsKeyboardWasShifted = false;
         } else {
             mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
-            setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState);
+            setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
             if (mPrevMainKeyboardWasShiftLocked) {
                 setShiftLocked(true);
             }
@@ -266,15 +269,15 @@
 
     // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
     // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
-    private void resetKeyboardStateToAlphabet(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
-        if (DEBUG_ACTION) {
-            Log.d(TAG, "resetKeyboardStateToAlphabet: " + this);
+    private void resetKeyboardStateToAlphabet(final int autoCapsFlags, final int recapitalizeMode) {
+        if (DEBUG_INTERNAL_ACTION) {
+            Log.d(TAG, "resetKeyboardStateToAlphabet: "
+                    + stateToString(autoCapsFlags, recapitalizeMode));
         }
         if (mIsAlphabetMode) return;
 
         mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
-        setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState);
+        setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
         if (mPrevMainKeyboardWasShiftLocked) {
             setShiftLocked(true);
         }
@@ -289,10 +292,9 @@
         }
     }
 
-    private void setAlphabetKeyboard(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
-        if (DEBUG_ACTION) {
-            Log.d(TAG, "setAlphabetKeyboard");
+    private void setAlphabetKeyboard(final int autoCapsFlags, final int recapitalizeMode) {
+        if (DEBUG_INTERNAL_ACTION) {
+            Log.d(TAG, "setAlphabetKeyboard: " + stateToString(autoCapsFlags, recapitalizeMode));
         }
 
         mSwitchActions.setAlphabetKeyboard();
@@ -301,11 +303,11 @@
         mIsSymbolShifted = false;
         mRecapitalizeMode = RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE;
         mSwitchState = SWITCH_STATE_ALPHA;
-        mSwitchActions.requestUpdatingShiftState(currentAutoCapsState, currentRecapitalizeState);
+        mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
     }
 
     private void setSymbolsKeyboard() {
-        if (DEBUG_ACTION) {
+        if (DEBUG_INTERNAL_ACTION) {
             Log.d(TAG, "setSymbolsKeyboard");
         }
         mSwitchActions.setSymbolsKeyboard();
@@ -318,7 +320,7 @@
     }
 
     private void setSymbolsShiftedKeyboard() {
-        if (DEBUG_ACTION) {
+        if (DEBUG_INTERNAL_ACTION) {
             Log.d(TAG, "setSymbolsShiftedKeyboard");
         }
         mSwitchActions.setSymbolsShiftedKeyboard();
@@ -331,7 +333,7 @@
     }
 
     private void setEmojiKeyboard() {
-        if (DEBUG_ACTION) {
+        if (DEBUG_INTERNAL_ACTION) {
             Log.d(TAG, "setEmojiKeyboard");
         }
         mIsAlphabetMode = false;
@@ -343,11 +345,12 @@
         mSwitchActions.setEmojiKeyboard();
     }
 
-    public void onPressKey(final int code, final boolean isSinglePointer,
-            final int currentAutoCapsState, final int currentRecapitalizeState) {
+    public void onPressKey(final int code, final boolean isSinglePointer, final int autoCapsFlags,
+            final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code) + " single="
-                    + isSinglePointer + " autoCaps=" + currentAutoCapsState + " " + this);
+            Log.d(TAG, "onPressKey: code=" + Constants.printableCode(code)
+                    + " single=" + isSinglePointer
+                    + " " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         if (code != Constants.CODE_SHIFT) {
             // Because the double tap shift key timer is to detect two consecutive shift key press,
@@ -359,7 +362,7 @@
         } else if (code == Constants.CODE_CAPSLOCK) {
             // Nothing to do here. See {@link #onReleaseKey(int,boolean)}.
         } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
-            onPressSymbol(currentAutoCapsState, currentRecapitalizeState);
+            onPressSymbol(autoCapsFlags, recapitalizeMode);
         } else {
             mShiftKeyState.onOtherKeyPressed();
             mSymbolKeyState.onOtherKeyPressed();
@@ -372,7 +375,7 @@
             // off because, for example, we may be in the #1 state within the manual temporary
             // shifted mode.
             if (!isSinglePointer && mIsAlphabetMode
-                    && currentAutoCapsState != TextUtils.CAP_MODE_CHARACTERS) {
+                    && autoCapsFlags != TextUtils.CAP_MODE_CHARACTERS) {
                 final boolean needsToResetAutoCaps = mAlphabetShiftState.isAutomaticShifted()
                         || (mAlphabetShiftState.isManualShifted() && mShiftKeyState.isReleasing());
                 if (needsToResetAutoCaps) {
@@ -382,34 +385,35 @@
         }
     }
 
-    public void onReleaseKey(final int code, final boolean withSliding,
-            final int currentAutoCapsState, final int currentRecapitalizeState) {
+    public void onReleaseKey(final int code, final boolean withSliding, final int autoCapsFlags,
+            final int recapitalizeMode) {
         if (DEBUG_EVENT) {
             Log.d(TAG, "onReleaseKey: code=" + Constants.printableCode(code)
-                    + " sliding=" + withSliding + " " + this);
+                    + " sliding=" + withSliding
+                    + " " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         if (code == Constants.CODE_SHIFT) {
-            onReleaseShift(withSliding, currentAutoCapsState, currentRecapitalizeState);
+            onReleaseShift(withSliding, autoCapsFlags, recapitalizeMode);
         } else if (code == Constants.CODE_CAPSLOCK) {
             setShiftLocked(!mAlphabetShiftState.isShiftLocked());
         } else if (code == Constants.CODE_SWITCH_ALPHA_SYMBOL) {
-            onReleaseSymbol(withSliding, currentAutoCapsState, currentRecapitalizeState);
+            onReleaseSymbol(withSliding, autoCapsFlags, recapitalizeMode);
         }
     }
 
-    private void onPressSymbol(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
-        toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState);
+    private void onPressSymbol(final int autoCapsFlags,
+            final int recapitalizeMode) {
+        toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
         mSymbolKeyState.onPress();
         mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL;
     }
 
-    private void onReleaseSymbol(final boolean withSliding, final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    private void onReleaseSymbol(final boolean withSliding, final int autoCapsFlags,
+            final int recapitalizeMode) {
         if (mSymbolKeyState.isChording()) {
             // Switch back to the previous keyboard mode if the user chords the mode change key and
             // another key, then releases the mode change key.
-            toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState);
+            toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
         } else if (!withSliding) {
             // If the mode change key is being released without sliding, we should forget the
             // previous symbols keyboard shift state and simply switch back to symbols layout
@@ -419,23 +423,23 @@
         mSymbolKeyState.onRelease();
     }
 
-    public void onUpdateShiftState(final int autoCaps, final int recapitalizeMode) {
+    public void onUpdateShiftState(final int autoCapsFlags, final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onUpdateShiftState: autoCaps=" + autoCaps + ", recapitalizeMode="
-                    + recapitalizeMode + " " + this);
+            Log.d(TAG, "onUpdateShiftState: " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         mRecapitalizeMode = recapitalizeMode;
-        updateAlphabetShiftState(autoCaps, recapitalizeMode);
+        updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
     }
 
     // TODO: Remove this method. Come up with a more comprehensive way to reset the keyboard layout
     // when a keyboard layout set doesn't get reloaded in LatinIME.onStartInputViewInternal().
-    public void onResetKeyboardStateToAlphabet(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    public void onResetKeyboardStateToAlphabet(final int autoCapsFlags,
+            final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onResetKeyboardStateToAlphabet: " + this);
+            Log.d(TAG, "onResetKeyboardStateToAlphabet: "
+                    + stateToString(autoCapsFlags, recapitalizeMode));
         }
-        resetKeyboardStateToAlphabet(currentAutoCapsState, currentRecapitalizeState);
+        resetKeyboardStateToAlphabet(autoCapsFlags, recapitalizeMode);
     }
 
     private void updateShiftStateForRecapitalize(final int recapitalizeMode) {
@@ -453,7 +457,7 @@
         }
     }
 
-    private void updateAlphabetShiftState(final int autoCaps, final int recapitalizeMode) {
+    private void updateAlphabetShiftState(final int autoCapsFlags, final int recapitalizeMode) {
         if (!mIsAlphabetMode) return;
         if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != recapitalizeMode) {
             // We are recapitalizing. Match the keyboard to the current recapitalize state.
@@ -466,7 +470,7 @@
             return;
         }
         if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
-            if (mShiftKeyState.isReleasing() && autoCaps != Constants.TextUtils.CAP_MODE_OFF) {
+            if (mShiftKeyState.isReleasing() && autoCapsFlags != Constants.TextUtils.CAP_MODE_OFF) {
                 // Only when shift key is releasing, automatic temporary upper case will be set.
                 setShifted(AUTOMATIC_SHIFT);
             } else {
@@ -526,8 +530,8 @@
         }
     }
 
-    private void onReleaseShift(final boolean withSliding, final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    private void onReleaseShift(final boolean withSliding, final int autoCapsFlags,
+            final int recapitalizeMode) {
         if (RecapitalizeStatus.NOT_A_RECAPITALIZE_MODE != mRecapitalizeMode) {
             // We are recapitalizing. We should match the keyboard state to the recapitalize
             // state in priority.
@@ -550,8 +554,7 @@
                 // After chording input, automatic shift state may have been changed depending on
                 // what characters were input.
                 mShiftKeyState.onRelease();
-                mSwitchActions.requestUpdatingShiftState(currentAutoCapsState,
-                        currentRecapitalizeState);
+                mSwitchActions.requestUpdatingShiftState(autoCapsFlags, recapitalizeMode);
                 return;
             } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) {
                 // In shift locked state, shift has been pressed and slid out to other key.
@@ -588,21 +591,20 @@
         mShiftKeyState.onRelease();
     }
 
-    public void onFinishSlidingInput(final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    public void onFinishSlidingInput(final int autoCapsFlags, final int recapitalizeMode) {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onFinishSlidingInput: " + this);
+            Log.d(TAG, "onFinishSlidingInput: " + stateToString(autoCapsFlags, recapitalizeMode));
         }
         // Switch back to the previous keyboard mode if the user cancels sliding input.
         switch (mSwitchState) {
         case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
-            toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState);
+            toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
             break;
         case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
             toggleShiftInSymbols();
             break;
         case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT:
-            setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState);
+            setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
             break;
         }
     }
@@ -611,12 +613,11 @@
         return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
     }
 
-    public void onEvent(final Event event, final int currentAutoCapsState,
-            final int currentRecapitalizeState) {
+    public void onEvent(final Event event, final int autoCapsFlags, final int recapitalizeMode) {
         final int code = event.isFunctionalKeyEvent() ? event.mKeyCode : event.mCodePoint;
         if (DEBUG_EVENT) {
             Log.d(TAG, "onEvent: code=" + Constants.printableCode(code)
-                    + " autoCaps=" + currentAutoCapsState + " " + this);
+                    + " " + stateToString(autoCapsFlags, recapitalizeMode));
         }
 
         switch (mSwitchState) {
@@ -652,7 +653,7 @@
             // Switch back to alpha keyboard mode if user types one or more non-space/enter
             // characters followed by a space/enter.
             if (isSpaceOrEnter(code)) {
-                toggleAlphabetAndSymbols(currentAutoCapsState, currentRecapitalizeState);
+                toggleAlphabetAndSymbols(autoCapsFlags, recapitalizeMode);
                 mPrevSymbolsKeyboardWasShifted = false;
             }
             break;
@@ -660,11 +661,11 @@
 
         // If the code is a letter, update keyboard shift state.
         if (Constants.isLetterCode(code)) {
-            updateAlphabetShiftState(currentAutoCapsState, currentRecapitalizeState);
+            updateAlphabetShiftState(autoCapsFlags, recapitalizeMode);
         } else if (code == Constants.CODE_EMOJI) {
             setEmojiKeyboard();
         } else if (code == Constants.CODE_ALPHA_FROM_EMOJI) {
-            setAlphabetKeyboard(currentAutoCapsState, currentRecapitalizeState);
+            setAlphabetKeyboard(autoCapsFlags, recapitalizeMode);
         }
     }
 
@@ -697,4 +698,9 @@
                 + " symbol=" + mSymbolKeyState
                 + " switch=" + switchStateToString(mSwitchState) + "]";
     }
+
+    private String stateToString(final int autoCapsFlags, final int recapitalizeMode) {
+        return this + " autoCapsFlags=" + CapsModeUtils.flagsToString(autoCapsFlags)
+                + " recapitalizeMode=" + RecapitalizeStatus.modeToString(recapitalizeMode);
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
index 21eaed9..8ed8010 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
@@ -25,6 +25,8 @@
 import java.util.List;
 import java.util.Locale;
 
+import javax.annotation.Nonnull;
+
 /**
  * This class determines that the language name on the spacebar should be displayed in what format.
  */
@@ -37,7 +39,7 @@
     private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList();
     private boolean mIsSystemLanguageSameAsInputLanguage;
 
-    public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) {
+    public int getLanguageOnSpacebarFormatType(@Nonnull final RichInputMethodSubtype subtype) {
         if (subtype.isNoLanguage()) {
             return FORMAT_TYPE_FULL_LOCALE;
         }
@@ -50,7 +52,7 @@
             return FORMAT_TYPE_MULTIPLE;
         }
         final String keyboardLanguage = locales[0].getLanguage();
-        final String keyboardLayout = SubtypeLocaleUtils.getKeyboardLayoutSetName(subtype);
+        final String keyboardLayout = subtype.getKeyboardLayoutSetName();
         int sameLanguageAndLayoutCount = 0;
         for (final InputMethodSubtype ims : mEnabledSubtypes) {
             final String language = SubtypeLocaleUtils.getSubtypeLocale(ims).getLanguage();
@@ -65,11 +67,30 @@
                 : FORMAT_TYPE_LANGUAGE_ONLY;
     }
 
-    public void updateEnabledSubtypes(final List<InputMethodSubtype> enabledSubtypes) {
+    public void onUpdateEnabledSubtypes(@Nonnull final List<InputMethodSubtype> enabledSubtypes) {
         mEnabledSubtypes = enabledSubtypes;
     }
 
-    public void updateIsSystemLanguageSameAsInputLanguage(final boolean isSame) {
-        mIsSystemLanguageSameAsInputLanguage = isSame;
+    public void onSubtypeChanged(@Nonnull final RichInputMethodSubtype subtype,
+           final boolean implicitlyEnabledSubtype, @Nonnull final Locale systemLocale) {
+        final Locale[] newLocales = subtype.getLocales();
+        if (newLocales.length > 1) {
+            // In multi-locales mode, the system language is never the same as the input language
+            // because there is no single input language.
+            mIsSystemLanguageSameAsInputLanguage = false;
+            return;
+        }
+        final Locale newLocale = newLocales[0];
+        if (systemLocale.equals(newLocale)) {
+            mIsSystemLanguageSameAsInputLanguage = true;
+            return;
+        }
+        if (!systemLocale.getLanguage().equals(newLocale.getLanguage())) {
+            mIsSystemLanguageSameAsInputLanguage = false;
+            return;
+        }
+        // If the subtype is enabled explicitly, the language name should be displayed even when
+        // the keyboard language and the system language are equal.
+        mIsSystemLanguageSameAsInputLanguage = implicitlyEnabledSubtype;
     }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index a0bb406..b1a3887 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -21,10 +21,10 @@
 
 import com.android.inputmethod.compat.CharacterCompat;
 import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.latin.common.CollectionUtils;
 import com.android.inputmethod.latin.common.Constants;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java
index 3a9aa81..8a375c6 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/NonDistinctMultitouchHelper.java
@@ -22,7 +22,7 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 public final class NonDistinctMultitouchHelper {
     private static final String TAG = NonDistinctMultitouchHelper.class.getSimpleName();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java
index ef4c74d..73a6f95 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/SlidingKeyInputDrawingPreview.java
@@ -23,7 +23,7 @@
 
 import com.android.inputmethod.keyboard.PointerTracker;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 
 /**
  * Draw rubber band preview graphics during sliding key input.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java
index 4565225..8068427 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/TimerHandler.java
@@ -22,8 +22,6 @@
 
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.keyboard.PointerTracker.DrawingProxy;
-import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
 import com.android.inputmethod.latin.common.Constants;
 import com.android.inputmethod.latin.utils.LeakGuardHandlerWrapper;
 
@@ -86,7 +84,7 @@
     }
 
     @Override
-    public void startKeyRepeatTimerOf(final PointerTracker tracker, final int repeatCount,
+    public void startKeyRepeatTimerOf(@Nonnull final PointerTracker tracker, final int repeatCount,
             final int delay) {
         final Key key = tracker.getKey();
         if (key == null || delay == 0) {
@@ -110,7 +108,7 @@
     }
 
     @Override
-    public void startLongPressTimerOf(final PointerTracker tracker, final int delay) {
+    public void startLongPressTimerOf(@Nonnull final PointerTracker tracker, final int delay) {
         final Key key = tracker.getKey();
         if (key == null) {
             return;
@@ -123,13 +121,13 @@
     }
 
     @Override
-    public void cancelLongPressTimerOf(final PointerTracker tracker) {
+    public void cancelLongPressTimersOf(@Nonnull final PointerTracker tracker) {
         removeMessages(MSG_LONGPRESS_KEY, tracker);
         removeMessages(MSG_LONGPRESS_SHIFT_KEY, tracker);
     }
 
     @Override
-    public void cancelLongPressShiftKeyTimers() {
+    public void cancelLongPressShiftKeyTimer() {
         removeMessages(MSG_LONGPRESS_SHIFT_KEY);
     }
 
@@ -139,7 +137,7 @@
     }
 
     @Override
-    public void startTypingStateTimer(final Key typedKey) {
+    public void startTypingStateTimer(@Nonnull final Key typedKey) {
         if (typedKey.isModifier() || typedKey.altCodeWhileTyping()) {
             return;
         }
@@ -190,9 +188,9 @@
     }
 
     @Override
-    public void cancelKeyTimersOf(final PointerTracker tracker) {
+    public void cancelKeyTimersOf(@Nonnull final PointerTracker tracker) {
         cancelKeyRepeatTimerOf(tracker);
-        cancelLongPressTimerOf(tracker);
+        cancelLongPressTimersOf(tracker);
     }
 
     public void cancelAllKeyTimers() {
@@ -201,7 +199,7 @@
     }
 
     @Override
-    public void startUpdateBatchInputTimer(final PointerTracker tracker) {
+    public void startUpdateBatchInputTimer(@Nonnull final PointerTracker tracker) {
         if (mGestureRecognitionUpdateTime <= 0) {
             return;
         }
@@ -211,7 +209,7 @@
     }
 
     @Override
-    public void cancelUpdateBatchInputTimer(final PointerTracker tracker) {
+    public void cancelUpdateBatchInputTimer(@Nonnull final PointerTracker tracker) {
         removeMessages(MSG_UPDATE_BATCH_INPUT, tracker);
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/TimerProxy.java b/java/src/com/android/inputmethod/keyboard/internal/TimerProxy.java
new file mode 100644
index 0000000..0ce3de8
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/TimerProxy.java
@@ -0,0 +1,133 @@
+/*
+ * 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.internal;
+
+import com.android.inputmethod.keyboard.Key;
+import com.android.inputmethod.keyboard.PointerTracker;
+
+import javax.annotation.Nonnull;
+
+public interface TimerProxy {
+    /**
+     * Start a timer to detect if a user is typing keys.
+     * @param typedKey the key that is typed.
+     */
+    public void startTypingStateTimer(@Nonnull Key typedKey);
+
+    /**
+     * Check if a user is key typing.
+     * @return true if a user is in typing.
+     */
+    public boolean isTypingState();
+
+    /**
+     * Start a timer to simulate repeated key presses while a user keep pressing a key.
+     * @param tracker the {@link PointerTracker} that points the key to be repeated.
+     * @param repeatCount the number of times that the key is repeating. Starting from 1.
+     * @param delay the interval delay to the next key repeat, in millisecond.
+     */
+    public void startKeyRepeatTimerOf(@Nonnull PointerTracker tracker, int repeatCount, int delay);
+
+    /**
+     * Start a timer to detect a long pressed key.
+     * If a key pointed by <code>tracker</code> is a shift key, start another timer to detect
+     * long pressed shift key.
+     * @param tracker the {@link PointerTracker} that starts long pressing.
+     * @param delay the delay to fire the long press timer, in millisecond.
+     */
+    public void startLongPressTimerOf(@Nonnull PointerTracker tracker, int delay);
+
+    /**
+     * Cancel timers for detecting a long pressed key and a long press shift key.
+     * @param tracker cancel long press timers of this {@link PointerTracker}.
+     */
+    public void cancelLongPressTimersOf(@Nonnull PointerTracker tracker);
+
+    /**
+     * Cancel a timer for detecting a long pressed shift key.
+     */
+    public void cancelLongPressShiftKeyTimer();
+
+    /**
+     * Cancel timers for detecting repeated key press, long pressed key, and long pressed shift key.
+     * @param tracker the {@link PointerTracker} that starts timers to be canceled.
+     */
+    public void cancelKeyTimersOf(@Nonnull PointerTracker tracker);
+
+    /**
+     * Start a timer to detect double tapped shift key.
+     */
+    public void startDoubleTapShiftKeyTimer();
+
+    /**
+     * Cancel a timer of detecting double tapped shift key.
+     */
+    public void cancelDoubleTapShiftKeyTimer();
+
+    /**
+     * Check if a timer of detecting double tapped shift key is running.
+     * @return true if detecting double tapped shift key is on going.
+     */
+    public boolean isInDoubleTapShiftKeyTimeout();
+
+    /**
+     * Start a timer to fire updating batch input while <code>tracker</code> is on hold.
+     * @param tracker the {@link PointerTracker} that stops moving.
+     */
+    public void startUpdateBatchInputTimer(@Nonnull PointerTracker tracker);
+
+    /**
+     * Cancel a timer of firing updating batch input.
+     * @param tracker the {@link PointerTracker} that resumes moving or ends gesture input.
+     */
+    public void cancelUpdateBatchInputTimer(@Nonnull PointerTracker tracker);
+
+    /**
+     * Cancel all timers of firing updating batch input.
+     */
+    public void cancelAllUpdateBatchInputTimers();
+
+    public static class Adapter implements TimerProxy {
+        @Override
+        public void startTypingStateTimer(@Nonnull Key typedKey) {}
+        @Override
+        public boolean isTypingState() { return false; }
+        @Override
+        public void startKeyRepeatTimerOf(@Nonnull PointerTracker tracker, int repeatCount,
+                int delay) {}
+        @Override
+        public void startLongPressTimerOf(@Nonnull PointerTracker tracker, int delay) {}
+        @Override
+        public void cancelLongPressTimersOf(@Nonnull PointerTracker tracker) {}
+        @Override
+        public void cancelLongPressShiftKeyTimer() {}
+        @Override
+        public void cancelKeyTimersOf(@Nonnull PointerTracker tracker) {}
+        @Override
+        public void startDoubleTapShiftKeyTimer() {}
+        @Override
+        public void cancelDoubleTapShiftKeyTimer() {}
+        @Override
+        public boolean isInDoubleTapShiftKeyTimeout() { return false; }
+        @Override
+        public void startUpdateBatchInputTimer(@Nonnull PointerTracker tracker) {}
+        @Override
+        public void cancelUpdateBatchInputTimer(@Nonnull PointerTracker tracker) {}
+        @Override
+        public void cancelAllUpdateBatchInputTimers() {}
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index fd6c24d..f8d02d6 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -16,7 +16,7 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.latin.utils.FileUtils;
+import com.android.inputmethod.latin.common.FileUtils;
 
 import java.io.File;
 
@@ -62,4 +62,9 @@
     public void deleteUnderlyingFile() {
         FileUtils.deleteRecursively(new File(mFilename));
     }
+
+    @Override
+    public String toString() {
+        return String.format("%s (offset=%d, length=%d)", mFilename, mOffset, mLength);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 8e9b5c6..46cd3b8 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -21,9 +21,10 @@
 import android.util.SparseArray;
 
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.FileUtils;
 import com.android.inputmethod.latin.common.InputPointers;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
@@ -33,7 +34,6 @@
 import com.android.inputmethod.latin.makedict.WordProperty;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
 import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
 
@@ -262,8 +262,8 @@
     }
 
     @Override
-    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+    public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
             final int sessionId, final float weightForLocale,
             final float[] inOutWeightOfLangModelVsSpatialModel) {
@@ -274,12 +274,13 @@
         Arrays.fill(session.mInputCodePoints, Constants.NOT_A_CODE);
         ngramContext.outputToArray(session.mPrevWordCodePointArrays,
                 session.mIsBeginningOfSentenceArray);
-        final InputPointers inputPointers = composer.getInputPointers();
-        final boolean isGesture = composer.isBatchMode();
+        final InputPointers inputPointers = composedData.mInputPointers;
+        final boolean isGesture = composedData.mIsBatchMode;
         final int inputSize;
         if (!isGesture) {
-            inputSize = composer.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
-                    session.mInputCodePoints);
+            inputSize =
+                    composedData.copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
+                        session.mInputCodePoints);
             if (inputSize < 0) {
                 return null;
             }
@@ -303,7 +304,7 @@
                     Dictionary.NOT_A_WEIGHT_OF_LANG_MODEL_VS_SPATIAL_MODEL;
         }
         // TOOD: Pass multiple previous words information for n-gram.
-        getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
+        getSuggestionsNative(mNativeDict, proximityInfoHandle,
                 getTraverseSession(sessionId).getSession(), inputPointers.getXCoordinates(),
                 inputPointers.getYCoordinates(), inputPointers.getTimes(),
                 inputPointers.getPointerIds(), session.mInputCodePoints, inputSize,
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 1570bda..5afb62b 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -21,11 +21,11 @@
 import android.content.res.AssetFileDescriptor;
 import android.util.Log;
 
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.io.File;
 import java.io.IOException;
@@ -284,7 +284,8 @@
                 final AssetFileAddress afa = AssetFileAddress.makeFromFileName(f.getPath());
                 if (null != afa) fileList.add(afa);
             } else {
-                Log.e(TAG, "Found a cached dictionary file but cannot read or use it");
+                Log.e(TAG, "Found a cached dictionary file for " + locale.toString()
+                        + " but cannot read or use it");
             }
         }
 
diff --git a/java/src/com/android/inputmethod/latin/DicTraverseSession.java b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
index 95390aa..aefefd3 100644
--- a/java/src/com/android/inputmethod/latin/DicTraverseSession.java
+++ b/java/src/com/android/inputmethod/latin/DicTraverseSession.java
@@ -17,7 +17,8 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.latin.common.Constants;
-import com.android.inputmethod.latin.settings.NativeSuggestOptions;
+import com.android.inputmethod.latin.common.NativeSuggestOptions;
+import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils;
 import com.android.inputmethod.latin.utils.JniUtils;
 
 import java.util.Locale;
@@ -43,7 +44,8 @@
     public final int[] mOutputAutoCommitFirstWordConfidence = new int[1];
     public final float[] mInputOutputWeightOfLangModelVsSpatialModel = new float[1];
 
-    public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
+    public final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions(
+            AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE);
 
     private static native long setDicTraverseSessionNative(String locale, long dictSize);
     private static native void initDicTraverseSessionNative(long nativeDicTraverseSession,
diff --git a/java/src/com/android/inputmethod/latin/Dictionary.java b/java/src/com/android/inputmethod/latin/Dictionary.java
index 28a62b2..7d7ed77 100644
--- a/java/src/com/android/inputmethod/latin/Dictionary.java
+++ b/java/src/com/android/inputmethod/latin/Dictionary.java
@@ -17,8 +17,8 @@
 package com.android.inputmethod.latin;
 
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
 
 import java.util.ArrayList;
@@ -87,9 +87,9 @@
 
     /**
      * Searches for suggestions for a given context.
-     * @param composer the key sequence to match with coordinate info, as a WordComposer
+     * @param composedData the key sequence to match with coordinate info
      * @param ngramContext the context for n-gram.
-     * @param proximityInfo the object for key proximity. May be ignored by some implementations.
+     * @param proximityInfoHandle the handle for key proximity. Is ignored by some implementations.
      * @param settingsValuesForSuggestion the settings values used for the suggestion.
      * @param sessionId the session id.
      * @param weightForLocale the weight given to this locale, to multiply the output scores for
@@ -99,8 +99,8 @@
      * a float array that has only one element. This can be updated when a different value is used.
      * @return the list of suggestions (possibly null if none)
      */
-    abstract public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+    abstract public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
             final int sessionId, final float weightForLocale,
             final float[] inOutWeightOfLangModelVsSpatialModel);
@@ -203,8 +203,8 @@
         }
 
         @Override
-        public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-                final NgramContext ngramContext, final ProximityInfo proximityInfo,
+        public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+                final NgramContext ngramContext, final long proximityInfoHandle,
                 final SettingsValuesForSuggestion settingsValuesForSuggestion,
                 final int sessionId, final float weightForLocale,
                 final float[] inOutWeightOfLangModelVsSpatialModel) {
diff --git a/java/src/com/android/inputmethod/latin/DictionaryCollection.java b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
index a6d7205..96575f6 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryCollection.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryCollection.java
@@ -18,8 +18,8 @@
 
 import android.util.Log;
 
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
 
 import java.util.ArrayList;
@@ -59,8 +59,8 @@
     }
 
     @Override
-    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+    public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
             final int sessionId, final float weightForLocale,
             final float[] inOutWeightOfLangModelVsSpatialModel) {
@@ -68,15 +68,15 @@
         if (dictionaries.isEmpty()) return null;
         // To avoid creating unnecessary objects, we get the list out of the first
         // dictionary and add the rest to it if not null, hence the get(0)
-        ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composer,
-                ngramContext, proximityInfo, settingsValuesForSuggestion, sessionId,
+        ArrayList<SuggestedWordInfo> suggestions = dictionaries.get(0).getSuggestions(composedData,
+                ngramContext, proximityInfoHandle, settingsValuesForSuggestion, sessionId,
                 weightForLocale, inOutWeightOfLangModelVsSpatialModel);
         if (null == suggestions) suggestions = new ArrayList<>();
         final int length = dictionaries.size();
         for (int i = 1; i < length; ++ i) {
-            final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(composer,
-                    ngramContext, proximityInfo, settingsValuesForSuggestion, sessionId,
-                    weightForLocale, inOutWeightOfLangModelVsSpatialModel);
+            final ArrayList<SuggestedWordInfo> sugg = dictionaries.get(i).getSuggestions(
+                    composedData, ngramContext, proximityInfoHandle, settingsValuesForSuggestion,
+                    sessionId, weightForLocale, inOutWeightOfLangModelVsSpatialModel);
             if (null != sugg) suggestions.addAll(sugg);
         }
         return suggestions;
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
index 4a22cde..d23639a 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFacilitator.java
@@ -23,7 +23,6 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary.UpdateEntriesForInputEventsCallback;
 import com.android.inputmethod.latin.NgramContext.WordInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
@@ -683,7 +682,7 @@
 
     // TODO: Revise the way to fusion suggestion results.
     public SuggestionResults getSuggestionResults(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId) {
         final DictionaryGroup[] dictionaryGroups = mDictionaryGroups;
         final SuggestionResults suggestionResults = new SuggestionResults(
@@ -698,8 +697,8 @@
                         ? dictionaryGroup.mWeightForGesturingInLocale
                         : dictionaryGroup.mWeightForTypingInLocale;
                 final ArrayList<SuggestedWordInfo> dictionarySuggestions =
-                        dictionary.getSuggestions(composer, ngramContext, proximityInfo,
-                                settingsValuesForSuggestion, sessionId,
+                        dictionary.getSuggestions(composer.getComposedDataSnapshot(), ngramContext,
+                                proximityInfoHandle, settingsValuesForSuggestion, sessionId,
                                 weightForLocale, weightOfLangModelVsSpatialModel);
                 if (null == dictionarySuggestions) continue;
                 suggestionResults.addAll(dictionarySuggestions);
diff --git a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
index 702d153..d9d22e0 100644
--- a/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ExpandableBinaryDictionary.java
@@ -20,9 +20,10 @@
 import android.util.Log;
 
 import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.FileUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
@@ -32,7 +33,6 @@
 import com.android.inputmethod.latin.utils.CombinedFormatUtils;
 import com.android.inputmethod.latin.utils.DistracterFilter;
 import com.android.inputmethod.latin.utils.ExecutorUtils;
-import com.android.inputmethod.latin.utils.FileUtils;
 import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
 
 import java.io.File;
@@ -120,7 +120,8 @@
     private static boolean needsToMigrateDictionary(final int formatVersion) {
         // When we bump up the dictionary format version, the old version should be added to here
         // for supporting migration. Note that native code has to support reading such formats.
-        return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING;
+        return formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING
+                || formatVersion == FormatSpec.VERSION402;
     }
 
     public boolean isValidDictionaryLocked() {
@@ -480,8 +481,8 @@
     }
 
     @Override
-    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+    public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion, final int sessionId,
             final float weightForLocale, final float[] inOutWeightOfLangModelVsSpatialModel) {
         reloadDictionaryIfRequired();
@@ -494,9 +495,9 @@
                     return null;
                 }
                 final ArrayList<SuggestedWordInfo> suggestions =
-                        mBinaryDictionary.getSuggestions(composer, ngramContext, proximityInfo,
-                                settingsValuesForSuggestion, sessionId, weightForLocale,
-                                inOutWeightOfLangModelVsSpatialModel);
+                        mBinaryDictionary.getSuggestions(composedData, ngramContext,
+                                proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+                                weightForLocale, inOutWeightOfLangModelVsSpatialModel);
                 if (mBinaryDictionary.isCorrupted()) {
                     Log.i(TAG, "Dictionary (" + mDictName +") is corrupted. "
                             + "Remove and regenerate it.");
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 3fa1270..719656b 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -77,6 +77,7 @@
 import com.android.inputmethod.latin.Suggest.OnGetSuggestedWordsCallback;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.common.InputPointers;
 import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.define.ProductionFlags;
@@ -93,7 +94,6 @@
 import com.android.inputmethod.latin.touchinputconsumer.GestureConsumer;
 import com.android.inputmethod.latin.utils.ApplicationUtils;
 import com.android.inputmethod.latin.utils.CapsModeUtils;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 import com.android.inputmethod.latin.utils.CursorAnchorInfoUtils;
 import com.android.inputmethod.latin.utils.DialogUtils;
 import com.android.inputmethod.latin.utils.ImportantNoticeUtils;
@@ -603,7 +603,7 @@
     // Has to be package-visible for unit tests
     @UsedForTesting
     void loadSettings() {
-        final Locale[] locales = mSubtypeSwitcher.getCurrentSubtypeLocales();
+        final Locale[] locales = mRichImm.getCurrentSubtypeLocales();
         final EditorInfo editorInfo = getCurrentInputEditorInfo();
         final InputAttributes inputAttributes = new InputAttributes(
                 editorInfo, isFullscreenMode(), getPackageName());
@@ -626,7 +626,7 @@
     private void refreshPersonalizationDictionarySession(
             final SettingsValues currentSettingsValues) {
         mDictionaryFacilitator.setIsMonolingualUser(
-                mSubtypeSwitcher.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes());
+                mRichImm.isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes());
         mPersonalizationDictionaryUpdater.onLoadSettings(
                 currentSettingsValues.mUsePersonalizedDicts);
         mContextualDictionaryUpdater.onLoadSettings(currentSettingsValues.mUsePersonalizedDicts);
@@ -657,7 +657,7 @@
     }
 
     void resetDictionaryFacilitatorIfNecessary() {
-        final Locale[] subtypeSwitcherLocales = mSubtypeSwitcher.getCurrentSubtypeLocales();
+        final Locale[] subtypeSwitcherLocales = mRichImm.getCurrentSubtypeLocales();
         if (mDictionaryFacilitator.isForLocales(subtypeSwitcherLocales)) {
             return;
         }
@@ -863,7 +863,7 @@
         // also wouldn't be consuming gesture data.
         mGestureConsumer = GestureConsumer.NULL_GESTURE_CONSUMER;
         mRichImm.clearSubtypeCaches();
-        mSubtypeSwitcher.refreshSubtypeInfo();
+        mSubtypeSwitcher.onSubtypeChanged(mRichImm.getCurrentRawSubtype());
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
         switcher.updateKeyboardTheme();
         final MainKeyboardView mainKeyboardView = switcher.getMainKeyboardView();
@@ -909,7 +909,7 @@
         // Update to a gesture consumer with the current editor and IME state.
         mGestureConsumer = GestureConsumer.newInstance(editorInfo,
                 mInputLogic.getPrivateCommandPerformer(),
-                Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()),
+                Arrays.asList(mRichImm.getCurrentSubtypeLocales()),
                 switcher.getKeyboard());
 
         // Forward this event to the accessibility utilities, if enabled.
@@ -947,7 +947,7 @@
             // span, so we should reset our state unconditionally, even if restarting is true.
             // We also tell the input logic about the combining rules for the current subtype, so
             // it can adjust its combiners if needed.
-            mInputLogic.startInput(mSubtypeSwitcher.getCombiningRulesExtraValueOfCurrentSubtype(),
+            mInputLogic.startInput(mRichImm.getCombiningRulesExtraValueOfCurrentSubtype(),
                     currentSettingsValues);
 
             resetDictionaryFacilitatorIfNecessary();
@@ -1077,11 +1077,12 @@
                     + ", cs=" + composingSpanStart + ", ce=" + composingSpanEnd);
         }
 
-        // This call happens when we have a hardware keyboard as well as when we don't. While we
-        // don't support hardware keyboards yet we should avoid doing the processing associated
-        // with cursor movement when we have a hardware keyboard since we are not in charge.
+        // This call happens whether our view is displayed or not, but if it's not then we should
+        // not attempt recorrection. This is true even with a hardware keyboard connected: if the
+        // view is not displayed we have no means of showing suggestions anyway, and if it is then
+        // we want to show suggestions anyway.
         final SettingsValues settingsValues = mSettings.getCurrent();
-        if ((!settingsValues.mHasHardwareKeyboard || ProductionFlags.IS_HARDWARE_KEYBOARD_SUPPORTED)
+        if (isInputViewShown()
                 && mInputLogic.onUpdateSelection(oldSelStart, oldSelEnd, newSelStart, newSelEnd,
                         settingsValues)) {
             mKeyboardSwitcher.requestUpdatingShiftState(getCurrentAutoCapsState(),
@@ -1194,7 +1195,7 @@
         if (hasHardwareKeyboard && visibleKeyboardView.getVisibility() == View.GONE) {
             // If there is a hardware keyboard and a visible software keyboard view has been hidden,
             // no visual element will be shown on the screen.
-            outInsets.touchableInsets = inputHeight;
+            outInsets.contentTopInsets = inputHeight;
             outInsets.visibleTopInsets = inputHeight;
             mInsetsUpdater.setInsets(outInsets);
             return;
@@ -1204,7 +1205,7 @@
                 ? mSuggestionStripView.getHeight() : 0;
         final int visibleTopY = inputHeight - visibleKeyboardView.getHeight() - suggestionsHeight;
         mSuggestionStripView.setMoreSuggestionsHeight(visibleTopY);
-        // Need to set touchable region only if a keyboard view is being shown.
+        // Need to set expanded touchable region only if a keyboard view is being shown.
         if (visibleKeyboardView.isShown()) {
             final int touchLeft = 0;
             final int touchTop = mKeyboardSwitcher.isShowingMoreKeysPanel() ? 0 : visibleTopY;
@@ -1423,7 +1424,7 @@
     // completely replace #onCodeInput.
     public void onEvent(@Nonnull final Event event) {
         if (Constants.CODE_SHORTCUT == event.mKeyCode) {
-            mSubtypeSwitcher.switchToShortcutIME(this);
+            mRichImm.switchToShortcutIME(this);
         }
         final InputTransaction completeInputTransaction =
                 mInputLogic.onCodeInput(mSettings.getCurrent(), event,
@@ -1467,7 +1468,7 @@
     public void onStartBatchInput() {
         mInputLogic.onStartBatchInput(mSettings.getCurrent(), mKeyboardSwitcher, mHandler);
         mGestureConsumer.onGestureStarted(
-                Arrays.asList(mSubtypeSwitcher.getCurrentSubtypeLocales()),
+                Arrays.asList(mRichImm.getCurrentSubtypeLocales()),
                 mKeyboardSwitcher.getKeyboard());
     }
 
@@ -1489,11 +1490,11 @@
     }
 
     /**
-     * To be called after the InputLogic has gotten a chance to act on the on-device decoding
-     * for the full gesture, possibly updating the TextView to reflect the first decoding.
+     * To be called after the InputLogic has gotten a chance to act on the suggested words by the
+     * IME for the full gesture, possibly updating the TextView to reflect the first suggestion.
      * <p>
      * This method must be run on the UI Thread.
-     * @param suggestedWords On-device decoding for the full gesture.
+     * @param suggestedWords suggested words by the IME for the full gesture.
      */
     public void onTailBatchInputResultShown(final SuggestedWords suggestedWords) {
         mGestureConsumer.onImeSuggestionsProcessed(suggestedWords,
@@ -1589,7 +1590,7 @@
                 // We should clear the contextual strip if there is no suggestion from dictionaries.
                 || noSuggestionsFromDictionaries) {
             mSuggestionStripView.setSuggestions(suggestedWords,
-                    mSubtypeSwitcher.getCurrentSubtype().isRtlSubtype());
+                    mRichImm.getCurrentSubtype().isRtlSubtype());
         }
     }
 
@@ -1810,7 +1811,7 @@
         public void onReceive(final Context context, final Intent intent) {
             final String action = intent.getAction();
             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
-                mSubtypeSwitcher.onNetworkStateChanged(intent);
+                mRichImm.onNetworkStateChanged(intent);
             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
                 AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
             }
diff --git a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
index bc8bd83..7b1a53a 100644
--- a/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ReadOnlyBinaryDictionary.java
@@ -16,8 +16,8 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.keyboard.ProximityInfo;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
 
 import java.util.ArrayList;
@@ -50,16 +50,16 @@
     }
 
     @Override
-    public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
-            final NgramContext ngramContext, final ProximityInfo proximityInfo,
+    public ArrayList<SuggestedWordInfo> getSuggestions(final ComposedData composedData,
+            final NgramContext ngramContext, final long proximityInfoHandle,
             final SettingsValuesForSuggestion settingsValuesForSuggestion,
             final int sessionId, final float weightForLocale,
             final float[] inOutWeightOfLangModelVsSpatialModel) {
         if (mLock.readLock().tryLock()) {
             try {
-                return mBinaryDictionary.getSuggestions(composer, ngramContext, proximityInfo,
-                        settingsValuesForSuggestion, sessionId, weightForLocale,
-                        inOutWeightOfLangModelVsSpatialModel);
+                return mBinaryDictionary.getSuggestions(composedData, ngramContext,
+                        proximityInfoHandle, settingsValuesForSuggestion, sessionId,
+                        weightForLocale, inOutWeightOfLangModelVsSpatialModel);
             } finally {
                 mLock.readLock().unlock();
             }
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index 8d8e7ac..a1ac55a 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -17,9 +17,15 @@
 package com.android.inputmethod.latin;
 
 import static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY;
 
 import android.content.Context;
+import android.content.Intent;
 import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.net.ConnectivityManager;
+import android.net.NetworkInfo;
+import android.os.AsyncTask;
 import android.os.Build;
 import android.os.IBinder;
 import android.preference.PreferenceManager;
@@ -28,7 +34,9 @@
 import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.latin.settings.AdditionalFeaturesSettingUtils;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
@@ -36,7 +44,11 @@
 
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
 
 import javax.annotation.Nonnull;
 
@@ -46,6 +58,7 @@
 // non final for easy mocking.
 public class RichInputMethodManager {
     private static final String TAG = RichInputMethodManager.class.getSimpleName();
+    private static final boolean DEBUG = false;
 
     private RichInputMethodManager() {
         // This utility class is not publicly instantiable.
@@ -56,6 +69,10 @@
     private Context mContext;
     private InputMethodManagerCompatWrapper mImmWrapper;
     private InputMethodInfoCache mInputMethodInfoCache;
+    private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
+    private InputMethodInfo mShortcutInputMethodInfo;
+    private InputMethodSubtype mShortcutSubtype;
+    private boolean mIsNetworkConnected;
     final HashMap<InputMethodInfo, List<InputMethodSubtype>>
             mSubtypeListCacheWithImplicitlySelectedSubtypes = new HashMap<>();
     final HashMap<InputMethodInfo, List<InputMethodSubtype>>
@@ -95,6 +112,11 @@
         SubtypeLocaleUtils.init(context);
         final InputMethodSubtype[] additionalSubtypes = getAdditionalSubtypes(context);
         setAdditionalInputMethodSubtypes(additionalSubtypes);
+
+        final ConnectivityManager connectivityManager =
+                (ConnectivityManager) context.getSystemService(Context.CONNECTIVITY_SERVICE);
+        final NetworkInfo info = connectivityManager.getActiveNetworkInfo();
+        mIsNetworkConnected = (info != null && info.isConnected());
     }
 
     public InputMethodSubtype[] getAdditionalSubtypes(final Context context) {
@@ -304,10 +326,47 @@
     }
 
     @Nonnull
+    public RichInputMethodSubtype onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) {
+        final RichInputMethodSubtype richSubtype = createCurrentRichInputMethodSubtype(newSubtype);
+        if (DEBUG) {
+            Log.w(TAG, "onSubtypeChanged: " + richSubtype.getNameForLogging());
+        }
+        mCurrentRichInputMethodSubtype = richSubtype;
+        return richSubtype;
+    }
+
+    private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
+
+    @UsedForTesting
+    static void forceSubtype(final InputMethodSubtype subtype) {
+        sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype);
+    }
+
+    public Locale[] getCurrentSubtypeLocales() {
+        if (null != sForcedSubtypeForTesting) {
+            return sForcedSubtypeForTesting.getLocales();
+        }
+        return getCurrentSubtype().getLocales();
+    }
+
+    public RichInputMethodSubtype getCurrentSubtype() {
+        if (null != sForcedSubtypeForTesting) {
+            return sForcedSubtypeForTesting;
+        }
+        return mCurrentRichInputMethodSubtype;
+    }
+
+
+    public String getCombiningRulesExtraValueOfCurrentSubtype() {
+        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
+    }
+
+    @Nonnull
     public InputMethodSubtype getCurrentRawSubtype() {
         return mImmWrapper.mImm.getCurrentInputMethodSubtype();
     }
 
+    @Nonnull
     public RichInputMethodSubtype createCurrentRichInputMethodSubtype(
             @Nonnull final InputMethodSubtype rawSubtype) {
         return AdditionalFeaturesSettingUtils.createRichInputMethodSubtype(this, rawSubtype,
@@ -432,4 +491,121 @@
         }
         return mImmWrapper.shouldOfferSwitchingToNextInputMethod(binder);
     }
+
+    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
+        final Locale systemLocale = mContext.getResources().getConfiguration().locale;
+        final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
+        final InputMethodManager inputMethodManager = getInputMethodManager();
+        final List<InputMethodInfo> enabledInputMethodInfoList =
+                inputMethodManager.getEnabledInputMethodList();
+        for (final InputMethodInfo info : enabledInputMethodInfoList) {
+            final List<InputMethodSubtype> enabledSubtypes =
+                    inputMethodManager.getEnabledInputMethodSubtypeList(
+                            info, true /* allowsImplicitlySelectedSubtypes */);
+            if (enabledSubtypes.isEmpty()) {
+                // An IME with no subtypes is found.
+                return false;
+            }
+            enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
+        }
+        for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
+            if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
+                    && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    // TODO: Make this private
+    void updateShortcutIME() {
+        if (DEBUG) {
+            Log.d(TAG, "Update shortcut IME from : "
+                    + (mShortcutInputMethodInfo == null
+                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+                    + (mShortcutSubtype == null ? "<null>" : (
+                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
+        }
+        // TODO: Update an icon for shortcut IME
+        final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
+                getInputMethodManager().getShortcutInputMethodsAndSubtypes();
+        mShortcutInputMethodInfo = null;
+        mShortcutSubtype = null;
+        for (final InputMethodInfo imi : shortcuts.keySet()) {
+            final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
+            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
+            // appropriate.
+            mShortcutInputMethodInfo = imi;
+            // TODO: Pick up the first found subtype for now. Should handle all subtypes
+            // as appropriate.
+            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
+            break;
+        }
+        if (DEBUG) {
+            Log.d(TAG, "Update shortcut IME to : "
+                    + (mShortcutInputMethodInfo == null
+                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
+                    + (mShortcutSubtype == null ? "<null>" : (
+                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
+        }
+    }
+
+    public void switchToShortcutIME(final InputMethodService context) {
+        if (mShortcutInputMethodInfo == null) {
+            return;
+        }
+
+        final String imiId = mShortcutInputMethodInfo.getId();
+        switchToTargetIME(imiId, mShortcutSubtype, context);
+    }
+
+    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
+            final InputMethodService context) {
+        final IBinder token = context.getWindow().getWindow().getAttributes().token;
+        if (token == null) {
+            return;
+        }
+        final InputMethodManager imm = getInputMethodManager();
+        new AsyncTask<Void, Void, Void>() {
+            @Override
+            protected Void doInBackground(Void... params) {
+                imm.setInputMethodAndSubtype(token, imiId, subtype);
+                return null;
+            }
+        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
+    }
+
+    public boolean isShortcutImeEnabled() {
+        updateShortcutIME();
+        if (mShortcutInputMethodInfo == null) {
+            return false;
+        }
+        if (mShortcutSubtype == null) {
+            return true;
+        }
+        return checkIfSubtypeBelongsToImeAndEnabled(
+                mShortcutInputMethodInfo, mShortcutSubtype);
+    }
+
+    public boolean isShortcutImeReady() {
+        updateShortcutIME();
+        if (mShortcutInputMethodInfo == null) {
+            return false;
+        }
+        if (mShortcutSubtype == null) {
+            return true;
+        }
+        if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
+            return mIsNetworkConnected;
+        }
+        return true;
+    }
+
+    public void onNetworkStateChanged(final Intent intent) {
+        final boolean noConnection = intent.getBooleanExtra(
+                ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
+        mIsNetworkConnected = !noConnection;
+
+        KeyboardSwitcher.getInstance().onNetworkStateChanged();
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
index 8d05553..ea8d4a2 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
@@ -16,33 +16,45 @@
 
 package com.android.inputmethod.latin;
 
+import static com.android.inputmethod.latin.common.Constants.Subtype.KEYBOARD_MODE;
+
+import android.util.Log;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
+import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
 import java.util.Arrays;
 import java.util.Locale;
 
+import javax.annotation.Nonnull;
+
 /**
  * Enrichment class for InputMethodSubtype to enable concurrent multi-lingual input.
  *
  * Right now, this returns the extra value of its primary subtype.
  */
 public final class RichInputMethodSubtype {
+    private static final String TAG = RichInputMethodSubtype.class.getSimpleName();
+
+    @Nonnull
     private final InputMethodSubtype mSubtype;
+    @Nonnull
     private final Locale[] mLocales;
 
-    public RichInputMethodSubtype(final InputMethodSubtype subtype, final Locale... locales) {
+    public RichInputMethodSubtype(@Nonnull final InputMethodSubtype subtype,
+            @Nonnull final Locale... locales) {
         mSubtype = subtype;
-        mLocales = new Locale[locales.length+1];
+        mLocales = new Locale[locales.length + 1];
         mLocales[0] = LocaleUtils.constructLocaleFromString(mSubtype.getLocale());
         System.arraycopy(locales, 0, mLocales, 1, locales.length);
     }
 
     // Extra values are determined by the primary subtype. This is probably right, but
     // we may have to revisit this later.
-    public String getExtraValueOf(final String key) {
+    public String getExtraValueOf(@Nonnull final String key) {
         return mSubtype.getExtraValueOf(key);
     }
 
@@ -80,6 +92,7 @@
     //  en_US azerty  T  English   English (US)
     //  zz    azerty  T  AZERTY    AZERTY
     // Get the RichInputMethodSubtype's full display name in its locale.
+    @Nonnull
     public String getFullDisplayName() {
         if (isNoLanguage()) {
             return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
@@ -88,6 +101,7 @@
     }
 
     // Get the RichInputMethodSubtype's middle display name in its locale.
+    @Nonnull
     public String getMiddleDisplayName() {
         if (isNoLanguage()) {
             return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
@@ -96,7 +110,7 @@
     }
 
     @Override
-    public boolean equals(Object o) {
+    public boolean equals(final Object o) {
         if (!(o instanceof RichInputMethodSubtype)) {
             return false;
         }
@@ -114,15 +128,96 @@
         return "Multi-lingual subtype: " + mSubtype.toString() + ", " + Arrays.toString(mLocales);
     }
 
+    @Nonnull
     public Locale[] getLocales() {
         return mLocales;
     }
 
     public boolean isRtlSubtype() {
         // The subtype is considered RTL if the language of the main subtype is RTL.
-        return SubtypeLocaleUtils.isRtlLanguage(mLocales[0]);
+        return LocaleUtils.isRtlLanguage(mLocales[0]);
     }
 
     // TODO: remove this method
+    @Nonnull
     public InputMethodSubtype getRawSubtype() { return mSubtype; }
+
+    @Nonnull
+    public String getKeyboardLayoutSetName() {
+        return SubtypeLocaleUtils.getKeyboardLayoutSetName(mSubtype);
+    }
+
+    // Dummy no language QWERTY subtype. See {@link R.xml.method}.
+    private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3;
+    private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE =
+            "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
+            + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
+            + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
+            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+    @Nonnull
+    private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
+            new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype(
+                    R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
+                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
+                    EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
+                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
+                    SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
+    // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
+    // Dummy Emoji subtype. See {@link R.xml.method}.
+    private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
+    private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE =
+            "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
+            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+    @Nonnull
+    private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
+            InputMethodSubtypeCompatUtils.newInputMethodSubtype(
+                    R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
+                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
+                    EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
+                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
+                    SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
+    private static RichInputMethodSubtype sNoLanguageSubtype;
+    private static RichInputMethodSubtype sEmojiSubtype;
+
+    @Nonnull
+    public static RichInputMethodSubtype getNoLanguageSubtype() {
+        RichInputMethodSubtype noLanguageSubtype = sNoLanguageSubtype;
+        if (noLanguageSubtype == null) {
+            final InputMethodSubtype rawNoLanguageSubtype = RichInputMethodManager.getInstance()
+                    .findSubtypeByLocaleAndKeyboardLayoutSet(
+                            SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
+            if (rawNoLanguageSubtype != null) {
+                noLanguageSubtype = new RichInputMethodSubtype(rawNoLanguageSubtype);
+            }
+        }
+        if (noLanguageSubtype != null) {
+            sNoLanguageSubtype = noLanguageSubtype;
+            return noLanguageSubtype;
+        }
+        Log.w(TAG, "Can't find any language with QWERTY subtype");
+        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
+                + DUMMY_NO_LANGUAGE_SUBTYPE);
+        return DUMMY_NO_LANGUAGE_SUBTYPE;
+    }
+
+    @Nonnull
+    public static RichInputMethodSubtype getEmojiSubtype() {
+        RichInputMethodSubtype emojiSubtype = sEmojiSubtype;
+        if (emojiSubtype == null) {
+            final InputMethodSubtype rawEmojiSubtype = RichInputMethodManager.getInstance()
+                    .findSubtypeByLocaleAndKeyboardLayoutSet(
+                            SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+            if (rawEmojiSubtype != null) {
+                emojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
+            }
+        }
+        if (emojiSubtype != null) {
+            sEmojiSubtype = emojiSubtype;
+            return emojiSubtype;
+        }
+        Log.w(TAG, "Can't find emoji subtype");
+        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
+                + DUMMY_EMOJI_SUBTYPE);
+        return DUMMY_EMOJI_SUBTYPE;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index 98bce95..23e348b 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -16,40 +16,18 @@
 
 package com.android.inputmethod.latin;
 
-import static com.android.inputmethod.latin.common.Constants.Subtype.ExtraValue.REQ_NETWORK_CONNECTIVITY;
 import android.content.Context;
-import android.content.Intent;
 import android.content.res.Resources;
-import android.inputmethodservice.InputMethodService;
-import android.net.ConnectivityManager;
-import android.net.NetworkInfo;
-import android.os.AsyncTask;
-import android.os.IBinder;
-import android.util.Log;
-import android.view.inputmethod.InputMethodInfo;
-import android.view.inputmethod.InputMethodManager;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.annotations.UsedForTesting;
-import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
-import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.internal.LanguageOnSpacebarHelper;
-import com.android.inputmethod.latin.common.Constants;
-import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
-import java.util.HashSet;
 import java.util.List;
-import java.util.Locale;
-import java.util.Map;
-import java.util.Set;
 
 import javax.annotation.Nonnull;
 
 public final class SubtypeSwitcher {
-    private static boolean DBG = DebugFlags.DEBUG_ENABLED;
-    private static final String TAG = SubtypeSwitcher.class.getSimpleName();
-
     private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
 
     private /* final */ RichInputMethodManager mRichImm;
@@ -57,41 +35,6 @@
 
     private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper =
             new LanguageOnSpacebarHelper();
-    private InputMethodInfo mShortcutInputMethodInfo;
-    private InputMethodSubtype mShortcutSubtype;
-    private RichInputMethodSubtype mCurrentRichInputMethodSubtype;
-    private RichInputMethodSubtype mNoLanguageSubtype;
-    private RichInputMethodSubtype mEmojiSubtype;
-    private boolean mIsNetworkConnected;
-
-    private static final String KEYBOARD_MODE = "keyboard";
-    // Dummy no language QWERTY subtype. See {@link R.xml.method}.
-    private static final int SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE = 0xdde0bfd3;
-    private static final String EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE =
-            "KeyboardLayoutSet=" + SubtypeLocaleUtils.QWERTY
-            + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
-            + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
-            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
-    private static final RichInputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
-            new RichInputMethodSubtype(InputMethodSubtypeCompatUtils.newInputMethodSubtype(
-                    R.string.subtype_no_language_qwerty, R.drawable.ic_ime_switcher_dark,
-                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
-                    EXTRA_VALUE_OF_DUMMY_NO_LANGUAGE_SUBTYPE,
-                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
-                    SUBTYPE_ID_OF_DUMMY_NO_LANGUAGE_SUBTYPE));
-    // Caveat: We probably should remove this when we add an Emoji subtype in {@link R.xml.method}.
-    // Dummy Emoji subtype. See {@link R.xml.method}.
-    private static final int SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE = 0xd78b2ed0;
-    private static final String EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE =
-            "KeyboardLayoutSet=" + SubtypeLocaleUtils.EMOJI
-            + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
-    private static final RichInputMethodSubtype DUMMY_EMOJI_SUBTYPE = new RichInputMethodSubtype(
-            InputMethodSubtypeCompatUtils.newInputMethodSubtype(
-                    R.string.subtype_emoji, R.drawable.ic_ime_switcher_dark,
-                    SubtypeLocaleUtils.NO_LANGUAGE, KEYBOARD_MODE,
-                    EXTRA_VALUE_OF_DUMMY_EMOJI_SUBTYPE,
-                    false /* isAuxiliary */, false /* overridesImplicitlyEnabledSubtype */,
-                    SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
 
     public static SubtypeSwitcher getInstance() {
         return sInstance;
@@ -113,18 +56,9 @@
         }
         mResources = context.getResources();
         mRichImm = RichInputMethodManager.getInstance();
-        ConnectivityManager connectivityManager = (ConnectivityManager) context.getSystemService(
-                Context.CONNECTIVITY_SERVICE);
 
-        final NetworkInfo info = connectivityManager.getActiveNetworkInfo();
-        mIsNetworkConnected = (info != null && info.isConnected());
-
-        refreshSubtypeInfo();
-        updateParametersOnStartInputView();
-    }
-
-    public void refreshSubtypeInfo() {
         onSubtypeChanged(mRichImm.getCurrentRawSubtype());
+        updateParametersOnStartInputView();
     }
 
     /**
@@ -134,219 +68,21 @@
     public void updateParametersOnStartInputView() {
         final List<InputMethodSubtype> enabledSubtypesOfThisIme =
                 mRichImm.getMyEnabledInputMethodSubtypeList(true);
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(enabledSubtypesOfThisIme);
-        updateShortcutIME();
-    }
-
-    private void updateShortcutIME() {
-        if (DBG) {
-            Log.d(TAG, "Update shortcut IME from : "
-                    + (mShortcutInputMethodInfo == null
-                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
-                    + (mShortcutSubtype == null ? "<null>" : (
-                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
-        }
-        // TODO: Update an icon for shortcut IME
-        final Map<InputMethodInfo, List<InputMethodSubtype>> shortcuts =
-                mRichImm.getInputMethodManager().getShortcutInputMethodsAndSubtypes();
-        mShortcutInputMethodInfo = null;
-        mShortcutSubtype = null;
-        for (final InputMethodInfo imi : shortcuts.keySet()) {
-            final List<InputMethodSubtype> subtypes = shortcuts.get(imi);
-            // TODO: Returns the first found IMI for now. Should handle all shortcuts as
-            // appropriate.
-            mShortcutInputMethodInfo = imi;
-            // TODO: Pick up the first found subtype for now. Should handle all subtypes
-            // as appropriate.
-            mShortcutSubtype = subtypes.size() > 0 ? subtypes.get(0) : null;
-            break;
-        }
-        if (DBG) {
-            Log.d(TAG, "Update shortcut IME to : "
-                    + (mShortcutInputMethodInfo == null
-                            ? "<null>" : mShortcutInputMethodInfo.getId()) + ", "
-                    + (mShortcutSubtype == null ? "<null>" : (
-                            mShortcutSubtype.getLocale() + ", " + mShortcutSubtype.getMode())));
-        }
+        mLanguageOnSpacebarHelper.onUpdateEnabledSubtypes(enabledSubtypesOfThisIme);
+        mRichImm.updateShortcutIME();
     }
 
     // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
     public void onSubtypeChanged(@Nonnull final InputMethodSubtype newSubtype) {
-        final RichInputMethodSubtype richSubtype =
-                mRichImm.createCurrentRichInputMethodSubtype(newSubtype);
-        if (DBG) {
-            Log.w(TAG, "onSubtypeChanged: " + richSubtype.getNameForLogging());
-        }
-        mCurrentRichInputMethodSubtype = richSubtype;
-        final Locale[] newLocales = richSubtype.getLocales();
-        if (newLocales.length > 1) {
-            // In multi-locales mode, the system language is never the same as the input language
-            // because there is no single input language.
-            mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false);
-        } else {
-            final Locale newLocale = newLocales[0];
-            final Locale systemLocale = mResources.getConfiguration().locale;
-            final boolean sameLocale = systemLocale.equals(newLocale);
-            final boolean sameLanguage = systemLocale.getLanguage().equals(newLocale.getLanguage());
-            final boolean implicitlyEnabled = mRichImm
-                    .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype);
-            mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(
-                    sameLocale || (sameLanguage && implicitlyEnabled));
-        }
-        updateShortcutIME();
+        final RichInputMethodSubtype richSubtype = mRichImm.onSubtypeChanged(newSubtype);
+        final boolean implicitlyEnabledSubtype = mRichImm
+                .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype);
+        mLanguageOnSpacebarHelper.onSubtypeChanged(
+                richSubtype, implicitlyEnabledSubtype, mResources.getConfiguration().locale);
+        mRichImm.updateShortcutIME();
     }
 
-    ////////////////////////////
-    // Shortcut IME functions //
-    ////////////////////////////
-
-    public void switchToShortcutIME(final InputMethodService context) {
-        if (mShortcutInputMethodInfo == null) {
-            return;
-        }
-
-        final String imiId = mShortcutInputMethodInfo.getId();
-        switchToTargetIME(imiId, mShortcutSubtype, context);
-    }
-
-    private void switchToTargetIME(final String imiId, final InputMethodSubtype subtype,
-            final InputMethodService context) {
-        final IBinder token = context.getWindow().getWindow().getAttributes().token;
-        if (token == null) {
-            return;
-        }
-        final InputMethodManager imm = mRichImm.getInputMethodManager();
-        new AsyncTask<Void, Void, Void>() {
-            @Override
-            protected Void doInBackground(Void... params) {
-                imm.setInputMethodAndSubtype(token, imiId, subtype);
-                return null;
-            }
-        }.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
-    }
-
-    public boolean isShortcutImeEnabled() {
-        updateShortcutIME();
-        if (mShortcutInputMethodInfo == null) {
-            return false;
-        }
-        if (mShortcutSubtype == null) {
-            return true;
-        }
-        return mRichImm.checkIfSubtypeBelongsToImeAndEnabled(
-                mShortcutInputMethodInfo, mShortcutSubtype);
-    }
-
-    public boolean isShortcutImeReady() {
-        updateShortcutIME();
-        if (mShortcutInputMethodInfo == null) {
-            return false;
-        }
-        if (mShortcutSubtype == null) {
-            return true;
-        }
-        if (mShortcutSubtype.containsExtraValueKey(REQ_NETWORK_CONNECTIVITY)) {
-            return mIsNetworkConnected;
-        }
-        return true;
-    }
-
-    public void onNetworkStateChanged(final Intent intent) {
-        final boolean noConnection = intent.getBooleanExtra(
-                ConnectivityManager.EXTRA_NO_CONNECTIVITY, false);
-        mIsNetworkConnected = !noConnection;
-
-        KeyboardSwitcher.getInstance().onNetworkStateChanged();
-    }
-
-    //////////////////////////////////
-    // Subtype Switching functions //
-    //////////////////////////////////
-
     public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) {
         return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype);
     }
-
-    public boolean isSystemLocaleSameAsLocaleOfAllEnabledSubtypesOfEnabledImes() {
-        final Locale systemLocale = mResources.getConfiguration().locale;
-        final Set<InputMethodSubtype> enabledSubtypesOfEnabledImes = new HashSet<>();
-        final InputMethodManager inputMethodManager = mRichImm.getInputMethodManager();
-        final List<InputMethodInfo> enabledInputMethodInfoList =
-                inputMethodManager.getEnabledInputMethodList();
-        for (final InputMethodInfo info : enabledInputMethodInfoList) {
-            final List<InputMethodSubtype> enabledSubtypes =
-                    inputMethodManager.getEnabledInputMethodSubtypeList(
-                            info, true /* allowsImplicitlySelectedSubtypes */);
-            if (enabledSubtypes.isEmpty()) {
-                // An IME with no subtypes is found.
-                return false;
-            }
-            enabledSubtypesOfEnabledImes.addAll(enabledSubtypes);
-        }
-        for (final InputMethodSubtype subtype : enabledSubtypesOfEnabledImes) {
-            if (!subtype.isAuxiliary() && !subtype.getLocale().isEmpty()
-                    && !systemLocale.equals(SubtypeLocaleUtils.getSubtypeLocale(subtype))) {
-                return false;
-            }
-        }
-        return true;
-    }
-
-    private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
-
-    @UsedForTesting
-    static void forceSubtype(final InputMethodSubtype subtype) {
-        sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype);
-    }
-
-    public Locale[] getCurrentSubtypeLocales() {
-        if (null != sForcedSubtypeForTesting) {
-            return sForcedSubtypeForTesting.getLocales();
-        }
-        return getCurrentSubtype().getLocales();
-    }
-
-    public RichInputMethodSubtype getCurrentSubtype() {
-        if (null != sForcedSubtypeForTesting) {
-            return sForcedSubtypeForTesting;
-        }
-        return mCurrentRichInputMethodSubtype;
-    }
-
-    public RichInputMethodSubtype getNoLanguageSubtype() {
-        if (mNoLanguageSubtype == null) {
-            mNoLanguageSubtype = new RichInputMethodSubtype(
-                    mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                            SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY));
-        }
-        if (mNoLanguageSubtype != null) {
-            return mNoLanguageSubtype;
-        }
-        Log.w(TAG, "Can't find any language with QWERTY subtype");
-        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
-                + DUMMY_NO_LANGUAGE_SUBTYPE);
-        return DUMMY_NO_LANGUAGE_SUBTYPE;
-    }
-
-    public RichInputMethodSubtype getEmojiSubtype() {
-        if (mEmojiSubtype == null) {
-            final InputMethodSubtype rawEmojiSubtype =
-                    mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                        SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
-            if (null != rawEmojiSubtype) {
-                mEmojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
-            }
-        }
-        if (mEmojiSubtype != null) {
-            return mEmojiSubtype;
-        }
-        Log.w(TAG, "Can't find emoji subtype");
-        Log.w(TAG, "No input method subtype found; returning dummy subtype: "
-                + DUMMY_EMOJI_SUBTYPE);
-        return DUMMY_EMOJI_SUBTYPE;
-    }
-
-    public String getCombiningRulesExtraValueOfCurrentSubtype() {
-        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 430f765..ee8d3f8 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -140,8 +140,8 @@
                 : typedWord;
 
         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
-                wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion,
-                SESSION_ID_TYPING);
+                wordComposer, ngramContext, proximityInfo.getNativeProximityInfo(),
+                settingsValuesForSuggestion, SESSION_ID_TYPING);
         final ArrayList<SuggestedWordInfo> suggestionsContainer =
                 getTransformedSuggestedWordInfoList(wordComposer, suggestionResults,
                         trailingSingleQuotesCount,
@@ -230,7 +230,7 @@
             inputStyle = inputStyleIfNotPrediction;
         }
         callback.onGetSuggestedWords(new SuggestedWords(suggestionsList,
-                suggestionResults.mRawSuggestions,
+                suggestionResults.mRawSuggestions, typedWord,
                 // TODO: this first argument is lying. If this is a whitelisted word which is an
                 // actual word, it says typedWordValid = false, which looks wrong. We should either
                 // rename the attribute or change the value.
@@ -247,8 +247,8 @@
             final int inputStyle, final int sequenceNumber,
             final OnGetSuggestedWordsCallback callback) {
         final SuggestionResults suggestionResults = mDictionaryFacilitator.getSuggestionResults(
-                wordComposer, ngramContext, proximityInfo, settingsValuesForSuggestion,
-                SESSION_ID_GESTURE);
+                wordComposer, ngramContext, proximityInfo.getNativeProximityInfo(),
+                settingsValuesForSuggestion, SESSION_ID_GESTURE);
         // For transforming words that don't come from a dictionary, because it's our best bet
         final Locale defaultLocale = mDictionaryFacilitator.getMostProbableLocale();
         final ArrayList<SuggestedWordInfo> suggestionsContainer =
@@ -286,8 +286,12 @@
         // (typedWordValid=true), not as an "auto correct word" (willAutoCorrect=false).
         // Note that because this method is never used to get predictions, there is no need to
         // modify inputType such in getSuggestedWordsForNonBatchInput.
+        final String pseudoTypedWord = suggestionsContainer.isEmpty() ? null
+                : suggestionsContainer.get(0).mWord;
+
         callback.onGetSuggestedWords(new SuggestedWords(suggestionsContainer,
                 suggestionResults.mRawSuggestions,
+                pseudoTypedWord,
                 true /* typedWordValid */,
                 false /* willAutoCorrect */,
                 false /* isObsoleteSuggestions */,
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index c51e20f..bddeac4 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -73,21 +73,11 @@
             final boolean willAutoCorrect,
             final boolean isObsoleteSuggestions,
             final int inputStyle) {
-        this(suggestedWordInfoList, rawSuggestions, typedWordValid, willAutoCorrect,
-                isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER);
-    }
-
-    public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
-            final ArrayList<SuggestedWordInfo> rawSuggestions,
-            final boolean typedWordValid,
-            final boolean willAutoCorrect,
-            final boolean isObsoleteSuggestions,
-            final int inputStyle,
-            final int sequenceNumber) {
         this(suggestedWordInfoList, rawSuggestions,
                 (suggestedWordInfoList.isEmpty() || isPrediction(inputStyle)) ? null
                         : suggestedWordInfoList.get(INDEX_OF_TYPED_WORD).mWord,
-                typedWordValid, willAutoCorrect, isObsoleteSuggestions, inputStyle, sequenceNumber);
+                typedWordValid, willAutoCorrect,
+                isObsoleteSuggestions, inputStyle, NOT_A_SEQUENCE_NUMBER);
     }
 
     public SuggestedWords(final ArrayList<SuggestedWordInfo> suggestedWordInfoList,
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 0b77f2c..78860d8 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -19,11 +19,12 @@
 import com.android.inputmethod.event.CombinerChain;
 import com.android.inputmethod.event.Event;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.ComposedData;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.common.InputPointers;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.define.DebugFlags;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -90,6 +91,10 @@
         refreshTypedWordCache();
     }
 
+    public ComposedData getComposedDataSnapshot() {
+        return new ComposedData(getInputPointers(), isBatchMode(), mTypedWordCache.toString());
+    }
+
     /**
      * Restart the combiners, possibly with a new spec.
      * @param combiningSpec The spec string for combining. This is found in the extra value.
@@ -134,38 +139,6 @@
         return mCodePointSize;
     }
 
-    /**
-     * Copy the code points in the typed word to a destination array of ints.
-     *
-     * If the array is too small to hold the code points in the typed word, nothing is copied and
-     * -1 is returned.
-     *
-     * @param destination the array of ints.
-     * @return the number of copied code points.
-     */
-    public int copyCodePointsExceptTrailingSingleQuotesAndReturnCodePointCount(
-            final int[] destination) {
-        // This method can be called on a separate thread and mTypedWordCache can change while we
-        // are executing this method.
-        final String typedWord = mTypedWordCache.toString();
-        // lastIndex is exclusive
-        final int lastIndex = typedWord.length()
-                - StringUtils.getTrailingSingleQuotesCount(typedWord);
-        if (lastIndex <= 0) {
-            // The string is empty or contains only single quotes.
-            return 0;
-        }
-
-        // The following function counts the number of code points in the text range which begins
-        // at index 0 and extends to the character at lastIndex.
-        final int codePointSize = Character.codePointCount(typedWord, 0, lastIndex);
-        if (codePointSize > destination.length) {
-            return -1;
-        }
-        return StringUtils.copyCodePointsAndReturnCodePointCount(destination, typedWord, 0,
-                lastIndex, true /* downCase */);
-    }
-
     public boolean isSingleLetter() {
         return size() == 1;
     }
diff --git a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
index d4be0e3..78bfd2b 100644
--- a/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
+++ b/java/src/com/android/inputmethod/latin/debug/ExternalDictionaryGetterForDebug.java
@@ -26,10 +26,10 @@
 import com.android.inputmethod.latin.BinaryDictionaryFileDumper;
 import com.android.inputmethod.latin.BinaryDictionaryGetter;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.utils.DialogUtils;
 import com.android.inputmethod.latin.utils.DictionaryInfoUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
diff --git a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
index 78d79ae..eba9654 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FormatSpec.java
@@ -171,14 +171,18 @@
     // ExpandableDictionary.matchesExpectedBinaryDictFormatVersionForThisType().
     public static final int VERSION2 = 2;
     public static final int VERSION201 = 201;
+    public static final int VERSION202 = 202;
     public static final int MINIMUM_SUPPORTED_VERSION_OF_CODE_POINT_TABLE = VERSION201;
     // Dictionary version used for testing.
     public static final int VERSION4_ONLY_FOR_TESTING = 399;
-    public static final int VERSION401 = 401;
-    public static final int VERSION4 = 402;
-    public static final int VERSION4_DEV = 403;
-    static final int MINIMUM_SUPPORTED_VERSION = VERSION2;
-    static final int MAXIMUM_SUPPORTED_VERSION = VERSION4_DEV;
+    public static final int VERSION402 = 402;
+    public static final int VERSION403 = 403;
+    public static final int VERSION4 = VERSION403;
+    public static final int VERSION4_DEV = VERSION403;
+    public static final int MINIMUM_SUPPORTED_STATIC_VERSION = VERSION202;
+    public static final int MAXIMUM_SUPPORTED_STATIC_VERSION = VERSION202;
+    static final int MINIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION4;
+    static final int MAXIMUM_SUPPORTED_DYNAMIC_VERSION = VERSION4_DEV;
 
     // TODO: Make this value adaptative to content data, store it in the header, and
     // use it in the reading code.
diff --git a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
index 331f85e..8c5eb0a 100644
--- a/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
+++ b/java/src/com/android/inputmethod/latin/personalization/PersonalizationHelper.java
@@ -19,7 +19,7 @@
 import android.content.Context;
 import android.util.Log;
 
-import com.android.inputmethod.latin.utils.FileUtils;
+import com.android.inputmethod.latin.common.FileUtils;
 
 import java.io.File;
 import java.io.FilenameFilter;
@@ -28,32 +28,45 @@
 import java.util.concurrent.ConcurrentHashMap;
 import java.util.concurrent.TimeUnit;
 
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
+
+/**
+ * Helps handle and manage personalized dictionaries such as {@link UserHistoryDictionary} and
+ * {@link PersonalizationDictionary}.
+ */
 public class PersonalizationHelper {
     private static final String TAG = PersonalizationHelper.class.getSimpleName();
     private static final boolean DEBUG = false;
+
     private static final ConcurrentHashMap<String, SoftReference<UserHistoryDictionary>>
             sLangUserHistoryDictCache = new ConcurrentHashMap<>();
     private static final ConcurrentHashMap<String, SoftReference<PersonalizationDictionary>>
             sLangPersonalizationDictCache = new ConcurrentHashMap<>();
 
+    @Nonnull
     public static UserHistoryDictionary getUserHistoryDictionary(
-            final Context context, final Locale locale) {
-        final String localeStr = locale.toString();
+            final Context context, final Locale locale, @Nullable final String accountName) {
+        String lookupStr = locale.toString();
+        if (accountName != null) {
+            lookupStr += "." + accountName;
+        }
         synchronized (sLangUserHistoryDictCache) {
-            if (sLangUserHistoryDictCache.containsKey(localeStr)) {
+            if (sLangUserHistoryDictCache.containsKey(lookupStr)) {
                 final SoftReference<UserHistoryDictionary> ref =
-                        sLangUserHistoryDictCache.get(localeStr);
+                        sLangUserHistoryDictCache.get(lookupStr);
                 final UserHistoryDictionary dict = ref == null ? null : ref.get();
                 if (dict != null) {
                     if (DEBUG) {
-                        Log.w(TAG, "Use cached UserHistoryDictionary for " + locale);
+                        Log.d(TAG, "Use cached UserHistoryDictionary for " + locale +
+                                " & account" + accountName);
                     }
                     dict.reloadDictionaryIfRequired();
                     return dict;
                 }
             }
             final UserHistoryDictionary dict = new UserHistoryDictionary(context, locale);
-            sLangUserHistoryDictCache.put(localeStr, new SoftReference<>(dict));
+            sLangUserHistoryDictCache.put(lookupStr, new SoftReference<>(dict));
             return dict;
         }
     }
diff --git a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
index 58782c6..946835c 100644
--- a/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/personalization/UserHistoryDictionary.java
@@ -17,30 +17,73 @@
 package com.android.inputmethod.latin.personalization;
 
 import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 
 import com.android.inputmethod.annotations.ExternallyReferenced;
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.NgramContext;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.define.ProductionFlags;
+import com.android.inputmethod.latin.settings.LocalSettingsConstants;
 import com.android.inputmethod.latin.utils.DistracterFilter;
 
 import java.io.File;
 import java.util.Locale;
 
 import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Locally gathers stats about the words user types and various other signals like auto-correction
  * cancellation or manual picks. This allows the keyboard to adapt to the typist over time.
  */
 public class UserHistoryDictionary extends DecayingExpandableBinaryDictionaryBase {
-    /* package */ static final String NAME = UserHistoryDictionary.class.getSimpleName();
+    static final String NAME = UserHistoryDictionary.class.getSimpleName();
 
     // TODO: Make this constructor private
-    /* package */ UserHistoryDictionary(final Context context, final Locale locale) {
-        super(context, getDictName(NAME, locale, null /* dictFile */), locale,
-                Dictionary.TYPE_USER_HISTORY, null /* dictFile */);
+    UserHistoryDictionary(final Context context, final Locale locale) {
+        super(context,
+                getUserHistoryDictName(
+                        NAME,
+                        locale,
+                        null /* dictFile */,
+                        context),
+                locale,
+                Dictionary.TYPE_USER_HISTORY,
+                null /* dictFile */);
+    }
+
+    /**
+     * @returns the name of the {@link UserHistoryDictionary}.
+     */
+    @UsedForTesting
+    static String getUserHistoryDictName(final String name, final Locale locale,
+            @Nullable final File dictFile, final Context context) {
+        if (!ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
+            return getDictName(name, locale, dictFile);
+        }
+        return getUserHistoryDictNamePerAccount(name, locale, dictFile, context);
+    }
+
+    /**
+     * Uses the currently signed in account to determine the dictionary name.
+     */
+    private static String getUserHistoryDictNamePerAccount(final String name, final Locale locale,
+            @Nullable final File dictFile, final Context context) {
+        if (dictFile != null) {
+            return dictFile.getName();
+        }
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
+        final String account = prefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME,
+                null /* default */);
+        String dictName = name + "." + locale.toString();
+        if (account != null) {
+            dictName += "." + account;
+        }
+        return dictName;
     }
 
     // Note: This method is called by {@link DictionaryFacilitator} using Java reflection.
@@ -48,7 +91,14 @@
     @ExternallyReferenced
     public static UserHistoryDictionary getDictionary(final Context context, final Locale locale,
             final File dictFile, final String dictNamePrefix) {
-        return PersonalizationHelper.getUserHistoryDictionary(context, locale);
+        final String account;
+        if (ProductionFlags.ENABLE_PER_ACCOUNT_USER_HISTORY_DICTIONARY) {
+            account = PreferenceManager.getDefaultSharedPreferences(context)
+                    .getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null /* default */);
+        } else {
+            account = null;
+        }
+        return PersonalizationHelper.getUserHistoryDictionary(context, locale, account);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java b/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java
index 01398f4..b749aa5 100644
--- a/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java
+++ b/java/src/com/android/inputmethod/latin/settings/CustomInputStylePreference.java
@@ -346,8 +346,10 @@
             super(context, android.R.layout.simple_spinner_item);
             setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
 
+            final String[] predefinedKeyboardLayoutSet = context.getResources().getStringArray(
+                    R.array.predefined_layouts);
             // TODO: Should filter out already existing combinations of locale and layout.
-            for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) {
+            for (final String layout : predefinedKeyboardLayoutSet) {
                 // This is a dummy subtype with NO_LANGUAGE, only for display.
                 final InputMethodSubtype subtype =
                         AdditionalSubtypeUtils.createDummyAdditionalSubtype(
diff --git a/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java
index 49db2bd..c0ceb88 100644
--- a/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/settings/PreferencesSettingsFragment.java
@@ -24,7 +24,7 @@
 
 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.SubtypeSwitcher;
+import com.android.inputmethod.latin.RichInputMethodManager;
 
 /**
  * "Preferences" settings sub screen.
@@ -49,7 +49,7 @@
         // When we are called from the Settings application but we are not already running, some
         // singleton and utility classes may not have been initialized.  We have to call
         // initialization method of these classes here. See {@link LatinIME#onCreate()}.
-        SubtypeSwitcher.init(context);
+        RichInputMethodManager.init(context);
 
         final boolean showVoiceKeyOption = res.getBoolean(
                 R.bool.config_enable_show_voice_key_option);
@@ -71,7 +71,7 @@
         super.onResume();
         final Preference voiceInputKeyOption = findPreference(Settings.PREF_VOICE_INPUT_KEY);
         if (voiceInputKeyOption != null) {
-            final boolean isShortcutImeEnabled = SubtypeSwitcher.getInstance()
+            final boolean isShortcutImeEnabled = RichInputMethodManager.getInstance()
                     .isShortcutImeEnabled();
             voiceInputKeyOption.setEnabled(isShortcutImeEnabled);
             voiceInputKeyOption.setSummary(
diff --git a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
index 509b41f..26415e7 100644
--- a/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/settings/SettingsValues.java
@@ -28,7 +28,6 @@
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.utils.AsyncResultHolder;
 import com.android.inputmethod.latin.utils.ResourceUtils;
 import com.android.inputmethod.latin.utils.TargetPackageInfoGetterTask;
@@ -140,7 +139,7 @@
                 DebugSettings.PREF_SLIDING_KEY_INPUT_PREVIEW, true);
         mShowsVoiceInputKey = needsToShowVoiceInputKey(prefs, res)
                 && mInputAttributes.mShouldShowVoiceInputKey
-                && SubtypeSwitcher.getInstance().isShortcutImeEnabled();
+                && RichInputMethodManager.getInstance().isShortcutImeEnabled();
         final String autoCorrectionThresholdRawValue = prefs.getString(
                 Settings.PREF_AUTO_CORRECTION_THRESHOLD,
                 res.getString(R.string.auto_correction_threshold_mode_index_modest));
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 315e369..bcf7bbf 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -168,7 +168,8 @@
             DictionaryFacilitator dictionaryFacilitatorForLocale =
                     mDictionaryFacilitatorCache.get(locale);
             return dictionaryFacilitatorForLocale.getSuggestionResults(composer, ngramContext,
-                    proximityInfo, mSettingsValuesForSuggestion, sessionId);
+                    proximityInfo.getNativeProximityInfo(), mSettingsValuesForSuggestion,
+                    sessionId);
         } finally {
             if (sessionId != null) {
                 mSessionIdPool.add(sessionId);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 3ad8fb9..c90e8a3 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -34,10 +34,10 @@
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.WordComposer;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.ScriptUtils;
 import com.android.inputmethod.latin.utils.SuggestionResults;
 
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
index 27a0f62..7991a24 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripLayoutHelper.java
@@ -50,10 +50,10 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.settings.Settings;
 import com.android.inputmethod.latin.settings.SettingsValues;
 import com.android.inputmethod.latin.utils.ResourceUtils;
-import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 import com.android.inputmethod.latin.utils.ViewLayoutUtils;
 
 import java.util.ArrayList;
@@ -570,8 +570,7 @@
             final boolean isRtlLanguage = (ViewCompat.getLayoutDirection(addToDictionaryStrip)
                     == ViewCompat.LAYOUT_DIRECTION_RTL);
             final String arrow = isRtlLanguage ? RIGHTWARDS_ARROW : LEFTWARDS_ARROW;
-            final boolean isRtlSystem = SubtypeLocaleUtils.isRtlLanguage(
-                    res.getConfiguration().locale);
+            final boolean isRtlSystem = LocaleUtils.isRtlLanguage(res.getConfiguration().locale);
             final CharSequence hint = res.getText(R.string.hint_add_to_dictionary);
             hintText = (isRtlLanguage == isRtlSystem) ? (arrow + hint) : (hint + arrow);
             hintWidth = width - wordWidth;
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
index eda8194..22fc35a 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordContents.java
@@ -28,7 +28,7 @@
 
 import com.android.inputmethod.compat.UserDictionaryCompatUtils;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 
 import java.util.ArrayList;
 import java.util.Locale;
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
index 90e4faa..b9ed353 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryList.java
@@ -31,7 +31,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 
 import java.util.List;
 import java.util.Locale;
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java
index e58727e..c0a946e 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettingsUtils.java
@@ -17,7 +17,7 @@
 package com.android.inputmethod.latin.userdictionary;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 
 import android.content.Context;
 import android.text.TextUtils;
diff --git a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
index 0db63fd..0dbc7c8 100644
--- a/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CapsModeUtils.java
@@ -24,6 +24,7 @@
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
 
+import java.util.ArrayList;
 import java.util.Locale;
 
 public final class CapsModeUtils {
@@ -326,4 +327,31 @@
         // Here we arrived at the start of the line. This should behave exactly like whitespace.
         return (START == state || LETTER == state) ? noCaps : caps;
     }
+
+    /**
+     * Convert capitalize mode flags into human readable text.
+     *
+     * @param capsFlags The modes flags to be converted. It may be any combination of
+     * {@link TextUtils#CAP_MODE_CHARACTERS}, {@link TextUtils#CAP_MODE_WORDS}, and
+     * {@link TextUtils#CAP_MODE_SENTENCES}.
+     * @return the text that describe the <code>capsMode</code>.
+     */
+    public static String flagsToString(final int capsFlags) {
+        final int capsFlagsMask = TextUtils.CAP_MODE_CHARACTERS | TextUtils.CAP_MODE_WORDS
+                | TextUtils.CAP_MODE_SENTENCES;
+        if ((capsFlags & ~capsFlagsMask) != 0) {
+            return "unknown<0x" + Integer.toHexString(capsFlags) + ">";
+        }
+        final ArrayList<String> builder = new ArrayList<>();
+        if ((capsFlags & android.text.TextUtils.CAP_MODE_CHARACTERS) != 0) {
+            builder.add("characters");
+        }
+        if ((capsFlags & android.text.TextUtils.CAP_MODE_WORDS) != 0) {
+            builder.add("words");
+        }
+        if ((capsFlags & android.text.TextUtils.CAP_MODE_SENTENCES) != 0) {
+            builder.add("sentences");
+        }
+        return builder.isEmpty() ? "none" : TextUtils.join("|", builder);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
index 4e0f5f5..8699f2c 100644
--- a/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CombinedFormatUtils.java
@@ -36,7 +36,8 @@
     public static final String WORD_TAG = "word";
     public static final String BEGINNING_OF_SENTENCE_TAG = "beginning_of_sentence";
     public static final String NOT_A_WORD_TAG = "not_a_word";
-    public static final String BLACKLISTED_TAG = "blacklisted";
+    public static final String POSSIBLY_OFFENSIVE_TAG = "possibly_offensive";
+    public static final String TRUE_VALUE = "true";
 
     public static String formatAttributeMap(final HashMap<String, String> attributeMap) {
         final StringBuilder builder = new StringBuilder();
@@ -61,13 +62,13 @@
         builder.append(",");
         builder.append(formatProbabilityInfo(wordProperty.mProbabilityInfo));
         if (wordProperty.mIsBeginningOfSentence) {
-            builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=true");
+            builder.append("," + BEGINNING_OF_SENTENCE_TAG + "=" + TRUE_VALUE);
         }
         if (wordProperty.mIsNotAWord) {
-            builder.append("," + NOT_A_WORD_TAG + "=true");
+            builder.append("," + NOT_A_WORD_TAG + "=" + TRUE_VALUE);
         }
         if (wordProperty.mIsPossiblyOffensive) {
-            builder.append("," + BLACKLISTED_TAG + "=true");
+            builder.append("," + POSSIBLY_OFFENSIVE_TAG + "=" + TRUE_VALUE);
         }
         builder.append("\n");
         if (wordProperty.mHasShortcuts) {
@@ -111,4 +112,8 @@
         }
         return builder.toString();
     }
+
+    public static boolean isLiteralTrue(final String value) {
+        return TRUE_VALUE.equalsIgnoreCase(value);
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
index 24025b2..81c3e3c 100644
--- a/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/DictionaryInfoUtils.java
@@ -28,6 +28,7 @@
 import com.android.inputmethod.latin.BinaryDictionaryGetter;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
index 56c8249..9c6a948 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
@@ -250,8 +250,9 @@
         composer.setComposingWord(codePoints, coordinates);
         final SuggestionResults suggestionResults;
         synchronized (mLock) {
-            suggestionResults = dictionaryFacilitator.getSuggestionResults(
-                    composer, NgramContext.EMPTY_PREV_WORDS_INFO, keyboard.getProximityInfo(),
+            suggestionResults = dictionaryFacilitator.getSuggestionResults(composer,
+                    NgramContext.EMPTY_PREV_WORDS_INFO,
+                    keyboard.getProximityInfo().getNativeProximityInfo(),
                     settingsValuesForSuggestion, 0 /* sessionId */);
         }
         if (suggestionResults.isEmpty()) {
diff --git a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
deleted file mode 100644
index c519a0d..0000000
--- a/java/src/com/android/inputmethod/latin/utils/LocaleUtils.java
+++ /dev/null
@@ -1,190 +0,0 @@
-/*
- * Copyright (C) 2011 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 android.text.TextUtils;
-
-import java.util.HashMap;
-import java.util.Locale;
-
-/**
- * A class to help with handling Locales in string form.
- *
- * This file has the same meaning and features (and shares all of its code) with
- * the one in the dictionary pack. They need to be kept synchronized; for any
- * update/bugfix to this file, consider also updating/fixing the version in the
- * dictionary pack.
- */
-public final class LocaleUtils {
-    private LocaleUtils() {
-        // Intentional empty constructor for utility class.
-    }
-
-    // Locale match level constants.
-    // A higher level of match is guaranteed to have a higher numerical value.
-    // Some room is left within constants to add match cases that may arise necessary
-    // in the future, for example differentiating between the case where the countries
-    // are both present and different, and the case where one of the locales does not
-    // specify the countries. This difference is not needed now.
-
-    // Nothing matches.
-    public static final int LOCALE_NO_MATCH = 0;
-    // The languages matches, but the country are different. Or, the reference locale requires a
-    // country and the tested locale does not have one.
-    public static final int LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER = 3;
-    // The languages and country match, but the variants are different. Or, the reference locale
-    // requires a variant and the tested locale does not have one.
-    public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER = 6;
-    // The required locale is null or empty so it will accept anything, and the tested locale
-    // is non-null and non-empty.
-    public static final int LOCALE_ANY_MATCH = 10;
-    // The language matches, and the tested locale specifies a country but the reference locale
-    // does not require one.
-    public static final int LOCALE_LANGUAGE_MATCH = 15;
-    // The language and the country match, and the tested locale specifies a variant but the
-    // reference locale does not require one.
-    public static final int LOCALE_LANGUAGE_AND_COUNTRY_MATCH = 20;
-    // The compared locales are fully identical. This is the best match level.
-    public static final int LOCALE_FULL_MATCH = 30;
-
-    // The level at which a match is "normally" considered a locale match with standard algorithms.
-    // Don't use this directly, use #isMatch to test.
-    private static final int LOCALE_MATCH = LOCALE_ANY_MATCH;
-
-    // Make this match the maximum match level. If this evolves to have more than 2 digits
-    // when written in base 10, also adjust the getMatchLevelSortedString method.
-    private static final int MATCH_LEVEL_MAX = 30;
-
-    /**
-     * Return how well a tested locale matches a reference locale.
-     *
-     * This will check the tested locale against the reference locale and return a measure of how
-     * a well it matches the reference. The general idea is that the tested locale has to match
-     * every specified part of the required locale. A full match occur when they are equal, a
-     * partial match when the tested locale agrees with the reference locale but is more specific,
-     * and a difference when the tested locale does not comply with all requirements from the
-     * reference locale.
-     * In more detail, if the reference locale specifies at least a language and the testedLocale
-     * does not specify one, or specifies a different one, LOCALE_NO_MATCH is returned. If the
-     * reference locale is empty or null, it will match anything - in the form of LOCALE_FULL_MATCH
-     * if the tested locale is empty or null, and LOCALE_ANY_MATCH otherwise. If the reference and
-     * tested locale agree on the language, but not on the country,
-     * LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER is returned if the reference locale specifies a country,
-     * and LOCALE_LANGUAGE_MATCH otherwise.
-     * If they agree on both the language and the country, but not on the variant,
-     * LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER is returned if the reference locale
-     * specifies a variant, and LOCALE_LANGUAGE_AND_COUNTRY_MATCH otherwise. If everything matches,
-     * LOCALE_FULL_MATCH is returned.
-     * Examples:
-     * en <=> en_US  => LOCALE_LANGUAGE_MATCH
-     * en_US <=> en => LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER
-     * en_US_POSIX <=> en_US_Android  =>  LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER
-     * en_US <=> en_US_Android => LOCALE_LANGUAGE_AND_COUNTRY_MATCH
-     * sp_US <=> en_US  =>  LOCALE_NO_MATCH
-     * de <=> de  => LOCALE_FULL_MATCH
-     * en_US <=> en_US => LOCALE_FULL_MATCH
-     * "" <=> en_US => LOCALE_ANY_MATCH
-     *
-     * @param referenceLocale the reference locale to test against.
-     * @param testedLocale the locale to test.
-     * @return a constant that measures how well the tested locale matches the reference locale.
-     */
-    public static int getMatchLevel(String referenceLocale, String testedLocale) {
-        if (TextUtils.isEmpty(referenceLocale)) {
-            return TextUtils.isEmpty(testedLocale) ? LOCALE_FULL_MATCH : LOCALE_ANY_MATCH;
-        }
-        if (null == testedLocale) return LOCALE_NO_MATCH;
-        String[] referenceParams = referenceLocale.split("_", 3);
-        String[] testedParams = testedLocale.split("_", 3);
-        // By spec of String#split, [0] cannot be null and length cannot be 0.
-        if (!referenceParams[0].equals(testedParams[0])) return LOCALE_NO_MATCH;
-        switch (referenceParams.length) {
-        case 1:
-            return 1 == testedParams.length ? LOCALE_FULL_MATCH : LOCALE_LANGUAGE_MATCH;
-        case 2:
-            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
-            if (!referenceParams[1].equals(testedParams[1]))
-                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
-            if (3 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH;
-            return LOCALE_FULL_MATCH;
-        case 3:
-            if (1 == testedParams.length) return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
-            if (!referenceParams[1].equals(testedParams[1]))
-                return LOCALE_LANGUAGE_MATCH_COUNTRY_DIFFER;
-            if (2 == testedParams.length) return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
-            if (!referenceParams[2].equals(testedParams[2]))
-                return LOCALE_LANGUAGE_AND_COUNTRY_MATCH_VARIANT_DIFFER;
-            return LOCALE_FULL_MATCH;
-        }
-        // It should be impossible to come here
-        return LOCALE_NO_MATCH;
-    }
-
-    /**
-     * Return a string that represents this match level, with better matches first.
-     *
-     * The strings are sorted in lexicographic order: a better match will always be less than
-     * a worse match when compared together.
-     */
-    public static String getMatchLevelSortedString(int matchLevel) {
-        // This works because the match levels are 0~99 (actually 0~30)
-        // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
-        return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
-    }
-
-    /**
-     * Find out whether a match level should be considered a match.
-     *
-     * This method takes a match level as returned by the #getMatchLevel method, and returns whether
-     * it should be considered a match in the usual sense with standard Locale functions.
-     *
-     * @param level the match level, as returned by getMatchLevel.
-     * @return whether this is a match or not.
-     */
-    public static boolean isMatch(int level) {
-        return LOCALE_MATCH <= level;
-    }
-
-    private static final HashMap<String, Locale> sLocaleCache = new HashMap<>();
-
-    /**
-     * Creates a locale from a string specification.
-     */
-    public static Locale constructLocaleFromString(final String localeStr) {
-        if (localeStr == null) {
-            return null;
-        }
-        synchronized (sLocaleCache) {
-            Locale retval = sLocaleCache.get(localeStr);
-            if (retval != null) {
-                return retval;
-            }
-            String[] localeParams = localeStr.split("_", 3);
-            if (localeParams.length == 1) {
-                retval = new Locale(localeParams[0]);
-            } else if (localeParams.length == 2) {
-                retval = new Locale(localeParams[0], localeParams[1]);
-            } else if (localeParams.length == 3) {
-                retval = new Locale(localeParams[0], localeParams[1], localeParams[2]);
-            }
-            if (retval != null) {
-                sLocaleCache.put(localeStr, retval);
-            }
-            return retval;
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
index 21daddc..a381649 100644
--- a/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
+++ b/java/src/com/android/inputmethod/latin/utils/RecapitalizeStatus.java
@@ -51,6 +51,17 @@
         }
     }
 
+    public static String modeToString(final int recapitalizeMode) {
+        switch (recapitalizeMode) {
+        case NOT_A_RECAPITALIZE_MODE: return "undefined";
+        case CAPS_MODE_ORIGINAL_MIXED_CASE: return "mixedCase";
+        case CAPS_MODE_ALL_LOWER: return "allLower";
+        case CAPS_MODE_FIRST_WORD_UPPER: return "firstWordUpper";
+        case CAPS_MODE_ALL_UPPER: return "allUpper";
+        default: return "unknown<" + recapitalizeMode + ">";
+        }
+    }
+
     /**
      * We store the location of the cursor and the string that was there before the recapitalize
      * action was done, and the location of the cursor and the string that was there after.
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 55c1dc9..013f024 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -27,13 +27,14 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.RichInputMethodSubtype;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.common.StringUtils;
 
-import java.util.Arrays;
 import java.util.HashMap;
 import java.util.Locale;
 
+import javax.annotation.Nonnull;
+
 /**
  * A helper class to deal with subtype locales.
   */
@@ -53,7 +54,6 @@
     private static volatile boolean sInitialized = false;
     private static final Object sInitializeLock = new Object();
     private static Resources sResources;
-    private static String[] sPredefinedKeyboardLayoutSet;
     // Keyboard layout to its display name map.
     private static final HashMap<String, String> sKeyboardLayoutToDisplayNameMap = new HashMap<>();
     // Keyboard layout to subtype name resource id map.
@@ -100,7 +100,6 @@
         sResources = res;
 
         final String[] predefinedLayoutSet = res.getStringArray(R.array.predefined_layouts);
-        sPredefinedKeyboardLayoutSet = predefinedLayoutSet;
         final String[] layoutDisplayNames = res.getStringArray(
                 R.array.predefined_layout_display_names);
         for (int i = 0; i < predefinedLayoutSet.length; i++) {
@@ -149,10 +148,6 @@
         }
     }
 
-    public static String[] getPredefinedKeyboardLayoutSet() {
-        return sPredefinedKeyboardLayoutSet;
-    }
-
     public static boolean isExceptionalLocale(final String localeString) {
         return sExceptionalLocaleToNameIdsMap.containsKey(localeString);
     }
@@ -173,7 +168,8 @@
         return nameId == null ? UNKNOWN_KEYBOARD_LAYOUT : nameId;
     }
 
-    public static Locale getDisplayLocaleOfSubtypeLocale(final String localeString) {
+    @Nonnull
+    public static Locale getDisplayLocaleOfSubtypeLocale(@Nonnull final String localeString) {
         if (NO_LANGUAGE.equals(localeString)) {
             return sResources.getConfiguration().locale;
         }
@@ -183,17 +179,20 @@
         return LocaleUtils.constructLocaleFromString(localeString);
     }
 
-    public static String getSubtypeLocaleDisplayNameInSystemLocale(final String localeString) {
+    public static String getSubtypeLocaleDisplayNameInSystemLocale(
+            @Nonnull final String localeString) {
         final Locale displayLocale = sResources.getConfiguration().locale;
         return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
     }
 
-    public static String getSubtypeLocaleDisplayName(final String localeString) {
+    @Nonnull
+    public static String getSubtypeLocaleDisplayName(@Nonnull final String localeString) {
         final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
         return getSubtypeLocaleDisplayNameInternal(localeString, displayLocale);
     }
 
-    public static String getSubtypeLanguageDisplayName(final String localeString) {
+    @Nonnull
+    public static String getSubtypeLanguageDisplayName(@Nonnull final String localeString) {
         final Locale displayLocale = getDisplayLocaleOfSubtypeLocale(localeString);
         final String languageString;
         if (sExceptionalLocaleDisplayedInRootLocale.containsKey(localeString)) {
@@ -205,8 +204,9 @@
         return getSubtypeLocaleDisplayNameInternal(languageString, displayLocale);
     }
 
-    private static String getSubtypeLocaleDisplayNameInternal(final String localeString,
-            final Locale displayLocale) {
+    @Nonnull
+    private static String getSubtypeLocaleDisplayNameInternal(@Nonnull final String localeString,
+            @Nonnull final Locale displayLocale) {
         if (NO_LANGUAGE.equals(localeString)) {
             // No language subtype should be displayed in system locale.
             return sResources.getString(R.string.subtype_no_language);
@@ -255,8 +255,9 @@
     //  en_US azerty  T  English (US) (AZERTY)   exception
     //  zz    azerty  T  Alphabet (AZERTY)       in system locale
 
-    private static String getReplacementString(final InputMethodSubtype subtype,
-            final Locale displayLocale) {
+    @Nonnull
+    private static String getReplacementString(@Nonnull final InputMethodSubtype subtype,
+            @Nonnull final Locale displayLocale) {
         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
                 && subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME)) {
             return subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME);
@@ -264,20 +265,24 @@
         return getSubtypeLocaleDisplayNameInternal(subtype.getLocale(), displayLocale);
     }
 
-    public static String getSubtypeDisplayNameInSystemLocale(final InputMethodSubtype subtype) {
+    @Nonnull
+    public static String getSubtypeDisplayNameInSystemLocale(
+            @Nonnull final InputMethodSubtype subtype) {
         final Locale displayLocale = sResources.getConfiguration().locale;
         return getSubtypeDisplayNameInternal(subtype, displayLocale);
     }
 
-    public static String getSubtypeNameForLogging(final InputMethodSubtype subtype) {
+    @Nonnull
+    public static String getSubtypeNameForLogging(@Nonnull final InputMethodSubtype subtype) {
         if (subtype == null) {
             return "<null subtype>";
         }
         return getSubtypeLocale(subtype) + "/" + getKeyboardLayoutSetName(subtype);
     }
 
-    private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
-            final Locale displayLocale) {
+    @Nonnull
+    private static String getSubtypeDisplayNameInternal(@Nonnull final InputMethodSubtype subtype,
+            @Nonnull final Locale displayLocale) {
         final String replacementString = getReplacementString(subtype, displayLocale);
         // TODO: rework this for multi-lingual subtypes
         final int nameResId = subtype.getNameResId();
@@ -302,24 +307,25 @@
                 getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
     }
 
-    public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
+    @Nonnull
+    public static Locale getSubtypeLocale(@Nonnull final InputMethodSubtype subtype) {
         final String localeString = subtype.getLocale();
         return LocaleUtils.constructLocaleFromString(localeString);
     }
 
-    public static String getKeyboardLayoutSetDisplayName(final InputMethodSubtype subtype) {
+    @Nonnull
+    public static String getKeyboardLayoutSetDisplayName(
+            @Nonnull final InputMethodSubtype subtype) {
         final String layoutName = getKeyboardLayoutSetName(subtype);
         return getKeyboardLayoutSetDisplayName(layoutName);
     }
 
-    public static String getKeyboardLayoutSetDisplayName(final String layoutName) {
+    @Nonnull
+    public static String getKeyboardLayoutSetDisplayName(@Nonnull final String layoutName) {
         return sKeyboardLayoutToDisplayNameMap.get(layoutName);
     }
 
-    public static String getKeyboardLayoutSetName(final RichInputMethodSubtype subtype) {
-        return getKeyboardLayoutSetName(subtype.getRawSubtype());
-    }
-
+    @Nonnull
     public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) {
         String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
         if (keyboardLayoutSet == null) {
@@ -339,22 +345,6 @@
         return keyboardLayoutSet;
     }
 
-    // TODO: Get this information from the framework instead of maintaining here by ourselves.
-    // Sorted list of known Right-To-Left language codes.
-    private static final String[] SORTED_RTL_LANGUAGES = {
-        "ar", // Arabic
-        "fa", // Persian
-        "iw", // Hebrew
-    };
-    static {
-        Arrays.sort(SORTED_RTL_LANGUAGES);
-    }
-
-    public static boolean isRtlLanguage(final Locale locale) {
-        final String language = locale.getLanguage();
-        return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0;
-    }
-
     public static String getCombiningRulesExtraValue(final InputMethodSubtype subtype) {
         return subtype.getExtraValueOf(COMBINING_RULES);
     }
diff --git a/native/dicttoolkit/Android.mk b/native/dicttoolkit/Android.mk
new file mode 100644
index 0000000..118682d
--- /dev/null
+++ b/native/dicttoolkit/Android.mk
@@ -0,0 +1,67 @@
+# 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.
+
+ifeq (,$(TARGET_BUILD_APPS))
+
+# Only build if it's explicitly requested, or running mm/mmm.
+ifneq ($(ONE_SHOT_MAKEFILE)$(filter $(MAKECMDGOALS),dicttoolkit),)
+
+# HACK: Temporarily disable host tool build on Mac until the build system is ready for C++11.
+LATINIME_HOST_OSNAME := $(shell uname -s)
+ifneq ($(LATINIME_HOST_OSNAME), Darwin) # TODO: Remove this
+
+LOCAL_PATH := $(call my-dir)
+
+include $(CLEAR_VARS)
+
+LATIN_IME_CORE_PATH := $(LOCAL_PATH)/../jni
+
+LATIN_IME_DICT_TOOLKIT_SRC_DIR := src
+LATIN_IME_CORE_SRC_DIR := ../jni/src
+
+LOCAL_CFLAGS += -Werror -Wall -Wextra -Weffc++ -Wformat=2 -Wcast-qual -Wcast-align \
+    -Wwrite-strings -Wfloat-equal -Wpointer-arith -Winit-self -Wredundant-decls \
+    -Woverloaded-virtual -Wsign-promo -Wno-system-headers
+
+# To suppress compiler warnings for unused variables/functions used for debug features etc.
+LOCAL_CFLAGS += -Wno-unused-parameter -Wno-unused-function
+LOCAL_CFLAGS += -std=c++11 -Wno-unused-parameter -Wno-unused-function
+
+include $(LOCAL_PATH)/NativeFileList.mk
+include $(LATIN_IME_CORE_PATH)/NativeFileList.mk
+
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_DICT_TOOLKIT_SRC_DIR) \
+    $(LATIN_IME_CORE_PATH)/$(LATIN_IME_CORE_SRC_DIR)
+
+LOCAL_SRC_FILES := $(LATIN_IME_DICT_TOOLKIT_MAIN_SRC_FILES) \
+    $(addprefix $(LATIN_IME_DICT_TOOLKIT_SRC_DIR)/, $(LATIN_IME_DICT_TOOLKIT_SRC_FILES)) \
+    $(addprefix $(LATIN_IME_CORE_SRC_DIR)/, $(LATIN_IME_CORE_SRC_FILES))
+
+LOCAL_MODULE := dicttoolkit
+LOCAL_MODULE_TAGS := optional
+
+LOCAL_CLANG := true
+LOCAL_CXX_STL := libc++
+
+include $(BUILD_HOST_EXECUTABLE)
+#################### Clean up the tmp vars
+include $(LOCAL_PATH)/CleanupNativeFileList.mk
+#################### Unit test
+include $(LOCAL_PATH)/UnitTests.mk
+
+endif # Darwin - TODO: Remove this
+
+endif
+
+endif # TARGET_BUILD_APPS
diff --git a/native/dicttoolkit/CleanupNativeFileList.mk b/native/dicttoolkit/CleanupNativeFileList.mk
new file mode 100644
index 0000000..b804b41
--- /dev/null
+++ b/native/dicttoolkit/CleanupNativeFileList.mk
@@ -0,0 +1,17 @@
+# 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.
+
+LATIN_IME_DICT_TOOLKIT_MAIN_SRC_FILES :=
+LATIN_IME_DICT_TOOLKIT_SRC_FILES :=
+LATIN_IME_DICT_TOOLKIT_TEST_FILES :=
diff --git a/native/dicttoolkit/NativeFileList.mk b/native/dicttoolkit/NativeFileList.mk
new file mode 100644
index 0000000..d2c8c3a
--- /dev/null
+++ b/native/dicttoolkit/NativeFileList.mk
@@ -0,0 +1,43 @@
+# 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.
+
+LATIN_IME_DICT_TOOLKIT_MAIN_SRC_FILES := \
+    dict_toolkit_main.cpp
+
+LATIN_IME_DICT_TOOLKIT_SRC_FILES := \
+    $(addprefix command_executors/, \
+        diff_executor.cpp \
+        header_executor.cpp \
+        help_executor.cpp \
+        info_executor.cpp \
+        makedict_executor.cpp) \
+    $(addprefix offdevice_intermediate_dict/, \
+        offdevice_intermediate_dict.cpp) \
+    $(addprefix utils/, \
+        arguments_parser.cpp \
+        command_utils.cpp \
+        utf8_utils.cpp)
+
+LATIN_IME_DICT_TOOLKIT_TEST_FILES := \
+    $(addprefix command_executors/, \
+        diff_executor_test.cpp \
+        header_executor_test.cpp \
+        info_executor_test.cpp \
+        makedict_executor_test.cpp) \
+    dict_toolkit_defines_test.cpp \
+    $(addprefix offdevice_intermediate_dict/, \
+        offdevice_intermediate_dict_test.cpp) \
+    $(addprefix utils/, \
+        command_utils_test.cpp \
+        utf8_utils_test.cpp)
diff --git a/native/dicttoolkit/UnitTests.mk b/native/dicttoolkit/UnitTests.mk
new file mode 100644
index 0000000..96e2873
--- /dev/null
+++ b/native/dicttoolkit/UnitTests.mk
@@ -0,0 +1,69 @@
+# 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.
+
+ifeq (,$(TARGET_BUILD_APPS))
+
+LOCAL_PATH := $(call my-dir)
+
+######################################
+include $(CLEAR_VARS)
+
+LATIN_IME_CORE_PATH := $(LOCAL_PATH)/../jni
+
+LATIN_IME_DICT_TOOLKIT_SRC_DIR := src
+LATIN_IME_CORE_SRC_DIR := ../jni/src
+LATIN_DICT_TOOLKIT_TEST_SRC_DIR := tests
+
+include $(LOCAL_PATH)/NativeFileList.mk
+include $(LATIN_IME_CORE_PATH)/NativeFileList.mk
+
+# TODO: Remove -std=c++11 once it is set by default on host build.
+LATIN_IME_SRC_DIR := src
+LOCAL_ADDRESS_SANITIZER := true
+LOCAL_CFLAGS += -std=c++11 -Wno-unused-parameter -Wno-unused-function
+LOCAL_CLANG := true
+LOCAL_CXX_STL := libc++
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_DICT_TOOLKIT_SRC_DIR) \
+    $(LATIN_IME_CORE_PATH)/$(LATIN_IME_CORE_SRC_DIR)
+LOCAL_MODULE := liblatinime_dicttoolkit_host_static_for_unittests
+LOCAL_MODULE_TAGS := optional
+LOCAL_SRC_FILES := \
+    $(addprefix $(LATIN_IME_DICT_TOOLKIT_SRC_DIR)/, $(LATIN_IME_DICT_TOOLKIT_SRC_FILES)) \
+    $(addprefix $(LATIN_IME_CORE_SRC_DIR)/, $(LATIN_IME_CORE_SRC_FILES))
+include $(BUILD_HOST_STATIC_LIBRARY)
+
+include $(CLEAR_VARS)
+
+# TODO: Remove -std=c++11 once it is set by default on host build.
+LOCAL_ADDRESS_SANITIZER := true
+LOCAL_CFLAGS += -std=c++11 -Wno-unused-parameter -Wno-unused-function
+LOCAL_CLANG := true
+LOCAL_CXX_STL := libc++
+LOCAL_C_INCLUDES += $(LOCAL_PATH)/$(LATIN_IME_DICT_TOOLKIT_SRC_DIR) \
+    $(LATIN_IME_CORE_PATH)/$(LATIN_IME_CORE_SRC_DIR)
+LOCAL_MODULE := dicttoolkit_unittests
+LOCAL_MODULE_TAGS := tests
+LOCAL_SRC_FILES := \
+    $(addprefix $(LATIN_DICT_TOOLKIT_TEST_SRC_DIR)/, $(LATIN_IME_DICT_TOOLKIT_TEST_FILES))
+LOCAL_STATIC_LIBRARIES += liblatinime_dicttoolkit_host_static_for_unittests
+include $(BUILD_HOST_NATIVE_TEST)
+
+include $(LOCAL_PATH)/CleanupNativeFileList.mk
+
+#################### Clean up the tmp vars
+LATINIME_HOST_OSNAME :=
+LATIN_IME_SRC_DIR :=
+LATIN_IME_TEST_SRC_DIR :=
+
+endif # TARGET_BUILD_APPS
diff --git a/native/dicttoolkit/dict_toolkit_main.cpp b/native/dicttoolkit/dict_toolkit_main.cpp
new file mode 100644
index 0000000..53cc5e9
--- /dev/null
+++ b/native/dicttoolkit/dict_toolkit_main.cpp
@@ -0,0 +1,39 @@
+/*
+ * 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.
+ */
+
+#include <cstdio>
+
+#include "dict_toolkit_defines.h"
+#include "utils/command_utils.h"
+
+void usage(int argc, char **argv) {
+    fprintf(stderr, "Usage: %s <command> [arguments]\n", argc > 0 ? argv[0] : "dicttoolkit");
+}
+
+int main(int argc, char **argv) {
+    if (argc < MIN_ARG_COUNT) {
+        usage(argc, argv);
+        return 1;
+    }
+    using namespace latinime::dicttoolkit;
+    const CommandType commandType = CommandUtils::getCommandType(argv[1]);
+    if (commandType == CommandType::Unknown) {
+        CommandUtils::printCommandUnknownMessage(argv[0], argv[1]);
+        return 1;
+    }
+    const auto executor = CommandUtils::getCommandExecutor(commandType);
+    return executor(argc - 1, argv + 1);
+}
diff --git a/native/dicttoolkit/run_tests.sh b/native/dicttoolkit/run_tests.sh
new file mode 100755
index 0000000..44c99c1
--- /dev/null
+++ b/native/dicttoolkit/run_tests.sh
@@ -0,0 +1,34 @@
+#!/bin/bash
+# Copyright 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.
+
+# check script arguments
+if [[ $(type -t mmm) != function ]]; then
+if [[ ${BASH_SOURCE[0]} != $0 ]]; then return; else exit 1; fi
+fi
+
+# Host build is never supported in unbundled (NDK/tapas) build
+if [[ -n $TARGET_BUILD_APPS ]]; then
+  echo "Host build is never supported in tapas build."  1>&2
+  echo "Use lunch command instead."  1>&2
+  if [[ ${BASH_SOURCE[0]} != $0 ]]; then return; else exit 1; fi
+fi
+
+test_name=dicttoolkit_unittests
+
+pushd $PWD > /dev/null
+cd $(gettop)
+(mmm -j16 packages/inputmethods/LatinIME/native/dicttoolkit) || (make -j16 $test_name)
+$ANDROID_HOST_OUT/bin/$test_name
+popd > /dev/null
diff --git a/native/dicttoolkit/src/command_executors/diff_executor.cpp b/native/dicttoolkit/src/command_executors/diff_executor.cpp
new file mode 100644
index 0000000..bf68306
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/diff_executor.cpp
@@ -0,0 +1,49 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/diff_executor.h"
+
+#include <cstdio>
+
+namespace latinime {
+namespace dicttoolkit {
+
+const char *const DiffExecutor::COMMAND_NAME = "diff";
+
+/* static */ int DiffExecutor::run(const int argc, char **argv) {
+    fprintf(stderr, "Command '%s' has not been implemented yet.\n", COMMAND_NAME);
+    return 0;
+}
+
+/* static */ void DiffExecutor::printUsage() {
+    printf("*** %s\n", COMMAND_NAME);
+    getArgumentsParser().printUsage(COMMAND_NAME, "Shows differences between two dictionaries.");
+}
+
+/* static */ const ArgumentsParser DiffExecutor::getArgumentsParser() {
+    std::unordered_map<std::string, OptionSpec> optionSpecs;
+    optionSpecs["p"] = OptionSpec::switchOption("(plumbing) produce output suitable for a script");
+
+    const std::vector<ArgumentSpec> argumentSpecs = {
+        ArgumentSpec::singleArgument("dict1", "dictionary file"),
+        ArgumentSpec::singleArgument("dict2", "dictionary file")
+    };
+
+    return ArgumentsParser(std::move(optionSpecs), std::move(argumentSpecs));
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/command_executors/diff_executor.h b/native/dicttoolkit/src/command_executors/diff_executor.h
new file mode 100644
index 0000000..f92ae49
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/diff_executor.h
@@ -0,0 +1,40 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_DIFF_EXECUTOR_H
+#define LATINIME_DICT_TOOLKIT_DIFF_EXECUTOR_H
+
+#include "dict_toolkit_defines.h"
+#include "utils/arguments_parser.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class DiffExecutor final {
+ public:
+    static const char *const COMMAND_NAME;
+
+    static int run(const int argc, char **argv);
+    static void printUsage();
+    static const ArgumentsParser getArgumentsParser();
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(DiffExecutor);
+};
+
+} // namespace dicttoolkit
+} // namepsace latinime
+#endif // LATINIME_DICT_TOOLKIT_DIFF_EXECUTOR_H
diff --git a/native/dicttoolkit/src/command_executors/header_executor.cpp b/native/dicttoolkit/src/command_executors/header_executor.cpp
new file mode 100644
index 0000000..b3d273b
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/header_executor.cpp
@@ -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.
+ */
+
+#include "command_executors/header_executor.h"
+
+#include <cstdio>
+
+namespace latinime {
+namespace dicttoolkit {
+
+const char *const HeaderExecutor::COMMAND_NAME = "header";
+
+/* static */ int HeaderExecutor::run(const int argc, char **argv) {
+    fprintf(stderr, "Command '%s' has not been implemented yet.\n", COMMAND_NAME);
+    return 0;
+}
+
+/* static */ void HeaderExecutor::printUsage() {
+    printf("*** %s\n", COMMAND_NAME);
+    getArgumentsParser().printUsage(COMMAND_NAME,
+            "Prints the header contents of a dictionary file.");
+}
+
+/* static */ const ArgumentsParser HeaderExecutor::getArgumentsParser() {
+    std::unordered_map<std::string, OptionSpec> optionSpecs;
+    optionSpecs["p"] = OptionSpec::switchOption("(plumbing) produce output suitable for a script");
+
+    const std::vector<ArgumentSpec> argumentSpecs = {
+        ArgumentSpec::singleArgument("dict", "prints the header contents of a dictionary file")
+    };
+
+    return ArgumentsParser(std::move(optionSpecs), std::move(argumentSpecs));
+}
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/command_executors/header_executor.h b/native/dicttoolkit/src/command_executors/header_executor.h
new file mode 100644
index 0000000..44cc9cf
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/header_executor.h
@@ -0,0 +1,40 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_HEADER_EXECUTOR_H
+#define LATINIME_DICT_TOOLKIT_HEADER_EXECUTOR_H
+
+#include "dict_toolkit_defines.h"
+#include "utils/arguments_parser.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class HeaderExecutor final {
+ public:
+    static const char *const COMMAND_NAME;
+
+    static int run(const int argc, char **argv);
+    static void printUsage();
+    static const ArgumentsParser getArgumentsParser();
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(HeaderExecutor);
+};
+
+} // namespace dicttoolkit
+} // namepsace latinime
+#endif // LATINIME_DICT_TOOLKIT_HEADER_EXECUTOR_H
diff --git a/native/dicttoolkit/src/command_executors/help_executor.cpp b/native/dicttoolkit/src/command_executors/help_executor.cpp
new file mode 100644
index 0000000..bd29a5b
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/help_executor.cpp
@@ -0,0 +1,52 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/help_executor.h"
+
+#include <cstdio>
+#include <functional>
+#include <vector>
+
+#include "command_executors/diff_executor.h"
+#include "command_executors/header_executor.h"
+#include "command_executors/info_executor.h"
+#include "command_executors/makedict_executor.h"
+#include "utils/command_utils.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+const char *const HelpExecutor::COMMAND_NAME = "help";
+
+/* static */ int HelpExecutor::run(const int argc, char **argv) {
+    printf("Available commands:\n\n");
+    const std::vector<std::function<void(void)>> printUsageMethods = {DiffExecutor::printUsage,
+            HeaderExecutor::printUsage, InfoExecutor::printUsage, MakedictExecutor::printUsage,
+            printUsage};
+    for (const auto &printUsageMethod : printUsageMethods) {
+        printUsageMethod();
+    }
+    return 0;
+}
+
+/* static */ void HelpExecutor::printUsage() {
+    printf("*** %s\n", COMMAND_NAME);
+    printf("Usage: %s\n", COMMAND_NAME);
+    printf("Show this help list.\n\n");
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/command_executors/help_executor.h b/native/dicttoolkit/src/command_executors/help_executor.h
new file mode 100644
index 0000000..280610e
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/help_executor.h
@@ -0,0 +1,38 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_HELP_EXECUTOR_H
+#define LATINIME_DICT_TOOLKIT_HELP_EXECUTOR_H
+
+#include "dict_toolkit_defines.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class HelpExecutor final {
+ public:
+    static const char *const COMMAND_NAME;
+
+    static int run(const int argc, char **argv);
+    static void printUsage();
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(HelpExecutor);
+};
+
+} // namespace dicttoolkit
+} // namepsace latinime
+#endif // LATINIME_DICT_TOOLKIT_HELP_EXECUTOR_H
diff --git a/native/dicttoolkit/src/command_executors/info_executor.cpp b/native/dicttoolkit/src/command_executors/info_executor.cpp
new file mode 100644
index 0000000..351da4a
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/info_executor.cpp
@@ -0,0 +1,54 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/info_executor.h"
+
+#include <cstdio>
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+namespace latinime {
+namespace dicttoolkit {
+
+const char *const InfoExecutor::COMMAND_NAME = "info";
+
+/* static */ int InfoExecutor::run(const int argc, char **argv) {
+    fprintf(stderr, "Command '%s' has not been implemented yet.\n", COMMAND_NAME);
+    return 0;
+}
+
+/* static */ void InfoExecutor::printUsage() {
+    printf("*** %s\n", COMMAND_NAME);
+    getArgumentsParser().printUsage(COMMAND_NAME,
+            "Prints various information about a dictionary file.");
+}
+
+/* static */const ArgumentsParser InfoExecutor::getArgumentsParser() {
+    std::unordered_map<std::string, OptionSpec> optionSpecs;
+    optionSpecs["p"] = OptionSpec::switchOption("(plumbing) produce output suitable for a script");
+
+    const std::vector<ArgumentSpec> argumentSpecs = {
+        ArgumentSpec::singleArgument("dict", "dictionary file name"),
+        ArgumentSpec::variableLengthArguments("word", 0 /* minCount */,
+                ArgumentSpec::UNLIMITED_COUNT, "word to show information")
+    };
+
+    return ArgumentsParser(std::move(optionSpecs), std::move(argumentSpecs));
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/command_executors/info_executor.h b/native/dicttoolkit/src/command_executors/info_executor.h
new file mode 100644
index 0000000..d4106d5
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/info_executor.h
@@ -0,0 +1,40 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_INFO_EXECUTOR_H
+#define LATINIME_DICT_TOOLKIT_INFO_EXECUTOR_H
+
+#include "dict_toolkit_defines.h"
+#include "utils/arguments_parser.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class InfoExecutor final {
+ public:
+    static const char *const COMMAND_NAME;
+
+    static int run(const int argc, char **argv);
+    static void printUsage();
+    static const ArgumentsParser getArgumentsParser();
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(InfoExecutor);
+};
+
+} // namepsace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_INFO_EXECUTOR_H
diff --git a/native/dicttoolkit/src/command_executors/makedict_executor.cpp b/native/dicttoolkit/src/command_executors/makedict_executor.cpp
new file mode 100644
index 0000000..8a84e80
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/makedict_executor.cpp
@@ -0,0 +1,55 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/makedict_executor.h"
+
+#include <cstdio>
+
+namespace latinime {
+namespace dicttoolkit {
+
+const char *const MakedictExecutor::COMMAND_NAME = "makedict";
+
+/* static */ int MakedictExecutor::run(const int argc, char **argv) {
+    fprintf(stderr, "Command '%s' has not been implemented yet.\n", COMMAND_NAME);
+    return 0;
+}
+
+/* static */ void MakedictExecutor::printUsage() {
+    printf("*** %s\n", COMMAND_NAME);
+    getArgumentsParser().printUsage(COMMAND_NAME,
+            "Converts a source dictionary file to one or several outputs.\n"
+            "Source can be a binary dictionary file or a combined format file.\n"
+            "Binary version 2 (Jelly Bean), 4, and combined format outputs are supported.");
+}
+
+/* static */const ArgumentsParser MakedictExecutor::getArgumentsParser() {
+    std::unordered_map<std::string, OptionSpec> optionSpecs;
+    optionSpecs["o"] = OptionSpec::keyValueOption("format", "2",
+            "output format version: 2/4/combined");
+    optionSpecs["t"] = OptionSpec::keyValueOption("mode", "off",
+            "code point table switch: on/off/auto");
+
+    const std::vector<ArgumentSpec> argumentSpecs = {
+        ArgumentSpec::singleArgument("src_dict", "source dictionary file"),
+        ArgumentSpec::singleArgument("dest_dict", "output dictionary file")
+    };
+
+    return ArgumentsParser(std::move(optionSpecs), std::move(argumentSpecs));
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/command_executors/makedict_executor.h b/native/dicttoolkit/src/command_executors/makedict_executor.h
new file mode 100644
index 0000000..c3de977
--- /dev/null
+++ b/native/dicttoolkit/src/command_executors/makedict_executor.h
@@ -0,0 +1,40 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_MAKEDICT_EXECUTOR_H
+#define LATINIME_DICT_TOOLKIT_MAKEDICT_EXECUTOR_H
+
+#include "dict_toolkit_defines.h"
+#include "utils/arguments_parser.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class MakedictExecutor final {
+ public:
+    static const char *const COMMAND_NAME;
+
+    static int run(const int argc, char **argv);
+    static void printUsage();
+    static const ArgumentsParser getArgumentsParser();
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(MakedictExecutor);
+};
+
+} // namespace dicttoolkit
+} // namepsace latinime
+#endif // LATINIME_DICT_TOOLKIT_MAKEDICT_EXECUTOR_H
diff --git a/native/dicttoolkit/src/dict_toolkit_defines.h b/native/dicttoolkit/src/dict_toolkit_defines.h
new file mode 100644
index 0000000..dbaae0c
--- /dev/null
+++ b/native/dicttoolkit/src/dict_toolkit_defines.h
@@ -0,0 +1,24 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_DEFINES_H
+#define LATINIME_DICT_TOOLKIT_DEFINES_H
+
+#include "defines.h"
+
+#define MIN_ARG_COUNT 2
+
+#endif // LATINIME_DICT_TOOLKIT_DEFINES_H
diff --git a/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.cpp b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.cpp
new file mode 100644
index 0000000..af28131
--- /dev/null
+++ b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.cpp
@@ -0,0 +1,126 @@
+/*
+ * 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.
+ */
+
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict.h"
+
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+bool OffdeviceIntermediateDict::addWord(const WordProperty &wordProperty) {
+    const CodePointArrayView codePoints = wordProperty.getCodePoints();
+    if (codePoints.empty() || codePoints.size() > MAX_WORD_LENGTH) {
+        return false;
+    }
+    return addWordInner(codePoints, wordProperty, mRootPtNodeArray);
+}
+
+bool OffdeviceIntermediateDict::addWordInner(const CodePointArrayView codePoints,
+        const WordProperty &wordProperty, OffdeviceIntermediateDictPtNodeArray &ptNodeArray) {
+    auto ptNodeList = ptNodeArray.getMutablePtNodeList();
+    auto ptNodeIt = ptNodeList->begin();
+    for (; ptNodeIt != ptNodeList->end(); ++ptNodeIt) {
+        const auto &ptNode = *ptNodeIt;
+        const CodePointArrayView ptNodeCodePoints = ptNode->getPtNodeCodePoints();
+        if (codePoints[0] < ptNodeCodePoints[0]) {
+            continue;
+        }
+        if (codePoints[0] > ptNodeCodePoints[0]) {
+            break;
+        }
+        size_t i = 1;
+        for (; i < codePoints.size(); ++i) {
+            if (i >= ptNodeCodePoints.size()) {
+                // Add new child.
+                return addWordInner(codePoints.skip(i), wordProperty,
+                        ptNode->getChildrenPtNodeArray());
+            }
+            if (codePoints[i] != ptNodeCodePoints[i]) {
+                break;
+            }
+        }
+        if (codePoints.size() == i && codePoints.size() == ptNodeCodePoints.size()) {
+            // All code points matched.
+            if (ptNode->getWordProperty()) {
+                //  Adding the same word multiple times is not supported.
+                return false;
+            }
+            ptNodeList->insert(ptNodeIt,
+                    std::make_shared<OffdeviceIntermediateDictPtNode>(wordProperty, *ptNode));
+            ptNodeList->erase(ptNodeIt);
+            return true;
+        }
+        // The (i+1)-th elements are different.
+        // Create and Add new parent ptNode for the common part.
+        auto newPtNode = codePoints.size() == i
+                ? std::make_shared<OffdeviceIntermediateDictPtNode>(codePoints, wordProperty)
+                : std::make_shared<OffdeviceIntermediateDictPtNode>(codePoints.limit(i));
+        ptNodeList->insert(ptNodeIt, newPtNode);
+        OffdeviceIntermediateDictPtNodeArray &childrenPtNodeArray =
+                newPtNode->getChildrenPtNodeArray();
+        // Add new child for the existing ptNode.
+        childrenPtNodeArray.getMutablePtNodeList()->push_back(
+                std::make_shared<OffdeviceIntermediateDictPtNode>(
+                        ptNodeCodePoints.skip(i), *ptNode));
+        ptNodeList->erase(ptNodeIt);
+        if (codePoints.size() != i) {
+            // Add a child for the new word.
+            return addWordInner(codePoints.skip(i), wordProperty, childrenPtNodeArray);
+        }
+        return true;
+    }
+    ptNodeList->insert(ptNodeIt,
+            std::make_shared<OffdeviceIntermediateDictPtNode>(codePoints, wordProperty));
+    return true;
+}
+
+const WordProperty *OffdeviceIntermediateDict::getWordProperty(
+        const CodePointArrayView codePoints) const {
+    const OffdeviceIntermediateDictPtNodeArray *ptNodeArray = &mRootPtNodeArray;
+    for (size_t i = 0; i < codePoints.size();) {
+        bool foundNext = false;
+        for (const auto ptNode : ptNodeArray->getPtNodeList()) {
+            const CodePointArrayView ptNodeCodePoints = ptNode->getPtNodeCodePoints();
+            if (codePoints[i] < ptNodeCodePoints[0]) {
+                continue;
+            }
+            if (codePoints[i] > ptNodeCodePoints[0]
+                     || codePoints.size() < ptNodeCodePoints.size()) {
+                return nullptr;
+            }
+            for (size_t j = 1; j < ptNodeCodePoints.size(); ++j) {
+                if (codePoints[i + j] != ptNodeCodePoints[j]) {
+                    return nullptr;
+                }
+            }
+            i += ptNodeCodePoints.size();
+            if (i == codePoints.size()) {
+                return ptNode->getWordProperty();
+            }
+            ptNodeArray = &ptNode->getChildrenPtNodeArray();
+            foundNext = true;
+            break;
+        }
+        if (!foundNext) {
+            break;
+        }
+    }
+    return nullptr;
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.h b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.h
new file mode 100644
index 0000000..13d26ba
--- /dev/null
+++ b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict.h
@@ -0,0 +1,54 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_H
+#define LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_H
+
+#include "dict_toolkit_defines.h"
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict_header.h"
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node_array.h"
+#include "suggest/core/dictionary/property/word_property.h"
+#include "utils/int_array_view.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+/**
+ * On memory patricia trie to represent a dictionary.
+ */
+class OffdeviceIntermediateDict final {
+ public:
+    OffdeviceIntermediateDict(const OffdeviceIntermediateDictHeader &header)
+            : mHeader(header), mRootPtNodeArray() {}
+
+    bool addWord(const WordProperty &wordProperty);
+    // The returned value will be invalid after modifying the dictionary. e.g. calling addWord().
+    const WordProperty *getWordProperty(const CodePointArrayView codePoints) const;
+    const OffdeviceIntermediateDictHeader &getHeader() const { return mHeader; }
+
+ private:
+    DISALLOW_ASSIGNMENT_OPERATOR(OffdeviceIntermediateDict);
+
+    const OffdeviceIntermediateDictHeader mHeader;
+    OffdeviceIntermediateDictPtNodeArray mRootPtNodeArray;
+
+    bool addWordInner(const CodePointArrayView codePoints, const WordProperty &wordProperty,
+            OffdeviceIntermediateDictPtNodeArray &ptNodeArray);
+};
+
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_H
diff --git a/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_header.h b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_header.h
new file mode 100644
index 0000000..440627a
--- /dev/null
+++ b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_header.h
@@ -0,0 +1,44 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_HEADER_H
+#define LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_HEADER_H
+
+#include <map>
+#include <vector>
+
+#include "dict_toolkit_defines.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class OffdeviceIntermediateDictHeader final {
+ public:
+    using AttributeMap = std::map<std::vector<int>, std::vector<int>>;
+
+    OffdeviceIntermediateDictHeader(const AttributeMap &attributesMap)
+            : mAttributeMap(attributesMap) {}
+
+ private:
+    DISALLOW_DEFAULT_CONSTRUCTOR(OffdeviceIntermediateDictHeader);
+    DISALLOW_ASSIGNMENT_OPERATOR(OffdeviceIntermediateDictHeader);
+
+    const AttributeMap mAttributeMap;
+};
+
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_HEADER_H
diff --git a/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node.h b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node.h
new file mode 100644
index 0000000..721ccd7
--- /dev/null
+++ b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node.h
@@ -0,0 +1,79 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_H
+#define LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_H
+
+#include <memory>
+
+#include "dict_toolkit_defines.h"
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node_array.h"
+#include "suggest/core/dictionary/property/word_property.h"
+#include "utils/int_array_view.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class OffdeviceIntermediateDictPtNode final {
+ public:
+    // Non-terminal
+    OffdeviceIntermediateDictPtNode(const CodePointArrayView ptNodeCodePoints)
+            : mPtNodeCodePoints(ptNodeCodePoints.toVector()), mChildrenPtNodeArray(),
+              mWortProperty(nullptr) {}
+
+    // Terminal
+    OffdeviceIntermediateDictPtNode(const CodePointArrayView ptNodeCodePoints,
+            const WordProperty &wordProperty)
+             : mPtNodeCodePoints(ptNodeCodePoints.toVector()), mChildrenPtNodeArray(),
+               mWortProperty(new WordProperty(wordProperty)) {}
+
+    // Replacing PtNodeCodePoints.
+    OffdeviceIntermediateDictPtNode(const CodePointArrayView ptNodeCodePoints,
+            const OffdeviceIntermediateDictPtNode &ptNode)
+            : mPtNodeCodePoints(ptNodeCodePoints.toVector()),
+              mChildrenPtNodeArray(ptNode.mChildrenPtNodeArray),
+              mWortProperty(new WordProperty(*ptNode.mWortProperty)) {}
+
+    // Replacing WordProperty.
+    OffdeviceIntermediateDictPtNode(const WordProperty &wordProperty,
+            const OffdeviceIntermediateDictPtNode &ptNode)
+            : mPtNodeCodePoints(ptNode.mPtNodeCodePoints),
+              mChildrenPtNodeArray(ptNode.mChildrenPtNodeArray),
+              mWortProperty(new WordProperty(wordProperty)) {}
+
+    const WordProperty *getWordProperty() const {
+        return mWortProperty.get();
+    }
+
+    const CodePointArrayView getPtNodeCodePoints() const {
+        return CodePointArrayView(mPtNodeCodePoints);
+    }
+
+    OffdeviceIntermediateDictPtNodeArray &getChildrenPtNodeArray() {
+        return mChildrenPtNodeArray;
+    }
+
+ private:
+    DISALLOW_COPY_AND_ASSIGN(OffdeviceIntermediateDictPtNode);
+
+    const std::vector<int> mPtNodeCodePoints;
+    OffdeviceIntermediateDictPtNodeArray mChildrenPtNodeArray;
+    const std::unique_ptr<WordProperty> mWortProperty;
+};
+
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_H
diff --git a/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node_array.h b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node_array.h
new file mode 100644
index 0000000..f87456c
--- /dev/null
+++ b/native/dicttoolkit/src/offdevice_intermediate_dict/offdevice_intermediate_dict_pt_node_array.h
@@ -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.
+ */
+
+#ifndef LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_ARRAY_H
+#define LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_ARRAY_H
+
+#include <list>
+#include <memory>
+
+#include "dict_toolkit_defines.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class OffdeviceIntermediateDictPtNode;
+
+class OffdeviceIntermediateDictPtNodeArray final {
+ public:
+    const std::list<std::shared_ptr<OffdeviceIntermediateDictPtNode>> &getPtNodeList() const {
+        return mPtNodes;
+    }
+
+    std::list<std::shared_ptr<OffdeviceIntermediateDictPtNode>> *getMutablePtNodeList() {
+        return &mPtNodes;
+    }
+
+ private:
+    DISALLOW_ASSIGNMENT_OPERATOR(OffdeviceIntermediateDictPtNodeArray);
+
+    std::list<std::shared_ptr<OffdeviceIntermediateDictPtNode>> mPtNodes;
+};
+
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_OFFDEVICE_INTERMEDIATE_DICT_PT_NODE_ARRAY_H
diff --git a/native/dicttoolkit/src/utils/arguments_and_options.h b/native/dicttoolkit/src/utils/arguments_and_options.h
new file mode 100644
index 0000000..d8f5985
--- /dev/null
+++ b/native/dicttoolkit/src/utils/arguments_and_options.h
@@ -0,0 +1,54 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_ARGUMENTS_AND_OPTIONS_H
+#define LATINIME_DICT_TOOLKIT_ARGUMENTS_AND_OPTIONS_H
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "dict_toolkit_defines.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class ArgumentsAndOptions {
+ public:
+    ArgumentsAndOptions() : mIsValid(false), mOptions(), mArguments() {}
+
+    ArgumentsAndOptions(std::unordered_map<std::string, std::string> &&options,
+            std::unordered_map<std::string, std::vector<std::string>> &&arguments)
+            : mIsValid(true), mOptions(std::move(options)), mArguments(std::move(arguments)) {}
+
+    bool isValid() const {
+        return mIsValid;
+    }
+
+    bool hasOption(const std::string &optionName) const {
+        return mOptions.find(optionName) != mOptions.end();
+    }
+
+ private:
+    DISALLOW_ASSIGNMENT_OPERATOR(ArgumentsAndOptions);
+
+    const bool mIsValid;
+    const std::unordered_map<std::string, std::string> mOptions;
+    const std::unordered_map<std::string, std::vector<std::string>> mArguments;
+};
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_ARGUMENTS_AND_OPTIONS_H
diff --git a/native/dicttoolkit/src/utils/arguments_parser.cpp b/native/dicttoolkit/src/utils/arguments_parser.cpp
new file mode 100644
index 0000000..039dae3
--- /dev/null
+++ b/native/dicttoolkit/src/utils/arguments_parser.cpp
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+#include "utils/arguments_parser.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+const int ArgumentSpec::UNLIMITED_COUNT = -1;
+
+bool ArgumentsParser::validateSpecs() const {
+    for (size_t i = 0; i < mArgumentSpecs.size() ; ++i) {
+        if (mArgumentSpecs[i].getMinCount() != mArgumentSpecs[i].getMaxCount()
+                && i != mArgumentSpecs.size() - 1) {
+            AKLOGE("Variable length argument must be at the end.");
+            return false;
+        }
+    }
+    return true;
+}
+
+void ArgumentsParser::printUsage(const std::string &commandName,
+        const std::string &description) const {
+    printf("Usage: %s", commandName.c_str());
+    for (const auto &option : mOptionSpecs) {
+        const std::string &optionName = option.first;
+        const OptionSpec &spec = option.second;
+        printf(" [-%s", optionName.c_str());
+        if (spec.takeValue()) {
+            printf(" <%s>", spec.getValueName().c_str());
+        }
+        printf("]");
+    }
+    for (const auto &argSpec : mArgumentSpecs) {
+        if (argSpec.getMinCount() == 0 && argSpec.getMaxCount() == 1) {
+            printf(" [<%s>]", argSpec.getName().c_str());
+        } else if (argSpec.getMinCount() == 1 && argSpec.getMaxCount() == 1) {
+            printf(" <%s>", argSpec.getName().c_str());
+        } else if (argSpec.getMinCount() == 0) {
+            printf(" [<%s>...]", argSpec.getName().c_str());
+        } else if (argSpec.getMinCount() == 1) {
+            printf(" <%s>...", argSpec.getName().c_str());
+        }
+    }
+    printf("\n%s\n\n", description.c_str());
+    for (const auto &option : mOptionSpecs) {
+        const std::string &optionName = option.first;
+        const OptionSpec &spec = option.second;
+        printf(" -%s", optionName.c_str());
+        if (spec.takeValue()) {
+            printf(" <%s>", spec.getValueName().c_str());
+        }
+        printf("\t\t\t%s", spec.getDescription().c_str());
+        if (spec.takeValue() && !spec.getDefaultValue().empty()) {
+            printf("\tdefault: %s", spec.getDefaultValue().c_str());
+        }
+        printf("\n");
+    }
+    for (const auto &argSpec : mArgumentSpecs) {
+        printf(" <%s>\t\t\t%s\n", argSpec.getName().c_str(), argSpec.getDescription().c_str());
+    }
+    printf("\n\n");
+}
+
+const ArgumentsAndOptions ArgumentsParser::parseArguments(const int argc, char **argv) const {
+    // TODO: Implement
+    return ArgumentsAndOptions();
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/utils/arguments_parser.h b/native/dicttoolkit/src/utils/arguments_parser.h
new file mode 100644
index 0000000..be2dd87
--- /dev/null
+++ b/native/dicttoolkit/src/utils/arguments_parser.h
@@ -0,0 +1,118 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_ARGUMENTS_PARSER_H
+#define LATINIME_DICT_TOOLKIT_ARGUMENTS_PARSER_H
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+#include "dict_toolkit_defines.h"
+#include "utils/arguments_and_options.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class OptionSpec {
+ public:
+    // Default constructor and assignment operator is enabled to be used with std::unordered_map.
+    OptionSpec() = default;
+    OptionSpec &operator=(const OptionSpec &) = default;
+
+    static OptionSpec keyValueOption(const std::string &valueName, const std::string &defaultValue,
+            const std::string &description) {
+        return OptionSpec(true /* takeValue */, valueName, defaultValue, description);
+    }
+
+    static OptionSpec switchOption(const std::string &description) {
+        return OptionSpec(false /* takeValue */, "" /* valueName */, "" /* defaultValue */,
+                description);
+    }
+
+    bool takeValue() const { return mTakeValue; }
+    const std::string &getValueName() const { return mValueName; }
+    const std::string &getDefaultValue() const { return mDefaultValue; }
+    const std::string &getDescription() const { return mDescription; }
+
+ private:
+    OptionSpec(const bool takeValue, const std::string &valueName, const std::string &defaultValue,
+            const std::string &description)
+            : mTakeValue(takeValue), mValueName(valueName), mDefaultValue(defaultValue),
+              mDescription(description) {}
+
+    // Whether the option have to be used with a value or just a switch.
+    // e.g. 'f' in "command -f /path/to/file" is mTakeValue == true.
+    //      'f' in "command -f -t" is mTakeValue == false.
+    bool mTakeValue;
+    // Name of the value used to show usage.
+    std::string mValueName;
+    std::string mDefaultValue;
+    std::string mDescription;
+};
+
+class ArgumentSpec {
+ public:
+    static const int UNLIMITED_COUNT;
+
+    static ArgumentSpec singleArgument(const std::string &name, const std::string &description) {
+        return ArgumentSpec(name, 1 /* minCount */, 1 /* maxCount */, description);
+    }
+
+    static ArgumentSpec variableLengthArguments(const std::string &name, const int minCount,
+            const int maxCount, const std::string &description) {
+        return ArgumentSpec(name, minCount, maxCount, description);
+    }
+
+    const std::string &getName() const { return mName; }
+    int getMinCount() const { return mMinCount; }
+    int getMaxCount() const { return mMaxCount; }
+    const std::string &getDescription() const { return mDescription; }
+
+ private:
+    DISALLOW_DEFAULT_CONSTRUCTOR(ArgumentSpec);
+
+    ArgumentSpec(const std::string &name, const int minCount, const int maxCount,
+            const std::string &description)
+            : mName(name), mMinCount(minCount), mMaxCount(maxCount), mDescription(description) {}
+
+    const std::string mName;
+    const int mMinCount;
+    const int mMaxCount;
+    const std::string mDescription;
+};
+
+class ArgumentsParser {
+ public:
+    ArgumentsParser(std::unordered_map<std::string, OptionSpec> &&optionSpecs,
+            std::vector<ArgumentSpec> &&argumentSpecs)
+            : mOptionSpecs(std::move(optionSpecs)), mArgumentSpecs(std::move(argumentSpecs)) {}
+
+    const ArgumentsAndOptions parseArguments(const int argc, char **argv) const;
+    bool validateSpecs() const;
+    void printUsage(const std::string &commandName, const std::string &description) const;
+
+ private:
+    DISALLOW_DEFAULT_CONSTRUCTOR(ArgumentsParser);
+    DISALLOW_ASSIGNMENT_OPERATOR(ArgumentsParser);
+
+    const std::unordered_map<std::string, OptionSpec> mOptionSpecs;
+    const std::vector<ArgumentSpec> mArgumentSpecs;
+};
+
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_ARGUMENTS_PARSER_H
diff --git a/native/dicttoolkit/src/utils/command_utils.cpp b/native/dicttoolkit/src/utils/command_utils.cpp
new file mode 100644
index 0000000..3419642
--- /dev/null
+++ b/native/dicttoolkit/src/utils/command_utils.cpp
@@ -0,0 +1,74 @@
+/*
+ * 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.
+ */
+
+#include "utils/command_utils.h"
+
+#include <cstdio>
+
+#include "command_executors/diff_executor.h"
+#include "command_executors/header_executor.h"
+#include "command_executors/help_executor.h"
+#include "command_executors/info_executor.h"
+#include "command_executors/makedict_executor.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+/* static */ CommandType CommandUtils::getCommandType(const std::string &commandName) {
+    if (commandName == InfoExecutor::COMMAND_NAME) {
+        return CommandType::Info;
+    } else if (commandName == DiffExecutor::COMMAND_NAME) {
+        return CommandType::Diff;
+    } else if (commandName == MakedictExecutor::COMMAND_NAME) {
+        return CommandType::Makedict;
+    } else if (commandName == HeaderExecutor::COMMAND_NAME) {
+        return CommandType::Header;
+    } else if (commandName == HelpExecutor::COMMAND_NAME) {
+        return CommandType::Help;
+    } else {
+        return CommandType::Unknown;
+    }
+}
+
+/* static */ void CommandUtils::printCommandUnknownMessage(const std::string &programName,
+        const std::string &commandName) {
+    fprintf(stderr, "Command '%s' is unknown. Try '%s %s' for more information.\n",
+            commandName.c_str(), programName.c_str(), HelpExecutor::COMMAND_NAME);
+}
+
+/* static */ std::function<int(int, char **)> CommandUtils::getCommandExecutor(
+        const CommandType commandType) {
+    switch (commandType) {
+        case CommandType::Info:
+            return InfoExecutor::run;
+        case CommandType::Diff:
+            return DiffExecutor::run;
+        case CommandType::Makedict:
+            return MakedictExecutor::run;
+        case CommandType::Header:
+            return HeaderExecutor::run;
+        case CommandType::Help:
+            return HelpExecutor::run;
+        default:
+            return [] (int, char **) -> int {
+                printf("Command executor not found.");
+                return 1;
+            };
+    }
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/utils/command_utils.h b/native/dicttoolkit/src/utils/command_utils.h
new file mode 100644
index 0000000..4a181f1
--- /dev/null
+++ b/native/dicttoolkit/src/utils/command_utils.h
@@ -0,0 +1,50 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_COMMAND_UTILS_H
+#define LATINIME_DICT_TOOLKIT_COMMAND_UTILS_H
+
+#include <functional>
+#include <memory>
+#include <string>
+
+#include "dict_toolkit_defines.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+enum class CommandType : int {
+    Info,
+    Diff,
+    Makedict,
+    Header,
+    Help,
+    Unknown
+};
+
+class CommandUtils {
+public:
+    static CommandType getCommandType(const std::string &commandName);
+    static void printCommandUnknownMessage(const std::string &programName,
+            const std::string &commandName);
+    static std::function<int(int, char **)> getCommandExecutor(const CommandType commandType);
+
+private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(CommandUtils);
+};
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_COMMAND_UTILS_H
diff --git a/native/dicttoolkit/src/utils/utf8_utils.cpp b/native/dicttoolkit/src/utils/utf8_utils.cpp
new file mode 100644
index 0000000..0f349f5
--- /dev/null
+++ b/native/dicttoolkit/src/utils/utf8_utils.cpp
@@ -0,0 +1,119 @@
+/*
+ * 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.
+ */
+
+#include "utils/utf8_utils.h"
+
+#include "utils/char_utils.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+const size_t Utf8Utils::MAX_SEQUENCE_SIZE_FOR_A_CODE_POINT = 4;
+const uint8_t Utf8Utils::FIRST_BYTE_MARKER_MASKS[] = {0, 0x80, 0xE0, 0xF0, 0xF8};
+const uint8_t Utf8Utils::FIRST_BYTE_MARKERS[] = {0, 0x00, 0xC0, 0xE0, 0xF0};
+const uint8_t Utf8Utils::FIRST_BYTE_CODE_POINT_BITS_MASKS[] = {0, 0x7F, 0x1F, 0x0F, 0x03};
+const int Utf8Utils::MAX_ENCODED_CODE_POINT_VALUES[] = {-1, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF};
+
+const uint8_t Utf8Utils::TRAILING_BYTE_CODE_POINT_BITS_MASK = 0x3F;
+const uint8_t Utf8Utils::TRAILING_BYTE_MARKER = 0x80;
+const size_t Utf8Utils::CODE_POINT_BIT_COUNT_IN_TRAILING_BYTE = 6;
+
+/* static */ std::vector<int> Utf8Utils::getCodePoints(const std::string &utf8Str) {
+    std::vector<int> codePoints;
+    int remainingByteCountForCurrentCodePoint = 0;
+    int currentCodePointSequenceSize = 0;
+    int codePoint = 0;
+    for (const char c : utf8Str) {
+        if (remainingByteCountForCurrentCodePoint == 0) {
+            currentCodePointSequenceSize = getSequenceSizeByCheckingFirstByte(c);
+            if (currentCodePointSequenceSize <= 0) {
+                AKLOGE("%x is an invalid utf8 first byte value.", c);
+                return std::vector<int>();
+            }
+            remainingByteCountForCurrentCodePoint = currentCodePointSequenceSize;
+            codePoint = maskFirstByte(c, remainingByteCountForCurrentCodePoint);
+        } else {
+            codePoint <<= CODE_POINT_BIT_COUNT_IN_TRAILING_BYTE;
+            codePoint += maskTrailingByte(c);
+        }
+        remainingByteCountForCurrentCodePoint--;
+        if (remainingByteCountForCurrentCodePoint == 0) {
+            if (codePoint <= MAX_ENCODED_CODE_POINT_VALUES[currentCodePointSequenceSize - 1]) {
+                AKLOGE("%d bytes encode for codePoint(%x) is a redundant UTF-8 sequence.",
+                        currentCodePointSequenceSize,  codePoint);
+                return std::vector<int>();
+            }
+            codePoints.push_back(codePoint);
+        }
+    }
+    return codePoints;
+}
+
+/* static */ int Utf8Utils::getSequenceSizeByCheckingFirstByte(const uint8_t firstByte) {
+    for (size_t i = 1; i <= MAX_SEQUENCE_SIZE_FOR_A_CODE_POINT; ++i) {
+        if ((firstByte & FIRST_BYTE_MARKER_MASKS[i]) == FIRST_BYTE_MARKERS[i]) {
+            return i;
+        }
+    }
+    // Not a valid utf8 char first byte.
+    return -1;
+}
+
+/* static */ AK_FORCE_INLINE int Utf8Utils::maskFirstByte(const uint8_t firstByte,
+        const int sequenceSize) {
+    return firstByte & FIRST_BYTE_CODE_POINT_BITS_MASKS[sequenceSize];
+}
+
+/* static */ AK_FORCE_INLINE int Utf8Utils::maskTrailingByte(const uint8_t secondOrLaterByte) {
+    return secondOrLaterByte & TRAILING_BYTE_CODE_POINT_BITS_MASK;
+}
+
+/* static */ std::string Utf8Utils::getUtf8String(const CodePointArrayView codePoints) {
+    std::string utf8String;
+    for (const int codePoint : codePoints) {
+        const int sequenceSize = getSequenceSizeToEncodeCodePoint(codePoint);
+        if (sequenceSize <= 0) {
+            AKLOGE("Cannot encode code point (%d).", codePoint);
+            return std::string();
+        }
+        const int trailingByteCount = sequenceSize - 1;
+        // Output first byte.
+        const int value = codePoint >> (trailingByteCount * CODE_POINT_BIT_COUNT_IN_TRAILING_BYTE);
+        utf8String.push_back(static_cast<char>(value | FIRST_BYTE_MARKERS[sequenceSize]));
+        // Output second and later bytes.
+        for (int i = 1; i < sequenceSize; ++i) {
+            const int shiftAmount = (trailingByteCount - i) * CODE_POINT_BIT_COUNT_IN_TRAILING_BYTE;
+            const int value = (codePoint >> shiftAmount) & TRAILING_BYTE_CODE_POINT_BITS_MASK;
+            utf8String.push_back(static_cast<char>(value | TRAILING_BYTE_MARKER));
+        }
+    }
+    return utf8String;
+}
+
+/* static */ int Utf8Utils::getSequenceSizeToEncodeCodePoint(const int codePoint) {
+    if (codePoint < 0) {
+        return -1;
+    }
+    for (size_t i = 1; i <= MAX_SEQUENCE_SIZE_FOR_A_CODE_POINT; ++i) {
+        if (codePoint <= MAX_ENCODED_CODE_POINT_VALUES[i]) {
+            return i;
+        }
+    }
+    return -1;
+}
+
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/src/utils/utf8_utils.h b/native/dicttoolkit/src/utils/utf8_utils.h
new file mode 100644
index 0000000..35818e5
--- /dev/null
+++ b/native/dicttoolkit/src/utils/utf8_utils.h
@@ -0,0 +1,56 @@
+/*
+ * 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 LATINIME_DICT_TOOLKIT_UTF8_UTILS_H
+#define LATINIME_DICT_TOOLKIT_UTF8_UTILS_H
+
+#include <cstdint>
+#include <string>
+#include <vector>
+
+#include "dict_toolkit_defines.h"
+#include "utils/int_array_view.h"
+
+namespace latinime {
+namespace dicttoolkit {
+
+class Utf8Utils {
+public:
+    static std::vector<int> getCodePoints(const std::string &utf8Str);
+    static std::string getUtf8String(const CodePointArrayView codePoints);
+
+private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(Utf8Utils);
+
+    // Values indexed by sequence size.
+    static const size_t MAX_SEQUENCE_SIZE_FOR_A_CODE_POINT;
+    static const uint8_t FIRST_BYTE_MARKER_MASKS[];
+    static const uint8_t FIRST_BYTE_MARKERS[];
+    static const uint8_t FIRST_BYTE_CODE_POINT_BITS_MASKS[];
+    static const int MAX_ENCODED_CODE_POINT_VALUES[];
+
+    static const uint8_t TRAILING_BYTE_CODE_POINT_BITS_MASK;
+    static const uint8_t TRAILING_BYTE_MARKER;
+    static const size_t CODE_POINT_BIT_COUNT_IN_TRAILING_BYTE;
+
+    static int getSequenceSizeByCheckingFirstByte(const uint8_t firstByte);
+    static int maskFirstByte(const uint8_t firstByte, const int encodeSize);
+    static int maskTrailingByte(const uint8_t secondOrLaterByte);
+    static int getSequenceSizeToEncodeCodePoint(const int codePoint);
+};
+} // namespace dicttoolkit
+} // namespace latinime
+#endif // LATINIME_DICT_TOOLKIT_UTF8_UTILS_H
diff --git a/native/dicttoolkit/tests/command_executors/diff_executor_test.cpp b/native/dicttoolkit/tests/command_executors/diff_executor_test.cpp
new file mode 100644
index 0000000..4441414
--- /dev/null
+++ b/native/dicttoolkit/tests/command_executors/diff_executor_test.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/diff_executor.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(DiffExecutorTests, TestArguemntSpecs) {
+    EXPECT_TRUE(DiffExecutor::getArgumentsParser().validateSpecs());
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/command_executors/header_executor_test.cpp b/native/dicttoolkit/tests/command_executors/header_executor_test.cpp
new file mode 100644
index 0000000..a94150b
--- /dev/null
+++ b/native/dicttoolkit/tests/command_executors/header_executor_test.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/header_executor.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(HeaderExecutorTests, TestArguemntSpecs) {
+    EXPECT_TRUE(HeaderExecutor::getArgumentsParser().validateSpecs());
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/command_executors/info_executor_test.cpp b/native/dicttoolkit/tests/command_executors/info_executor_test.cpp
new file mode 100644
index 0000000..debe8c6
--- /dev/null
+++ b/native/dicttoolkit/tests/command_executors/info_executor_test.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/info_executor.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(InfoExecutorTests, TestArguemntSpecs) {
+    EXPECT_TRUE(InfoExecutor::getArgumentsParser().validateSpecs());
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/command_executors/makedict_executor_test.cpp b/native/dicttoolkit/tests/command_executors/makedict_executor_test.cpp
new file mode 100644
index 0000000..44eb3dc
--- /dev/null
+++ b/native/dicttoolkit/tests/command_executors/makedict_executor_test.cpp
@@ -0,0 +1,31 @@
+/*
+ * 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.
+ */
+
+#include "command_executors/makedict_executor.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(MakedictExecutorTests, TestArguemntSpecs) {
+    EXPECT_TRUE(MakedictExecutor::getArgumentsParser().validateSpecs());
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/dict_toolkit_defines_test.cpp b/native/dicttoolkit/tests/dict_toolkit_defines_test.cpp
new file mode 100644
index 0000000..3445bd0
--- /dev/null
+++ b/native/dicttoolkit/tests/dict_toolkit_defines_test.cpp
@@ -0,0 +1,32 @@
+/*
+ * 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.
+ */
+
+#include "dict_toolkit_defines.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+// Initial trivial test case.
+TEST(DictToolkitDefinesTest, TestKeycodeSpace) {
+    EXPECT_EQ(' ', KEYCODE_SPACE);
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/offdevice_intermediate_dict/offdevice_intermediate_dict_test.cpp b/native/dicttoolkit/tests/offdevice_intermediate_dict/offdevice_intermediate_dict_test.cpp
new file mode 100644
index 0000000..f2e24ab
--- /dev/null
+++ b/native/dicttoolkit/tests/offdevice_intermediate_dict/offdevice_intermediate_dict_test.cpp
@@ -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.
+ */
+
+#include "offdevice_intermediate_dict/offdevice_intermediate_dict.h"
+
+#include <gtest/gtest.h>
+
+#include <vector>
+
+#include "suggest/core/dictionary/property/word_property.h"
+#include "utils/int_array_view.h"
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+const std::vector<int> getCodePointVector(const char *str) {
+    std::vector<int> codePoints;
+    while (*str) {
+        codePoints.push_back(*str);
+        ++str;
+    }
+    return codePoints;
+}
+
+const WordProperty getDummpWordProperty(const std::vector<int> &&codePoints) {
+    return WordProperty(std::move(codePoints), UnigramProperty(), std::vector<NgramProperty>());
+}
+
+TEST(OffdeviceIntermediateDictTest, TestAddWordProperties) {
+    OffdeviceIntermediateDict dict = OffdeviceIntermediateDict(
+            OffdeviceIntermediateDictHeader(OffdeviceIntermediateDictHeader::AttributeMap()));
+    EXPECT_EQ(nullptr, dict.getWordProperty(CodePointArrayView()));
+
+    const WordProperty wordProperty0 = getDummpWordProperty(getCodePointVector("abcd"));
+    EXPECT_TRUE(dict.addWord(wordProperty0));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty0.getCodePoints()));
+
+    const WordProperty wordProperty1 = getDummpWordProperty(getCodePointVector("efgh"));
+    EXPECT_TRUE(dict.addWord(wordProperty1));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty1.getCodePoints()));
+
+    const WordProperty wordProperty2 = getDummpWordProperty(getCodePointVector("ab"));
+    EXPECT_TRUE(dict.addWord(wordProperty2));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty2.getCodePoints()));
+
+    const WordProperty wordProperty3 = getDummpWordProperty(getCodePointVector("abcdefg"));
+    EXPECT_TRUE(dict.addWord(wordProperty3));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty3.getCodePoints()));
+
+    const WordProperty wordProperty4 = getDummpWordProperty(getCodePointVector("efef"));
+    EXPECT_TRUE(dict.addWord(wordProperty4));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty4.getCodePoints()));
+
+    const WordProperty wordProperty5 = getDummpWordProperty(getCodePointVector("ef"));
+    EXPECT_TRUE(dict.addWord(wordProperty5));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty5.getCodePoints()));
+
+    const WordProperty wordProperty6 = getDummpWordProperty(getCodePointVector("abcd"));
+    EXPECT_FALSE(dict.addWord(wordProperty6)) << "Adding the same word multiple times should fail.";
+
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty0.getCodePoints()));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty1.getCodePoints()));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty2.getCodePoints()));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty3.getCodePoints()));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty4.getCodePoints()));
+    EXPECT_NE(nullptr, dict.getWordProperty(wordProperty5.getCodePoints()));
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/utils/command_utils_test.cpp b/native/dicttoolkit/tests/utils/command_utils_test.cpp
new file mode 100644
index 0000000..9d79c9d
--- /dev/null
+++ b/native/dicttoolkit/tests/utils/command_utils_test.cpp
@@ -0,0 +1,37 @@
+/*
+ * 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.
+ */
+
+#include "utils/command_utils.h"
+
+#include <gtest/gtest.h>
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(CommandUtilsTests, TestGetCommandType) {
+    EXPECT_EQ(CommandUtils::getCommandType(""), CommandType::Unknown);
+    EXPECT_EQ(CommandUtils::getCommandType("abc"), CommandType::Unknown);
+    EXPECT_EQ(CommandUtils::getCommandType("info"), CommandType::Info);
+    EXPECT_EQ(CommandUtils::getCommandType("diff"), CommandType::Diff);
+    EXPECT_EQ(CommandUtils::getCommandType("makedict"), CommandType::Makedict);
+    EXPECT_EQ(CommandUtils::getCommandType("header"), CommandType::Header);
+    EXPECT_EQ(CommandUtils::getCommandType("help"), CommandType::Help);
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/dicttoolkit/tests/utils/utf8_utils_test.cpp b/native/dicttoolkit/tests/utils/utf8_utils_test.cpp
new file mode 100644
index 0000000..9c59a8b
--- /dev/null
+++ b/native/dicttoolkit/tests/utils/utf8_utils_test.cpp
@@ -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.
+ */
+
+#include "utils/utf8_utils.h"
+
+#include <gtest/gtest.h>
+
+#include <vector>
+
+#include "utils/int_array_view.h"
+
+namespace latinime {
+namespace dicttoolkit {
+namespace {
+
+TEST(Utf8UtilsTests, TestGetCodePoints) {
+    {
+        const std::vector<int> codePoints = Utf8Utils::getCodePoints("");
+        EXPECT_EQ(0u, codePoints.size());
+    }
+    {
+        const std::vector<int> codePoints = Utf8Utils::getCodePoints("test");
+        EXPECT_EQ(4u, codePoints.size());
+        EXPECT_EQ('t', codePoints[0]);
+        EXPECT_EQ('e', codePoints[1]);
+        EXPECT_EQ('s', codePoints[2]);
+        EXPECT_EQ('t', codePoints[3]);
+    }
+    {
+        const std::vector<int> codePoints = Utf8Utils::getCodePoints(u8"\u3042a\u03C2\u0410");
+        EXPECT_EQ(4u, codePoints.size());
+        EXPECT_EQ(0x3042, codePoints[0]); // HIRAGANA LETTER A
+        EXPECT_EQ('a', codePoints[1]);
+        EXPECT_EQ(0x03C2, codePoints[2]); // CYRILLIC CAPITAL LETTER A
+        EXPECT_EQ(0x0410, codePoints[3]); // GREEK SMALL LETTER FINAL SIGMA
+    }
+    {
+        const std::vector<int> codePoints = Utf8Utils::getCodePoints(u8"\U0001F36A?\U0001F752");
+        EXPECT_EQ(3u, codePoints.size());
+        EXPECT_EQ(0x1F36A, codePoints[0]); // COOKIE
+        EXPECT_EQ('?', codePoints[1]);
+        EXPECT_EQ(0x1F752, codePoints[2]); // ALCHEMICAL SYMBOL FOR STARRED TRIDENT
+    }
+
+    // Redundant UTF-8 sequences must be rejected.
+    EXPECT_TRUE(Utf8Utils::getCodePoints("\xC0\xAF").empty());
+    EXPECT_TRUE(Utf8Utils::getCodePoints("\xE0\x80\xAF").empty());
+    EXPECT_TRUE(Utf8Utils::getCodePoints("\xF0\x80\x80\xAF").empty());
+}
+
+TEST(Utf8UtilsTests, TestGetUtf8String) {
+    {
+        const std::vector<int> codePoints = {'t', 'e', 's', 't'};
+        EXPECT_EQ("test", Utf8Utils::getUtf8String(CodePointArrayView(codePoints)));
+    }
+    {
+        const std::vector<int> codePoints = {
+                0x00E0 /* LATIN SMALL LETTER A WITH GRAVE */,
+                0x03C2 /* GREEK SMALL LETTER FINAL SIGMA */,
+                0x0430 /* CYRILLIC SMALL LETTER A */,
+                0x3042 /* HIRAGANA LETTER A */,
+                0x1F36A /* COOKIE */,
+                0x1F752 /* ALCHEMICAL SYMBOL FOR STARRED TRIDENT */
+        };
+        EXPECT_EQ(u8"\u00E0\u03C2\u0430\u3042\U0001F36A\U0001F752",
+                Utf8Utils::getUtf8String(CodePointArrayView(codePoints)));
+    }
+}
+
+} // namespace
+} // namespace dicttoolkit
+} // namespace latinime
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 118f600..9c065e0 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -35,6 +35,7 @@
 #include "utils/int_array_view.h"
 #include "utils/jni_data_utils.h"
 #include "utils/log_utils.h"
+#include "utils/profiler.h"
 #include "utils/time_keeper.h"
 
 namespace latinime {
@@ -43,8 +44,8 @@
 
 static jlong latinime_BinaryDictionary_open(JNIEnv *env, jclass clazz, jstring sourceDir,
         jlong dictOffset, jlong dictSize, jboolean isUpdatable) {
-    PROF_OPEN;
-    PROF_START(66);
+    PROF_INIT;
+    PROF_TIMER_START(66);
     const jsize sourceDirUtf8Length = env->GetStringUTFLength(sourceDir);
     if (sourceDirUtf8Length <= 0) {
         AKLOGE("DICT: Can't get sourceDir string");
@@ -63,8 +64,7 @@
 
     Dictionary *const dictionary =
             new Dictionary(env, std::move(dictionaryStructureWithBufferPolicy));
-    PROF_END(66);
-    PROF_CLOSE;
+    PROF_TIMER_END(66);
     return reinterpret_cast<jlong>(dictionary);
 }
 
@@ -586,7 +586,7 @@
         }
         if (!dictionaryStructureWithBufferPolicy->addUnigramEntry(
                 CodePointArrayView(wordCodePoints, wordCodePointCount),
-                wordProperty.getUnigramProperty())) {
+                &wordProperty.getUnigramProperty())) {
             LogUtils::logToJava(env, "Cannot add unigram to the new dict.");
             return false;
         }
@@ -605,7 +605,7 @@
                 return false;
             }
         }
-        for (const NgramProperty &ngramProperty : *wordProperty.getNgramProperties()) {
+        for (const NgramProperty &ngramProperty : wordProperty.getNgramProperties()) {
             if (!dictionaryStructureWithBufferPolicy->addNgramEntry(&ngramProperty)) {
                 LogUtils::logToJava(env, "Cannot add ngram to the new dict.");
                 return false;
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 8851185..0e67b4d 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -23,10 +23,10 @@
 #define AK_FORCE_INLINE inline
 #endif // __GNUC__
 
-#if defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
+#if defined(FLAG_DBG)
 #undef AK_FORCE_INLINE
 #define AK_FORCE_INLINE inline
-#endif // defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
+#endif // defined(FLAG_DBG)
 
 // Must be equal to Constants.Dictionary.MAX_WORD_LENGTH in Java
 #define MAX_WORD_LENGTH 48
@@ -172,69 +172,6 @@
 #define INTS_TO_CHARS(input, length, output)
 #endif // defined(FLAG_DO_PROFILE) || defined(FLAG_DBG)
 
-#ifdef FLAG_DO_PROFILE
-// Profiler
-#include <time.h>
-
-#define PROF_BUF_SIZE 100
-static float profile_buf[PROF_BUF_SIZE];
-static float profile_old[PROF_BUF_SIZE];
-static unsigned int profile_counter[PROF_BUF_SIZE];
-
-#define PROF_RESET               prof_reset()
-#define PROF_COUNT(prof_buf_id)  ++profile_counter[prof_buf_id]
-#define PROF_OPEN                do { PROF_RESET; PROF_START(PROF_BUF_SIZE - 1); } while (0)
-#define PROF_START(prof_buf_id)  do { \
-        PROF_COUNT(prof_buf_id); profile_old[prof_buf_id] = (clock()); } while (0)
-#define PROF_CLOSE               do { PROF_END(PROF_BUF_SIZE - 1); PROF_OUTALL; } while (0)
-#define PROF_END(prof_buf_id)    profile_buf[prof_buf_id] += ((clock()) - profile_old[prof_buf_id])
-#define PROF_CLOCKOUT(prof_buf_id) \
-        AKLOGI("%s : clock is %f", __FUNCTION__, (clock() - profile_old[prof_buf_id]))
-#define PROF_OUTALL              do { AKLOGI("--- %s ---", __FUNCTION__); prof_out(); } while (0)
-
-static inline void prof_reset(void) {
-    for (int i = 0; i < PROF_BUF_SIZE; ++i) {
-        profile_buf[i] = 0;
-        profile_old[i] = 0;
-        profile_counter[i] = 0;
-    }
-}
-
-static inline void prof_out(void) {
-    if (profile_counter[PROF_BUF_SIZE - 1] != 1) {
-        AKLOGI("Error: You must call PROF_OPEN before PROF_CLOSE.");
-    }
-    AKLOGI("Total time is %6.3f ms.",
-            profile_buf[PROF_BUF_SIZE - 1] * 1000.0f / static_cast<float>(CLOCKS_PER_SEC));
-    float all = 0.0f;
-    for (int i = 0; i < PROF_BUF_SIZE - 1; ++i) {
-        all += profile_buf[i];
-    }
-    if (all < 1.0f) all = 1.0f;
-    for (int i = 0; i < PROF_BUF_SIZE - 1; ++i) {
-        if (profile_buf[i] > 0.0f) {
-            AKLOGI("(%d): Used %4.2f%%, %8.4f ms. Called %d times.",
-                    i, (profile_buf[i] * 100.0f / all),
-                    profile_buf[i] * 1000.0f / static_cast<float>(CLOCKS_PER_SEC),
-                    profile_counter[i]);
-        }
-    }
-}
-
-#else // FLAG_DO_PROFILE
-#define PROF_BUF_SIZE 0
-#define PROF_RESET
-#define PROF_COUNT(prof_buf_id)
-#define PROF_OPEN
-#define PROF_START(prof_buf_id)
-#define PROF_CLOSE
-#define PROF_END(prof_buf_id)
-#define PROF_CLOCK_OUT(prof_buf_id)
-#define PROF_CLOCKOUT(prof_buf_id)
-#define PROF_OUTALL
-
-#endif // FLAG_DO_PROFILE
-
 #ifdef FLAG_DBG
 #define DEBUG_DICT true
 #define DEBUG_DICT_FULL false
diff --git a/native/jni/src/suggest/core/dictionary/error_type_utils.cpp b/native/jni/src/suggest/core/dictionary/error_type_utils.cpp
index 1e2494e..8f07ce2 100644
--- a/native/jni/src/suggest/core/dictionary/error_type_utils.cpp
+++ b/native/jni/src/suggest/core/dictionary/error_type_utils.cpp
@@ -31,6 +31,7 @@
 
 const ErrorTypeUtils::ErrorType ErrorTypeUtils::ERRORS_TREATED_AS_AN_EXACT_MATCH =
         NOT_AN_ERROR | MATCH_WITH_WRONG_CASE | MATCH_WITH_MISSING_ACCENT | MATCH_WITH_DIGRAPH;
+const ErrorTypeUtils::ErrorType ErrorTypeUtils::ERRORS_TREATED_AS_A_PERFECT_MATCH = NOT_AN_ERROR;
 
 const ErrorTypeUtils::ErrorType
         ErrorTypeUtils::ERRORS_TREATED_AS_AN_EXACT_MATCH_WITH_INTENTIONAL_OMISSION =
diff --git a/native/jni/src/suggest/core/dictionary/error_type_utils.h b/native/jni/src/suggest/core/dictionary/error_type_utils.h
index fd1d5fc..e92c509 100644
--- a/native/jni/src/suggest/core/dictionary/error_type_utils.h
+++ b/native/jni/src/suggest/core/dictionary/error_type_utils.h
@@ -52,6 +52,10 @@
         return (containedErrorTypes & ~ERRORS_TREATED_AS_AN_EXACT_MATCH) == 0;
     }
 
+    static bool isPerfectMatch(const ErrorType containedErrorTypes) {
+        return (containedErrorTypes & ~ERRORS_TREATED_AS_A_PERFECT_MATCH) == 0;
+    }
+
     static bool isExactMatchWithIntentionalOmission(const ErrorType containedErrorTypes) {
         return (containedErrorTypes
                 & ~ERRORS_TREATED_AS_AN_EXACT_MATCH_WITH_INTENTIONAL_OMISSION) == 0;
@@ -73,6 +77,7 @@
     DISALLOW_IMPLICIT_CONSTRUCTORS(ErrorTypeUtils);
 
     static const ErrorType ERRORS_TREATED_AS_AN_EXACT_MATCH;
+    static const ErrorType ERRORS_TREATED_AS_A_PERFECT_MATCH;
     static const ErrorType ERRORS_TREATED_AS_AN_EXACT_MATCH_WITH_INTENTIONAL_OMISSION;
 };
 } // namespace latinime
diff --git a/native/jni/src/suggest/core/dictionary/property/historical_info.h b/native/jni/src/suggest/core/dictionary/property/historical_info.h
index f9bd6fd..e5ce1ea 100644
--- a/native/jni/src/suggest/core/dictionary/property/historical_info.h
+++ b/native/jni/src/suggest/core/dictionary/property/historical_info.h
@@ -38,6 +38,7 @@
         return mTimestamp;
     }
 
+    // TODO: Remove
     int getLevel() const {
         return mLevel;
     }
diff --git a/native/jni/src/suggest/core/dictionary/property/word_property.h b/native/jni/src/suggest/core/dictionary/property/word_property.h
index b5314fa..d4db3f0 100644
--- a/native/jni/src/suggest/core/dictionary/property/word_property.h
+++ b/native/jni/src/suggest/core/dictionary/property/word_property.h
@@ -23,6 +23,7 @@
 #include "jni.h"
 #include "suggest/core/dictionary/property/ngram_property.h"
 #include "suggest/core/dictionary/property/unigram_property.h"
+#include "utils/int_array_view.h"
 
 namespace latinime {
 
@@ -33,10 +34,10 @@
     WordProperty()
             : mCodePoints(), mUnigramProperty(), mNgrams() {}
 
-    WordProperty(const std::vector<int> &&codePoints, const UnigramProperty *const unigramProperty,
-            const std::vector<NgramProperty> *const ngrams)
-            : mCodePoints(std::move(codePoints)), mUnigramProperty(*unigramProperty),
-              mNgrams(*ngrams) {}
+    WordProperty(const std::vector<int> &&codePoints, const UnigramProperty &unigramProperty,
+            const std::vector<NgramProperty> &ngrams)
+            : mCodePoints(std::move(codePoints)), mUnigramProperty(unigramProperty),
+              mNgrams(ngrams) {}
 
     void outputProperties(JNIEnv *const env, jintArray outCodePoints, jbooleanArray outFlags,
             jintArray outProbabilityInfo, jobject outNgramPrevWordsArray,
@@ -44,12 +45,16 @@
             jobject outNgramProbabilities, jobject outShortcutTargets,
             jobject outShortcutProbabilities) const;
 
-    const UnigramProperty *getUnigramProperty() const {
-        return &mUnigramProperty;
+    const CodePointArrayView getCodePoints() const {
+        return CodePointArrayView(mCodePoints);
     }
 
-    const std::vector<NgramProperty> *getNgramProperties() const {
-        return &mNgrams;
+    const UnigramProperty &getUnigramProperty() const {
+        return mUnigramProperty;
+    }
+
+    const std::vector<NgramProperty> &getNgramProperties() const {
+        return mNgrams;
     }
 
  private:
diff --git a/native/jni/src/suggest/core/policy/scoring.h b/native/jni/src/suggest/core/policy/scoring.h
index ce3684a..b9dda83 100644
--- a/native/jni/src/suggest/core/policy/scoring.h
+++ b/native/jni/src/suggest/core/policy/scoring.h
@@ -30,7 +30,7 @@
  public:
     virtual int calculateFinalScore(const float compoundDistance, const int inputSize,
             const ErrorTypeUtils::ErrorType containedErrorTypes, const bool forceCommit,
-            const bool boostExactMatches) const = 0;
+            const bool boostExactMatches, const bool hasProbabilityZero) const = 0;
     virtual void getMostProbableString(const DicTraverseSession *const traverseSession,
             const float weightOfLangModelVsSpatialModel,
             SuggestionResults *const outSuggestionResults) const = 0;
diff --git a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
index 3283f6d..74db959 100644
--- a/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
+++ b/native/jni/src/suggest/core/result/suggestions_output_utils.cpp
@@ -76,6 +76,52 @@
             weightOfLangModelVsSpatialModelToOutputSuggestions, outSuggestionResults);
 }
 
+/* static */ bool SuggestionsOutputUtils::shouldBlockWord(
+        const SuggestOptions *const suggestOptions, const DicNode *const terminalDicNode,
+        const WordAttributes wordAttributes, const bool isLastWord) {
+    const bool currentWordExactMatch =
+            ErrorTypeUtils::isExactMatch(terminalDicNode->getContainedErrorTypes());
+    // When we have to block offensive words, non-exact matched offensive words should not be
+    // output.
+    const bool shouldBlockOffensiveWords = suggestOptions->blockOffensiveWords();
+
+    const bool isBlockedOffensiveWord = shouldBlockOffensiveWords &&
+            wordAttributes.isPossiblyOffensive();
+
+    // This function is called in two situations:
+    //
+    // 1) At the end of a search, in which case terminalDicNode will point to the last DicNode
+    //    of the search, and isLastWord will be true.
+    //                    "fuck"
+    //                        |
+    //                        \ terminalDicNode (isLastWord=true, currentWordExactMatch=true)
+    //    In this case, if the current word is an exact match, we will always let the word
+    //    through, even if the user is blocking offensive words (it's exactly what they typed!)
+    //
+    // 2) In the middle of the search, when we hit a terminal node, to decide whether or not
+    //    to start a new search at root, to try to match the rest of the input. In this case,
+    //    terminalDicNode will point to the terminal node we just hit, and isLastWord will be
+    //    false.
+    //                    "fuckvthis"
+    //                        |
+    //                        \ terminalDicNode (isLastWord=false, currentWordExactMatch=true)
+    //
+    // In this case, we should NOT allow the match through (correcting "fuckthis" to "fuck this"
+    // when offensive words are blocked would be a bad idea).
+    //
+    // In the case of a multi-word correction where the offensive word is typed last (eg.
+    // for the input "allfuck"), this function will be called with isLastWord==true, but
+    // currentWordExactMatch==false. So we are OK in this case as well.
+    //                    "allfuck"
+    //                           |
+    //                           \ terminalDicNode (isLastWord=true, currentWordExactMatch=false)
+    if (isLastWord && currentWordExactMatch) {
+        return false;
+    } else {
+        return isBlockedOffensiveWord;
+    }
+}
+
 /* static */ void SuggestionsOutputUtils::outputSuggestionsOfDicNode(
         const Scoring *const scoringPolicy, DicTraverseSession *traverseSession,
         const DicNode *const terminalDicNode, const float weightOfLangModelVsSpatialModel,
@@ -98,24 +144,16 @@
     const bool isExactMatchWithIntentionalOmission =
             ErrorTypeUtils::isExactMatchWithIntentionalOmission(
                     terminalDicNode->getContainedErrorTypes());
-    const bool isFirstCharUppercase = terminalDicNode->isFirstCharUppercase();
-    // Heuristic: We exclude probability=0 first-char-uppercase words from exact match.
-    // (e.g. "AMD" and "and")
-    const bool isSafeExactMatch = isExactMatch
-            && !(wordAttributes.isPossiblyOffensive() && isFirstCharUppercase);
     const int outputTypeFlags =
             (wordAttributes.isPossiblyOffensive() ? Dictionary::KIND_FLAG_POSSIBLY_OFFENSIVE : 0)
-            | ((isSafeExactMatch && boostExactMatches) ? Dictionary::KIND_FLAG_EXACT_MATCH : 0)
+            | ((isExactMatch && boostExactMatches) ? Dictionary::KIND_FLAG_EXACT_MATCH : 0)
             | (isExactMatchWithIntentionalOmission ?
                     Dictionary::KIND_FLAG_EXACT_MATCH_WITH_INTENTIONAL_OMISSION : 0);
-
     // Entries that are blacklisted or do not represent a word should not be output.
     const bool isValidWord = !(wordAttributes.isBlacklisted() || wordAttributes.isNotAWord());
-    // When we have to block offensive words, non-exact matched offensive words should not be
-    // output.
-    const bool blockOffensiveWords = traverseSession->getSuggestOptions()->blockOffensiveWords();
-    const bool isBlockedOffensiveWord = blockOffensiveWords && wordAttributes.isPossiblyOffensive()
-            && !isSafeExactMatch;
+
+    const bool shouldBlockThisWord = shouldBlockWord(traverseSession->getSuggestOptions(),
+            terminalDicNode, wordAttributes, true /* isLastWord */);
 
     // Increase output score of top typing suggestion to ensure autocorrection.
     // TODO: Better integration with java side autocorrection logic.
@@ -123,11 +161,11 @@
             compoundDistance, traverseSession->getInputSize(),
             terminalDicNode->getContainedErrorTypes(),
             (forceCommitMultiWords && terminalDicNode->hasMultipleWords()),
-            boostExactMatches);
+            boostExactMatches, wordAttributes.getProbability() == 0);
 
     // Don't output invalid or blocked offensive words. However, we still need to submit their
     // shortcuts if any.
-    if (isValidWord && !isBlockedOffensiveWord) {
+    if (isValidWord && !shouldBlockThisWord) {
         int codePoints[MAX_WORD_LENGTH];
         terminalDicNode->outputResult(codePoints);
         const int indexToPartialCommit = outputSecondWordFirstLetterInputIndex ?
diff --git a/native/jni/src/suggest/core/result/suggestions_output_utils.h b/native/jni/src/suggest/core/result/suggestions_output_utils.h
index bf84978..eca1f78 100644
--- a/native/jni/src/suggest/core/result/suggestions_output_utils.h
+++ b/native/jni/src/suggest/core/result/suggestions_output_utils.h
@@ -18,6 +18,7 @@
 #define LATINIME_SUGGESTIONS_OUTPUT_UTILS
 
 #include "defines.h"
+#include "suggest/core/dictionary/word_attributes.h"
 
 namespace latinime {
 
@@ -25,11 +26,19 @@
 class DicNode;
 class DicTraverseSession;
 class Scoring;
+class SuggestOptions;
 class SuggestionResults;
 
 class SuggestionsOutputUtils {
  public:
     /**
+     * Returns true if we should block the incoming word, in the context of the user's
+     * preferences to include or not include possibly offensive words
+     */
+    static bool shouldBlockWord(const SuggestOptions *const suggestOptions,
+            const DicNode *const terminalDicNode, const WordAttributes wordAttributes,
+            const bool isLastWord);
+    /**
      * Outputs the final list of suggestions (i.e., terminal nodes).
      */
     static void outputSuggestions(const Scoring *const scoringPolicy,
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 68a3645..e5e9b46 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -29,6 +29,7 @@
 #include "suggest/core/result/suggestions_output_utils.h"
 #include "suggest/core/session/dic_traverse_session.h"
 #include "suggest/core/suggest_options.h"
+#include "utils/profiler.h"
 
 namespace latinime {
 
@@ -48,8 +49,8 @@
         int *inputXs, int *inputYs, int *times, int *pointerIds, int *inputCodePoints,
         int inputSize, const float weightOfLangModelVsSpatialModel,
         SuggestionResults *const outSuggestionResults) const {
-    PROF_OPEN;
-    PROF_START(0);
+    PROF_INIT;
+    PROF_TIMER_START(0);
     const float maxSpatialDistance = TRAVERSAL->getMaxSpatialDistance();
     DicTraverseSession *tSession = static_cast<DicTraverseSession *>(traverseSession);
     tSession->setupForGetSuggestions(pInfo, inputCodePoints, inputSize, inputXs, inputYs, times,
@@ -57,8 +58,8 @@
     // TODO: Add the way to evaluate cache
 
     initializeSearch(tSession);
-    PROF_END(0);
-    PROF_START(1);
+    PROF_TIMER_END(0);
+    PROF_TIMER_START(1);
 
     // keep expanding search dicNodes until all have terminated.
     while (tSession->getDicTraverseCache()->activeSize() > 0) {
@@ -66,12 +67,11 @@
         tSession->getDicTraverseCache()->advanceActiveDicNodes();
         tSession->getDicTraverseCache()->advanceInputIndex(inputSize);
     }
-    PROF_END(1);
-    PROF_START(2);
+    PROF_TIMER_END(1);
+    PROF_TIMER_START(2);
     SuggestionsOutputUtils::outputSuggestions(
             SCORING, tSession, weightOfLangModelVsSpatialModel, outSuggestionResults);
-    PROF_END(2);
-    PROF_CLOSE;
+    PROF_TIMER_END(2);
 }
 
 /**
@@ -416,6 +416,11 @@
             traverseSession->getDictionaryStructurePolicy()->getWordAttributesInContext(
                     dicNode->getPrevWordIds(), dicNode->getWordId(),
                     traverseSession->getMultiBigramMap());
+    if (SuggestionsOutputUtils::shouldBlockWord(traverseSession->getSuggestOptions(),
+            dicNode, wordAttributes, false /* isLastWord */)) {
+        return;
+    }
+
     if (!TRAVERSAL->isGoodToTraverseNextWord(dicNode, wordAttributes.getProbability())) {
         return;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
index 44c2f44..7a5acd7 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_policy.h
@@ -134,15 +134,17 @@
         // same so we use them for both here.
         switch (mDictFormatVersion) {
             case FormatUtils::VERSION_2:
-                return FormatUtils::VERSION_2;
             case FormatUtils::VERSION_201:
-                return FormatUtils::VERSION_201;
+                AKLOGE("Dictionary versions 2 and 201 are incompatible with this version");
+                return FormatUtils::UNKNOWN_VERSION;
+            case FormatUtils::VERSION_202:
+                return FormatUtils::VERSION_202;
             case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
                 return FormatUtils::VERSION_4_ONLY_FOR_TESTING;
-            case FormatUtils::VERSION_4:
-                return FormatUtils::VERSION_4;
-            case FormatUtils::VERSION_4_DEV:
-                return FormatUtils::VERSION_4_DEV;
+            case FormatUtils::VERSION_402:
+                return FormatUtils::VERSION_402;
+            case FormatUtils::VERSION_403:
+                return FormatUtils::VERSION_403;
             default:
                 return FormatUtils::UNKNOWN_VERSION;
         }
@@ -245,7 +247,7 @@
     }
 
     bool supportsBeginningOfSentence() const {
-        return mDictFormatVersion >= FormatUtils::VERSION_4;
+        return mDictFormatVersion >= FormatUtils::VERSION_402;
     }
 
     const int *getCodePointTable() const {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/header/header_read_write_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/header/header_read_write_utils.cpp
index 41a8b13..19ed0d4 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/header/header_read_write_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/header/header_read_write_utils.cpp
@@ -111,11 +111,12 @@
     switch (version) {
         case FormatUtils::VERSION_2:
         case FormatUtils::VERSION_201:
-            // Version 2 or 201 dictionary writing is not supported.
+        case FormatUtils::VERSION_202:
+            // None of the static dictionaries (v2x) support writing
             return false;
         case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
-        case FormatUtils::VERSION_4:
-        case FormatUtils::VERSION_4_DEV:
+        case FormatUtils::VERSION_402:
+        case FormatUtils::VERSION_403:
             return buffer->writeUintAndAdvancePosition(version /* data */,
                     HEADER_DICTIONARY_VERSION_SIZE, writingPos);
         default:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/bigram_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/bigram_dict_content.cpp
index 9e1adff..15ac883 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/bigram_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/bigram_dict_content.cpp
@@ -65,6 +65,8 @@
             (encodedTargetTerminalId == Ver4DictConstants::INVALID_BIGRAM_TARGET_TERMINAL_ID) ?
                     Ver4DictConstants::NOT_A_TERMINAL_ID : encodedTargetTerminalId;
     if (mHasHistoricalInfo) {
+        // Hack for better migration.
+        count += level;
         const HistoricalInfo historicalInfo(timestamp, level, count);
         return BigramEntry(hasNext, probability, &historicalInfo, targetTerminalId);
     } else {
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/probability_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/probability_dict_content.cpp
index ef6166f..61ef4aa 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/probability_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/content/probability_dict_content.cpp
@@ -50,7 +50,8 @@
                 Ver4DictConstants::WORD_LEVEL_FIELD_SIZE, &entryPos);
         const int count = buffer->readUintAndAdvancePosition(
                 Ver4DictConstants::WORD_COUNT_FIELD_SIZE, &entryPos);
-        const HistoricalInfo historicalInfo(timestamp, level, count);
+        // Hack for better migration.
+        const HistoricalInfo historicalInfo(timestamp, level, count + level);
         return ProbabilityEntry(flags, probability, &historicalInfo);
     } else {
         return ProbabilityEntry(flags, probability);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
index 08e39ce..ca7d93b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/backward/v402/ver4_patricia_trie_policy.cpp
@@ -140,7 +140,7 @@
 
 const WordAttributes Ver4PatriciaTriePolicy::getWordAttributes(const int probability,
         const PtNodeParams &ptNodeParams) const {
-    return WordAttributes(probability, ptNodeParams.isBlacklisted(), ptNodeParams.isNotAWord(),
+    return WordAttributes(probability, false /* isBlacklisted */, ptNodeParams.isNotAWord(),
             ptNodeParams.getProbability() == 0);
 }
 
@@ -164,7 +164,7 @@
     }
     const int ptNodePos = getTerminalPtNodePosFromWordId(wordId);
     const PtNodeParams ptNodeParams(mNodeReader.fetchPtNodeParamsInBufferFromPtNodePos(ptNodePos));
-    if (ptNodeParams.isDeleted() || ptNodeParams.isBlacklisted() || ptNodeParams.isNotAWord()) {
+    if (ptNodeParams.isDeleted() || ptNodeParams.isNotAWord()) {
         return NOT_A_PROBABILITY;
     }
     if (prevWordIds.empty()) {
@@ -614,7 +614,7 @@
     const UnigramProperty unigramProperty(ptNodeParams.representsBeginningOfSentence(),
             ptNodeParams.isNotAWord(), ptNodeParams.isPossiblyOffensive(),
             ptNodeParams.getProbability(), *historicalInfo, std::move(shortcuts));
-    return WordProperty(wordCodePoints.toVector(), &unigramProperty, &ngrams);
+    return WordProperty(wordCodePoints.toVector(), unigramProperty, ngrams);
 }
 
 int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.cpp
index 372c9e3..9a9a21b 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/dictionary_structure_with_buffer_policy_factory.cpp
@@ -58,7 +58,7 @@
                 const DictionaryHeaderStructurePolicy::AttributeMap *const attributeMap) {
     FormatUtils::FORMAT_VERSION dictFormatVersion = FormatUtils::getFormatVersion(formatVersion);
     switch (dictFormatVersion) {
-        case FormatUtils::VERSION_4: {
+        case FormatUtils::VERSION_402: {
             return newPolicyForOnMemoryV4Dict<backward::v402::Ver4DictConstants,
                     backward::v402::Ver4DictBuffers,
                     backward::v402::Ver4DictBuffers::Ver4DictBuffersPtr,
@@ -66,7 +66,7 @@
                             dictFormatVersion, locale, attributeMap);
         }
         case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
-        case FormatUtils::VERSION_4_DEV: {
+        case FormatUtils::VERSION_403: {
             return newPolicyForOnMemoryV4Dict<Ver4DictConstants, Ver4DictBuffers,
                     Ver4DictBuffers::Ver4DictBuffersPtr, Ver4PatriciaTriePolicy>(
                             dictFormatVersion, locale, attributeMap);
@@ -115,9 +115,10 @@
     switch (formatVersion) {
         case FormatUtils::VERSION_2:
         case FormatUtils::VERSION_201:
-            AKLOGE("Given path is a directory but the format is version 2 or 201. path: %s", path);
+        case FormatUtils::VERSION_202:
+            AKLOGE("Given path is a directory but the format is version 2xx. path: %s", path);
             break;
-        case FormatUtils::VERSION_4: {
+        case FormatUtils::VERSION_402: {
             return newPolicyForV4Dict<backward::v402::Ver4DictConstants,
                     backward::v402::Ver4DictBuffers,
                     backward::v402::Ver4DictBuffers::Ver4DictBuffersPtr,
@@ -125,7 +126,7 @@
                             headerFilePath, formatVersion, std::move(mmappedBuffer));
         }
         case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
-        case FormatUtils::VERSION_4_DEV: {
+        case FormatUtils::VERSION_403: {
             return newPolicyForV4Dict<Ver4DictConstants, Ver4DictBuffers,
                     Ver4DictBuffers::Ver4DictBuffersPtr, Ver4PatriciaTriePolicy>(
                             headerFilePath, formatVersion, std::move(mmappedBuffer));
@@ -177,11 +178,14 @@
     switch (FormatUtils::detectFormatVersion(mmappedBuffer->getReadOnlyByteArrayView())) {
         case FormatUtils::VERSION_2:
         case FormatUtils::VERSION_201:
+            AKLOGE("Dictionary versions 2 and 201 are incompatible with this version");
+            break;
+        case FormatUtils::VERSION_202:
             return DictionaryStructureWithBufferPolicy::StructurePolicyPtr(
                     new PatriciaTriePolicy(std::move(mmappedBuffer)));
         case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
-        case FormatUtils::VERSION_4:
-        case FormatUtils::VERSION_4_DEV:
+        case FormatUtils::VERSION_402:
+        case FormatUtils::VERSION_403:
             AKLOGE("Given path is a file but the format is version 4. path: %s", path);
             break;
         default:
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h
index 585e87a..e52706e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/pt_common/pt_node_params.h
@@ -144,17 +144,6 @@
         return PatriciaTrieReadingUtils::isTerminal(mFlags);
     }
 
-    AK_FORCE_INLINE bool isBlacklisted() const {
-        // Note: this method will be removed in the next change.
-        // It is used in getProbabilityOfWord and getWordAttributes for both v402 and v403.
-        // * getProbabilityOfWord will be changed to no longer return NOT_A_PROBABILITY
-        //   when isBlacklisted (i.e. to only check if isNotAWord or isDeleted)
-        // * getWordAttributes will be changed to always return blacklisted=false and
-        //   isPossiblyOffensive according to the function below (instead of the current
-        //   behaviour of checking if the probability is zero)
-        return PatriciaTrieReadingUtils::isPossiblyOffensive(mFlags);
-    }
-
     AK_FORCE_INLINE bool isPossiblyOffensive() const {
         return PatriciaTrieReadingUtils::isPossiblyOffensive(mFlags);
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
index 66fd18a..1a51aca 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.cpp
@@ -14,7 +14,6 @@
  * limitations under the License.
  */
 
-
 #include "suggest/policyimpl/dictionary/structure/v2/patricia_trie_policy.h"
 
 #include "defines.h"
@@ -317,8 +316,8 @@
 
 const WordAttributes PatriciaTriePolicy::getWordAttributes(const int probability,
         const PtNodeParams &ptNodeParams) const {
-    return WordAttributes(probability, ptNodeParams.isBlacklisted(), ptNodeParams.isNotAWord(),
-            ptNodeParams.getProbability() == 0);
+    return WordAttributes(probability, false /* isBlacklisted */, ptNodeParams.isNotAWord(),
+            ptNodeParams.isPossiblyOffensive());
 }
 
 int PatriciaTriePolicy::getProbability(const int unigramProbability,
@@ -345,10 +344,9 @@
     const int ptNodePos = getTerminalPtNodePosFromWordId(wordId);
     const PtNodeParams ptNodeParams =
             mPtNodeReader.fetchPtNodeParamsInBufferFromPtNodePos(ptNodePos);
-    if (ptNodeParams.isNotAWord() || ptNodeParams.isBlacklisted()) {
-        // If this is not a word, or if it's a blacklisted entry, it should behave as
-        // having no probability outside of the suggestion process (where it should be used
-        // for shortcuts).
+    if (ptNodeParams.isNotAWord()) {
+        // If this is not a word, it should behave as having no probability outside of the
+        // suggestion process (where it should be used for shortcuts).
         return NOT_A_PROBABILITY;
     }
     if (!prevWordIds.empty()) {
@@ -480,7 +478,7 @@
     const UnigramProperty unigramProperty(ptNodeParams.representsBeginningOfSentence(),
             ptNodeParams.isNotAWord(), ptNodeParams.isPossiblyOffensive(),
             ptNodeParams.getProbability(), HistoricalInfo(), std::move(shortcuts));
-    return WordProperty(wordCodePoints.toVector(), &unigramProperty, &ngrams);
+    return WordProperty(wordCodePoints.toVector(), unigramProperty, ngrams);
 }
 
 int PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
index f4d340f..9c4ab18 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h
@@ -105,7 +105,7 @@
             encodedEntry = (encodedEntry << (Ver4DictConstants::WORD_LEVEL_FIELD_SIZE * CHAR_BIT))
                     | static_cast<uint8_t>(mHistoricalInfo.getLevel());
             encodedEntry = (encodedEntry << (Ver4DictConstants::WORD_COUNT_FIELD_SIZE * CHAR_BIT))
-                    | static_cast<uint8_t>(mHistoricalInfo.getCount());
+                    | static_cast<uint16_t>(mHistoricalInfo.getCount());
         } else {
             encodedEntry = (encodedEntry << (Ver4DictConstants::PROBABILITY_SIZE * CHAR_BIT))
                     | static_cast<uint8_t>(mProbability);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
index eb6080a..bd89b8d 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.cpp
@@ -49,8 +49,8 @@
 const int Ver4DictConstants::NOT_A_TERMINAL_ADDRESS = 0;
 const int Ver4DictConstants::TERMINAL_ID_FIELD_SIZE = 4;
 const int Ver4DictConstants::TIME_STAMP_FIELD_SIZE = 4;
-const int Ver4DictConstants::WORD_LEVEL_FIELD_SIZE = 1;
-const int Ver4DictConstants::WORD_COUNT_FIELD_SIZE = 1;
+const int Ver4DictConstants::WORD_LEVEL_FIELD_SIZE = 0;
+const int Ver4DictConstants::WORD_COUNT_FIELD_SIZE = 2;
 
 const uint8_t Ver4DictConstants::FLAG_REPRESENTS_BEGINNING_OF_SENTENCE = 0x1;
 const uint8_t Ver4DictConstants::FLAG_NOT_A_VALID_ENTRY = 0x2;
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
index 600b5ff..13d7a57 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_dict_constants.h
@@ -47,6 +47,7 @@
     static const int NOT_A_TERMINAL_ADDRESS;
     static const int TERMINAL_ID_FIELD_SIZE;
     static const int TIME_STAMP_FIELD_SIZE;
+    // TODO: Remove
     static const int WORD_LEVEL_FIELD_SIZE;
     static const int WORD_COUNT_FIELD_SIZE;
     // Flags in probability entry.
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
index 96d789f..7449cd0 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_policy.cpp
@@ -146,8 +146,16 @@
             if (!probabilityEntry.isValid()) {
                 continue;
             }
-            const int probability = probabilityEntry.hasHistoricalInfo() ?
-                    0 : probabilityEntry.getProbability();
+            int probability = NOT_A_PROBABILITY;
+            if (probabilityEntry.hasHistoricalInfo()) {
+                // TODO: Quit checking count here.
+                // If count <= 1, the word can be an invaild word. The actual probability should
+                // be checked using getWordAttributesInContext() in onVisitEntry().
+                probability = probabilityEntry.getHistoricalInfo()->getCount() <= 1 ?
+                        NOT_A_PROBABILITY : 0;
+            } else {
+                probability = probabilityEntry.getProbability();
+            }
             listener->onVisitEntry(probability, entry.getWordId());
         }
     }
@@ -552,7 +560,7 @@
             wordAttributes.isNotAWord(), wordAttributes.isBlacklisted(),
             wordAttributes.isPossiblyOffensive(), wordAttributes.getProbability(),
             *historicalInfo, std::move(shortcuts));
-    return WordProperty(wordCodePoints.toVector(), &unigramProperty, &ngrams);
+    return WordProperty(wordCodePoints.toVector(), unigramProperty, ngrams);
 }
 
 int Ver4PatriciaTriePolicy::getNextWordAndNextToken(const int token, int *const outCodePoints,
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
index 9d8e866..edcb436 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/dict_file_writing_utils.cpp
@@ -44,13 +44,13 @@
     TimeKeeper::setCurrentTime();
     const FormatUtils::FORMAT_VERSION formatVersion = FormatUtils::getFormatVersion(dictVersion);
     switch (formatVersion) {
-        case FormatUtils::VERSION_4:
+        case FormatUtils::VERSION_402:
             return createEmptyV4DictFile<backward::v402::Ver4DictConstants,
                     backward::v402::Ver4DictBuffers,
                     backward::v402::Ver4DictBuffers::Ver4DictBuffersPtr>(
                             filePath, localeAsCodePointVector, attributeMap, formatVersion);
         case FormatUtils::VERSION_4_ONLY_FOR_TESTING:
-        case FormatUtils::VERSION_4_DEV:
+        case FormatUtils::VERSION_403:
             return createEmptyV4DictFile<Ver4DictConstants, Ver4DictBuffers,
                     Ver4DictBuffers::Ver4DictBuffersPtr>(
                             filePath, localeAsCodePointVector, attributeMap, formatVersion);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
index 0cffe56..e225c23 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.cpp
@@ -28,15 +28,17 @@
 /* static */ FormatUtils::FORMAT_VERSION FormatUtils::getFormatVersion(const int formatVersion) {
     switch (formatVersion) {
         case VERSION_2:
-            return VERSION_2;
         case VERSION_201:
-            return VERSION_201;
+            AKLOGE("Dictionary versions 2 and 201 are incompatible with this version");
+            return UNKNOWN_VERSION;
+        case VERSION_202:
+            return VERSION_202;
         case VERSION_4_ONLY_FOR_TESTING:
             return VERSION_4_ONLY_FOR_TESTING;
-        case VERSION_4:
-            return VERSION_4;
-        case VERSION_4_DEV:
-            return VERSION_4_DEV;
+        case VERSION_402:
+            return VERSION_402;
+        case VERSION_403:
+            return VERSION_403;
         default:
             return UNKNOWN_VERSION;
     }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
index 9631008..1616efc 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/format_utils.h
@@ -31,11 +31,15 @@
  public:
     enum FORMAT_VERSION {
         // These MUST have the same values as the relevant constants in FormatSpec.java.
+        // TODO: Remove VERSION_2 and VERSION_201 when we:
+        // * Confirm that old versions of LatinIME download old-format dictionaries
+        // * We no longer need the corresponding constants on the Java side for dicttool
         VERSION_2 = 2,
         VERSION_201 = 201,
+        VERSION_202 = 202,
         VERSION_4_ONLY_FOR_TESTING = 399,
-        VERSION_4 = 402,
-        VERSION_4_DEV = 403,
+        VERSION_402 = 402,
+        VERSION_403 = 403,
         UNKNOWN_VERSION = -1
     };
 
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
index a6f9a8b..856808a 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.cpp
@@ -24,6 +24,7 @@
 const float ScoringParams::AUTOCORRECT_OUTPUT_THRESHOLD = 1.0f;
 
 const float ScoringParams::EXACT_MATCH_PROMOTION = 1.1f;
+const float ScoringParams::PERFECT_MATCH_PROMOTION = 1.1f;
 const float ScoringParams::CASE_ERROR_PENALTY_FOR_EXACT_MATCH = 0.01f;
 const float ScoringParams::ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH = 0.02f;
 const float ScoringParams::DIGRAPH_PENALTY_FOR_EXACT_MATCH = 0.03f;
diff --git a/native/jni/src/suggest/policyimpl/typing/scoring_params.h b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
index b8f8895..6f327a3 100644
--- a/native/jni/src/suggest/policyimpl/typing/scoring_params.h
+++ b/native/jni/src/suggest/policyimpl/typing/scoring_params.h
@@ -34,6 +34,7 @@
     static const int THRESHOLD_SHORT_WORD_LENGTH;
 
     static const float EXACT_MATCH_PROMOTION;
+    static const float PERFECT_MATCH_PROMOTION;
     static const float CASE_ERROR_PENALTY_FOR_EXACT_MATCH;
     static const float ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH;
     static const float DIGRAPH_PENALTY_FOR_EXACT_MATCH;
diff --git a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
index 0240bcf..6acd767 100644
--- a/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
+++ b/native/jni/src/suggest/policyimpl/typing/typing_scoring.h
@@ -44,23 +44,50 @@
 
     AK_FORCE_INLINE int calculateFinalScore(const float compoundDistance, const int inputSize,
             const ErrorTypeUtils::ErrorType containedErrorTypes, const bool forceCommit,
-            const bool boostExactMatches) const {
+            const bool boostExactMatches, const bool hasProbabilityZero) const {
         const float maxDistance = ScoringParams::DISTANCE_WEIGHT_LANGUAGE
                 + static_cast<float>(inputSize) * ScoringParams::TYPING_MAX_OUTPUT_SCORE_PER_INPUT;
         float score = ScoringParams::TYPING_BASE_OUTPUT_SCORE - compoundDistance / maxDistance;
         if (forceCommit) {
             score += ScoringParams::AUTOCORRECT_OUTPUT_THRESHOLD;
         }
-        if (boostExactMatches && ErrorTypeUtils::isExactMatch(containedErrorTypes)) {
-            score += ScoringParams::EXACT_MATCH_PROMOTION;
-            if ((ErrorTypeUtils::MATCH_WITH_WRONG_CASE & containedErrorTypes) != 0) {
-                score -= ScoringParams::CASE_ERROR_PENALTY_FOR_EXACT_MATCH;
+        if (hasProbabilityZero) {
+            // Previously, when both legitimate 0-frequency words (such as distracters) and
+            // offensive words were encoded in the same way, distracters would never show up
+            // when the user blocked offensive words (the default setting, as well as the
+            // setting for regression tests).
+            //
+            // When b/11031090 was fixed and a separate encoding was used for offensive words,
+            // 0-frequency words would no longer be blocked when they were an "exact match"
+            // (where case mismatches and accent mismatches would be considered an "exact
+            // match"). The exact match boosting functionality meant that, for example, when
+            // the user typed "mt" they would be suggested the word "Mt", although they most
+            // probably meant to type "my".
+            //
+            // For this reason, we introduced this change, which does the following:
+            // * Defines the "perfect match" as a really exact match, with no room for case or
+            // accent mismatches
+            // * When the target word has probability zero (as "Mt" does, because it is a
+            // distracter), ONLY boost its score if it is a perfect match.
+            //
+            // By doing this, when the user types "mt", the word "Mt" will NOT be boosted, and
+            // they will get "my". However, if the user makes an explicit effort to type "Mt",
+            // we do boost the word "Mt" so that the user's input is not autocorrected to "My".
+            if (boostExactMatches && ErrorTypeUtils::isPerfectMatch(containedErrorTypes)) {
+                score += ScoringParams::PERFECT_MATCH_PROMOTION;
             }
-            if ((ErrorTypeUtils::MATCH_WITH_MISSING_ACCENT & containedErrorTypes) != 0) {
-                score -= ScoringParams::ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH;
-            }
-            if ((ErrorTypeUtils::MATCH_WITH_DIGRAPH & containedErrorTypes) != 0) {
-                score -= ScoringParams::DIGRAPH_PENALTY_FOR_EXACT_MATCH;
+        } else {
+            if (boostExactMatches && ErrorTypeUtils::isExactMatch(containedErrorTypes)) {
+                score += ScoringParams::EXACT_MATCH_PROMOTION;
+                if ((ErrorTypeUtils::MATCH_WITH_WRONG_CASE & containedErrorTypes) != 0) {
+                    score -= ScoringParams::CASE_ERROR_PENALTY_FOR_EXACT_MATCH;
+                }
+                if ((ErrorTypeUtils::MATCH_WITH_MISSING_ACCENT & containedErrorTypes) != 0) {
+                    score -= ScoringParams::ACCENT_ERROR_PENALTY_FOR_EXACT_MATCH;
+                }
+                if ((ErrorTypeUtils::MATCH_WITH_DIGRAPH & containedErrorTypes) != 0) {
+                    score -= ScoringParams::DIGRAPH_PENALTY_FOR_EXACT_MATCH;
+                }
             }
         }
         return static_cast<int>(score * SUGGEST_INTERFACE_OUTPUT_SCALE);
diff --git a/native/jni/src/utils/int_array_view.h b/native/jni/src/utils/int_array_view.h
index 4083731..e0f6710 100644
--- a/native/jni/src/utils/int_array_view.h
+++ b/native/jni/src/utils/int_array_view.h
@@ -133,6 +133,29 @@
         return std::vector<int>(begin(), end());
     }
 
+    std::vector<IntArrayView> split(const int separator, const int limit = S_INT_MAX) const {
+        if (limit <= 0) {
+            return std::vector<IntArrayView>();
+        }
+        std::vector<IntArrayView> result;
+        if (limit == 1) {
+            result.emplace_back(mPtr, mSize);
+            return result;
+        }
+        size_t startIndex = 0;
+        for (size_t i = 0; i < mSize; ++i) {
+            if (mPtr[i] == separator) {
+                result.emplace_back(mPtr + startIndex, i - startIndex);
+                startIndex = i + 1;
+                if (result.size() >= static_cast<size_t>(limit - 1)) {
+                    break;
+                }
+            }
+        }
+        result.emplace_back(mPtr + startIndex, mSize - startIndex);
+        return result;
+    }
+
  private:
     DISALLOW_ASSIGNMENT_OPERATOR(IntArrayView);
 
diff --git a/native/jni/src/utils/profiler.h b/native/jni/src/utils/profiler.h
new file mode 100644
index 0000000..5f107fe
--- /dev/null
+++ b/native/jni/src/utils/profiler.h
@@ -0,0 +1,86 @@
+/*
+ * 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 LATINIME_PROFILER_H
+#define LATINIME_PROFILER_H
+
+#ifdef FLAG_DO_PROFILE
+
+#include "defines.h"
+
+#include <ctime>
+#include <unordered_map>
+
+namespace latinime {
+
+class Profiler final {
+ public:
+    Profiler(const clockid_t clockId)
+            : mClockId(clockId), mStartTime(getTimeInMicroSec()), mStartTimes(), mTimes(),
+              mCounters() {}
+
+    ~Profiler() {
+        const float totalTime =
+                static_cast<float>(getTimeInMicroSec() - mStartTime) / 1000.f;
+        AKLOGI("Total time is %6.3f ms.", totalTime);
+        for (const auto &time : mTimes) {
+            AKLOGI("(%d): Used %4.2f%%, %8.4f ms. Called %d times.", time.first,
+                    time.second / totalTime * 100.0f, time.second, mCounters[time.first]);
+        }
+    }
+
+    void startTimer(const int id) {
+        mStartTimes[id] = getTimeInMicroSec();
+    }
+
+    void endTimer(const int id) {
+        mTimes[id] += static_cast<float>(getTimeInMicroSec() - mStartTimes[id]) / 1000.0f;
+        mCounters[id]++;
+    }
+
+    operator bool() const { return false; }
+
+ private:
+    DISALLOW_IMPLICIT_CONSTRUCTORS(Profiler);
+
+    const clockid_t mClockId;
+    int64_t mStartTime;
+    std::unordered_map<int, int64_t> mStartTimes;
+    std::unordered_map<int, float> mTimes;
+    std::unordered_map<int, int> mCounters;
+
+    int64_t getTimeInMicroSec() {
+        timespec time;
+        clock_gettime(mClockId, &time);
+        return static_cast<int64_t>(time.tv_sec) * 1000000
+                + static_cast<int64_t>(time.tv_nsec) / 1000;
+    }
+};
+} // namespace latinime
+
+#define PROF_INIT Profiler __LATINIME__PROFILER__(CLOCK_THREAD_CPUTIME_ID)
+#define PROF_TIMER_START(timer_id) __LATINIME__PROFILER__.startTimer(timer_id)
+#define PROF_TIMER_END(timer_id) __LATINIME__PROFILER__.endTimer(timer_id)
+
+#else // FLAG_DO_PROFILE
+
+#define PROF_INIT
+#define PROF_TIMER_START(timer_id)
+#define PROF_TIMER_END(timer_id)
+
+#endif // FLAG_DO_PROFILE
+
+#endif /* LATINIME_PROFILER_H */
diff --git a/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content_test.cpp b/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content_test.cpp
index 86040f1..313a9af 100644
--- a/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content_test.cpp
+++ b/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content_test.cpp
@@ -52,16 +52,14 @@
 
     const int flag = 0xF0;
     const int timestamp = 0x3FFFFFFF;
-    const int level = 3;
     const int count = 10;
     const int wordId = 100;
-    const HistoricalInfo historicalInfo(timestamp, level, count);
+    const HistoricalInfo historicalInfo(timestamp, 0 /* level */, count);
     const ProbabilityEntry probabilityEntry(flag, &historicalInfo);
     languageModelDictContent.setProbabilityEntry(wordId, &probabilityEntry);
     const ProbabilityEntry entry = languageModelDictContent.getProbabilityEntry(wordId);
     EXPECT_EQ(flag, entry.getFlags());
     EXPECT_EQ(timestamp, entry.getHistoricalInfo()->getTimestamp());
-    EXPECT_EQ(level, entry.getHistoricalInfo()->getLevel());
     EXPECT_EQ(count, entry.getHistoricalInfo()->getCount());
 
     // Remove
diff --git a/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/probability_entry_test.cpp b/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/probability_entry_test.cpp
index 260b347..eb78034 100644
--- a/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/probability_entry_test.cpp
+++ b/native/jni/tests/suggest/policyimpl/dictionary/structure/v4/content/probability_entry_test.cpp
@@ -39,20 +39,18 @@
 TEST(ProbabilityEntryTest, TestEncodeDecodeWithHistoricalInfo) {
     const int flag = 0xF0;
     const int timestamp = 0x3FFFFFFF;
-    const int level = 3;
-    const int count = 10;
+    const int count = 0xABCD;
 
-    const HistoricalInfo historicalInfo(timestamp, level, count);
+    const HistoricalInfo historicalInfo(timestamp, 0 /* level */, count);
     const ProbabilityEntry entry(flag, &historicalInfo);
 
     const uint64_t encodedEntry = entry.encode(true /* hasHistoricalInfo */);
-    EXPECT_EQ(0xF03FFFFFFF030Aull, encodedEntry);
+    EXPECT_EQ(0xF03FFFFFFFABCDull, encodedEntry);
     const ProbabilityEntry decodedEntry =
             ProbabilityEntry::decode(encodedEntry, true /* hasHistoricalInfo */);
 
     EXPECT_EQ(flag, decodedEntry.getFlags());
     EXPECT_EQ(timestamp, decodedEntry.getHistoricalInfo()->getTimestamp());
-    EXPECT_EQ(level, decodedEntry.getHistoricalInfo()->getLevel());
     EXPECT_EQ(count, decodedEntry.getHistoricalInfo()->getCount());
 }
 
diff --git a/native/jni/tests/suggest/policyimpl/dictionary/utils/format_utils_test.cpp b/native/jni/tests/suggest/policyimpl/dictionary/utils/format_utils_test.cpp
index 15f560c..4942005 100644
--- a/native/jni/tests/suggest/policyimpl/dictionary/utils/format_utils_test.cpp
+++ b/native/jni/tests/suggest/policyimpl/dictionary/utils/format_utils_test.cpp
@@ -62,14 +62,14 @@
     }
     {
         const std::vector<uint8_t> buffer =
-                getBuffer(FormatUtils::MAGIC_NUMBER, FormatUtils::VERSION_4, 0, 0);
-        EXPECT_EQ(FormatUtils::VERSION_4, FormatUtils::detectFormatVersion(
+                getBuffer(FormatUtils::MAGIC_NUMBER, FormatUtils::VERSION_402, 0, 0);
+        EXPECT_EQ(FormatUtils::VERSION_402, FormatUtils::detectFormatVersion(
                 ReadOnlyByteArrayView(buffer.data(), buffer.size())));
     }
     {
         const std::vector<uint8_t> buffer =
-                getBuffer(FormatUtils::MAGIC_NUMBER, FormatUtils::VERSION_4_DEV, 0, 0);
-        EXPECT_EQ(FormatUtils::VERSION_4_DEV, FormatUtils::detectFormatVersion(
+                getBuffer(FormatUtils::MAGIC_NUMBER, FormatUtils::VERSION_403, 0, 0);
+        EXPECT_EQ(FormatUtils::VERSION_403, FormatUtils::detectFormatVersion(
                 ReadOnlyByteArrayView(buffer.data(), buffer.size())));
     }
 
diff --git a/native/jni/tests/utils/int_array_view_test.cpp b/native/jni/tests/utils/int_array_view_test.cpp
index 4757a41..2fce633 100644
--- a/native/jni/tests/utils/int_array_view_test.cpp
+++ b/native/jni/tests/utils/int_array_view_test.cpp
@@ -151,5 +151,52 @@
     EXPECT_EQ(std::vector<int>(), CodePointArrayView().toVector());
 }
 
+TEST(IntArrayViewTest, TestSplit) {
+    EXPECT_TRUE(IntArrayView().split(0, 0).empty());
+    {
+        const auto intArrayViews = IntArrayView().split(0, 1);
+        EXPECT_EQ(1u, intArrayViews.size());
+        EXPECT_TRUE(intArrayViews[0].empty());
+    }
+    {
+        const auto intArrayViews = IntArrayView().split(0, 100);
+        EXPECT_EQ(1u, intArrayViews.size());
+        EXPECT_TRUE(intArrayViews[0].empty());
+    }
+
+    const std::vector<int> intVector = {1, 2, 3, 3, 2, 3};
+    const IntArrayView intArrayView(intVector);
+    {
+        const auto intArrayViews = intArrayView.split(2);
+        EXPECT_EQ(3u, intArrayViews.size());
+        EXPECT_EQ(std::vector<int>({1}), intArrayViews[0].toVector());
+        EXPECT_EQ(std::vector<int>({3, 3}), intArrayViews[1].toVector());
+        EXPECT_EQ(std::vector<int>({3}), intArrayViews[2].toVector());
+    }
+    {
+        const auto intArrayViews = intArrayView.split(2, 2);
+        EXPECT_EQ(2u, intArrayViews.size());
+        EXPECT_EQ(std::vector<int>({1}), intArrayViews[0].toVector());
+        EXPECT_EQ(std::vector<int>({3, 3, 2, 3}), intArrayViews[1].toVector());
+    }
+    {
+        const auto intArrayViews = intArrayView.split(2, 1);
+        EXPECT_EQ(1u, intArrayViews.size());
+        EXPECT_EQ(intVector, intArrayViews[0].toVector());
+    }
+    {
+        const auto intArrayViews = intArrayView.split(2, 0);
+        EXPECT_EQ(0u, intArrayViews.size());
+    }
+    {
+        const auto intArrayViews = intArrayView.split(3);
+        EXPECT_EQ(4u, intArrayViews.size());
+        EXPECT_EQ(std::vector<int>({1, 2}), intArrayViews[0].toVector());
+        EXPECT_EQ(std::vector<int>(), intArrayViews[1].toVector());
+        EXPECT_EQ(std::vector<int>({2}), intArrayViews[2].toVector());
+        EXPECT_EQ(std::vector<int>(), intArrayViews[3].toVector());
+    }
+}
+
 }  // namespace
 }  // namespace latinime
diff --git a/tests/Android.mk b/tests/Android.mk
index 7cdabf2..0b54491 100644
--- a/tests/Android.mk
+++ b/tests/Android.mk
@@ -12,7 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-LOCAL_PATH:= $(call my-dir)
+LOCAL_PATH := $(call my-dir)
 include $(CLEAR_VARS)
 
 # We only want this apk build for tests.
diff --git a/tests/src/com/android/inputmethod/keyboard/action/ActionTestsBase.java b/tests/src/com/android/inputmethod/keyboard/action/ActionTestsBase.java
index 94caf51..1ea68e4 100644
--- a/tests/src/com/android/inputmethod/keyboard/action/ActionTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/action/ActionTestsBase.java
@@ -30,7 +30,7 @@
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKeyVisual;
 import com.android.inputmethod.latin.common.Constants;
-import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.utils.RunInLocale;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
index e619801..6bb255b 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
@@ -30,10 +30,11 @@
 import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
-import java.util.Arrays;
-import java.util.List;
+import java.util.ArrayList;
 import java.util.Locale;
 
+import javax.annotation.Nonnull;
+
 @SmallTest
 public class LanguageOnSpacebarHelperTests extends AndroidTestCase {
     private final LanguageOnSpacebarHelper mLanguageOnSpacebarHelper =
@@ -48,6 +49,7 @@
     RichInputMethodSubtype FR_CH_SWISS;
     RichInputMethodSubtype FR_CH_QWERTY;
     RichInputMethodSubtype FR_CH_QWERTZ;
+    RichInputMethodSubtype IW_HEBREW;
     RichInputMethodSubtype ZZ_QWERTY;
 
     @Override
@@ -56,116 +58,160 @@
         final Context context = getContext();
         RichInputMethodManager.init(context);
         mRichImm = RichInputMethodManager.getInstance();
-        SubtypeLocaleUtils.init(context);
 
-        EN_US_QWERTY = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.US.toString(), "qwerty"));
-        EN_GB_QWERTY = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.UK.toString(), "qwerty"));
-        FR_AZERTY = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.FRENCH.toString(), "azerty"));
-        FR_CA_QWERTY = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.CANADA_FRENCH.toString(), "qwerty"));
-        FR_CH_SWISS = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "fr_CH", "swiss"));
+        EN_US_QWERTY = findSubtypeOf(Locale.US.toString(), "qwerty");
+        EN_GB_QWERTY = findSubtypeOf(Locale.UK.toString(), "qwerty");
+        FR_AZERTY = findSubtypeOf(Locale.FRENCH.toString(), "azerty");
+        FR_CA_QWERTY = findSubtypeOf(Locale.CANADA_FRENCH.toString(), "qwerty");
+        FR_CH_SWISS = findSubtypeOf("fr_CH", "swiss");
         FR_CH_QWERTZ = new RichInputMethodSubtype(
                 AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype("fr_CH", "qwertz"));
         FR_CH_QWERTY = new RichInputMethodSubtype(
                 AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype("fr_CH", "qwerty"));
-        ZZ_QWERTY = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                SubtypeLocaleUtils.NO_LANGUAGE, "qwerty"));
+        IW_HEBREW = findSubtypeOf("iw", "hebrew");
+        ZZ_QWERTY = findSubtypeOf(SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
     }
 
-    private static List<InputMethodSubtype> asList(final InputMethodSubtype ... subtypes) {
-        return Arrays.asList(subtypes);
+    @Nonnull
+    private RichInputMethodSubtype findSubtypeOf(final String localeString,
+            final String keyboardLayoutSetName) {
+        final InputMethodSubtype subtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                localeString, keyboardLayoutSetName);
+        if (subtype == null) {
+            throw new RuntimeException("Can't find subtype of " + localeString + " with "
+                    + keyboardLayoutSetName);
+        }
+        return new RichInputMethodSubtype(subtype);
     }
 
-    public void testOneSubtype() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(EN_US_QWERTY.getRawSubtype()));
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
-        assertEquals("one same English (US)", FORMAT_TYPE_NONE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("one same NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
-
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(FR_AZERTY.getRawSubtype()));
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false /* isSame */);
-        assertEquals("one diff English (US)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("one diff NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
+    private void enableSubtypes(final RichInputMethodSubtype ... subtypes) {
+        final ArrayList<InputMethodSubtype> enabledSubtypes = new ArrayList<>();
+        for (final RichInputMethodSubtype subtype : subtypes) {
+            enabledSubtypes.add(subtype.getRawSubtype());
+        }
+        mLanguageOnSpacebarHelper.onUpdateEnabledSubtypes(enabledSubtypes);
     }
 
-    public void testTwoSubtypes() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(EN_US_QWERTY.getRawSubtype(),
-                FR_AZERTY.getRawSubtype()));
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
-        assertEquals("two same English (US)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("two same French)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_AZERTY));
-        assertEquals("two same NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
-
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false /* isSame */);
-        assertEquals("two diff English (US)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("two diff French", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_AZERTY));
-        assertEquals("two diff NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
+    private void assertFormatType(final RichInputMethodSubtype subtype,
+            final boolean implicitlyEnabledSubtype, final Locale systemLocale,
+            final int expectedFormat) {
+        mLanguageOnSpacebarHelper.onSubtypeChanged(subtype, implicitlyEnabledSubtype, systemLocale);
+        assertEquals(subtype.getLocales()[0] + " implicitly=" + implicitlyEnabledSubtype
+                + " in " + systemLocale, expectedFormat,
+                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype));
     }
 
-    public void testSameLanuageSubtypes() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(
-                asList(EN_US_QWERTY.getRawSubtype(), EN_GB_QWERTY.getRawSubtype(),
-                        FR_AZERTY.getRawSubtype(), ZZ_QWERTY.getRawSubtype()));
+    public void testOneSubtypeImplicitlyEnabled() {
+        enableSubtypes(EN_US_QWERTY);
+        assertFormatType(EN_US_QWERTY, true, Locale.US,            FORMAT_TYPE_NONE);
 
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
-        assertEquals("two same English (US)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("two same English (UK)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_GB_QWERTY));
-        assertEquals("two same NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
+        enableSubtypes(EN_GB_QWERTY);
+        assertFormatType(EN_GB_QWERTY, true, Locale.UK,            FORMAT_TYPE_NONE);
 
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false /* isSame */);
-        assertEquals("two diff English (US)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
-        assertEquals("two diff English (UK)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_GB_QWERTY));
-        assertEquals("two diff NoLanguage", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(ZZ_QWERTY));
+        enableSubtypes(FR_AZERTY);
+        assertFormatType(FR_AZERTY,    true, Locale.FRANCE,        FORMAT_TYPE_NONE);
+
+        enableSubtypes(FR_CA_QWERTY);
+        assertFormatType(FR_CA_QWERTY, true, Locale.CANADA_FRENCH, FORMAT_TYPE_NONE);
     }
 
-    public void testMultiSameLanuageSubtypes() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(
-                asList(FR_AZERTY.getRawSubtype(), FR_CA_QWERTY.getRawSubtype(),
-                        FR_CH_SWISS.getRawSubtype(), FR_CH_QWERTY.getRawSubtype(),
-                        FR_CH_QWERTZ.getRawSubtype()));
+    public void testOneSubtypeExplicitlyEnabled() {
+        enableSubtypes(EN_US_QWERTY);
+        assertFormatType(EN_US_QWERTY, false, Locale.UK,     FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
 
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
-        assertEquals("multi same French", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_AZERTY));
-        assertEquals("multi same French (CA)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CA_QWERTY));
-        assertEquals("multi same French (CH)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_SWISS));
-        assertEquals("multi same French (CH) (QWERTY)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_QWERTY));
-        assertEquals("multi same French (CH) (QWERTZ)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_QWERTZ));
+        enableSubtypes(EN_GB_QWERTY);
+        assertFormatType(EN_GB_QWERTY, false, Locale.US,     FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(EN_GB_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
 
-        mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false /* isSame */);
-        assertEquals("multi diff French", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_AZERTY));
-        assertEquals("multi diff French (CA)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CA_QWERTY));
-        assertEquals("multi diff French (CH)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_SWISS));
-        assertEquals("multi diff French (CH) (QWERTY)", FORMAT_TYPE_FULL_LOCALE,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_QWERTY));
-        assertEquals("multi diff French (CH) (QWERTZ)", FORMAT_TYPE_LANGUAGE_ONLY,
-                mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(FR_CH_QWERTZ));
+        enableSubtypes(FR_AZERTY);
+        assertFormatType(FR_AZERTY,    false, Locale.US,            FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_AZERTY,    false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+
+        enableSubtypes(FR_CA_QWERTY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.US,            FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE,        FORMAT_TYPE_LANGUAGE_ONLY);
+    }
+
+    public void testOneSubtypeImplicitlyEnabledWithNoLanguageSubtype() {
+        final Locale Locale_IW = new Locale("iw");
+        enableSubtypes(IW_HEBREW, ZZ_QWERTY);
+        // TODO: Should this be FORMAT_TYPE_NONE?
+        assertFormatType(IW_HEBREW,    true, Locale_IW, FORMAT_TYPE_LANGUAGE_ONLY);
+        // TODO: Should this be FORMAT_TYPE_NONE?
+        assertFormatType(ZZ_QWERTY,    true, Locale_IW, FORMAT_TYPE_FULL_LOCALE);
+    }
+
+    public void testTwoSubtypesExplicitlyEnabled() {
+        enableSubtypes(EN_US_QWERTY, FR_AZERTY);
+        assertFormatType(EN_US_QWERTY, false, Locale.US,     FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_AZERTY,    false, Locale.US,     FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_AZERTY,    false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(EN_US_QWERTY, false, Locale.JAPAN,  FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_AZERTY,    false, Locale.JAPAN,  FORMAT_TYPE_LANGUAGE_ONLY);
+
+        enableSubtypes(EN_US_QWERTY, ZZ_QWERTY);
+        assertFormatType(EN_US_QWERTY, false, Locale.US,     FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(ZZ_QWERTY,    false, Locale.US,     FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(EN_US_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(ZZ_QWERTY,    false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE);
+
+    }
+
+    public void testMultiSubtypeWithSameLanuageAndSameLayout() {
+        // Explicitly enable en_US, en_GB, fr_FR, and no language keyboards.
+        enableSubtypes(EN_US_QWERTY, EN_GB_QWERTY, FR_CA_QWERTY, ZZ_QWERTY);
+
+        assertFormatType(EN_US_QWERTY, false, Locale.US,    FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(EN_GB_QWERTY, false, Locale.US,    FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CA_QWERTY, false, Locale.US,    FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(ZZ_QWERTY,    false, Locale.US,    FORMAT_TYPE_FULL_LOCALE);
+
+        assertFormatType(EN_US_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(EN_GB_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(ZZ_QWERTY,    false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE);
+    }
+
+    public void testMultiSubtypesWithSameLanguageButHaveDifferentLayout() {
+        enableSubtypes(FR_AZERTY, FR_CA_QWERTY, FR_CH_SWISS, FR_CH_QWERTZ);
+
+        assertFormatType(FR_AZERTY,    false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_SWISS,  false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+
+        assertFormatType(FR_AZERTY,    false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_SWISS,  false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+
+        assertFormatType(FR_AZERTY,    false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_SWISS,  false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+    }
+
+    public void testMultiSubtypesWithSameLanguageAndMayHaveSameLayout() {
+        enableSubtypes(FR_AZERTY, FR_CA_QWERTY, FR_CH_SWISS, FR_CH_QWERTY, FR_CH_QWERTZ);
+
+        assertFormatType(FR_AZERTY,    false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_SWISS,  false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTY, false, Locale.FRANCE, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.FRANCE, FORMAT_TYPE_LANGUAGE_ONLY);
+
+        assertFormatType(FR_AZERTY,    false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_SWISS,  false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTY, false, Locale.CANADA_FRENCH, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.CANADA_FRENCH, FORMAT_TYPE_LANGUAGE_ONLY);
+
+        assertFormatType(FR_AZERTY,    false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CA_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_SWISS,  false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
+        assertFormatType(FR_CH_QWERTY, false, Locale.JAPAN, FORMAT_TYPE_FULL_LOCALE);
+        assertFormatType(FR_CH_QWERTZ, false, Locale.JAPAN, FORMAT_TYPE_LANGUAGE_ONLY);
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsBengaliBD.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsBengaliBD.java
index 6262589..2d38c87 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsBengaliBD.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsBengaliBD.java
@@ -46,6 +46,11 @@
         }
 
         @Override
+        public ExpectedKey[] getSpaceKeys(final boolean isPhone) {
+            return joinKeys(LANGUAGE_SWITCH_KEY, SPACE_KEY, key(ZWNJ_KEY, ZWJ_KEY));
+        }
+
+        @Override
         public ExpectedKey getCurrencyKey() { return CURRENCY_RUPEE; }
 
         // U+09F3: "৳" BENGALI RUPEE SIGN
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
index b976448..cff489d 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryDecayingTests.java
@@ -22,6 +22,8 @@
 
 import com.android.inputmethod.latin.NgramContext.WordInfo;
 import com.android.inputmethod.latin.common.CodePointUtils;
+import com.android.inputmethod.latin.common.FileUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
@@ -30,8 +32,6 @@
 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;
 import com.android.inputmethod.latin.utils.WordInputEventForPersonalization;
 
 import java.io.File;
@@ -49,7 +49,7 @@
     private static final String TEST_LOCALE = "test";
     private static final int DUMMY_PROBABILITY = 0;
     private static final int[] DICT_FORMAT_VERSIONS =
-            new int[] { FormatSpec.VERSION4, FormatSpec.VERSION4_DEV };
+            new int[] { FormatSpec.VERSION402, FormatSpec.VERSION403, FormatSpec.VERSION4_DEV };
     private static final String DICTIONARY_ID = "TestDecayingBinaryDictionary";
 
     private int mCurrentTime = 0;
@@ -73,11 +73,11 @@
     }
 
     private static boolean supportsCountBasedNgram(final int formatVersion) {
-        return formatVersion >= FormatSpec.VERSION4_DEV;
+        return formatVersion >= FormatSpec.VERSION403;
     }
 
     private static boolean supportsNgram(final int formatVersion) {
-        return formatVersion >= FormatSpec.VERSION4_DEV;
+        return formatVersion >= FormatSpec.VERSION403;
     }
 
     private void onInputWord(final BinaryDictionary binaryDictionary, final String word,
diff --git a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
index a1ae93c..60d2de1 100644
--- a/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/BinaryDictionaryTests.java
@@ -24,12 +24,12 @@
 import com.android.inputmethod.latin.NgramContext.WordInfo;
 import com.android.inputmethod.latin.common.CodePointUtils;
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.FileUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.WeightedString;
 import com.android.inputmethod.latin.makedict.WordProperty;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.FileUtils;
 
 import java.io.File;
 import java.io.IOException;
@@ -45,19 +45,11 @@
     private static final String TEST_DICT_FILE_EXTENSION = ".testDict";
     private static final String TEST_LOCALE = "test";
     private static final int[] DICT_FORMAT_VERSIONS =
-            new int[] { FormatSpec.VERSION4, FormatSpec.VERSION4_DEV };
+            new int[] { FormatSpec.VERSION402, FormatSpec.VERSION403, FormatSpec.VERSION4_DEV };
     private static final String DICTIONARY_ID = "TestBinaryDictionary";
 
-    private static boolean canCheckBigramProbability(final int formatVersion) {
-        return formatVersion > FormatSpec.VERSION401;
-    }
-
-    private static boolean supportsBeginningOfSentence(final int formatVersion) {
-        return formatVersion > FormatSpec.VERSION401;
-    }
-
     private static boolean supportsNgram(final int formatVersion) {
-        return formatVersion >= FormatSpec.VERSION4_DEV;
+        return formatVersion >= FormatSpec.VERSION403;
     }
 
     private HashSet<File> mDictFilesToBeDeleted = new HashSet<>();
@@ -84,19 +76,13 @@
 
     private File createEmptyDictionaryWithAttributesAndGetFile(final int formatVersion,
             final HashMap<String, String> attributeMap) {
-        if (formatVersion == FormatSpec.VERSION4
-                || formatVersion == FormatSpec.VERSION4_ONLY_FOR_TESTING
-                || formatVersion == FormatSpec.VERSION4_DEV) {
-            try {
-                final File dictFile = createEmptyVer4DictionaryAndGetFile(formatVersion,
-                        attributeMap);
-                mDictFilesToBeDeleted.add(dictFile);
-                return dictFile;
-            } catch (final IOException e) {
-                fail(e.toString());
-            }
-        } else {
-            fail("Dictionary format version " + formatVersion + " is not supported.");
+        try {
+            final File dictFile = createEmptyVer4DictionaryAndGetFile(formatVersion,
+                    attributeMap);
+            mDictFilesToBeDeleted.add(dictFile);
+            return dictFile;
+        } catch (final IOException e) {
+            fail(e.toString());
         }
         return null;
     }
@@ -350,18 +336,14 @@
         assertTrue(isValidBigram(binaryDictionary, "aaa", "bcc"));
         assertTrue(isValidBigram(binaryDictionary, "abb", "aaa"));
         assertTrue(isValidBigram(binaryDictionary, "abb", "bcc"));
-        if (canCheckBigramProbability(formatVersion)) {
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc"));
-        }
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc"));
 
         addBigramWords(binaryDictionary, "aaa", "abb", updatedBigramProbability);
-        if (canCheckBigramProbability(formatVersion)) {
-            assertEquals(updatedBigramProbability,
-                    getBigramProbability(binaryDictionary, "aaa", "abb"));
-        }
+        assertEquals(updatedBigramProbability,
+                getBigramProbability(binaryDictionary, "aaa", "abb"));
 
         assertFalse(isValidBigram(binaryDictionary, "bcc", "aaa"));
         assertFalse(isValidBigram(binaryDictionary, "bcc", "bbc"));
@@ -381,17 +363,12 @@
         addUnigramWord(binaryDictionary, "abc", unigramProbability);
         addUnigramWord(binaryDictionary, "f", unigramProbability);
 
-        if (canCheckBigramProbability(formatVersion)) {
-            assertEquals(bigramProbability,
-                    getBigramProbability(binaryDictionary, "abcde", "fghij"));
-        }
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abcde", "fghij"));
         assertEquals(Dictionary.NOT_A_PROBABILITY,
                 getBigramProbability(binaryDictionary, "abcde", "fgh"));
         addBigramWords(binaryDictionary, "abcde", "fghij", updatedBigramProbability);
-        if (canCheckBigramProbability(formatVersion)) {
-            assertEquals(updatedBigramProbability,
-                    getBigramProbability(binaryDictionary, "abcde", "fghij"));
-        }
+        assertEquals(updatedBigramProbability,
+                getBigramProbability(binaryDictionary, "abcde", "fghij"));
     }
 
     public void testRandomlyAddBigramWords() {
@@ -441,10 +418,8 @@
             final int bigramProbability = bigramProbabilities.get(bigram);
             assertEquals(bigramProbability != Dictionary.NOT_A_PROBABILITY,
                     isValidBigram(binaryDictionary, bigram.first, bigram.second));
-            if (canCheckBigramProbability(formatVersion)) {
-                assertEquals(bigramProbability,
-                        getBigramProbability(binaryDictionary, bigram.first, bigram.second));
-            }
+            assertEquals(bigramProbability,
+                    getBigramProbability(binaryDictionary, bigram.first, bigram.second));
         }
     }
 
@@ -594,12 +569,10 @@
         assertEquals(unigramProbability, binaryDictionary.getFrequency("aaa"));
         assertEquals(unigramProbability, binaryDictionary.getFrequency("abb"));
         assertEquals(unigramProbability, binaryDictionary.getFrequency("bcc"));
-        if (canCheckBigramProbability(formatVersion)) {
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa"));
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc"));
-        }
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "abb"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bcc"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "aaa"));
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "abb", "bcc"));
         assertFalse(isValidBigram(binaryDictionary, "bcc", "aaa"));
         assertFalse(isValidBigram(binaryDictionary, "bcc", "bbc"));
         assertFalse(isValidBigram(binaryDictionary, "aaa", "aaa"));
@@ -661,10 +634,8 @@
             final int bigramProbability = bigramProbabilities.get(bigram);
             assertEquals(bigramProbability != Dictionary.NOT_A_PROBABILITY,
                     isValidBigram(binaryDictionary, bigram.first, bigram.second));
-            if (canCheckBigramProbability(formatVersion)) {
-                assertEquals(bigramProbability,
-                        getBigramProbability(binaryDictionary, bigram.first, bigram.second));
-            }
+            assertEquals(bigramProbability,
+                    getBigramProbability(binaryDictionary, bigram.first, bigram.second));
         }
     }
 
@@ -768,10 +739,8 @@
                     probability = Dictionary.NOT_A_PROBABILITY;
                 }
 
-                if (canCheckBigramProbability(formatVersion)) {
-                    assertEquals(probability,
-                            getBigramProbability(binaryDictionary, bigram.first, bigram.second));
-                }
+                assertEquals(probability,
+                        getBigramProbability(binaryDictionary, bigram.first, bigram.second));
                 assertEquals(probability != Dictionary.NOT_A_PROBABILITY,
                         isValidBigram(binaryDictionary, bigram.first, bigram.second));
             }
@@ -971,10 +940,8 @@
             for (final WeightedString bigramTarget : wordProperty.getBigrams()) {
                 final String word1 = bigramTarget.mWord;
                 assertTrue(bigramWord1s.contains(word1));
-                if (canCheckBigramProbability(formatVersion)) {
-                    final int bigramProbability = bigramProbabilities.get(new Pair<>(word0, word1));
-                    assertEquals(bigramProbability, bigramTarget.getProbability());
-                }
+                final int bigramProbability = bigramProbabilities.get(new Pair<>(word0, word1));
+                assertEquals(bigramProbability, bigramTarget.getProbability());
             }
         }
     }
@@ -1057,10 +1024,8 @@
                     final String word1 = bigramTarget.mWord;
                     assertTrue(bigramWord1s.contains(word1));
                     final Pair<String, String> bigram = new Pair<>(word0, word1);
-                    if (canCheckBigramProbability(formatVersion)) {
-                        final int bigramProbability = bigramProbabilitiesToCheckLater.get(bigram);
-                        assertEquals(bigramProbability, bigramTarget.getProbability());
-                    }
+                    final int bigramProbability = bigramProbabilitiesToCheckLater.get(bigram);
+                    assertEquals(bigramProbability, bigramTarget.getProbability());
                     bigramSet.remove(bigram);
                 }
             }
@@ -1198,7 +1163,7 @@
 
     public void testPossiblyOffensiveAttributeMaintained() {
         final BinaryDictionary binaryDictionary =
-                getEmptyBinaryDictionary(FormatSpec.VERSION4_DEV);
+                getEmptyBinaryDictionary(FormatSpec.VERSION403);
         binaryDictionary.addUnigramEntry("ddd", 100, null, Dictionary.NOT_A_PROBABILITY,
                 false, true, true, 0);
         WordProperty wordProperty = binaryDictionary.getWordProperty("ddd", false);
@@ -1236,11 +1201,9 @@
         assertEquals(toFormatVersion, binaryDictionary.getFormatVersion());
         assertEquals(unigramProbability, binaryDictionary.getFrequency("aaa"));
         assertEquals(unigramProbability, binaryDictionary.getFrequency("bbb"));
-        if (canCheckBigramProbability(toFormatVersion)) {
-            assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bbb"));
-            assertEquals(bigramProbability, binaryDictionary.getNgramProbability(
-                    NgramContext.BEGINNING_OF_SENTENCE, "aaa"));
-        }
+        assertEquals(bigramProbability, getBigramProbability(binaryDictionary, "aaa", "bbb"));
+        assertEquals(bigramProbability, binaryDictionary.getNgramProbability(
+                NgramContext.BEGINNING_OF_SENTENCE, "aaa"));
         assertTrue(isValidBigram(binaryDictionary, "aaa", "bbb"));
         WordProperty wordProperty = binaryDictionary.getWordProperty("ccc",
                 false /* isBeginningOfSentence */);
@@ -1311,10 +1274,8 @@
                 binaryDictionary.getPropertyForGettingStats(BinaryDictionary.UNIGRAM_COUNT_QUERY)));
 
         for (final Pair<String, String> bigram : bigrams) {
-            if (canCheckBigramProbability(toFormatVersion)) {
-                assertEquals((int)bigramProbabilities.get(bigram),
-                        getBigramProbability(binaryDictionary, bigram.first, bigram.second));
-            }
+            assertEquals((int)bigramProbabilities.get(bigram),
+                    getBigramProbability(binaryDictionary, bigram.first, bigram.second));
             assertTrue(isValidBigram(binaryDictionary, bigram.first, bigram.second));
         }
         assertEquals(bigramProbabilities.size(), Integer.parseInt(
@@ -1323,9 +1284,7 @@
 
     public void testBeginningOfSentence() {
         for (final int formatVersion : DICT_FORMAT_VERSIONS) {
-            if (supportsBeginningOfSentence(formatVersion)) {
-                testBeginningOfSentence(formatVersion);
-            }
+            testBeginningOfSentence(formatVersion);
         }
     }
 
diff --git a/tests/src/com/android/inputmethod/latin/InputTestsBase.java b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
index 926a2d3..00e0b52 100644
--- a/tests/src/com/android/inputmethod/latin/InputTestsBase.java
+++ b/tests/src/com/android/inputmethod/latin/InputTestsBase.java
@@ -44,10 +44,10 @@
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.common.Constants;
 import com.android.inputmethod.latin.common.InputPointers;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.common.StringUtils;
 import com.android.inputmethod.latin.settings.DebugSettings;
 import com.android.inputmethod.latin.settings.Settings;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
 import java.util.Locale;
@@ -387,7 +387,7 @@
                 false /* isAuxiliary */,
                 false /* overridesImplicitlyEnabledSubtype */,
                 0 /* id */);
-        SubtypeSwitcher.forceSubtype(subtype);
+        RichInputMethodManager.forceSubtype(subtype);
         mLatinIME.onCurrentInputMethodSubtypeChanged(subtype);
         runMessages();
         mKeyboard = mLatinIME.mKeyboardSwitcher.getKeyboard();
diff --git a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java b/tests/src/com/android/inputmethod/latin/RichInputMethodSubtypeTests.java
similarity index 97%
rename from tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java
rename to tests/src/com/android/inputmethod/latin/RichInputMethodSubtypeTests.java
index 83afd78..aed7d6a 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/RichInputMethodSubtypeTests.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (C) 2011 The Android Open Source Project
+ * 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.
@@ -14,7 +14,7 @@
  * limitations under the License.
  */
 
-package com.android.inputmethod.latin.utils;
+package com.android.inputmethod.latin;
 
 import android.content.Context;
 import android.content.res.Resources;
@@ -26,12 +26,15 @@
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
 import com.android.inputmethod.latin.RichInputMethodSubtype;
+import com.android.inputmethod.latin.utils.AdditionalSubtypeUtils;
+import com.android.inputmethod.latin.utils.RunInLocale;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
 import java.util.ArrayList;
 import java.util.Locale;
 
 @SmallTest
-public class SpacebarLanguageUtilsTests extends AndroidTestCase {
+public class RichInputMethodSubtypeTests extends AndroidTestCase {
     // All input method subtypes of LatinIME.
     private final ArrayList<RichInputMethodSubtype> mSubtypesList = new ArrayList<>();
 
diff --git a/tests/src/com/android/inputmethod/latin/WordComposerTests.java b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
index 20256e6..8ae475f 100644
--- a/tests/src/com/android/inputmethod/latin/WordComposerTests.java
+++ b/tests/src/com/android/inputmethod/latin/WordComposerTests.java
@@ -20,8 +20,8 @@
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.latin.common.Constants;
+import com.android.inputmethod.latin.common.CoordinateUtils;
 import com.android.inputmethod.latin.common.StringUtils;
-import com.android.inputmethod.latin.utils.CoordinateUtils;
 
 /**
  * Unit tests for WordComposer.
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
index a35fa13..d833b97 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderEncoderTests.java
@@ -67,6 +67,8 @@
     private static final SparseArray<List<Integer>> sChainBigrams = new SparseArray<>();
     private static final HashMap<String, List<String>> sShortcuts = new HashMap<>();
 
+    final Random mRandom;
+
     public BinaryDictDecoderEncoderTests() {
         this(System.currentTimeMillis(), DEFAULT_MAX_UNIGRAMS);
     }
@@ -75,10 +77,10 @@
         super();
         BinaryDictionaryUtils.setCurrentTimeForTest(0);
         Log.e(TAG, "Testing dictionary: seed is " + seed);
-        final Random random = new Random(seed);
+        mRandom = new Random(seed);
         sWords.clear();
         sWordsWithVariousCodePoints.clear();
-        generateWords(maxUnigrams, random);
+        generateWords(maxUnigrams, mRandom);
 
         for (int i = 0; i < sWords.size(); ++i) {
             sChainBigrams.put(i, new ArrayList<Integer>());
@@ -96,10 +98,10 @@
 
         sShortcuts.clear();
         for (int i = 0; i < NUM_OF_NODES_HAVING_SHORTCUTS; ++i) {
-            final int from = Math.abs(random.nextInt()) % sWords.size();
+            final int from = Math.abs(mRandom.nextInt()) % sWords.size();
             sShortcuts.put(sWords.get(from), new ArrayList<String>());
             for (int j = 0; j < NUM_OF_SHORTCUTS; ++j) {
-                final int to = Math.abs(random.nextInt()) % sWords.size();
+                final int to = Math.abs(mRandom.nextInt()) % sWords.size();
                 sShortcuts.get(sWords.get(from)).add(sWords.get(to));
             }
         }
@@ -314,14 +316,14 @@
         final String dictVersion = Long.toString(System.currentTimeMillis());
         final String codePointTableAttribute = DictionaryHeader.CODE_POINT_TABLE_KEY;
         final File file = BinaryDictUtils.getDictFile(dictName, dictVersion,
-                BinaryDictUtils.VERSION201_OPTIONS, getContext().getCacheDir());
+                BinaryDictUtils.STATIC_OPTIONS, getContext().getCacheDir());
 
         // Write a test dictionary
         final DictEncoder dictEncoder = new Ver2DictEncoder(file,
                 Ver2DictEncoder.CODE_POINT_TABLE_ON);
         final FormatSpec.FormatOptions formatOptions =
                 new FormatSpec.FormatOptions(
-                        FormatSpec.MINIMUM_SUPPORTED_VERSION_OF_CODE_POINT_TABLE);
+                        FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION);
         final FusionDictionary sourcedict = new FusionDictionary(new PtNodeArray(),
                 BinaryDictUtils.makeDictionaryOptions(dictName, dictVersion, formatOptions));
         addUnigrams(words.size(), sourcedict, words, null /* shortcutMap */);
@@ -359,11 +361,11 @@
         final List<String> results = new ArrayList<>();
 
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER,
-                BinaryDictUtils.VERSION4_OPTIONS_WITHOUT_TIMESTAMP);
+                BinaryDictUtils.DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP);
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_BUFFER,
-                BinaryDictUtils.VERSION4_OPTIONS_WITH_TIMESTAMP);
+                BinaryDictUtils.DYNAMIC_OPTIONS_WITH_TIMESTAMP);
         for (final String result : results) {
             Log.d(TAG, result);
         }
@@ -373,11 +375,11 @@
         final List<String> results = new ArrayList<>();
 
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY,
-                BinaryDictUtils.VERSION4_OPTIONS_WITHOUT_TIMESTAMP);
+                BinaryDictUtils.DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP);
         runReadAndWriteTests(results, BinaryDictUtils.USE_BYTE_ARRAY,
-                BinaryDictUtils.VERSION4_OPTIONS_WITH_TIMESTAMP);
+                BinaryDictUtils.DYNAMIC_OPTIONS_WITH_TIMESTAMP);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -501,7 +503,7 @@
         final ArrayList<String> results = new ArrayList<>();
 
         runReadUnigramsAndBigramsTests(results, BinaryDictUtils.USE_BYTE_BUFFER,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -512,7 +514,7 @@
         final ArrayList<String> results = new ArrayList<>();
 
         runReadUnigramsAndBigramsTests(results, BinaryDictUtils.USE_BYTE_ARRAY,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -604,11 +606,10 @@
                 + " : " + outputOptions(bufferType, formatOptions));
 
         // Test a word that isn't contained within the dictionary.
-        final Random random = new Random((int)System.currentTimeMillis());
         final int[] codePointSet = CodePointUtils.generateCodePointSet(DEFAULT_CODE_POINT_SET_SIZE,
-                random);
+                mRandom);
         for (int i = 0; i < 1000; ++i) {
-            final String word = CodePointUtils.generateWord(random, codePointSet);
+            final String word = CodePointUtils.generateWord(mRandom, codePointSet);
             if (sWords.indexOf(word) != -1) continue;
             checkGetTerminalPosition(dictDecoder, word, false);
         }
@@ -623,9 +624,9 @@
         final ArrayList<String> results = new ArrayList<>();
 
         runGetTerminalPositionTests(BinaryDictUtils.USE_BYTE_ARRAY,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
         runGetTerminalPositionTests(BinaryDictUtils.USE_BYTE_BUFFER,
-                BinaryDictUtils.VERSION2_OPTIONS);
+                BinaryDictUtils.STATIC_OPTIONS);
 
         for (final String result : results) {
             Log.d(TAG, result);
@@ -633,7 +634,7 @@
     }
 
     public void testVer2DictGetWordProperty() {
-        final FormatOptions formatOptions = BinaryDictUtils.VERSION2_OPTIONS;
+        final FormatOptions formatOptions = BinaryDictUtils.STATIC_OPTIONS;
         final ArrayList<String> words = sWords;
         final HashMap<String, List<String>> shortcuts = sShortcuts;
         final String dictName = "testGetWordProperty";
@@ -669,7 +670,7 @@
     }
 
     public void testVer2DictIteration() {
-        final FormatOptions formatOptions = BinaryDictUtils.VERSION2_OPTIONS;
+        final FormatOptions formatOptions = BinaryDictUtils.STATIC_OPTIONS;
         final ArrayList<String> words = sWords;
         final HashMap<String, List<String>> shortcuts = sShortcuts;
         final SparseArray<List<Integer>> bigrams = sEmptyBigrams;
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
index 120b96b..be75565 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictDecoderUtils.java
@@ -17,11 +17,16 @@
 package com.android.inputmethod.latin.makedict;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
 import java.io.File;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.nio.ByteBuffer;
 import java.util.HashMap;
+import java.util.LinkedList;
+
+import javax.annotation.Nonnull;
 
 /**
  * Decodes binary files for a FusionDictionary.
@@ -361,6 +366,43 @@
     }
 
     /**
+     * Helper method that brutally decodes a header from a byte array.
+     *
+     * @param headerBuffer a buffer containing the bytes of the header.
+     * @return a hashmap of the attributes stored in the header
+     */
+    @Nonnull
+    public static HashMap<String, String> decodeHeaderAttributes(@Nonnull final byte[] headerBuffer)
+            throws UnsupportedFormatException {
+        final StringBuilder sb = new StringBuilder();
+        final LinkedList<String> keyValues = new LinkedList<>();
+        int index = 0;
+        while (index < headerBuffer.length) {
+            if (headerBuffer[index] == FormatSpec.PTNODE_CHARACTERS_TERMINATOR) {
+                keyValues.add(sb.toString());
+                sb.setLength(0);
+            } else if (CharEncoding.fitsOnOneByte(headerBuffer[index] & 0xFF,
+                    null /* codePointTable */)) {
+                sb.appendCodePoint(headerBuffer[index] & 0xFF);
+            } else {
+                sb.appendCodePoint(((headerBuffer[index] & 0xFF) << 16)
+                        + ((headerBuffer[index + 1] & 0xFF) << 8)
+                        + (headerBuffer[index + 2] & 0xFF));
+                index += 2;
+            }
+            index += 1;
+        }
+        if ((keyValues.size() & 1) != 0) {
+            throw new UnsupportedFormatException("Odd number of attributes");
+        }
+        final HashMap<String, String> attributes = new HashMap<>();
+        for (int i = 0; i < keyValues.size(); i += 2) {
+            attributes.put(keyValues.get(i), keyValues.get(i + 1));
+        }
+        return attributes;
+    }
+
+    /**
      * Helper method to pass a file name instead of a File object to isBinaryDictionary.
      */
     public static boolean isBinaryDictionary(final String filename) {
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
index 60e3825..ce905c4 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictEncoderUtils.java
@@ -819,12 +819,18 @@
             final ArrayList<Entry<Integer, Integer>> codePointOccurrenceArray)
                     throws IOException, UnsupportedFormatException {
         final int version = formatOptions.mVersion;
-        if (version < FormatSpec.MINIMUM_SUPPORTED_VERSION
-                || version > FormatSpec.MAXIMUM_SUPPORTED_VERSION) {
+        if ((version >= FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION &&
+                version <= FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION) || (
+                version >= FormatSpec.MINIMUM_SUPPORTED_DYNAMIC_VERSION &&
+                version <= FormatSpec.MAXIMUM_SUPPORTED_DYNAMIC_VERSION)) {
+            // Dictionary is valid
+        } else {
             throw new UnsupportedFormatException("Requested file format version " + version
-                    + ", but this implementation only supports versions "
-                    + FormatSpec.MINIMUM_SUPPORTED_VERSION + " through "
-                    + FormatSpec.MAXIMUM_SUPPORTED_VERSION);
+                    + ", but this implementation only supports static versions "
+                    + FormatSpec.MINIMUM_SUPPORTED_STATIC_VERSION + " through "
+                    + FormatSpec.MAXIMUM_SUPPORTED_STATIC_VERSION + " and dynamic versions "
+                    + FormatSpec.MINIMUM_SUPPORTED_DYNAMIC_VERSION + " through "
+                    + FormatSpec.MAXIMUM_SUPPORTED_DYNAMIC_VERSION);
         }
 
         ByteArrayOutputStream headerBuffer = new ByteArrayOutputStream(256);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
index 8eabf74..9c1e4cf 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/BinaryDictUtils.java
@@ -28,13 +28,11 @@
 
     public static final String TEST_DICT_FILE_EXTENSION = ".testDict";
 
-    public static final FormatSpec.FormatOptions VERSION2_OPTIONS =
-            new FormatSpec.FormatOptions(FormatSpec.VERSION2);
-    public static final FormatSpec.FormatOptions VERSION201_OPTIONS =
-            new FormatSpec.FormatOptions(FormatSpec.VERSION201);
-    public static final FormatSpec.FormatOptions VERSION4_OPTIONS_WITHOUT_TIMESTAMP =
+    public static final FormatSpec.FormatOptions STATIC_OPTIONS =
+            new FormatSpec.FormatOptions(FormatSpec.VERSION202);
+    public static final FormatSpec.FormatOptions DYNAMIC_OPTIONS_WITHOUT_TIMESTAMP =
             new FormatSpec.FormatOptions(FormatSpec.VERSION4, false /* hasTimestamp */);
-    public static final FormatSpec.FormatOptions VERSION4_OPTIONS_WITH_TIMESTAMP =
+    public static final FormatSpec.FormatOptions DYNAMIC_OPTIONS_WITH_TIMESTAMP =
             new FormatSpec.FormatOptions(FormatSpec.VERSION4, true /* hasTimestamp */);
 
     public static DictionaryOptions makeDictionaryOptions(final String id, final String version,
@@ -55,7 +53,8 @@
     public static File getDictFile(final String name, final String version,
             final FormatOptions formatOptions, final File directory) {
         if (formatOptions.mVersion == FormatSpec.VERSION2
-                || formatOptions.mVersion == FormatSpec.VERSION201) {
+                || formatOptions.mVersion == FormatSpec.VERSION201
+                || formatOptions.mVersion == FormatSpec.VERSION202) {
             return new File(directory, name + "." + version + TEST_DICT_FILE_EXTENSION);
         } else if (formatOptions.mVersion == FormatSpec.VERSION4) {
             return new File(directory, name + "." + version);
@@ -71,7 +70,7 @@
                 file.mkdir();
             }
             return new Ver4DictEncoder(file);
-        } else if (formatOptions.mVersion == FormatSpec.VERSION2) {
+        } else if (formatOptions.mVersion == FormatSpec.VERSION202) {
             return new Ver2DictEncoder(file, Ver2DictEncoder.CODE_POINT_TABLE_OFF);
         } else {
             throw new RuntimeException("The format option has a wrong version : "
diff --git a/tests/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java b/tests/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java
index 457e7af..5c261a9 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/Ver2DictDecoder.java
@@ -178,7 +178,8 @@
             throw new IOException("Cannot read the dictionary header.");
         }
         if (header.mFormatOptions.mVersion != FormatSpec.VERSION2 &&
-                header.mFormatOptions.mVersion != FormatSpec.VERSION201) {
+                header.mFormatOptions.mVersion != FormatSpec.VERSION201 &&
+                header.mFormatOptions.mVersion != FormatSpec.VERSION202) {
             throw new UnsupportedFormatException("File header has a wrong version : "
                     + header.mFormatOptions.mVersion);
         }
diff --git a/tests/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java b/tests/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
index 2c2152b..b52b8c4 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/Ver2DictEncoder.java
@@ -124,7 +124,8 @@
     @Override
     public void writeDictionary(final FusionDictionary dict, final FormatOptions formatOptions)
             throws IOException, UnsupportedFormatException {
-        if (formatOptions.mVersion > FormatSpec.VERSION201) {
+        // We no longer support anything but the latest version of v2.
+        if (formatOptions.mVersion != FormatSpec.VERSION202) {
             throw new UnsupportedFormatException(
                     "The given format options has wrong version number : "
                     + formatOptions.mVersion);
diff --git a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
index 7e54ce9..63ea89c 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictDecoder.java
@@ -18,7 +18,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.BinaryDictionary;
-import com.android.inputmethod.latin.utils.FileUtils;
+import com.android.inputmethod.latin.common.FileUtils;
 
 import java.io.File;
 import java.io.FileNotFoundException;
diff --git a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
index 1554219..1e4bd76 100644
--- a/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
+++ b/tests/src/com/android/inputmethod/latin/makedict/Ver4DictEncoder.java
@@ -20,10 +20,10 @@
 import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.NgramContext;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary.PtNode;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
index 778f6e8..a84df28 100644
--- a/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
+++ b/tests/src/com/android/inputmethod/latin/personalization/UserHistoryDictionaryTests.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.personalization;
 
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.LargeTest;
 import android.util.Log;
@@ -23,9 +25,10 @@
 import com.android.inputmethod.latin.ExpandableBinaryDictionary;
 import com.android.inputmethod.latin.NgramContext;
 import com.android.inputmethod.latin.NgramContext.WordInfo;
+import com.android.inputmethod.latin.common.FileUtils;
+import com.android.inputmethod.latin.settings.LocalSettingsConstants;
 import com.android.inputmethod.latin.utils.BinaryDictionaryUtils;
 import com.android.inputmethod.latin.utils.DistracterFilter;
-import com.android.inputmethod.latin.utils.FileUtils;
 
 import java.io.File;
 import java.io.FilenameFilter;
@@ -36,6 +39,8 @@
 import java.util.Random;
 import java.util.concurrent.TimeUnit;
 
+import javax.annotation.Nullable;
+
 /**
  * Unit tests for UserHistoryDictionary
  */
@@ -44,6 +49,7 @@
     private static final String TAG = UserHistoryDictionaryTests.class.getSimpleName();
     private static final int WAIT_FOR_WRITING_FILE_IN_MILLISECONDS = 3000;
     private static final String TEST_LOCALE_PREFIX = "test_";
+    private static final String TEST_ACCOUNT = "account@example.com";
 
     private static final String[] CHARACTERS = {
         "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
@@ -52,15 +58,18 @@
 
     private int mCurrentTime = 0;
 
+    private SharedPreferences mPrefs;
+    private String mLastKnownAccount = null;
+
     private void removeAllTestDictFiles() {
         final Locale dummyLocale = new Locale(TEST_LOCALE_PREFIX);
-        final String dictName = ExpandableBinaryDictionary.getDictName(
-                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
+        final String dictName = UserHistoryDictionary.getUserHistoryDictName(
+                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
         final File dictFile = ExpandableBinaryDictionary.getDictFile(
                 mContext, dictName, null /* dictFile */);
         final FilenameFilter filenameFilter = new FilenameFilter() {
             @Override
-            public boolean accept(File dir, String filename) {
+            public boolean accept(final File dir, final String filename) {
                 return filename.startsWith(UserHistoryDictionary.NAME + "." + TEST_LOCALE_PREFIX);
             }
         };
@@ -99,6 +108,12 @@
     @Override
     protected void setUp() throws Exception {
         super.setUp();
+
+        mPrefs = PreferenceManager.getDefaultSharedPreferences(getContext());
+        // Keep track of the current account so that we restore it when the test finishes.
+        mLastKnownAccount = mPrefs.getString(LocalSettingsConstants.PREF_ACCOUNT_NAME, null);
+        updateAccountName(TEST_ACCOUNT);
+
         resetCurrentTimeForTestMode();
         removeAllTestDictFiles();
     }
@@ -107,6 +122,10 @@
     protected void tearDown() throws Exception {
         removeAllTestDictFiles();
         stopTestModeInNativeCode();
+
+        // Restore the account that was present before running the test.
+        updateAccountName(mLastKnownAccount);
+
         super.tearDown();
     }
 
@@ -115,6 +134,14 @@
         setCurrentTimeForTestMode(mCurrentTime);
     }
 
+    private void updateAccountName(@Nullable final String accountName) {
+        if (accountName == null) {
+            mPrefs.edit().remove(LocalSettingsConstants.PREF_ACCOUNT_NAME).apply();
+        } else {
+            mPrefs.edit().putString(LocalSettingsConstants.PREF_ACCOUNT_NAME, accountName).apply();
+        }
+    }
+
     private void forcePassingShortTime() {
         // 3 days.
         final int timeToElapse = (int)TimeUnit.DAYS.toSeconds(3);
@@ -142,7 +169,7 @@
      */
     private static String generateWord(final int value) {
         final int lengthOfChars = CHARACTERS.length;
-        StringBuilder builder = new StringBuilder();
+        final StringBuilder builder = new StringBuilder();
         long lvalue = Math.abs((long)value);
         while (lvalue > 0) {
             builder.append(CHARACTERS[(int)(lvalue % lengthOfChars)]);
@@ -162,7 +189,7 @@
     private static void addToDict(final UserHistoryDictionary dict, final List<String> words,
             final int timestamp) {
         NgramContext ngramContext = NgramContext.EMPTY_PREV_WORDS_INFO;
-        for (String word : words) {
+        for (final String word : words) {
             UserHistoryDictionary.addToDictionary(dict, ngramContext, word, true, timestamp,
                     DistracterFilter.EMPTY_DISTRACTER_FILTER);
             ngramContext = ngramContext.getNextNgramContext(new WordInfo(word));
@@ -204,12 +231,12 @@
         Log.d(TAG, "This test can be used for profiling.");
         Log.d(TAG, "Usage: please set UserHistoryDictionary.PROFILE_SAVE_RESTORE to true.");
         final Locale dummyLocale = getDummyLocale("random_words");
-        final String dictName = ExpandableBinaryDictionary.getDictName(
-                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
+        final String dictName = UserHistoryDictionary.getUserHistoryDictName(
+                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
         final File dictFile = ExpandableBinaryDictionary.getDictFile(
                 mContext, dictName, null /* dictFile */);
         final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                getContext(), dummyLocale);
+                getContext(), dummyLocale, TEST_ACCOUNT);
 
         final int numberOfWords = 1000;
         final Random random = new Random(123456);
@@ -232,12 +259,12 @@
             // Create filename suffixes for this test.
             for (int i = 0; i < numberOfLanguages; i++) {
                 final Locale dummyLocale = getDummyLocale("switching_languages" + i);
-                final String dictName = ExpandableBinaryDictionary.getDictName(
-                        UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
+                final String dictName = UserHistoryDictionary.getUserHistoryDictName(
+                        UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
                 dictFiles[i] = ExpandableBinaryDictionary.getDictFile(
                         mContext, dictName, null /* dictFile */);
                 dicts[i] = PersonalizationHelper.getUserHistoryDictionary(getContext(),
-                        dummyLocale);
+                        dummyLocale, TEST_ACCOUNT);
                 clearHistory(dicts[i]);
             }
 
@@ -262,14 +289,14 @@
 
     public void testAddManyWords() {
         final Locale dummyLocale = getDummyLocale("many_random_words");
-        final String dictName = ExpandableBinaryDictionary.getDictName(
-                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */);
+        final String dictName = UserHistoryDictionary.getUserHistoryDictName(
+                UserHistoryDictionary.NAME, dummyLocale, null /* dictFile */, getContext());
         final File dictFile = ExpandableBinaryDictionary.getDictFile(
                 mContext, dictName, null /* dictFile */);
         final int numberOfWords = 10000;
         final Random random = new Random(123456);
         final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                getContext(), dummyLocale);
+                getContext(), dummyLocale, TEST_ACCOUNT);
         clearHistory(dict);
         try {
             addAndWriteRandomWords(dict, numberOfWords, random, true /* checksContents */);
@@ -281,7 +308,7 @@
     public void testDecaying() {
         final Locale dummyLocale = getDummyLocale("decaying");
         final UserHistoryDictionary dict = PersonalizationHelper.getUserHistoryDictionary(
-                getContext(), dummyLocale);
+                getContext(), dummyLocale, TEST_ACCOUNT);
         final int numberOfWords = 5000;
         final Random random = new Random(123456);
         resetCurrentTimeForTestMode();
@@ -309,4 +336,9 @@
             assertFalse(dict.isInDictionary(word));
         }
     }
-}
+
+    public void testRandomWords_NullAccount() {
+        updateAccountName(null);
+        testRandomWords();
+    }
+}
\ No newline at end of file
diff --git a/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java
index 131865a..f50b8e0 100644
--- a/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/BinaryDictionaryUtilsTests.java
@@ -20,6 +20,8 @@
 import android.test.suitebuilder.annotation.LargeTest;
 
 import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.common.FileUtils;
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
 import com.android.inputmethod.latin.makedict.FormatSpec;
 
diff --git a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
index 4646a82..9680d85 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CapsModeUtilsTests.java
@@ -21,8 +21,8 @@
 import android.test.suitebuilder.annotation.SmallTest;
 import android.text.TextUtils;
 
+import com.android.inputmethod.latin.common.LocaleUtils;
 import com.android.inputmethod.latin.settings.SpacingAndPunctuations;
-import com.android.inputmethod.latin.utils.LocaleUtils;
 
 import java.util.Locale;
 
diff --git a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
index a5979c3..47fd5fe 100644
--- a/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/CollectionUtilsTests.java
@@ -19,6 +19,8 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.inputmethod.latin.common.CollectionUtils;
+
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -29,14 +31,45 @@
 @SmallTest
 public class CollectionUtilsTests extends AndroidTestCase {
     /**
+     * Tests that {@link CollectionUtils#arrayAsList(Object[],int,int)} fails as expected
+     * with some invalid inputs.
+     */
+    public void testArrayAsListFailure() {
+        final String[] array = { "0", "1" };
+        // Negative start
+        try {
+            CollectionUtils.arrayAsList(array, -1, 1);
+            fail("Failed to catch start < 0");
+        } catch (final IllegalArgumentException e) {
+            assertEquals("Invalid start: -1 end: 1 with array.length: 2", e.getMessage());
+        }
+        // start > end
+        try {
+            CollectionUtils.arrayAsList(array, 1, -1);
+            fail("Failed to catch start > end");
+        } catch (final IllegalArgumentException e) {
+            assertEquals("Invalid start: 1 end: -1 with array.length: 2", e.getMessage());
+        }
+        // end > array.length
+        try {
+            CollectionUtils.arrayAsList(array, 1, 3);
+            fail("Failed to catch end > array.length");
+        } catch (final IllegalArgumentException e) {
+            assertEquals("Invalid start: 1 end: 3 with array.length: 2", e.getMessage());
+        }
+    }
+
+    /**
      * Tests that {@link CollectionUtils#arrayAsList(Object[],int,int)} gives the expected
      * results for a few valid inputs.
      */
     public void testArrayAsList() {
-        final String[] array = { "0", "1", "2", "3", "4" };
         final ArrayList<String> empty = new ArrayList<>();
+        assertEquals(empty, CollectionUtils.arrayAsList(new String[] { }, 0, 0));
+        final String[] array = { "0", "1", "2", "3", "4" };
         assertEquals(empty, CollectionUtils.arrayAsList(array, 0, 0));
         assertEquals(empty, CollectionUtils.arrayAsList(array, 1, 1));
+        assertEquals(empty, CollectionUtils.arrayAsList(array, array.length, array.length));
         final ArrayList<String> expected123 = new ArrayList<>(Arrays.asList("1", "2", "3"));
         assertEquals(expected123, CollectionUtils.arrayAsList(array, 1, 4));
     }
diff --git a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
index 54f478f..03dcdfc 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
@@ -434,8 +434,8 @@
     // locale layout         |  display name
     // ------ -------------- - ----------------------
     //  sr    south_slavic   F  Српски
-    //  sr_ZZ serbian_qwertz F  српски (латиница)
-    //  sr_ZZ qwerty         T  српски (QWERTY)
+    //  sr_ZZ serbian_qwertz F  Српски (латиница)
+    //  sr_ZZ qwerty         T  Српски (QWERTY)
 
     public void testSerbianLatinSubtypesInSerbianSystemLocale() {
         final RunInLocale<Void> tests = new RunInLocale<Void>() {
@@ -445,12 +445,10 @@
                         SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR));
                 // These are preliminary subtypes and may not exist.
                 if (SR_LATN != null) {
-                    // TODO: Uncommented because of the current translation of these strings
-                    // in Seriban are described in Latin script.
-//                    assertEquals("sr_ZZ", "српски (латиница)",
-//                            SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN));
-//                    assertEquals("sr_ZZ", "српски (QWERTY)",
-//                            SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN_QWERTY));
+                    assertEquals("sr_ZZ", "Српски (латиница)",
+                            SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN));
+                    assertEquals("sr_ZZ", "Српски (QWERTY)",
+                            SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(SR_LATN_QWERTY));
                 }
                 return null;
             }
diff --git a/tools/dicttool/Android.mk b/tools/dicttool/Android.mk
index 81c0706..1a9f029 100644
--- a/tools/dicttool/Android.mk
+++ b/tools/dicttool/Android.mk
@@ -42,23 +42,15 @@
 # a significant part of the dependencies are mocked in the compat/ directory, with empty or
 # nearly-empty implementations, for parts that we don't use in Dicttool.
 LATINIME_SRC_FILES_FOR_DICTTOOL := \
-        event/Combiner.java \
-        event/Event.java \
         latin/BinaryDictionary.java \
         latin/DicTraverseSession.java \
         latin/Dictionary.java \
-        latin/LastComposedWord.java \
         latin/NgramContext.java \
         latin/SuggestedWords.java \
-        latin/WordComposer.java \
-        latin/settings/NativeSuggestOptions.java \
         latin/settings/SettingsValuesForSuggestion.java \
         latin/utils/BinaryDictionaryUtils.java \
         latin/utils/CombinedFormatUtils.java \
-        latin/utils/CoordinateUtils.java \
-        latin/utils/FileUtils.java \
-        latin/utils/JniUtils.java \
-        latin/utils/LocaleUtils.java
+        latin/utils/JniUtils.java
 
 LATINIME_OVERRIDABLE_SRC_FILES_FOR_DICTTOOL := \
         latin/define/DebugFlags.java
diff --git a/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java b/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java
deleted file mode 100644
index c4457a1..0000000
--- a/tools/dicttool/compat/com/android/inputmethod/event/CombinerChain.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * 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;
-
-import java.util.ArrayList;
-
-/**
- * Compatibility class that stands in for the combiner chain in LatinIME.
- *
- * This is not used by dicttool, it's just needed by the dependency chain.
- */
-// TODO: there should not be a dependency to this in dicttool, so there
-// should be a sensible way to separate them cleanly.
-public class CombinerChain {
-    private StringBuilder mComposingWord;
-    public CombinerChain(final String initialText, final Combiner... combinerList) {
-        mComposingWord = new StringBuilder(initialText);
-    }
-
-    public Event processEvent(final ArrayList<Event> previousEvents, final Event newEvent) {
-        return newEvent;
-    }
-
-    public void applyProcessedEvent(final Event event) {
-        mComposingWord.append(event.getTextToCommit());
-    }
-
-    public CharSequence getComposingWordWithCombiningFeedback() {
-        return mComposingWord;
-    }
-
-    public void reset() {
-        mComposingWord.setLength(0);
-    }
-
-    public static Combiner[] createCombiners(final String spec) {
-        // Dicttool never uses a combiner at all, so we just return a zero-sized array.
-        return new Combiner[0];
-    }
-}
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
index 1c5dfa9..84c3956 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtils.java
@@ -19,6 +19,10 @@
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderUtils;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
+import com.android.inputmethod.latin.makedict.DictionaryHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec;
+import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
+import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
 import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 
@@ -27,12 +31,18 @@
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
+import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+import javax.annotation.Nonnull;
+import javax.annotation.Nullable;
 
 /**
  * Class grouping utilities for offline dictionary making.
@@ -44,26 +54,27 @@
     // Prefix and suffix are arbitrary, the values do not really matter
     private final static String PREFIX = "dicttool";
     private final static String SUFFIX = ".tmp";
-
     private final static int COPY_BUFFER_SIZE = 8192;
 
-    public static class DecoderChainSpec {
+    public static class DecoderChainSpec<T> {
         public final static int COMPRESSION = 1;
         public final static int ENCRYPTION = 2;
-        private final static int MAX_DECODE_DEPTH = 4;
 
-        final int[] mDecoderSpec;
-        File mFile;
+        private final static int[][] VALID_DECODER_CHAINS = {
+            { }, { COMPRESSION }, { ENCRYPTION, COMPRESSION }
+        };
+
+        private final int mDecoderSpecIndex;
+        public T mResult;
 
         public DecoderChainSpec() {
-            mDecoderSpec = new int[0];
-            mFile = null;
+            mDecoderSpecIndex = 0;
+            mResult = null;
         }
 
-        public DecoderChainSpec(final DecoderChainSpec src, final int newStep) {
-            mDecoderSpec = Arrays.copyOf(src.mDecoderSpec, src.mDecoderSpec.length + 1);
-            mDecoderSpec[src.mDecoderSpec.length] = newStep;
-            mFile = src.mFile;
+        private DecoderChainSpec(final DecoderChainSpec<T> src) {
+            mDecoderSpecIndex = src.mDecoderSpecIndex + 1;
+            mResult = src.mResult;
         }
 
         private String getStepDescription(final int step) {
@@ -79,12 +90,122 @@
 
         public String describeChain() {
             final StringBuilder s = new StringBuilder("raw");
-            for (final int step : mDecoderSpec) {
+            for (final int step : VALID_DECODER_CHAINS[mDecoderSpecIndex]) {
                 s.append(" > ");
                 s.append(getStepDescription(step));
             }
             return s.toString();
         }
+
+        /**
+         * Returns the next sequential spec. If exhausted, return null.
+         */
+        public DecoderChainSpec next() {
+            if (mDecoderSpecIndex + 1 >= VALID_DECODER_CHAINS.length) {
+                return null;
+            }
+            return new DecoderChainSpec(this);
+        }
+
+        public InputStream getStream(final File src) throws FileNotFoundException, IOException {
+            InputStream input = new BufferedInputStream(new FileInputStream(src));
+            for (final int step : VALID_DECODER_CHAINS[mDecoderSpecIndex]) {
+                switch (step) {
+                case COMPRESSION:
+                    input = Compress.getUncompressedStream(input);
+                    break;
+                case ENCRYPTION:
+                    input = Crypt.getDecryptedStream(input);
+                    break;
+                }
+            }
+            return input;
+        }
+    }
+
+    public interface InputProcessor<T> {
+        @Nonnull
+        public T process(@Nonnull final InputStream input)
+                throws IOException, UnsupportedFormatException;
+    }
+
+    public static class CopyProcessor implements InputProcessor<File> {
+        @Override @Nonnull
+        public File process(@Nonnull final InputStream input) throws IOException,
+                UnsupportedFormatException {
+            final File dst = File.createTempFile(PREFIX, SUFFIX);
+            dst.deleteOnExit();
+            try (final OutputStream output = new BufferedOutputStream(new FileOutputStream(dst))) {
+                copy(input, output);
+                output.flush();
+                output.close();
+                if (BinaryDictDecoderUtils.isBinaryDictionary(dst)
+                        || CombinedInputOutput.isCombinedDictionary(dst.getAbsolutePath())) {
+                    return dst;
+                }
+            }
+            throw new UnsupportedFormatException("Input stream not at the expected format");
+        }
+    }
+
+    public static class HeaderReaderProcessor implements InputProcessor<DictionaryHeader> {
+        // Arbitrarily limit the header length to 32k. Sounds like it would never be larger
+        // than this. Revisit this if needed later.
+        private final int MAX_HEADER_LENGTH = 32 * 1024;
+        @Override @Nonnull
+        public DictionaryHeader process(final InputStream input) throws IOException,
+                UnsupportedFormatException {
+            // Do everything as curtly and ad-hoc as possible for performance.
+            final byte[] tmpBuffer = new byte[12];
+            if (tmpBuffer.length != input.read(tmpBuffer)) {
+                throw new UnsupportedFormatException("File too short, not a dictionary");
+            }
+            // Ad-hoc check for the magic number. See FormatSpec.java as well as
+            // byte_array_utils.h and BinaryDictEncoderUtils#writeDictionaryHeader().
+            final int MAGIC_NUMBER_START_OFFSET = 0;
+            final int VERSION_START_OFFSET = 4;
+            final int HEADER_SIZE_OFFSET = 8;
+            final int magicNumber = ((tmpBuffer[MAGIC_NUMBER_START_OFFSET] & 0xFF) << 24)
+                    + ((tmpBuffer[MAGIC_NUMBER_START_OFFSET + 1] & 0xFF) << 16)
+                    + ((tmpBuffer[MAGIC_NUMBER_START_OFFSET + 2] & 0xFF) << 8)
+                    + (tmpBuffer[MAGIC_NUMBER_START_OFFSET + 3] & 0xFF);
+            if (magicNumber != FormatSpec.MAGIC_NUMBER) {
+                throw new UnsupportedFormatException("Wrong magic number");
+            }
+            final int version = ((tmpBuffer[VERSION_START_OFFSET] & 0xFF) << 8)
+                    + (tmpBuffer[VERSION_START_OFFSET + 1] & 0xFF);
+            if (version != FormatSpec.VERSION2 && version != FormatSpec.VERSION201
+                    && version != FormatSpec.VERSION202) {
+                throw new UnsupportedFormatException("Only versions 2, 201, 202 are supported");
+            }
+            final int totalHeaderSize = ((tmpBuffer[HEADER_SIZE_OFFSET] & 0xFF) << 24)
+                    + ((tmpBuffer[HEADER_SIZE_OFFSET + 1] & 0xFF) << 16)
+                    + ((tmpBuffer[HEADER_SIZE_OFFSET + 2] & 0xFF) << 8)
+                    + (tmpBuffer[HEADER_SIZE_OFFSET + 3] & 0xFF);
+            if (totalHeaderSize > MAX_HEADER_LENGTH) {
+                throw new UnsupportedFormatException("Header too large");
+            }
+            final byte[] headerBuffer = new byte[totalHeaderSize - tmpBuffer.length];
+            readStreamExhaustively(input, headerBuffer);
+            final HashMap<String, String> attributes =
+                    BinaryDictDecoderUtils.decodeHeaderAttributes(headerBuffer);
+            return new DictionaryHeader(totalHeaderSize, new DictionaryOptions(attributes),
+                    new FormatOptions(version, false /* hasTimestamp */));
+        }
+    }
+
+    private static void readStreamExhaustively(final InputStream inputStream,
+            final byte[] outBuffer) throws IOException, UnsupportedFormatException {
+        int readBytes = 0;
+        int readBytesLastCycle = -1;
+        while (readBytes != outBuffer.length) {
+            readBytesLastCycle = inputStream.read(outBuffer, readBytes,
+                    outBuffer.length - readBytes);
+            if (readBytesLastCycle == -1)
+                throw new UnsupportedFormatException("File shorter than specified in the header"
+                        + " (expected " + outBuffer.length + ", read " + readBytes + ")");
+            readBytes += readBytesLastCycle;
+        }
     }
 
     public static void copy(final InputStream input, final OutputStream output) throws IOException {
@@ -95,94 +216,51 @@
     }
 
     /**
-     * Returns a decrypted/uncompressed dictionary.
+     * Process a dictionary, decrypting/uncompressing it on the fly as necessary.
      *
-     * This will decrypt/uncompress any number of times as necessary until it finds the
-     * dictionary signature, and copy the decoded file to a temporary place.
-     * If this is not a dictionary, the method returns null.
+     * This will execute the given processor repeatedly with the possible alternatives
+     * for dictionary format until the processor does not throw an exception.
+     * If the processor succeeds for none of the possible formats, the method returns null.
      */
-    public static DecoderChainSpec getRawDictionaryOrNull(final File src) {
-        return getRawDictionaryOrNullInternal(new DecoderChainSpec(), src, 0);
-    }
-
-    private static DecoderChainSpec getRawDictionaryOrNullInternal(
-            final DecoderChainSpec spec, final File src, final int depth) {
-        // Unfortunately the decoding scheme we use can consider any data to be encrypted
-        // and will produce some output, meaning it's not possible to reliably detect encrypted
-        // data. Thus, some non-dictionary files (especially small) ones may successfully decrypt
-        // over and over, ending in a stack overflow. Hence we limit the depth at which we try
-        // decoding the file.
-        if (depth > DecoderChainSpec.MAX_DECODE_DEPTH) {
-            return null;
-        }
-        if (BinaryDictDecoderUtils.isBinaryDictionary(src)
-                || CombinedInputOutput.isCombinedDictionary(src.getAbsolutePath())) {
-            spec.mFile = src;
-            return spec;
-        }
-        // It's not a raw dictionary - try to see if it's compressed.
-        final File uncompressedFile = tryGetUncompressedFile(src);
-        if (null != uncompressedFile) {
-            final DecoderChainSpec newSpec =
-                    getRawDictionaryOrNullInternal(spec, uncompressedFile, depth + 1);
-            if (null == newSpec) return null;
-            return new DecoderChainSpec(newSpec, DecoderChainSpec.COMPRESSION);
-        }
-        // It's not a compressed either - try to see if it's crypted.
-        final File decryptedFile = tryGetDecryptedFile(src);
-        if (null != decryptedFile) {
-            final DecoderChainSpec newSpec =
-                    getRawDictionaryOrNullInternal(spec, decryptedFile, depth + 1);
-            if (null == newSpec) return null;
-            return new DecoderChainSpec(newSpec, DecoderChainSpec.ENCRYPTION);
+    @Nullable
+    public static <T> DecoderChainSpec<T> decodeDictionaryForProcess(@Nonnull final File src,
+            @Nonnull final InputProcessor<T> processor) {
+        @Nonnull DecoderChainSpec spec = new DecoderChainSpec();
+        while (null != spec) {
+            try {
+                final InputStream input = spec.getStream(src);
+                spec.mResult = processor.process(input);
+                try {
+                    input.close();
+                } catch (IOException e) {
+                    // CipherInputStream doesn't like being closed without having read the
+                    // entire stream, for some reason. But we don't want to because it's a waste
+                    // of resources. We really, really don't care about this.
+                    // However on close() CipherInputStream does throw this exception, wrapped
+                    // in an IOException so we need to catch it.
+                    if (!(e.getCause() instanceof javax.crypto.BadPaddingException)) {
+                        throw e;
+                    }
+                }
+                return spec;
+            } catch (IOException | UnsupportedFormatException | ArrayIndexOutOfBoundsException e) {
+                // If the format is not the right one for this file, the processor will throw one
+                // of these exceptions. In our case, that means we should try the next spec,
+                // since it may still be at another format we haven't tried yet.
+                // TODO: stop using exceptions for this non-exceptional case.
+            }
+            spec = spec.next();
         }
         return null;
     }
 
-    /* Try to uncompress the file passed as an argument.
-     *
-     * If the file can be uncompressed, the uncompressed version is returned. Otherwise, null
-     * is returned.
+    /**
+     * Get a decoder chain spec with a raw dictionary file. This makes a new file on the
+     * disk ready for any treatment the client wants.
      */
-    private static File tryGetUncompressedFile(final File src) {
-        try {
-            final File dst = File.createTempFile(PREFIX, SUFFIX);
-            dst.deleteOnExit();
-            try (
-                final InputStream input = Compress.getUncompressedStream(
-                        new BufferedInputStream(new FileInputStream(src)));
-                final OutputStream output = new BufferedOutputStream(new FileOutputStream(dst))
-            ) {
-                copy(input, output);
-                return dst;
-            }
-        } catch (final IOException e) {
-            // Could not uncompress the file: presumably the file is simply not a compressed file
-            return null;
-        }
-    }
-
-    /* Try to decrypt the file passed as an argument.
-     *
-     * If the file can be decrypted, the decrypted version is returned. Otherwise, null
-     * is returned.
-     */
-    private static File tryGetDecryptedFile(final File src) {
-        try {
-            final File dst = File.createTempFile(PREFIX, SUFFIX);
-            dst.deleteOnExit();
-            try (
-                final InputStream input = Crypt.getDecryptedStream(
-                        new BufferedInputStream(new FileInputStream(src)));
-                final OutputStream output = new BufferedOutputStream(new FileOutputStream(dst))
-            ) {
-                copy(input, output);
-                return dst;
-            }
-        } catch (final IOException e) {
-            // Could not decrypt the file: presumably the file is simply not a crypted file
-            return null;
-        }
+    @Nullable
+    public static DecoderChainSpec<File> getRawDictionaryOrNull(@Nonnull final File src) {
+        return decodeDictionaryForProcess(src, new CopyProcessor());
     }
 
     static FusionDictionary getDictionary(final String filename, final boolean report) {
@@ -192,28 +270,28 @@
             System.out.println("Size : " + file.length() + " bytes");
         }
         try {
-            final DecoderChainSpec decodedSpec = getRawDictionaryOrNull(file);
+            final DecoderChainSpec<File> decodedSpec = getRawDictionaryOrNull(file);
             if (null == decodedSpec) {
                 throw new RuntimeException("Does not seem to be a dictionary file " + filename);
             }
-            if (CombinedInputOutput.isCombinedDictionary(decodedSpec.mFile.getAbsolutePath())) {
+            if (CombinedInputOutput.isCombinedDictionary(decodedSpec.mResult.getAbsolutePath())) {
                 if (report) {
                     System.out.println("Format : Combined format");
                     System.out.println("Packaging : " + decodedSpec.describeChain());
-                    System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
+                    System.out.println("Uncompressed size : " + decodedSpec.mResult.length());
                 }
                 try (final BufferedReader reader = new BufferedReader(
-                        new InputStreamReader(new FileInputStream(decodedSpec.mFile), "UTF-8"))) {
+                        new InputStreamReader(new FileInputStream(decodedSpec.mResult), "UTF-8"))) {
                     return CombinedInputOutput.readDictionaryCombined(reader);
                 }
             }
             final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(
-                    decodedSpec.mFile, 0, decodedSpec.mFile.length(),
+                    decodedSpec.mResult, 0, decodedSpec.mResult.length(),
                     DictDecoder.USE_BYTEARRAY);
             if (report) {
                 System.out.println("Format : Binary dictionary format");
                 System.out.println("Packaging : " + decodedSpec.describeChain());
-                System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
+                System.out.println("Uncompressed size : " + decodedSpec.mResult.length());
             }
             return dictDecoder.readDictionaryBinary(false /* deleteDictIfBroken */);
         } catch (final IOException | UnsupportedFormatException e) {
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
index 48d2e59..955c572 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CombinedInputOutput.java
@@ -98,6 +98,7 @@
         String word = null;
         ProbabilityInfo probabilityInfo = new ProbabilityInfo(0);
         boolean isNotAWord = false;
+        boolean isPossiblyOffensive = false;
         ArrayList<WeightedString> bigrams = new ArrayList<>();
         ArrayList<WeightedString> shortcuts = new ArrayList<>();
         while (null != (line = reader.readLine())) {
@@ -106,7 +107,7 @@
             if (args[0].matches(CombinedFormatUtils.WORD_TAG + "=.*")) {
                 if (null != word) {
                     dict.add(word, probabilityInfo, shortcuts.isEmpty() ? null : shortcuts,
-                            isNotAWord, false /* isPossiblyOffensive */);
+                            isNotAWord, isPossiblyOffensive);
                     for (WeightedString s : bigrams) {
                         dict.setBigram(word, s.mWord, s.mProbabilityInfo);
                     }
@@ -114,27 +115,37 @@
                 if (!shortcuts.isEmpty()) shortcuts = new ArrayList<>();
                 if (!bigrams.isEmpty()) bigrams = new ArrayList<>();
                 isNotAWord = false;
+                isPossiblyOffensive = false;
                 for (String param : args) {
                     final String params[] = param.split("=", 2);
                     if (2 != params.length) throw new RuntimeException("Wrong format : " + line);
-                    if (CombinedFormatUtils.WORD_TAG.equals(params[0])) {
-                        word = params[1];
-                    } else if (CombinedFormatUtils.PROBABILITY_TAG.equals(params[0])) {
-                        probabilityInfo = new ProbabilityInfo(Integer.parseInt(params[1]),
-                                probabilityInfo.mTimestamp, probabilityInfo.mLevel,
-                                probabilityInfo.mCount);
-                    } else if (CombinedFormatUtils.HISTORICAL_INFO_TAG.equals(params[0])) {
-                        final String[] historicalInfoParams =
-                                params[1].split(CombinedFormatUtils.HISTORICAL_INFO_SEPARATOR);
-                        if (historicalInfoParams.length != HISTORICAL_INFO_ELEMENT_COUNT) {
-                            throw new RuntimeException("Wrong format (historical info) : " + line);
-                        }
-                        probabilityInfo = new ProbabilityInfo(probabilityInfo.mProbability,
-                                Integer.parseInt(historicalInfoParams[0]),
-                                Integer.parseInt(historicalInfoParams[1]),
-                                Integer.parseInt(historicalInfoParams[2]));
-                    } else if (CombinedFormatUtils.NOT_A_WORD_TAG.equals(params[0])) {
-                        isNotAWord = "true".equals(params[1]);
+                    switch (params[0]) {
+                        case CombinedFormatUtils.WORD_TAG:
+                            word = params[1];
+                            break;
+                        case CombinedFormatUtils.PROBABILITY_TAG:
+                            probabilityInfo = new ProbabilityInfo(Integer.parseInt(params[1]),
+                                    probabilityInfo.mTimestamp, probabilityInfo.mLevel,
+                                    probabilityInfo.mCount);
+                            break;
+                        case CombinedFormatUtils.HISTORICAL_INFO_TAG:
+                            final String[] historicalInfoParams = params[1].split(
+                                    CombinedFormatUtils.HISTORICAL_INFO_SEPARATOR);
+                            if (historicalInfoParams.length != HISTORICAL_INFO_ELEMENT_COUNT) {
+                                throw new RuntimeException("Wrong format (historical info) : "
+                                        + line);
+                            }
+                            probabilityInfo = new ProbabilityInfo(probabilityInfo.mProbability,
+                                    Integer.parseInt(historicalInfoParams[0]),
+                                    Integer.parseInt(historicalInfoParams[1]),
+                                    Integer.parseInt(historicalInfoParams[2]));
+                            break;
+                        case CombinedFormatUtils.NOT_A_WORD_TAG:
+                            isNotAWord = CombinedFormatUtils.isLiteralTrue(params[1]);
+                            break;
+                        case CombinedFormatUtils.POSSIBLY_OFFENSIVE_TAG:
+                            isPossiblyOffensive = CombinedFormatUtils.isLiteralTrue(params[1]);
+                            break;
                     }
                 }
             } else if (args[0].matches(CombinedFormatUtils.SHORTCUT_TAG + "=.*")) {
@@ -190,7 +201,7 @@
         }
         if (null != word) {
             dict.add(word, probabilityInfo, shortcuts.isEmpty() ? null : shortcuts, isNotAWord,
-                    false /* isPossiblyOffensive */);
+                    isPossiblyOffensive);
             for (WeightedString s : bigrams) {
                 dict.setBigram(word, s.mWord, s.mProbabilityInfo);
             }
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CommandList.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CommandList.java
index 0d93c7f..8fdf763 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CommandList.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/CommandList.java
@@ -18,7 +18,9 @@
 
 public class CommandList {
     public static void populate() {
+        // TODO: Move some commands to native code.
         Dicttool.addCommand("info", Info.class);
+        Dicttool.addCommand("header", Header.class);
         Dicttool.addCommand("diff", Diff.class);
         Dicttool.addCommand("compress", Compress.Compressor.class);
         Dicttool.addCommand("uncompress", Compress.Uncompressor.class);
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
index 8f9e4a3..6187853 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/DictionaryMaker.java
@@ -120,7 +120,7 @@
             String inputCombined = null;
             String outputBinary = null;
             String outputCombined = null;
-            int outputBinaryFormatVersion = FormatSpec.VERSION201; // the default version is 201.
+            int outputBinaryFormatVersion = FormatSpec.VERSION202; // the default version is 202.
             // Don't use code point table by default.
             int codePointTableMode = Ver2DictEncoder.CODE_POINT_TABLE_OFF;
 
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Header.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Header.java
new file mode 100644
index 0000000..ba96c0a
--- /dev/null
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Header.java
@@ -0,0 +1,70 @@
+/**
+ * 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.dicttool;
+
+import com.android.inputmethod.latin.BinaryDictionary;
+import com.android.inputmethod.latin.dicttool.BinaryDictOffdeviceUtils.DecoderChainSpec;
+import com.android.inputmethod.latin.makedict.DictionaryHeader;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.util.Arrays;
+import java.util.Locale;
+
+public class Header extends Dicttool.Command {
+    public static final String COMMAND = "header";
+
+    public Header() {
+    }
+
+    @Override
+    public String getHelp() {
+        return COMMAND + " <filename>: prints the header contents of a dictionary file";
+    }
+
+    @Override
+    public void run() throws UnsupportedFormatException {
+        final boolean plumbing;
+        if (mArgs.length > 0 && "-p".equals(mArgs[0])) {
+            plumbing = true;
+            mArgs = Arrays.copyOfRange(mArgs, 1, mArgs.length);
+        } else {
+            plumbing = false;
+        }
+        if (mArgs.length < 1) {
+            throw new RuntimeException("Not enough arguments for command " + COMMAND);
+        }
+        final String filename = mArgs[0];
+        final File dictFile = new File(filename);
+        final DecoderChainSpec<DictionaryHeader> spec =
+                BinaryDictOffdeviceUtils.decodeDictionaryForProcess(dictFile,
+                        new BinaryDictOffdeviceUtils.HeaderReaderProcessor());
+        if (null == spec) {
+            throw new UnsupportedFormatException(filename
+                    + " doesn't seem to be a valid version 2 dictionary file");
+        }
+
+        final DictionaryHeader header = spec.mResult;
+        System.out.println("Dictionary : " + dictFile.getAbsolutePath());
+        System.out.println("Size : " + dictFile.length() + " bytes");
+        System.out.println("Format : Binary dictionary format");
+        System.out.println("Format version : " + header.mFormatOptions.mVersion);
+        System.out.println("Packaging : " + spec.describeChain());
+        System.out.println("Header attributes :");
+        System.out.print(header.mDictionaryOptions.toString(2 /* indentCount */, plumbing));
+    }
+}
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
index 47ea706..3efa10a 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Package.java
@@ -16,6 +16,8 @@
 
 package com.android.inputmethod.latin.dicttool;
 
+import com.android.inputmethod.latin.makedict.DictionaryHeader;
+
 import java.io.BufferedOutputStream;
 import java.io.File;
 import java.io.FileNotFoundException;
@@ -77,16 +79,16 @@
             if (mArgs.length != 2) {
                 throw new RuntimeException("Too many/too few arguments for command " + COMMAND);
             }
-            final BinaryDictOffdeviceUtils.DecoderChainSpec decodedSpec =
-                    BinaryDictOffdeviceUtils.getRawDictionaryOrNull(new File(mArgs[0]));
+            final BinaryDictOffdeviceUtils.DecoderChainSpec<DictionaryHeader> decodedSpec =
+                    BinaryDictOffdeviceUtils.decodeDictionaryForProcess(new File(mArgs[0]),
+                            new BinaryDictOffdeviceUtils.HeaderReaderProcessor());
             if (null == decodedSpec) {
                 System.out.println(mArgs[0] + " does not seem to be a dictionary");
                 return;
             }
             System.out.println("Packaging : " + decodedSpec.describeChain());
-            System.out.println("Uncompressed size : " + decodedSpec.mFile.length());
             try (
-                final InputStream input = getFileInputStream(decodedSpec.mFile);
+                final InputStream input = decodedSpec.getStream(new File(mArgs[0]));
                 final OutputStream output = new BufferedOutputStream(
                         getFileOutputStreamOrStdOut(mArgs[1]))
             ) {
diff --git a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
index b6383d7..e2dd519 100644
--- a/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
+++ b/tools/dicttool/src/com/android/inputmethod/latin/dicttool/Test.java
@@ -16,10 +16,10 @@
 
 package com.android.inputmethod.latin.dicttool;
 
+import com.android.inputmethod.latin.common.FileUtils;
 import com.android.inputmethod.latin.makedict.BinaryDictDecoderEncoderTests;
 import com.android.inputmethod.latin.makedict.BinaryDictEncoderFlattenTreeTests;
 import com.android.inputmethod.latin.makedict.FusionDictionaryTest;
-import com.android.inputmethod.latin.utils.FileUtils;
 
 import java.io.File;
 import java.io.IOException;
diff --git a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
index 6cdbff7..ea9d4cc 100644
--- a/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
+++ b/tools/dicttool/tests/com/android/inputmethod/latin/dicttool/BinaryDictOffdeviceUtilsTests.java
@@ -16,10 +16,17 @@
 
 package com.android.inputmethod.latin.dicttool;
 
+import com.android.inputmethod.latin.common.CodePointUtils;
+import com.android.inputmethod.latin.dicttool.BinaryDictOffdeviceUtils;
+import com.android.inputmethod.latin.dicttool.Compress;
+import com.android.inputmethod.latin.dicttool.Crypt;
+import com.android.inputmethod.latin.dicttool.BinaryDictOffdeviceUtils.DecoderChainSpec;
 import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.BinaryDictUtils;
 import com.android.inputmethod.latin.makedict.DictDecoder;
 import com.android.inputmethod.latin.makedict.DictEncoder;
 import com.android.inputmethod.latin.makedict.DictionaryHeader;
+import com.android.inputmethod.latin.makedict.FormatSpec;
 import com.android.inputmethod.latin.makedict.FormatSpec.DictionaryOptions;
 import com.android.inputmethod.latin.makedict.FormatSpec.FormatOptions;
 import com.android.inputmethod.latin.makedict.FusionDictionary;
@@ -35,13 +42,37 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
+import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.HashSet;
+import java.util.Random;
+import java.util.Set;
 
 /**
  * Unit tests for BinaryDictOffdeviceUtils
  */
 public class BinaryDictOffdeviceUtilsTests extends TestCase {
     private static final int TEST_FREQ = 37; // Some arbitrary value unlikely to happen by chance
+    private static final int CODE_POINT_SET_SIZE = 300;
+    final Random mRandom;
+    private static final ArrayList<String> sWords = new ArrayList<>();
+
+    public BinaryDictOffdeviceUtilsTests(final long seed, final int maxUnigrams) {
+        super();
+        mRandom = new Random(seed);
+        sWords.clear();
+        generateWords(maxUnigrams, mRandom);
+    }
+
+    private static void generateWords(final int maxUnigrams, final Random random) {
+        final int[] codePointSet = CodePointUtils.generateCodePointSet(
+                CODE_POINT_SET_SIZE, random);
+        final Set<String> wordSet = new HashSet<>();
+        while (wordSet.size() < maxUnigrams) {
+            wordSet.add(CodePointUtils.generateWord(random, codePointSet));
+        }
+        sWords.addAll(wordSet);
+    }
 
     public void testGetRawDictWorks() throws IOException, UnsupportedFormatException {
         final String VERSION = "1";
@@ -68,23 +99,17 @@
         final File dst = File.createTempFile("testGetRawDict", ".tmp");
         dst.deleteOnExit();
         try (final OutputStream out = Compress.getCompressedStream(
-                Compress.getCompressedStream(
-                        Compress.getCompressedStream(
-                                new BufferedOutputStream(new FileOutputStream(dst)))))) {
+                new BufferedOutputStream(new FileOutputStream(dst)))) {
             final DictEncoder dictEncoder = new Ver2DictEncoder(out);
-            dictEncoder.writeDictionary(dict, new FormatOptions(2, false));
+            dictEncoder.writeDictionary(dict, new FormatOptions(FormatSpec.VERSION202, false));
         }
 
         // Test for an actually compressed dictionary and its contents
-        final BinaryDictOffdeviceUtils.DecoderChainSpec decodeSpec =
+        final BinaryDictOffdeviceUtils.DecoderChainSpec<File> decodeSpec =
                 BinaryDictOffdeviceUtils.getRawDictionaryOrNull(dst);
-        for (final int step : decodeSpec.mDecoderSpec) {
-            assertEquals("Wrong decode spec",
-                    BinaryDictOffdeviceUtils.DecoderChainSpec.COMPRESSION, step);
-        }
-        assertEquals("Wrong decode spec", 3, decodeSpec.mDecoderSpec.length);
-        final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(decodeSpec.mFile, 0,
-                decodeSpec.mFile.length());
+        assertEquals("Wrong decode spec", "raw > compression", decodeSpec.describeChain());
+        final DictDecoder dictDecoder = BinaryDictIOUtils.getDictDecoder(decodeSpec.mResult, 0,
+                decodeSpec.mResult.length());
         final FusionDictionary resultDict =
                 dictDecoder.readDictionaryBinary(false /* deleteDictIfBroken */);
         assertEquals("Wrong version attribute", VERSION, resultDict.mOptions.mAttributes.get(
@@ -125,4 +150,64 @@
         assertNull("Wrongly identified data file",
                 BinaryDictOffdeviceUtils.getRawDictionaryOrNull(gzDst));
     }
+
+    public void runTestHeaderReaderProcessorWithOneSpec(final boolean compress, final boolean crypt)
+            throws IOException, UnsupportedFormatException {
+        final String dictName = "testHeaderReaderProcessor";
+        final String dictVersion = Long.toString(System.currentTimeMillis());
+        final FormatOptions formatOptions = BinaryDictUtils.STATIC_OPTIONS;
+        final int MAX_NUMBER_OF_OPTIONS_TO_ADD = 5;
+        final HashMap<String, String> options = new HashMap<>();
+        // Required attributes
+        options.put("dictionary", "main:en_US");
+        options.put("locale", "en_US");
+        options.put("version", Integer.toString(mRandom.nextInt()));
+        // Add some random options for test
+        final int numberOfOptionsToAdd = mRandom.nextInt() % (MAX_NUMBER_OF_OPTIONS_TO_ADD + 1);
+        for (int i = 0; i < numberOfOptionsToAdd; ++i) {
+            options.put(sWords.get(2 * i), sWords.get(2 * 1 + 1));
+        }
+        final FusionDictionary dict = new FusionDictionary(new PtNodeArray(),
+                new DictionaryOptions(options));
+
+        for (int i = 0; i < sWords.size(); ++i) {
+            final String word = sWords.get(i);
+            dict.add(word, new ProbabilityInfo(TEST_FREQ), null /* shortcuts */,
+                    false /* isNotAWord */, false /* isPossiblyOffensive */);
+        }
+
+        File file = File.createTempFile(dictName, ".tmp");
+        final DictEncoder dictEncoder = BinaryDictUtils.getDictEncoder(file, formatOptions);
+        dictEncoder.writeDictionary(dict, formatOptions);
+
+        if (compress) {
+            final File rawFile = file;
+            file = File.createTempFile(dictName + ".compress", ".tmp");
+            final Compress.Compressor compressCommand = new Compress.Compressor();
+            compressCommand.setArgs(new String[] { rawFile.getPath(), file.getPath() });
+            compressCommand.run();
+        }
+        if (crypt) {
+            final File rawFile = file;
+            file = File.createTempFile(dictName + ".crypt", ".tmp");
+            final Crypt.Encrypter cryptCommand = new Crypt.Encrypter();
+            cryptCommand.setArgs(new String[] { rawFile.getPath(), file.getPath() });
+            cryptCommand.run();
+        }
+
+        final DecoderChainSpec<DictionaryHeader> spec =
+                BinaryDictOffdeviceUtils.decodeDictionaryForProcess(file,
+                        new BinaryDictOffdeviceUtils.HeaderReaderProcessor());
+        assertNotNull("Can't decode a dictionary we just wrote : " + file, spec);
+        final DictionaryHeader header = spec.mResult;
+        assertEquals("raw" + (crypt ? " > encryption" : "") + (compress ? " > compression" : ""),
+                spec.describeChain());
+        assertEquals(header.mDictionaryOptions.mAttributes, options);
+    }
+
+    public void testHeaderReaderProcessor() throws IOException, UnsupportedFormatException {
+        runTestHeaderReaderProcessorWithOneSpec(false /* compress */, false /* crypt */);
+        runTestHeaderReaderProcessorWithOneSpec(true /* compress */, false /* crypt */);
+        runTestHeaderReaderProcessorWithOneSpec(true /* compress */, true /* crypt */);
+    }
 }