Merge "Introduce onEvent() to improve testability"
diff --git a/java/res/xml-sw600dp/key_comma.xml b/java/res/xml-sw600dp/key_comma.xml
index 67199e2..e616a8d 100644
--- a/java/res/xml-sw600dp/key_comma.xml
+++ b/java/res/xml-sw600dp/key_comma.xml
@@ -2,7 +2,7 @@
 <!--
 /*
 **
-** Copyright 2014, The Android Open Source Project
+** Copyright 2012, 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.
@@ -21,27 +21,36 @@
 <merge
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
 >
-    <!-- The table comma key which may have settings as popup key. -->
-    <!-- Kept as a separate file for cleaner overriding by an overlay.  -->
-    <key-style
-        latin:styleName="baseTabletCommaKeyStyle"
-        latin:keySpec="!text/keyspec_tablet_comma"
-        latin:keyHintLabel="!text/keyhintlabel_tablet_comma"
-        latin:keyLabelFlags="hasPopupHint"
-        latin:parentStyle="hasShiftedLetterHintStyle" />
     <switch>
         <case
-            latin:clobberSettingsKey="true"
+            latin:mode="url"
         >
             <Key
-                latin:moreKeys="!text/morekeys_tablet_comma"
-                latin:keyStyle="baseTabletCommaKeyStyle" />
+                latin:keySpec="/"
+                latin:keyStyle="settingsMoreKeysStyle" />
         </case>
-        <!-- clobberSettingsKey="false" -->
+        <case
+            latin:mode="email"
+        >
+            <Key
+                latin:keySpec="\@"
+                latin:keyStyle="settingsMoreKeysStyle" />
+        </case>
+        <case
+            latin:keyboardLayoutSet="dvorak"
+        >
+            <Key
+                latin:keySpec="!"
+                latin:moreKeys="!text/morekeys_exclamation,%"
+                latin:keyStyle="settingsMoreKeysStyle" />
+        </case>
         <default>
             <Key
-                latin:moreKeys="!text/morekeys_tablet_comma,!text/keyspec_settings"
-                latin:keyStyle="baseTabletCommaKeyStyle" />
+                latin:keySpec="!text/keyspec_tablet_comma"
+                latin:moreKeys="!text/morekeys_tablet_comma,%"
+                latin:keyHintLabel="!text/keyhintlabel_tablet_comma"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:keyStyle="settingsMoreKeysStyle" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml-sw600dp/key_f1.xml b/java/res/xml-sw600dp/key_f1.xml
deleted file mode 100644
index ba78a64..0000000
--- a/java/res/xml-sw600dp/key_f1.xml
+++ /dev/null
@@ -1,36 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, 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.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:mode="email"
-        >
-            <Key
-                latin:keySpec="\@" />
-        </case>
-        <default>
-            <Key
-                latin:keySpec="/" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/key_period.xml b/java/res/xml-sw600dp/key_period.xml
index d2909d8..df1daf7 100644
--- a/java/res/xml-sw600dp/key_period.xml
+++ b/java/res/xml-sw600dp/key_period.xml
@@ -35,6 +35,13 @@
                 latin:moreKeys="!autoColumnOrder!8,\\,,.,',#,),(,/,;,@,:,-,&quot;,+,\\%,&amp;"
                 latin:backgroundType="functional" />
         </case>
+        <case
+            latin:keyboardLayoutSet="dvorak"
+        >
+            <Key
+                latin:keySpec="\?"
+                latin:moreKeys="!text/morekeys_tablet_period,!text/morekeys_question" />
+        </case>
         <default>
             <Key
                 latin:keySpec="!text/keyspec_tablet_period"
diff --git a/java/res/xml-sw600dp/key_question_exclamation.xml b/java/res/xml-sw600dp/key_question_exclamation.xml
deleted file mode 100644
index edee5c5..0000000
--- a/java/res/xml-sw600dp/key_question_exclamation.xml
+++ /dev/null
@@ -1,39 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, 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.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <switch>
-        <case
-            latin:mode="email|url"
-        >
-            <Key
-                latin:keySpec="-" />
-        </case>
-        <default>
-            <Key
-                latin:keySpec="\?"
-                latin:keyHintLabel="!"
-                latin:moreKeys="!"
-                latin:keyStyle="hasShiftedLetterHintStyle" />
-        </default>
-    </switch>
-</merge>
diff --git a/java/res/xml-sw600dp/key_space_5kw.xml b/java/res/xml-sw600dp/key_space_7kw.xml
similarity index 93%
rename from java/res/xml-sw600dp/key_space_5kw.xml
rename to java/res/xml-sw600dp/key_space_7kw.xml
index 8302184..3311f81 100644
--- a/java/res/xml-sw600dp/key_space_5kw.xml
+++ b/java/res/xml-sw600dp/key_space_7kw.xml
@@ -34,7 +34,7 @@
                 latin:keyStyle="languageSwitchKeyStyle" />
             <Key
                 latin:keyStyle="spaceKeyStyle"
-                latin:keyWidth="27.0%p" />
+                latin:keyWidth="45.0%p" />
             <Key
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
@@ -44,7 +44,7 @@
         >
             <Key
                 latin:keyStyle="spaceKeyStyle"
-                latin:keyWidth="36.0%p" />
+                latin:keyWidth="54.0%p" />
             <Key
                 latin:keyStyle="zwnjKeyStyle" />
         </case>
@@ -55,13 +55,13 @@
                 latin:keyStyle="languageSwitchKeyStyle" />
             <Key
                 latin:keyStyle="spaceKeyStyle"
-                latin:keyWidth="36.0%p" />
+                latin:keyWidth="54.0%p" />
         </case>
         <!-- languageSwitchKeyEnabled="false" -->
         <default>
             <Key
                 latin:keyStyle="spaceKeyStyle"
-                latin:keyWidth="45.0%p" />
+                latin:keyWidth="63.0%p" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml-sw600dp/key_styles_common.xml b/java/res/xml-sw600dp/key_styles_common.xml
index 1b52b1e..c750a93 100644
--- a/java/res/xml-sw600dp/key_styles_common.xml
+++ b/java/res/xml-sw600dp/key_styles_common.xml
@@ -36,6 +36,9 @@
         </default>
     </switch>
     <!-- Base key style for the key which may have settings key as more keys. -->
+    <key-style
+        latin:styleName="baseSettingsMoreKeysStyle"
+        latin:parentStyle="hasShiftedLetterHintStyle" />
     <include
         latin:keyboardLayout="@xml/key_styles_settings" />
     <!-- Functional key styles -->
diff --git a/java/res/xml-sw600dp/row_dvorak4.xml b/java/res/xml-sw600dp/row_dvorak4.xml
deleted file mode 100644
index ab2b560..0000000
--- a/java/res/xml-sw600dp/row_dvorak4.xml
+++ /dev/null
@@ -1,49 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, 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.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <Row
-        latin:keyWidth="9.0%p"
-        latin:backgroundType="functional"
-    >
-        <Key
-            latin:keyStyle="toSymbolKeyStyle"
-            latin:keyWidth="10.0%p" />
-        <include
-            latin:keyboardLayout="@xml/key_settings" />
-        <Key
-            latin:keySpec="_"
-            latin:keyHintLabel="-"
-            latin:moreKeys="-"
-            latin:keyStyle="hasShiftedLetterHintStyle" />
-        <include
-            latin:keyXPos="28.0%p"
-            latin:keyboardLayout="@xml/key_space_5kw"
-            latin:backgroundType="normal" />
-        <include
-            latin:keyboardLayout="@xml/key_f1" />
-        <include
-            latin:keyboardLayout="@xml/key_question_exclamation" />
-        <include
-            latin:keyboardLayout="@xml/key_f2" />
-    </Row>
-</merge>
diff --git a/java/res/xml-sw600dp/row_pcqwerty5.xml b/java/res/xml-sw600dp/row_pcqwerty5.xml
index ac07f11..4dcae14 100644
--- a/java/res/xml-sw600dp/row_pcqwerty5.xml
+++ b/java/res/xml-sw600dp/row_pcqwerty5.xml
@@ -50,6 +50,6 @@
             latin:keyXPos="-9.0%p"
             latin:keyWidth="9.0%p"
             latin:backgroundType="functional"
-            latin:keyboardLayout="@xml/key_f2" />
+            latin:keyboardLayout="@xml/key_emoji" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/row_qwerty4.xml b/java/res/xml-sw600dp/row_qwerty4.xml
index 0eb86f2..ed7150d 100644
--- a/java/res/xml-sw600dp/row_qwerty4.xml
+++ b/java/res/xml-sw600dp/row_qwerty4.xml
@@ -30,18 +30,14 @@
             latin:keyWidth="10.0%p" />
         <include
             latin:keyboardLayout="@xml/key_comma" />
-        <Key
-            latin:keySpec="_" />
         <!-- Space key. -->
         <include
-            latin:keyXPos="28.0%p"
-            latin:keyboardLayout="@xml/key_space_5kw"
+            latin:keyXPos="19.0%p"
+            latin:keyboardLayout="@xml/key_space_7kw"
             latin:backgroundType="normal" />
         <include
-            latin:keyboardLayout="@xml/key_f1" />
-        <include
             latin:keyboardLayout="@xml/key_period" />
         <include
-            latin:keyboardLayout="@xml/key_f2" />
+            latin:keyboardLayout="@xml/key_emoji" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/rows_dvorak.xml b/java/res/xml-sw600dp/rows_dvorak.xml
index 8859267..c8f5e3a 100644
--- a/java/res/xml-sw600dp/rows_dvorak.xml
+++ b/java/res/xml-sw600dp/rows_dvorak.xml
@@ -53,6 +53,8 @@
             latin:keyStyle="shiftKeyStyle"
             latin:keyWidth="fillRight" />
     </Row>
+    <!-- Dvorak layout shares almost the same row with Qwerty layout.
+         The difference is defined in xml-sw600dp/row_qwerty4.xml. -->
     <include
-        latin:keyboardLayout="@xml/row_dvorak4" />
+        latin:keyboardLayout="@xml/row_qwerty4" />
 </merge>
diff --git a/java/res/xml-sw600dp/rows_symbols.xml b/java/res/xml-sw600dp/rows_symbols.xml
index a915c33..05e7c68 100644
--- a/java/res/xml-sw600dp/rows_symbols.xml
+++ b/java/res/xml-sw600dp/rows_symbols.xml
@@ -70,6 +70,6 @@
         <include
             latin:keyboardLayout="@xml/row_symbols4" />
         <include
-            latin:keyboardLayout="@xml/key_f2" />
+            latin:keyboardLayout="@xml/key_emoji" />
     </Row>
 </merge>
diff --git a/java/res/xml-sw600dp/rows_symbols_shift.xml b/java/res/xml-sw600dp/rows_symbols_shift.xml
index 7ead4d5..70ac42e 100644
--- a/java/res/xml-sw600dp/rows_symbols_shift.xml
+++ b/java/res/xml-sw600dp/rows_symbols_shift.xml
@@ -72,6 +72,6 @@
         <include
             latin:keyboardLayout="@xml/row_symbols_shift4" />
         <include
-            latin:keyboardLayout="@xml/key_f2" />
+            latin:keyboardLayout="@xml/key_emoji" />
     </Row>
 </merge>
diff --git a/java/res/xml/key_f1.xml b/java/res/xml/key_comma.xml
similarity index 82%
rename from java/res/xml/key_f1.xml
rename to java/res/xml/key_comma.xml
index 7bd7385..cf919a8 100644
--- a/java/res/xml/key_f1.xml
+++ b/java/res/xml/key_comma.xml
@@ -23,6 +23,15 @@
 >
     <switch>
         <case
+            latin:keyboardLayoutSet="dvorak"
+        >
+            <Key
+                latin:keySpec="q"
+                latin:moreKeys="!text/morekeys_q,%"
+                latin:backgroundType="normal"
+                latin:keyStyle="settingsMoreKeysStyle" />
+        </case>
+        <case
             latin:mode="url"
         >
             <Key
diff --git a/java/res/xml/key_f2.xml b/java/res/xml/key_emoji.xml
similarity index 100%
rename from java/res/xml/key_f2.xml
rename to java/res/xml/key_emoji.xml
diff --git a/java/res/xml/key_period.xml b/java/res/xml/key_period.xml
index e1d4bbd..fc27c02 100644
--- a/java/res/xml/key_period.xml
+++ b/java/res/xml/key_period.xml
@@ -48,6 +48,14 @@
                 latin:moreKeys="!text/morekeys_punctuation"
                 latin:backgroundType="functional" />
         </case>
+        <case
+            latin:keyboardLayoutSet="dvorak"
+        >
+            <Key
+                latin:keySpec="z"
+                latin:keyLabelFlags="hasPopupHint"
+                latin:moreKeys="!text/morekeys_punctuation,!text/morekeys_z" />
+        </case>
         <default>
             <Key
                 latin:keySpec="!text/keyspec_period"
diff --git a/java/res/xml/key_styles_common.xml b/java/res/xml/key_styles_common.xml
index 2d006c9..167e6f8 100644
--- a/java/res/xml/key_styles_common.xml
+++ b/java/res/xml/key_styles_common.xml
@@ -36,6 +36,8 @@
         </default>
     </switch>
     <!-- Base key style for the key which may have settings key as more keys. -->
+    <key-style
+        latin:styleName="baseSettingsMoreKeysStyle" />
     <include
         latin:keyboardLayout="@xml/key_styles_settings" />
     <!-- Functional key styles -->
diff --git a/java/res/xml/key_styles_settings.xml b/java/res/xml/key_styles_settings.xml
index 956b402..a504bed 100644
--- a/java/res/xml/key_styles_settings.xml
+++ b/java/res/xml/key_styles_settings.xml
@@ -29,15 +29,17 @@
         >
             <key-style
                 latin:styleName="settingsMoreKeysStyle"
-                latin:backgroundType="functional" />
+                latin:backgroundType="functional"
+                latin:parentStyle="baseSettingsMoreKeysStyle" />
         </case>
         <!-- clobberSettingsKey="false" -->
         <default>
             <key-style
                 latin:styleName="settingsMoreKeysStyle"
                 latin:keyLabelFlags="hasPopupHint"
-                latin:moreKeys="!text/keyspec_settings"
-                latin:backgroundType="functional" />
+                latin:additionalMoreKeys="!text/keyspec_settings"
+                latin:backgroundType="functional"
+                latin:parentStyle="baseSettingsMoreKeysStyle" />
         </default>
     </switch>
 </merge>
diff --git a/java/res/xml/row_dvorak4.xml b/java/res/xml/row_dvorak4.xml
deleted file mode 100644
index e7a3ee7..0000000
--- a/java/res/xml/row_dvorak4.xml
+++ /dev/null
@@ -1,45 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<!--
-/*
-**
-** Copyright 2012, 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.
-*/
--->
-
-<merge
-    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
->
-    <Row
-        latin:keyWidth="10%p"
-    >
-        <Key
-            latin:keyStyle="toSymbolKeyStyle"
-            latin:keyWidth="15%p" />
-        <Key
-            latin:keySpec="q"
-            latin:backgroundType="normal"
-            latin:keyStyle="settingsMoreKeysStyle" />
-        <include
-            latin:keyXPos="25%p"
-            latin:keyboardLayout="@xml/key_space_5kw" />
-        <Key
-            latin:keySpec="z"
-            latin:keyLabelFlags="hasPopupHint"
-            latin:moreKeys="!text/morekeys_punctuation,!text/morekeys_z" />
-        <Key
-            latin:keyStyle="enterKeyStyle"
-            latin:keyWidth="fillRight" />
-    </Row>
-</merge>
diff --git a/java/res/xml/row_qwerty4.xml b/java/res/xml/row_qwerty4.xml
index 509092d..5bc104f 100644
--- a/java/res/xml/row_qwerty4.xml
+++ b/java/res/xml/row_qwerty4.xml
@@ -28,7 +28,7 @@
             latin:keyStyle="toSymbolKeyStyle"
             latin:keyWidth="15%p" />
         <include
-            latin:keyboardLayout="@xml/key_f1" />
+            latin:keyboardLayout="@xml/key_comma" />
         <include
             latin:keyXPos="25%p"
             latin:keyboardLayout="@xml/key_space_5kw" />
diff --git a/java/res/xml/rows_dvorak.xml b/java/res/xml/rows_dvorak.xml
index 13d7021..f656613 100644
--- a/java/res/xml/rows_dvorak.xml
+++ b/java/res/xml/rows_dvorak.xml
@@ -49,6 +49,8 @@
             latin:keyWidth="fillRight"
             latin:visualInsetsLeft="1%p" />
     </Row>
+    <!-- Dvorak layout shares almost the same row with Qwerty layout.
+         The difference is defined in xml/row_qwerty4.xml. -->
     <include
-        latin:keyboardLayout="@xml/row_dvorak4" />
+        latin:keyboardLayout="@xml/row_qwerty4" />
 </merge>
diff --git a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
index b84d402..94a1ee6 100644
--- a/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
+++ b/java/src/com/android/inputmethod/accessibility/MainKeyboardAccessibilityDelegate.java
@@ -121,7 +121,7 @@
      */
     private void announceKeyboardLanguage(final Keyboard keyboard) {
         final String languageText = SubtypeLocaleUtils.getSubtypeDisplayNameInSystemLocale(
-                keyboard.mId.mSubtype);
+                keyboard.mId.mSubtype.getRawSubtype());
         sendWindowStateChanged(languageText);
     }
 
diff --git a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
index 3658672..b9a5367 100644
--- a/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
+++ b/java/src/com/android/inputmethod/compat/InputMethodSubtypeCompatUtils.java
@@ -21,6 +21,7 @@
 
 import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Method;
@@ -64,6 +65,10 @@
                 overridesImplicitlyEnabledSubtype, id);
     }
 
