Merge "Add Bengali keyboard layout"
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
index 4ea7fb8..ee9125a 100644
--- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
@@ -19,6 +19,7 @@
 import android.os.Build;
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
 
 import java.lang.reflect.Constructor;
@@ -64,7 +65,12 @@
     }
 
     public static boolean isAsciiCapable(final InputMethodSubtype subtype) {
-        return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable)
+        return isAsciiCapableWithAPI(subtype)
                 || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE);
     }
+
+    @UsedForTesting
+    public static boolean isAsciiCapableWithAPI(final InputMethodSubtype subtype) {
+        return (Boolean)CompatUtils.invoke(subtype, false, METHOD_isAsciiCapable);
+    }
 }
diff --git a/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
new file mode 100644
index 0000000..9676458
--- /dev/null
+++ b/java/src/com/android/inputmethod/compat/LocaleSpanCompatUtils.java
@@ -0,0 +1,59 @@
+/*
+ * 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.compat;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.compat.CompatUtils;
+
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Method;
+import java.util.Locale;
+
+@UsedForTesting
+public final class LocaleSpanCompatUtils {
+    // Note that LocaleSpan(Locale locale) has been introduced in API level 17
+    // (Build.VERSION_CODE.JELLY_BEAN_MR1).
+    private static Class<?> getLocalSpanClass() {
+        try {
+            return Class.forName("android.text.style.LocaleSpan");
+        } catch (ClassNotFoundException e) {
+            return null;
+        }
+    }
+    private static final Constructor<?> LOCALE_SPAN_CONSTRUCTOR;
+    private static final Method LOCALE_SPAN_GET_LOCALE;
+    static {
+        final Class<?> localeSpanClass = getLocalSpanClass();
+        LOCALE_SPAN_CONSTRUCTOR = CompatUtils.getConstructor(localeSpanClass, Locale.class);
+        LOCALE_SPAN_GET_LOCALE = CompatUtils.getMethod(localeSpanClass, "getLocale");
+    }
+
+    @UsedForTesting
+    public static boolean isLocaleSpanAvailable() {
+        return (LOCALE_SPAN_CONSTRUCTOR != null && LOCALE_SPAN_GET_LOCALE != null);
+    }
+
+    @UsedForTesting
+    public static Object newLocaleSpan(final Locale locale) {
+        return CompatUtils.newInstance(LOCALE_SPAN_CONSTRUCTOR, locale);
+    }
+
+    @UsedForTesting
+    public static Locale getLocaleFromLocaleSpan(final Object localeSpan) {
+        return (Locale) CompatUtils.invoke(localeSpan, null, LOCALE_SPAN_GET_LOCALE);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java
index 31fa867..ad411f9 100644
--- a/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java
+++ b/java/src/com/android/inputmethod/latin/settings/AdditionalSubtypeSettings.java
@@ -150,8 +150,9 @@
             // TODO: Should filter out already existing combinations of locale and layout.
             for (final String layout : SubtypeLocaleUtils.getPredefinedKeyboardLayoutSet()) {
                 // This is a dummy subtype with NO_LANGUAGE, only for display.
-                final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
-                        SubtypeLocaleUtils.NO_LANGUAGE, layout, null);
+                final InputMethodSubtype subtype =
+                        AdditionalSubtypeUtils.createDummyAdditionalSubtype(
+                                SubtypeLocaleUtils.NO_LANGUAGE, layout);
                 add(new KeyboardLayoutSetItem(subtype));
             }
         }
@@ -286,8 +287,9 @@
                         (SubtypeLocaleItem) mSubtypeLocaleSpinner.getSelectedItem();
                 final KeyboardLayoutSetItem layout =
                         (KeyboardLayoutSetItem) mKeyboardLayoutSetSpinner.getSelectedItem();
-                final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
-                        locale.first, layout.first, Constants.Subtype.ExtraValue.ASCII_CAPABLE);
+                final InputMethodSubtype subtype =
+                        AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                                locale.first, layout.first);
                 setSubtype(subtype);
                 notifyChanged();
                 if (isEditing) {
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index b57eab3..90c8f61 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -374,8 +374,8 @@
     public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
         final int script = ScriptUtils.getScriptFromSpellCheckerLocale(locale);
         final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
-        final InputMethodSubtype subtype = AdditionalSubtypeUtils.createAdditionalSubtype(
-                locale.toString(), keyboardLayoutName, null);
+        final InputMethodSubtype subtype = AdditionalSubtypeUtils.createDummyAdditionalSubtype(
+                locale.toString(), keyboardLayoutName);
         final KeyboardLayoutSet keyboardLayoutSet = createKeyboardSetForSpellChecker(subtype);
 
         final DictionaryCollection dictionaryCollection =
diff --git a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
index 3ca7c7e..db7f2a5 100644
--- a/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtils.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.utils;
 
 import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
@@ -56,25 +57,33 @@
     private static final int LENGTH_WITH_EXTRA_VALUE = (INDEX_OF_EXTRA_VALUE + 1);
     private static final String PREF_SUBTYPE_SEPARATOR = ";";
 
-    public static InputMethodSubtype createAdditionalSubtype(final String localeString,
-            final String keyboardLayoutSetName, final String extraValue) {
-        final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
-        final String layoutDisplayNameExtraValue;
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
-                && SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
-            final String layoutDisplayName = SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(
-                    keyboardLayoutSetName);
-            layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
-                    UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
-        } else {
-            layoutDisplayNameExtraValue = extraValue;
-        }
-        final String additionalSubtypeExtraValue =
-                StringUtils.appendToCommaSplittableTextIfNotExists(
-                        IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
+    private static InputMethodSubtype createAdditionalSubtypeInternal(
+            final String localeString, final String keyboardLayoutSetName,
+            final boolean isAsciiCapable, final boolean isEmojiCapable) {
         final int nameId = SubtypeLocaleUtils.getSubtypeNameId(localeString, keyboardLayoutSetName);
-        return buildInputMethodSubtype(
-                nameId, localeString, layoutExtraValue, additionalSubtypeExtraValue);
+        final String platformVersionDependentExtraValues = getPlatformVersionDependentExtraValue(
+                localeString, keyboardLayoutSetName, isAsciiCapable, isEmojiCapable);
+        final int platformVersionIndependentSubtypeId =
+                getPlatformVersionIndependentSubtypeId(localeString, keyboardLayoutSetName);
+        // NOTE: In KitKat and later, InputMethodSubtypeBuilder#setIsAsciiCapable is also available.
+        // TODO: Use InputMethodSubtypeBuilder#setIsAsciiCapable when appropriate.
+        return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId,
+                R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE,
+                platformVersionDependentExtraValues,
+                false /* isAuxiliary */, false /* overrideImplicitlyEnabledSubtype */,
+                platformVersionIndependentSubtypeId);
+    }
+
+    public static InputMethodSubtype createDummyAdditionalSubtype(
+            final String localeString, final String keyboardLayoutSetName) {
+        return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
+                false /* isAsciiCapable */, false /* isEmojiCapable */);
+    }
+
+    public static InputMethodSubtype createAsciiEmojiCapableAdditionalSubtype(
+            final String localeString, final String keyboardLayoutSetName) {
+        return createAdditionalSubtypeInternal(localeString, keyboardLayoutSetName,
+                true /* isAsciiCapable */, true /* isEmojiCapable */);
     }
 
     public static String getPrefSubtype(final InputMethodSubtype subtype) {
@@ -106,10 +115,10 @@
             }
             final String localeString = elems[INDEX_OF_LOCALE];
             final String keyboardLayoutSetName = elems[INDEX_OF_KEYBOARD_LAYOUT];
