Merge "Squash a ridiculously improbable NPE"
diff --git a/java/res/values-ar/donottranslate-more-keys.xml b/java/res/values-ar/donottranslate-more-keys.xml
index cde6860..e49677a 100644
--- a/java/res/values-ar/donottranslate-more-keys.xml
+++ b/java/res/values-ar/donottranslate-more-keys.xml
@@ -70,10 +70,8 @@
     <string name="keylabel_for_symbols_percent">\u066a</string>
     <string name="more_keys_for_comma">,</string>
     <string name="more_keys_for_f1">,</string>
-    <!-- @icon/3 is iconSettingsKey -->
-    <string name="more_keys_for_f1_settings">\\,,\@icon/3|\@integer/key_settings</string>
-    <!-- @icon/7 is iconTabKey -->
-    <string name="more_keys_for_f1_navigate">\\,,\@icon/7|\@integer/key_tab</string>
+    <string name="more_keys_for_f1_settings">\\,,\@icon/settingsKey|\@integer/key_settings</string>
+    <string name="more_keys_for_f1_navigate">\\,,\@icon/tabKey|\@integer/key_tab</string>
     <string name="more_keys_for_symbols_question">\?</string>
     <string name="more_keys_for_symbols_semicolon">;</string>
     <string name="more_keys_for_symbols_percent">%,‰</string>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 9892d78..57930c6 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -248,12 +248,10 @@
             <!-- This should be aligned with the KeyboardIcons.ICONS_TO_ATTRS_MAP -->
             <enum name="iconShiftKey" value="1" />
             <enum name="iconDeleteKey" value="2" />
-            <!-- This is also represented as "@icon/3" in keyboard layout XML. -->
             <enum name="iconSettingsKey" value="3" />
             <enum name="iconSpaceKey" value="4" />
             <enum name="iconReturnKey" value="5" />
             <enum name="iconSearchKey" value="6" />
-            <!-- This is also represented as "@icon/7" in keyboard layout XML. -->
             <enum name="iconTabKey" value="7" />
             <enum name="iconShortcutKey" value="8" />
             <enum name="iconShortcutForLabel" value="9" />
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 55f35f0..5e68043 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -25,7 +25,8 @@
     <bool name="config_enable_show_voice_key_option">true</bool>
     <bool name="config_enable_show_popup_on_keypress_option">true</bool>
     <bool name="config_enable_bigram_suggestions_option">true</bool>
-    <bool name="config_enable_usability_study_mode_option">false</bool>
+    <!-- TODO: Disable the following configuration for production. -->
+    <bool name="config_enable_usability_study_mode_option">true</bool>
     <bool name="config_sliding_key_input_enabled">true</bool>
     <bool name="config_digit_more_keys_enabled">true</bool>
     <!-- Whether or not Popup on key press is enabled by default -->
diff --git a/java/res/values/donottranslate-more-keys.xml b/java/res/values/donottranslate-more-keys.xml
index 3a3ea1e..ce15381 100644
--- a/java/res/values/donottranslate-more-keys.xml
+++ b/java/res/values/donottranslate-more-keys.xml
@@ -91,10 +91,8 @@
     <string name="keylabel_for_symbols_percent">%</string>
     <string name="more_keys_for_comma"></string>
     <string name="more_keys_for_f1"></string>
-    <!-- @icon/3 is iconSettingsKey -->
-    <string name="more_keys_for_f1_settings">\@icon/3|\@integer/key_settings</string>
-    <!-- @icon/7 is iconTabKey -->
-    <string name="more_keys_for_f1_navigate">\@icon/7|\@integer/key_tab</string>
+    <string name="more_keys_for_f1_settings">\@icon/settingsKey|\@integer/key_settings</string>
+    <string name="more_keys_for_f1_navigate">\@icon/tabKey|\@integer/key_tab</string>
     <string name="more_keys_for_symbols_question">¿</string>
     <string name="more_keys_for_symbols_semicolon"></string>
     <string name="more_keys_for_symbols_percent">‰</string>
diff --git a/java/res/xml-bg/keyboard_set.xml b/java/res/xml-bg/keyboard_set.xml
new file mode 100644
index 0000000..a789de6
--- /dev/null
+++ b/java/res/xml-bg/keyboard_set.xml
@@ -0,0 +1,42 @@
+<?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.
+*/
+-->
+
+<KeyboardSet
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:keyboardLocale="bg">
+    <Element
+        latin:elementName="alphabet"
+        latin:elementKeyboard="@xml/kbd_bulgarian" />
+    <Element
+        latin:elementName="symbols"
+        latin:elementKeyboard="@xml/kbd_symbols" />
+    <Element
+        latin:elementName="symbolsShift"
+        latin:elementKeyboard="@xml/kbd_symbols_shift" />
+    <Element
+        latin:elementName="phone"
+        latin:elementKeyboard="@xml/kbd_phone" />
+    <Element
+        latin:elementName="phoneShift"
+        latin:elementKeyboard="@xml/kbd_phone_shift" />
+    <Element
+        latin:elementName="number"
+        latin:elementKeyboard="@xml/kbd_number" />
+</KeyboardSet>
diff --git a/java/res/xml-sw600dp/kbd_key_styles.xml b/java/res/xml-sw600dp/kbd_key_styles.xml
index ab90c98..abba592 100644
--- a/java/res/xml-sw600dp/kbd_key_styles.xml
+++ b/java/res/xml-sw600dp/kbd_key_styles.xml
@@ -34,7 +34,7 @@
             <key-style
                 latin:styleName="f2PopupStyle"
                 latin:keyLabelFlags="hasPopupHint"
-                latin:moreKeys="\@icon/3|\@integer/key_settings"
+                latin:moreKeys="\@icon/settingsKey|\@integer/key_settings"
                 latin:backgroundType="functional" />
         </default>
     </switch>
diff --git a/java/res/xml-sw600dp/kbd_rows_bulgarian.xml b/java/res/xml-sw600dp/kbd_rows_bulgarian.xml
new file mode 100644
index 0000000..746398d
--- /dev/null
+++ b/java/res/xml-sw600dp/kbd_rows_bulgarian.xml
@@ -0,0 +1,138 @@
+<?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"
+>
+    <include
+        latin:keyboardLayout="@xml/kbd_key_styles" />
+    <Row
+        latin:keyWidth="7.692%p"
+    >
+        <Key
+            latin:keyLabel="ч" />
+        <Key
+            latin:keyLabel="ш" />
+        <Key
+            latin:keyLabel="е" />
+        <Key
+            latin:keyLabel="р" />
+        <Key
+            latin:keyLabel="т" />
+        <Key
+            latin:keyLabel="ъ" />
+        <Key
+            latin:keyLabel="у" />
+        <Key
+            latin:keyLabel="и"
+            latin:moreKeys="ѝ" />
+        <Key
+            latin:keyLabel="о" />
+        <Key
+            latin:keyLabel="п" />
+        <Key
+            latin:keyLabel="я" />
+        <Key
+            latin:keyStyle="deleteKeyStyle"
+            latin:keyWidth="fillBoth" />
+    </Row>
+    <Row
+        latin:keyWidth="7.692%p"
+    >
+        <Key
+            latin:keyLabel="а"
+            latin:keyXPos="4.000%p" />
+        <Key
+            latin:keyLabel="с" />
+        <Key
+            latin:keyLabel="д" />
+        <Key
+            latin:keyLabel="ф" />
+        <Key
+            latin:keyLabel="г" />
+        <Key
+            latin:keyLabel="х" />
+        <Key
+            latin:keyLabel="й" />
+        <Key
+            latin:keyLabel="к" />
+        <Key
+            latin:keyLabel="л" />
+        <Key
+            latin:keyLabel="щ" />
+        <Key
+            latin:keyLabel="ь" />
+        <Key
+            latin:keyStyle="returnKeyStyle"
+            latin:keyWidth="fillBoth" />
+    </Row>
+    <Row
+        latin:keyWidth="7.692%p"
+    >
+        <Key
+            latin:keyStyle="shiftKeyStyle"
+            latin:keyWidth="10.000%p" />
+        <Key
+            latin:keyLabel="з" />
+        <Key
+            latin:keyLabel="ж" />
+        <Key
+            latin:keyLabel="ц" />
+        <Key
+            latin:keyLabel="в" />
+        <Key
+            latin:keyLabel="б" />
+        <Key
+            latin:keyLabel="н" />
+        <Key
+            latin:keyLabel="м" />
+        <Key
+            latin:keyLabel="ю" />
+        <switch>
+            <case
+                latin:mode="email"
+            >
+                <Key
+                    latin:keyLabel="," />
+                <Key
+                    latin:keyLabel="." />
+            </case>
+            <default>
+                <Key
+                    latin:keyLabel=","
+                    latin:keyLabelFlags="hasUppercaseLetter"
+                    latin:keyHintLabel="!"
+                    latin:moreKeys="!" />
+                <Key
+                    latin:keyLabel="."
+                    latin:keyLabelFlags="hasUppercaseLetter"
+                    latin:keyHintLabel="\?"
+                    latin:moreKeys="\?" />
+            </default>
+        </switch>
+        <Spacer
+            latin:keyXPos="-10.000%p"
+            latin:keyWidth="0%p" />
+        <include
+            latin:keyboardLayout="@xml/kbd_row3_smiley" />
+    </Row>
+    <include
+        latin:keyboardLayout="@xml/kbd_qwerty_row4" />
+</merge>
diff --git a/java/res/xml-sw768dp/kbd_rows_bulgarian.xml b/java/res/xml-sw768dp/kbd_rows_bulgarian.xml
new file mode 100644
index 0000000..42b3da5
--- /dev/null
+++ b/java/res/xml-sw768dp/kbd_rows_bulgarian.xml
@@ -0,0 +1,122 @@
+<?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"
+>
+    <include
+        latin:keyboardLayout="@xml/kbd_key_styles" />
+    <Row
+        latin:keyWidth="7.333%p"
+    >
+        <Key
+            latin:keyStyle="tabKeyStyle"
+            latin:keyLabelFlags="alignLeft" />
+        <Key
+            latin:keyLabel="ч" />
+        <Key
+            latin:keyLabel="ш" />
+        <Key
+            latin:keyLabel="е" />
+        <Key
+            latin:keyLabel="р" />
+        <Key
+            latin:keyLabel="т" />
+        <Key
+            latin:keyLabel="ъ" />
+        <Key
+            latin:keyLabel="у" />
+        <Key
+            latin:keyLabel="и"
+            latin:moreKeys="ѝ" />
+        <Key
+            latin:keyLabel="о" />
+        <Key
+            latin:keyLabel="п" />
+        <Key
+            latin:keyLabel="я" />
+        <Key
+            latin:keyStyle="deleteKeyStyle"
+            latin:keyWidth="fillBoth" />
+    </Row>
+    <Row
+        latin:keyWidth="7.194%p"
+    >
+        <Key
+            latin:keyStyle="toSymbolKeyStyle"
+            latin:keyLabelFlags="alignLeft"
+            latin:keyWidth="9.375%p" />
+        <Key
+            latin:keyLabel="а" />
+        <Key
+            latin:keyLabel="с" />
+        <Key
+            latin:keyLabel="д" />
+        <Key
+            latin:keyLabel="ф" />
+        <Key
+            latin:keyLabel="г" />
+        <Key
+            latin:keyLabel="х" />
+        <Key
+            latin:keyLabel="й" />
+        <Key
+            latin:keyLabel="к" />
+        <Key
+            latin:keyLabel="л" />
+        <Key
+            latin:keyLabel="щ" />
+        <Key
+            latin:keyLabel="ь" />
+        <Key
+            latin:keyStyle="returnKeyStyle"
+            latin:keyWidth="fillBoth" />
+    </Row>
+    <Row
+        latin:keyWidth="7.125%p"
+    >
+        <Key
+            latin:keyStyle="shiftKeyStyle"
+            latin:keyWidth="14.375%p" />
+        <Key
+            latin:keyLabel="з" />
+        <Key
+            latin:keyLabel="ж" />
+        <Key
+            latin:keyLabel="ц" />
+        <Key
+            latin:keyLabel="в" />
+        <Key
+            latin:keyLabel="б" />
+        <Key
+            latin:keyLabel="н" />
+        <Key
+            latin:keyLabel="м" />
+        <Key
+            latin:keyLabel="ю" />
+        <include
+            latin:keyboardLayout="@xml/kbd_row3_comma_period" />
+        <Key
+            latin:keyStyle="shiftKeyStyle"
+            latin:keyWidth="fillBoth" />
+    </Row>
+    <include
+        latin:keyboardLayout="@xml/kbd_qwerty_row4" />
+</merge>
diff --git a/java/res/xml/kbd_bulgarian.xml b/java/res/xml/kbd_bulgarian.xml
new file mode 100644
index 0000000..f114383
--- /dev/null
+++ b/java/res/xml/kbd_bulgarian.xml
@@ -0,0 +1,26 @@
+<?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.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+>
+    <include
+        latin:keyboardLayout="@xml/kbd_rows_bulgarian" />
+</Keyboard>
diff --git a/java/res/xml/kbd_rows_bulgarian.xml b/java/res/xml/kbd_rows_bulgarian.xml
new file mode 100644
index 0000000..7b18361
--- /dev/null
+++ b/java/res/xml/kbd_rows_bulgarian.xml
@@ -0,0 +1,128 @@
+<?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"
+>
+    <include
+        latin:keyboardLayout="@xml/kbd_key_styles" />
+    <Row
+        latin:keyWidth="9.091%p"
+    >
+        <Key
+            latin:keyLabel="ч"
+            latin:keyHintLabel="1"
+            latin:moreKeys="1" />
+        <Key
+            latin:keyLabel="ш"
+            latin:keyHintLabel="2"
+            latin:moreKeys="2" />
+        <Key
+            latin:keyLabel="е"
+            latin:keyHintLabel="3"
+            latin:moreKeys="3" />
+        <Key
+            latin:keyLabel="р"
+            latin:keyHintLabel="4"
+            latin:moreKeys="4" />
+        <Key
+            latin:keyLabel="т"
+            latin:keyHintLabel="5"
+            latin:moreKeys="5" />
+        <Key
+            latin:keyLabel="ъ"
+            latin:keyHintLabel="6"
+            latin:moreKeys="6" />
+        <Key
+            latin:keyLabel="у"
+            latin:keyHintLabel="7"
+            latin:moreKeys="7" />
+        <Key
+            latin:keyLabel="и"
+            latin:keyHintLabel="8"
+            latin:moreKeys="8,ѝ" />
+        <Key
+            latin:keyLabel="о"
+            latin:keyHintLabel="9"
+            latin:moreKeys="9" />
+        <Key
+            latin:keyLabel="п"
+            latin:keyHintLabel="0"
+            latin:moreKeys="0" />
+        <Key
+            latin:keyLabel="я"
+            latin:keyWidth="fillRight" />
+    </Row>
+    <Row
+            latin:keyWidth="9.091%p"
+    >
+        <Key
+            latin:keyLabel="а" />
+        <Key
+            latin:keyLabel="с" />
+        <Key
+            latin:keyLabel="д" />
+        <Key
+            latin:keyLabel="ф" />
+        <Key
+            latin:keyLabel="г" />
+        <Key
+            latin:keyLabel="х" />
+        <Key
+            latin:keyLabel="й" />
+        <Key
+            latin:keyLabel="к" />
+        <Key
+            latin:keyLabel="л" />
+        <Key
+            latin:keyLabel="щ" />
+        <Key
+            latin:keyLabel="ь"
+            latin:keyWidth="fillRight" />
+    </Row>
+    <Row
+        latin:keyWidth="9.091%p"
+    >
+        <Key
+            latin:keyStyle="shiftKeyStyle"
+            latin:keyWidth="13.636%p" />
+        <Key
+            latin:keyLabel="з" />
+        <Key
+            latin:keyLabel="ж" />
+        <Key
+            latin:keyLabel="ц" />
+        <Key
+            latin:keyLabel="в" />
+        <Key
+            latin:keyLabel="б" />
+        <Key
+            latin:keyLabel="н" />
+        <Key
+            latin:keyLabel="м" />
+        <Key
+            latin:keyLabel="ю" />
+        <Key
+            latin:keyStyle="deleteKeyStyle"
+            latin:keyWidth="fillRight" />
+    </Row>
+    <include
+        latin:keyboardLayout="@xml/kbd_qwerty_row4" />
+</merge>
diff --git a/java/res/xml/method.xml b/java/res/xml/method.xml
index 6c82736..650f91b 100644
--- a/java/res/xml/method.xml
+++ b/java/res/xml/method.xml
@@ -20,8 +20,8 @@
 <!-- The attributes in this XML file provide configuration information -->
 <!-- for the Input Method Manager. -->
 
-<!-- Keyboard: en_US, en_GB, ar, be, cs, da, de, de(QWERTY), es, es_US, et, fi, fr, fr_CA, fr_CH,
-     hr, hu, it, iw, ky, lt, lv, nb, nl, pl, pt, ro, ru, sk, sl, sr, sv, tr, uk, vi -->
+<!-- Keyboard: en_US, en_GB, ar, be, bg, cs, da, de, de(QWERTY), es, es_US, et, fi, fr, fr_CA,
+     fr_CH, hr, hu, it, iw, ky, lt, lv, nb, nl, pl, pt, ro, ru, sk, sl, sr, sv, tr, uk, vi -->
 <!-- TODO: use <lang>_keyboard icon instead of a common keyboard icon. -->
 <!-- If IME doesn't have an applicable subtype, the first subtype will be used as a default
      subtype.-->
@@ -54,6 +54,12 @@
     />
     <subtype android:icon="@drawable/ic_subtype_keyboard"
             android:label="@string/subtype_generic"
+            android:imeSubtypeLocale="bg"
+            android:imeSubtypeMode="keyboard"
+            android:imeSubtypeExtraValue="SupportTouchPositionCorrection"
+    />
+    <subtype android:icon="@drawable/ic_subtype_keyboard"
+            android:label="@string/subtype_generic"
             android:imeSubtypeLocale="cs"
             android:imeSubtypeMode="keyboard"
             android:imeSubtypeExtraValue="AsciiCapable,SupportTouchPositionCorrection"
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index ebd6150..3a9423f 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -101,7 +101,7 @@
     /** Text to output when pressed. This can be multiple characters, like ".com" */
     public final CharSequence mOutputText;
     /** More keys */