+    public static boolean isAsciiCapable(final RichInputMethodSubtype subtype) {
+        return isAsciiCapable(subtype.getRawSubtype());
+    }
+
     public static boolean isAsciiCapable(final InputMethodSubtype subtype) {
         return isAsciiCapableWithAPI(subtype)
                 || subtype.containsExtraValueKey(Constants.Subtype.ExtraValue.ASCII_CAPABLE);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 3c11675..538e515 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -21,9 +21,9 @@
 import android.text.InputType;
 import android.text.TextUtils;
 import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.utils.InputTypeUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
@@ -62,7 +62,7 @@
     public static final int ELEMENT_EMOJI_CATEGORY5 = 15;
     public static final int ELEMENT_EMOJI_CATEGORY6 = 16;
 
-    public final InputMethodSubtype mSubtype;
+    public final RichInputMethodSubtype mSubtype;
     public final Locale mLocale;
     public final int mWidth;
     public final int mHeight;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index d6d0b21..0804ceb 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -28,7 +28,6 @@
 import android.util.SparseArray;
 import android.util.Xml;
 import android.view.inputmethod.EditorInfo;
-import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
 import com.android.inputmethod.compat.InputMethodSubtypeCompatUtils;
@@ -37,6 +36,7 @@
 import com.android.inputmethod.keyboard.internal.KeysCache;
 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;
@@ -109,7 +109,7 @@
         boolean mVoiceInputKeyEnabled;
         boolean mNoSettingsKey;
         boolean mLanguageSwitchKeyEnabled;
-        InputMethodSubtype mSubtype;
+        RichInputMethodSubtype mSubtype;
         boolean mIsSpellChecker;
         int mKeyboardWidth;
         int mKeyboardHeight;
@@ -245,7 +245,7 @@
             return this;
         }
 