-            final String extraValue = (elems.length == LENGTH_WITH_EXTRA_VALUE)
-                    ? elems[INDEX_OF_EXTRA_VALUE] : null;
-            final InputMethodSubtype subtype = createAdditionalSubtype(
-                    localeString, keyboardLayoutSetName, extraValue);
+            // Here we assume that all the additional subtypes have AsciiCapable and EmojiCapable.
+            // This is actually what the setting dialog for additional subtype is doing.
+            final InputMethodSubtype subtype = createAsciiEmojiCapableAdditionalSubtype(
+                    localeString, keyboardLayoutSetName);
             if (subtype.getNameResId() == SubtypeLocaleUtils.UNKNOWN_KEYBOARD_LAYOUT) {
                 // Skip unknown keyboard layout subtype. This may happen when predefined keyboard
                 // layout has been removed.
@@ -148,35 +157,80 @@
         return sb.toString();
     }
 
-    private static InputMethodSubtype buildInputMethodSubtype(final int nameId,
-            final String localeString, final String layoutExtraValue,
-            final String additionalSubtypeExtraValue) {
-        // To preserve additional subtype settings and user's selection across OS updates, subtype
-        // id shouldn't be changed. New attributes, such as emojiCapable, are carefully excluded
-        // from the calculation of subtype id.
-        final String compatibleExtraValue = StringUtils.joinCommaSplittableText(
-                layoutExtraValue, additionalSubtypeExtraValue);
-        final int compatibleSubtypeId = getInputMethodSubtypeId(localeString, compatibleExtraValue);
-        final String extraValue;
-        // Color Emoji is supported from KitKat.
-        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
-            extraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
-                    EMOJI_CAPABLE, compatibleExtraValue);
-        } else {
-            extraValue = compatibleExtraValue;
+    /**
+     * Returns the extra value that is optimized for the running OS.
+     * <p>
+     * Historically the extra value has been used as the last resort to annotate various kinds of
+     * attributes. Some of these attributes are valid only on some platform versions. Thus we cannot
+     * assume that the extra values stored in a persistent storage are always valid. We need to
+     * regenerate the extra value on the fly instead.
+     * </p>
+     * @param localeString the locale string (e.g., "en_US").
+     * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak").
+     * @param isAsciiCapable true when ASCII characters are supported with this layout.
+     * @param isEmojiCapable true when Unicode Emoji characters are supported with this layout.
+     * @return extra value that is optimized for the running OS.
+     * @see #getPlatformVersionIndependentSubtypeId(String, String)
+     */
+    private static String getPlatformVersionDependentExtraValue(final String localeString,
+            final String keyboardLayoutSetName, final boolean isAsciiCapable,
+            final boolean isEmojiCapable) {
+        final ArrayList<String> extraValueItems = new ArrayList<>();
+        extraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName);
+        if (isAsciiCapable) {
+            extraValueItems.add(ASCII_CAPABLE);
         }
-        return InputMethodSubtypeCompatUtils.newInputMethodSubtype(nameId,
-                R.drawable.ic_ime_switcher_dark, localeString, KEYBOARD_MODE, extraValue,
-                false, false, compatibleSubtypeId);
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN &&
+                SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
+            extraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" +
+                    SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName));
+        }
+        if (isEmojiCapable && Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            extraValueItems.add(EMOJI_CAPABLE);
+        }
+        extraValueItems.add(IS_ADDITIONAL_SUBTYPE);
+        return TextUtils.join(",", extraValueItems);
     }
 