-    public final CharSequence[] mMoreKeys;
+    public final String[] mMoreKeys;
     /** More keys maximum column number */
     public final int mMaxMoreKeysColumn;
 
@@ -166,7 +166,12 @@
     }
 
     private static Drawable getIcon(Keyboard.Params params, String moreKeySpec) {
-        return params.mIconsSet.getIconByIconId(MoreKeySpecParser.getIconId(moreKeySpec));
+        final int iconAttrId = MoreKeySpecParser.getIconAttrId(moreKeySpec);
+        if (iconAttrId == KeyboardIconsSet.ICON_UNDEFINED) {
+            return null;
+        } else {
+            return params.mIconsSet.getIconByAttrId(iconAttrId);
+        }
     }
 
     /**
@@ -255,7 +260,7 @@
         // Update row to have current x coordinate.
         row.setXPos(keyXPos + keyWidth);
 
-        final CharSequence[] moreKeys = style.getTextArray(keyAttr,
+        final String[] moreKeys = style.getTextArray(keyAttr,
                 R.styleable.Keyboard_Key_moreKeys);
         // In Arabic symbol layouts, we'd like to keep digits in more keys regardless of
         // config_digit_more_keys_enabled.
@@ -277,12 +282,15 @@
                 R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0);
         mVisualInsetsRight = (int) Keyboard.Builder.getDimensionOrFraction(keyAttr,
                 R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0);
-        mPreviewIcon = iconsSet.getIconByIconId(style.getInt(keyAttr,
+        final int previewIconAttrId = KeyboardIconsSet.getIconAttrId(style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_keyIconPreview, KeyboardIconsSet.ICON_UNDEFINED));
-        mIcon = iconsSet.getIconByIconId(style.getInt(keyAttr,
+        mPreviewIcon = iconsSet.getIconByAttrId(previewIconAttrId);
+        final int iconAttrId = KeyboardIconsSet.getIconAttrId(style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_keyIcon, KeyboardIconsSet.ICON_UNDEFINED));
-        mDisabledIcon = iconsSet.getIconByIconId(style.getInt(keyAttr,
+        mIcon = iconsSet.getIconByAttrId(iconAttrId);
+        final int disabledIconAttrId = KeyboardIconsSet.getIconAttrId(style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_keyIconDisabled, KeyboardIconsSet.ICON_UNDEFINED));
+        mDisabledIcon = iconsSet.getIconByAttrId(disabledIconAttrId);
         mHintLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyHintLabel);
 
         mLabel = style.getText(keyAttr, R.styleable.Keyboard_Key_keyLabel);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 22ab3e4..4967a5e 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -137,8 +137,7 @@
         mKeyboardSet = builder.build();
         final KeyboardId mainKeyboardId = mKeyboardSet.getMainKeyboardId();
         try {
-            mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols),
-                    hasDistinctMultitouch());
+            mState.onLoadKeyboard(mResources.getString(R.string.layout_switch_back_symbols));
         } catch (RuntimeException e) {
             Log.w(TAG, "loading keyboard failed: " + mainKeyboardId, e);
             LatinImeLogger.logOnException(mainKeyboardId.toString(), e);
@@ -316,7 +315,7 @@
     }
 
     /**
-     * Updates state machine to figure out when to automatically snap back to the previous mode.
+     * Updates state machine to figure out when to automatically switch back to the previous mode.
      */
     public void onCodeInput(int code) {
         mState.onCodeInput(code, isSinglePointer(), mInputMethodService.getCurrentAutoCapsState());
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
index 548b5ea..9742913 100644
--- a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
@@ -34,7 +34,7 @@
     }
 
     public static class Builder extends Keyboard.Builder<Builder.MiniKeyboardParams> {
-        private final CharSequence[] mMoreKeys;
+        private final String[] mMoreKeys;
 
         public static class MiniKeyboardParams extends Keyboard.Params {
             /* package */int mTopRowAdjustment;
@@ -230,16 +230,14 @@
                     parentKey.mX + (mParams.mDefaultKeyWidth - width) / 2, view.getMeasuredWidth());
         }
 