-        public Builder setSubtype(final InputMethodSubtype subtype) {
+        public Builder setSubtype(final RichInputMethodSubtype subtype) {
             final boolean asciiCapable = InputMethodSubtypeCompatUtils.isAsciiCapable(subtype);
             // TODO: Consolidate with {@link InputAttributes}.
             @SuppressWarnings("deprecation")
@@ -254,7 +254,7 @@
             final boolean forceAscii = EditorInfoCompatUtils.hasFlagForceAscii(
                     mParams.mEditorInfo.imeOptions)
                     || deprecatedForceAscii;
-            final InputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
+            final RichInputMethodSubtype keyboardSubtype = (forceAscii && !asciiCapable)
                     ? SubtypeSwitcher.getInstance().getNoLanguageSubtype()
                     : subtype;
             mParams.mSubtype = keyboardSubtype;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 140e768..77cdf49 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -261,6 +261,9 @@
     }
 
     public void onToggleEmojiKeyboard() {
+        if (mKeyboardLayoutSet == null) {
+            return;
+        }
         if (isShowingEmojiPalettes()) {
             setAlphabetKeyboard();
         } else {
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 1ef53a6..847d907 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -34,7 +34,6 @@
 import android.view.MotionEvent;
 import android.view.View;
 import android.view.ViewGroup;
-import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
 import com.android.inputmethod.accessibility.MainKeyboardAccessibilityDelegate;
@@ -54,10 +53,10 @@
 import com.android.inputmethod.keyboard.internal.TimerHandler;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.settings.DebugSettings;
 import com.android.inputmethod.latin.utils.CoordinateUtils;
-import com.android.inputmethod.latin.utils.SpacebarLanguageUtils;
 import com.android.inputmethod.latin.utils.TypefaceUtils;
 
 import java.util.WeakHashMap;
@@ -849,16 +848,16 @@
 
     // Layout language name on spacebar.
     private String layoutLanguageOnSpacebar(final Paint paint,
-            final InputMethodSubtype subtype, final int width) {
+            final RichInputMethodSubtype subtype, final int width) {
         // Choose appropriate language name to fit into the width.
         if (mLanguageOnSpacebarFormatType == LanguageOnSpacebarHelper.FORMAT_TYPE_FULL_LOCALE) {
-            final String fullText = SpacebarLanguageUtils.getFullDisplayName(subtype);
+            final String fullText = subtype.getFullDisplayName();
             if (fitsTextIntoWidth(width, fullText, paint)) {
                 return fullText;
             }
         }
 
-        final String middleText = SpacebarLanguageUtils.getMiddleDisplayName(subtype);
+        final String middleText = subtype.getMiddleDisplayName();
         if (fitsTextIntoWidth(width, middleText, paint)) {
             return middleText;
         }
@@ -872,7 +871,7 @@
         paint.setTextAlign(Align.CENTER);
         paint.setTypeface(Typeface.DEFAULT);
         paint.setTextSize(mLanguageOnSpacebarTextSize);
-        final InputMethodSubtype subtype = getKeyboard().mId.mSubtype;
+        final RichInputMethodSubtype subtype = getKeyboard().mId.mSubtype;
         final String language = layoutLanguageOnSpacebar(paint, subtype, width);
         // Draw language text with shadow
         final float descent = paint.descent();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
index 6400a24..72ad2bd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelper.java
@@ -18,6 +18,7 @@
 
 import android.view.inputmethod.InputMethodSubtype;
 
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
 
 import java.util.Collections;
@@ -34,8 +35,8 @@
     private List<InputMethodSubtype> mEnabledSubtypes = Collections.emptyList();
     private boolean mIsSystemLanguageSameAsInputLanguage;
 
-    public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) {
-        if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
+    public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) {
+        if (subtype.isNoLanguage()) {
             return FORMAT_TYPE_FULL_LOCALE;
         }
         // Only this subtype is enabled and equals to the system locale.
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index be9ceab..d743598 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -51,6 +51,7 @@
 import android.view.inputmethod.CompletionInfo;
 import android.view.inputmethod.CursorAnchorInfo;
 import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethod;
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.accessibility.AccessibilityUtils;
@@ -747,7 +748,8 @@
     public void onCurrentInputMethodSubtypeChanged(final InputMethodSubtype subtype) {
         // Note that the calling sequence of onCreate() and onCurrentInputMethodSubtypeChanged()
         // is not guaranteed. It may even be called at the same time on a different thread.
-        mSubtypeSwitcher.onSubtypeChanged(subtype);
+        final RichInputMethodSubtype richSubtype = new RichInputMethodSubtype(subtype);
+        mSubtypeSwitcher.onSubtypeChanged(richSubtype);
         mInputLogic.onSubtypeChanged(SubtypeLocaleUtils.getCombiningRulesExtraValue(subtype));
         loadKeyboard();
     }
@@ -1138,6 +1140,17 @@
     }
 
     @Override
+    public boolean onShowInputRequested(final int flags, final boolean configChange) {
+        if ((flags & InputMethod.SHOW_EXPLICIT) == 0 && mKeyboardSwitcher.hasHardwareKeyboard()) {
+            // Even when IME is implicitly shown and physical keyboard is connected, we should
+            // show {@link InputView}.
+            // See {@link InputMethodService#onShowInputRequested(int,boolean)}.
+            return true;
+        }
+        return super.onShowInputRequested(flags, configChange);
+    }
+
+    @Override
     public boolean onEvaluateFullscreenMode() {
         if (mKeyboardSwitcher.hasHardwareKeyboard()) {
             // If there is a hardware keyboard, disable full screen mode.
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index ea63cef..0355576 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -16,9 +16,13 @@
 
 package com.android.inputmethod.latin;
 
+import android.graphics.Color;
 import android.inputmethodservice.InputMethodService;
 import android.os.Build;
+import android.text.SpannableStringBuilder;
+import android.text.Spanned;
 import android.text.TextUtils;
+import android.text.style.BackgroundColorSpan;
 import android.util.Log;
 import android.view.KeyEvent;
 import android.view.inputmethod.CompletionInfo;
@@ -81,6 +85,18 @@
      */
     private final StringBuilder mComposingText = new StringBuilder();
 
+    /**
+     * This variable is a temporary object used in
+     * {@link #commitTextWithBackgroundColor(CharSequence, int, int)} to avoid object creation.
+     */
+    private SpannableStringBuilder mTempObjectForCommitText = new SpannableStringBuilder();
+    /**
+     * This variable is used to track whether the last committed text had the background color or
+     * not.
+     * TODO: Omit this flag if possible.
+     */
+    private boolean mLastCommittedTextHasBackgroundColor = false;
+
     private final InputMethodService mParent;
     InputConnection mIC;
     int mNestLevel;
@@ -219,12 +235,37 @@
         // it works, but it's wrong and should be fixed.
         mCommittedTextBeforeComposingText.append(mComposingText);
         mComposingText.setLength(0);
+        // TODO: Clear this flag in setComposingRegion() and setComposingText() as well if needed.
+        mLastCommittedTextHasBackgroundColor = false;
         if (null != mIC) {
             mIC.finishComposingText();
         }
     }
 
-    public void commitText(final CharSequence text, final int i) {
+    /**
+     * Synonym of {@code commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT}.
+     * @param text The text to commit. This may include styles.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param newCursorPosition The new cursor position around the text.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     */
+    public void commitText(final CharSequence text, final int newCursorPosition) {
+        commitTextWithBackgroundColor(text, newCursorPosition, Color.TRANSPARENT);
+    }
+
+    /**
+     * Calls {@link InputConnection#commitText(CharSequence, int)} with the given background color.
+     * @param text The text to commit. This may include styles.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param newCursorPosition The new cursor position around the text.
+     * See {@link InputConnection#commitText(CharSequence, int)}.
+     * @param color The background color to be attached. Set {@link Color#TRANSPARENT} to disable
+     * the background color. Note that this method specifies {@link BackgroundColorSpan} with
+     * {@link Spanned#SPAN_COMPOSING} flag, meaning that the background color persists until
+     * {@link #finishComposingText()} is called.
+     */
+    public void commitTextWithBackgroundColor(final CharSequence text, final int newCursorPosition,
+            final int color) {
         if (DEBUG_BATCH_NESTING) checkBatchEdit();
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
         mCommittedTextBeforeComposingText.append(text);
@@ -234,11 +275,43 @@
         mExpectedSelStart += text.length() - mComposingText.length();
         mExpectedSelEnd = mExpectedSelStart;
         mComposingText.setLength(0);
+        mLastCommittedTextHasBackgroundColor = false;
         if (null != mIC) {
-            mIC.commitText(text, i);
+            if (color == Color.TRANSPARENT) {
+                mIC.commitText(text, newCursorPosition);
+            } else {
+                mTempObjectForCommitText.clear();
+                mTempObjectForCommitText.append(text);
+                final BackgroundColorSpan backgroundColorSpan = new BackgroundColorSpan(color);
+                mTempObjectForCommitText.setSpan(backgroundColorSpan, 0, text.length(),
+                        Spanned.SPAN_COMPOSING | Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+                mIC.commitText(mTempObjectForCommitText, newCursorPosition);
+                mLastCommittedTextHasBackgroundColor = true;
+            }
         }
     }
 
+    /**
+     * Removes the background color from the highlighted text if necessary. Should be called while
+     * there is no on-going composing text.
+     *
+     * <p>CAVEAT: This method internally calls {@link InputConnection#finishComposingText()}.
+     * Be careful of any unexpected side effects.</p>
+     */
+    public void removeBackgroundColorFromHighlightedTextIfNecessary() {
+        // TODO: We haven't yet full tested if we really need to check this flag or not. Omit this
+        // flag if everything works fine without this condition.
+        if (!mLastCommittedTextHasBackgroundColor) {
+            return;
+        }
+        if (mComposingText.length() > 0) {
+            Log.e(TAG, "clearSpansWithComposingFlags should be called when composing text is " +
+                    "empty. mComposingText=" + mComposingText);
+            return;
+        }
+        finishComposingText();
+    }
+
     public CharSequence getSelectedText(final int flags) {
         return (null == mIC) ? null : mIC.getSelectedText(flags);
     }
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index e43c182..0d5ce7d 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -298,10 +298,14 @@
         return INDEX_NOT_FOUND;
     }
 
-    public InputMethodSubtype getCurrentInputMethodSubtype(
-            final InputMethodSubtype defaultSubtype) {
+    public RichInputMethodSubtype getCurrentInputMethodSubtype(
+            final RichInputMethodSubtype defaultSubtype) {
         final InputMethodSubtype currentSubtype = mImmWrapper.mImm.getCurrentInputMethodSubtype();
-        return (currentSubtype != null) ? currentSubtype : defaultSubtype;
+        if (currentSubtype == null) {
+            return defaultSubtype;
+        }
+        // TODO: Determine locales to use for multi-lingual use.
+        return new RichInputMethodSubtype(currentSubtype);
     }
 
     public boolean hasMultipleEnabledIMEsOrSubtypes(final boolean shouldIncludeAuxiliarySubtypes) {
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
new file mode 100644
index 0000000..0b08c48
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodSubtype.java
@@ -0,0 +1,125 @@
+/*
+ * 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;
+
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.latin.utils.LocaleUtils;
+import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
+
+import java.util.Arrays;
+import java.util.Locale;
+
+/**
+ * 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 final InputMethodSubtype mSubtype;
+    private final Locale[] mLocales;
+
+    public RichInputMethodSubtype(final InputMethodSubtype subtype, final Locale... locales) {
+        mSubtype = subtype;
+        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) {
+        return mSubtype.getExtraValueOf(key);
+    }
+
+    // The mode is also determined by the primary subtype.
+    public String getMode() {
+        return mSubtype.getMode();
+    }
+
+    public boolean isNoLanguage() {
+        if (mLocales.length > 1) {
+            return false;
+        }
+        return SubtypeLocaleUtils.NO_LANGUAGE.equals(mSubtype.getLocale());
+    }
+
+    public String getNameForLogging() {
+        return toString();
+    }
+
+    // InputMethodSubtype's display name for spacebar text in its locale.
+    //        isAdditionalSubtype (T=true, F=false)
+    // locale layout  |  Middle      Full
+    // ------ ------- - --------- ----------------------
+    //  en_US qwerty  F  English   English (US)           exception
+    //  en_GB qwerty  F  English   English (UK)           exception
+    //  es_US spanish F  Español   Español (EE.UU.)       exception
+    //  fr    azerty  F  Français  Français
+    //  fr_CA qwerty  F  Français  Français (Canada)
+    //  fr_CH swiss   F  Français  Français (Suisse)
+    //  de    qwertz  F  Deutsch   Deutsch
+    //  de_CH swiss   T  Deutsch   Deutsch (Schweiz)
+    //  zz    qwerty  F  QWERTY    QWERTY
+    //  fr    qwertz  T  Français  Français
+    //  de    qwerty  T  Deutsch   Deutsch
+    //  en_US azerty  T  English   English (US)
+    //  zz    azerty  T  AZERTY    AZERTY
+    // Get the RichInputMethodSubtype's full display name in its locale.
+    public String getFullDisplayName() {
+        if (isNoLanguage()) {
+            return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
+        }
+        return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(mSubtype.getLocale());
+    }
+
+    // Get the RichInputMethodSubtype's middle display name in its locale.
+    public String getMiddleDisplayName() {
+        if (isNoLanguage()) {
+            return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(mSubtype);
+        }
+        return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(mSubtype.getLocale());
+    }
+
+    @Override
+    public boolean equals(Object o) {
+        if (!(o instanceof RichInputMethodSubtype)) {
+            return false;
+        }
+        final RichInputMethodSubtype other = (RichInputMethodSubtype)o;
+        return mSubtype.equals(other.mSubtype) && Arrays.equals(mLocales, other.mLocales);
+    }
+
+    @Override
+    public int hashCode() {
+        return mSubtype.hashCode() + Arrays.hashCode(mLocales);
+    }
+
+    @Override
+    public String toString() {
+        return "Multi-lingual subtype: " + mSubtype.toString() + ", " + Arrays.toString(mLocales);
+    }
+
+    // TODO: remove this method! We can always have several locales. Multi-lingual input will only
+    // be done when this method is gone.
+    public String getLocale() {
+        return mSubtype.getLocale();
+    }
+
+    // TODO: remove this method
+    public InputMethodSubtype getRawSubtype() { return mSubtype; }
+}
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index a725e16..45d67ff 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -58,8 +58,8 @@
             new LanguageOnSpacebarHelper();
     private InputMethodInfo mShortcutInputMethodInfo;
     private InputMethodSubtype mShortcutSubtype;
-    private InputMethodSubtype mNoLanguageSubtype;
-    private InputMethodSubtype mEmojiSubtype;
+    private RichInputMethodSubtype mNoLanguageSubtype;
+    private RichInputMethodSubtype mEmojiSubtype;
     private boolean mIsNetworkConnected;
 
     private static final String KEYBOARD_MODE = "keyboard";
@@ -70,26 +70,26 @@
             + "," + Constants.Subtype.ExtraValue.ASCII_CAPABLE
             + "," + Constants.Subtype.ExtraValue.ENABLED_WHEN_DEFAULT_IS_NOT_ASCII_CAPABLE
             + "," + Constants.Subtype.ExtraValue.EMOJI_CAPABLE;
-    private static final InputMethodSubtype DUMMY_NO_LANGUAGE_SUBTYPE =
-            InputMethodSubtypeCompatUtils.newInputMethodSubtype(
+    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);
+                    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 InputMethodSubtype DUMMY_EMOJI_SUBTYPE =
+    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);
+                    SUBTYPE_ID_OF_DUMMY_EMOJI_SUBTYPE));
 
     public static SubtypeSwitcher getInstance() {
         return sInstance;
@@ -165,18 +165,17 @@
     }
 
     // Update the current subtype. LatinIME.onCurrentInputMethodSubtypeChanged calls this function.
-    public void onSubtypeChanged(final InputMethodSubtype newSubtype) {
+    public void onSubtypeChanged(final RichInputMethodSubtype newSubtype) {
         if (DBG) {
-            Log.w(TAG, "onSubtypeChanged: "
-                    + SubtypeLocaleUtils.getSubtypeNameForLogging(newSubtype));
+            Log.w(TAG, "onSubtypeChanged: " + newSubtype.getNameForLogging());
         }
 
         final Locale newLocale = SubtypeLocaleUtils.getSubtypeLocale(newSubtype);
         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);
+        final boolean implicitlyEnabled = mRichImm
+                .checkIfSubtypeBelongsToThisImeAndImplicitlyEnabled(newSubtype.getRawSubtype());
         mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(
                 sameLocale || (sameLanguage && implicitlyEnabled));
 
@@ -250,7 +249,7 @@
     // Subtype Switching functions //
     //////////////////////////////////
 
-    public int getLanguageOnSpacebarFormatType(final InputMethodSubtype subtype) {
+    public int getLanguageOnSpacebarFormatType(final RichInputMethodSubtype subtype) {
         return mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(subtype);
     }
 
@@ -279,10 +278,10 @@
         return true;
     }
 
-    private static InputMethodSubtype sForcedSubtypeForTesting = null;
+    private static RichInputMethodSubtype sForcedSubtypeForTesting = null;
     @UsedForTesting
     void forceSubtype(final InputMethodSubtype subtype) {
-        sForcedSubtypeForTesting = subtype;
+        sForcedSubtypeForTesting = new RichInputMethodSubtype(subtype);
     }
 
     public Locale getCurrentSubtypeLocale() {
@@ -292,17 +291,18 @@
         return SubtypeLocaleUtils.getSubtypeLocale(getCurrentSubtype());
     }
 
-    public InputMethodSubtype getCurrentSubtype() {
+    public RichInputMethodSubtype getCurrentSubtype() {
         if (null != sForcedSubtypeForTesting) {
             return sForcedSubtypeForTesting;
         }
         return mRichImm.getCurrentInputMethodSubtype(getNoLanguageSubtype());
     }
 
-    public InputMethodSubtype getNoLanguageSubtype() {
+    public RichInputMethodSubtype getNoLanguageSubtype() {
         if (mNoLanguageSubtype == null) {
-            mNoLanguageSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY);
+            mNoLanguageSubtype = new RichInputMethodSubtype(
+                    mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                            SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.QWERTY));
         }
         if (mNoLanguageSubtype != null) {
             return mNoLanguageSubtype;
@@ -313,10 +313,14 @@
         return DUMMY_NO_LANGUAGE_SUBTYPE;
     }
 
-    public InputMethodSubtype getEmojiSubtype() {
+    public RichInputMethodSubtype getEmojiSubtype() {
         if (mEmojiSubtype == null) {
-            mEmojiSubtype = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                    SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+            final InputMethodSubtype rawEmojiSubtype =
+                    mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                        SubtypeLocaleUtils.NO_LANGUAGE, SubtypeLocaleUtils.EMOJI);
+            if (null != rawEmojiSubtype) {
+                mEmojiSubtype = new RichInputMethodSubtype(rawEmojiSubtype);
+            }
         }
         if (mEmojiSubtype != null) {
             return mEmojiSubtype;
@@ -328,6 +332,6 @@
     }
 
     public String getCombiningRulesExtraValueOfCurrentSubtype() {
-        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype());
+        return SubtypeLocaleUtils.getCombiningRulesExtraValue(getCurrentSubtype().getRawSubtype());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index d7693af..38fcb68 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -19,6 +19,7 @@
 import android.text.TextUtils;
 import android.view.inputmethod.CompletionInfo;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.define.DebugFlags;
 import com.android.inputmethod.latin.utils.StringUtils;
 
@@ -420,4 +421,18 @@
                 mWillAutoCorrect, mIsObsoleteSuggestions, mIsPrediction,
                 INPUT_STYLE_TAIL_BATCH);
     }
+
+    /**
+     * @return the {@link SuggestedWordInfo} which corresponds to the word that is originally
+     * typed by the user. Otherwise returns {@code null}. Note that gesture input is not
+     * considered to be a typed word.
+     */
+    @UsedForTesting
+    public SuggestedWordInfo getTypedWordInfoOrNull() {
+        if (this == EMPTY) {
+            return null;
+        }
+        final SuggestedWordInfo info = getInfo(SuggestedWords.INDEX_OF_TYPED_WORD);
+        return (info.getKind() == SuggestedWordInfo.KIND_TYPED) ? info : null;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
index 348bae6..616828e 100644
--- a/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
+++ b/java/src/com/android/inputmethod/latin/inputlogic/InputLogic.java
@@ -16,6 +16,7 @@
 
 package com.android.inputmethod.latin.inputlogic;
 
+import android.graphics.Color;
 import android.os.SystemClock;
 import android.text.SpannableString;
 import android.text.TextUtils;
@@ -232,6 +233,20 @@
     }
 
     /**
+     * Determines whether "Touch again to save" should be shown or not.
+     * @param suggestionInfo the suggested word chosen by the user.
+     * @return {@code true} if we should show the "Touch again to save" hint.
+     */
+    private boolean shouldShowAddToDictionaryHint(final SuggestedWordInfo suggestionInfo) {
+        // We should show the "Touch again to save" hint if the user pressed the first entry
+        // AND it's in none of our current dictionaries (main, user or otherwise).
+        return (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED)
+                || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION))
+                && !mDictionaryFacilitator.isValidWord(suggestionInfo.mWord, true /* ignoreCase */)
+                && mDictionaryFacilitator.isUserDictionaryEnabled();
+    }
+
+    /**
      * A suggestion was picked from the suggestion strip.
      * @param settingsValues the current values of the settings.
      * @param suggestionInfo the suggestion info.
@@ -297,14 +312,7 @@
         mSpaceState = SpaceState.PHANTOM;
         inputTransaction.requireShiftUpdate(InputTransaction.SHIFT_UPDATE_NOW);
 
-        // We should show the "Touch again to save" hint if the user pressed the first entry
-        // AND it's in none of our current dictionaries (main, user or otherwise).
-        final boolean showingAddToDictionaryHint =
-                (suggestionInfo.isKindOf(SuggestedWordInfo.KIND_TYPED)
-                        || suggestionInfo.isKindOf(SuggestedWordInfo.KIND_OOV_CORRECTION))
-                        && !mDictionaryFacilitator.isValidWord(suggestion, true /* ignoreCase */);
-
-        if (showingAddToDictionaryHint && mDictionaryFacilitator.isUserDictionaryEnabled()) {
+        if (shouldShowAddToDictionaryHint(suggestionInfo)) {
             mSuggestionStripViewAccessor.showAddToDictionaryHint(suggestion);
         } else {
             // If we're not showing the "Touch again to save", then update the suggestion strip.
@@ -743,6 +751,13 @@
             final InputTransaction inputTransaction,
             // TODO: remove this argument
             final LatinIME.UIHandler handler) {
+        // In case the "add to dictionary" hint was still displayed.
+        // TODO: Do we really need to check if we have composing text here?
+        if (!mWordComposer.isComposingWord() &&
+                mSuggestionStripViewAccessor.isShowingAddToDictionaryHint()) {
+            mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
+        }
+
         final int codePoint = event.mCodePoint;
         mSpaceState = SpaceState.NONE;
         if (inputTransaction.mSettingsValues.isWordSeparator(codePoint)
@@ -838,8 +853,6 @@
             } else {
                 sendKeyCodePoint(settingsValues, codePoint);
             }
-            // In case the "add to dictionary" hint was still displayed.
-            mSuggestionStripViewAccessor.dismissAddToDictionaryHint();
         }
         inputTransaction.setRequiresUpdateSuggestions();
     }
@@ -1994,7 +2007,9 @@
     }
 
     /**
-     * Commits the chosen word to the text field and saves it for later retrieval.
+     * Commits the chosen word to the text field and saves it for later retrieval. This is a
+     * synonym of {@code commitChosenWordWithBackgroundColor(settingsValues, chosenWord,
+     * commitType, separatorString, Color.TRANSPARENT}.
      *
      * @param settingsValues the current values of the settings.
      * @param chosenWord the word we want to commit.
@@ -2003,6 +2018,23 @@
      */
     private void commitChosenWord(final SettingsValues settingsValues, final String chosenWord,
             final int commitType, final String separatorString) {
+        commitChosenWordWithBackgroundColor(settingsValues, chosenWord, commitType, separatorString,
+                Color.TRANSPARENT);
+    }
+
+    /**
+     * Commits the chosen word to the text field and saves it for later retrieval.
+     *
+     * @param settingsValues the current values of the settings.
+     * @param chosenWord the word we want to commit.
+     * @param commitType the type of the commit, as one of LastComposedWord.COMMIT_TYPE_*
+     * @param separatorString the separator that's causing the commit, or NOT_A_SEPARATOR if none.
+     * @param backgroundColor the background color to be specified with the committed text. Pass
+     * {@link Color#TRANSPARENT} to not specify the background color.
+     */
+    private void commitChosenWordWithBackgroundColor(final SettingsValues settingsValues,
+            final String chosenWord, final int commitType, final String separatorString,
+            final int backgroundColor) {
         final SuggestedWords suggestedWords = mSuggestedWords;
         final CharSequence chosenWordWithSuggestions =
                 SuggestionSpanUtils.getTextWithSuggestionSpan(mLatinIME, chosenWord,
@@ -2012,7 +2044,7 @@
         // information from the 1st previous word.
         final PrevWordsInfo prevWordsInfo = mConnection.getPrevWordsInfoFromNthPreviousWord(
                 settingsValues.mSpacingAndPunctuations, mWordComposer.isComposingWord() ? 2 : 1);
-        mConnection.commitText(chosenWordWithSuggestions, 1);
+        mConnection.commitTextWithBackgroundColor(chosenWordWithSuggestions, 1, backgroundColor);
         // Add the word to the user history dictionary
         performAdditionToUserHistoryDictionary(settingsValues, chosenWord, prevWordsInfo);
         // TODO: figure out here if this is an auto-correct or if the best word is actually
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 90398de..47bff3e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -39,6 +39,7 @@
 import com.android.inputmethod.latin.DictionaryFactory;
 import com.android.inputmethod.latin.PrevWordsInfo;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
 import com.android.inputmethod.latin.UserBinaryDictionary;
@@ -334,7 +335,7 @@
         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(this, editorInfo);
         builder.setKeyboardGeometry(
                 SPELLCHECKER_DUMMY_KEYBOARD_WIDTH, SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT);
-        builder.setSubtype(subtype);
+        builder.setSubtype(new RichInputMethodSubtype(subtype));
         builder.setIsSpellChecker(true /* isSpellChecker */);
         builder.disableTouchPositionCorrectionData();
         return builder.build();
diff --git a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
index 2797328..2207ffe 100644
--- a/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
+++ b/java/src/com/android/inputmethod/latin/utils/DistracterFilterCheckingExactMatchesAndSuggestions.java
@@ -35,6 +35,7 @@
 import com.android.inputmethod.keyboard.KeyboardLayoutSet;
 import com.android.inputmethod.latin.DictionaryFacilitator;
 import com.android.inputmethod.latin.PrevWordsInfo;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.WordComposer;
 import com.android.inputmethod.latin.settings.SettingsValuesForSuggestion;
@@ -131,7 +132,7 @@
         final int keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(res);
         final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
         builder.setKeyboardGeometry(keyboardWidth, keyboardHeight);
-        builder.setSubtype(subtype);
+        builder.setSubtype(new RichInputMethodSubtype(subtype));
         builder.setIsSpellChecker(false /* isSpellChecker */);
         final KeyboardLayoutSet layoutSet = builder.build();
         mKeyboard = layoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
diff --git a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java b/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java
deleted file mode 100644
index 1ca895f..0000000
--- a/java/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtils.java
+++ /dev/null
@@ -1,58 +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.latin.utils;
-
-import android.view.inputmethod.InputMethodSubtype;
-
-public final class SpacebarLanguageUtils {
-    private SpacebarLanguageUtils() {
-        // Intentional empty constructor for utility class.
-    }
-
-    // InputMethodSubtype's display name for spacebar text in its locale.
-    //        isAdditionalSubtype (T=true, F=false)
-    // locale layout  |  Middle      Full
-    // ------ ------- - --------- ----------------------
-    //  en_US qwerty  F  English   English (US)           exception
-    //  en_GB qwerty  F  English   English (UK)           exception
-    //  es_US spanish F  Español   Español (EE.UU.)       exception
-    //  fr    azerty  F  Français  Français
-    //  fr_CA qwerty  F  Français  Français (Canada)
-    //  fr_CH swiss   F  Français  Français (Suisse)
-    //  de    qwertz  F  Deutsch   Deutsch
-    //  de_CH swiss   T  Deutsch   Deutsch (Schweiz)
-    //  zz    qwerty  F  QWERTY    QWERTY
-    //  fr    qwertz  T  Français  Français
-    //  de    qwerty  T  Deutsch   Deutsch
-    //  en_US azerty  T  English   English (US)
-    //  zz    azerty  T  AZERTY    AZERTY
-    // Get InputMethodSubtype's full display name in its locale.
-    public static String getFullDisplayName(final InputMethodSubtype subtype) {
-        if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
-            return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
-        }
-        return SubtypeLocaleUtils.getSubtypeLocaleDisplayName(subtype.getLocale());
-    }
-
-    // Get InputMethodSubtype's middle display name in its locale.
-    public static String getMiddleDisplayName(final InputMethodSubtype subtype) {
-        if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
-            return SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype);
-        }
-        return SubtypeLocaleUtils.getSubtypeLanguageDisplayName(subtype.getLocale());
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
index 0db4106..96a6510 100644
--- a/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtils.java
@@ -27,12 +27,17 @@
 
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 
 import java.util.Arrays;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Locale;
 
+/**
+ * A helper class to deal with subtype locales.
+  */
+// TODO: consolidate this into RichInputMethodSubtype
 public final class SubtypeLocaleUtils {
     private static final String TAG = SubtypeLocaleUtils.class.getSimpleName();
 
@@ -109,10 +114,10 @@
             sKeyboardLayoutToNameIdsMap.put(key, noLanguageResId);
         }
 
-        final String[] excetionalLocaleInRootLocale = res.getStringArray(
+        final String[] exceptionalLocaleInRootLocale = res.getStringArray(
                 R.array.subtype_locale_displayed_in_root_locale);
-        for (int i = 0; i < excetionalLocaleInRootLocale.length; i++) {
-            sExceptionalLocaleDisplayedInRootLocale.add(excetionalLocaleInRootLocale[i]);
+        for (int i = 0; i < exceptionalLocaleInRootLocale.length; i++) {
+            sExceptionalLocaleDisplayedInRootLocale.add(exceptionalLocaleInRootLocale[i]);
         }
 
         final String[] exceptionalLocales = res.getStringArray(
@@ -260,6 +265,7 @@
     private static String getSubtypeDisplayNameInternal(final InputMethodSubtype subtype,
             final Locale displayLocale) {
         final String replacementString = getReplacementString(subtype, displayLocale);
+        // TODO: rework this for multi-lingual subtypes
         final int nameResId = subtype.getNameResId();
         final RunInLocale<String> getSubtypeName = new RunInLocale<String>() {
             @Override
@@ -282,12 +288,14 @@
                 getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
     }
 
-    public static boolean isNoLanguage(final InputMethodSubtype subtype) {
+    public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
         final String localeString = subtype.getLocale();
-        return NO_LANGUAGE.equals(localeString);
+        return LocaleUtils.constructLocaleFromString(localeString);
     }
 
-    public static Locale getSubtypeLocale(final InputMethodSubtype subtype) {
+    // TODO: remove this. When RichInputMethodSubtype#getLocale is removed we can do away with this
+    // method at the same time.
+    public static Locale getSubtypeLocale(final RichInputMethodSubtype subtype) {
         final String localeString = subtype.getLocale();
         return LocaleUtils.constructLocaleFromString(localeString);
     }
@@ -301,6 +309,10 @@
         return sKeyboardLayoutToDisplayNameMap.get(layoutName);
     }
 
+    public static String getKeyboardLayoutSetName(final RichInputMethodSubtype subtype) {
+        return getKeyboardLayoutSetName(subtype.getRawSubtype());
+    }
+
     public static String getKeyboardLayoutSetName(final InputMethodSubtype subtype) {
         String keyboardLayoutSet = subtype.getExtraValueOf(KEYBOARD_LAYOUT_SET);
         if (keyboardLayoutSet == null) {
@@ -336,7 +348,7 @@
         return Arrays.binarySearch(SORTED_RTL_LANGUAGES, language) >= 0;
     }
 
-    public static boolean isRtlLanguage(final InputMethodSubtype subtype) {
+    public static boolean isRtlLanguage(final RichInputMethodSubtype subtype) {
         return isRtlLanguage(getSubtypeLocale(subtype));
     }
 
diff --git a/native/jni/NativeFileList.mk b/native/jni/NativeFileList.mk
index 7a732a5..4a89b2b 100644
--- a/native/jni/NativeFileList.mk
+++ b/native/jni/NativeFileList.mk
@@ -128,6 +128,7 @@
     suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content_test.cpp \
     suggest/policyimpl/dictionary/structure/v4/content/probability_entry_test.cpp \
     suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer_test.cpp \
+    suggest/policyimpl/dictionary/utils/byte_array_utils_test.cpp \
     suggest/policyimpl/dictionary/utils/trie_map_test.cpp \
     utils/autocorrection_threshold_utils_test.cpp \
     utils/int_array_view_test.cpp
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
index bbcea2e..ea2d24e 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.cpp
@@ -16,6 +16,11 @@
 
 #include "suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h"
 
+#include <algorithm>
+#include <cstring>
+
+#include "suggest/policyimpl/dictionary/utils/forgetting_curve_utils.h"
+
 namespace latinime {
 
 bool LanguageModelDictContent::save(FILE *const file) const {
@@ -66,6 +71,19 @@
     return mTrieMap.remove(wordId, bitmapEntryIndex);
 }
 
+bool LanguageModelDictContent::truncateEntries(const int *const entryCounts,
+        const int *const maxEntryCounts, const HeaderPolicy *const headerPolicy) {
+    for (int i = 0; i <= MAX_PREV_WORD_COUNT_FOR_N_GRAM; ++i) {
+        if (entryCounts[i] <= maxEntryCounts[i]) {
+            continue;
+        }
+        if (!turncateEntriesInSpecifiedLevel(headerPolicy, maxEntryCounts[i], i)) {
+            return false;
+        }
+    }
+    return true;
+}
+
 bool LanguageModelDictContent::runGCInner(
         const TerminalPositionLookupTable::TerminalIdMap *const terminalIdMap,
         const TrieMap::TrieMapRange trieMapRange,
@@ -118,4 +136,129 @@
     return bitmapEntryIndex;
 }
 
+bool LanguageModelDictContent::updateAllProbabilityEntriesInner(const int bitmapEntryIndex,
+        const int level, const HeaderPolicy *const headerPolicy, int *const outEntryCounts) {
+    for (const auto &entry : mTrieMap.getEntriesInSpecifiedLevel(bitmapEntryIndex)) {
+        if (level > MAX_PREV_WORD_COUNT_FOR_N_GRAM) {
+            AKLOGE("Invalid level. level: %d, MAX_PREV_WORD_COUNT_FOR_N_GRAM: %d.",
+                    level, MAX_PREV_WORD_COUNT_FOR_N_GRAM);
+            return false;
+        }
+        const ProbabilityEntry probabilityEntry =
+                ProbabilityEntry::decode(entry.value(), mHasHistoricalInfo);
+        if (mHasHistoricalInfo && !probabilityEntry.representsBeginningOfSentence()) {
+            const HistoricalInfo historicalInfo = ForgettingCurveUtils::createHistoricalInfoToSave(
+                    probabilityEntry.getHistoricalInfo(), headerPolicy);
+            if (ForgettingCurveUtils::needsToKeep(&historicalInfo, headerPolicy)) {
+                // Update the entry.
+                const ProbabilityEntry updatedEntry(probabilityEntry.getFlags(), &historicalInfo);
+                if (!mTrieMap.put(entry.key(), updatedEntry.encode(mHasHistoricalInfo),
+                        bitmapEntryIndex)) {
+                    return false;
+                }
+            } else {
+                // Remove the entry.
+                if (!mTrieMap.remove(entry.key(), bitmapEntryIndex)) {
+                    return false;
+                }
+                continue;
+            }
+        }
+        if (!probabilityEntry.representsBeginningOfSentence()) {
+            outEntryCounts[level] += 1;
+        }
+        if (!entry.hasNextLevelMap()) {
+            continue;
+        }
+        if (!updateAllProbabilityEntriesInner(entry.getNextLevelBitmapEntryIndex(), level + 1,
+                headerPolicy, outEntryCounts)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool LanguageModelDictContent::turncateEntriesInSpecifiedLevel(
+        const HeaderPolicy *const headerPolicy, const int maxEntryCount, const int targetLevel) {
+    std::vector<int> prevWordIds;
+    std::vector<EntryInfoToTurncate> entryInfoVector;
+    if (!getEntryInfo(headerPolicy, targetLevel, mTrieMap.getRootBitmapEntryIndex(),
+            &prevWordIds, &entryInfoVector)) {
+        return false;
+    }
+    if (static_cast<int>(entryInfoVector.size()) <= maxEntryCount) {
+        return true;
+    }
+    const int entryCountToRemove = static_cast<int>(entryInfoVector.size()) - maxEntryCount;
+    std::partial_sort(entryInfoVector.begin(), entryInfoVector.begin() + entryCountToRemove,
+            entryInfoVector.end(),
+            EntryInfoToTurncate::Comparator());
+    for (int i = 0; i < entryCountToRemove; ++i) {
+        const EntryInfoToTurncate &entryInfo = entryInfoVector[i];
+        if (!removeNgramProbabilityEntry(
+                WordIdArrayView(entryInfo.mPrevWordIds, entryInfo.mEntryLevel), entryInfo.mKey)) {
+            return false;
+        }
+    }
+    return true;
+}
+
+bool LanguageModelDictContent::getEntryInfo(const HeaderPolicy *const headerPolicy,
+        const int targetLevel, const int bitmapEntryIndex,  std::vector<int> *const prevWordIds,
+        std::vector<EntryInfoToTurncate> *const outEntryInfo) const {
+    const int currentLevel = prevWordIds->size();
+    for (const auto &entry : mTrieMap.getEntriesInSpecifiedLevel(bitmapEntryIndex)) {
+        if (currentLevel < targetLevel) {
+            if (!entry.hasNextLevelMap()) {
+                continue;
+            }
+            prevWordIds->push_back(entry.key());
+            if (!getEntryInfo(headerPolicy, targetLevel, entry.getNextLevelBitmapEntryIndex(),
+                    prevWordIds, outEntryInfo)) {
+                return false;
+            }
+            prevWordIds->pop_back();
+            continue;
+        }
+        const ProbabilityEntry probabilityEntry =
+                ProbabilityEntry::decode(entry.value(), mHasHistoricalInfo);
+        const int probability = (mHasHistoricalInfo) ?
+                ForgettingCurveUtils::decodeProbability(probabilityEntry.getHistoricalInfo(),
+                        headerPolicy) : probabilityEntry.getProbability();
+        outEntryInfo->emplace_back(probability,
+                probabilityEntry.getHistoricalInfo()->getTimeStamp(),
+                entry.key(), targetLevel, prevWordIds->data());
+    }
+    return true;
+}
+
+bool LanguageModelDictContent::EntryInfoToTurncate::Comparator::operator()(
+        const EntryInfoToTurncate &left, const EntryInfoToTurncate &right) const {
+    if (left.mProbability != right.mProbability) {
+        return left.mProbability < right.mProbability;
+    }
+    if (left.mTimestamp != right.mTimestamp) {
+        return left.mTimestamp > right.mTimestamp;
+    }
+    if (left.mKey != right.mKey) {
+        return left.mKey < right.mKey;
+    }
+    if (left.mEntryLevel != right.mEntryLevel) {
+        return left.mEntryLevel > right.mEntryLevel;
+    }
+    for (int i = 0; i < left.mEntryLevel; ++i) {
+        if (left.mPrevWordIds[i] != right.mPrevWordIds[i]) {
+            return left.mPrevWordIds[i] < right.mPrevWordIds[i];
+        }
+    }
+    // left and rigth represent the same entry.
+    return false;
+}
+
+LanguageModelDictContent::EntryInfoToTurncate::EntryInfoToTurncate(const int probability,
+        const int timestamp, const int key, const int entryLevel, const int *const prevWordIds)
+        : mProbability(probability), mTimestamp(timestamp), mKey(key), mEntryLevel(entryLevel) {
+    memmove(mPrevWordIds, prevWordIds, mEntryLevel * sizeof(mPrevWordIds[0]));
+}
+
 } // namespace latinime
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
index bd07f2f..43b2aab 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/content/language_model_dict_content.h
@@ -18,6 +18,7 @@
 #define LATINIME_LANGUAGE_MODEL_DICT_CONTENT_H
 
 #include <cstdio>
+#include <vector>
 
 #include "defines.h"
 #include "suggest/policyimpl/dictionary/structure/v4/content/probability_entry.h"
@@ -29,6 +30,8 @@
 
 namespace latinime {
 
+class HeaderPolicy;
+
 /**
  * Class representing language model.
  *
@@ -73,9 +76,45 @@
 
     bool removeNgramProbabilityEntry(const WordIdArrayView prevWordIds, const int wordId);
 
+    bool updateAllProbabilityEntries(const HeaderPolicy *const headerPolicy,
+            int *const outEntryCounts) {
+        for (int i = 0; i <= MAX_PREV_WORD_COUNT_FOR_N_GRAM; ++i) {
+            outEntryCounts[i] = 0;
+        }
+        return updateAllProbabilityEntriesInner(mTrieMap.getRootBitmapEntryIndex(), 0 /* level */,
+                headerPolicy, outEntryCounts);
+    }
+
+    // entryCounts should be created by updateAllProbabilityEntries.
+    bool truncateEntries(const int *const entryCounts, const int *const maxEntryCounts,
+            const HeaderPolicy *const headerPolicy);
+
  private:
     DISALLOW_COPY_AND_ASSIGN(LanguageModelDictContent);
 
+    class EntryInfoToTurncate {
+     public:
+        class Comparator {
+         public:
+            bool operator()(const EntryInfoToTurncate &left,
+                    const EntryInfoToTurncate &right) const;
+         private:
+            DISALLOW_ASSIGNMENT_OPERATOR(Comparator);
+        };
+
+        EntryInfoToTurncate(const int probability, const int timestamp, const int key,
+                const int entryLevel, const int *const prevWordIds);
+
+        int mProbability;
+        int mTimestamp;
+        int mKey;
+        int mEntryLevel;
+        int mPrevWordIds[MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1];
+
+     private:
+        DISALLOW_DEFAULT_CONSTRUCTOR(EntryInfoToTurncate);
+    };
+
     TrieMap mTrieMap;
     const bool mHasHistoricalInfo;
 
@@ -84,6 +123,13 @@
             int *const outNgramCount);
     int createAndGetBitmapEntryIndex(const WordIdArrayView prevWordIds);
     int getBitmapEntryIndex(const WordIdArrayView prevWordIds) const;
+    bool updateAllProbabilityEntriesInner(const int bitmapEntryIndex, const int level,
+            const HeaderPolicy *const headerPolicy, int *const outEntryCounts);
+    bool turncateEntriesInSpecifiedLevel(const HeaderPolicy *const headerPolicy,
+            const int maxEntryCount, const int targetLevel);
+    bool getEntryInfo(const HeaderPolicy *const headerPolicy, const int targetLevel,
+            const int bitmapEntryIndex, std::vector<int> *const prevWordIds,
+            std::vector<EntryInfoToTurncate> *const outEntryInfo) const;
 };
 } // namespace latinime
 #endif /* LATINIME_LANGUAGE_MODEL_DICT_CONTENT_H */
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
index fb6840b..b7c31bf 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_node_writer.cpp
@@ -161,29 +161,15 @@
     const ProbabilityEntry originalProbabilityEntry =
             mBuffers->getLanguageModelDictContent()->getProbabilityEntry(
                     toBeUpdatedPtNodeParams->getTerminalId());
-    if (originalProbabilityEntry.hasHistoricalInfo()) {
-        const HistoricalInfo historicalInfo = ForgettingCurveUtils::createHistoricalInfoToSave(
-                originalProbabilityEntry.getHistoricalInfo(), mHeaderPolicy);
-        const ProbabilityEntry probabilityEntry(originalProbabilityEntry.getFlags(),
-                &historicalInfo);
-        if (!mBuffers->getMutableLanguageModelDictContent()->setProbabilityEntry(
-                toBeUpdatedPtNodeParams->getTerminalId(), &probabilityEntry)) {
-            AKLOGE("Cannot write updated probability entry. terminalId: %d",
-                    toBeUpdatedPtNodeParams->getTerminalId());
-            return false;
-        }
-        const bool isValid = ForgettingCurveUtils::needsToKeep(&historicalInfo, mHeaderPolicy);
-        if (!isValid) {
-            if (!markPtNodeAsWillBecomeNonTerminal(toBeUpdatedPtNodeParams)) {
-                AKLOGE("Cannot mark PtNode as willBecomeNonTerminal.");
-                return false;
-            }
-        }
-        *outNeedsToKeepPtNode = isValid;
-    } else {
-        // No need to update probability.
+    if (originalProbabilityEntry.isValid()) {
         *outNeedsToKeepPtNode = true;
+        return true;
     }
+    if (!markPtNodeAsWillBecomeNonTerminal(toBeUpdatedPtNodeParams)) {
+        AKLOGE("Cannot mark PtNode as willBecomeNonTerminal.");
+        return false;
+    }
+    *outNeedsToKeepPtNode = false;
     return true;
 }
 
@@ -380,6 +366,7 @@
             isTerminal, ptNodeParams->getCodePointCount() > 1 /* hasMultipleChars */);
 }
 
+// TODO: Move probability handling code to LanguageModelDictContent.
 const ProbabilityEntry Ver4PatriciaTrieNodeWriter::createUpdatedEntryFrom(
         const ProbabilityEntry *const originalProbabilityEntry,
         const ProbabilityEntry *const probabilityEntry) const {
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 04e3018..2ea248e 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
@@ -127,21 +127,28 @@
     if (ptNodePos == NOT_A_DICT_POS) {
         return NOT_A_PROBABILITY;
     }
-    const PtNodeParams ptNodeParams(mNodeReader.fetchPtNodeParamsInBufferFromPtNodePos(ptNodePos));
+    const PtNodeParams ptNodeParams = mNodeReader.fetchPtNodeParamsInBufferFromPtNodePos(ptNodePos);
     if (ptNodeParams.isDeleted() || ptNodeParams.isBlacklisted() || ptNodeParams.isNotAWord()) {
         return NOT_A_PROBABILITY;
     }
     if (prevWordsPtNodePos) {
-        const int bigramsPosition = getBigramsPositionOfPtNode(prevWordsPtNodePos[0]);
-        BinaryDictionaryBigramsIterator bigramsIt(&mBigramPolicy, bigramsPosition);
-        while (bigramsIt.hasNext()) {
-            bigramsIt.next();
-            if (bigramsIt.getBigramPos() == ptNodePos
-                    && bigramsIt.getProbability() != NOT_A_PROBABILITY) {
-                return getProbability(ptNodeParams.getProbability(), bigramsIt.getProbability());
-            }
+        // TODO: Support n-gram.
+        const PtNodeParams prevWordPtNodeParams =
+                mNodeReader.fetchPtNodeParamsInBufferFromPtNodePos(prevWordsPtNodePos[0]);
+        const int prevWordTerminalId = prevWordPtNodeParams.getTerminalId();
+        const ProbabilityEntry probabilityEntry =
+                mBuffers->getLanguageModelDictContent()->getNgramProbabilityEntry(
+                        IntArrayView::fromObject(&prevWordTerminalId),
+                        ptNodeParams.getTerminalId());
+        if (!probabilityEntry.isValid()) {
+            return NOT_A_PROBABILITY;
         }
-        return NOT_A_PROBABILITY;
+        if (mHeaderPolicy->hasHistoricalInfoOfWords()) {
+            return ForgettingCurveUtils::decodeProbability(probabilityEntry.getHistoricalInfo(),
+                    mHeaderPolicy);
+        } else {
+            return probabilityEntry.getProbability();
+        }
     }
     return getProbability(ptNodeParams.getProbability(), NOT_A_PROBABILITY);
 }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
index 4220312..d53575a 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/structure/v4/ver4_patricia_trie_writing_helper.cpp
@@ -85,6 +85,27 @@
             mBuffers, headerPolicy, &ptNodeReader, &ptNodeArrayReader, &bigramPolicy,
             &shortcutPolicy);
 
+    int entryCountTable[MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1];
+    if (!mBuffers->getMutableLanguageModelDictContent()->updateAllProbabilityEntries(headerPolicy,
+            entryCountTable)) {
+        AKLOGE("Failed to update probabilities in language model dict content.");
+        return false;
+    }
+    if (headerPolicy->isDecayingDict()) {
+        int maxEntryCountTable[MAX_PREV_WORD_COUNT_FOR_N_GRAM + 1];
+        maxEntryCountTable[0] = headerPolicy->getMaxUnigramCount();
+        maxEntryCountTable[1] = headerPolicy->getMaxBigramCount();
+        for (size_t i = 2; i < NELEMS(maxEntryCountTable); ++i) {
+            // TODO: Have max n-gram count.
+            maxEntryCountTable[i] = headerPolicy->getMaxBigramCount();
+        }
+        if (!mBuffers->getMutableLanguageModelDictContent()->truncateEntries(entryCountTable,
+                maxEntryCountTable,  headerPolicy)) {
+            AKLOGE("Failed to truncate entries in language model dict content.");
+            return false;
+        }
+    }
+
     DynamicPtReadingHelper readingHelper(&ptNodeReader, &ptNodeArrayReader);
     readingHelper.initWithPtNodeArrayPos(rootPtNodeArrayPos);
     DynamicPtGcEventListeners
@@ -187,6 +208,7 @@
     return true;
 }
 
+// TODO: Remove.
 bool Ver4PatriciaTrieWritingHelper::truncateUnigrams(
         const Ver4PatriciaTrieNodeReader *const ptNodeReader,
         Ver4PatriciaTrieNodeWriter *const ptNodeWriter, const int maxUnigramCount) {
@@ -227,6 +249,7 @@
     return true;
 }
 
+// TODO: Remove.
 bool Ver4PatriciaTrieWritingHelper::truncateBigrams(const int maxBigramCount) {
     const TerminalPositionLookupTable *const terminalPosLookupTable =
             mBuffers->getTerminalPositionLookupTable();
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp b/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
index 833063c..ecbe792 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/buffer_with_extendable_buffer.cpp
@@ -31,7 +31,7 @@
 
 uint32_t BufferWithExtendableBuffer::readUintAndAdvancePosition(const int size,
         int *const pos) const {
-    const int value = readUint(size, *pos);
+    const uint32_t value = readUint(size, *pos);
     *pos += size;
     return value;
 }
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
index c0a9fcb..4b3c989 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/byte_array_utils.h
@@ -114,7 +114,7 @@
         return buffer[(*pos)++];
     }
 
-    static AK_FORCE_INLINE int readUint(const uint8_t *const buffer,
+    static AK_FORCE_INLINE uint32_t readUint(const uint8_t *const buffer,
             const int size, const int pos) {
         // size must be in 1 to 4.
         ASSERT(size >= 1 && size <= 4);
diff --git a/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h b/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
index 6d91790..c2aeac2 100644
--- a/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
+++ b/native/jni/src/suggest/policyimpl/dictionary/utils/trie_map.h
@@ -84,6 +84,10 @@
                 return mValue;
             }
 
+            AK_FORCE_INLINE int getNextLevelBitmapEntryIndex() const {
+                return mNextLevelBitmapEntryIndex;
+            }
+
          private:
             const TrieMap *const mTrieMap;
             const int mKey;
diff --git a/native/jni/tests/suggest/policyimpl/dictionary/utils/byte_array_utils_test.cpp b/native/jni/tests/suggest/policyimpl/dictionary/utils/byte_array_utils_test.cpp
new file mode 100644
index 0000000..a1c310d
--- /dev/null
+++ b/native/jni/tests/suggest/policyimpl/dictionary/utils/byte_array_utils_test.cpp
@@ -0,0 +1,92 @@
+/*
+ * 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 "suggest/policyimpl/dictionary/utils/byte_array_utils.h"
+
+#include <gtest/gtest.h>
+
+#include <cstdint>
+
+namespace latinime {
+namespace {
+
+TEST(ByteArrayUtilsTest, TestReadInt) {
+    const uint8_t buffer[] = { 0x1u, 0x8Au, 0x0u, 0xAAu };
+
+    EXPECT_EQ(0x01u, ByteArrayUtils::readUint8(buffer, 0));
+    EXPECT_EQ(0x8Au, ByteArrayUtils::readUint8(buffer, 1));
+    EXPECT_EQ(0x0u, ByteArrayUtils::readUint8(buffer, 2));
+    EXPECT_EQ(0xAAu, ByteArrayUtils::readUint8(buffer, 3));
+
+    EXPECT_EQ(0x018Au, ByteArrayUtils::readUint16(buffer, 0));
+    EXPECT_EQ(0x8A00u, ByteArrayUtils::readUint16(buffer, 1));
+    EXPECT_EQ(0xAAu, ByteArrayUtils::readUint16(buffer, 2));
+
+    EXPECT_EQ(0x18A00AAu, ByteArrayUtils::readUint32(buffer, 0));
+
+    int pos = 0;
+    EXPECT_EQ(0x18A00, ByteArrayUtils::readSint24AndAdvancePosition(buffer, &pos));
+    pos = 1;
+    EXPECT_EQ(-0xA00AA, ByteArrayUtils::readSint24AndAdvancePosition(buffer, &pos));
+}
+
+TEST(ByteArrayUtilsTest, TestWriteAndReadInt) {
+    uint8_t buffer[4];
+
+    int pos = 0;
+    const uint8_t data_1B = 0xC8;
+    ByteArrayUtils::writeUintAndAdvancePosition(buffer, data_1B, 1, &pos);
+    EXPECT_EQ(data_1B, ByteArrayUtils::readUint(buffer, 1, 0));
+
+    pos = 0;
+    const uint32_t data_4B = 0xABCD1234;
+    ByteArrayUtils::writeUintAndAdvancePosition(buffer, data_4B, 4, &pos);
+    EXPECT_EQ(data_4B, ByteArrayUtils::readUint(buffer, 4, 0));
+}
+
+TEST(ByteArrayUtilsTest, TestReadCodePoint) {
+    const uint8_t buffer[] = { 0x10, 0xFF, 0x00u, 0x20u, 0x41u, 0x1Fu, 0x60 };
+
+    EXPECT_EQ(0x10FF00, ByteArrayUtils::readCodePoint(buffer, 0));
+    EXPECT_EQ(0x20, ByteArrayUtils::readCodePoint(buffer, 3));
+    EXPECT_EQ(0x41, ByteArrayUtils::readCodePoint(buffer, 4));
+    EXPECT_EQ(NOT_A_CODE_POINT, ByteArrayUtils::readCodePoint(buffer, 5));
+
+    int pos = 0;
+    int codePointArray[3];
+    EXPECT_EQ(3, ByteArrayUtils::readStringAndAdvancePosition(buffer, MAX_WORD_LENGTH,
+            codePointArray, &pos));
+    EXPECT_EQ(0x10FF00, codePointArray[0]);
+    EXPECT_EQ(0x20, codePointArray[1]);
+    EXPECT_EQ(0x41, codePointArray[2]);
+    EXPECT_EQ(0x60, ByteArrayUtils::readCodePoint(buffer, pos));
+}
+
+TEST(ByteArrayUtilsTest, TestWriteAndReadCodePoint) {
+    uint8_t buffer[10];
+
+    const int codePointArray[] = { 0x10FF00, 0x20, 0x41 };
+    int pos = 0;
+    ByteArrayUtils::writeCodePointsAndAdvancePosition(buffer, codePointArray, 3,
+            true /* writesTerminator */, &pos);
+    EXPECT_EQ(0x10FF00, ByteArrayUtils::readCodePoint(buffer, 0));
+    EXPECT_EQ(0x20, ByteArrayUtils::readCodePoint(buffer, 3));
+    EXPECT_EQ(0x41, ByteArrayUtils::readCodePoint(buffer, 4));
+    EXPECT_EQ(NOT_A_CODE_POINT, ByteArrayUtils::readCodePoint(buffer, 5));
+}
+
+}  // namespace
+}  // namespace latinime
diff --git a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
index cf884bf..0a76a9d 100644
--- a/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/KeyboardLayoutSetTestsBase.java
@@ -29,6 +29,7 @@
 import com.android.inputmethod.latin.Constants;
 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.ResourceUtils;
 import com.android.inputmethod.latin.utils.SubtypeLocaleUtils;
@@ -112,13 +113,13 @@
                 "Unknown subtype: locale=" + locale + " keyboardLayout=" + keyboardLayout);
     }
 
-    protected final KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo) {
         return createKeyboardLayoutSet(subtype, editorInfo, false /* voiceInputKeyEnabled */,
                 false /* languageSwitchKeyEnabled */);
     }
 