-    private static int getInputMethodSubtypeId(final String localeString, final String extraValue) {
-        // From the compatibility point of view, the calculation of subtype id has been copied from
-        // {@link InputMethodSubtype} of JellyBean MR2.
+    /**
+     * Returns the subtype ID that is supposed to be compatible between different version of OSes.
+     * <p>
+     * From the compatibility point of view, it is important to keep subtype id predictable and
+     * stable between different OSes. For this purpose, the calculation code in this method is
+     * carefully chosen and then fixed. Treat the following code as no more or less than a
+     * hash function. Each component to be hashed can be different from the corresponding value
+     * that is used to instantiate {@link InputMethodSubtype} actually.
+     * For example, you don't need to update <code>compatibilityExtraValueItems</code> in this
+     * method even when we need to add some new extra values for the actual instance of
+     * {@link InputMethodSubtype}.
+     * </p>
+     * @param localeString the locale string (e.g., "en_US").
+     * @param keyboardLayoutSetName the keyboard layout set name (e.g., "dvorak").
+     * @return a platform-version independent subtype ID.
+     * @see #getPlatformVersionDependentExtraValue(String, String, boolean, boolean)
+     */
+    private static int getPlatformVersionIndependentSubtypeId(final String localeString,
+            final String keyboardLayoutSetName) {
+        // For compatibility reasons, we concatenate the extra values in the following order.
+        // - KeyboardLayoutSet
+        // - AsciiCapable
+        // - UntranslatableReplacementStringInSubtypeName
+        // - EmojiCapable
+        // - isAdditionalSubtype
+        final ArrayList<String> compatibilityExtraValueItems = new ArrayList<>();
+        compatibilityExtraValueItems.add(KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName);
+        compatibilityExtraValueItems.add(ASCII_CAPABLE);
+        if (SubtypeLocaleUtils.isExceptionalLocale(localeString)) {
+            compatibilityExtraValueItems.add(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" +
+                    SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(keyboardLayoutSetName));
+        }
+        compatibilityExtraValueItems.add(EMOJI_CAPABLE);
+        compatibilityExtraValueItems.add(IS_ADDITIONAL_SUBTYPE);
+        final String compatibilityExtraValues = TextUtils.join(",", compatibilityExtraValueItems);
         return Arrays.hashCode(new Object[] {
                 localeString,
                 KEYBOARD_MODE,
-                extraValue,
+                compatibilityExtraValues,
                 false /* isAuxiliary */,
                 false /* overrideImplicitlyEnabledSubtype */ });
     }
diff --git a/java/src/com/android/inputmethod/latin/utils/StringUtils.java b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
index e4237a7..ceb0383 100644
--- a/java/src/com/android/inputmethod/latin/utils/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/StringUtils.java
@@ -75,31 +75,6 @@
         return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
     }
 