-        private static int getMaxKeyWidth(KeyboardView view, CharSequence[] moreKeys,
-                int minKeyWidth) {
+        private static int getMaxKeyWidth(KeyboardView view, String[] moreKeys, int minKeyWidth) {
             final int padding = (int) view.getContext().getResources()
                     .getDimension(R.dimen.mini_keyboard_key_horizontal_padding);
             Paint paint = null;
             int maxWidth = minKeyWidth;
-            for (CharSequence moreKeySpec : moreKeys) {
-                final CharSequence label = MoreKeySpecParser.getLabel(moreKeySpec.toString());
-                // If the label is single letter, minKeyWidth is enough to hold
-                // the label.
+            for (String moreKeySpec : moreKeys) {
+                final String label = MoreKeySpecParser.getLabel(moreKeySpec);
+                // If the label is single letter, minKeyWidth is enough to hold the label.
                 if (label != null && label.length() > 1) {
                     if (paint == null) {
                         paint = new Paint();
@@ -258,7 +256,7 @@
         public MiniKeyboard build() {
             final MiniKeyboardParams params = mParams;
             for (int n = 0; n < mMoreKeys.length; n++) {
-                final String moreKeySpec = mMoreKeys[n].toString();
+                final String moreKeySpec = mMoreKeys[n];
                 final int row = n / params.mNumColumns;
                 final Key key = new Key(mResources, params, moreKeySpec, params.getX(n, row),
                         params.getY(row), params.mDefaultKeyWidth, params.mDefaultRowHeight);
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
index 5dd8340..faea389 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -16,11 +16,13 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.util.Log;
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.XmlParseUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -30,7 +32,7 @@
 import java.util.HashMap;
 
 public class KeyStyles {
-    private static final String TAG = "KeyStyles";
+    private static final String TAG = KeyStyles.class.getSimpleName();
     private static final boolean DEBUG = false;
 
     private final HashMap<String, DeclaredKeyStyle> mStyles =
@@ -38,19 +40,19 @@
     private static final KeyStyle EMPTY_KEY_STYLE = new EmptyKeyStyle();
 
     public interface KeyStyle {
-        public CharSequence[] getTextArray(TypedArray a, int index);
+        public String[] getTextArray(TypedArray a, int index);
         public CharSequence getText(TypedArray a, int index);
         public int getInt(TypedArray a, int index, int defaultValue);
         public int getFlag(TypedArray a, int index, int defaultValue);
     }
 
-    /* package */ static class EmptyKeyStyle implements KeyStyle {
+    private static class EmptyKeyStyle implements KeyStyle {
         EmptyKeyStyle() {
             // Nothing to do.
         }
 
         @Override
-        public CharSequence[] getTextArray(TypedArray a, int index) {
+        public String[] getTextArray(TypedArray a, int index) {
             return parseTextArray(a, index);
         }
 
@@ -69,64 +71,77 @@
             return a.getInt(index, defaultValue);
         }
 
-        protected static CharSequence[] parseTextArray(TypedArray a, int index) {
+        protected static String[] parseTextArray(TypedArray a, int index) {
             if (!a.hasValue(index))
                 return null;
             final CharSequence text = a.getText(index);
-            return parseCsvText(text);
-        }
-
-        /* package */ static CharSequence[] parseCsvText(CharSequence text) {
-            final int size = text.length();
-            if (size == 0) return null;
-            if (size == 1) return new CharSequence[] { text };
-            final StringBuilder sb = new StringBuilder();
-            ArrayList<CharSequence> list = null;
-            int start = 0;
-            for (int pos = 0; pos < size; pos++) {
-                final char c = text.charAt(pos);
-                if (c == ',') {
-                    if (list == null) list = new ArrayList<CharSequence>();
-                    if (sb.length() == 0) {
-                        list.add(text.subSequence(start, pos));
-                    } else {
-                        list.add(sb.toString());
-                        sb.setLength(0);
-                    }
-                    start = pos + 1;
-                    continue;
-                } else if (c == '\\') {
-                    if (start == pos) {
-                        // Skip escape character at the beginning of the value.
-                        start++;
-                        pos++;
-                    } else {
-                        if (start < pos && sb.length() == 0)
-                            sb.append(text.subSequence(start, pos));
-                        pos++;
-                        if (pos < size)
-                            sb.append(text.charAt(pos));
-                    }
-                } else if (sb.length() > 0) {
-                    sb.append(c);
-                }
-            }
-            if (list == null) {
-                return new CharSequence[] { sb.length() > 0 ? sb : text.subSequence(start, size) };
-            } else {
-                list.add(sb.length() > 0 ? sb : text.subSequence(start, size));
-                return list.toArray(new CharSequence[list.size()]);
-            }
+            return parseCsvText(text.toString(), a.getResources(), R.string.english_ime_name);
         }
     }
 
-    /* package */ static class DeclaredKeyStyle extends EmptyKeyStyle {
+    /* package for test */
+    static String[] parseCsvText(String rawText, Resources res, int packageNameResId) {
+        final String text = Utils.resolveStringResource(rawText, res, packageNameResId);
+        final int size = text.length();
+        if (size == 0) {
+            return null;
+        }
+        if (size == 1) {
+            return new String[] { text };
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        ArrayList<String> list = null;
+        int start = 0;
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == ',') {
+                if (list == null) {
+                    list = new ArrayList<String>();
+                }
+                if (sb.length() == 0) {
+                    list.add(text.substring(start, pos));
+                } else {
+                    list.add(sb.toString());
+                    sb.setLength(0);
+                }
+                start = pos + 1;
+                continue;
+            } else if (c == Utils.ESCAPE_CHAR) {
+                if (start == pos) {
+                    // Skip escape character at the beginning of the value.
+                    start++;
+                    pos++;
+                } else {
+                    if (start < pos && sb.length() == 0) {
+                        sb.append(text.subSequence(start, pos));
+                    }
+                    pos++;
+                    if (pos < size) {
+                        sb.append(text.charAt(pos));
+                    }
+                }
+            } else if (sb.length() > 0) {
+                sb.append(c);
+            }
+        }
+        if (list == null) {
+            return new String[] {
+                    sb.length() > 0 ? sb.toString() : text.substring(start)
+            };
+        } else {
+            list.add(sb.length() > 0 ? sb.toString() : text.substring(start));
+            return list.toArray(new String[list.size()]);
+        }
+    }
+
+    private static class DeclaredKeyStyle extends EmptyKeyStyle {
         private final HashMap<Integer, Object> mAttributes = new HashMap<Integer, Object>();
 
         @Override
-        public CharSequence[] getTextArray(TypedArray a, int index) {
+        public String[] getTextArray(TypedArray a, int index) {
             return a.hasValue(index)
-                    ? super.getTextArray(a, index) : (CharSequence[])mAttributes.get(index);
+                    ? super.getTextArray(a, index) : (String[])mAttributes.get(index);
         }
 
         @Override
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
index 6313a61..09ecbca 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardIconsSet.java
@@ -31,34 +31,36 @@
     private static final String TAG = KeyboardIconsSet.class.getSimpleName();
 
     public static final int ICON_UNDEFINED = 0;
+    private static final int ATTR_UNDEFINED = 0;
 
     private final Map<Integer, Drawable> mIcons = new HashMap<Integer, Drawable>();
 
     // The key value should be aligned with the enum value of Keyboard.icon*.
     private static final Map<Integer, Integer> ICONS_TO_ATTRS_MAP = new HashMap<Integer, Integer>();
+    private static final Map<String, Integer> NAME_TO_ATTRS_MAP = new HashMap<String, Integer>();
     private static final Collection<Integer> VALID_ATTRS;
 
     static {
-        addIconIdMap(1, R.styleable.Keyboard_iconShiftKey);
-        addIconIdMap(2, R.styleable.Keyboard_iconDeleteKey);
-        // This is also represented as "@icon/3" in keyboard layout XML.
-        addIconIdMap(3, R.styleable.Keyboard_iconSettingsKey);
-        addIconIdMap(4, R.styleable.Keyboard_iconSpaceKey);
-        addIconIdMap(5, R.styleable.Keyboard_iconReturnKey);
-        addIconIdMap(6, R.styleable.Keyboard_iconSearchKey);
-        // This is also represented as "@icon/7" in keyboard layout XML.
-        addIconIdMap(7, R.styleable.Keyboard_iconTabKey);
-        addIconIdMap(8, R.styleable.Keyboard_iconShortcutKey);
-        addIconIdMap(9, R.styleable.Keyboard_iconShortcutForLabel);
-        addIconIdMap(10, R.styleable.Keyboard_iconSpaceKeyForNumberLayout);
-        addIconIdMap(11, R.styleable.Keyboard_iconShiftKeyShifted);
-        addIconIdMap(12, R.styleable.Keyboard_iconDisabledShortcutKey);
-        addIconIdMap(13, R.styleable.Keyboard_iconPreviewTabKey);
+        addIconIdMap(1, "shiftKey", R.styleable.Keyboard_iconShiftKey);
+        addIconIdMap(2, "deleteKey", R.styleable.Keyboard_iconDeleteKey);
+        addIconIdMap(3, "settingsKey", R.styleable.Keyboard_iconSettingsKey);
+        addIconIdMap(4, "spaceKey", R.styleable.Keyboard_iconSpaceKey);
+        addIconIdMap(5, "returnKey", R.styleable.Keyboard_iconReturnKey);
+        addIconIdMap(6, "searchKey", R.styleable.Keyboard_iconSearchKey);
+        addIconIdMap(7, "tabKey", R.styleable.Keyboard_iconTabKey);
+        addIconIdMap(8, "shortcutKey", R.styleable.Keyboard_iconShortcutKey);
+        addIconIdMap(9, "shortcutForLabel", R.styleable.Keyboard_iconShortcutForLabel);
+        addIconIdMap(10, "spaceKeyForNumberLayout",
+                R.styleable.Keyboard_iconSpaceKeyForNumberLayout);
+        addIconIdMap(11, "shiftKeyShifted", R.styleable.Keyboard_iconShiftKeyShifted);
+        addIconIdMap(12, "disabledShortcurKey", R.styleable.Keyboard_iconDisabledShortcutKey);
+        addIconIdMap(13, "previewTabKey", R.styleable.Keyboard_iconPreviewTabKey);
         VALID_ATTRS = ICONS_TO_ATTRS_MAP.values();
     }
 
-    private static void addIconIdMap(int iconId, int attrId) {
+    private static void addIconIdMap(int iconId, String name, Integer attrId) {
         ICONS_TO_ATTRS_MAP.put(iconId, attrId);
+        NAME_TO_ATTRS_MAP.put(name, attrId);
     }
 
     public void loadIcons(final TypedArray keyboardAttrs) {
@@ -76,18 +78,29 @@
         }
     }
 
-    public Drawable getIconByIconId(final Integer iconId) {
+    public static int getIconAttrId(final Integer iconId) {
         if (iconId == ICON_UNDEFINED) {
-            return null;
+            return ATTR_UNDEFINED;
         }
         final Integer attrId = ICONS_TO_ATTRS_MAP.get(iconId);
         if (attrId == null) {
             throw new IllegalArgumentException("icon id is out of range: " + iconId);
         }
-        return getIconByAttrId(attrId);
+        return attrId;
+    }
+
+    public static int getIconAttrId(final String iconName) {
+        final Integer attrId = NAME_TO_ATTRS_MAP.get(iconName);
+        if (attrId == null) {
+            throw new IllegalArgumentException("unknown icon name: " + iconName);
+        }
+        return attrId;
     }
 
     public Drawable getIconByAttrId(final Integer attrId) {
+        if (attrId == ATTR_UNDEFINED) {
+            return null;
+        }
         if (!VALID_ATTRS.contains(attrId)) {
             throw new IllegalArgumentException("unknown icon attribute id: " + attrId);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index c43b985..af16e49 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -26,7 +26,7 @@
  *
  * This class contains all keyboard state transition logic.
  *
- * The input events are {@link #onLoadKeyboard(String, boolean)}, {@link #onSaveKeyboardState()},
+ * The input events are {@link #onLoadKeyboard(String)}, {@link #onSaveKeyboardState()},
  * {@link #onPressKey(int)}, {@link #onReleaseKey(int, boolean)},
  * {@link #onCodeInput(int, boolean, boolean)}, {@link #onCancelInput(boolean)},
  * {@link #onUpdateShiftState(boolean)}.
@@ -58,7 +58,7 @@
         public void requestUpdatingShiftState();
     }
 
-    private KeyboardShiftState mKeyboardShiftState = new KeyboardShiftState();
+    private final SwitchActions mSwitchActions;
 
     private ShiftKeyState mShiftKeyState = new ShiftKeyState("Shift");
     private ModifierKeyState mSymbolKeyState = new ModifierKeyState("Symbol");
@@ -72,17 +72,15 @@
     private static final int SWITCH_STATE_CHORDING_ALPHA = 5;
     private static final int SWITCH_STATE_CHORDING_SYMBOL = 6;
     private int mSwitchState = SWITCH_STATE_ALPHA;
-
     private String mLayoutSwitchBackSymbols;
-    private boolean mHasDistinctMultitouch;
-
-    private final SwitchActions mSwitchActions;
 
     private boolean mIsAlphabetMode;
+    private KeyboardShiftState mAlphabetShiftState = new KeyboardShiftState();
     private boolean mIsSymbolShifted;
+    private boolean mPrevMainKeyboardWasShiftLocked;
+    private boolean mPrevSymbolsKeyboardWasShifted;
 
     private final SavedKeyboardState mSavedKeyboardState = new SavedKeyboardState();
-    private boolean mPrevMainKeyboardWasShiftLocked;
 
     static class SavedKeyboardState {
         public boolean mIsValid;
@@ -95,17 +93,17 @@
         mSwitchActions = switchActions;
     }
 
-    public void onLoadKeyboard(String layoutSwitchBackSymbols, boolean hasDistinctMultitouch) {
+    public void onLoadKeyboard(String layoutSwitchBackSymbols) {
         if (DEBUG_EVENT) {
             Log.d(TAG, "onLoadKeyboard");
         }
         mLayoutSwitchBackSymbols = layoutSwitchBackSymbols;
-        mHasDistinctMultitouch = hasDistinctMultitouch;
-        mKeyboardShiftState.setShifted(false);
-        mKeyboardShiftState.setShiftLocked(false);
+        // Reset alphabet shift state.
+        mAlphabetShiftState.setShiftLocked(false);
+        mPrevMainKeyboardWasShiftLocked = false;
+        mPrevSymbolsKeyboardWasShifted = false;
         mShiftKeyState.onRelease();
         mSymbolKeyState.onRelease();
-        mPrevMainKeyboardWasShiftLocked = false;
         onRestoreKeyboardState();
     }
 
@@ -113,9 +111,9 @@
         final SavedKeyboardState state = mSavedKeyboardState;
         state.mIsAlphabetMode = mIsAlphabetMode;
         if (mIsAlphabetMode) {
-            state.mIsShiftLocked = mKeyboardShiftState.isShiftLocked();
+            state.mIsShiftLocked = mAlphabetShiftState.isShiftLocked();
             state.mIsShifted = !state.mIsShiftLocked
-                    && mKeyboardShiftState.isShiftedOrShiftLocked();
+                    && mAlphabetShiftState.isShiftedOrShiftLocked();
         } else {
             state.mIsShiftLocked = false;
             state.mIsShifted = mIsSymbolShifted;
@@ -157,25 +155,23 @@
 
     // TODO: Remove this method.
     public boolean isShiftLocked() {
-        return mKeyboardShiftState.isShiftLocked();
+        return mAlphabetShiftState.isShiftLocked();
     }
 
     private void setShifted(int shiftMode) {
         if (DEBUG_ACTION) {
             Log.d(TAG, "setShifted: shiftMode=" + shiftModeToString(shiftMode));
         }
-        if (shiftMode == SwitchActions.AUTOMATIC_SHIFT) {
-            mKeyboardShiftState.setAutomaticTemporaryUpperCase();
-        } else {
-            final boolean shifted = (shiftMode == SwitchActions.MANUAL_SHIFT);
-            // On non-distinct multi touch panel device, we should also turn off the shift locked
-            // state when shift key is pressed to go to normal mode.
-            // On the other hand, on distinct multi touch panel device, turning off the shift
-            // locked state with shift key pressing is handled by onReleaseShift().
-            if (!mHasDistinctMultitouch && !shifted && mKeyboardShiftState.isShiftLocked()) {
-                mSwitchActions.setShiftLocked(false);
-            }
-            mKeyboardShiftState.setShifted(shifted);
+        switch (shiftMode) {
+        case SwitchActions.AUTOMATIC_SHIFT:
+            mAlphabetShiftState.setAutomaticTemporaryUpperCase();
+            break;
+        case SwitchActions.MANUAL_SHIFT:
+            mAlphabetShiftState.setShifted(true);
+            break;
+        case SwitchActions.UNSHIFT:
+            mAlphabetShiftState.setShifted(false);
+            break;
         }
         mSwitchActions.setShifted(shiftMode);
     }
@@ -184,7 +180,7 @@
         if (DEBUG_ACTION) {
             Log.d(TAG, "setShiftLocked: shiftLocked=" + shiftLocked);
         }
-        mKeyboardShiftState.setShiftLocked(shiftLocked);
+        mAlphabetShiftState.setShiftLocked(shiftLocked);
         mSwitchActions.setShiftLocked(shiftLocked);
     }
 
@@ -208,6 +204,7 @@
         if (DEBUG_ACTION) {
             Log.d(TAG, "setAlphabetKeyboard");
         }
+        mPrevSymbolsKeyboardWasShifted = mIsSymbolShifted;
         mSwitchActions.setAlphabetKeyboard();
         mIsAlphabetMode = true;
         mIsSymbolShifted = false;
@@ -219,13 +216,21 @@
 
     // TODO: Make this method private
     public void setSymbolsKeyboard() {
+        mPrevMainKeyboardWasShiftLocked = mAlphabetShiftState.isShiftLocked();
+        if (mPrevSymbolsKeyboardWasShifted) {
+            setSymbolsShiftedKeyboard();
+            return;
+        }
+
         if (DEBUG_ACTION) {
             Log.d(TAG, "setSymbolsKeyboard");
         }
-        mPrevMainKeyboardWasShiftLocked = mKeyboardShiftState.isShiftLocked();
         mSwitchActions.setSymbolsKeyboard();
         mIsAlphabetMode = false;
         mIsSymbolShifted = false;
+        // Reset alphabet shift state.
+        mAlphabetShiftState.setShiftLocked(false);
+        mPrevSymbolsKeyboardWasShifted = false;
         mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
     }
 
@@ -236,6 +241,9 @@
         mSwitchActions.setSymbolsShiftedKeyboard();
         mIsAlphabetMode = false;
         mIsSymbolShifted = true;
+        // Reset alphabet shift state.
+        mAlphabetShiftState.setShiftLocked(false);
+        mPrevSymbolsKeyboardWasShifted = false;
         mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
     }
 
@@ -273,7 +281,7 @@
     }
 
     private void onReleaseSymbol() {
-        // Snap back to the previous keyboard mode if the user chords the mode change key and
+        // Switch back to the previous keyboard mode if the user chords the mode change key and
         // another key, then releases the mode change key.
         if (mSwitchState == SWITCH_STATE_CHORDING_ALPHA) {
             toggleAlphabetAndSymbols();
@@ -290,7 +298,7 @@
 
     private void onUpdateShiftStateInternal(boolean autoCaps) {
         if (mIsAlphabetMode) {
-            if (!mKeyboardShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
+            if (!mAlphabetShiftState.isShiftLocked() && !mShiftKeyState.isIgnoring()) {
                 if (mShiftKeyState.isReleasing() && autoCaps) {
                     // Only when shift key is releasing, automatic temporary upper case will be set.
                     setShifted(SwitchActions.AUTOMATIC_SHIFT);
@@ -308,17 +316,17 @@
 
     private void onPressShift() {
         if (mIsAlphabetMode) {
-            if (mKeyboardShiftState.isShiftLocked()) {
+            if (mAlphabetShiftState.isShiftLocked()) {
                 // Shift key is pressed while caps lock state, we will treat this state as shifted
                 // caps lock state and mark as if shift key pressed while normal state.
                 setShifted(SwitchActions.MANUAL_SHIFT);
                 mShiftKeyState.onPress();
-            } else if (mKeyboardShiftState.isAutomaticTemporaryUpperCase()) {
+            } else if (mAlphabetShiftState.isAutomaticTemporaryUpperCase()) {
                 // Shift key is pressed while automatic temporary upper case, we have to move to
                 // manual temporary upper case.
                 setShifted(SwitchActions.MANUAL_SHIFT);
                 mShiftKeyState.onPress();
-            } else if (mKeyboardShiftState.isShiftedOrShiftLocked()) {
+            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()) {
                 // In manual upper case state, we just record shift key has been pressing while
                 // shifted state.
                 mShiftKeyState.onPressOnShifted();
@@ -337,30 +345,34 @@
 
     private void onReleaseShift(boolean withSliding) {
         if (mIsAlphabetMode) {
-            final boolean isShiftLocked = mKeyboardShiftState.isShiftLocked();
+            final boolean isShiftLocked = mAlphabetShiftState.isShiftLocked();
             if (mShiftKeyState.isMomentary()) {
                 // After chording input while normal state.
-                setShifted(SwitchActions.UNSHIFT);
-            } else if (isShiftLocked && !mKeyboardShiftState.isShiftLockShifted()
+                if (mAlphabetShiftState.isShiftLockShifted()) {
+                    setShiftLocked(true);
+                } else {
+                    setShifted(SwitchActions.UNSHIFT);
+                }
+            } else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
                     && (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
                     && !withSliding) {
                 // Shift has been long pressed, ignore this release.
             } else if (isShiftLocked && !mShiftKeyState.isIgnoring() && !withSliding) {
                 // Shift has been pressed without chording while caps lock state.
                 setShiftLocked(false);
-            } else if (mKeyboardShiftState.isShiftedOrShiftLocked()
+            } else if (mAlphabetShiftState.isShiftedOrShiftLocked()
                     && mShiftKeyState.isPressingOnShifted() && !withSliding) {
                 // Shift has been pressed without chording while shifted state.
                 setShifted(SwitchActions.UNSHIFT);
-            } else if (mKeyboardShiftState.isManualTemporaryUpperCaseFromAuto()
+            } else if (mAlphabetShiftState.isManualTemporaryUpperCaseFromAuto()
                     && mShiftKeyState.isPressing() && !withSliding) {
                 // Shift has been pressed without chording while manual temporary upper case
                 // transited from automatic temporary upper case.
                 setShifted(SwitchActions.UNSHIFT);
             }
         } else {
-            // In symbol mode, snap back to the previous keyboard mode if the user chords the shift
-            // key and another key, then releases the shift key.
+            // In symbol mode, switch back to the previous keyboard mode if the user chords the
+            // shift key and another key, then releases the shift key.
             if (mSwitchState == SWITCH_STATE_CHORDING_SYMBOL) {
                 toggleShiftInSymbols();
             }
@@ -372,7 +384,7 @@
         if (DEBUG_EVENT) {
             Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
         }
-        // Snap back to the previous keyboard mode if the user cancels sliding input.
+        // Switch back to the previous keyboard mode if the user cancels sliding input.
         if (isSinglePointer) {
             if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
                 toggleAlphabetAndSymbols();
@@ -405,7 +417,7 @@
         }
 
         if (mIsAlphabetMode && code == Keyboard.CODE_CAPSLOCK) {
-            if (mKeyboardShiftState.isShiftLocked()) {
+            if (mAlphabetShiftState.isShiftLocked()) {
                 setShiftLocked(false);
                 // Shift key is long pressed or double tapped while caps lock state, we will
                 // toggle back to normal state. And mark as if shift key is released.
@@ -431,13 +443,13 @@
                     mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
                 }
             } else if (isSinglePointer) {
-                // Snap back to the previous keyboard mode if the user pressed the mode change key
+                // Switch back to the previous keyboard mode if the user pressed the mode change key
                 // and slid to other key, then released the finger.
-                // If the user cancels the sliding input, snapping back to the previous keyboard
+                // If the user cancels the sliding input, switching back to the previous keyboard
                 // mode is handled by {@link #onCancelInput}.
                 toggleAlphabetAndSymbols();
             } else {
-                // Chording input is being started. The keyboard mode will be snapped back to the
+                // Chording input is being started. The keyboard mode will be switched back to the
                 // previous mode in {@link onReleaseSymbol} when the mode change key is released.
                 mSwitchState = SWITCH_STATE_CHORDING_ALPHA;
             }
@@ -447,12 +459,12 @@
                 // Detected only the shift key has been pressed on symbol layout, and then released.
                 mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
             } else if (isSinglePointer) {
-                // Snap back to the previous keyboard mode if the user pressed the shift key on
+                // Switch back to the previous keyboard mode if the user pressed the shift key on
                 // symbol mode and slid to other key, then released the finger.
                 toggleShiftInSymbols();
                 mSwitchState = SWITCH_STATE_SYMBOL;
             } else {
-                // Chording input is being started. The keyboard mode will be snapped back to the
+                // Chording input is being started. The keyboard mode will be switched back to the
                 // previous mode in {@link onReleaseShift} when the shift key is released.
                 mSwitchState = SWITCH_STATE_CHORDING_SYMBOL;
             }
@@ -462,14 +474,14 @@
                     || code == Keyboard.CODE_OUTPUT_TEXT)) {
                 mSwitchState = SWITCH_STATE_SYMBOL;
             }
-            // Snap back to alpha keyboard mode immediately if user types a quote character.
+            // Switch back to alpha keyboard mode immediately if user types a quote character.
             if (isLayoutSwitchBackCharacter(code)) {
                 setAlphabetKeyboard();
             }
             break;
         case SWITCH_STATE_SYMBOL:
         case SWITCH_STATE_CHORDING_SYMBOL:
-            // Snap back to alpha keyboard mode if user types one or more non-space/enter
+            // Switch back to alpha keyboard mode if user types one or more non-space/enter
             // characters followed by a space/enter or a quote character.
             if (isSpaceCharacter(code) || isLayoutSwitchBackCharacter(code)) {
                 setAlphabetKeyboard();
@@ -507,7 +519,8 @@
 
     @Override
     public String toString() {
-        return "[keyboard=" + mKeyboardShiftState
+        return "[keyboard=" + (mIsAlphabetMode ? mAlphabetShiftState.toString()
+                        : (mIsSymbolShifted ? "SYMBOLS_SHIFTED" : "SYMBOLS"))
                 + " shift=" + mShiftKeyState
                 + " symbol=" + mSymbolKeyState
                 + " switch=" + switchStateToString(mSwitchState) + "]";
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
index 93be31e..1677773 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
@@ -18,10 +18,10 @@
 
 import android.content.res.Resources;
 import android.text.TextUtils;
-import android.util.Log;
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Utils;
 
 import java.util.ArrayList;
 
@@ -31,20 +31,16 @@
  * Each "more key" specification is one of the following:
  * - A single letter (Letter)
  * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (@icon/icon_number|@integer/key_code)
+ * - Icon followed by keyOutputText or code (@icon/icon_name|@integer/key_code)
  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\'
  * character.
  * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
  * See {@link KeyboardIconsSet} about icon_number.
  */
 public class MoreKeySpecParser {
-    private static final String TAG = MoreKeySpecParser.class.getSimpleName();
-
-    private static final char ESCAPE = '\\';
-    private static final String LABEL_END = "|";
-    private static final String PREFIX_AT = "@";
-    private static final String PREFIX_ICON = PREFIX_AT + "icon/";
-    private static final String PREFIX_CODE = PREFIX_AT + "integer/";
+    private static final char LABEL_END = '|';
+    private static final String PREFIX_ICON = Utils.PREFIX_AT + "icon" + Utils.SUFFIX_SLASH;
+    private static final String PREFIX_CODE = Utils.PREFIX_AT + "integer" + Utils.SUFFIX_SLASH;
 
     private MoreKeySpecParser() {
         // Intentional empty constructor for utility class.
@@ -53,8 +49,9 @@
     private static boolean hasIcon(String moreKeySpec) {
         if (moreKeySpec.startsWith(PREFIX_ICON)) {
             final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (end > 0)
+            if (end > 0) {
                 return true;
+            }
             throw new MoreKeySpecParserError("outputText or code not specified: " + moreKeySpec);
         }
         return false;
@@ -70,13 +67,14 @@
     }
 
     private static String parseEscape(String text) {
-        if (text.indexOf(ESCAPE) < 0)
+        if (text.indexOf(Utils.ESCAPE_CHAR) < 0) {
             return text;
+        }
         final int length = text.length();
         final StringBuilder sb = new StringBuilder();
         for (int pos = 0; pos < length; pos++) {
             final char c = text.charAt(pos);
-            if (c == ESCAPE && pos + 1 < length) {
+            if (c == Utils.ESCAPE_CHAR && pos + 1 < length) {
                 sb.append(text.charAt(++pos));
             } else {
                 sb.append(c);
@@ -86,18 +84,19 @@
     }
 
     private static int indexOfLabelEnd(String moreKeySpec, int start) {
-        if (moreKeySpec.indexOf(ESCAPE, start) < 0) {
+        if (moreKeySpec.indexOf(Utils.ESCAPE_CHAR, start) < 0) {
             final int end = moreKeySpec.indexOf(LABEL_END, start);
-            if (end == 0)
+            if (end == 0) {
                 throw new MoreKeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
+            }
             return end;
         }
         final int length = moreKeySpec.length();
         for (int pos = start; pos < length; pos++) {
             final char c = moreKeySpec.charAt(pos);
-            if (c == ESCAPE && pos + 1 < length) {
+            if (c == Utils.ESCAPE_CHAR && pos + 1 < length) {
                 pos++;
-            } else if (moreKeySpec.startsWith(LABEL_END, pos)) {
+            } else if (c == LABEL_END) {
                 return pos;
             }
         }
@@ -105,79 +104,75 @@
     }
 
     public static String getLabel(String moreKeySpec) {
-        if (hasIcon(moreKeySpec))
+        if (hasIcon(moreKeySpec)) {
             return null;
+        }
         final int end = indexOfLabelEnd(moreKeySpec, 0);
         final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
                 : parseEscape(moreKeySpec);
-        if (TextUtils.isEmpty(label))
+        if (TextUtils.isEmpty(label)) {
             throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
+        }
         return label;
     }
 
     public static String getOutputText(String moreKeySpec) {
-        if (hasCode(moreKeySpec))
+        if (hasCode(moreKeySpec)) {
             return null;
+        }
         final int end = indexOfLabelEnd(moreKeySpec, 0);
         if (end > 0) {
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0)
+            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
                     throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": "
                             + moreKeySpec);
-            final String outputText = parseEscape(moreKeySpec.substring(end + LABEL_END.length()));
-            if (!TextUtils.isEmpty(outputText))
+            }
+            final String outputText = parseEscape(
+                    moreKeySpec.substring(end + /* LABEL_END */1));
+            if (!TextUtils.isEmpty(outputText)) {
                 return outputText;
+            }
             throw new MoreKeySpecParserError("Empty outputText: " + moreKeySpec);
         }
         final String label = getLabel(moreKeySpec);
-        if (label == null)
+        if (label == null) {
             throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
+        }
         // Code is automatically generated for one letter label. See {@link getCode()}.
-        if (label.length() == 1)
-            return null;
-        return label;
+        return (label.length() == 1) ? null : label;
     }
 
     public static int getCode(Resources res, String moreKeySpec) {
         if (hasCode(moreKeySpec)) {
             final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0)
+            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
                 throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
-            final int resId = getResourceId(res,
-                    moreKeySpec.substring(end + LABEL_END.length() + PREFIX_AT.length()));
+            }
+            final int resId = Utils.getResourceId(res,
+                    moreKeySpec.substring(end + /* LABEL_END */1 + /* PREFIX_AT */1),
+                    R.string.english_ime_name);
             final int code = res.getInteger(resId);
             return code;
         }
-        if (indexOfLabelEnd(moreKeySpec, 0) > 0)
-            return Keyboard.CODE_UNSPECIFIED;
+        if (indexOfLabelEnd(moreKeySpec, 0) > 0) {
+            return Keyboard.CODE_OUTPUT_TEXT;
+        }
         final String label = getLabel(moreKeySpec);
         // Code is automatically generated for one letter label.
-        if (label != null && label.length() == 1)
+        if (label != null && label.length() == 1) {
             return label.charAt(0);
-        return Keyboard.CODE_UNSPECIFIED;
+        }
+        return Keyboard.CODE_OUTPUT_TEXT;
     }
 
-    public static int getIconId(String moreKeySpec) {
+    public static int getIconAttrId(String moreKeySpec) {
         if (hasIcon(moreKeySpec)) {
-            int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length() + 1);
-            final String iconId = moreKeySpec.substring(PREFIX_ICON.length(), end);
-            try {
-                return Integer.valueOf(iconId);
-            } catch (NumberFormatException e) {
-                Log.w(TAG, "illegal icon id specified: " + iconId);
-                return KeyboardIconsSet.ICON_UNDEFINED;
-            }
+            final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
+            final String name = moreKeySpec.substring(PREFIX_ICON.length(), end);
+            return KeyboardIconsSet.getIconAttrId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static int getResourceId(Resources res, String name) {
-        String packageName = res.getResourcePackageName(R.string.english_ime_name);
-        int resId = res.getIdentifier(name, null, packageName);
-        if (resId == 0)
-            throw new MoreKeySpecParserError("Unknown resource: " + name);
-        return resId;
-    }
-
     @SuppressWarnings("serial")
     public static class MoreKeySpecParserError extends RuntimeException {
         public MoreKeySpecParserError(String message) {
@@ -196,21 +191,19 @@
         }
     };
 
-    public static CharSequence[] filterOut(Resources res, CharSequence[] moreKeys,
-            CodeFilter filter) {
+    public static String[] filterOut(Resources res, String[] moreKeys, CodeFilter filter) {
         if (moreKeys == null || moreKeys.length < 1) {
             return null;
         }
-        if (moreKeys.length == 1
-                && filter.shouldFilterOut(getCode(res, moreKeys[0].toString()))) {
+        if (moreKeys.length == 1 && filter.shouldFilterOut(getCode(res, moreKeys[0]))) {
             return null;
         }
-        ArrayList<CharSequence> filtered = null;
+        ArrayList<String> filtered = null;
         for (int i = 0; i < moreKeys.length; i++) {
-            final CharSequence moreKeySpec = moreKeys[i];
-            if (filter.shouldFilterOut(getCode(res, moreKeySpec.toString()))) {
+            final String moreKeySpec = moreKeys[i];
+            if (filter.shouldFilterOut(getCode(res, moreKeySpec))) {
                 if (filtered == null) {
-                    filtered = new ArrayList<CharSequence>();
+                    filtered = new ArrayList<String>();
                     for (int j = 0; j < i; j++) {
                         filtered.add(moreKeys[j]);
                     }
@@ -225,6 +218,6 @@
         if (filtered.size() == 0) {
             return null;
         }
-        return filtered.toArray(new CharSequence[filtered.size()]);
+        return filtered.toArray(new String[filtered.size()]);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d11aaeb..94c47bd 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1881,6 +1881,10 @@
             }
             return;
         }
+        // We need to log before we commit, because the word composer will store away the user
+        // typed word.
+        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
+                suggestion.toString(), index, suggestions.mWords);
         mExpectingUpdateSelection = true;
         commitChosenWord(suggestion, WordComposer.COMMIT_TYPE_MANUAL_PICK);
         // Add the word to the auto dictionary if it's not a known word
@@ -1890,10 +1894,6 @@
         } else {
             addToOnlyBigramDictionary(suggestion, 1);
         }
-        // TODO: the following is fishy, because it seems there may be cases where we are not
-        // composing a word at all. Maybe throw an exception if !mWordComposer.isComposingWord() ?
-        LatinImeLogger.logOnManualSuggestion(mWordComposer.getTypedWord().toString(),
-                suggestion.toString(), index, suggestions.mWords);
         // Follow it with a space
         if (mInputAttributes.mInsertSpaceOnPickSuggestionManually) {
             sendMagicSpace();
@@ -1924,8 +1924,7 @@
             // Updating the predictions right away may be slow and feel unresponsive on slower
             // terminals. On the other hand if we just postUpdateBigramPredictions() it will
             // take a noticeable delay to update them which may feel uneasy.
-        }
-        if (showingAddToDictionaryHint) {
+        } else {
             if (mIsUserDictionaryAvailable) {
                 mSuggestionsView.showAddToDictionaryHint(
                         suggestion, mSettingsValues.mHintToSaveText);
@@ -1942,9 +1941,6 @@
      * Commits the chosen word to the text field and saves it for later retrieval.
      */
     private void commitChosenWord(final CharSequence bestWord, final int commitType) {
-        final KeyboardSwitcher switcher = mKeyboardSwitcher;
-        if (!switcher.isKeyboardAvailable())
-            return;
         final InputConnection ic = getCurrentInputConnection();
         if (ic != null) {
             mVoiceProxy.rememberReplacedWord(bestWord, mSettingsValues.mWordSeparators);
@@ -2133,12 +2129,12 @@
             final String wordBeforeCursor =
                     ic.getTextBeforeCursor(cancelLength + 1, 0).subSequence(0, cancelLength)
                     .toString();
-            if (!autoCorrectedTo.equals(wordBeforeCursor)) {
+            if (!TextUtils.equals(autoCorrectedTo, wordBeforeCursor)) {
                 throw new RuntimeException("cancelAutoCorrect check failed: we thought we were "
                         + "reverting \"" + autoCorrectedTo
                         + "\", but before the cursor we found \"" + wordBeforeCursor + "\"");
             }
-            if (originallyTypedWord.equals(wordBeforeCursor)) {
+            if (TextUtils.equals(originallyTypedWord, wordBeforeCursor)) {
                 throw new RuntimeException("cancelAutoCorrect check failed: we wanted to cancel "
                         + "auto correction and revert to \"" + originallyTypedWord
                         + "\" but we found this very string before the cursor");
@@ -2158,12 +2154,22 @@
 
     // "ic" must not be null
     private void restartSuggestionsOnManuallyPickedTypedWord(final InputConnection ic) {
+        // Note: this relies on the last word still being held in the WordComposer, in
+        // the field for suggestion resuming.
+        // Note: in the interest of code simplicity, we may want to just call
+        // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving
+        // the old WordComposer allows to reuse the actual typed coordinates.
+        mWordComposer.resumeSuggestionOnKeptWord();
+        // We resume suggestion, and then we want to set the composing text to the content
+        // of the word composer again. But since we just manually picked a word, there is
+        // no composing text at the moment, so we have to delete the word before we set a
+        // new composing text.
         final int restartLength = mWordComposer.size();
         if (DEBUG) {
             final String wordBeforeCursor =
                 ic.getTextBeforeCursor(restartLength + 1, 0).subSequence(0, restartLength)
                 .toString();
-            if (!mWordComposer.getTypedWord().equals(wordBeforeCursor)) {
+            if (!TextUtils.equals(mWordComposer.getTypedWord(), wordBeforeCursor)) {
                 throw new RuntimeException("restartSuggestionsOnManuallyPickedTypedWord "
                         + "check failed: we thought we were reverting \""
                         + mWordComposer.getTypedWord()
@@ -2171,13 +2177,8 @@
                         + wordBeforeCursor + "\"");
             }
         }
+        // Warning: this +1 takes into account the extra space added by the manual pick process.
         ic.deleteSurroundingText(restartLength + 1, 0);
-
-        // Note: this relies on the last word still being held in the WordComposer
-        // Note: in the interest of code simplicity, we may want to just call
-        // restartSuggestionsOnWordBeforeCursorIfAtEndOfWord instead, but retrieving
-        // the old WordComposer allows to reuse the actual typed coordinates.
-        mWordComposer.resumeSuggestionOnKeptWord();
         ic.setComposingText(mWordComposer.getTypedWord(), 1);
         mHandler.cancelUpdateBigramPredictions();
         mHandler.postUpdateSuggestions();
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index 8e0cfa1..bc8a130 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -16,14 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
-import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
-import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
-import com.android.inputmethod.compat.InputTypeCompatUtils;
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.keyboard.KeyboardId;
-import com.android.inputmethod.latin.define.JniLibName;
-
 import android.content.Context;
 import android.content.Intent;
 import android.content.pm.PackageManager;
@@ -41,6 +33,14 @@
 import android.util.Log;
 import android.view.inputmethod.EditorInfo;
 
+import com.android.inputmethod.compat.InputMethodInfoCompatWrapper;
+import com.android.inputmethod.compat.InputMethodManagerCompatWrapper;
+import com.android.inputmethod.compat.InputMethodSubtypeCompatWrapper;
+import com.android.inputmethod.compat.InputTypeCompatUtils;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.latin.define.JniLibName;
+
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileInputStream;
@@ -62,6 +62,12 @@
     private static boolean DBG = LatinImeLogger.sDBG;
     private static boolean DBG_EDIT_DISTANCE = false;
 
+    // Constants for resource name parsing.
+    public static final char ESCAPE_CHAR = '\\';
+    public static final char PREFIX_AT = '@';
+    public static final char SUFFIX_SLASH = '/';
+    private static final String PREFIX_STRING = PREFIX_AT + "string";
+
     private Utils() {
         // Intentional empty constructor for utility class.
     }
@@ -793,4 +799,62 @@
             LatinImeLogger.logOnAutoCorrectionCancelled();
         }
     }
+
+    public static int getResourceId(Resources res, String name, int packageNameResId) {
+        String packageName = res.getResourcePackageName(packageNameResId);
+        int resId = res.getIdentifier(name, null, packageName);
+        if (resId == 0) {
+            throw new RuntimeException("Unknown resource: " + name);
+        }
+        return resId;
+    }
+
+    public static String resolveStringResource(String text, Resources res, int packageNameResId) {
+        final int size = text.length();
+        if (size < PREFIX_STRING.length()) {
+            return text;
+        }
+
+        StringBuilder sb = null;
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == PREFIX_AT && text.startsWith(PREFIX_STRING, pos)) {
+                if (sb == null) {
+                    sb = new StringBuilder(text.substring(0, pos));
+                }
+                final int end = Utils.searchResourceNameEnd(text, pos + PREFIX_STRING.length());
+                final String resName = text.substring(pos + 1, end);
+                final int resId = getResourceId(res, resName, packageNameResId);
+                sb.append(res.getString(resId));
+                pos = end - 1;
+            } else if (c == ESCAPE_CHAR) {
+                pos++;
+                if (sb != null) {
+                    sb.append(c);
+                    if (pos < size) {
+                        sb.append(text.charAt(pos));
+                    }
+                }
+            } else if (sb != null) {
+                sb.append(c);
+            }
+        }
+        return (sb == null) ? text : sb.toString();
+    }
+
+    private static int searchResourceNameEnd(String text, int start) {
+        final int size = text.length();
+        if (start >= size || text.charAt(start) != SUFFIX_SLASH) {
+            throw new RuntimeException("Resource name not specified");
+        }
+        for (int pos = start + 1; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // String resource name should be consisted of [a-z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
 }
diff --git a/native/src/correction.cpp b/native/src/correction.cpp
index 6a129d4..63dd283 100644
--- a/native/src/correction.cpp
+++ b/native/src/correction.cpp
@@ -39,15 +39,15 @@
     }
 }
 
-inline static void dumpEditDistance10ForDebug(int *editDistanceTable, const int inputLength,
-        const int outputLength) {
+inline static void dumpEditDistance10ForDebug(int *editDistanceTable,
+        const int editDistanceTableWidth, const int outputLength) {
     if (DEBUG_DICT) {
         AKLOGI("EditDistanceTable");
         for (int i = 0; i <= 10; ++i) {
             int c[11];
             for (int j = 0; j <= 10; ++j) {
-                if (j < inputLength + 1 && i < outputLength + 1) {
-                    c[j] = (editDistanceTable + i * (inputLength + 1))[j];
+                if (j < editDistanceTableWidth + 1 && i < outputLength + 1) {
+                    c[j] = (editDistanceTable + i * (editDistanceTableWidth + 1))[j];
                 } else {
                     c[j] = -1;
                 }
@@ -81,12 +81,12 @@
     }
 }
 
-inline static int getCurrentEditDistance(
-        int *editDistanceTable, const int inputLength, const int outputLength) {
+inline static int getCurrentEditDistance(int *editDistanceTable, const int editDistanceTableWidth,
+        const int outputLength, const int inputLength) {
     if (DEBUG_EDIT_DISTANCE) {
         AKLOGI("getCurrentEditDistance %d, %d", inputLength, outputLength);
     }
-    return editDistanceTable[(inputLength + 1) * (outputLength + 1) - 1];
+    return editDistanceTable[(editDistanceTableWidth + 1) * (outputLength) + inputLength];
 }
 
 //////////////////////
@@ -165,6 +165,16 @@
 }
 
 int Correction::getFinalFreq(const int freq, unsigned short **word, int *wordLength) {
+    return getFinalFreqInternal(freq, word, wordLength, mInputLength);
+}
+
+int Correction::getFinalFreqForSubQueue(const int freq, unsigned short **word, int *wordLength,
+        const int inputLength) {
+    return getFinalFreqInternal(freq, word, wordLength, inputLength);
+}
+
+int Correction::getFinalFreqInternal(const int freq, unsigned short **word, int *wordLength,
+        const int inputLength) {
     const int outputIndex = mTerminalOutputIndex;
     const int inputIndex = mTerminalInputIndex;
     *wordLength = outputIndex + 1;
@@ -173,8 +183,9 @@
     }
 
     *word = mWord;
-    return Correction::RankingAlgorithm::calculateFinalFreq(
-            inputIndex, outputIndex, freq, mEditDistanceTable, this);
+    int finalFreq = Correction::RankingAlgorithm::calculateFinalFreq(
+            inputIndex, outputIndex, freq, mEditDistanceTable, this, inputLength);
+    return finalFreq;
 }
 
 bool Correction::initProcessState(const int outputIndex) {
@@ -613,9 +624,9 @@
 
 /* static */
 int Correction::RankingAlgorithm::calculateFinalFreq(const int inputIndex, const int outputIndex,
-        const int freq, int* editDistanceTable, const Correction* correction) {
+        const int freq, int* editDistanceTable, const Correction* correction,
+        const int inputLength) {
     const int excessivePos = correction->getExcessivePos();
-    const int inputLength = correction->mInputLength;
     const int typedLetterMultiplier = correction->TYPED_LETTER_MULTIPLIER;
     const int fullWordMultiplier = correction->FULL_WORD_MULTIPLIER;
     const ProximityInfo *proximityInfo = correction->mProximityInfo;
@@ -640,13 +651,13 @@
     const unsigned short* word = correction->mWord;
     const bool skipped = skippedCount > 0;
 
-    const int quoteDiffCount = max(0, getQuoteCount(word, outputIndex + 1)
+    const int quoteDiffCount = max(0, getQuoteCount(word, outputLength)
             - getQuoteCount(proximityInfo->getPrimaryInputWord(), inputLength));
 
     // TODO: Calculate edit distance for transposed and excessive
     int ed = 0;
     if (DEBUG_DICT_FULL) {
-        dumpEditDistance10ForDebug(editDistanceTable, inputLength, outputIndex + 1);
+        dumpEditDistance10ForDebug(editDistanceTable, correction->mInputLength, outputLength);
     }
     int adjustedProximityMatchedCount = proximityMatchedCount;
 
@@ -654,22 +665,22 @@
 
     // TODO: Optimize this.
     if (transposedCount > 0 || proximityMatchedCount > 0 || skipped || excessiveCount > 0) {
-        ed = getCurrentEditDistance(editDistanceTable, inputLength, outputIndex + 1)
-                - transposedCount;
+        ed = getCurrentEditDistance(editDistanceTable, correction->mInputLength, outputLength,
+                inputLength) - transposedCount;
 
         const int matchWeight = powerIntCapped(typedLetterMultiplier,
-                max(inputLength, outputIndex + 1) - ed);
+                max(inputLength, outputLength) - ed);
         multiplyIntCapped(matchWeight, &finalFreq);
 
         // TODO: Demote further if there are two or more excessive chars with longer user input?
-        if (inputLength > outputIndex + 1) {
+        if (inputLength > outputLength) {
             multiplyRate(INPUT_EXCEEDS_OUTPUT_DEMOTION_RATE, &finalFreq);
         }
 
         ed = max(0, ed - quoteDiffCount);
 
         if (transposedCount < 1) {
-            if (ed == 1 && (inputLength == outputIndex || inputLength == outputIndex + 2)) {
+            if (ed == 1 && (inputLength == outputLength - 1 || inputLength == outputLength + 1)) {
                 // Promote a word with just one skipped or excessive char
                 if (sameLength) {
                     multiplyRate(WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE, &finalFreq);
@@ -681,7 +692,7 @@
                 sameLength = true;
             }
         }
-        adjustedProximityMatchedCount = min(max(0, ed - (outputIndex + 1 - inputLength)),
+        adjustedProximityMatchedCount = min(max(0, ed - (outputLength - inputLength)),
                 proximityMatchedCount);
     } else {
         const int matchWeight = powerIntCapped(typedLetterMultiplier, matchCount);
@@ -783,7 +794,8 @@
     // Promotion for an exactly matched word
     if (ed == 0) {
         // Full exact match
-        if (sameLength && transposedCount == 0 && !skipped && excessiveCount == 0) {
+        if (sameLength && transposedCount == 0 && !skipped && excessiveCount == 0
+                && quoteDiffCount == 0) {
             finalFreq = capped255MultForFullMatchAccentsOrCapitalizationDifference(finalFreq);
         }
     }
@@ -828,14 +840,14 @@
     }
 
     if (DEBUG_DICT_FULL) {
-        AKLOGI("calc: %d, %d", outputIndex, sameLength);
+        AKLOGI("calc: %d, %d", outputLength, sameLength);
     }
 
     if (DEBUG_CORRECTION_FREQ) {
-        DUMP_WORD(correction->mWord, outputIndex + 1);
-        AKLOGI("FinalFreq: [P%d, S%d, T%d, E%d] %d, %d, %d, %d, %d", proximityMatchedCount,
-                skippedCount, transposedCount, excessiveCount, lastCharExceeded, sameLength,
-                quoteDiffCount, ed, finalFreq);
+        DUMP_WORD(correction->mWord, outputLength);
+        AKLOGI("FinalFreq: [P%d, S%d, T%d, E%d] %d, %d, %d, %d, %d, %d", proximityMatchedCount,
+                skippedCount, transposedCount, excessiveCount, outputLength, lastCharExceeded,
+                sameLength, quoteDiffCount, ed, finalFreq);
     }
 
     return finalFreq;
@@ -881,11 +893,12 @@
     if (firstWordLength == 0 || secondWordLength == 0) {
         return 0;
     }
-    const int firstDemotionRate = 100 - 100 / (firstWordLength + 1);
+    const int firstDemotionRate = 100 - TWO_WORDS_CORRECTION_DEMOTION_BASE / (firstWordLength + 1);
     int tempFirstFreq = firstFreq;
     multiplyRate(firstDemotionRate, &tempFirstFreq);
 
-    const int secondDemotionRate = 100 - 100 / (secondWordLength + 1);
+    const int secondDemotionRate = 100
+            - TWO_WORDS_CORRECTION_DEMOTION_BASE / (secondWordLength + 1);
     int tempSecondFreq = secondFreq;
     multiplyRate(secondDemotionRate, &tempSecondFreq);
 
diff --git a/native/src/correction.h b/native/src/correction.h
index 22a424f..0715551 100644
--- a/native/src/correction.h
+++ b/native/src/correction.h
@@ -75,6 +75,8 @@
     int getFreqForSplitTwoWords(
             const int firstFreq, const int secondFreq, const unsigned short *word);
     int getFinalFreq(const int freq, unsigned short **word, int* wordLength);
+    int getFinalFreqForSubQueue(const int freq, unsigned short **word, int* wordLength,
+            const int inputLength);
 
     CorrectionType processCharAndCalcState(const int32_t c, const bool isTerminal);
 
@@ -97,7 +99,8 @@
     class RankingAlgorithm {
      public:
         static int calculateFinalFreq(const int inputIndex, const int depth,
-                const int freq, int *editDistanceTable, const Correction* correction);
+                const int freq, int *editDistanceTable, const Correction* correction,
+                const int inputLength);
         static int calcFreqForSplitTwoWords(const int firstFreq, const int secondFreq,
                 const Correction* correction, const unsigned short *word);
         static int calcFreqForSplitTwoWordsOld(const int firstFreq, const int secondFreq,
@@ -122,6 +125,8 @@
             const int32_t c, const bool isTerminal, const bool inputIndexIncremented);
     inline CorrectionType processUnrelatedCorrectionType();
     inline void addCharToCurrentWord(const int32_t c);
+    inline int getFinalFreqInternal(const int freq, unsigned short **word, int* wordLength,
+            const int inputLength);
 
     const int TYPED_LETTER_MULTIPLIER;
     const int FULL_WORD_MULTIPLIER;
diff --git a/native/src/defines.h b/native/src/defines.h
index d739043..119a7d7 100644
--- a/native/src/defines.h
+++ b/native/src/defines.h
@@ -187,7 +187,7 @@
 // The following "rate"s are used as a multiplier before dividing by 100, so they are in percent.
 #define WORDS_WITH_MISSING_CHARACTER_DEMOTION_RATE 80
 #define WORDS_WITH_MISSING_CHARACTER_DEMOTION_START_POS_10X 12
-#define WORDS_WITH_MISSING_SPACE_CHARACTER_DEMOTION_RATE 67
+#define WORDS_WITH_MISSING_SPACE_CHARACTER_DEMOTION_RATE 58
 #define WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE 75
 #define WORDS_WITH_EXCESSIVE_CHARACTER_OUT_OF_PROXIMITY_DEMOTION_RATE 75
 #define WORDS_WITH_TRANSPOSED_CHARACTERS_DEMOTION_RATE 70
@@ -199,6 +199,8 @@
 #define INPUT_EXCEEDS_OUTPUT_DEMOTION_RATE 70
 #define FIRST_CHAR_DIFFERENT_DEMOTION_RATE 96
 #define TWO_WORDS_CAPITALIZED_DEMOTION_RATE 50
+#define TWO_WORDS_CORRECTION_DEMOTION_BASE 80
+#define TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER 1
 #define ZERO_DISTANCE_PROMOTION_RATE 110
 #define NEUTRAL_SCORE_SQUARED_RADIUS 8.0f
 #define HALF_SCORE_SQUARED_RADIUS 32.0f
@@ -212,8 +214,10 @@
 // Holds up to 1 candidate for each word
 #define SUB_QUEUE_MAX_WORDS 1
 #define SUB_QUEUE_MAX_COUNT 10
+#define SUB_QUEUE_MIN_WORD_LENGTH 4
 
-#define TWO_WORDS_CORRECTION_THRESHOLD 0.22f
+#define TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD 0.39
+#define START_TWO_WORDS_CORRECTION_THRESHOLD 0.22
 
 #define MAX_DEPTH_MULTIPLIER 3
 
diff --git a/native/src/unigram_dictionary.cpp b/native/src/unigram_dictionary.cpp
index 8be95bc..2c5b940 100644
--- a/native/src/unigram_dictionary.cpp
+++ b/native/src/unigram_dictionary.cpp
@@ -254,7 +254,7 @@
                         proximityInfo->getPrimaryInputWord(), i, word, wordLength, score);
                 ns += 0;
                 AKLOGI("--- TOP SUB WORDS for %d --- %d %f [%d]", i, score, ns,
-                        (ns > TWO_WORDS_CORRECTION_THRESHOLD));
+                        (ns > TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD));
                 DUMP_WORD(proximityInfo->getPrimaryInputWord(), i);
                 DUMP_WORD(word, wordLength);
             }
@@ -343,43 +343,45 @@
         WordsPriorityQueuePool *queuePool, const bool addToMasterQueue) {
     const int inputIndex = correction->getInputIndex();
     const bool addToSubQueue = inputIndex < SUB_QUEUE_MAX_COUNT;
-    if (!addToMasterQueue && !addToSubQueue) {
-        return;
-    }
-    WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
-    WordsPriorityQueue *subQueue = queuePool->getSubQueue1(inputIndex);
+
     int wordLength;
     unsigned short* wordPointer;
-    const int finalFreq = correction->getFinalFreq(freq, &wordPointer, &wordLength);
-    if (finalFreq != NOT_A_FREQUENCY) {
-        if (!terminalAttributes.isShortcutOnly()) {
-            if (addToMasterQueue) {
+
+    if (addToMasterQueue) {
+        WordsPriorityQueue *masterQueue = queuePool->getMasterQueue();
+        const int finalFreq = correction->getFinalFreq(freq, &wordPointer, &wordLength);
+        if (finalFreq != NOT_A_FREQUENCY) {
+            if (!terminalAttributes.isShortcutOnly()) {
                 addWord(wordPointer, wordLength, finalFreq, masterQueue);
             }
-            // TODO: Check the validity of "inputIndex == wordLength"
-            //if (addToSubQueue && inputIndex == wordLength) {
-            if (addToSubQueue) {
-                addWord(wordPointer, wordLength, finalFreq, subQueue);
+
+            // Please note that the shortcut candidates will be added to the master queue only.
+            TerminalAttributes::ShortcutIterator iterator =
+                    terminalAttributes.getShortcutIterator();
+            while (iterator.hasNextShortcutTarget()) {
+                // TODO: addWord only supports weak ordering, meaning we have no means
+                // to control the order of the shortcuts relative to one another or to the word.
+                // We need to either modulate the frequency of each shortcut according
+                // to its own shortcut frequency or to make the queue
+                // so that the insert order is protected inside the queue for words
+                // with the same score.
+                uint16_t shortcutTarget[MAX_WORD_LENGTH_INTERNAL];
+                const int shortcutTargetStringLength = iterator.getNextShortcutTarget(
+                        MAX_WORD_LENGTH_INTERNAL, shortcutTarget);
+                addWord(shortcutTarget, shortcutTargetStringLength, finalFreq, masterQueue);
             }
         }
-        // Please note that the shortcut candidates will be added to the master queue only.
-        if (!addToMasterQueue) {
-            return;
-        }
+    }
 
-        // From here, below is the code to add shortcut candidates.
-        TerminalAttributes::ShortcutIterator iterator = terminalAttributes.getShortcutIterator();
-        while (iterator.hasNextShortcutTarget()) {
-            // TODO: addWord only supports weak ordering, meaning we have no means to control the
-            // order of the shortcuts relative to one another or to the word. We need to either
-            // modulate the frequency of each shortcut according to its own shortcut frequency or
-            // to make the queue so that the insert order is protected inside the queue for words
-            // with the same score.
-            uint16_t shortcutTarget[MAX_WORD_LENGTH_INTERNAL];
-            const int shortcutTargetStringLength = iterator.getNextShortcutTarget(
-                    MAX_WORD_LENGTH_INTERNAL, shortcutTarget);
-            addWord(shortcutTarget, shortcutTargetStringLength, finalFreq, masterQueue);
-        }
+    // We only allow two words + other error correction for words with SUB_QUEUE_MIN_WORD_LENGTH
+    // or more length.
+    if (inputIndex >= SUB_QUEUE_MIN_WORD_LENGTH && addToSubQueue) {
+        // TODO: Check the validity of "inputIndex == wordLength"
+        //if (addToSubQueue && inputIndex == wordLength) {
+        WordsPriorityQueue *subQueue = queuePool->getSubQueue1(inputIndex);
+        const int finalFreq = correction->getFinalFreqForSubQueue(freq, &wordPointer, &wordLength,
+                inputIndex);
+        addWord(wordPointer, wordLength, finalFreq, subQueue);
     }
 }
 
@@ -397,20 +399,57 @@
     }
     const bool isSpaceProximity = spaceProximityPos >= 0;
     const int firstWordStartPos = 0;
+
+    const int firstTypedWordLength = isSpaceProximity ? spaceProximityPos : missingSpacePos;
+    int firstFreq = getMostFrequentWordLike(0, firstTypedWordLength, proximityInfo, mWord);
+    unsigned short* firstWord = 0;
+    int firstWordLength = 0;
+    if (firstFreq > 0) {
+        firstWordLength = firstTypedWordLength;
+        firstWord = mWord;
+    } else {
+        if (masterQueue->size() > 0) {
+            double nsForMaster = masterQueue->getHighestNormalizedScore(
+                    proximityInfo->getPrimaryInputWord(), inputLength, 0, 0, 0);
+            if (nsForMaster > START_TWO_WORDS_CORRECTION_THRESHOLD) {
+                // Do nothing if the highest suggestion exceeds the threshold.
+                return;
+            }
+        }
+        WordsPriorityQueue* firstWordQueue = queuePool->getSubQueue1(firstTypedWordLength);
+        if (firstWordQueue->size() < 1) {
+            return;
+        }
+        int score = 0;
+        const double ns = firstWordQueue->getHighestNormalizedScore(
+                proximityInfo->getPrimaryInputWord(), firstTypedWordLength, &firstWord, &score,
+                &firstWordLength);
+        // Two words correction won't be done if the score of the first word doesn't exceed the
+        // threshold.
+        if (ns < TWO_WORDS_CORRECTION_WITH_OTHER_ERROR_THRESHOLD) {
+            return;
+        }
+        firstFreq = score >> (firstWordLength
+                + TWO_WORDS_PLUS_OTHER_ERROR_CORRECTION_DEMOTION_DIVIDER);
+    }
+
+    if (firstFreq <= 0) {
+        return;
+    }
+
     const int secondWordStartPos = isSpaceProximity ? (spaceProximityPos + 1) : missingSpacePos;
-    const int firstWordLength = isSpaceProximity ? spaceProximityPos : missingSpacePos;
     const int secondWordLength = isSpaceProximity
             ? (inputLength - spaceProximityPos - 1)
             : (inputLength - missingSpacePos);
 
     if (inputLength >= MAX_WORD_LENGTH) return;
+
     if (0 >= firstWordLength || 0 >= secondWordLength || firstWordStartPos >= secondWordStartPos
             || firstWordStartPos < 0 || secondWordStartPos + secondWordLength > inputLength)
         return;
 
     const int newWordLength = firstWordLength + secondWordLength + 1;
 
-
     // Space proximity preparation
     //WordsPriorityQueue *subQueue = queuePool->getSubQueue1();
     //initSuggestions(proximityInfo, xcoordinates, ycoordinates, codes, firstWordLength, subQueue,
@@ -420,15 +459,12 @@
 
     // Allocating variable length array on stack
     unsigned short word[newWordLength];
-    const int firstFreq = getMostFrequentWordLike(
-            firstWordStartPos, firstWordLength, proximityInfo, mWord);
     if (DEBUG_DICT) {
         AKLOGI("First freq: %d", firstFreq);
     }
-    if (firstFreq <= 0) return;
 
     for (int i = 0; i < firstWordLength; ++i) {
-        word[i] = mWord[i];
+        word[i] = firstWord[i];
     }
 
     const int secondFreq = getMostFrequentWordLike(
diff --git a/native/src/words_priority_queue.h b/native/src/words_priority_queue.h
index 6262439..c85f2b9 100644
--- a/native/src/words_priority_queue.h
+++ b/native/src/words_priority_queue.h
@@ -47,6 +47,7 @@
         for (int i = 0; i < maxWordLength; ++i) {
             mSuggestedWords[i].mUsed = false;
         }
+        mHighestSuggestedWord = 0;
     }
 
     ~WordsPriorityQueue() {
@@ -79,6 +80,9 @@
             DUMP_WORD(word, wordLength);
         }
         mSuggestions.push(sw);
+        if (!mHighestSuggestedWord || mHighestSuggestedWord->mScore < sw->mScore) {
+            mHighestSuggestedWord = sw;
+        }
     }
 
     SuggestedWord* top() {
@@ -88,6 +92,7 @@
     }
 
     int outputSuggestions(int *frequencies, unsigned short *outputChars) {
+        mHighestSuggestedWord = 0;
         const unsigned int size = min(MAX_WORDS, mSuggestions.size());
         int index = size - 1;
         while (!mSuggestions.empty() && index >= 0) {
@@ -116,6 +121,7 @@
     }
 
     void clear() {
+        mHighestSuggestedWord = 0;
         while (!mSuggestions.empty()) {
             SuggestedWord* sw = mSuggestions.top();
             if (DEBUG_WORDS_PRIORITY_QUEUE) {
@@ -134,6 +140,28 @@
         DUMP_WORD(mSuggestions.top()->mWord, mSuggestions.top()->mWordLength);
     }
 
+    double getHighestNormalizedScore(const unsigned short* before, const int beforeLength,
+            unsigned short** outWord, int *outScore, int *outLength) {
+        if (!mHighestSuggestedWord) {
+            return 0.0;
+        }
+        SuggestedWord* sw = mHighestSuggestedWord;
+        const int score = sw->mScore;
+        unsigned short* word = sw->mWord;
+        const int wordLength = sw->mWordLength;
+        if (outScore) {
+            *outScore = score;
+        }
+        if (outWord) {
+            *outWord = word;
+        }
+        if (outLength) {
+            *outLength = wordLength;
+        }
+        return Correction::RankingAlgorithm::calcNormalizedScore(
+                before, beforeLength, word, wordLength, score);
+    }
+
  private:
     struct wordComparator {
         bool operator ()(SuggestedWord * left, SuggestedWord * right) {
@@ -158,6 +186,7 @@
     const unsigned int MAX_WORDS;
     const unsigned int MAX_WORD_LENGTH;
     SuggestedWord* mSuggestedWords;
+    SuggestedWord* mHighestSuggestedWord;
 };
 }
 
diff --git a/tests/res/values/strings.xml b/tests/res/values/strings.xml
new file mode 100644
index 0000000..bfd1c17
--- /dev/null
+++ b/tests/res/values/strings.xml
@@ -0,0 +1,50 @@
+<?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.
+*/
+-->
+
+<resources>
+    <string name="empty_string">""</string>
+    <string name="single_char">"a"</string>
+    <string name="space">" "</string>
+    <string name="single_label">"abc"</string>
+    <string name="spaces">"   "</string>
+    <string name="spaces_in_label">"a b c"</string>
+    <string name="spaces_at_beginning_of_label">" abc"</string>
+    <string name="spaces_at_end_of_label">"abc "</string>
+    <string name="label_surrounded_by_spaces">" abc "</string>
+    <string name="escaped_char">"\\a"</string>
+    <string name="escaped_comma">"\\,"</string>
+    <string name="escaped_escape">"\\\\"</string>
+    <string name="escaped_label">"a\\bc"</string>
+    <string name="escaped_label_at_beginning">"\\abc"</string>
+    <string name="escaped_label_with_comma">"a\\,c"</string>
+    <string name="escaped_label_with_comma_at_beginning">"\\,bc"</string>
+    <string name="escaped_label_with_successive">"\\,\\\\bc"</string>
+    <string name="escaped_label_with_escape">"a\\\\c"</string>
+    <string name="multiple_chars">"a,b,c"</string>
+    <string name="multiple_chars_surrounded_by_spaces">" a , b , c "</string>
+    <string name="multiple_labels">"abc,def,ghi"</string>
+    <string name="multiple_labels_surrounded_by_spaces">" abc , def , ghi "</string>
+    <string name="multiple_chars_with_comma">"a,\\,,c"</string>
+    <string name="multiple_chars_with_comma_surrounded_by_spaces">" a , \\, , c "</string>
+    <string name="multiple_labels_with_escape">"\\abc,d\\ef,gh\\i"</string>
+    <string name="multiple_labels_with_escape_surrounded_by_spaces">" \\abc , d\\ef , gh\\i "</string>
+    <string name="multiple_labels_with_comma_and_escape">"ab\\\\,d\\\\\\,,g\\,i"</string>
+    <string name="multiple_labels_with_comma_and_escape_surrounded_by_spaces">" ab\\\\ , d\\\\\\, , g\\,i "</string>
+</resources>
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
index 4050a71..29881d9 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
@@ -16,30 +16,53 @@
 
 package com.android.inputmethod.keyboard.internal;
 
-import com.android.inputmethod.keyboard.internal.KeyStyles.EmptyKeyStyle;
-
+import android.content.res.Resources;
 import android.test.AndroidTestCase;
 import android.text.TextUtils;
 
+import com.android.inputmethod.latin.tests.R;
+
+import java.util.Arrays;
+
 public class KeyStylesTests extends AndroidTestCase {
+    private Resources mTestResources;
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mTestResources = getTestContext().getResources();
+    }
+
     private static String format(String message, Object expected, Object actual) {
         return message + " expected:<" + expected + "> but was:<" + actual + ">";
     }
 
-    private static void assertTextArray(String message, CharSequence value,
-            CharSequence ... expected) {
-        final CharSequence actual[] = EmptyKeyStyle.parseCsvText(value);
+    private void assertTextArray(String message, String value, String ... expected) {
+        final String actual[] = KeyStyles.parseCsvText(value, mTestResources,
+                R.string.empty_string);
         if (expected.length == 0) {
             assertNull(message, actual);
             return;
         }
-        assertSame(message + ": result length", expected.length, actual.length);
+        assertEquals(message + ": expected=" + Arrays.toString(expected)
+                + " actual=" + Arrays.toString(actual)
+                + ": result length", expected.length, actual.length);
         for (int i = 0; i < actual.length; i++) {
             final boolean equals = TextUtils.equals(expected[i], actual[i]);
             assertTrue(format(message + ": result at " + i + ":", expected[i], actual[i]), equals);
         }
     }
 
+    private void assertError(String message, String value, String ... expected) {
+        try {
+            assertTextArray(message, value, expected);
+            fail(message);
+        } catch (Exception pcpe) {
+            // success.
+        }
+    }
+
     public void testParseCsvTextZero() {
         assertTextArray("Empty string", "");
     }
@@ -52,7 +75,10 @@
         assertTextArray("Spaces in label", "a b c", "a b c");
         assertTextArray("Spaces at beginning of label", " abc", " abc");
         assertTextArray("Spaces at end of label", "abc ", "abc ");
-        assertTextArray("label surrounded by spaces", " abc ", " abc ");
+        assertTextArray("Label surrounded by spaces", " abc ", " abc ");
+
+        assertTextArray("Incomplete resource reference 1", "string", "string");
+        assertTextArray("Incomplete resource reference 2", "@strin", "@strin");
     }
 
     public void testParseCsvTextSingleEscaped() {
@@ -60,11 +86,13 @@
         assertTextArray("Escaped comma", "\\,", ",");
         assertTextArray("Escaped escape", "\\\\", "\\");
         assertTextArray("Escaped label", "a\\bc", "abc");
-        assertTextArray("Escaped label at begininng", "\\abc", "abc");
+        assertTextArray("Escaped label at beginning", "\\abc", "abc");
         assertTextArray("Escaped label with comma", "a\\,c", "a,c");
         assertTextArray("Escaped label with comma at beginning", "\\,bc", ",bc");
         assertTextArray("Escaped label with successive", "\\,\\\\bc", ",\\bc");
         assertTextArray("Escaped label with escape", "a\\\\c", "a\\c");
+
+        assertTextArray("Escaped @string", "\\@string/empty_string", "@string/empty_string");
     }
 
     public void testParseCsvTextMulti() {
@@ -86,5 +114,109 @@
                 "ab\\\\,d\\\\\\,,g\\,i", "ab\\", "d\\,", "g,i");
         assertTextArray("Multiple labels with comma and escape surrounded by spaces",
                 " ab\\\\ , d\\\\\\, , g\\,i ", " ab\\ ", " d\\, ", " g,i ");
+
+        assertTextArray("Multiple escaped @string", "\\@,\\@string/empty_string",
+                "@", "@string/empty_string");
+    }
+
+    public void testParseCsvResourceError() {
+        assertError("Incomplete resource name 1", "@string", "@string");
+        assertError("Incomplete resource name 2", "@string/", "@string/");
+        assertError("Non existing resource", "@string/non_existing");
+    }
+
+    public void testParseCsvResourceZero() {
+        assertTextArray("Empty string",
+                "@string/empty_string");
+    }
+
+    public void testParseCsvResourceSingle() {
+        assertTextArray("Single char",
+                "@string/single_char", "a");
+        assertTextArray("Space",
+                "@string/space", " ");
+        assertTextArray("Single label",
+                "@string/single_label", "abc");
+        assertTextArray("Spaces",
+                "@string/spaces", "   ");
+        assertTextArray("Spaces in label",
+                "@string/spaces_in_label", "a b c");
+        assertTextArray("Spaces at beginning of label",
+                "@string/spaces_at_beginning_of_label", " abc");
+        assertTextArray("Spaces at end of label",
+                "@string/spaces_at_end_of_label", "abc ");
+        assertTextArray("label surrounded by spaces",
+                "@string/label_surrounded_by_spaces", " abc ");
+    }
+
+    public void testParseCsvResourceSingleEscaped() {
+        assertTextArray("Escaped char",
+                "@string/escaped_char", "a");
+        assertTextArray("Escaped comma",
+                "@string/escaped_comma", ",");
+        assertTextArray("Escaped escape",
+                "@string/escaped_escape", "\\");
+        assertTextArray("Escaped label",
+                "@string/escaped_label", "abc");
+        assertTextArray("Escaped label at beginning",
+                "@string/escaped_label_at_beginning", "abc");
+        assertTextArray("Escaped label with comma",
+                "@string/escaped_label_with_comma", "a,c");
+        assertTextArray("Escaped label with comma at beginning",
+                "@string/escaped_label_with_comma_at_beginning", ",bc");
+        assertTextArray("Escaped label with successive",
+                "@string/escaped_label_with_successive", ",\\bc");
+        assertTextArray("Escaped label with escape",
+                "@string/escaped_label_with_escape", "a\\c");
+    }
+
+    public void testParseCsvResourceMulti() {
+        assertTextArray("Multiple chars",
+                "@string/multiple_chars", "a", "b", "c");
+        assertTextArray("Multiple chars surrounded by spaces",
+                "@string/multiple_chars_surrounded_by_spaces",
+                " a ", " b ", " c ");
+        assertTextArray("Multiple labels",
+                "@string/multiple_labels", "abc", "def", "ghi");
+        assertTextArray("Multiple labels surrounded by spaces",
+                "@string/multiple_labels_surrounded_by_spaces", " abc ", " def ", " ghi ");
+    }
+
+    public void testParseCsvResourcetMultiEscaped() {
+        assertTextArray("Multiple chars with comma",
+                "@string/multiple_chars_with_comma",
+                "a", ",", "c");
+        assertTextArray("Multiple chars with comma surrounded by spaces",
+                "@string/multiple_chars_with_comma_surrounded_by_spaces",
+                " a ", " , ", " c ");
+        assertTextArray("Multiple labels with escape",
+                "@string/multiple_labels_with_escape",
+                "abc", "def", "ghi");
+        assertTextArray("Multiple labels with escape surrounded by spaces",
+                "@string/multiple_labels_with_escape_surrounded_by_spaces",
+                " abc ", " def ", " ghi ");
+        assertTextArray("Multiple labels with comma and escape",
+                "@string/multiple_labels_with_comma_and_escape",
+                "ab\\", "d\\,", "g,i");
+        assertTextArray("Multiple labels with comma and escape surrounded by spaces",
+                "@string/multiple_labels_with_comma_and_escape_surrounded_by_spaces",
+                " ab\\ ", " d\\, ", " g,i ");
+    }
+
+    public void testParseMultipleResources() {
+        assertTextArray("Literals and resources",
+                "1,@string/multiple_chars,z", "1", "a", "b", "c", "z");
+        assertTextArray("Multiple single resource chars and labels",
+                "@string/single_char,@string/single_label,@string/escaped_comma",
+                "a", "abc", ",");
+        assertTextArray("Multiple multiple resource chars and labels",
+                "@string/multiple_chars,@string/multiple_labels,@string/multiple_chars_with_comma",
+                "a", "b", "c", "abc", "def", "ghi", "a", ",", "c");
+        assertTextArray("Concatenated resources",
+                "@string/multiple_chars@string/multiple_labels@string/multiple_chars_with_comma",
+                "a", "b", "cabc", "def", "ghia", ",", "c");
+        assertTextArray("Concatenated resource and literal",
+                "abc@string/multiple_labels",
+                "abcabc", "def", "ghi");
     }
 }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
new file mode 100644
index 0000000..729120b
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateMultiTouchTests.java
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+public class KeyboardStateMultiTouchTests extends KeyboardStateTestsBase {
+    // Shift key chording input.
+    public void testChording() {
+        // Press shift key and hold, enter into choring shift state.
+        pressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+
+        // Press/release letter keys.
+        chordingPressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        chordingPressAndReleaseKey('X', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Release shift key, switch back to alphabet.
+        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
+
+        // Press symbols key and hold, enter into choring symbols state.
+        pressKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED);
+
+        // Press/release symbol letter keys.
+        chordingPressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        chordingPressAndReleaseKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Release symbols key, switch back to alphabet.
+        releaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED);
+    }
+
+    // Shift key chording input in shift locked.
+    public void testShiftChordingShiftLocked() {
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Press shift key and hold, enter into choring shift state.
+        pressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+
+        // Press/release letter keys.
+        chordingPressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        chordingPressAndReleaseKey('X', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Release shift key, switch back to alphabet shift locked.
+        releaseKey(CODE_SHIFT, ALPHABET_SHIFT_LOCKED);
+
+        // Press symbols key and hold, enter into choring symbols state.
+        pressKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED);
+
+        // Press/release symbol letter keys.
+        chordingPressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        chordingPressAndReleaseKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Release symbols key, switch back to alphabet shift locked.
+        releaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Symbols key chording input.
+    public void testSymbolsChording() {
+        // Press/release symbols key, enter symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Press shift key and hold, enter into choring symbols shifted state.
+        pressKey(CODE_SHIFT, SYMBOLS_SHIFTED);
+
+        // Press/release symbols keys.
+        chordingPressAndReleaseKey('1', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        chordingPressAndReleaseKey('2', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+
+        // Release shift key, switch back to symbols.
+        releaseKey(CODE_SHIFT, SYMBOLS_UNSHIFTED);
+
+        // Press "ABC" key and hold, enter into choring alphabet state.
+        pressKey(CODE_SYMBOL, ALPHABET_UNSHIFTED);
+
+        // Press/release letter keys.
+        chordingPressAndReleaseKey('a', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        chordingPressAndReleaseKey('b', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Release "ABC" key, switch back to symbols.
+        releaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED);
+    }
+
+    // Symbols shifted key chording input in symbol.
+    public void testSymbolsShiftedChording() {
+        // Press/release symbols key, enter symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press/release shift key, enter symbols shifted.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+
+        // Press shift key and hold, enter into chording symbols state.
+        pressKey(CODE_SHIFT, SYMBOLS_UNSHIFTED);
+
+        // Press/release symbol letter keys.
+        chordingPressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        chordingPressAndReleaseKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Release shift key, switch back to symbols shifted state.
+        releaseKey(CODE_SHIFT, SYMBOLS_SHIFTED);
+
+        // Press "ABC" key and hold, enter into choring alphabet state.
+        pressKey(CODE_SYMBOL, ALPHABET_UNSHIFTED);
+
+        // Press/release letter keys.
+        chordingPressAndReleaseKey('a', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        chordingPressAndReleaseKey('b', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Release "ABC" key, switch back to symbols.
+        releaseKey(CODE_SYMBOL, SYMBOLS_SHIFTED);
+    }
+
+    // Chording shift key in automatic upper case.
+    public void testAutomaticUpperCaseChording() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state with auto caps enabled.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press shift key and hold, enter into chording shift state.
+        pressKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED);
+
+        // Press/release letter keys.
+        chordingPressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Release shift key, switch back to alphabet.
+        releaseKey(CODE_SHIFT, ALPHABET_UNSHIFTED);
+    }
+
+    // Chording symbol key in automatic upper case.
+    public void testAutomaticUpperCaseChording2() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state with auto caps enabled.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press "123?" key and hold, enter into chording symbols state.
+        pressKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED);
+
+        // Press/release symbol letter keys.
+        chordingPressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Release "123?" key, switch back to alphabet.
+        releaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED);
+    }
+
+    // TODO: Multitouch test
+
+    // TODO: n-Keys roll over test
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateNonDistinctTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateNonDistinctTests.java
deleted file mode 100644
index e3f0e07..0000000
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateNonDistinctTests.java
+++ /dev/null
@@ -1,430 +0,0 @@
-/*
- * Copyright (C) 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.
- */
-
-package com.android.inputmethod.keyboard.internal;
-
-import android.test.AndroidTestCase;
-
-public class KeyboardStateNonDistinctTests extends AndroidTestCase
-        implements MockKeyboardSwitcher.Constants {
-    protected MockKeyboardSwitcher mSwitcher;
-
-    public boolean hasDistinctMultitouch() {
-        return false;
-    }
-
-    @Override
-    protected void setUp() throws Exception {
-        super.setUp();
-
-        mSwitcher = new MockKeyboardSwitcher();
-
-        final String layoutSwitchBackSymbols = "";
-        mSwitcher.loadKeyboard(layoutSwitchBackSymbols, hasDistinctMultitouch());
-    }
-
-    public void assertAlphabetNormal() {
-        assertTrue(mSwitcher.assertAlphabetNormal());
-    }
-
-    public void assertAlphabetManualShifted() {
-        assertTrue(mSwitcher.assertAlphabetManualShifted());
-    }
-
-    public void assertAlphabetAutomaticShifted() {
-        assertTrue(mSwitcher.assertAlphabetAutomaticShifted());
-    }
-
-    public void assertAlphabetShiftLocked() {
-        assertTrue(mSwitcher.assertAlphabetShiftLocked());
-    }
-
-    public void assertSymbolsNormal() {
-        assertTrue(mSwitcher.assertSymbolsNormal());
-    }
-
-    public void assertSymbolsShifted() {
-        assertTrue(mSwitcher.assertSymbolsShifted());
-    }
-
-    // Initial state test.
-    public void testLoadKeyboard() {
-        assertAlphabetNormal();
-    }
-
-    // Shift key in alphabet mode.
-    public void testShift() {
-        // Press/release shift key, enter into shift state.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Press/release shift key, back to normal state.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-
-        // Press/release shift key, enter into shift state.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Press/release letter key, snap back to normal state.
-        mSwitcher.onPressKey('Z');
-        mSwitcher.onCodeInput('Z');
-        mSwitcher.onReleaseKey('Z');
-        assertAlphabetNormal();
-    }
-
-    // Shift key sliding input.
-    public void testShiftSliding() {
-        // Press shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Slide out shift key.
-        mSwitcher.onReleaseKey(CODE_SHIFT, SLIDING);
-        assertAlphabetManualShifted();
-
-        // Enter into letter key.
-        mSwitcher.onPressKey('Z');
-        assertAlphabetManualShifted();
-        // Release letter key, snap back to alphabet.
-        mSwitcher.onCodeInput('Z');
-        mSwitcher.onReleaseKey('Z');
-        assertAlphabetNormal();
-    }
-
-    public void enterSymbolsMode() {
-        // Press/release "?123" key.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput(CODE_SYMBOL);
-        mSwitcher.onReleaseKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-    }
-
-    public void leaveSymbolsMode() {
-        // Press/release "ABC" key.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertAlphabetNormal();
-        mSwitcher.onCodeInput(CODE_SYMBOL);
-        mSwitcher.onReleaseKey(CODE_SYMBOL);
-        assertAlphabetNormal();
-    }
-
-    // Switching between alphabet and symbols.
-    public void testAlphabetAndSymbols() {
-        enterSymbolsMode();
-        leaveSymbolsMode();
-    }
-
-    // Switching between alphabet shift locked and symbols.
-    public void testAlphabetShiftLockedAndSymbols() {
-        enterShiftLockWithLongPressShift();
-        enterSymbolsMode();
-
-        // Press/release "ABC" key, switch back to shift locked mode.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertAlphabetShiftLocked();
-        mSwitcher.onCodeInput(CODE_SYMBOL);
-        mSwitcher.onReleaseKey(CODE_SYMBOL);
-        assertAlphabetShiftLocked();
-    }
-
-    // Symbols key sliding input.
-    public void testSymbolsSliding() {
-        // Press "123?" key.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-        // Slide out from "123?" key.
-        mSwitcher.onReleaseKey(CODE_SYMBOL, SLIDING);
-        assertSymbolsNormal();
-
-        // Enter into letter key.
-        mSwitcher.onPressKey('z');
-        assertSymbolsNormal();
-        // Release letter key, snap back to alphabet.
-        mSwitcher.onCodeInput('z');
-        mSwitcher.onReleaseKey('z');
-        assertAlphabetNormal();
-    }
-
-    // Switching between symbols and symbols shifted.
-    public void testSymbolsAndSymbolsShifted() {
-        enterSymbolsMode();
-
-        // Press/release "=\<" key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertSymbolsShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertSymbolsShifted();
-
-        // Press/release "?123" key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertSymbolsNormal();
-
-        leaveSymbolsMode();
-    }
-
-    // Symbols shift sliding input
-    public void testSymbolsShiftSliding() {
-        enterSymbolsMode();
-
-        // Press "=\<" key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertSymbolsShifted();
-        // Slide out "=\<" key.
-        mSwitcher.onReleaseKey(CODE_SHIFT, SLIDING);
-        assertSymbolsShifted();
-
-        // Enter into symbol shifted letter key.
-        mSwitcher.onPressKey('~');
-        assertSymbolsShifted();
-        // Release symbol shifted letter key, snap back to symbols.
-        mSwitcher.onCodeInput('~');
-        mSwitcher.onReleaseKey('~');
-        assertSymbolsNormal();
-    }
-
-    // Symbols shift sliding input from symbols shifted.
-    public void testSymbolsShiftSliding2() {
-        enterSymbolsMode();
-
-        // Press/release "=\<" key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertSymbolsShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertSymbolsShifted();
-
-        // Press "123?" key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertSymbolsNormal();
-        // Slide out "123?" key.
-        mSwitcher.onReleaseKey(CODE_SHIFT, SLIDING);
-        assertSymbolsNormal();
-
-        // Enter into symbol letter key.
-        mSwitcher.onPressKey('1');
-        assertSymbolsNormal();
-        // Release symbol letter key, snap back to symbols shift.
-        mSwitcher.onCodeInput('1');
-        mSwitcher.onReleaseKey('1');
-        assertSymbolsShifted();
-    }
-
-    // Automatic snap back to alphabet from symbols by space key.
-    public void testSnapBackBySpace() {
-        enterSymbolsMode();
-
-        // Enter a symbol letter.
-        mSwitcher.onPressKey('1');
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput('1');
-        mSwitcher.onReleaseKey('1');
-        assertSymbolsNormal();
-        // Enter space, snap back to alphabet.
-        mSwitcher.onPressKey(CODE_SPACE);
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput(CODE_SPACE);
-        mSwitcher.onReleaseKey(CODE_SPACE);
-        assertAlphabetNormal();
-    }
-
-    // TODO: Add automatic snap back to shift locked test.
-
-    // Automatic snap back to alphabet from symbols by registered letters.
-    public void testSnapBack() {
-        final String snapBackChars = "'";
-        final int snapBackCode = snapBackChars.codePointAt(0);
-        final boolean hasDistinctMultitouch = true;
-        mSwitcher.loadKeyboard(snapBackChars, hasDistinctMultitouch);
-
-        enterSymbolsMode();
-
-        // Enter a symbol letter.
-        mSwitcher.onPressKey('1');
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput('1');
-        mSwitcher.onReleaseKey('1');
-        assertSymbolsNormal();
-        // Enter snap back letter, snap back to alphabet.
-        mSwitcher.onPressKey(snapBackCode);
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput(snapBackCode);
-        mSwitcher.onReleaseKey(snapBackCode);
-        assertAlphabetNormal();
-    }
-
-    // Automatic upper case test
-    public void testAutomaticUpperCase() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state with auto caps enabled.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-
-        // Press shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Release shift key.
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-    }
-
-    // Sliding from shift key in automatic upper case.
-    public void testAutomaticUpperCaseSliding() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state with auto caps enabled.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-
-        // Press shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Slide out shift key.
-        mSwitcher.onReleaseKey(CODE_SHIFT, SLIDING);
-        assertAlphabetManualShifted();
-        // Enter into letter key.
-        mSwitcher.onPressKey('Z');
-        assertAlphabetManualShifted();
-        // Release letter key, snap back to alphabet.
-        mSwitcher.onCodeInput('Z');
-        mSwitcher.onReleaseKey('Z');
-        assertAlphabetNormal();
-    }
-
-    // Sliding from symbol key in automatic upper case.
-    public void testAutomaticUpperCaseSliding2() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state with auto caps enabled.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-
-        // Press "123?" key.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-        // Slide out "123?" key.
-        mSwitcher.onReleaseKey(CODE_SYMBOL, SLIDING);
-        assertSymbolsNormal();
-        // Enter into symbol letter keys.
-        mSwitcher.onPressKey('1');
-        assertSymbolsNormal();
-        // Release symbol letter key, snap back to alphabet.
-        mSwitcher.onCodeInput('1');
-        mSwitcher.onReleaseKey('1');
-        assertAlphabetNormal();
-    }
-
-    public void enterShiftLockWithLongPressShift() {
-        // Long press shift key
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Long press recognized in LatinKeyboardView.KeyTimerHandler.
-        mSwitcher.onCodeInput(CODE_CAPSLOCK);
-        assertAlphabetShiftLocked();
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetShiftLocked();
-    }
-
-    public void leaveShiftLockWithLongPressShift() {
-        // Press shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Long press recognized in LatinKeyboardView.KeyTimerHandler.
-        mSwitcher.onCodeInput(CODE_CAPSLOCK);
-        assertAlphabetNormal();
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-    }
-
-    // Long press shift key.
-    // TODO: Move long press recognizing timer/logic into KeyboardState.
-    public void testLongPressShift() {
-        enterShiftLockWithLongPressShift();
-        leaveShiftLockWithLongPressShift();
-     }
-
-    // Leave shift lock with single tap shift key.
-    public void testShiftInShiftLock() {
-        enterShiftLockWithLongPressShift();
-        assertAlphabetShiftLocked();
-
-        // Tap shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT, SINGLE);
-        assertAlphabetManualShifted();
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-    }
-
-    // Double tap shift key.
-    // TODO: Move double tap recognizing timer/logic into KeyboardState.
-    public void testDoubleTapShift() {
-        // First shift key tap.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Second shift key tap.
-        // Double tap recognized in LatinKeyboardView.KeyTimerHandler.
-        mSwitcher.onCodeInput(CODE_CAPSLOCK);
-        assertAlphabetShiftLocked();
-
-        // First shift key tap.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-        // Second shift key tap.
-        // Second tap is ignored in LatinKeyboardView.KeyTimerHandler.
-    }
-
-    // Update shift state.
-    public void testUpdateShiftState() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-    }
-
-    // Update shift state when shift locked.
-    public void testUpdateShiftStateInShiftLocked() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        enterShiftLockWithLongPressShift();
-        assertAlphabetShiftLocked();
-        // Update shift state when shift locked
-        mSwitcher.updateShiftState();
-        assertAlphabetShiftLocked();
-    }
-
-    // TODO: Change focus test.
-
-    // TODO: Change orientation test.
-}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
new file mode 100644
index 0000000..d13ca63
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
@@ -0,0 +1,353 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+public class KeyboardStateSingleTouchTests extends KeyboardStateTestsBase {
+    // Shift key in alphabet mode.
+    public void testShift() {
+        // Press/release shift key, enter into shift state.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press/release shift key, back to normal state.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+
+        // Press/release shift key, enter into shift state.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press/release letter key, switch back to normal state.
+        pressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Shift key sliding input.
+    public void testShiftSliding() {
+        // Press and slide from shift key.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Enter/release letter key, switch back to alphabet.
+        pressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Switching between alphabet and symbols.
+    public void testAlphabetAndSymbols() {
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press/release "?123" key, back to alphabet.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Switching between alphabet shift locked and symbols.
+    public void testAlphabetShiftLockedAndSymbols() {
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Press/release "ABC" key, switch back to shift locked mode.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Symbols key sliding input.
+    public void testSymbolsSliding() {
+        // Press and slide from "123?" key.
+        pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter/release into symbol key, switch back to alphabet.
+        pressAndReleaseKey('!', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Switching between symbols and symbols shifted.
+    public void testSymbolsAndSymbolsShifted() {
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Press/release "=\<" key, enter into symbols shifted.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+    }
+
+    // Symbols shift sliding input
+    public void testSymbolsShiftSliding() {
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Press and slide from "=\<" key.
+        pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+
+        // Enter/release symbol shifted letter key, switch back to symbols.
+        pressAndReleaseKey('~', SYMBOLS_SHIFTED, SYMBOLS_UNSHIFTED);
+    }
+
+    // Symbols shift sliding input from symbols shifted.
+    public void testSymbolsShiftSliding2() {
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Press/release "=\<" key, enter into symbols shifted.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+
+        // Press and slide from "123?" key.
+        pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter/release symbol letter key, switch back to symbols shifted.
+        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_SHIFTED);
+    }
+
+    // Automatic switch back to alphabet from symbols by space key.
+    public void testSwitchBackBySpace() {
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter a symbol letter.
+        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter space, switch back to alphabet.
+        pressAndReleaseKey(CODE_SPACE, SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Automatic switch back to shift locked test.
+    public void testSwitchBackBySpaceInShiftLocked() {
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter a symbol letter.
+        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter space, switch back to alphabet.
+        pressAndReleaseKey(CODE_SPACE, SYMBOLS_UNSHIFTED, ALPHABET_SHIFT_LOCKED);
+    }
+
+
+    // Automatic switch back to alphabet from symbols by registered letters.
+    public void testSwitchBackChar() {
+        // Set switch back chars.
+        final String switchBackSymbols = "'";
+        final int switchBackCode = switchBackSymbols.codePointAt(0);
+        setLayoutSwitchBackSymbols(switchBackSymbols);
+        loadKeyboard(ALPHABET_UNSHIFTED);
+
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter a symbol letter.
+        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter switch back letter, switch back to alphabet.
+        pressAndReleaseKey(switchBackCode, SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Automatic upper case test
+    public void testAutomaticUpperCase() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state with auto caps enabled.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press/release shift key, back to alphabet.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Sliding from shift key in automatic upper case.
+    public void testAutomaticUpperCaseSliding() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state with auto caps enabled.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press and slide from shift key.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Enter and release letter key, back to alphabet.
+        pressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Sliding from symbol key in automatic upper case.
+    public void testAutomaticUpperCaseSliding2() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state with auto caps enabled.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press and slide from "123?" key.
+        pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+
+        // Enter and release symbol letter keys, back to alphabet.
+        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Long press shift key.
+    // TODO: Move long press recognizing timer/logic into KeyboardState.
+    public void testLongPressShift() {
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release letter key, remain in shift locked.
+        pressAndReleaseKey('A', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release letter key, remain in shift locked.
+        pressAndReleaseKey('B', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release word separator, remain in shift locked.
+        pressAndReleaseKey(CODE_SPACE, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+
+        // Long press shift key, back to alphabet.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+     }
+
+    // Leave shift lock with single tap shift key.
+    public void testShiftInShiftLock() {
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Press/release shift key, back to alphabet.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+    }
+
+    // Double tap shift key.
+    // TODO: Move double tap recognizing timer/logic into KeyboardState.
+    public void testDoubleTapShift() {
+        // First shift key tap.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+
+        // Second shift key tap.
+        // Double tap recognized in LatinKeyboardView.KeyTimerHandler.
+        secondTapShiftKey(ALPHABET_SHIFT_LOCKED);
+
+        // First shift key tap.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+
+        // Second shift key tap.
+        // Second tap is ignored in LatinKeyboardView.KeyTimerHandler.
+    }
+
+    // Update shift state.
+    public void testUpdateShiftState() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press/release letter key, back to alphabet.
+        pressAndReleaseKey('A', ALPHABET_AUTOMATIC_SHIFTED, ALPHABET_UNSHIFTED);
+
+        // Press/release letter key
+        pressAndReleaseKey('b', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Press/release auto caps trigger letter, back to automatic shifted.
+        pressAndReleaseKey(CODE_AUTO_CAPS_TRIGGER, ALPHABET_UNSHIFTED, ALPHABET_AUTOMATIC_SHIFTED);
+    }
+
+    // Update shift state when shift locked.
+    public void testUpdateShiftStateInShiftLocked() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+
+        // Update shift state when shift locked
+        updateShiftState(ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Change focus to new text field.
+    public void testChangeFocus() {
+        // Press/release shift key.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_UNSHIFTED);
+
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_UNSHIFTED);
+
+        // Press/release symbol key.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_UNSHIFTED);
+
+        // Press/release symbol key.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press/release shift key.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_UNSHIFTED);
+    }
+
+    // Change focus to auto caps text field.
+    public void testChangeFocusAutoCaps() {
+        // Set auto caps mode on.
+        setAutoCapsMode(AUTO_CAPS);
+
+        // Update shift state.
+        updateShiftState(ALPHABET_AUTOMATIC_SHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press/release shift key, enter alphabet.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press/release symbol key.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+
+        // Press/release symbol key.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press/release shift key.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Change focus to new text field.
+        loadKeyboard(ALPHABET_AUTOMATIC_SHIFTED);
+    }
+
+    // Change orientation.
+    public void testChangeOrientation() {
+        // Press/release shift key.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Rotate device.
+        rotateDevice(ALPHABET_MANUAL_SHIFTED);
+
+        // Long press shift key, enter alphabet shift locked.
+        longPressShiftKey(ALPHABET_MANUAL_SHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Rotate device.
+        rotateDevice(ALPHABET_SHIFT_LOCKED);
+
+        // Press/release symbol key.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Rotate device.
+        rotateDevice(SYMBOLS_UNSHIFTED);
+
+        // Press/release shift key.
+        pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Rotate device.
+        rotateDevice(SYMBOLS_SHIFTED);
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTests.java
deleted file mode 100644
index 19339f7..0000000
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTests.java
+++ /dev/null
@@ -1,117 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License"); you may not
- * use this file except in compliance with the License. You may obtain a copy of
- * the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
- * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
- * License for the specific language governing permissions and limitations under
- * the License.
- */
-
-package com.android.inputmethod.keyboard.internal;
-
-public class KeyboardStateTests extends KeyboardStateNonDistinctTests {
-    @Override
-    public boolean hasDistinctMultitouch() {
-        return true;
-    }
-
-    // Shift key chording input.
-    public void testShiftChording() {
-        // Press shift key and hold, enter into choring shift state.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-
-        // Press/release letter keys.
-        mSwitcher.onPressKey('Z');
-        mSwitcher.onCodeInput('Z', MULTI);
-        mSwitcher.onReleaseKey('Z');
-        assertAlphabetManualShifted();
-        mSwitcher.onPressKey('X');
-        mSwitcher.onCodeInput('X', MULTI);
-        mSwitcher.onReleaseKey('X');
-        assertAlphabetManualShifted();
-
-        // Release shift key, snap back to normal state.
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        mSwitcher.updateShiftState();
-        assertAlphabetNormal();
-    }
-
-    // Symbols key chording input.
-    public void testSymbolsChording() {
-        // Press symbols key and hold, enter into choring shift state.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-
-        // Press/release symbol letter keys.
-        mSwitcher.onPressKey('1');
-        mSwitcher.onCodeInput('1', MULTI);
-        mSwitcher.onReleaseKey('1');
-        assertSymbolsNormal();
-        mSwitcher.onPressKey('2');
-        mSwitcher.onCodeInput('2', MULTI);
-        mSwitcher.onReleaseKey('2');
-        assertSymbolsNormal();
-
-        // Release shift key, snap back to normal state.
-        mSwitcher.onCodeInput(CODE_SYMBOL);
-        mSwitcher.onReleaseKey(CODE_SYMBOL);
-        mSwitcher.updateShiftState();
-        assertAlphabetNormal();
-    }
-
-    // Chording shift key in automatic upper case.
-    public void testAutomaticUpperCaseChording() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state with auto caps enabled.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-
-        // Press shift key.
-        mSwitcher.onPressKey(CODE_SHIFT);
-        assertAlphabetManualShifted();
-        // Press/release letter keys.
-        mSwitcher.onPressKey('Z');
-        mSwitcher.onCodeInput('Z', MULTI);
-        mSwitcher.onReleaseKey('Z');
-        assertAlphabetManualShifted();
-        // Release shift key, snap back to alphabet.
-        mSwitcher.onCodeInput(CODE_SHIFT);
-        mSwitcher.onReleaseKey(CODE_SHIFT);
-        assertAlphabetNormal();
-    }
-
-    // Chording symbol key in automatic upper case.
-    public void testAutomaticUpperCaseChrding2() {
-        mSwitcher.setAutoCapsMode(AUTO_CAPS);
-        // Update shift state with auto caps enabled.
-        mSwitcher.updateShiftState();
-        assertAlphabetAutomaticShifted();
-
-        // Press "123?" key.
-        mSwitcher.onPressKey(CODE_SYMBOL);
-        assertSymbolsNormal();
-        // Press/release symbol letter keys.
-        mSwitcher.onPressKey('1');
-        assertSymbolsNormal();
-        mSwitcher.onCodeInput('1', MULTI);
-        mSwitcher.onReleaseKey('1');
-        assertSymbolsNormal();
-        // Release "123?" key, snap back to alphabet.
-        mSwitcher.onCodeInput(CODE_SYMBOL);
-        mSwitcher.onReleaseKey(CODE_SYMBOL);
-        assertAlphabetNormal();
-    }
-
-    // TODO: Multitouch test
-
-    // TODO: n-Keys roll over test
-}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java
new file mode 100644
index 0000000..62df2cf
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java
@@ -0,0 +1,113 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import android.test.AndroidTestCase;
+
+public class KeyboardStateTestsBase extends AndroidTestCase
+        implements MockKeyboardSwitcher.Constants {
+    protected MockKeyboardSwitcher mSwitcher;
+
+    private String mLayoutSwitchBackSymbols = "";
+
+    @Override
+    protected void setUp() throws Exception {
+        super.setUp();
+
+        mSwitcher = new MockKeyboardSwitcher();
+        mSwitcher.setAutoCapsMode(NO_AUTO_CAPS);
+
+        loadKeyboard(ALPHABET_UNSHIFTED);
+    }
+
+    public void setAutoCapsMode(boolean autoCaps) {
+        mSwitcher.setAutoCapsMode(autoCaps);
+    }
+
+    public void setLayoutSwitchBackSymbols(String switchBackSymbols) {
+        mLayoutSwitchBackSymbols = switchBackSymbols;
+    }
+
+    public void updateShiftState(int afterUpdate) {
+        mSwitcher.updateShiftState();
+        assertEquals(afterUpdate, mSwitcher.getLayoutId());
+    }
+
+    public void loadKeyboard(int afterLoad) {
+        mSwitcher.loadKeyboard(mLayoutSwitchBackSymbols);
+        updateShiftState(afterLoad);
+    }
+
+    public void rotateDevice(int afterRotate) {
+        mSwitcher.saveKeyboardState();
+        mSwitcher.loadKeyboard(mLayoutSwitchBackSymbols);
+        assertEquals(afterRotate, mSwitcher.getLayoutId());
+    }
+
+    public void pressKey(int code, int afterPress) {
+        mSwitcher.onPressKey(code);
+        assertEquals(afterPress, mSwitcher.getLayoutId());
+    }
+
+    public void releaseKey(int code, int afterRelease) {
+        mSwitcher.onCodeInput(code, SINGLE);
+        mSwitcher.onReleaseKey(code, NOT_SLIDING);
+        assertEquals(afterRelease, mSwitcher.getLayoutId());
+    }
+
+    public void pressAndReleaseKey(int code, int afterPress, int afterRelease) {
+        pressKey(code, afterPress);
+        releaseKey(code, afterRelease);
+    }
+
+    public void chordingPressKey(int code, int afterPress) {
+        pressKey(code, afterPress);
+    }
+
+    public void chordingReleaseKey(int code, int afterRelease) {
+        mSwitcher.onCodeInput(code, MULTI);
+        mSwitcher.onReleaseKey(code, NOT_SLIDING);
+        assertEquals(afterRelease, mSwitcher.getLayoutId());
+    }
+
+    public void chordingPressAndReleaseKey(int code, int afterPress, int afterRelease) {
+        chordingPressKey(code, afterPress);
+        chordingReleaseKey(code, afterRelease);
+    }
+
+    public void pressAndSlideFromKey(int code, int afterPress, int afterSlide) {
+        pressKey(code, afterPress);
+        mSwitcher.onReleaseKey(code, SLIDING);
+        assertEquals(afterSlide, mSwitcher.getLayoutId());
+    }
+
+    public void longPressShiftKey(int afterPress, int afterLongPress) {
+        // Long press shift key
+        mSwitcher.onPressKey(CODE_SHIFT);
+        assertEquals(afterPress, mSwitcher.getLayoutId());
+        // Long press recognized in LatinKeyboardView.KeyTimerHandler.
+        mSwitcher.onCodeInput(CODE_CAPSLOCK, SINGLE);
+        assertEquals(afterLongPress, mSwitcher.getLayoutId());
+        mSwitcher.onReleaseKey(CODE_SHIFT, NOT_SLIDING);
+        assertEquals(afterLongPress, mSwitcher.getLayoutId());
+    }
+
+    public void secondTapShiftKey(int afterTap) {
+        mSwitcher.onCodeInput(CODE_CAPSLOCK, SINGLE);
+        assertEquals(afterTap, mSwitcher.getLayoutId());
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
index 1e294d1..87b4636 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
@@ -21,10 +21,10 @@
 
 public class MockKeyboardSwitcher implements KeyboardState.SwitchActions {
     public interface Constants {
-        // Argument for KeyboardState.onPressKey and onReleaseKey.
+        // Argument for {@link KeyboardState#onPressKey} and {@link KeyboardState#onReleaseKey}.
         public static final boolean NOT_SLIDING = false;
         public static final boolean SLIDING = true;
-        // Argument for KeyboardState.onCodeInput.
+        // Argument for {@link KeyboardState#onCodeInput}.
         public static final boolean SINGLE = true;
         public static final boolean MULTI = false;
         public static final boolean NO_AUTO_CAPS = false;
@@ -34,18 +34,17 @@
         public static final int CODE_SYMBOL = Keyboard.CODE_SWITCH_ALPHA_SYMBOL;
         public static final int CODE_CAPSLOCK = Keyboard.CODE_CAPSLOCK;
         public static final int CODE_SPACE = Keyboard.CODE_SPACE;
+        public static final int CODE_AUTO_CAPS_TRIGGER = Keyboard.CODE_SPACE;
+
+        public static final int ALPHABET_UNSHIFTED = 0;
+        public static final int ALPHABET_MANUAL_SHIFTED = 1;
+        public static final int ALPHABET_AUTOMATIC_SHIFTED = 2;
+        public static final int ALPHABET_SHIFT_LOCKED = 3;
+        public static final int SYMBOLS_UNSHIFTED = 4;
+        public static final int SYMBOLS_SHIFTED = 5;
     }
 
-    public static final String WORD_SEPARATORS = " ,.";
-
-    private static final int ALPHABET_UNSHIFTED = 0;
-    private static final int ALPHABET_MANUAL_SHIFTED = 1;
-    private static final int ALPHABET_AUTOMATIC_SHIFTED = 2;
-    private static final int ALPHABET_SHIFT_LOCKED = 3;
-    private static final int SYMBOLS_UNSHIFTED = 4;
-    private static final int SYMBOLS_SHIFTED = 5;
-
-    private int mLayout = ALPHABET_UNSHIFTED;
+    private int mLayout = Constants.ALPHABET_UNSHIFTED;
 
     private boolean mAutoCapsMode = Constants.NO_AUTO_CAPS;
     // Following InputConnection's behavior. Simulating InputType.TYPE_TEXT_FLAG_CAP_WORDS.
@@ -53,28 +52,8 @@
 
     private final KeyboardState mState = new KeyboardState(this);
 
-    public boolean assertAlphabetNormal() {
-        return mLayout == ALPHABET_UNSHIFTED;
-    }
-
-    public boolean assertAlphabetManualShifted() {
-        return mLayout == ALPHABET_MANUAL_SHIFTED;
-    }
-
-    public boolean assertAlphabetAutomaticShifted() {
-        return mLayout == ALPHABET_AUTOMATIC_SHIFTED;
-    }
-
-    public boolean assertAlphabetShiftLocked() {
-        return mLayout == ALPHABET_SHIFT_LOCKED;
-    }
-
-    public boolean assertSymbolsNormal() {
-        return mLayout == SYMBOLS_UNSHIFTED;
-    }
-
-    public boolean assertSymbolsShifted() {
-        return mLayout == SYMBOLS_SHIFTED;
+    public int getLayoutId() {
+        return mLayout;
     }
 
     public void setAutoCapsMode(boolean autoCaps) {
@@ -83,37 +62,37 @@
 
     @Override
     public void setAlphabetKeyboard() {
-        mLayout = ALPHABET_UNSHIFTED;
+        mLayout = Constants.ALPHABET_UNSHIFTED;
     }
 
     @Override
     public void setShifted(int shiftMode) {
         if (shiftMode == SwitchActions.UNSHIFT) {
-            mLayout = ALPHABET_UNSHIFTED;
+            mLayout = Constants.ALPHABET_UNSHIFTED;
         } else if (shiftMode == SwitchActions.MANUAL_SHIFT) {
-            mLayout = ALPHABET_MANUAL_SHIFTED;
+            mLayout = Constants.ALPHABET_MANUAL_SHIFTED;
         } else if (shiftMode == SwitchActions.AUTOMATIC_SHIFT) {
-            mLayout = ALPHABET_AUTOMATIC_SHIFTED;
+            mLayout = Constants.ALPHABET_AUTOMATIC_SHIFTED;
         }
     }
 
     @Override
     public void setShiftLocked(boolean shiftLocked) {
         if (shiftLocked) {
-            mLayout = ALPHABET_SHIFT_LOCKED;
+            mLayout = Constants.ALPHABET_SHIFT_LOCKED;
         } else {
-            mLayout = ALPHABET_UNSHIFTED;
+            mLayout = Constants.ALPHABET_UNSHIFTED;
         }
     }
 
     @Override
     public void setSymbolsKeyboard() {
-        mLayout = SYMBOLS_UNSHIFTED;
+        mLayout = Constants.SYMBOLS_UNSHIFTED;
     }
 
     @Override
     public void setSymbolsShiftedKeyboard() {
-        mLayout = SYMBOLS_SHIFTED;
+        mLayout = Constants.SYMBOLS_SHIFTED;
     }
 
     @Override
@@ -125,29 +104,26 @@
         mState.onUpdateShiftState(mAutoCapsMode && mAutoCapsState);
     }
 
-    public void loadKeyboard(String layoutSwitchBackSymbols,
-            boolean hasDistinctMultitouch) {
-        mState.onLoadKeyboard(layoutSwitchBackSymbols, hasDistinctMultitouch);
+    public void loadKeyboard(String layoutSwitchBackSymbols) {
+        mState.onLoadKeyboard(layoutSwitchBackSymbols);
+    }
+
+    public void saveKeyboardState() {
+        mState.onSaveKeyboardState();
     }
 
     public void onPressKey(int code) {
         mState.onPressKey(code);
     }
 
-    public void onReleaseKey(int code) {
-        onReleaseKey(code, Constants.NOT_SLIDING);
-    }
-
     public void onReleaseKey(int code, boolean withSliding) {
         mState.onReleaseKey(code, withSliding);
     }
 
-    public void onCodeInput(int code) {
-        onCodeInput(code, Constants.SINGLE);
-    }
-
     public void onCodeInput(int code, boolean isSinglePointer) {
-        mAutoCapsState = (WORD_SEPARATORS.indexOf(code) >= 0);
+        if (Keyboard.isLetterCode(code)) {
+            mAutoCapsState = (code == Constants.CODE_AUTO_CAPS_TRIGGER);
+        }
         mState.onCodeInput(code, isSinglePointer, mAutoCapsMode && mAutoCapsState);
     }
 
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java
index 0eb3242..74aaf9a 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java
@@ -25,11 +25,11 @@
 public class MoreKeySpecParserTests extends AndroidTestCase {
     private Resources mRes;
 
-    private static final int ICON_SETTINGS_KEY = 5;
+    private static final int ICON_SETTINGS_KEY = R.styleable.Keyboard_iconSettingsKey;
     private static final int ICON_UNDEFINED = KeyboardIconsSet.ICON_UNDEFINED;
 
     private static final String CODE_SETTINGS = "@integer/key_settings";
-    private static final String ICON_SETTINGS = "@icon/" + ICON_SETTINGS_KEY;
+    private static final String ICON_SETTINGS = "@icon/settingsKey";
     private static final String CODE_NON_EXISTING = "@integer/non_existing";
     private static final String ICON_NON_EXISTING = "@icon/non_existing";
 
@@ -53,7 +53,7 @@
         String actualOutputText = MoreKeySpecParser.getOutputText(moreKeySpec);
         assertEquals(message + ": ouptputText:", expectedOutputText, actualOutputText);
 
-        int actualIcon = MoreKeySpecParser.getIconId(moreKeySpec);
+        int actualIcon = MoreKeySpecParser.getIconAttrId(moreKeySpec);
         assertEquals(message + ": icon:", expectedIcon, actualIcon);
 
         int actualCode = MoreKeySpecParser.getCode(mRes, moreKeySpec);
@@ -66,7 +66,7 @@
             assertParser(message, moreKeySpec, expectedLabel, expectedOutputText, expectedIcon,
                     expectedCode);
             fail(message);
-        } catch (MoreKeySpecParser.MoreKeySpecParserError pcpe) {
+        } catch (Exception pcpe) {
             // success.
         }
     }
@@ -89,78 +89,78 @@
         assertParser("Single escaped at", "\\@",
                 "@", null, ICON_UNDEFINED, '@');
         assertParser("Single letter with outputText", "a|abc",
-                "a", "abc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped outputText", "a|a\\|c",
-                "a", "a|c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with comma outputText", "a|a,b",
-                "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped comma outputText", "a|a\\,b",
-                "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with outputText starts with at", "a|@bc",
-                "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with outputText contains at", "a|a@c",
-                "a", "a@c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped at outputText", "a|\\@bc",
-                "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single escaped escape with outputText", "\\\\|\\\\",
-                "\\", "\\", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "\\", "\\", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single escaped bar with outputText", "\\||\\|",
-                "|", "|", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "|", "|", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with code", "a|" + CODE_SETTINGS,
                 "a", null, ICON_UNDEFINED, mCodeSettings);
     }
 
     public void testLabel() {
         assertParser("Simple label", "abc",
-                "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped bar", "a\\|c",
-                "a|c", "a|c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a|c", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped escape", "a\\\\c",
-                "a\\c", "a\\c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a\\c", "a\\c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with comma", "a,c",
-                "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped comma", "a\\,c",
-                "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label starts with at", "@bc",
-                "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label contains at", "a@c",
-                "a@c", "a@c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a@c", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped at", "\\@bc",
-                "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped letter", "\\abc",
-                "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with outputText", "abc|def",
-                "abc", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with comma and outputText", "a,c|def",
-                "a,c", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a,c", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Escaped comma label with outputText", "a\\,c|def",
-                "a,c", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a,c", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Escaped label with outputText", "a\\|c|def",
-                "a|c", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a|c", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped bar outputText", "abc|d\\|f",
-                "abc", "d|f", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "d|f", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Escaped escape label with outputText", "a\\\\|def",
-                "a\\", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a\\", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label starts with at and outputText", "@bc|def",
-                "@bc", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "@bc", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label contains at label and outputText", "a@c|def",
-                "a@c", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a@c", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Escaped at label with outputText", "\\@bc|def",
-                "@bc", "def", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "@bc", "def", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with comma outputText", "abc|a,b",
-                "abc", "a,b", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped comma outputText", "abc|a\\,b",
-                "abc", "a,b", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with outputText starts with at", "abc|@bc",
-                "abc", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with outputText contains at", "abc|a@c",
-                "abc", "a@c", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped at outputText", "abc|\\@bc",
-                "abc", "@bc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped bar outputText", "abc|d\\|f",
-                "abc", "d|f", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "abc", "d|f", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Escaped bar label with escaped bar outputText", "a\\|c|d\\|f",
-                "a|c", "d|f", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+                "a|c", "d|f", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with code", "abc|" + CODE_SETTINGS,
                 "abc", null, ICON_UNDEFINED, mCodeSettings);
         assertParser("Escaped label with code", "a\\|c|" + CODE_SETTINGS,
@@ -169,13 +169,13 @@
 
     public void testIconAndCode() {
         assertParser("Icon with outputText", ICON_SETTINGS + "|abc",
-                null, "abc", ICON_SETTINGS_KEY, Keyboard.CODE_UNSPECIFIED);
+                null, "abc", ICON_SETTINGS_KEY, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Icon with outputText starts with at", ICON_SETTINGS + "|@bc",
-                null, "@bc", ICON_SETTINGS_KEY, Keyboard.CODE_UNSPECIFIED);
+                null, "@bc", ICON_SETTINGS_KEY, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Icon with outputText contains at", ICON_SETTINGS + "|a@c",
-                null, "a@c", ICON_SETTINGS_KEY, Keyboard.CODE_UNSPECIFIED);
+                null, "a@c", ICON_SETTINGS_KEY, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Icon with escaped at outputText", ICON_SETTINGS + "|\\@bc",
-                null, "@bc", ICON_SETTINGS_KEY, Keyboard.CODE_UNSPECIFIED);
+                null, "@bc", ICON_SETTINGS_KEY, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label starts with at and code", "@bc|" + CODE_SETTINGS,
                 "@bc", null, ICON_UNDEFINED, mCodeSettings);
         assertParser("Label contains at and code", "a@c|" + CODE_SETTINGS,
@@ -201,8 +201,8 @@
                 null, null, ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
         assertParserError("Icon without code", ICON_SETTINGS,
                 null, null, ICON_SETTINGS_KEY, Keyboard.CODE_UNSPECIFIED);
-        assertParser("Non existing icon", ICON_NON_EXISTING + "|abc",
-                null, "abc", ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
+        assertParserError("Non existing icon", ICON_NON_EXISTING + "|abc",
+                null, "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParserError("Non existing code", "abc|" + CODE_NON_EXISTING,
                 "abc", null, ICON_UNDEFINED, Keyboard.CODE_UNSPECIFIED);
         assertParserError("Third bar at end", "a|b|",
diff --git a/tests/src/com/android/inputmethod/latin/InputLogicTests.java b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
new file mode 100644
index 0000000..06ee5bf
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/InputLogicTests.java
@@ -0,0 +1,122 @@
+/*
+ * Copyright (C) 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.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.Context;
+import android.content.Intent;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+import android.test.ServiceTestCase;
+import android.text.InputType;
+import android.util.Log;
+import android.view.LayoutInflater;
+import android.view.ViewGroup;
+import android.view.View;
+import android.view.inputmethod.BaseInputConnection;
+import android.view.inputmethod.EditorInfo;
+import android.view.inputmethod.InputConnection;
+import android.widget.FrameLayout;
+import android.widget.TextView;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardActionListener;
+
+public class InputLogicTests extends ServiceTestCase<LatinIME> {
+
+    private static final String PREF_DEBUG_MODE = "debug_mode";
+
+    private LatinIME mLatinIME;
+    private TextView mTextView;
+
+    public InputLogicTests() {
+        super(LatinIME.class);
+    }
+
+    // returns the previous setting value
+    private boolean setDebugMode(final boolean mode) {
+        final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(mLatinIME);
+        final boolean previousDebugSetting = prefs.getBoolean(PREF_DEBUG_MODE, false);
+        final SharedPreferences.Editor editor = prefs.edit();
+        editor.putBoolean(PREF_DEBUG_MODE, true);
+        editor.commit();
+        return previousDebugSetting;
+    }
+
+    @Override
+    protected void setUp() {
+        try {
+            super.setUp();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        mTextView = new TextView(getContext());
+        mTextView.setInputType(InputType.TYPE_CLASS_TEXT);
+        mTextView.setEnabled(true);
+        setupService();
+        mLatinIME = getService();
+        final boolean previousDebugSetting = setDebugMode(true);
+        mLatinIME.onCreate();
+        setDebugMode(previousDebugSetting);
+        final EditorInfo ei = new EditorInfo();
+        final InputConnection ic = mTextView.onCreateInputConnection(ei);
+        final LayoutInflater inflater =
+                (LayoutInflater)getContext().getSystemService(Context.LAYOUT_INFLATER_SERVICE);
+        final ViewGroup vg = new FrameLayout(getContext());
+        final View inputView = inflater.inflate(R.layout.input_view, vg);
+        mLatinIME.setInputView(inputView);
+        mLatinIME.onBindInput();
+        mLatinIME.onCreateInputView();
+        mLatinIME.onStartInputView(ei, false);
+        mLatinIME.onCreateInputMethodInterface().startInput(ic, ei);
+    }
+
+    // type(int) and type(String): helper methods to send a code point resp. a string to LatinIME.
+    private void type(final int codePoint) {
+        // onPressKey and onReleaseKey are explicitly deactivated here, but they do happen in the
+        // code (although multitouch/slide input and other factors make the sequencing complicated).
+        // They are supposed to be entirely deconnected from the input logic from LatinIME point of
+        // view and only delegates to the parts of the code that care. So we don't include them here
+        // to keep these tests as pinpoint as possible and avoid bringing it too many dependencies,
+        // but keep them in mind if something breaks. Commenting them out as is should work.
+        //mLatinIME.onPressKey(codePoint);
+        mLatinIME.onCodeInput(codePoint, new int[] { codePoint },
+                KeyboardActionListener.NOT_A_TOUCH_COORDINATE,
+                KeyboardActionListener.NOT_A_TOUCH_COORDINATE);
+        //mLatinIME.onReleaseKey(codePoint, false);
+    }
+
+    private void type(final String stringToType) {
+        for (int i = 0; i < stringToType.length(); ++i) {
+            type(stringToType.codePointAt(i));
+        }
+    }
+
+    public void testTypeWord() {
+        final String wordToType = "abcd";
+        type(wordToType);
+        assertEquals("type word", wordToType, mTextView.getText().toString());
+    }
+
+    public void testPickSuggestionThenBackspace() {
+        final String wordToType = "tgis";
+        type(wordToType);
+        mLatinIME.pickSuggestionManually(0, wordToType);
+        type(Keyboard.CODE_DELETE);
+        assertEquals("press suggestion then backspace", wordToType, mTextView.getText().toString());
+    }
+
+}