-    protected final KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
             final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
             final boolean languageSwitchKeyEnabled) {
         final Context context = mThemeContext;
@@ -127,7 +128,7 @@
         final int keyboardHeight = ResourceUtils.getDefaultKeyboardHeight(res);
         final Builder builder = new Builder(context, editorInfo);
         builder.setKeyboardGeometry(keyboardWidth, keyboardHeight)
-                .setSubtype(subtype)
+                .setSubtype(new RichInputMethodSubtype(subtype))
                 .setVoiceInputKeyEnabled(voiceInputKeyEnabled)
                 .setLanguageSwitchKeyEnabled(languageSwitchKeyEnabled);
         return builder.build();
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
index 6ea2758..e619801 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/LanguageOnSpacebarHelperTests.java
@@ -26,6 +26,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 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.SubtypeLocaleUtils;
 
@@ -40,14 +41,14 @@
 
     private RichInputMethodManager mRichImm;
 
-    InputMethodSubtype EN_US_QWERTY;
-    InputMethodSubtype EN_GB_QWERTY;
-    InputMethodSubtype FR_AZERTY;
-    InputMethodSubtype FR_CA_QWERTY;
-    InputMethodSubtype FR_CH_SWISS;
-    InputMethodSubtype FR_CH_QWERTY;
-    InputMethodSubtype FR_CH_QWERTZ;
-    InputMethodSubtype ZZ_QWERTY;
+    RichInputMethodSubtype EN_US_QWERTY;
+    RichInputMethodSubtype EN_GB_QWERTY;
+    RichInputMethodSubtype FR_AZERTY;
+    RichInputMethodSubtype FR_CA_QWERTY;
+    RichInputMethodSubtype FR_CH_SWISS;
+    RichInputMethodSubtype FR_CH_QWERTY;
+    RichInputMethodSubtype FR_CH_QWERTZ;
+    RichInputMethodSubtype ZZ_QWERTY;
 
     @Override
     protected void setUp() throws Exception {
@@ -57,22 +58,22 @@
         mRichImm = RichInputMethodManager.getInstance();
         SubtypeLocaleUtils.init(context);
 
-        EN_US_QWERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.US.toString(), "qwerty");
-        EN_GB_QWERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.UK.toString(), "qwerty");
-        FR_AZERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.FRENCH.toString(), "azerty");
-        FR_CA_QWERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.CANADA_FRENCH.toString(), "qwerty");
-        FR_CH_SWISS = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "fr_CH", "swiss");
-        FR_CH_QWERTZ = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
-                "fr_CH", "qwertz");
-        FR_CH_QWERTY = AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
-                "fr_CH", "qwerty");
-        ZZ_QWERTY = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
+        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"));
+        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"));
     }
 
     private static List<InputMethodSubtype> asList(final InputMethodSubtype ... subtypes) {
@@ -80,14 +81,14 @@
     }
 
     public void testOneSubtype() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(EN_US_QWERTY));