-    public static String joinCommaSplittableText(final String head, final String tail) {
-        if (TextUtils.isEmpty(head) && TextUtils.isEmpty(tail)) {
-            return EMPTY_STRING;
-        }
-        // Here either head or tail is not null.
-        if (TextUtils.isEmpty(head)) {
-            return tail;
-        }
-        if (TextUtils.isEmpty(tail)) {
-            return head;
-        }
-        return head + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + tail;
-    }
-
-    public static String appendToCommaSplittableTextIfNotExists(final String text,
-            final String extraValues) {
-        if (TextUtils.isEmpty(extraValues)) {
-            return text;
-        }
-        if (containsInCommaSplittableText(text, extraValues)) {
-            return extraValues;
-        }
-        return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text;
-    }
-
     public static String removeFromCommaSplittableTextIfExists(final String text,
             final String extraValues) {
         if (TextUtils.isEmpty(extraValues)) {
diff --git a/tests/src/com/android/inputmethod/compat/LocaleSpanCompatUtilsTests.java b/tests/src/com/android/inputmethod/compat/LocaleSpanCompatUtilsTests.java
new file mode 100644
index 0000000..a920373
--- /dev/null
+++ b/tests/src/com/android/inputmethod/compat/LocaleSpanCompatUtilsTests.java
@@ -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.
+ */
+
+package com.android.inputmethod.compat;
+
+import android.os.Build;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.Locale;
+
+@SmallTest
+public class LocaleSpanCompatUtilsTests extends AndroidTestCase {
+    public void testInstantiatable() {
+        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR1) {
+            // LocaleSpan isn't yet available.
+            return;
+        }
+        assertTrue(LocaleSpanCompatUtils.isLocaleSpanAvailable());
+        final Object japaneseLocaleSpan = LocaleSpanCompatUtils.newLocaleSpan(Locale.JAPANESE);
+        assertNotNull(japaneseLocaleSpan);
+        assertEquals(Locale.JAPANESE,
+                LocaleSpanCompatUtils.getLocaleFromLocaleSpan(japaneseLocaleSpan));
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
index ab7d1b2..cf884bf 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
@@ -104,8 +104,8 @@
             final Locale subtypeLocale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
             if (locale.equals(subtypeLocale)) {
                 // Create additional subtype.
-                return AdditionalSubtypeUtils.createAdditionalSubtype(
-                        locale.toString(), keyboardLayout, null /* extraValue */);
+                return AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                        locale.toString(), keyboardLayout);
             }
         }
         throw new RuntimeException(
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
index 0be1e37..6ea2758 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
@@ -67,10 +67,10 @@
                 Locale.CANADA_FRENCH.toString(), "qwerty");
         FR_CH_SWISS = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 "fr_CH", "swiss");
-        FR_CH_QWERTZ = AdditionalSubtypeUtils.createAdditionalSubtype(
-                "fr_CH", "qwertz", null);
-        FR_CH_QWERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                "fr_CH", "qwerty", null);
+        FR_CH_QWERTZ = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                "fr_CH", "qwertz");
+        FR_CH_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                "fr_CH", "qwerty");
         ZZ_QWERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
     }
diff --git a/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java
new file mode 100644
index 0000000..91c9c37
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java
@@ -0,0 +1,175 @@
+/*
+ * Copyright (C) 2014 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.utils;
+
+import android.content.Context;
+import android.os.Build;
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
+
+import java.util.Locale;
+
+import static com.android.inputmethod.latin.Constants.Subtype.KEYBOARD_MODE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.ASCII_CAPABLE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.IS_ADDITIONAL_SUBTYPE;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
+import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue
+        .UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME;
+
+@SmallTest
+public class AdditionalSubtypeUtilsTests extends AndroidTestCase {
+
+    /**
+     * Predictable subtype ID for en_US dvorak layout. This is actually a hash code calculated as
+     * follows.
+     * <code>
+     * final boolean isAuxiliary = false;
+     * final boolean overrideImplicitlyEnabledSubtype = false;
+     * final int SUBTYPE_ID_EN_US_DVORAK = Arrays.hashCode(new Object[] {
+     *         "en_US",
+     *         "keyboard",
+     *         "KeyboardLayoutSet=dvorak"
+     *                 + ",AsciiCapable"
+     *                 + ",UntranslatableReplacementStringInSubtypeName=Dvorak"
+     *                 + ",EmojiCapable"
+     *                 + ",isAdditionalSubtype",
+     *         isAuxiliary,
+     *         overrideImplicitlyEnabledSubtype });
+     * </code>
+     */
+    private static int SUBTYPE_ID_EN_US_DVORAK = 0xb3c0cc56;
+    private static String EXTRA_VALUE_EN_US_DVORAK_ICS =
+            "KeyboardLayoutSet=dvorak" +
+            ",AsciiCapable" +
+            ",isAdditionalSubtype";
+    private static String EXTRA_VALUE_EN_US_DVORAK_JELLY_BEAN =
+            "KeyboardLayoutSet=dvorak" +
+            ",AsciiCapable" +
+            ",UntranslatableReplacementStringInSubtypeName=Dvorak" +
+            ",isAdditionalSubtype";
+    private static String EXTRA_VALUE_EN_US_DVORAK_KITKAT =
+            "KeyboardLayoutSet=dvorak" +
+            ",AsciiCapable" +
+            ",UntranslatableReplacementStringInSubtypeName=Dvorak" +
+            ",EmojiCapable" +
+            ",isAdditionalSubtype";
+
+    /**
+     * Predictable subtype ID for azerty layout. This is actually a hash code calculated as follows.
+     * <code>
+     * final boolean isAuxiliary = false;
+     * final boolean overrideImplicitlyEnabledSubtype = false;
+     * final int SUBTYPE_ID_ZZ_AZERTY = Arrays.hashCode(new Object[] {
+     *         "zz",
+     *         "keyboard",
+     *         "KeyboardLayoutSet=azerty"
+     *                 + ",AsciiCapable"
+     *                 + ",EmojiCapable"
+     *                 + ",isAdditionalSubtype",
+     *         isAuxiliary,
+     *         overrideImplicitlyEnabledSubtype });
+     * </code>
+     */
+    private static int SUBTYPE_ID_ZZ_AZERTY = 0x5b6be697;
+    private static String EXTRA_VALUE_ZZ_AZERTY_ICS =
+            "KeyboardLayoutSet=azerty" +
+            ",AsciiCapable" +
+            ",isAdditionalSubtype";
+    private static String EXTRA_VALUE_ZZ_AZERTY_KITKAT =
+            "KeyboardLayoutSet=azerty" +
+            ",AsciiCapable" +
+            ",EmojiCapable" +
+            ",isAdditionalSubtype";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+        final Context context = getContext();
+        SubtypeLocaleUtils.init(context);
+    }
+
+    private static void assertEnUsDvorak(InputMethodSubtype subtype) {
+        assertEquals("en_US", subtype.getLocale());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            assertEquals(EXTRA_VALUE_EN_US_DVORAK_KITKAT, subtype.getExtraValue());
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
+            assertEquals(EXTRA_VALUE_EN_US_DVORAK_JELLY_BEAN, subtype.getExtraValue());
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            assertEquals(EXTRA_VALUE_EN_US_DVORAK_ICS, subtype.getExtraValue());
+        }
+        assertTrue(subtype.containsExtraValueKey(ASCII_CAPABLE));
+        assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapable(subtype));
+        // TODO: Enable following test
+        // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+        //    assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapableWithAPI(subtype));
+        // }
+        assertTrue(subtype.containsExtraValueKey(EMOJI_CAPABLE));
+        assertTrue(subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE));
+        assertEquals("dvorak", subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET));
+        assertEquals("Dvorak", subtype.getExtraValueOf(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME));
+        assertEquals(KEYBOARD_MODE, subtype.getMode());
+        assertEquals(SUBTYPE_ID_EN_US_DVORAK, subtype.hashCode());
+    }
+
+    private static void assertAzerty(InputMethodSubtype subtype) {
+        assertEquals("zz", subtype.getLocale());
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            assertEquals(EXTRA_VALUE_ZZ_AZERTY_KITKAT, subtype.getExtraValue());
+        } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) {
+            assertEquals(EXTRA_VALUE_ZZ_AZERTY_ICS, subtype.getExtraValue());
+        }
+        assertTrue(subtype.containsExtraValueKey(ASCII_CAPABLE));
+        assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapable(subtype));
+        // TODO: Enable following test
+        // if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+        //    assertTrue(InputMethodSubtypeCompatUtils.isAsciiCapableWithAPI(subtype));
+        // }
+        assertTrue(subtype.containsExtraValueKey(EMOJI_CAPABLE));
+        assertTrue(subtype.containsExtraValueKey(IS_ADDITIONAL_SUBTYPE));
+        assertEquals("azerty", subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET));
+        assertFalse(subtype.containsExtraValueKey(UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME));
+        assertEquals(KEYBOARD_MODE, subtype.getMode());
+        assertEquals(SUBTYPE_ID_ZZ_AZERTY, subtype.hashCode());
+    }
+
+    public void testRestorable() {
+        final InputMethodSubtype EN_UK_DVORAK =
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                        Locale.US.toString(), "dvorak");
+        final InputMethodSubtype ZZ_AZERTY =
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                        SubtypeLocaleUtils.NO_LANGUAGE, "azerty");
+        assertEnUsDvorak(EN_UK_DVORAK);
+        assertAzerty(ZZ_AZERTY);
+
+        // Make sure the subtype can be stored and restored in a deterministic manner.
+        final InputMethodSubtype[] subtypes = { EN_UK_DVORAK, ZZ_AZERTY };
+        final String prefSubtype = AdditionalSubtypeUtils.createPrefSubtypes(subtypes);
+        final InputMethodSubtype[] restoredSubtypes =
+                AdditionalSubtypeUtils.createAdditionalSubtypesArray(prefSubtype);
+        assertEquals(2, restoredSubtypes.length);
+        final InputMethodSubtype restored_EN_UK_DVORAK = restoredSubtypes[0];
+        final InputMethodSubtype restored_ZZ_AZERTY = restoredSubtypes[1];
+
+        assertEnUsDvorak(restored_EN_UK_DVORAK);
+        assertAzerty(restored_ZZ_AZERTY);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguagetUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguagetUtilsTests.java
index 4156de7..fdde342 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguagetUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguagetUtilsTests.java
@@ -87,20 +87,20 @@
                 "de_CH", "swiss");
         ZZ = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