+        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));
+        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(FR_AZERTY.getRawSubtype()));
         mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(false /* isSame */);
         assertEquals("one diff English (US)", FORMAT_TYPE_LANGUAGE_ONLY,
                 mLanguageOnSpacebarHelper.getLanguageOnSpacebarFormatType(EN_US_QWERTY));
@@ -96,8 +97,8 @@
     }
 
     public void testTwoSubtypes() {
-        mLanguageOnSpacebarHelper.updateEnabledSubtypes(asList(EN_US_QWERTY, FR_AZERTY));
-
+        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));
@@ -117,7 +118,8 @@
 
     public void testSameLanuageSubtypes() {
         mLanguageOnSpacebarHelper.updateEnabledSubtypes(
-                asList(EN_US_QWERTY, EN_GB_QWERTY, FR_AZERTY, ZZ_QWERTY));
+                asList(EN_US_QWERTY.getRawSubtype(), EN_GB_QWERTY.getRawSubtype(),
+                        FR_AZERTY.getRawSubtype(), ZZ_QWERTY.getRawSubtype()));
 
         mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
         assertEquals("two same English (US)", FORMAT_TYPE_FULL_LOCALE,
@@ -138,7 +140,9 @@
 
     public void testMultiSameLanuageSubtypes() {
         mLanguageOnSpacebarHelper.updateEnabledSubtypes(
-                asList(FR_AZERTY, FR_CA_QWERTY, FR_CH_SWISS, FR_CH_QWERTY, FR_CH_QWERTZ));
+                asList(FR_AZERTY.getRawSubtype(), FR_CA_QWERTY.getRawSubtype(),
+                        FR_CH_SWISS.getRawSubtype(), FR_CH_QWERTY.getRawSubtype(),
+                        FR_CH_QWERTZ.getRawSubtype()));
 
         mLanguageOnSpacebarHelper.updateIsSystemLanguageSameAsInputLanguage(true /* isSame */);
         assertEquals("multi same French", FORMAT_TYPE_LANGUAGE_ONLY,
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/Arabic.java b/tests/src/com/android/inputmethod/keyboard/layout/Arabic.java
index fa81865..3f85e4b 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/Arabic.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/Arabic.java
@@ -78,8 +78,7 @@
             // U+061F: "؟" ARABIC QUESTION MARK
             // U+061B: "؛" ARABIC SEMICOLON
             return joinKeys(key("\u060C", joinMoreKeys(
-                    ":", "!", "\u061F", "\u061B", "-", "\"", "'", SETTINGS_KEY)),
-                    "_");
+                    ":", "!", "\u061F", "\u061B", "-", "\"", "'", SETTINGS_KEY)));
         }
 
         @Override
@@ -90,7 +89,7 @@
             // U+060C: "،" ARABIC COMMA
             // U+061F: "؟" ARABIC QUESTION MARK
             // U+061B: "؛" ARABIC SEMICOLON
-            return joinKeys("/", key(".", getPunctuationMoreKeys(isPhone)));
+            return joinKeys(key(".", getPunctuationMoreKeys(isPhone)));
         }
 
         @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/ArmenianPhonetic.java b/tests/src/com/android/inputmethod/keyboard/layout/ArmenianPhonetic.java
index dba91b4..2cecedc 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/ArmenianPhonetic.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/ArmenianPhonetic.java
@@ -62,14 +62,14 @@
         public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
             // U+055D: "՝" ARMENIAN COMMA
             return isPhone ? joinKeys(key("\u055D", SETTINGS_KEY))
-                    : joinKeys(key("\u055D", SETTINGS_KEY), "_");
+                    : joinKeys(key("\u055D", SETTINGS_KEY));
         }
 
         @Override
         public ExpectedKey[] getKeysRightToSpacebar(final boolean isPhone) {
             // U+0589: "։" ARMENIAN FULL STOP
             final ExpectedKey fullStopKey = key("\u0589", getPunctuationMoreKeys(isPhone));
-            return isPhone ? joinKeys(fullStopKey) : joinKeys("/", fullStopKey);
+            return joinKeys(fullStopKey);
         }
 
         @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/Dvorak.java b/tests/src/com/android/inputmethod/keyboard/layout/Dvorak.java
index e75cfd0..ba94c8d 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/Dvorak.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/Dvorak.java
@@ -20,13 +20,14 @@
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKeyboardBuilder;
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKey.ExpectedAdditionalMoreKey;
+import com.android.inputmethod.latin.settings.CustomInputStyleSettingsFragment;
 
 import java.util.Locale;
 
 /**
- * The QWERTY alphabet keyboard.
+ * The Dvorak alphabet keyboard.
  */
-public final class Dvorak extends LayoutBase {
+public class Dvorak extends LayoutBase {
     private static final String LAYOUT_NAME = "dvorak";
 
     public Dvorak(final LayoutCustomizer customizer) {
@@ -51,17 +52,19 @@
 
         @Override
         public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
-            return isPhone ? joinKeys(key("q", SETTINGS_KEY)) :
-                joinKeys(SETTINGS_KEY, key("_", moreKey("-")));
+            // U+00A1: "¡" INVERTED EXCLAMATION MARK
+            return isPhone ? joinKeys(key("q", SETTINGS_KEY))
+                    : joinKeys(key("!", joinMoreKeys("\u00A1", SETTINGS_KEY)));
         }
 
         @Override
         public ExpectedKey[] getKeysRightToSpacebar(final boolean isPhone) {
             final ExpectedAdditionalMoreKey[] punctuationMoreKeys =
                     convertToAdditionalMoreKeys(getPunctuationMoreKeys(isPhone));
+            // U+00BF: "¿" INVERTED QUESTION MARK
             return isPhone
                     ? joinKeys(key("z", punctuationMoreKeys))
-                    : joinKeys("/", key("?", moreKey("!")));
+                    : joinKeys(key("?", joinMoreKeys(punctuationMoreKeys, "\u00BF")));
         }
 
         private static ExpectedAdditionalMoreKey[] convertToAdditionalMoreKeys(
@@ -76,7 +79,33 @@
     }
 
     @Override
-    ExpectedKey[][] getCommonAlphabetLayout(final boolean isPhone) { return ALPHABET_COMMON; }
+    public ExpectedKey[][] getCommonAlphabetLayout(final boolean isPhone) {
+        return ALPHABET_COMMON;
+    }
+
+    protected ExpectedKey getRow1_1Key(final boolean isPhone, final int elementId) {
+        if (elementId == KeyboardId.ELEMENT_ALPHABET
+                || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+            return key("'", joinMoreKeys(additionalMoreKey("1"), "!", "\""));
+        }
+        return key("\"", additionalMoreKey("1"));
+    }
+
+    protected ExpectedKey getRow1_2Key(final boolean isPhone, final int elementId) {
+        if (elementId == KeyboardId.ELEMENT_ALPHABET
+                || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+            return key(",", joinMoreKeys(additionalMoreKey("2"), "?", "<"));
+        }
+        return key("<", additionalMoreKey("2"));
+    }
+
+    protected ExpectedKey getRow1_3Key(final boolean isPhone, final int elementId) {
+        if (elementId == KeyboardId.ELEMENT_ALPHABET
+                || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
+            return key(".", joinMoreKeys(additionalMoreKey("3"), ">"));
+        }
+        return key(">", additionalMoreKey("3"));
+    }
 
     @Override
     public ExpectedKey[][] getLayout(final boolean isPhone, final int elementId) {
@@ -86,18 +115,9 @@
         }
         final ExpectedKeyboardBuilder builder = new ExpectedKeyboardBuilder(
                 getCommonAlphabetLayout(isPhone));
-        if (elementId == KeyboardId.ELEMENT_ALPHABET
-                || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED) {
-            builder.addKeysOnTheLeftOfRow(1,
-                    key("'", joinMoreKeys(additionalMoreKey("1"), "!", "\"")),
-                    key(",", joinMoreKeys(additionalMoreKey("2"), "?", "<")),
-                    key(".", joinMoreKeys(additionalMoreKey("3"), ">")));
-        } else {
-            builder.addKeysOnTheLeftOfRow(1,
-                    key("\"", additionalMoreKey("1")),
-                    key("<", additionalMoreKey("2")),
-                    key(">", additionalMoreKey("3")));
-        }
+        builder.replaceKeyOfLabel(ROW1_1, getRow1_1Key(isPhone, elementId))
+                .replaceKeyOfLabel(ROW1_2, getRow1_2Key(isPhone, elementId))
+                .replaceKeyOfLabel(ROW1_3, getRow1_3Key(isPhone, elementId));
         convertCommonLayoutToKeyboard(builder, isPhone);
         getCustomizer().setAccentedLetters(builder);
         if (elementId != KeyboardId.ELEMENT_ALPHABET) {
@@ -107,8 +127,13 @@
         return builder.build();
     }
 
+    public static final String ROW1_1 = "ROW1_1";
+    public static final String ROW1_2 = "ROW1_2";
+    public static final String ROW1_3 = "ROW1_3";
+
     private static final ExpectedKey[][] ALPHABET_COMMON = new ExpectedKeyboardBuilder()
             .setKeysOfRow(1,
+                    ROW1_1, ROW1_2, ROW1_3,
                     key("p", additionalMoreKey("4")),
                     key("y", additionalMoreKey("5")),
                     key("f", additionalMoreKey("6")),
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/Farsi.java b/tests/src/com/android/inputmethod/keyboard/layout/Farsi.java
index a513740..7390457 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/Farsi.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/Farsi.java
@@ -77,8 +77,7 @@
             // U+061B: "؛" ARABIC SEMICOLON
             return joinKeys(key("\u060C", joinMoreKeys(
                     ":", "!", "\u061F", "\u061B", "-", RtlSymbols.DOUBLE_ANGLE_QUOTES_LR_RTL,
-                    SETTINGS_KEY)),
-                    "_");
+                    SETTINGS_KEY)));
         }
 
         @Override
@@ -86,7 +85,7 @@
             if (isPhone) {
                 return super.getKeysRightToSpacebar(isPhone);
             }
-            return joinKeys("/", key(".", getPunctuationMoreKeys(isPhone)));
+            return joinKeys(key(".", getPunctuationMoreKeys(isPhone)));
         }
 
         @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/HindiCompact.java b/tests/src/com/android/inputmethod/keyboard/layout/HindiCompact.java