-        DE_QWERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.GERMAN.toString(), "qwerty", null);
-        FR_QWERTZ = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.FRENCH.toString(), "qwertz", null);
-        EN_US_AZERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.US.toString(), "azerty", null);
-        EN_UK_DVORAK = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.UK.toString(), "dvorak", null);
-        ES_US_COLEMAK = AdditionalSubtypeUtils.createAdditionalSubtype(
-                "es_US", "colemak", null);
-        ZZ_AZERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                SubtypeLocaleUtils.NO_LANGUAGE, "azerty", null);
-        ZZ_PC = AdditionalSubtypeUtils.createAdditionalSubtype(
-                SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty", null);
+        DE_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.GERMAN.toString(), "qwerty");
+        FR_QWERTZ = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.FRENCH.toString(), "qwertz");
+        EN_US_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.US.toString(), "azerty");
+        EN_UK_DVORAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.UK.toString(), "dvorak");
+        ES_US_COLEMAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                "es_US", "colemak");
+        ZZ_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                SubtypeLocaleUtils.NO_LANGUAGE, "azerty");
+        ZZ_PC = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty");
     }
 
     public void testAllFullDisplayNameForSpacebar() {
diff --git a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
index cd9a983..4448a6b 100644
--- a/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/StringAndJsonUtilsTests.java
@@ -56,48 +56,6 @@
         assertTrue("in 2 elements", StringUtils.containsInCommaSplittableText("key", "key1,key"));
     }
 