index 2b625c3..c2a15f3 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/HindiCompact.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/HindiCompact.java
@@ -50,7 +50,7 @@
         public ExpectedKey[] getKeysRightToSpacebar(final boolean isPhone) {
             // U+0964: "।" DEVANAGARI DANDA
             final ExpectedKey periodKey = key("\u0964", getPunctuationMoreKeys(isPhone));
-            return isPhone ? joinKeys(periodKey) : joinKeys("/", periodKey);
+            return joinKeys(periodKey);
         }
 
         @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/LayoutBase.java b/tests/src/com/android/inputmethod/keyboard/layout/LayoutBase.java
index 0548a01..b05789b 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/LayoutBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/LayoutBase.java
@@ -149,6 +149,24 @@
         }
 
         /**
+         * Get the enter key.
+         * @param isPhone true if requesting phone's key.
+         * @return the array of {@link ExpectedKey} that should be placed as an enter key.
+         */
+        public ExpectedKey getEnterKey(final boolean isPhone) {
+            return isPhone ? key(ENTER_KEY, EMOJI_ACTION_KEY) : ENTER_KEY;
+        }
+
+        /**
+         * Get the emoji key.
+         * @param isPhone true if requesting phone's key.
+         * @return the array of {@link ExpectedKey} that should be placed as an emoji key.
+         */
+        public ExpectedKey getEmojiKey(final boolean isPhone) {
+            return EMOJI_NORMAL_KEY;
+        }
+
+        /**
          * Get the space keys.
          * @param isPhone true if requesting phone's keys.
          * @return the array of {@link ExpectedKey} that should be placed at the center of the
@@ -165,8 +183,7 @@
          */
         public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
             // U+002C: "," COMMA
-            return isPhone ? joinKeys(key("\u002C", SETTINGS_KEY))
-                    : joinKeys(key("\u002C", SETTINGS_KEY), "_");
+            return joinKeys(key("\u002C", SETTINGS_KEY));
         }
 
         /**
@@ -176,7 +193,7 @@
          */
         public ExpectedKey[] getKeysRightToSpacebar(final boolean isPhone) {
             final ExpectedKey periodKey = key(".", getPunctuationMoreKeys(isPhone));
-            return isPhone ? joinKeys(periodKey) : joinKeys("/", periodKey);
+            return joinKeys(periodKey);
         }
 
         /**
@@ -241,7 +258,25 @@
      */
     public final LayoutCustomizer getCustomizer() { return mCustomizer; }
 
-    // Icon id.
+    // Icon ids.
+    private static final int ICON_DELETE = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_DELETE_KEY);
+    private static final int ICON_SPACE = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_SPACE_KEY);
+    private static final int ICON_TAB = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_TAB_KEY);
+    private static final int ICON_SHORTCUT = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_SHORTCUT_KEY);
+    private static final int ICON_SETTINGS = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_SETTINGS_KEY);
+    private static final int ICON_LANGUAGE_SWITCH = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_LANGUAGE_SWITCH_KEY);
+    private static final int ICON_ENTER = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_ENTER_KEY);
+    private static final int ICON_EMOJI_ACTION = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_EMOJI_ACTION_KEY);
+    private static final int ICON_EMOJI_NORMAL = KeyboardIconsSet.getIconId(
+            KeyboardIconsSet.NAME_EMOJI_NORMAL_KEY);
     private static final int ICON_SHIFT = KeyboardIconsSet.getIconId(
             KeyboardIconsSet.NAME_SHIFT_KEY);
     private static final int ICON_SHIFTED_SHIFT = KeyboardIconsSet.getIconId(
@@ -251,11 +286,21 @@
     private static final int ICON_ZWJ = KeyboardIconsSet.getIconId(
             KeyboardIconsSet.NAME_ZWJ_KEY);
 
-    // Functional key.
+    // Functional keys.
+    public static final ExpectedKey DELETE_KEY = key(ICON_DELETE, Constants.CODE_DELETE);
+    public static final ExpectedKey TAB_KEY = key(ICON_TAB, Constants.CODE_TAB);
+    public static final ExpectedKey SHORTCUT_KEY = key(ICON_SHORTCUT, Constants.CODE_SHORTCUT);
+    public static final ExpectedKey SETTINGS_KEY = key(ICON_SETTINGS, Constants.CODE_SETTINGS);
+    public static final ExpectedKey LANGUAGE_SWITCH_KEY = key(
+            ICON_LANGUAGE_SWITCH, Constants.CODE_LANGUAGE_SWITCH);
+    public static final ExpectedKey ENTER_KEY = key(ICON_ENTER, Constants.CODE_ENTER);
+    public static final ExpectedKey EMOJI_ACTION_KEY = key(ICON_EMOJI_ACTION, Constants.CODE_EMOJI);
+    public static final ExpectedKey EMOJI_NORMAL_KEY = key(ICON_EMOJI_NORMAL, Constants.CODE_EMOJI);
+    public static final ExpectedKey SPACE_KEY = key(ICON_SPACE, Constants.CODE_SPACE);
     static final ExpectedKey CAPSLOCK_MORE_KEY = key(" ", Constants.CODE_CAPSLOCK);
-    static final ExpectedKey SHIFT_KEY = key(ICON_SHIFT,
+    public static final ExpectedKey SHIFT_KEY = key(ICON_SHIFT,
             Constants.CODE_SHIFT, CAPSLOCK_MORE_KEY);
-    static final ExpectedKey SHIFTED_SHIFT_KEY = key(ICON_SHIFTED_SHIFT,
+    public static final ExpectedKey SHIFTED_SHIFT_KEY = key(ICON_SHIFTED_SHIFT,
             Constants.CODE_SHIFT, CAPSLOCK_MORE_KEY);
     static final ExpectedKey ALPHABET_KEY = key("ABC", Constants.CODE_SWITCH_ALPHA_SYMBOL);
     static final ExpectedKey SYMBOLS_KEY = key("?123", Constants.CODE_SWITCH_ALPHA_SYMBOL);
@@ -271,6 +316,9 @@
     // U+200D: ZERO WIDTH JOINER
     static final ExpectedKey ZWNJ_KEY = key(ICON_ZWNJ, "\u200C");
     static final ExpectedKey ZWJ_KEY = key(ICON_ZWJ, "\u200D");
+    // Domain key
+    public static final ExpectedKey DOMAIN_KEY =
+            key(".com", joinMoreKeys(".net", ".org", ".gov", ".edu")).preserveCase();
 
     // Punctuation more keys for phone form factor.
     public static final ExpectedKey[] PHONE_PUNCTUATION_MORE_KEYS = joinKeys(
@@ -300,12 +348,12 @@
         if (isPhone) {
             builder.addKeysOnTheRightOfRow(numberOfRows - 1, DELETE_KEY)
                     .addKeysOnTheLeftOfRow(numberOfRows, customizer.getSymbolsKey())
-                    .addKeysOnTheRightOfRow(numberOfRows, key(ENTER_KEY, EMOJI_ACTION_KEY));
+                    .addKeysOnTheRightOfRow(numberOfRows, customizer.getEnterKey(isPhone));
         } else {
             builder.addKeysOnTheRightOfRow(1, DELETE_KEY)
-                    .addKeysOnTheRightOfRow(numberOfRows - 2, ENTER_KEY)
+                    .addKeysOnTheRightOfRow(numberOfRows - 2, customizer.getEnterKey(isPhone))
                     .addKeysOnTheLeftOfRow(numberOfRows, customizer.getSymbolsKey())
-                    .addKeysOnTheRightOfRow(numberOfRows, EMOJI_NORMAL_KEY);
+                    .addKeysOnTheRightOfRow(numberOfRows, customizer.getEmojiKey(isPhone));
         }
         builder.addKeysOnTheLeftOfRow(
                 numberOfRows - 1, (Object[])customizer.getLeftShiftKeys(isPhone));
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/Myanmar.java b/tests/src/com/android/inputmethod/keyboard/layout/Myanmar.java
index 15c74ed..3c70d32 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/Myanmar.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/Myanmar.java
@@ -55,14 +55,14 @@
             // U+002C: "," COMMA
             // U+104A: "၊" MYANMAR SIGN LITTLE SECTION
             return isPhone ? joinKeys(key("\u002C", SETTINGS_KEY))
-                    : joinKeys(key("\u104A", moreKey(","), SETTINGS_KEY), "_");
+                    : joinKeys(key("\u104A", moreKey(","), SETTINGS_KEY));
         }
 
         @Override
         public ExpectedKey[] getKeysRightToSpacebar(final boolean isPhone) {
             // U+104B: "။" MYANMAR SIGN SECTION
             final ExpectedKey periodKey = key("\u104B", getPunctuationMoreKeys(isPhone));
-            return isPhone ? joinKeys(periodKey) : joinKeys("/", periodKey);
+            return joinKeys(periodKey);
         }
 
         @Override
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/Symbols.java b/tests/src/com/android/inputmethod/keyboard/layout/Symbols.java
index 2cee2d9..8030897 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/Symbols.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/Symbols.java
@@ -41,18 +41,18 @@
                 customizer.getSingleQuoteMoreKeys(), customizer.getSingleAngleQuoteKeys())));
         if (isPhone) {
             builder.addKeysOnTheLeftOfRow(3, customizer.getSymbolsShiftKey(isPhone))
-                    .addKeysOnTheRightOfRow(3, DELETE_KEY)
+                    .addKeysOnTheRightOfRow(3, LayoutBase.DELETE_KEY)
                     .addKeysOnTheLeftOfRow(4, customizer.getAlphabetKey())
-                    .addKeysOnTheRightOfRow(4, key(ENTER_KEY, EMOJI_ACTION_KEY));
+                    .addKeysOnTheRightOfRow(4, customizer.getEnterKey(isPhone));
         } else {
             // Tablet symbols keyboard has extra two keys at the left edge of the 3rd row.
             builder.addKeysOnTheLeftOfRow(3, (Object[])joinKeys("\\", "="));
-            builder.addKeysOnTheRightOfRow(1, DELETE_KEY)
-                    .addKeysOnTheRightOfRow(2, ENTER_KEY)
+            builder.addKeysOnTheRightOfRow(1, LayoutBase.DELETE_KEY)
+                    .addKeysOnTheRightOfRow(2, customizer.getEnterKey(isPhone))
                     .addKeysOnTheLeftOfRow(3, customizer.getSymbolsShiftKey(isPhone))
                     .addKeysOnTheRightOfRow(3, customizer.getSymbolsShiftKey(isPhone))
                     .addKeysOnTheLeftOfRow(4, customizer.getAlphabetKey())
-                    .addKeysOnTheRightOfRow(4, EMOJI_NORMAL_KEY);
+                    .addKeysOnTheRightOfRow(4, customizer.getEmojiKey(isPhone));
         }
         return builder.build();
     }
@@ -167,7 +167,7 @@
                     // U+00BF: "¿" INVERTED QUESTION MARK
                     key("?", moreKey("\u00BF")))
             .setKeysOfRow(4,
-                    key(","), key("_"), SPACE_KEY, key("/"),
+                    key(","), key("_"), LayoutBase.SPACE_KEY, key("/"),
                     // U+2026: "…" HORIZONTAL ELLIPSIS
                     key(".", moreKey("\u2026")))
             .build();
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/SymbolsShifted.java b/tests/src/com/android/inputmethod/keyboard/layout/SymbolsShifted.java
index 7ed103b..19cb607 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/SymbolsShifted.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/SymbolsShifted.java
@@ -37,20 +37,20 @@
         builder.replaceKeyOfLabel(OTHER_CURRENCIES, (Object[])customizer.getOtherCurrencyKeys());
         if (isPhone) {
             builder.addKeysOnTheLeftOfRow(3, customizer.getBackToSymbolsKey())
-                    .addKeysOnTheRightOfRow(3, DELETE_KEY)
+                    .addKeysOnTheRightOfRow(3, LayoutBase.DELETE_KEY)
                     .addKeysOnTheLeftOfRow(4, customizer.getAlphabetKey())
-                    .addKeysOnTheRightOfRow(4, key(ENTER_KEY, EMOJI_ACTION_KEY));
+                    .addKeysOnTheRightOfRow(4, customizer.getEnterKey(isPhone));
         } else {
             // Tablet symbols shifted keyboard has extra two keys at the right edge of the 3rd row.
             // U+00BF: "¿" INVERTED QUESTION MARK
             // U+00A1: "¡" INVERTED EXCLAMATION MARK
             builder.addKeysOnTheRightOfRow(3, (Object[])joinKeys("\u00A1", "\u00BF"));
-            builder.addKeysOnTheRightOfRow(1, DELETE_KEY)
-                    .addKeysOnTheRightOfRow(2, ENTER_KEY)
+            builder.addKeysOnTheRightOfRow(1, LayoutBase.DELETE_KEY)
+                    .addKeysOnTheRightOfRow(2, customizer.getEnterKey(isPhone))
                     .addKeysOnTheLeftOfRow(3, customizer.getBackToSymbolsKey())
                     .addKeysOnTheRightOfRow(3, customizer.getBackToSymbolsKey())
                     .addKeysOnTheLeftOfRow(4, customizer.getAlphabetKey())
-                    .addKeysOnTheRightOfRow(4, EMOJI_NORMAL_KEY);
+                    .addKeysOnTheRightOfRow(4, customizer.getEmojiKey(isPhone));
         }
         return builder.build();
     }
@@ -122,7 +122,7 @@
                     // U+2264: "≤" LESS-THAN OR EQUAL TO
                     // U+00AB: "«" LEFT-POINTING DOUBLE ANGLE QUOTATION MARK
                     key("<", joinMoreKeys("\u2039", "\u2264", "\u00AB")),
-                    SPACE_KEY,
+                    LayoutBase.SPACE_KEY,
                     // U+203A: "›" SINGLE RIGHT-POINTING ANGLE QUOTATION MARK
                     // U+2265: "≥" GREATER-THAN EQUAL TO
                     // U+00BB: "»" RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/expected/AbstractLayoutBase.java b/tests/src/com/android/inputmethod/keyboard/layout/expected/AbstractLayoutBase.java
index 0f7bef2..3556cb4 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/expected/AbstractLayoutBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/expected/AbstractLayoutBase.java
@@ -16,9 +16,7 @@
 
 package com.android.inputmethod.keyboard.layout.expected;
 
-import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
 import com.android.inputmethod.keyboard.layout.expected.ExpectedKey.ExpectedAdditionalMoreKey;