-    public void testJoinCommaSplittableText() {
-        assertEquals("2 nulls", "",
-                StringUtils.joinCommaSplittableText(null, null));
-        assertEquals("null and empty", "",
-                StringUtils.joinCommaSplittableText(null, ""));
-        assertEquals("empty and null", "",
-                StringUtils.joinCommaSplittableText("", null));
-        assertEquals("2 empties", "",
-                StringUtils.joinCommaSplittableText("", ""));
-        assertEquals("text and null", "text",
-                StringUtils.joinCommaSplittableText("text", null));
-        assertEquals("text and empty", "text",
-                StringUtils.joinCommaSplittableText("text", ""));
-        assertEquals("null and text", "text",
-                StringUtils.joinCommaSplittableText(null, "text"));
-        assertEquals("empty and text", "text",
-                StringUtils.joinCommaSplittableText("", "text"));
-        assertEquals("2 texts", "text1,text2",
-                StringUtils.joinCommaSplittableText("text1", "text2"));
-    }
-
-    public void testAppendToCommaSplittableTextIfNotExists() {
-        assertEquals("null", "key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", null));
-        assertEquals("empty", "key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", ""));
-
-        assertEquals("not in 1 element", "key1,key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1"));
-        assertEquals("not in 2 elements", "key1,key2,key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key2"));
-
-        assertEquals("in 1 element", "key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key"));
-        assertEquals("in 2 elements at position 1", "key,key2",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key,key2"));
-        assertEquals("in 2 elements at position 2", "key1,key",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key"));
-        assertEquals("in 3 elements at position 2", "key1,key,key3",
-                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key,key3"));
-    }
-
     public void testRemoveFromCommaSplittableTextIfExists() {
         assertEquals("null", "", StringUtils.removeFromCommaSplittableTextIfExists("key", null));
         assertEquals("empty", "", StringUtils.removeFromCommaSplittableTextIfExists("key", ""));
diff --git a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
index 8e409ab..ce3df7d 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
@@ -87,20 +87,20 @@
                 "de_CH", "swiss");
         ZZ = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
                 SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
-        DE_QWERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.GERMAN.toString(), "qwerty", null);
-        FR_QWERTZ = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.FRENCH.toString(), "qwertz", null);
-        EN_US_AZERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.US.toString(), "azerty", null);
-        EN_UK_DVORAK = AdditionalSubtypeUtils.createAdditionalSubtype(
-                Locale.UK.toString(), "dvorak", null);
-        ES_US_COLEMAK = AdditionalSubtypeUtils.createAdditionalSubtype(
-                "es_US", "colemak", null);
-        ZZ_AZERTY = AdditionalSubtypeUtils.createAdditionalSubtype(
-                SubtypeLocaleUtils.NO_LANGUAGE, "azerty", null);
-        ZZ_PC = AdditionalSubtypeUtils.createAdditionalSubtype(
-                SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty", null);
+        DE_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.GERMAN.toString(), "qwerty");
+        FR_QWERTZ = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.FRENCH.toString(), "qwertz");
+        EN_US_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.US.toString(), "azerty");
+        EN_UK_DVORAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                Locale.UK.toString(), "dvorak");
+        ES_US_COLEMAK = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                "es_US", "colemak");
+        ZZ_AZERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                SubtypeLocaleUtils.NO_LANGUAGE, "azerty");
+        ZZ_PC = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty");
     }
 
     public void testAllFullDisplayName() {