-import com.android.inputmethod.latin.Constants;
 
 /**
  * Base class to create an expected keyboard for unit test.
@@ -104,36 +102,4 @@
     public static ExpectedKey[] joinKeys(final Object ... keys) {
         return ExpectedKeyboardBuilder.joinKeys(keys);
     }
-
-    // Icon ids.
-    private static final int ICON_DELETE = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_DELETE_KEY);
-    private static final int ICON_SPACE = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_SPACE_KEY);
-    private static final int ICON_TAB = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_TAB_KEY);
-    private static final int ICON_SHORTCUT = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_SHORTCUT_KEY);
-    private static final int ICON_SETTINGS = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_SETTINGS_KEY);
-    private static final int ICON_LANGUAGE_SWITCH = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_LANGUAGE_SWITCH_KEY);
-    private static final int ICON_ENTER = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_ENTER_KEY);
-    private static final int ICON_EMOJI_ACTION = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_EMOJI_ACTION_KEY);
-    private static final int ICON_EMOJI_NORMAL = KeyboardIconsSet.getIconId(
-            KeyboardIconsSet.NAME_EMOJI_NORMAL_KEY);
-
-    // Functional keys.
-    public static final ExpectedKey DELETE_KEY = key(ICON_DELETE, Constants.CODE_DELETE);
-    public static final ExpectedKey TAB_KEY = key(ICON_TAB, Constants.CODE_TAB);
-    public static final ExpectedKey SHORTCUT_KEY = key(ICON_SHORTCUT, Constants.CODE_SHORTCUT);
-    public static final ExpectedKey SETTINGS_KEY = key(ICON_SETTINGS, Constants.CODE_SETTINGS);
-    public static final ExpectedKey LANGUAGE_SWITCH_KEY = key(
-            ICON_LANGUAGE_SWITCH, Constants.CODE_LANGUAGE_SWITCH);
-    public static final ExpectedKey ENTER_KEY = key(ICON_ENTER, Constants.CODE_ENTER);
-    public static final ExpectedKey EMOJI_ACTION_KEY = key(ICON_EMOJI_ACTION, Constants.CODE_EMOJI);
-    public static final ExpectedKey EMOJI_NORMAL_KEY = key(ICON_EMOJI_NORMAL, Constants.CODE_EMOJI);
-    public static final ExpectedKey SPACE_KEY = key(ICON_SPACE, Constants.CODE_SPACE);
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKey.java b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKey.java
index 0e1c71c..2674a6a 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKey.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKey.java
@@ -148,6 +148,18 @@
         return newInstance(mVisual.toUpperCase(locale), mOutput.toUpperCase(locale));
     }
 
+    public ExpectedKey preserveCase() {
+        final ExpectedKey[] moreKeys = getMoreKeys();
+        final ExpectedKey[] casePreservedMoreKeys = new ExpectedKey[moreKeys.length];
+        for (int index = 0; index < moreKeys.length; index++) {
+            final ExpectedKey moreKey = moreKeys[index];
+            casePreservedMoreKeys[index] = newInstance(
+                    moreKey.getVisual().preserveCase(), moreKey.getOutput().preserveCase());
+        }
+        return newInstance(
+                getVisual().preserveCase(), getOutput().preserveCase(), casePreservedMoreKeys);
+    }
+
     public boolean equalsTo(final Key key) {
         // This key has no "more keys".
         return mVisual.equalsTo(key) && mOutput.equalsTo(key) && key.getMoreKeys() == null;
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyOutput.java b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyOutput.java
index 1be51e6..737d169 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyOutput.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyOutput.java
@@ -42,6 +42,7 @@
     }
 
     abstract ExpectedKeyOutput toUpperCase(final Locale locale);
+    abstract ExpectedKeyOutput preserveCase();
     abstract boolean equalsTo(final String text);
     abstract boolean equalsTo(final Key key);
     abstract boolean equalsTo(final MoreKeySpec moreKeySpec);
@@ -69,6 +70,11 @@
         }
 
         @Override
+        ExpectedKeyOutput preserveCase() {
+            return new CasePreservedCode(mCode);
+        }
+
+        @Override
         boolean equalsTo(final String text) {
             return StringUtils.codePointCount(text) == 1 && text.codePointAt(0) == mCode;
         }
@@ -93,6 +99,16 @@
             return Constants.isLetterCode(mCode) ? StringUtils.newSingleCodePointString(mCode)
                     : Constants.printableCode(mCode);
         }
+
+        private static class CasePreservedCode extends Code {
+            CasePreservedCode(final int code) { super(code); }
+
+            @Override
+            ExpectedKeyOutput toUpperCase(final Locale locale) { return this; }
+
+            @Override
+            ExpectedKeyOutput preserveCase() { return this; }
+        }
     }
 
     /**
@@ -109,6 +125,11 @@
         }
 
         @Override
+        ExpectedKeyOutput preserveCase() {
+            return new CasePreservedText(mText);
+        }
+
+        @Override
         boolean equalsTo(final String text) {
             return text.equals(text);
         }
@@ -134,5 +155,15 @@
         public String toString() {
             return mText;
         }
+
+        private static class CasePreservedText extends Text {
+            CasePreservedText(final String text) { super(text); }
+
+            @Override
+            ExpectedKeyOutput toUpperCase(final Locale locale) { return this; }
+
+            @Override
+            ExpectedKeyOutput preserveCase() { return this; }
+        }
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyVisual.java b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyVisual.java
index 0a0da32..facdf70 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyVisual.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/expected/ExpectedKeyVisual.java
@@ -37,6 +37,7 @@
     }
 
     abstract ExpectedKeyVisual toUpperCase(final Locale locale);
+    abstract ExpectedKeyVisual preserveCase();
     abstract boolean equalsTo(final String text);
     abstract boolean equalsTo(final Key key);
     abstract boolean equalsTo(final MoreKeySpec moreKeySpec);
@@ -59,6 +60,11 @@
         }
 
         @Override
+        ExpectedKeyVisual preserveCase() {
+            return this;
+        }
+
+        @Override
         boolean equalsTo(final String text) {
             return false;
         }
@@ -103,6 +109,11 @@
         }
 
         @Override
+        ExpectedKeyVisual preserveCase() {
+            return new CasePreservedLabel(mLabel);
+        }
+
+        @Override
         boolean equalsTo(final String text) {
             return mLabel.equals(text);
         }
@@ -131,5 +142,15 @@
         public String toString() {
             return mLabel;
         }
+
+        private static class CasePreservedLabel extends Label {
+            CasePreservedLabel(final String label) { super(label); }
+
+            @Override
+            ExpectedKeyVisual toUpperCase(final Locale locale) { return this; }
+
+            @Override
+            ExpectedKeyVisual preserveCase() { return this; }
+        }
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java
new file mode 100644
index 0000000..37ca092
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakEmail.java
@@ -0,0 +1,91 @@
+/*
+ * 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.layout.tests;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.layout.Dvorak;
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
+import com.android.inputmethod.keyboard.layout.tests.TestsEnglishDvorak.EnglishDvorakCustomizer;
+
+import java.util.Locale;
+
+/**
+ * en_US: English (United States)/dvorak, email input field.
+ */
+@SmallTest
+public class TestsDvorakEmail extends LayoutTestsBase {
+    private static final Locale LOCALE = new Locale("en", "US");
+    private static final LayoutBase LAYOUT = new DvorakEmail(new DvorakEmailCustomizer(LOCALE));
+
+    @Override
+    LayoutBase getLayout() { return LAYOUT; }
+
+    @Override
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+            final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
+            final boolean languageSwitchKeyEnabled) {
+        final EditorInfo emailField = new EditorInfo();
+        emailField.inputType =
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+        return super.createKeyboardLayoutSet(
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+    }
+
+    private static class DvorakEmailCustomizer extends EnglishDvorakCustomizer {
+        DvorakEmailCustomizer(final Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public ExpectedKey getEnterKey(final boolean isPhone) {
+            return isPhone ? LayoutBase.ENTER_KEY : super.getEnterKey(isPhone);
+        }
+
+        @Override
+        public ExpectedKey getEmojiKey(final boolean isPhone) {
+            return LayoutBase.DOMAIN_KEY;
+        }
+
+        @Override
+        public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
+            return isPhone ? super.getKeysLeftToSpacebar(isPhone)
+                    : joinKeys(key("@", LayoutBase.SETTINGS_KEY));
+        }
+    }
+
+    private static class DvorakEmail extends Dvorak {
+        public DvorakEmail(final LayoutCustomizer customizer) {
+            super(customizer);
+        }
+
+        @Override
+        protected ExpectedKey getRow1_1Key(final boolean isPhone, final int elementId) {
+            if (isPhone && (elementId == KeyboardId.ELEMENT_ALPHABET
+                    || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)) {
+                return key("@", joinMoreKeys(additionalMoreKey("1")));
+            }
+            return super.getRow1_1Key(isPhone, elementId);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java
new file mode 100644
index 0000000..3bcae0c
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsDvorakUrl.java
@@ -0,0 +1,91 @@
+/*
+ * 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.layout.tests;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.layout.Dvorak;
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
+import com.android.inputmethod.keyboard.layout.tests.TestsEnglishDvorak.EnglishDvorakCustomizer;
+
+import java.util.Locale;
+
+/**
+ * en_US: English (United States)/dvorak, URL input field.
+ */
+@SmallTest
+public class TestsDvorakUrl extends LayoutTestsBase {
+    private static final Locale LOCALE = new Locale("en", "US");
+    private static final LayoutBase LAYOUT = new DvorakEmail(new DvorakUrlCustomizer(LOCALE));
+
+    @Override
+    LayoutBase getLayout() { return LAYOUT; }
+
+    @Override
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+            final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
+            final boolean languageSwitchKeyEnabled) {
+        final EditorInfo emailField = new EditorInfo();
+        emailField.inputType =
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
+        return super.createKeyboardLayoutSet(
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+    }
+
+    private static class DvorakUrlCustomizer extends EnglishDvorakCustomizer {
+        DvorakUrlCustomizer(final Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public ExpectedKey getEnterKey(final boolean isPhone) {
+            return isPhone ? LayoutBase.ENTER_KEY : super.getEnterKey(isPhone);
+        }
+
+        @Override
+        public ExpectedKey getEmojiKey(final boolean isPhone) {
+            return LayoutBase.DOMAIN_KEY;
+        }
+
+        @Override
+        public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
+            return isPhone ? super.getKeysLeftToSpacebar(isPhone)
+                    : joinKeys(key("/", LayoutBase.SETTINGS_KEY));
+        }
+    }
+
+    private static class DvorakEmail extends Dvorak {
+        public DvorakEmail(final LayoutCustomizer customizer) {
+            super(customizer);
+        }
+
+        @Override
+        protected ExpectedKey getRow1_1Key(final boolean isPhone, final int elementId) {
+            if (isPhone && (elementId == KeyboardId.ELEMENT_ALPHABET
+                    || elementId == KeyboardId.ELEMENT_ALPHABET_AUTOMATIC_SHIFTED)) {
+                return key("/", joinMoreKeys(additionalMoreKey("1")));
+            }
+            return super.getRow1_1Key(isPhone, elementId);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsEnglishDvorak.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsEnglishDvorak.java
index a052693..e647f8a 100644
--- a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsEnglishDvorak.java
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsEnglishDvorak.java
@@ -36,7 +36,7 @@
     @Override
     LayoutBase getLayout() { return LAYOUT; }
 
-    private static class EnglishDvorakCustomizer extends DvorakCustomizer {
+    public static class EnglishDvorakCustomizer extends DvorakCustomizer {
         private final EnglishCustomizer mEnglishCustomizer;
 
         EnglishDvorakCustomizer(final Locale locale) {
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java
new file mode 100644
index 0000000..8563d69
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyEmail.java
@@ -0,0 +1,73 @@
+/*
+ * 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.layout.tests;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.Qwerty;
+import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
+
+import java.util.Locale;
+
+/**
+ * en_US: English (United States)/qwerty, email input field.
+ */
+@SmallTest
+public class TestsQwertyEmail extends LayoutTestsBase {
+    private static final Locale LOCALE = new Locale("en", "US");
+    private static final LayoutBase LAYOUT = new Qwerty(new EnglishEmailCustomizer(LOCALE));
+
+    @Override
+    LayoutBase getLayout() { return LAYOUT; }
+
+    @Override
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+            final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
+            final boolean languageSwitchKeyEnabled) {
+        final EditorInfo emailField = new EditorInfo();
+        emailField.inputType =
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS;
+        return super.createKeyboardLayoutSet(
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+    }
+
+    private static class EnglishEmailCustomizer extends EnglishCustomizer {
+        EnglishEmailCustomizer(final Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public ExpectedKey getEnterKey(final boolean isPhone) {
+            return isPhone ? LayoutBase.ENTER_KEY : super.getEnterKey(isPhone);
+        }
+
+        @Override
+        public ExpectedKey getEmojiKey(final boolean isPhone) {
+            return LayoutBase.DOMAIN_KEY;
+        }
+
+        @Override
+        public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
+            return joinKeys(key("@", LayoutBase.SETTINGS_KEY));
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java
new file mode 100644
index 0000000..1c1a2bb
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/layout/tests/TestsQwertyUrl.java
@@ -0,0 +1,73 @@
+/*
+ * 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.layout.tests;
+
+import android.test.suitebuilder.annotation.SmallTest;
+import android.text.InputType;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputMethodSubtype;
+
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.layout.LayoutBase;
+import com.android.inputmethod.keyboard.layout.Qwerty;
+import com.android.inputmethod.keyboard.layout.expected.ExpectedKey;
+
+import java.util.Locale;
+
+/**
+ * en_US: English (United States)/qwerty, URL input field.
+ */
+@SmallTest
+public class TestsQwertyUrl extends LayoutTestsBase {
+    private static final Locale LOCALE = new Locale("en", "US");
+    private static final LayoutBase LAYOUT = new Qwerty(new EnglishUrlCustomizer(LOCALE));
+
+    @Override
+    LayoutBase getLayout() { return LAYOUT; }
+
+    @Override
+    protected KeyboardLayoutSet createKeyboardLayoutSet(final InputMethodSubtype subtype,
+            final EditorInfo editorInfo, final boolean voiceInputKeyEnabled,
+            final boolean languageSwitchKeyEnabled) {
+        final EditorInfo emailField = new EditorInfo();
+        emailField.inputType =
+                InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_URI;
+        return super.createKeyboardLayoutSet(
+                subtype, emailField, voiceInputKeyEnabled, languageSwitchKeyEnabled);
+    }
+
+    private static class EnglishUrlCustomizer extends EnglishCustomizer {
+        EnglishUrlCustomizer(final Locale locale) {
+            super(locale);
+        }
+
+        @Override
+        public ExpectedKey getEnterKey(final boolean isPhone) {
+            return isPhone ? LayoutBase.ENTER_KEY : super.getEnterKey(isPhone);
+        }
+
+        @Override
+        public ExpectedKey getEmojiKey(final boolean isPhone) {
+            return LayoutBase.DOMAIN_KEY;
+        }
+
+        @Override
+        public ExpectedKey[] getKeysLeftToSpacebar(final boolean isPhone) {
+            return joinKeys(key("/", LayoutBase.SETTINGS_KEY));
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
index a5f20b5..2785dec 100644
--- a/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
+++ b/tests/src/com/android/inputmethod/latin/SuggestedWordsTests.java
@@ -23,24 +23,49 @@
 
 import java.util.ArrayList;
 import java.util.Locale;
-import java.util.Random;
 
 @SmallTest
 public class SuggestedWordsTests extends AndroidTestCase {
+
+    /**
+     * Helper method to create a dummy {@link SuggestedWordInfo} with specifying
+     * {@link SuggestedWordInfo#KIND_TYPED}.
+     *
+     * @param word the word to be used to create {@link SuggestedWordInfo}.
+     * @return a new instance of {@link SuggestedWordInfo}.
+     */
+    private static SuggestedWordInfo createTypedWordInfo(final String word) {
+        // Use 100 as the frequency because the numerical value does not matter as
+        // long as it's > 1 and < INT_MAX.
+        return new SuggestedWordInfo(word, 100 /* score */,
+                SuggestedWordInfo.KIND_TYPED,
+                null /* sourceDict */,
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                1 /* autoCommitFirstWordConfidence */);
+    }
+
+    /**
+     * Helper method to create a dummy {@link SuggestedWordInfo} with specifying
+     * {@link SuggestedWordInfo#KIND_CORRECTION}.
+     *
+     * @param word the word to be used to create {@link SuggestedWordInfo}.
+     * @return a new instance of {@link SuggestedWordInfo}.
+     */
+    private static SuggestedWordInfo createCorrectionWordInfo(final String word) {
+        return new SuggestedWordInfo(word, 1 /* score */,
+                SuggestedWordInfo.KIND_CORRECTION,
+                null /* sourceDict */,
+                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
+                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */);
+    }
+
     public void testGetSuggestedWordsExcludingTypedWord() {
         final String TYPED_WORD = "typed";
-        final int TYPED_WORD_FREQ = 5;
         final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
         final ArrayList<SuggestedWordInfo> list = new ArrayList<>();
-        list.add(new SuggestedWordInfo(TYPED_WORD, TYPED_WORD_FREQ,
-                SuggestedWordInfo.KIND_TYPED, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
+        list.add(createTypedWordInfo(TYPED_WORD));
         for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
-            list.add(new SuggestedWordInfo("" + i, 1, SuggestedWordInfo.KIND_CORRECTION,
-                    null /* sourceDict */,
-                    SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                    SuggestedWordInfo.NOT_A_CONFIDENCE /* autoCommitFirstWordConfidence */));
+            list.add(createCorrectionWordInfo(Integer.toString(i)));
         }
 
         final SuggestedWords words = new SuggestedWords(
@@ -66,19 +91,9 @@
     }
 
     // Helper for testGetTransformedWordInfo
-    private SuggestedWordInfo createWordInfo(final String s) {
-        // Use 100 as the frequency because the numerical value does not matter as
-        // long as it's > 1 and < INT_MAX.
-        return new SuggestedWordInfo(s, 100,
-                SuggestedWordInfo.KIND_TYPED, null /* sourceDict */,
-                SuggestedWordInfo.NOT_AN_INDEX /* indexOfTouchPointOfSecondWord */,
-                new Random().nextInt(1000000) /* autoCommitFirstWordConfidence */);
-    }
-
-    // Helper for testGetTransformedWordInfo
     private SuggestedWordInfo transformWordInfo(final String info,
             final int trailingSingleQuotesCount) {
-        final SuggestedWordInfo suggestedWordInfo = createWordInfo(info);
+        final SuggestedWordInfo suggestedWordInfo = createTypedWordInfo(info);
         final SuggestedWordInfo returnedWordInfo =
                 Suggest.getTransformedSuggestedWordInfo(suggestedWordInfo,
                 Locale.ENGLISH, false /* isAllUpperCase */, false /* isFirstCharCapitalized */,
@@ -102,4 +117,35 @@
         result = transformWordInfo("didn't", 3);
         assertEquals(result.mWord, "didn't''");
     }
+
+    public void testGetTypedWordInfoOrNull() {
+        final String TYPED_WORD = "typed";
+        final int NUMBER_OF_ADDED_SUGGESTIONS = 5;
+        final ArrayList<SuggestedWordInfo> list = new ArrayList<>();
+        list.add(createTypedWordInfo(TYPED_WORD));
+        for (int i = 0; i < NUMBER_OF_ADDED_SUGGESTIONS; ++i) {
+            list.add(createCorrectionWordInfo(Integer.toString(i)));
+        }
+
+        // Make sure getTypedWordInfoOrNull() returns non-null object.
+        final SuggestedWords wordsWithTypedWord = new SuggestedWords(
+                list, null /* rawSuggestions */,
+                false /* typedWordValid */,
+                false /* willAutoCorrect */,
+                false /* isObsoleteSuggestions */,
+                false /* isPrediction*/,
+                SuggestedWords.INPUT_STYLE_NONE);
+        final SuggestedWordInfo typedWord = wordsWithTypedWord.getTypedWordInfoOrNull();
+        assertNotNull(typedWord);
+        assertEquals(TYPED_WORD, typedWord.mWord);
+
+        // Make sure getTypedWordInfoOrNull() returns null.
+        final SuggestedWords wordsWithoutTypedWord =
+                wordsWithTypedWord.getSuggestedWordsExcludingTypedWord(
+                        SuggestedWords.INPUT_STYLE_NONE);
+        assertNull(wordsWithoutTypedWord.getTypedWordInfoOrNull());
+
+        // Make sure getTypedWordInfoOrNull() returns null.
+        assertNull(SuggestedWords.EMPTY.getTypedWordInfoOrNull());
+    }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java
index 91c9c37..66a12b9 100644
--- a/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/AdditionalSubtypeUtilsTests.java
@@ -151,25 +151,25 @@
     }
 
     public void testRestorable() {
-        final InputMethodSubtype EN_UK_DVORAK =
+        final InputMethodSubtype EN_US_DVORAK =
                 AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
                         Locale.US.toString(), "dvorak");
         final InputMethodSubtype ZZ_AZERTY =
                 AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
                         SubtypeLocaleUtils.NO_LANGUAGE, "azerty");
-        assertEnUsDvorak(EN_UK_DVORAK);
+        assertEnUsDvorak(EN_US_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 InputMethodSubtype[] subtypes = { EN_US_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_EN_US_DVORAK = restoredSubtypes[0];
         final InputMethodSubtype restored_ZZ_AZERTY = restoredSubtypes[1];
 
-        assertEnUsDvorak(restored_EN_UK_DVORAK);
+        assertEnUsDvorak(restored_EN_US_DVORAK);
         assertAzerty(restored_ZZ_AZERTY);
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java
index aef517c..b766ab2 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SpacebarLanguageUtilsTests.java
@@ -24,6 +24,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 
 import java.util.ArrayList;
 import java.util.Locale;
@@ -31,28 +32,28 @@
 @SmallTest
 public class SpacebarLanguageUtilsTests extends AndroidTestCase {
     // All input method subtypes of LatinIME.
-    private final ArrayList<InputMethodSubtype> mSubtypesList = new ArrayList<>();
+    private final ArrayList<RichInputMethodSubtype> mSubtypesList = new ArrayList<>();
 
     private RichInputMethodManager mRichImm;
     private Resources mRes;
 
-    InputMethodSubtype EN_US;
-    InputMethodSubtype EN_GB;
-    InputMethodSubtype ES_US;
-    InputMethodSubtype FR;
-    InputMethodSubtype FR_CA;
-    InputMethodSubtype FR_CH;
-    InputMethodSubtype DE;
-    InputMethodSubtype DE_CH;
-    InputMethodSubtype HI_ZZ;
-    InputMethodSubtype ZZ;
-    InputMethodSubtype DE_QWERTY;
-    InputMethodSubtype FR_QWERTZ;
-    InputMethodSubtype EN_US_AZERTY;
-    InputMethodSubtype EN_UK_DVORAK;
-    InputMethodSubtype ES_US_COLEMAK;
-    InputMethodSubtype ZZ_AZERTY;
-    InputMethodSubtype ZZ_PC;
+    RichInputMethodSubtype EN_US;
+    RichInputMethodSubtype EN_GB;
+    RichInputMethodSubtype ES_US;
+    RichInputMethodSubtype FR;
+    RichInputMethodSubtype FR_CA;
+    RichInputMethodSubtype FR_CH;
+    RichInputMethodSubtype DE;
+    RichInputMethodSubtype DE_CH;
+    RichInputMethodSubtype HI_ZZ;
+    RichInputMethodSubtype ZZ;
+    RichInputMethodSubtype DE_QWERTY;
+    RichInputMethodSubtype FR_QWERTZ;
+    RichInputMethodSubtype EN_US_AZERTY;
+    RichInputMethodSubtype EN_UK_DVORAK;
+    RichInputMethodSubtype ES_US_COLEMAK;
+    RichInputMethodSubtype ZZ_AZERTY;
+    RichInputMethodSubtype ZZ_PC;
 
     @Override
     protected void setUp() throws Exception {
@@ -67,53 +68,60 @@
         final int subtypeCount = imi.getSubtypeCount();
         for (int index = 0; index < subtypeCount; index++) {
             final InputMethodSubtype subtype = imi.getSubtypeAt(index);
-            mSubtypesList.add(subtype);
+            mSubtypesList.add(new RichInputMethodSubtype(subtype));
         }
 
-        EN_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.US.toString(), "qwerty");
-        EN_GB = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.UK.toString(), "qwerty");
-        ES_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "es_US", "spanish");
-        FR = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.FRENCH.toString(), "azerty");
-        FR_CA = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.CANADA_FRENCH.toString(), "qwerty");
-        FR_CH = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "fr_CH", "swiss");
-        DE = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                Locale.GERMAN.toString(), "qwertz");
-        DE_CH = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "de_CH", "swiss");
-        HI_ZZ = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                "hi_ZZ", "qwerty");
-        ZZ = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
-                SubtypeLocaleUtils.NO_LANGUAGE, "qwerty");
-        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");
+        EN_US = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                Locale.US.toString(), "qwerty"));
+        EN_GB = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                Locale.UK.toString(), "qwerty"));
+        ES_US = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                "es_US", "spanish"));
+        FR = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                Locale.FRENCH.toString(), "azerty"));
+        FR_CA = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                Locale.CANADA_FRENCH.toString(), "qwerty"));
+        FR_CH = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                "fr_CH", "swiss"));
+        DE = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                Locale.GERMAN.toString(), "qwertz"));
+        DE_CH = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                "de_CH", "swiss"));
+        HI_ZZ = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                "hi_ZZ", "qwerty"));
+        ZZ = new RichInputMethodSubtype(mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
+                SubtypeLocaleUtils.NO_LANGUAGE, "qwerty"));
+        DE_QWERTY = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    Locale.GERMAN.toString(), "qwerty"));
+        FR_QWERTZ = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    Locale.FRENCH.toString(), "qwertz"));
+        EN_US_AZERTY = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    Locale.US.toString(), "azerty"));
+        EN_UK_DVORAK = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    Locale.UK.toString(), "dvorak"));
+        ES_US_COLEMAK = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    "es_US", "colemak"));
+        ZZ_AZERTY = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    SubtypeLocaleUtils.NO_LANGUAGE, "azerty"));
+        ZZ_PC = new RichInputMethodSubtype(
+                AdditionalSubtypeUtils.createAsciiEmojiCapableAdditionalSubtype(
+                    SubtypeLocaleUtils.NO_LANGUAGE, "pcqwerty"));
     }
 
     public void testAllFullDisplayNameForSpacebar() {
-        for (final InputMethodSubtype subtype : mSubtypesList) {
+        for (final RichInputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocaleUtils
-                    .getSubtypeDisplayNameInSystemLocale(subtype);
-            final String spacebarText = SpacebarLanguageUtils.getFullDisplayName(subtype);
+                    .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype());
+            final String spacebarText = subtype.getFullDisplayName();
             final String languageName = SubtypeLocaleUtils
                     .getSubtypeLocaleDisplayName(subtype.getLocale());
-            if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
+            if (subtype.isNoLanguage()) {
                 assertFalse(subtypeName, spacebarText.contains(languageName));
             } else {
                 assertTrue(subtypeName, spacebarText.contains(languageName));
@@ -122,19 +130,19 @@
     }
 
    public void testAllMiddleDisplayNameForSpacebar() {
-        for (final InputMethodSubtype subtype : mSubtypesList) {
+        for (final RichInputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocaleUtils
-                    .getSubtypeDisplayNameInSystemLocale(subtype);
+                    .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype());
             if (SubtypeLocaleUtils.sExceptionalLocaleDisplayedInRootLocale.contains(
                     subtype.getLocale())) {
                 // Skip test because the language part of this locale string doesn't represent
                 // the locale to be displayed on the spacebar (for example hi_ZZ and Hinglish).
                 continue;
             }
-            final String spacebarText = SpacebarLanguageUtils.getMiddleDisplayName(subtype);
-            if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
-                assertEquals(subtypeName,
-                        SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(subtype), spacebarText);
+            final String spacebarText = subtype.getMiddleDisplayName();
+            if (subtype.isNoLanguage()) {
+                assertEquals(subtypeName, SubtypeLocaleUtils.getKeyboardLayoutSetDisplayName(
+                        subtype.getRawSubtype()), spacebarText);
             } else {
                 final Locale locale = SubtypeLocaleUtils.getSubtypeLocale(subtype);
                 assertEquals(subtypeName,
@@ -166,47 +174,27 @@
     private final RunInLocale<Void> testsPredefinedSubtypesForSpacebar = new RunInLocale<Void>() {
         @Override
         protected Void job(final Resources res) {
-            assertEquals("en_US", "English (US)",
-                    SpacebarLanguageUtils.getFullDisplayName(EN_US));
-            assertEquals("en_GB", "English (UK)",
-                    SpacebarLanguageUtils.getFullDisplayName(EN_GB));
-            assertEquals("es_US", "Español (EE.UU.)",
-                    SpacebarLanguageUtils.getFullDisplayName(ES_US));
-            assertEquals("fr", "Français",
-                    SpacebarLanguageUtils.getFullDisplayName(FR));
-            assertEquals("fr_CA", "Français (Canada)",
-                    SpacebarLanguageUtils.getFullDisplayName(FR_CA));
-            assertEquals("fr_CH", "Français (Suisse)",
-                    SpacebarLanguageUtils.getFullDisplayName(FR_CH));
-            assertEquals("de", "Deutsch",
-                    SpacebarLanguageUtils.getFullDisplayName(DE));
-            assertEquals("de_CH", "Deutsch (Schweiz)",
-                    SpacebarLanguageUtils.getFullDisplayName(DE_CH));
-            assertEquals("hi_ZZ", "Hinglish",
-                    SpacebarLanguageUtils.getFullDisplayName(HI_ZZ));
-            assertEquals("zz", "QWERTY",
-                    SpacebarLanguageUtils.getFullDisplayName(ZZ));
+            assertEquals("en_US", "English (US)", EN_US.getFullDisplayName());
+            assertEquals("en_GB", "English (UK)", EN_GB.getFullDisplayName());
+            assertEquals("es_US", "Español (EE.UU.)", ES_US.getFullDisplayName());
+            assertEquals("fr", "Français", FR.getFullDisplayName());
+            assertEquals("fr_CA", "Français (Canada)", FR_CA.getFullDisplayName());
+            assertEquals("fr_CH", "Français (Suisse)", FR_CH.getFullDisplayName());
+            assertEquals("de", "Deutsch", DE.getFullDisplayName());
+            assertEquals("de_CH", "Deutsch (Schweiz)", DE_CH.getFullDisplayName());
+            assertEquals("hi_ZZ", "Hinglish", HI_ZZ.getFullDisplayName());
+            assertEquals("zz", "QWERTY", ZZ.getFullDisplayName());
 
-            assertEquals("en_US", "English",
-                    SpacebarLanguageUtils.getMiddleDisplayName(EN_US));
-            assertEquals("en_GB", "English",
-                    SpacebarLanguageUtils.getMiddleDisplayName(EN_GB));
-            assertEquals("es_US", "Español",
-                    SpacebarLanguageUtils.getMiddleDisplayName(ES_US));
-            assertEquals("fr", "Français",
-                    SpacebarLanguageUtils.getMiddleDisplayName(FR));
-            assertEquals("fr_CA", "Français",
-                    SpacebarLanguageUtils.getMiddleDisplayName(FR_CA));
-            assertEquals("fr_CH", "Français",
-                    SpacebarLanguageUtils.getMiddleDisplayName(FR_CH));
-            assertEquals("de", "Deutsch",
-                    SpacebarLanguageUtils.getMiddleDisplayName(DE));
-            assertEquals("de_CH", "Deutsch",
-                    SpacebarLanguageUtils.getMiddleDisplayName(DE_CH));
-            assertEquals("hi_ZZ", "Hinglish",
-                    SpacebarLanguageUtils.getMiddleDisplayName(HI_ZZ));
-            assertEquals("zz", "QWERTY",
-                    SpacebarLanguageUtils.getMiddleDisplayName(ZZ));
+            assertEquals("en_US", "English", EN_US.getMiddleDisplayName());
+            assertEquals("en_GB", "English", EN_GB.getMiddleDisplayName());
+            assertEquals("es_US", "Español", ES_US.getMiddleDisplayName());
+            assertEquals("fr", "Français", FR.getMiddleDisplayName());
+            assertEquals("fr_CA", "Français", FR_CA.getMiddleDisplayName());
+            assertEquals("fr_CH", "Français", FR_CH.getMiddleDisplayName());
+            assertEquals("de", "Deutsch", DE.getMiddleDisplayName());
+            assertEquals("de_CH", "Deutsch", DE_CH.getMiddleDisplayName());
+            assertEquals("hi_ZZ", "Hinglish", HI_ZZ.getMiddleDisplayName());
+            assertEquals("zz", "QWERTY", ZZ.getMiddleDisplayName());
             return null;
         }
     };
@@ -214,35 +202,21 @@
     private final RunInLocale<Void> testsAdditionalSubtypesForSpacebar = new RunInLocale<Void>() {
         @Override
         protected Void job(final Resources res) {
-            assertEquals("fr qwertz", "Français",
-                    SpacebarLanguageUtils.getFullDisplayName(FR_QWERTZ));
-            assertEquals("de qwerty", "Deutsch",
-                    SpacebarLanguageUtils.getFullDisplayName(DE_QWERTY));
-            assertEquals("en_US azerty", "English (US)",
-                    SpacebarLanguageUtils.getFullDisplayName(EN_US_AZERTY));
-            assertEquals("en_UK dvorak", "English (UK)",
-                    SpacebarLanguageUtils.getFullDisplayName(EN_UK_DVORAK));
-            assertEquals("es_US colemak", "Español (EE.UU.)",
-                    SpacebarLanguageUtils.getFullDisplayName(ES_US_COLEMAK));
-            assertEquals("zz azerty", "AZERTY",
-                    SpacebarLanguageUtils.getFullDisplayName(ZZ_AZERTY));
-            assertEquals("zz pc", "PC",
-                    SpacebarLanguageUtils.getFullDisplayName(ZZ_PC));
+            assertEquals("fr qwertz", "Français", FR_QWERTZ.getFullDisplayName());
+            assertEquals("de qwerty", "Deutsch", DE_QWERTY.getFullDisplayName());
+            assertEquals("en_US azerty", "English (US)", EN_US_AZERTY.getFullDisplayName());
+            assertEquals("en_UK dvorak", "English (UK)", EN_UK_DVORAK.getFullDisplayName());
+            assertEquals("es_US colemak", "Español (EE.UU.)", ES_US_COLEMAK.getFullDisplayName());
+            assertEquals("zz azerty", "AZERTY", ZZ_AZERTY.getFullDisplayName());
+            assertEquals("zz pc", "PC", ZZ_PC.getFullDisplayName());
 
-            assertEquals("fr qwertz", "Français",
-                    SpacebarLanguageUtils.getMiddleDisplayName(FR_QWERTZ));
-            assertEquals("de qwerty", "Deutsch",
-                    SpacebarLanguageUtils.getMiddleDisplayName(DE_QWERTY));
-            assertEquals("en_US azerty", "English",
-                    SpacebarLanguageUtils.getMiddleDisplayName(EN_US_AZERTY));
-            assertEquals("en_UK dvorak", "English",
-                    SpacebarLanguageUtils.getMiddleDisplayName(EN_UK_DVORAK));
-            assertEquals("es_US colemak", "Español",
-                    SpacebarLanguageUtils.getMiddleDisplayName(ES_US_COLEMAK));
-            assertEquals("zz azerty", "AZERTY",
-                    SpacebarLanguageUtils.getMiddleDisplayName(ZZ_AZERTY));
-            assertEquals("zz pc", "PC",
-                    SpacebarLanguageUtils.getMiddleDisplayName(ZZ_PC));
+            assertEquals("fr qwertz", "Français", FR_QWERTZ.getMiddleDisplayName());
+            assertEquals("de qwerty", "Deutsch", DE_QWERTY.getMiddleDisplayName());
+            assertEquals("en_US azerty", "English", EN_US_AZERTY.getMiddleDisplayName());
+            assertEquals("en_UK dvorak", "English", EN_UK_DVORAK.getMiddleDisplayName());
+            assertEquals("es_US colemak", "Español", ES_US_COLEMAK.getMiddleDisplayName());
+            assertEquals("zz azerty", "AZERTY", ZZ_AZERTY.getMiddleDisplayName());
+            assertEquals("zz pc", "PC", ZZ_PC.getMiddleDisplayName());
             return null;
         }
     };
diff --git a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
index 4f0b3cc..5afbbbe 100644
--- a/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/utils/SubtypeLocaleUtilsTests.java
@@ -24,6 +24,7 @@
 import android.view.inputmethod.InputMethodSubtype;
 
 import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.RichInputMethodSubtype;
 
 import java.util.ArrayList;
 import java.util.Locale;
@@ -31,7 +32,7 @@
 @SmallTest
 public class SubtypeLocaleUtilsTests extends AndroidTestCase {
     // All input method subtypes of LatinIME.
-    private final ArrayList<InputMethodSubtype> mSubtypesList = new ArrayList<>();
+    private final ArrayList<RichInputMethodSubtype> mSubtypesList = new ArrayList<>();
 
     private RichInputMethodManager mRichImm;
     private Resources mRes;
@@ -67,7 +68,7 @@
         final int subtypeCount = imi.getSubtypeCount();
         for (int index = 0; index < subtypeCount; index++) {
             final InputMethodSubtype subtype = imi.getSubtypeAt(index);
-            mSubtypesList.add(subtype);
+            mSubtypesList.add(new RichInputMethodSubtype(subtype));
         }
 
         EN_US = mRichImm.findSubtypeByLocaleAndKeyboardLayoutSet(
@@ -107,12 +108,12 @@
     }
 
     public void testAllFullDisplayName() {
-        for (final InputMethodSubtype subtype : mSubtypesList) {
+        for (final RichInputMethodSubtype subtype : mSubtypesList) {
             final String subtypeName = SubtypeLocaleUtils
-                    .getSubtypeDisplayNameInSystemLocale(subtype);
-            if (SubtypeLocaleUtils.isNoLanguage(subtype)) {
+                    .getSubtypeDisplayNameInSystemLocale(subtype.getRawSubtype());
+            if (subtype.isNoLanguage()) {
                 final String layoutName = SubtypeLocaleUtils
-                        .getKeyboardLayoutSetDisplayName(subtype);
+                        .getKeyboardLayoutSetDisplayName(subtype.getRawSubtype());
                 assertTrue(subtypeName, subtypeName.contains(layoutName));
             } else {
                 final String languageName = SubtypeLocaleUtils
@@ -308,10 +309,12 @@
                 .findSubtypeByLocaleAndKeyboardLayoutSet("iw", "hebrew");
         assertNotNull("Hebrew", HEBREW);
 
-        for (final InputMethodSubtype subtype : mSubtypesList) {
+        for (final RichInputMethodSubtype subtype : mSubtypesList) {
+            final InputMethodSubtype rawSubtype = subtype.getRawSubtype();
             final String subtypeName = SubtypeLocaleUtils
-                    .getSubtypeDisplayNameInSystemLocale(subtype);
-            if (subtype.equals(ARABIC) || subtype.equals(FARSI) || subtype.equals(HEBREW)) {
+                    .getSubtypeDisplayNameInSystemLocale(rawSubtype);
+            if (rawSubtype.equals(ARABIC) || rawSubtype.equals(FARSI)
+                    || rawSubtype.equals(HEBREW)) {
                 assertTrue(subtypeName, SubtypeLocaleUtils.isRtlLanguage(subtype));
             } else {
                 assertFalse(subtypeName, SubtypeLocaleUtils.isRtlLanguage(subtype));