merge in jb-ub-latinimegoogle-azuki history after reset to jb-ub-latinimegoogle
diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml
index 4a8b955..fb973f3 100644
--- a/java/AndroidManifest.xml
+++ b/java/AndroidManifest.xml
@@ -32,7 +32,7 @@
     <uses-permission android:name="android.permission.WRITE_USER_DICTIONARY" />
 
     <application android:label="@string/english_ime_name"
-            android:icon="@mipmap/ic_ime_settings"
+            android:icon="@mipmap/ic_launcher_keyboard"
             android:killAfterRestore="false"
             android:supportsRtl="true">
 
@@ -56,13 +56,23 @@
 
         <activity android:name=".setup.SetupActivity"
                 android:label="@string/english_ime_name"
-                android:icon="@drawable/ic_setup_wizard">
+                android:icon="@mipmap/ic_launcher_keyboard"
+                android:launchMode="singleTask"
+                android:noHistory="true">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
                 <category android:name="android.intent.category.LAUNCHER" />
             </intent-filter>
         </activity>
 
+        <activity android:name=".setup.SetupWizardActivity"
+                android:label="@string/english_ime_name"
+                android:clearTaskOnLaunch="true">
+            <intent-filter>
+                <action android:name="android.intent.action.MAIN" />
+            </intent-filter>
+        </activity>
+
         <receiver android:name=".setup.LauncherIconVisibilityManager">
             <intent-filter>
                 <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
@@ -125,7 +135,6 @@
 
         <activity android:name="com.android.inputmethod.dictionarypack.DictionarySettingsActivity"
                   android:label="@string/dictionary_settings_title"
-                  android:icon="@mipmap/ic_ime_settings"
                   android:theme="@android:style/Theme.Holo"
                   android:uiOptions="splitActionBarWhenNarrow">
             <intent-filter>
@@ -135,7 +144,6 @@
 
         <activity android:name="com.android.inputmethod.dictionarypack.DownloadOverMeteredDialog"
                   android:label="@string/dictionary_install_over_metered_network_prompt"
-                  android:icon="@mipmap/ic_ime_settings"
                   android:theme="@android:style/Theme.Holo">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN"/>
diff --git a/java/res/drawable-hdpi/ic_setup_wizard.png b/java/res/drawable-hdpi/ic_setup_wizard.png
deleted file mode 100644
index 38fca6d..0000000
--- a/java/res/drawable-hdpi/ic_setup_wizard.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-mdpi/ic_setup_wizard.png b/java/res/drawable-mdpi/ic_setup_wizard.png
deleted file mode 100644
index 66e62b8..0000000
--- a/java/res/drawable-mdpi/ic_setup_wizard.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-xhdpi/ic_setup_wizard.png b/java/res/drawable-xhdpi/ic_setup_wizard.png
deleted file mode 100644
index 53f70a6..0000000
--- a/java/res/drawable-xhdpi/ic_setup_wizard.png
+++ /dev/null
Binary files differ
diff --git a/java/res/drawable-xxhdpi/ic_setup_wizard.png b/java/res/drawable-xxhdpi/ic_setup_wizard.png
deleted file mode 100644
index 6414b4f..0000000
--- a/java/res/drawable-xxhdpi/ic_setup_wizard.png
+++ /dev/null
Binary files differ
diff --git a/java/res/layout-land/setup_steps_screen.xml b/java/res/layout-land/setup_steps_screen.xml
index 0b4a096..cf8c424 100644
--- a/java/res/layout-land/setup_steps_screen.xml
+++ b/java/res/layout-land/setup_steps_screen.xml
@@ -27,7 +27,9 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="@integer/setup_title_weight_in_screen"
-        android:orientation="vertical">
+        android:orientation="vertical"
+        android:layout_marginEnd="@dimen/setup_title_end_margin"
+        android:layout_marginRight="@dimen/setup_title_end_margin">
         <include layout="@layout/setup_steps_title" />
     </LinearLayout>
     <LinearLayout
diff --git a/java/res/layout-land/setup_welcome_screen.xml b/java/res/layout-land/setup_welcome_screen.xml
index 8b162e2..38aea2c 100644
--- a/java/res/layout-land/setup_welcome_screen.xml
+++ b/java/res/layout-land/setup_welcome_screen.xml
@@ -27,7 +27,9 @@
         android:layout_width="0dp"
         android:layout_height="match_parent"
         android:layout_weight="@integer/setup_title_weight_in_screen"
-        android:orientation="vertical">
+        android:orientation="vertical"
+        android:layout_marginEnd="@dimen/setup_title_end_margin"
+        android:layout_marginRight="@dimen/setup_title_end_margin">
         <include layout="@layout/setup_welcome_title" />
     </LinearLayout>
     <LinearLayout
diff --git a/java/res/layout/setup_welcome_video.xml b/java/res/layout/setup_welcome_video.xml
index 8c04e63..c39411e 100644
--- a/java/res/layout/setup_welcome_video.xml
+++ b/java/res/layout/setup_welcome_video.xml
@@ -25,24 +25,27 @@
         android:orientation="horizontal"
         android:paddingTop="@dimen/setup_welcome_video_vertical_margin"
         android:paddingBottom="@dimen/setup_welcome_video_vertical_margin">
-        <View
-            android:layout_weight="@integer/setup_welcome_video_left_padding_weight_in_screen"
-            android:layout_width="0dp"
-            android:layout_height="0dp" />
-        <VideoView
-            android:id="@+id/setup_welcome_video"
-            android:background="@color/setup_background"
+        <LinearLayout
+            android:id="@+id/setup_welcome_video_frame"
             android:layout_weight="@integer/setup_welcome_video_weight_in_screen"
             android:layout_width="0dp"
-            android:layout_height="wrap_content" />
-        <ImageView
-            android:id="@+id/setup_welcome_image"
-            android:visibility="gone"
-            android:layout_weight="@integer/setup_welcome_video_weight_in_screen"
-            android:layout_width="0dp"
-            android:layout_height="wrap_content" />
+            android:layout_height="wrap_content"
+            android:orientation="horizontal"
+            android:padding="1dp"
+            android:background="@color/setup_welcome_video_margin_color" >
+            <VideoView
+                android:id="@+id/setup_welcome_video"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:background="@color/setup_background" />
+            <ImageView
+                android:id="@+id/setup_welcome_image"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:visibility="gone" />
+        </LinearLayout>
         <View
-            android:layout_weight="@integer/setup_welcome_video_right_padding_weight_in_screen"
+            android:layout_weight="@integer/setup_welcome_video_end_padding_weight_in_screen"
             android:layout_width="0dp"
             android:layout_height="0dp" />
     </LinearLayout>
diff --git a/java/res/layout/setup_wizard.xml b/java/res/layout/setup_wizard.xml
index 176f836..87db4d0 100644
--- a/java/res/layout/setup_wizard.xml
+++ b/java/res/layout/setup_wizard.xml
@@ -19,6 +19,7 @@
 -->
 
 <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/setup_wizard"
     android:layout_width="match_parent"
     android:layout_height="match_parent"
     android:background="@color/setup_background"
diff --git a/java/res/layout/user_dictionary_add_word_fullscreen.xml b/java/res/layout/user_dictionary_add_word_fullscreen.xml
index 75e86c5..219485b 100644
--- a/java/res/layout/user_dictionary_add_word_fullscreen.xml
+++ b/java/res/layout/user_dictionary_add_word_fullscreen.xml
@@ -19,12 +19,6 @@
     android:layout_height="wrap_content"
     android:orientation="vertical" >
 
-    <TextView
-        style="?android:attr/listSeparatorTextViewStyle"
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:text="@string/user_dict_settings_add_screen_title" />
-
     <EditText
         android:id="@+id/user_dictionary_add_word_text"
         android:layout_width="match_parent"
diff --git a/java/res/mipmap-hdpi/ic_ime_settings.png b/java/res/mipmap-hdpi/ic_ime_settings.png
deleted file mode 100644
index 486c70d..0000000
--- a/java/res/mipmap-hdpi/ic_ime_settings.png
+++ /dev/null
Binary files differ
diff --git a/java/res/mipmap-hdpi/ic_launcher_keyboard.png b/java/res/mipmap-hdpi/ic_launcher_keyboard.png
new file mode 100644
index 0000000..36b1cca
--- /dev/null
+++ b/java/res/mipmap-hdpi/ic_launcher_keyboard.png
Binary files differ
diff --git a/java/res/mipmap-mdpi/ic_ime_settings.png b/java/res/mipmap-mdpi/ic_ime_settings.png
deleted file mode 100644
index 75f4afb..0000000
--- a/java/res/mipmap-mdpi/ic_ime_settings.png
+++ /dev/null
Binary files differ
diff --git a/java/res/mipmap-mdpi/ic_launcher_keyboard.png b/java/res/mipmap-mdpi/ic_launcher_keyboard.png
new file mode 100644
index 0000000..67ef189
--- /dev/null
+++ b/java/res/mipmap-mdpi/ic_launcher_keyboard.png
Binary files differ
diff --git a/java/res/mipmap-xhdpi/ic_ime_settings.png b/java/res/mipmap-xhdpi/ic_ime_settings.png
deleted file mode 100644
index bbf1919..0000000
--- a/java/res/mipmap-xhdpi/ic_ime_settings.png
+++ /dev/null
Binary files differ
diff --git a/java/res/mipmap-xhdpi/ic_launcher_keyboard.png b/java/res/mipmap-xhdpi/ic_launcher_keyboard.png
new file mode 100644
index 0000000..b332083
--- /dev/null
+++ b/java/res/mipmap-xhdpi/ic_launcher_keyboard.png
Binary files differ
diff --git a/java/res/mipmap-xxhdpi/ic_ime_settings.png b/java/res/mipmap-xxhdpi/ic_ime_settings.png
deleted file mode 100644
index 16fc693..0000000
--- a/java/res/mipmap-xxhdpi/ic_ime_settings.png
+++ /dev/null
Binary files differ
diff --git a/java/res/mipmap-xxhdpi/ic_launcher_keyboard.png b/java/res/mipmap-xxhdpi/ic_launcher_keyboard.png
new file mode 100644
index 0000000..acc424f
--- /dev/null
+++ b/java/res/mipmap-xxhdpi/ic_launcher_keyboard.png
Binary files differ
diff --git a/java/res/raw/setup_welcome_image.png b/java/res/raw/setup_welcome_image.png
index 17e3111..db5dc34 100644
--- a/java/res/raw/setup_welcome_image.png
+++ b/java/res/raw/setup_welcome_image.png
Binary files differ
diff --git a/java/res/raw/setup_welcome_video.mp4 b/java/res/raw/setup_welcome_video.mp4
index 09357d8..8208525 100644
--- a/java/res/raw/setup_welcome_video.mp4
+++ b/java/res/raw/setup_welcome_video.mp4
Binary files differ
diff --git a/java/res/values-h1200dp-port/setup-dimens-large-tablet-port.xml b/java/res/values-h1200dp-port/setup-dimens-large-tablet-port.xml
index ad61c1a..d1df81e 100644
--- a/java/res/values-h1200dp-port/setup-dimens-large-tablet-port.xml
+++ b/java/res/values-h1200dp-port/setup-dimens-large-tablet-port.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">50</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">25</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">25</integer>
+    <integer name="setup_welcome_video_weight_in_screen">70</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">30</integer>
 </resources>
diff --git a/java/res/values-h330dp-land/setup-dimens-large-phone-land.xml b/java/res/values-h330dp-land/setup-dimens-large-phone-land.xml
index 69a8a7a..babc05b 100644
--- a/java/res/values-h330dp-land/setup-dimens-large-phone-land.xml
+++ b/java/res/values-h330dp-land/setup-dimens-large-phone-land.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">80</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">10</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">10</integer>
+    <integer name="setup_welcome_video_weight_in_screen">70</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">30</integer>
 </resources>
diff --git a/java/res/values-h520dp-land/setup-dimens-small-tablet-land.xml b/java/res/values-h520dp-land/setup-dimens-small-tablet-land.xml
index 6a14d59..7725874 100644
--- a/java/res/values-h520dp-land/setup-dimens-small-tablet-land.xml
+++ b/java/res/values-h520dp-land/setup-dimens-small-tablet-land.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
     <integer name="setup_welcome_video_weight_in_screen">60</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">20</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">20</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">40</integer>
 </resources>
diff --git a/java/res/values-h540dp-port/setup-dimens-large-phone-port.xml b/java/res/values-h540dp-port/setup-dimens-large-phone-port.xml
index b8dd33d..42d2284 100644
--- a/java/res/values-h540dp-port/setup-dimens-large-phone-port.xml
+++ b/java/res/values-h540dp-port/setup-dimens-large-phone-port.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">80</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">10</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">10</integer>
+    <integer name="setup_welcome_video_weight_in_screen">70</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">30</integer>
 </resources>
diff --git a/java/res/values-h720dp-land/setup-dimens-large-tablet-land.xml b/java/res/values-h720dp-land/setup-dimens-large-tablet-land.xml
index 0004a3c..b5f0e5c 100644
--- a/java/res/values-h720dp-land/setup-dimens-large-tablet-land.xml
+++ b/java/res/values-h720dp-land/setup-dimens-large-tablet-land.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">50</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">25</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">25</integer>
+    <integer name="setup_welcome_video_weight_in_screen">70</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">30</integer>
 </resources>
diff --git a/java/res/values-h800dp-port/setup-dimens-small-tablet-port.xml b/java/res/values-h800dp-port/setup-dimens-small-tablet-port.xml
index 87c991c..770b4e8 100644
--- a/java/res/values-h800dp-port/setup-dimens-small-tablet-port.xml
+++ b/java/res/values-h800dp-port/setup-dimens-small-tablet-port.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">24dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
     <integer name="setup_welcome_video_weight_in_screen">60</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">20</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">20</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">40</integer>
 </resources>
diff --git a/java/res/values-land/keyboard-heights.xml b/java/res/values-land/keyboard-heights.xml
index 4ebeda5..670be33 100644
--- a/java/res/values-land/keyboard-heights.xml
+++ b/java/res/values-land/keyboard-heights.xml
@@ -19,19 +19,21 @@
 -->
 
 <resources>
-    <!-- Build.HARDWARE,keyboard_height_in_dp -->
+    <!-- Build condition,keyboard_height_in_dp -->
     <string-array name="keyboard_heights" translatable="false">
     <!-- Preferable keyboard height in absolute scale: 1.100in -->
         <!-- Droid -->
-        <item>sholes,194.3333</item>
+        <item>HARDWARE=sholes,194.3333</item>
         <!-- Nexus One -->
-        <item>mahimahi,186.2667</item>
+        <item>HARDWARE=mahimahi,186.2667</item>
         <!-- Nexus S -->
-        <item>herring,171.9385</item>
+        <item>HARDWARE=herring,171.9385</item>
         <!-- Galaxy Nexus -->
-        <item>tuna,173.4207</item>
+        <item>HARDWARE=tuna,173.4207</item>
     <!-- Preferable keyboard height in absolute scale: 45.0mm -->
         <!-- Xoom -->
-        <item>stingray,265.4378</item>
+        <item>HARDWARE=stingray,265.4378</item>
+    <!-- Default value for unknown device: empty string -->
+        <item>,</item>
     </string-array>
 </resources>
diff --git a/java/res/values-land/setup-dimens-small-phone-land.xml b/java/res/values-land/setup-dimens-small-phone-land.xml
index 63f4661..e6dbaa7 100644
--- a/java/res/values-land/setup-dimens-small-phone-land.xml
+++ b/java/res/values-land/setup-dimens-small-phone-land.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
-    <dimen name="setup_welcome_video_vertical_margin">24dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">80</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">10</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">10</integer>
+    <dimen name="setup_title_end_margin">12dp</dimen>
+    <dimen name="setup_welcome_video_vertical_margin">12dp</dimen>
+    <integer name="setup_welcome_video_weight_in_screen">60</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">40</integer>
 </resources>
diff --git a/java/res/values-port/setup-dimens-small-phone-port.xml b/java/res/values-port/setup-dimens-small-phone-port.xml
index 34f4d92..a3b18af 100644
--- a/java/res/values-port/setup-dimens-small-phone-port.xml
+++ b/java/res/values-port/setup-dimens-small-phone-port.xml
@@ -33,8 +33,8 @@
     <dimen name="setup_step_horizontal_line_height">2dp</dimen>
     <integer name="setup_title_weight_in_screen">40</integer>
     <integer name="setup_body_weight_in_screen">60</integer>
+    <dimen name="setup_title_end_margin">16dp</dimen>
     <dimen name="setup_welcome_video_vertical_margin">16dp</dimen>
-    <integer name="setup_welcome_video_weight_in_screen">80</integer>
-    <integer name="setup_welcome_video_left_padding_weight_in_screen">10</integer>
-    <integer name="setup_welcome_video_right_padding_weight_in_screen">10</integer>
+    <integer name="setup_welcome_video_weight_in_screen">70</integer>
+    <integer name="setup_welcome_video_end_padding_weight_in_screen">30</integer>
 </resources>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index 586fbe6..75b476c 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -85,8 +85,8 @@
     <dimen name="suggestion_text_size">22dp</dimen>
     <dimen name="more_suggestions_hint_text_size">33dp</dimen>
 
-    <!-- Gesture preview trail parameters -->
-    <dimen name="gesture_preview_trail_width">2.5dp</dimen>
+    <!-- Gesture trail parameters -->
+    <dimen name="gesture_trail_width">2.5dp</dimen>
     <!-- Gesture floating preview text parameters -->
     <dimen name="gesture_floating_preview_text_size">28dp</dimen>
     <dimen name="gesture_floating_preview_text_offset">87dp</dimen>
diff --git a/java/res/values-sw768dp/dimens.xml b/java/res/values-sw768dp/dimens.xml
index 2fd7322..91251f5 100644
--- a/java/res/values-sw768dp/dimens.xml
+++ b/java/res/values-sw768dp/dimens.xml
@@ -86,8 +86,8 @@
     <dimen name="suggestion_text_size">22dp</dimen>
     <dimen name="more_suggestions_hint_text_size">33dp</dimen>
 
-    <!-- Gesture preview trail parameters -->
-    <dimen name="gesture_preview_trail_width">2.5dp</dimen>
+    <!-- Gesture trail parameters -->
+    <dimen name="gesture_trail_width">2.5dp</dimen>
     <!-- Gesture floating preview text parameters -->
     <dimen name="gesture_floating_preview_text_size">26dp</dimen>
     <dimen name="gesture_floating_preview_text_offset">86dp</dimen>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index a71e7cc..478a5c0 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -108,17 +108,25 @@
         <attr name="backgroundDimAlpha" format="integer" />
         <!-- More keys keyboard will shown at touched point. -->
         <attr name="showMoreKeysKeyboardAtTouchedPoint" format="boolean" />
+        <!-- Minimum distance between gesture trail sampling points. -->
+        <attr name="gestureTrailMinSamplingDistance" format="dimension" />
+        <!-- Maximum angular threshold between gesture trail interpolation segments in degree. -->
+        <attr name="gestureTrailMaxInterpolationAngularThreshold" format="integer" />
+        <!-- Maximum distance threshold between gesture trail interpolation segments. -->
+        <attr name="gestureTrailMaxInterpolationDistanceThreshold" format="dimension" />
+        <!-- Maximum number of gesture trail interpolation segments. -->
+        <attr name="gestureTrailMaxInterpolationSegments" format="integer" />
         <!-- Delay after gesture trail starts fading out in millisecond. -->
-        <attr name="gesturePreviewTrailFadeoutStartDelay" format="integer" />
-        <!-- Duration while gesture preview trail is fading out in millisecond. -->
-        <attr name="gesturePreviewTrailFadeoutDuration" format="integer" />
-        <!-- Interval of updating gesture preview trail in millisecond. -->
-        <attr name="gesturePreviewTrailUpdateInterval" format="integer" />
-        <attr name="gesturePreviewTrailColor" format="color" />
-        <attr name="gesturePreviewTrailStartWidth" format="dimension" />
-        <attr name="gesturePreviewTrailEndWidth" format="dimension" />
-        <attr name="gesturePreviewTrailBodyRatio" format="integer" />
-        <attr name="gesturePreviewTrailShadowRatio" format="integer" />
+        <attr name="gestureTrailFadeoutStartDelay" format="integer" />
+        <!-- Duration while gesture trail is fading out in millisecond. -->
+        <attr name="gestureTrailFadeoutDuration" format="integer" />
+        <!-- Interval of updating gesture trail in millisecond. -->
+        <attr name="gestureTrailUpdateInterval" format="integer" />
+        <attr name="gestureTrailColor" format="color" />
+        <attr name="gestureTrailStartWidth" format="dimension" />
+        <attr name="gestureTrailEndWidth" format="dimension" />
+        <attr name="gestureTrailBodyRatio" format="integer" />
+        <attr name="gestureTrailShadowRatio" format="integer" />
         <!-- Delay after gesture input and gesture floating preview text dismissing in millisecond -->
         <attr name="gestureFloatingPreviewTextLingerTimeout" format="integer" />
         <!-- Attributes for GestureFloatingPreviewText -->
diff --git a/java/res/values/colors.xml b/java/res/values/colors.xml
index 8a8049f..daa167c 100644
--- a/java/res/values/colors.xml
+++ b/java/res/values/colors.xml
@@ -58,4 +58,5 @@
     <color name="setup_text_dark">#FF707070</color>
     <color name="setup_text_action">@android:color/holo_blue_light</color>
     <color name="setup_step_background">@android:color/background_light</color>
+    <color name="setup_welcome_video_margin_color">#FFCCCCCC</color>
 </resources>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 23b5794..d3a21f2 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -31,7 +31,7 @@
     <bool name="config_default_next_word_prediction">true</bool>
     <bool name="config_default_sound_enabled">false</bool>
     <bool name="config_default_vibration_enabled">true</bool>
-    <integer name="config_max_vibration_duration">250</integer> <!-- milliseconds -->
+    <integer name="config_max_vibration_duration">100</integer> <!-- milliseconds -->
     <integer name="config_delay_update_suggestions">100</integer>
     <integer name="config_delay_update_old_suggestions">300</integer>
     <integer name="config_delay_update_shift_state">100</integer>
@@ -75,9 +75,9 @@
     <bool name="config_show_more_keys_keyboard_at_touched_point">false</bool>
     <bool name="config_block_potentially_offensive">true</bool>
     <integer name="config_gesture_floating_preview_text_linger_timeout">200</integer>
-    <integer name="config_gesture_preview_trail_fadeout_start_delay">100</integer>
-    <integer name="config_gesture_preview_trail_fadeout_duration">800</integer>
-    <integer name="config_gesture_preview_trail_update_interval">20</integer>
+    <integer name="config_gesture_trail_fadeout_start_delay">100</integer>
+    <integer name="config_gesture_trail_fadeout_duration">800</integer>
+    <integer name="config_gesture_trail_update_interval">20</integer>
     <!-- Static threshold for gesture after fast typing (msec) -->
     <integer name="config_gesture_static_time_threshold_after_fast_typing">500</integer>
     <!-- Static threshold for starting gesture detection (keyWidth%/sec) -->
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index da735cf..98ae76c 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -100,14 +100,22 @@
     <integer name="suggestions_count_in_strip">3</integer>
     <fraction name="center_suggestion_percentile">36%</fraction>
 
-    <!-- Gesture preview trail parameters -->
-    <dimen name="gesture_preview_trail_start_width">10.0dp</dimen>
-    <dimen name="gesture_preview_trail_end_width">2.5dp</dimen>
+    <!-- Gesture trail parameters -->
+    <!-- Minimum distance between gesture trail sampling points. -->
+    <dimen name="gesture_trail_min_sampling_distance">9.6dp</dimen>
+    <!-- Maximum angular threshold between gesture trails interpolation segments in degree. -->
+    <integer name="gesture_trail_max_interpolation_angular_threshold">15</integer>
+    <!-- Maximum distance threshold between gesture trails interpolation segments. -->
+    <dimen name="gesture_trail_max_interpolation_distance_threshold">16.0dp</dimen>
+    <!-- Maximum number of gesture trail interpolation segments. -->
+    <integer name="gesture_trail_max_interpolation_segments">6</integer>
+    <dimen name="gesture_trail_start_width">10.0dp</dimen>
+    <dimen name="gesture_trail_end_width">2.5dp</dimen>
     <!-- Percentages of gesture preview taril body and shadow, in proportion to the trail width.
          A negative value of the shadow ratio disables drawing shadow. -->
     <!-- TODO: May use the shadow to alleviate rugged trail drawing. -->
-    <integer name="gesture_preview_trail_body_ratio">100</integer>
-    <integer name="gesture_preview_trail_shadow_ratio">-1</integer>
+    <integer name="gesture_trail_body_ratio">100</integer>
+    <integer name="gesture_trail_shadow_ratio">-1</integer>
     <!-- Gesture floating preview text parameters -->
     <dimen name="gesture_floating_preview_text_size">24dp</dimen>
     <dimen name="gesture_floating_preview_text_offset">73dp</dimen>
diff --git a/java/res/values/keyboard-heights.xml b/java/res/values/keyboard-heights.xml
index 418d3e5..c651a89 100644
--- a/java/res/values/keyboard-heights.xml
+++ b/java/res/values/keyboard-heights.xml
@@ -19,21 +19,21 @@
 -->
 
 <resources>
-    <!-- Build.HARDWARE,keyboard_height_in_dp -->
+    <!-- Build condition,keyboard_height_in_dp -->
     <string-array name="keyboard_heights" translatable="false">
     <!-- Preferable keyboard height in absolute scale: 1.285in -->
         <!-- Droid -->
-        <item>sholes,227.0167</item>
+        <item>HARDWARE=sholes,227.0167</item>
         <!-- Nexus One -->
-        <item>mahimahi,217.5932</item>
+        <item>HARDWARE=mahimahi,217.5932</item>
         <!-- Nexus S -->
-        <item>herring,200.8554</item>
+        <item>HARDWARE=herring,200.8554</item>
         <!-- Galaxy Nexus -->
-        <item>tuna,202.5869</item>
+        <item>HARDWARE=tuna,202.5869</item>
     <!-- Preferable keyboard height in absolute scale: 48.0mm -->
         <!-- Xoom -->
-        <item>stingray,283.1337</item>
+        <item>HARDWARE=stingray,283.1337</item>
     <!-- Default value for unknown device: empty string -->
-        <item>DEFAULT,</item>
+        <item>,</item>
     </string-array>
 </resources>
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 10400be..9ce5051 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -18,17 +18,36 @@
 */
 -->
 <resources>
-    <!-- Build.HARDWARE,duration_in_milliseconds -->
+    <!-- Build condition,duration_in_milliseconds -->
     <string-array name="keypress_vibration_durations" translatable="false">
         <!-- Nexus S -->
-        <item>herring,5</item>
+        <item>MODEL=Nexus S:BRAND=google,5</item>
         <!-- Galaxy Nexus -->
-        <item>tuna,5</item>
+        <item>MODEL=Galaxy Nexus:BRAND=google,5</item>
         <!-- Nexus 4 -->
-        <item>mako,5</item>
+        <item>MODEL=Nexus 4:BRAND=google,8</item>
         <!-- Nexus 10 -->
-        <item>manta,16</item>
+        <item>MODEL=Nexus 10:BRAND=google,16</item>
+        <!-- Samsung Galaxy SII -->
+        <item>MODEL=GT-I(9100[GMPT]?|9108|9210T?):MANUFACTURER=samsung,8</item>
+        <item>MODEL=SGH-(I9[27]7R?|I927|T989D?):MANUFACTURER=samsung,8</item>
+        <item>MODEL=SHW-M250[KLS]?|SPH-D710|SCH-R760:MANUFACTURER=samsung,8</item>
+        <item>MODEL=ISW11SC|SC-02C:MANUFACTURER=samsung,8</item>
+        <!-- Samsung Galaxy SIII -->
+        <item>MODEL=(SAMSUNG-)?GT-I(930[05][NT]?|9308):MANUFACTURER=samsung,8</item>
+        <item>MODEL=(SAMSUNG-)?SGH-(T999[V]?|I747[M]?|N064|N035):MANUFACTURER=samsung,8</item>
+        <item>MODEL=(SAMSUNG-)?SCH-(J021|R530|I535|I939):MANUFACTURER=samsung,8</item>
+        <item>MODEL=(SAMSUNG-)?(SCL21|SC-06D|SC-03E]):MANUFACTURER=samsung,8</item>
+        <item>MODEL=(SAMSUNG-)?(SHV-210[KLS]?|SPH-L710):MANUFACTURER=samsung,8</item>
+        <!-- LG Optimus G -->
+        <item>MODEL=LG-E97[013]|LS970|L-01E:MANUFACTURER=LGE,15</item>
+        <!-- HTC One X -->
+        <item>MODEL=HTC One X:MANUFACTURER=HTC,20</item>
+        <!-- Motorola Razor M -->
+        <item>MODEL=XT907:MANUFACTURER=motorola,30</item>
+        <!-- Sony Xperia Z -->
+        <item>MODEL=C6603:MANUFACTURER=Sony,35</item>
         <!-- Default value for unknown device -->
-        <item>DEFAULT,20</item>
+        <item>,20</item>
     </string-array>
 </resources>
diff --git a/java/res/values/keypress-volumes.xml b/java/res/values/keypress-volumes.xml
index 047fe0c..a096c34 100644
--- a/java/res/values/keypress-volumes.xml
+++ b/java/res/values/keypress-volumes.xml
@@ -18,15 +18,15 @@
 */
 -->
 <resources>
+    <!-- Build condition,volume -->
     <string-array name="keypress_volumes" translatable="false">
-        <!-- Build.HARDWARE,volume -->
-        <item>herring,0.5f</item>
-        <item>tuna,0.5f</item>
-        <item>stingray,0.4f</item>
-        <item>grouper,0.3f</item>
-        <item>mako,0.3f</item>
-        <item>manta,0.2f</item>
+        <item>HARDWARE=herring,0.5f</item>
+        <item>HARDWARE=tuna,0.5f</item>
+        <item>HARDWARE=stingray,0.4f</item>
+        <item>HARDWARE=grouper,0.3f</item>
+        <item>HARDWARE=mako,0.3f</item>
+        <item>HARDWARE=manta,0.2f</item>
         <!-- Default value for unknown device -->
-        <item>DEFAULT,0.2f</item>
+        <item>,0.2f</item>
     </string-array>
 </resources>
diff --git a/java/res/values/phantom-sudden-move-event-device-list.xml b/java/res/values/phantom-sudden-move-event-device-list.xml
index 22f5102..53002b3 100644
--- a/java/res/values/phantom-sudden-move-event-device-list.xml
+++ b/java/res/values/phantom-sudden-move-event-device-list.xml
@@ -19,11 +19,11 @@
 -->
 <resources>
     <string-array name="phantom_sudden_move_event_device_list" translatable="false">
-        <!-- "Build.HARDWARE,true" that needs "phantom sudden move event" hack.
+        <!-- "Build condition,true" that needs "phantom sudden move event" hack.
              See {@link com.android.inputmethod.keyboard.PointerTracker}. -->
         <!-- Xoom -->
-        <item>stingray,true</item>
+        <item>HARDWARE=stingray,true</item>
         <!-- Default value for unknown device -->
-        <item>DEFAULT,false</item>
+        <item>,false</item>
     </string-array>
 </resources>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index dad7e20..8b6c29e 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -64,14 +64,18 @@
         <item name="gestureFloatingPreviewHorizontalPadding">@dimen/gesture_floating_preview_horizontal_padding</item>
         <item name="gestureFloatingPreviewVerticalPadding">@dimen/gesture_floating_preview_vertical_padding</item>
         <item name="gestureFloatingPreviewRoundRadius">@dimen/gesture_floating_preview_round_radius</item>
-        <item name="gesturePreviewTrailFadeoutStartDelay">@integer/config_gesture_preview_trail_fadeout_start_delay</item>
-        <item name="gesturePreviewTrailFadeoutDuration">@integer/config_gesture_preview_trail_fadeout_duration</item>
-        <item name="gesturePreviewTrailUpdateInterval">@integer/config_gesture_preview_trail_update_interval</item>
-        <item name="gesturePreviewTrailColor">@color/highlight_color_default</item>
-        <item name="gesturePreviewTrailStartWidth">@dimen/gesture_preview_trail_start_width</item>
-        <item name="gesturePreviewTrailEndWidth">@dimen/gesture_preview_trail_end_width</item>
-        <item name="gesturePreviewTrailBodyRatio">@integer/gesture_preview_trail_body_ratio</item>
-        <item name="gesturePreviewTrailShadowRatio">@integer/gesture_preview_trail_shadow_ratio</item>
+        <item name="gestureTrailMinSamplingDistance">@dimen/gesture_trail_min_sampling_distance</item>
+        <item name="gestureTrailMaxInterpolationAngularThreshold">@integer/gesture_trail_max_interpolation_angular_threshold</item>
+        <item name="gestureTrailMaxInterpolationDistanceThreshold">@dimen/gesture_trail_max_interpolation_distance_threshold</item>
+        <item name="gestureTrailMaxInterpolationSegments">@integer/gesture_trail_max_interpolation_segments</item>
+        <item name="gestureTrailFadeoutStartDelay">@integer/config_gesture_trail_fadeout_start_delay</item>
+        <item name="gestureTrailFadeoutDuration">@integer/config_gesture_trail_fadeout_duration</item>
+        <item name="gestureTrailUpdateInterval">@integer/config_gesture_trail_update_interval</item>
+        <item name="gestureTrailColor">@color/highlight_color_default</item>
+        <item name="gestureTrailStartWidth">@dimen/gesture_trail_start_width</item>
+        <item name="gestureTrailEndWidth">@dimen/gesture_trail_end_width</item>
+        <item name="gestureTrailBodyRatio">@integer/gesture_trail_body_ratio</item>
+        <item name="gestureTrailShadowRatio">@integer/gesture_trail_shadow_ratio</item>
         <!-- Common attributes of MainKeyboardView -->
         <item name="keyHysteresisDistance">@dimen/config_key_hysteresis_distance</item>
         <item name="keyHysteresisDistanceForSlidingModifier">@dimen/config_key_hysteresis_distance_for_sliding_modifier</item>
@@ -344,7 +348,7 @@
         <item name="keyTextShadowRadius">0.0</item>
         <item name="slidingKeyInputPreviewColor">@color/highlight_translucent_color_ics</item>
         <item name="gestureFloatingPreviewTextColor">@color/highlight_color_ics</item>
-        <item name="gesturePreviewTrailColor">@color/highlight_color_ics</item>
+        <item name="gestureTrailColor">@color/highlight_color_ics</item>
     </style>
     <style
         name="MainKeyboardView.IceCreamSandwich"
diff --git a/java/res/values/sudden-jumping-touch-event-device-list.xml b/java/res/values/sudden-jumping-touch-event-device-list.xml
index 3fdc0c7..3a9c379 100644
--- a/java/res/values/sudden-jumping-touch-event-device-list.xml
+++ b/java/res/values/sudden-jumping-touch-event-device-list.xml
@@ -19,13 +19,13 @@
 -->
 <resources>
     <string-array name="sudden_jumping_touch_event_device_list" translatable="false">
-        <!-- "Build.HARDWARE,true" that needs "sudden jump touch event" hack.
+        <!-- "Build condition,true" that needs "sudden jump touch event" hack.
              See {@link com.android.inputmethod.keyboard.SuddenJumpingTouchEventHandler}. -->
         <!-- Nexus One -->
-        <item>mahimahi,true</item>
+        <item>HARDWARE=mahimahi,true</item>
         <!-- Droid -->
-        <item>sholes,true</item>
+        <item>HARDWARE=sholes,true</item>
         <!-- Default value for unknown device -->
-        <item>DEFAULT,false</item>
+        <item>,false</item>
     </string-array>
 </resources>
diff --git a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
index fb75d6d..6183223 100644
--- a/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
+++ b/java/src/com/android/inputmethod/dictionarypack/DictionarySettingsFragment.java
@@ -66,6 +66,8 @@
     private boolean mChangedSettings;
     private DictionaryListInterfaceState mDictionaryListInterfaceState =
             new DictionaryListInterfaceState();
+    private TreeMap<String, WordListPreference> mCurrentPreferenceMap =
+            new TreeMap<String, WordListPreference>(); // never null
 
     private final BroadcastReceiver mConnectivityChangedReceiver = new BroadcastReceiver() {
             @Override
@@ -278,7 +280,7 @@
             return result;
         } else {
             final String systemLocaleString = Locale.getDefault().toString();
-            final TreeMap<String, WordListPreference> prefList =
+            final TreeMap<String, WordListPreference> prefMap =
                     new TreeMap<String, WordListPreference>();
             final int idIndex = cursor.getColumnIndex(MetadataDbHelper.WORDLISTID_COLUMN);
             final int versionIndex = cursor.getColumnIndex(MetadataDbHelper.VERSION_COLUMN);
@@ -299,16 +301,31 @@
                 // The key is sorted in lexicographic order, according to the match level, then
                 // the description.
                 final String key = matchLevelString + "." + description + "." + wordlistId;
-                final WordListPreference existingPref = prefList.get(key);
+                final WordListPreference existingPref = prefMap.get(key);
                 if (null == existingPref || hasPriority(status, existingPref.mStatus)) {
-                    final WordListPreference pref = new WordListPreference(activity,
-                            mDictionaryListInterfaceState, mClientId, wordlistId, version, locale,
-                            description, status, filesize);
-                    prefList.put(key, pref);
+                    final WordListPreference oldPreference = mCurrentPreferenceMap.get(key);
+                    final WordListPreference pref;
+                    if (null != oldPreference
+                            && oldPreference.mVersion == version
+                            && oldPreference.mLocale.equals(locale)) {
+                        // If the old preference has all the new attributes, reuse it. We test
+                        // for version and locale because although attributes other than status
+                        // need to be the same, others have been tested through the key of the
+                        // map. Also, status may differ so we don't want to use #equals() here.
+                        pref = oldPreference;
+                        pref.mStatus = status;
+                    } else {
+                        // Otherwise, discard it and create a new one instead.
+                        pref = new WordListPreference(activity, mDictionaryListInterfaceState,
+                                mClientId, wordlistId, version, locale, description, status,
+                                filesize);
+                    }
+                    prefMap.put(key, pref);
                 }
             } while (cursor.moveToNext());
             cursor.close();
-            return prefList.values();
+            mCurrentPreferenceMap = prefMap;
+            return prefMap.values();
         }
     }
 
diff --git a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
index 29015d6..451a0fb 100644
--- a/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
+++ b/java/src/com/android/inputmethod/dictionarypack/WordListPreference.java
@@ -58,6 +58,8 @@
     // The metadata word list id and version of this word list.
     public final String mWordlistId;
     public final int mVersion;
+    public final Locale mLocale;
+    public final String mDescription;
     // The status
     public int mStatus;
     // The size of the dictionary file
@@ -80,6 +82,8 @@
         mVersion = version;
         mWordlistId = wordlistId;
         mFilesize = filesize;
+        mLocale = locale;
+        mDescription = description;
 
         setLayoutResource(R.layout.dictionary_line);
 
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index d160038..1550e77 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -105,7 +105,7 @@
     /** Hit bounding box of the key */
     public final Rect mHitBox = new Rect();
 
-    /** More keys */
+    /** More keys. It is guaranteed that this is null or an array of one or more elements */
     public final MoreKeySpec[] mMoreKeys;
     /** More keys column number and flags */
     private final int mMoreKeysColumnAndFlags;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
index 60d09d6..9eeee5b 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardActionListener.java
@@ -27,8 +27,9 @@
      *
      * @param primaryCode the unicode of the key being pressed. If the touch is not on a valid key,
      *            the value will be zero.
+     * @param isSinglePointer true if pressing has occurred while no other key is being pressed.
      */
-    public void onPressKey(int primaryCode);
+    public void onPressKey(int primaryCode, boolean isSinglePointer);
 
     /**
      * Called when the user releases a key. This is sent after the {@link #onCodeInput} is called.
@@ -88,6 +89,11 @@
     public void onCancelInput();
 
     /**
+     * Called when user finished sliding key input.
+     */
+    public void onFinishSlidingInput();
+
+    /**
      * Send a non-"code input" custom request to the listener.
      * @return true if the request has been consumed, false otherwise.
      */
@@ -97,7 +103,7 @@
         public static final Adapter EMPTY_LISTENER = new Adapter();
 
         @Override
-        public void onPressKey(int primaryCode) {}
+        public void onPressKey(int primaryCode, boolean isSinglePointer) {}
         @Override
         public void onReleaseKey(int primaryCode, boolean withSliding) {}
         @Override
@@ -115,6 +121,8 @@
         @Override
         public void onCancelInput() {}
         @Override
+        public void onFinishSlidingInput() {}
+        @Override
         public boolean onCustomRequest(int requestCode) {
             return false;
         }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index bc9e8cd..1fe23a3 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -29,17 +29,18 @@
 import android.content.res.XmlResourceParser;
 import android.text.InputType;
 import android.text.TextUtils;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.Xml;
 import android.view.inputmethod.EditorInfo;
 import android.view.inputmethod.InputMethodSubtype;
 
-import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.compat.EditorInfoCompatUtils;
 import com.android.inputmethod.keyboard.internal.KeyboardBuilder;
 import com.android.inputmethod.keyboard.internal.KeyboardParams;
 import com.android.inputmethod.keyboard.internal.KeysCache;
+import com.android.inputmethod.latin.AdditionalSubtype;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.InputAttributes;
 import com.android.inputmethod.latin.InputTypeUtils;
@@ -72,6 +73,8 @@
     private static final String TAG_ELEMENT = "Element";
 
     private static final String KEYBOARD_LAYOUT_SET_RESOURCE_PREFIX = "keyboard_layout_set_";
+    private static final int SPELLCHECKER_DUMMY_KEYBOARD_WIDTH = 480;
+    private static final int SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT = 800;
 
     private final Context mContext;
     private final Params mParams;
@@ -282,8 +285,7 @@
             return this;
         }
 
-        @UsedForTesting
-        public void disableTouchPositionCorrectionDataForTest() {
+        public void disableTouchPositionCorrectionData() {
             mParams.mDisableTouchPositionCorrectionDataForTest = true;
         }
 
@@ -413,4 +415,47 @@
             }
         }
     }
+
+    public static KeyboardLayoutSet createKeyboardSetForSpellChecker(final Context context,
+            final String locale, final String layout) {
+        final InputMethodSubtype subtype =
+                AdditionalSubtype.createAdditionalSubtype(locale, layout, null);
+        return createKeyboardSet(context, subtype, SPELLCHECKER_DUMMY_KEYBOARD_WIDTH,
+                SPELLCHECKER_DUMMY_KEYBOARD_HEIGHT, false);
+    }
+
+    public static KeyboardLayoutSet createKeyboardSetForTest(final Context context,
+            final InputMethodSubtype subtype, final int orientation,
+            final boolean testCasesHaveTouchCoordinates) {
+        final DisplayMetrics dm = context.getResources().getDisplayMetrics();
+        final int width;
+        final int height;
+        if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
+            width = Math.max(dm.widthPixels, dm.heightPixels);
+            height = Math.min(dm.widthPixels, dm.heightPixels);
+        } else if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+            width = Math.min(dm.widthPixels, dm.heightPixels);
+            height = Math.max(dm.widthPixels, dm.heightPixels);
+        } else {
+            throw new RuntimeException("Orientation should be ORIENTATION_LANDSCAPE or "
+                    + "ORIENTATION_PORTRAIT: orientation=" + orientation);
+        }
+        return createKeyboardSet(context, subtype, width, height, testCasesHaveTouchCoordinates);
+    }
+
+    private static KeyboardLayoutSet createKeyboardSet(final Context context,
+            final InputMethodSubtype subtype, final int width, final int height,
+            final boolean testCasesHaveTouchCoordinates) {
+        final EditorInfo editorInfo = new EditorInfo();
+        editorInfo.inputType = InputType.TYPE_CLASS_TEXT;
+        final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
+                context, editorInfo);
+        builder.setScreenGeometry(width, height);
+        builder.setSubtype(subtype);
+        if (!testCasesHaveTouchCoordinates) {
+            // For spell checker and tests
+            builder.disableTouchPositionCorrectionData();
+        }
+        return builder.build();
+    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 39afe90..ad08d64 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -216,19 +216,19 @@
         mState.onResetKeyboardStateToAlphabet();
     }
 
-    public void onPressKey(final int code) {
+    public void onPressKey(final int code, final boolean isSinglePointer) {
         if (isVibrateAndSoundFeedbackRequired()) {
             mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
         }
-        mState.onPressKey(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
+        mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState());
     }
 
     public void onReleaseKey(final int code, final boolean withSliding) {
         mState.onReleaseKey(code, withSliding);
     }
 
-    public void onCancelInput() {
-        mState.onCancelInput(isSinglePointer());
+    public void onFinishSlidingInput() {
+        mState.onFinishSlidingInput();
     }
 
     // Implements {@link KeyboardState.SwitchActions}.
@@ -346,15 +346,11 @@
         return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
     }
 
-    private boolean isSinglePointer() {
-        return mKeyboardView != null && mKeyboardView.getPointerCount() == 1;
-    }
-
     /**
      * Updates state machine to figure out when to automatically switch back to the previous mode.
      */
     public void onCodeInput(final int code) {
-        mState.onCodeInput(code, isSinglePointer(), mLatinIME.getCurrentAutoCapsState());
+        mState.onCodeInput(code, mLatinIME.getCurrentAutoCapsState());
     }
 
     public MainKeyboardView getMainKeyboardView() {
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index e4e75c3..7941fcb 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -497,7 +497,7 @@
             }
         }
 
-        if (key.hasPopupHint() && key.mMoreKeys != null && key.mMoreKeys.length > 0) {
+        if (key.hasPopupHint() && key.mMoreKeys != null) {
             drawKeyPopupHint(key, canvas, paint, params);
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index a0ac475..6c6fc61 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -910,10 +910,10 @@
         mSlidingKeyInputPreview.dismissSlidingKeyInputPreview();
     }
 
-    public void setGesturePreviewMode(final boolean drawsGesturePreviewTrail,
+    public void setGesturePreviewMode(final boolean drawsGestureTrail,
             final boolean drawsGestureFloatingPreviewText) {
         mGestureFloatingPreviewText.setPreviewEnabled(drawsGestureFloatingPreviewText);
-        mGestureTrailsPreview.setPreviewEnabled(drawsGesturePreviewTrail);
+        mGestureTrailsPreview.setPreviewEnabled(drawsGestureTrail);
     }
 
     public void showGestureFloatingPreviewText(final SuggestedWords suggestedWords) {
@@ -927,7 +927,7 @@
     }
 
     @Override
-    public void showGesturePreviewTrail(final PointerTracker tracker) {
+    public void showGestureTrail(final PointerTracker tracker) {
         locatePreviewPlacerView();
         mGestureFloatingPreviewText.setPreviewPosition(tracker);
         mGestureTrailsPreview.setPreviewPosition(tracker);
@@ -1100,10 +1100,6 @@
         return false;
     }
 
-    public int getPointerCount() {
-        return mOldPointerCount;
-    }
-
     @Override
     public boolean dispatchTouchEvent(MotionEvent event) {
         if (AccessibilityUtils.getInstance().isTouchExplorationEnabled()) {
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 0556fdd..1742393 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -25,6 +25,7 @@
 import com.android.inputmethod.keyboard.internal.GestureStroke;
 import com.android.inputmethod.keyboard.internal.GestureStroke.GestureStrokeParams;
 import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints;
+import com.android.inputmethod.keyboard.internal.GestureStrokeWithPreviewPoints.GestureStrokePreviewParams;
 import com.android.inputmethod.keyboard.internal.PointerTrackerQueue;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.Constants;
@@ -83,7 +84,7 @@
         public void dismissKeyPreview(PointerTracker tracker);
         public void showSlidingKeyInputPreview(PointerTracker tracker);
         public void dismissSlidingKeyInputPreview();
-        public void showGesturePreviewTrail(PointerTracker tracker);
+        public void showGestureTrail(PointerTracker tracker);
     }
 
     public interface TimerProxy {
@@ -161,6 +162,7 @@
     // Parameters for pointer handling.
     private static PointerTrackerParams sParams;
     private static GestureStrokeParams sGestureStrokeParams;
+    private static GestureStrokePreviewParams sGesturePreviewParams;
     private static boolean sNeedsPhantomSuddenMoveEventHack;
     // Move this threshold to resource.
     // TODO: Device specific parameter would be better for device specific hack?
@@ -339,12 +341,14 @@
         sNeedsPhantomSuddenMoveEventHack = needsPhantomSuddenMoveEventHack;
         sParams = PointerTrackerParams.DEFAULT;
         sGestureStrokeParams = GestureStrokeParams.DEFAULT;
+        sGesturePreviewParams = GestureStrokePreviewParams.DEFAULT;
         sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
     }
 
     public static void setParameters(final TypedArray mainKeyboardViewAttr) {
         sParams = new PointerTrackerParams(mainKeyboardViewAttr);
         sGestureStrokeParams = new GestureStrokeParams(mainKeyboardViewAttr);
+        sGesturePreviewParams = new GestureStrokePreviewParams(mainKeyboardViewAttr);
         sTimeRecorder = new TimeRecorder(sParams, sGestureStrokeParams);
     }
 
@@ -428,7 +432,7 @@
         }
         mPointerId = id;
         mGestureStrokeWithPreviewPoints = new GestureStrokeWithPreviewPoints(
-                id, sGestureStrokeParams);
+                id, sGestureStrokeParams, sGesturePreviewParams);
         setKeyDetectorInner(handler.getKeyDetector());
         mListener = handler.getKeyboardActionListener();
         mDrawingProxy = handler.getDrawingProxy();
@@ -455,7 +459,7 @@
             return false;
         }
         if (key.isEnabled()) {
-            mListener.onPressKey(key.mCode);
+            mListener.onPressKey(key.mCode, getActivePointerTrackerCount() == 1);
             final boolean keyboardLayoutHasBeenChanged = mKeyboardLayoutHasBeenChanged;
             mKeyboardLayoutHasBeenChanged = false;
             mTimerProxy.startTypingStateTimer(key);
@@ -523,6 +527,13 @@
         }
     }
 
+    private void callListenerOnFinishSlidingInput() {
+        if (DEBUG_LISTENER) {
+            Log.d(TAG, String.format("[%d] onFinishSlidingInput", mPointerId));
+        }
+        mListener.onFinishSlidingInput();
+    }
+
     private void callListenerOnCancelInput() {
         if (DEBUG_LISTENER) {
             Log.d(TAG, String.format("[%d] onCancelInput", mPointerId));
@@ -733,7 +744,7 @@
             dismissAllMoreKeysPanels();
         }
         mTimerProxy.cancelLongPressTimer();
-        mDrawingProxy.showGesturePreviewTrail(this);
+        mDrawingProxy.showGestureTrail(this);
     }
 
     public void updateBatchInputByTimer(final long eventTime) {
@@ -749,7 +760,7 @@
         if (mIsTrackingForActionDisabled) {
             return;
         }
-        mDrawingProxy.showGesturePreviewTrail(this);
+        mDrawingProxy.showGestureTrail(this);
     }
 
     private void updateBatchInput(final long eventTime) {
@@ -790,7 +801,7 @@
         if (mIsTrackingForActionDisabled) {
             return;
         }
-        mDrawingProxy.showGesturePreviewTrail(this);
+        mDrawingProxy.showGestureTrail(this);
     }
 
     private void cancelBatchInput() {
@@ -1032,7 +1043,7 @@
 
     private void processSildeOutFromOldKey(final Key oldKey) {
         setReleasedKeyGraphics(oldKey);
-        callListenerOnRelease(oldKey, oldKey.mCode, true);
+        callListenerOnRelease(oldKey, oldKey.mCode, true /* withSliding */);
         startSlidingKeyInput(oldKey);
         mTimerProxy.cancelKeyTimers();
     }
@@ -1164,6 +1175,8 @@
 
     private void onUpEventInternal(final int x, final int y, final long eventTime) {
         mTimerProxy.cancelKeyTimers();
+        final boolean isInSlidingKeyInput = mIsInSlidingKeyInput;
+        final boolean isInSlidingKeyInputFromModifier = mIsInSlidingKeyInputFromModifier;
         resetSlidingKeyInput();
         mIsDetectingGesture = false;
         final Key currentKey = mCurrentKey;
@@ -1184,7 +1197,7 @@
 
         if (sInGesture) {
             if (currentKey != null) {
-                callListenerOnRelease(currentKey, currentKey.mCode, true);
+                callListenerOnRelease(currentKey, currentKey.mCode, true /* withSliding */);
             }
             mayEndBatchInput(eventTime);
             return;
@@ -1193,8 +1206,13 @@
         if (mIsTrackingForActionDisabled) {
             return;
         }
-        if (currentKey != null && !currentKey.isRepeatable()) {
-            detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
+        if (currentKey != null && currentKey.isRepeatable() && !isInSlidingKeyInput) {
+            // Repeatable key has been registered in {@link #onDownEventInternal(int,int,long)}.
+            return;
+        }
+        detectAndSendKey(currentKey, mKeyX, mKeyY, eventTime);
+        if (isInSlidingKeyInputFromModifier) {
+            callListenerOnFinishSlidingInput();
         }
     }
 
@@ -1243,10 +1261,13 @@
     }
 
     private void startRepeatKey(final Key key) {
-        if (key != null && key.isRepeatable() && !sInGesture) {
-            onRegisterKey(key);
-            mTimerProxy.startKeyRepeatTimer(this);
-        }
+        if (sInGesture) return;
+        if (key == null) return;
+        if (!key.isRepeatable()) return;
+        // Don't start key repeat when we are in sliding input mode.
+        if (mIsInSlidingKeyInput) return;
+        onRegisterKey(key);
+        mTimerProxy.startKeyRepeatTimer(this);
     }
 
     public void onRegisterKey(final Key key) {
@@ -1299,9 +1320,15 @@
     }
 
     private void startLongPressTimer(final Key key) {
-        if (key != null && key.isLongPressEnabled() && !sInGesture) {
-            mTimerProxy.startLongPressTimer(this);
-        }
+        if (sInGesture) return;
+        if (key == null) return;
+        if (!key.isLongPressEnabled()) return;
+        // Caveat: Please note that isLongPressEnabled() can be true even if the current key
+        // doesn't have its more keys. (e.g. spacebar, globe key)
+        // We always need to start the long press timer if the key has its more keys regardless of
+        // whether or not we are in the sliding input mode.
+        if (mIsInSlidingKeyInput && key.mMoreKeys == null) return;
+        mTimerProxy.startLongPressTimer(this);
     }
 
     private void detectAndSendKey(final Key key, final int x, final int y, final long eventTime) {
@@ -1312,7 +1339,7 @@
 
         final int code = key.mCode;
         callListenerOnCodeInput(key, code, x, y, eventTime);
-        callListenerOnRelease(key, code, false);
+        callListenerOnRelease(key, code, false /* withSliding */);
     }
 
     private void printTouchEvent(final String title, final int x, final int y,
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index b77e378..57d3fed 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -79,23 +79,6 @@
         mNativeProximityInfo = createNativeProximityInfo(touchPositionCorrection);
     }
 
-    /**
-     * Constructor for subclasses such as
-     * {@link com.android.inputmethod.latin.spellcheck.SpellCheckerProximityInfo}.
-     */
-    protected ProximityInfo(final int[] proximityCharsArray, final int gridWidth,
-            final int gridHeight) {
-        this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
-        mNativeProximityInfo = setProximityInfoNative("" /* locale */,
-                gridWidth /* displayWidth */, gridHeight /* displayHeight */,
-                gridWidth, gridHeight, 1 /* mostCommonKeyWidth */,
-                1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */,
-                null /*keyXCoordinates */, null /* keyYCoordinates */,
-                null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */,
-                null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */,
-                null /* sweetSpotRadii */);
-    }
-
     private long mNativeProximityInfo;
     static {
         JniUtils.loadNativeLibrary();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
index 93ff264..70363e6 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStroke.java
@@ -145,7 +145,7 @@
     public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
         mKeyWidth = keyWidth;
         mMinYCoordinate = -(int)(keyboardHeight * EXTRA_GESTURE_TRAIL_AREA_ABOVE_KEYBOARD_RATIO);
-        mMaxYCoordinate = keyboardHeight - 1;
+        mMaxYCoordinate = keyboardHeight;
         // TODO: Find an appropriate base metric for these length. Maybe diagonal length of the key?
         mDetectFastMoveSpeedThreshold = (int)(keyWidth * mParams.mDetectFastMoveSpeedThreshold);
         mGestureDynamicDistanceThresholdFrom =
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
index 7a51e25..b31f00b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureStrokeWithPreviewPoints.java
@@ -16,6 +16,9 @@
 
 package com.android.inputmethod.keyboard.internal;
 
+import android.content.res.TypedArray;
+
+import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.ResizableIntArray;
 
 public final class GestureStrokeWithPreviewPoints extends GestureStroke {
@@ -25,6 +28,8 @@
     private final ResizableIntArray mPreviewXCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
     private final ResizableIntArray mPreviewYCoordinates = new ResizableIntArray(PREVIEW_CAPACITY);
 
+    private final GestureStrokePreviewParams mPreviewParams;
+
     private int mStrokeId;
     private int mLastPreviewSize;
     private final HermiteInterpolator mInterpolator = new HermiteInterpolator();
@@ -32,23 +37,53 @@
 
     private int mLastX;
     private int mLastY;
-    private double mMinPreviewSamplingDistance;
     private double mDistanceFromLastSample;
-    private double mInterpolationDistanceThreshold;
 
-    // TODO: Move these constants to resource.
-    // TODO: Use "dp" instead of ratio to the keyWidth because table has rather large keys.
-    // The minimum trail distance between sample points for preview in keyWidth unit when using
-    // interpolation.
-    private static final float MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH = 0.2f;
-    // The angular threshold to use interpolation in radian. PI/12 is 15 degree.
-    private static final double INTERPOLATION_ANGULAR_THRESHOLD = Math.PI / 12.0d;
-    // The distance threshold to use interpolation in keyWidth unit.
-    private static final float INTERPOLATION_DISTANCE_THRESHOLD_TO_KEY_WIDTH = 0.5f;
-    private static final int MAX_INTERPOLATION_PARTITIONS = 6;
+    public static final class GestureStrokePreviewParams {
+        public final double mMinSamplingDistance; // in pixel
+        public final double mMaxInterpolationAngularThreshold; // in radian
+        public final double mMaxInterpolationDistanceThreshold; // in pixel
+        public final int mMaxInterpolationSegments;
 
-    public GestureStrokeWithPreviewPoints(final int pointerId, final GestureStrokeParams params) {
-        super(pointerId, params);
+        public static final GestureStrokePreviewParams DEFAULT = new GestureStrokePreviewParams();
+
+        private static final int DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD = 15; // in degree
+
+        private GestureStrokePreviewParams() {
+            mMinSamplingDistance = 0.0d;
+            mMaxInterpolationAngularThreshold =
+                    degreeToRadian(DEFAULT_MAX_INTERPOLATION_ANGULAR_THRESHOLD);
+            mMaxInterpolationDistanceThreshold = mMinSamplingDistance;
+            mMaxInterpolationSegments = 4;
+        }
+
+        private static double degreeToRadian(final int degree) {
+            return (double)degree / 180.0d * Math.PI;
+        }
+
+        public GestureStrokePreviewParams(final TypedArray mainKeyboardViewAttr) {
+            mMinSamplingDistance = mainKeyboardViewAttr.getDimension(
+                    R.styleable.MainKeyboardView_gestureTrailMinSamplingDistance,
+                    (float)DEFAULT.mMinSamplingDistance);
+            final int interpolationAngularDegree = mainKeyboardViewAttr.getInteger(R.styleable
+                    .MainKeyboardView_gestureTrailMaxInterpolationAngularThreshold, 0);
+            mMaxInterpolationAngularThreshold = (interpolationAngularDegree <= 0)
+                    ? DEFAULT.mMaxInterpolationAngularThreshold
+                    : degreeToRadian(interpolationAngularDegree);
+            mMaxInterpolationDistanceThreshold = mainKeyboardViewAttr.getDimension(R.styleable
+                    .MainKeyboardView_gestureTrailMaxInterpolationDistanceThreshold,
+                    (float)DEFAULT.mMaxInterpolationDistanceThreshold);
+            mMaxInterpolationSegments = mainKeyboardViewAttr.getInteger(
+                    R.styleable.MainKeyboardView_gestureTrailMaxInterpolationSegments,
+                    DEFAULT.mMaxInterpolationSegments);
+        }
+    }
+
+    public GestureStrokeWithPreviewPoints(final int pointerId,
+            final GestureStrokeParams strokeParams,
+            final GestureStrokePreviewParams previewParams) {
+        super(pointerId, strokeParams);
+        mPreviewParams = previewParams;
     }
 
     @Override
@@ -66,19 +101,12 @@
         return mStrokeId;
     }
 
-    @Override
-    public void setKeyboardGeometry(final int keyWidth, final int keyboardHeight) {
-        super.setKeyboardGeometry(keyWidth, keyboardHeight);
-        mMinPreviewSamplingDistance = keyWidth * MIN_PREVIEW_SAMPLING_RATIO_TO_KEY_WIDTH;
-        mInterpolationDistanceThreshold = keyWidth * INTERPOLATION_DISTANCE_THRESHOLD_TO_KEY_WIDTH;
-    }
-
     private boolean needsSampling(final int x, final int y) {
         mDistanceFromLastSample += Math.hypot(x - mLastX, y - mLastY);
         mLastX = x;
         mLastY = y;
         final boolean isDownEvent = (mPreviewEventTimes.getLength() == 0);
-        if (mDistanceFromLastSample >= mMinPreviewSamplingDistance || isDownEvent) {
+        if (mDistanceFromLastSample >= mPreviewParams.mMinSamplingDistance || isDownEvent) {
             mDistanceFromLastSample = 0.0d;
             return true;
         }
@@ -117,14 +145,14 @@
      *
      * @param lastInterpolatedIndex the start index of the last interpolated segment of
      *        <code>eventTimes</code>, <code>xCoords</code>, and <code>yCoords</code>.
-     * @param eventTimes the event time array of gesture preview trail to be drawn.
-     * @param xCoords the x-coordinates array of gesture preview trail to be drawn.
-     * @param yCoords the y-coordinates array of gesture preview trail to be drawn.
+     * @param eventTimes the event time array of gesture trail to be drawn.
+     * @param xCoords the x-coordinates array of gesture trail to be drawn.
+     * @param yCoords the y-coordinates array of gesture trail to be drawn.
      * @return the start index of the last interpolated segment of input arrays.
      */
     public int interpolateStrokeAndReturnStartIndexOfLastSegment(final int lastInterpolatedIndex,
             final ResizableIntArray eventTimes, final ResizableIntArray xCoords,
-            final ResizableIntArray yCoords) {
+            final ResizableIntArray yCoords, final ResizableIntArray types) {
         final int size = mPreviewEventTimes.getLength();
         final int[] pt = mPreviewEventTimes.getPrimitiveArray();
         final int[] px = mPreviewXCoordinates.getPrimitiveArray();
@@ -144,28 +172,34 @@
             final double m1 = Math.atan2(mInterpolator.mSlope1Y, mInterpolator.mSlope1X);
             final double m2 = Math.atan2(mInterpolator.mSlope2Y, mInterpolator.mSlope2X);
             final double deltaAngle = Math.abs(angularDiff(m2, m1));
-            final int partitionsByAngle = (int)Math.ceil(
-                    deltaAngle / INTERPOLATION_ANGULAR_THRESHOLD);
+            final int segmentsByAngle = (int)Math.ceil(
+                    deltaAngle / mPreviewParams.mMaxInterpolationAngularThreshold);
             final double deltaDistance = Math.hypot(mInterpolator.mP1X - mInterpolator.mP2X,
                     mInterpolator.mP1Y - mInterpolator.mP2Y);
-            final int partitionsByDistance = (int)Math.ceil(deltaDistance
-                    / mInterpolationDistanceThreshold);
-            final int partitions = Math.min(MAX_INTERPOLATION_PARTITIONS,
-                    Math.max(partitionsByAngle, partitionsByDistance));
+            final int segmentsByDistance = (int)Math.ceil(deltaDistance
+                    / mPreviewParams.mMaxInterpolationDistanceThreshold);
+            final int segments = Math.min(mPreviewParams.mMaxInterpolationSegments,
+                    Math.max(segmentsByAngle, segmentsByDistance));
             final int t1 = eventTimes.get(d1);
             final int dt = pt[p2] - pt[p1];
             d1++;
-            for (int i = 1; i < partitions; i++) {
-                final float t = i / (float)partitions;
+            for (int i = 1; i < segments; i++) {
+                final float t = i / (float)segments;
                 mInterpolator.interpolate(t);
                 eventTimes.add(d1, (int)(dt * t) + t1);
                 xCoords.add(d1, (int)mInterpolator.mInterpolatedX);
                 yCoords.add(d1, (int)mInterpolator.mInterpolatedY);
+                if (GestureTrail.DBG_SHOW_POINTS) {
+                    types.add(d1, GestureTrail.POINT_TYPE_INTERPOLATED);
+                }
                 d1++;
             }
             eventTimes.add(d1, pt[p2]);
             xCoords.add(d1, px[p2]);
             yCoords.add(d1, py[p2]);
+            if (GestureTrail.DBG_SHOW_POINTS) {
+                types.add(d1, GestureTrail.POINT_TYPE_SAMPLED);
+            }
         }
         return lastInterpolatedDrawIndex;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
similarity index 81%
rename from java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
rename to java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
index 7fd1bed..03dd1c3 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrail.java
@@ -18,6 +18,7 @@
 
 import android.content.res.TypedArray;
 import android.graphics.Canvas;
+import android.graphics.Color;
 import android.graphics.Paint;
 import android.graphics.Path;
 import android.graphics.Rect;
@@ -28,19 +29,26 @@
 import com.android.inputmethod.latin.ResizableIntArray;
 
 /*
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailFadeoutStartDelay
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailFadeoutDuration
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailUpdateInterval
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailColor
- * @attr ref R.styleable#MainKeyboardView_gesturePreviewTrailWidth
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutStartDelay
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailFadeoutDuration
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailUpdateInterval
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailColor
+ * @attr ref R.styleable#MainKeyboardView_gestureTrailWidth
  */
-final class GesturePreviewTrail {
+final class GestureTrail {
+    public static final boolean DBG_SHOW_POINTS = false;
+    public static final int POINT_TYPE_SAMPLED = 0;
+    public static final int POINT_TYPE_INTERPOLATED = 1;
+    public static final int POINT_TYPE_COMPROMISED = 2;
+
     private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
 
     // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}.
     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
+    private final ResizableIntArray mPointTypes = new ResizableIntArray(
+            DBG_SHOW_POINTS ? DEFAULT_CAPACITY : 0);
     private int mCurrentStrokeId = -1;
     // The wall time of the zero value in {@link #mEventTimes}
     private long mCurrentTimeBase;
@@ -62,26 +70,26 @@
 
         public Params(final TypedArray mainKeyboardViewAttr) {
             mTrailColor = mainKeyboardViewAttr.getColor(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailColor, 0);
+                    R.styleable.MainKeyboardView_gestureTrailColor, 0);
             mTrailStartWidth = mainKeyboardViewAttr.getDimension(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailStartWidth, 0.0f);
+                    R.styleable.MainKeyboardView_gestureTrailStartWidth, 0.0f);
             mTrailEndWidth = mainKeyboardViewAttr.getDimension(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailEndWidth, 0.0f);
+                    R.styleable.MainKeyboardView_gestureTrailEndWidth, 0.0f);
             final int PERCENTAGE_INT = 100;
             mTrailBodyRatio = (float)mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailBodyRatio, PERCENTAGE_INT)
+                    R.styleable.MainKeyboardView_gestureTrailBodyRatio, PERCENTAGE_INT)
                     / (float)PERCENTAGE_INT;
             final int trailShadowRatioInt = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailShadowRatio, 0);
+                    R.styleable.MainKeyboardView_gestureTrailShadowRatio, 0);
             mTrailShadowEnabled = (trailShadowRatioInt > 0);
             mTrailShadowRatio = (float)trailShadowRatioInt / (float)PERCENTAGE_INT;
-            mFadeoutStartDelay = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutStartDelay, 0);
-            mFadeoutDuration = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailFadeoutDuration, 0);
+            mFadeoutStartDelay = DBG_SHOW_POINTS ? 2000 : mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_gestureTrailFadeoutStartDelay, 0);
+            mFadeoutDuration = DBG_SHOW_POINTS ? 200 : mainKeyboardViewAttr.getInt(
+                    R.styleable.MainKeyboardView_gestureTrailFadeoutDuration, 0);
             mTrailLingerDuration = mFadeoutStartDelay + mFadeoutDuration;
             mUpdateInterval = mainKeyboardViewAttr.getInt(
-                    R.styleable.MainKeyboardView_gesturePreviewTrailUpdateInterval, 0);
+                    R.styleable.MainKeyboardView_gestureTrailUpdateInterval, 0);
         }
     }
 
@@ -125,7 +133,7 @@
         final int lastInterpolatedIndex = (strokeId == mCurrentStrokeId)
                 ? mLastInterpolatedDrawIndex : trailSize;
         mLastInterpolatedDrawIndex = stroke.interpolateStrokeAndReturnStartIndexOfLastSegment(
-                lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates);
+                lastInterpolatedIndex, mEventTimes, mXCoordinates, mYCoordinates, mPointTypes);
         if (strokeId != mCurrentStrokeId) {
             final int elapsedTime = (int)(downTime - mCurrentTimeBase);
             for (int i = mTrailStartIndex; i < trailSize; i++) {
@@ -178,12 +186,12 @@
     private final Rect mRoundedLineBounds = new Rect();
 
     /**
-     * Draw gesture preview trail
-     * @param canvas The canvas to draw the gesture preview trail
-     * @param paint The paint object to be used to draw the gesture preview trail
+     * Draw gesture trail
+     * @param canvas The canvas to draw the gesture trail
+     * @param paint The paint object to be used to draw the gesture trail
      * @param outBoundsRect the bounding box of this gesture trail drawing
-     * @param params The drawing parameters of gesture preview trail
-     * @return true if some gesture preview trails remain to be drawn
+     * @param params The drawing parameters of gesture trail
+     * @return true if some gesture trails remain to be drawn
      */
     public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
             final Rect outBoundsRect, final Params params) {
@@ -204,6 +212,7 @@
         final int[] eventTimes = mEventTimes.getPrimitiveArray();
         final int[] xCoords = mXCoordinates.getPrimitiveArray();
         final int[] yCoords = mYCoordinates.getPrimitiveArray();
+        final int[] pointTypes = mPointTypes.getPrimitiveArray();
         final int sinceDown = (int)(SystemClock.uptimeMillis() - mCurrentTimeBase);
         int startIndex;
         for (startIndex = mTrailStartIndex; startIndex < trailSize; startIndex++) {
@@ -246,6 +255,17 @@
                         final int alpha = getAlpha(elapsedTime, params);
                         paint.setAlpha(alpha);
                         canvas.drawPath(path, paint);
+                        if (DBG_SHOW_POINTS) {
+                            if (pointTypes[i] == POINT_TYPE_INTERPOLATED) {
+                                paint.setColor(Color.RED);
+                            } else if (pointTypes[i] == POINT_TYPE_SAMPLED) {
+                                paint.setColor(0xFFA000FF);
+                            } else {
+                                paint.setColor(Color.GREEN);
+                            }
+                            canvas.drawCircle(p1x - 1, p1y - 1, 2, paint);
+                            paint.setColor(params.mTrailColor);
+                        }
                     }
                 }
                 p1x = p2x;
@@ -265,6 +285,9 @@
             mEventTimes.setLength(newSize);
             mXCoordinates.setLength(newSize);
             mYCoordinates.setLength(newSize);
+            if (DBG_SHOW_POINTS) {
+                mPointTypes.setLength(newSize);
+            }
             // The start index of the last segment of the stroke
             // {@link mLastInterpolatedDrawIndex} should also be updated because all array
             // elements have just been shifted for compaction or been zeroed.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
index 85558f1..1e4c43e 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GestureTrailsPreview.java
@@ -29,7 +29,7 @@
 import android.view.View;
 
 import com.android.inputmethod.keyboard.PointerTracker;
-import com.android.inputmethod.keyboard.internal.GesturePreviewTrail.Params;
+import com.android.inputmethod.keyboard.internal.GestureTrail.Params;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 
@@ -37,9 +37,8 @@
  * Draw gesture trail preview graphics during gesture.
  */
 public final class GestureTrailsPreview extends AbstractDrawingPreview {
-    private final SparseArray<GesturePreviewTrail> mGesturePreviewTrails =
-            CollectionUtils.newSparseArray();
-    private final Params mGesturePreviewTrailParams;
+    private final SparseArray<GestureTrail> mGestureTrails = CollectionUtils.newSparseArray();
+    private final Params mGestureTrailParams;
     private final Paint mGesturePaint;
     private int mOffscreenWidth;
     private int mOffscreenHeight;
@@ -48,20 +47,20 @@
     private final Canvas mOffscreenCanvas = new Canvas();
     private final Rect mOffscreenSrcRect = new Rect();
     private final Rect mDirtyRect = new Rect();
-    private final Rect mGesturePreviewTrailBoundsRect = new Rect(); // per trail
+    private final Rect mGestureTrailBoundsRect = new Rect(); // per trail
 
     private final DrawingHandler mDrawingHandler;
 
     private static final class DrawingHandler
             extends StaticInnerHandlerWrapper<GestureTrailsPreview> {
-        private static final int MSG_UPDATE_GESTURE_PREVIEW_TRAIL = 0;
+        private static final int MSG_UPDATE_GESTURE_TRAIL = 0;
 
-        private final Params mGesturePreviewTrailParams;
+        private final Params mGestureTrailParams;
 
         public DrawingHandler(final GestureTrailsPreview outerInstance,
-                final Params gesturePreviewTrailParams) {
+                final Params gestureTrailParams) {
             super(outerInstance);
-            mGesturePreviewTrailParams = gesturePreviewTrailParams;
+            mGestureTrailParams = gestureTrailParams;
         }
 
         @Override
@@ -69,23 +68,23 @@
             final GestureTrailsPreview preview = getOuterInstance();
             if (preview == null) return;
             switch (msg.what) {
-            case MSG_UPDATE_GESTURE_PREVIEW_TRAIL:
+            case MSG_UPDATE_GESTURE_TRAIL:
                 preview.getDrawingView().invalidate();
                 break;
             }
         }
 
         public void postUpdateGestureTrailPreview() {
-            removeMessages(MSG_UPDATE_GESTURE_PREVIEW_TRAIL);
-            sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_PREVIEW_TRAIL),
-                    mGesturePreviewTrailParams.mUpdateInterval);
+            removeMessages(MSG_UPDATE_GESTURE_TRAIL);
+            sendMessageDelayed(obtainMessage(MSG_UPDATE_GESTURE_TRAIL),
+                    mGestureTrailParams.mUpdateInterval);
         }
     }
 
     public GestureTrailsPreview(final View drawingView, final TypedArray mainKeyboardViewAttr) {
         super(drawingView);
-        mGesturePreviewTrailParams = new Params(mainKeyboardViewAttr);
-        mDrawingHandler = new DrawingHandler(this, mGesturePreviewTrailParams);
+        mGestureTrailParams = new Params(mainKeyboardViewAttr);
+        mDrawingHandler = new DrawingHandler(this, mGestureTrailParams);
         final Paint gesturePaint = new Paint();
         gesturePaint.setAntiAlias(true);
         gesturePaint.setXfermode(new PorterDuffXfermode(PorterDuff.Mode.SRC));
@@ -133,21 +132,20 @@
             offscreenCanvas.drawRect(dirtyRect, paint);
         }
         dirtyRect.setEmpty();
-        boolean needsUpdatingGesturePreviewTrail = false;
+        boolean needsUpdatingGestureTrail = false;
         // Draw gesture trails to offscreen buffer.
-        synchronized (mGesturePreviewTrails) {
+        synchronized (mGestureTrails) {
             // Trails count == fingers count that have ever been active.
-            final int trailsCount = mGesturePreviewTrails.size();
+            final int trailsCount = mGestureTrails.size();
             for (int index = 0; index < trailsCount; index++) {
-                final GesturePreviewTrail trail = mGesturePreviewTrails.valueAt(index);
-                needsUpdatingGesturePreviewTrail |=
-                        trail.drawGestureTrail(offscreenCanvas, paint,
-                                mGesturePreviewTrailBoundsRect, mGesturePreviewTrailParams);
-                // {@link #mGesturePreviewTrailBoundsRect} has bounding box of the trail.
-                dirtyRect.union(mGesturePreviewTrailBoundsRect);
+                final GestureTrail trail = mGestureTrails.valueAt(index);
+                needsUpdatingGestureTrail |= trail.drawGestureTrail(offscreenCanvas, paint,
+                        mGestureTrailBoundsRect, mGestureTrailParams);
+                // {@link #mGestureTrailBoundsRect} has bounding box of the trail.
+                dirtyRect.union(mGestureTrailBoundsRect);
             }
         }
-        return needsUpdatingGesturePreviewTrail;
+        return needsUpdatingGestureTrail;
     }
 
     /**
@@ -161,9 +159,9 @@
         }
         mayAllocateOffscreenBuffer();
         // Draw gesture trails to offscreen buffer.
-        final boolean needsUpdatingGesturePreviewTrail = drawGestureTrails(
+        final boolean needsUpdatingGestureTrail = drawGestureTrails(
                 mOffscreenCanvas, mGesturePaint, mDirtyRect);
-        if (needsUpdatingGesturePreviewTrail) {
+        if (needsUpdatingGestureTrail) {
             mDrawingHandler.postUpdateGestureTrailPreview();
         }
         // Transfer offscreen buffer to screen.
@@ -185,12 +183,12 @@
         if (!isPreviewEnabled()) {
             return;
         }
-        GesturePreviewTrail trail;
-        synchronized (mGesturePreviewTrails) {
-            trail = mGesturePreviewTrails.get(tracker.mPointerId);
+        GestureTrail trail;
+        synchronized (mGestureTrails) {
+            trail = mGestureTrails.get(tracker.mPointerId);
             if (trail == null) {
-                trail = new GesturePreviewTrail();
-                mGesturePreviewTrails.put(tracker.mPointerId, trail);
+                trail = new GestureTrail();
+                mGestureTrails.put(tracker.mPointerId, trail);
             }
         }
         trail.addStroke(tracker.getGestureStrokeWithPreviewPoints(), tracker.getDownTime());
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 962bde9..6af1bd7 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -28,9 +28,9 @@
  * This class contains all keyboard state transition logic.
  *
  * The input events are {@link #onLoadKeyboard()}, {@link #onSaveKeyboardState()},
- * {@link #onPressKey(int, boolean, int)}, {@link #onReleaseKey(int, boolean)},
- * {@link #onCodeInput(int, boolean, int)}, {@link #onCancelInput(boolean)},
- * {@link #onUpdateShiftState(int, int)}, {@link #onLongPressTimeout(int)}.
+ * {@link #onPressKey(int,boolean,int)}, {@link #onReleaseKey(int,boolean)},
+ * {@link #onCodeInput(int,int)}, {@link #onFinishSlidingInput()}, {@link #onCancelInput()},
+ * {@link #onUpdateShiftState(int,int)}, {@link #onLongPressTimeout(int)}.
  *
  * The actions are {@link SwitchActions}'s methods.
  */
@@ -74,6 +74,7 @@
     private static final int SWITCH_STATE_SYMBOL = 2;
     private static final int SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL = 3;
     private static final int SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE = 4;
+    private static final int SWITCH_STATE_MOMENTARY_ALPHA_SHIFT = 5;
     private int mSwitchState = SWITCH_STATE_ALPHA;
 
     private boolean mIsAlphabetMode;
@@ -525,6 +526,9 @@
             } else if (mAlphabetShiftState.isShiftLockShifted() && withSliding) {
                 // In shift locked state, shift has been pressed and slid out to other key.
                 setShiftLocked(true);
+            } else if (mAlphabetShiftState.isManualShifted() && withSliding) {
+                // Shift has been pressed and slid out to other key.
+                mSwitchState = SWITCH_STATE_MOMENTARY_ALPHA_SHIFT;
             } else if (isShiftLocked && !mAlphabetShiftState.isShiftLockShifted()
                     && (mShiftKeyState.isPressing() || mShiftKeyState.isPressingOnShifted())
                     && !withSliding) {
@@ -554,17 +558,21 @@
         mShiftKeyState.onRelease();
     }
 
-    public void onCancelInput(final boolean isSinglePointer) {
+    public void onFinishSlidingInput() {
         if (DEBUG_EVENT) {
-            Log.d(TAG, "onCancelInput: single=" + isSinglePointer + " " + this);
+            Log.d(TAG, "onFinishSlidingInput: " + this);
         }
         // Switch back to the previous keyboard mode if the user cancels sliding input.
-        if (isSinglePointer) {
-            if (mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL) {
-                toggleAlphabetAndSymbols();
-            } else if (mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE) {
-                toggleShiftInSymbols();
-            }
+        switch (mSwitchState) {
+        case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL:
+            toggleAlphabetAndSymbols();
+            break;
+        case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
+            toggleShiftInSymbols();
+            break;
+        case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT:
+            setAlphabetKeyboard();
+            break;
         }
     }
 
@@ -577,10 +585,9 @@
         return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
     }
 
-    public void onCodeInput(final int code, final boolean isSinglePointer, final int autoCaps) {
+    public void onCodeInput(final int code, final int autoCaps) {
         if (DEBUG_EVENT) {
             Log.d(TAG, "onCodeInput: code=" + Constants.printableCode(code)
-                    + " single=" + isSinglePointer
                     + " autoCaps=" + autoCaps + " " + this);
         }
 
@@ -593,23 +600,12 @@
                 } else {
                     mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
                 }
-            } else if (isSinglePointer) {
-                // 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, switching back to the previous keyboard
-                // mode is handled by {@link #onCancelInput}.
-                toggleAlphabetAndSymbols();
             }
             break;
         case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE:
             if (code == Constants.CODE_SHIFT) {
                 // Detected only the shift key has been pressed on symbol layout, and then released.
                 mSwitchState = SWITCH_STATE_SYMBOL_BEGIN;
-            } else if (isSinglePointer) {
-                // 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;
             }
             break;
         case SWITCH_STATE_SYMBOL_BEGIN:
@@ -650,6 +646,7 @@
         case SWITCH_STATE_SYMBOL: return "SYMBOL";
         case SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL: return "MOMENTARY-ALPHA-SYMBOL";
         case SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE: return "MOMENTARY-SYMBOL-MORE";
+        case SWITCH_STATE_MOMENTARY_ALPHA_SHIFT: return "MOMENTARY-ALPHA_SHIFT";
         default: return null;
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 8901f99..31ef3cd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -48,6 +48,9 @@
 
     public void add(final Element pointer) {
         synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                Log.d(TAG, "add: " + pointer + " " + this);
+            }
             final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
             final int arraySize = mArraySize;
             if (arraySize < expandableArray.size()) {
@@ -61,24 +64,27 @@
 
     public void remove(final Element pointer) {
         synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                Log.d(TAG, "remove: " + pointer + " " + this);
+            }
             final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
             final int arraySize = mArraySize;
-            int newSize = 0;
+            int newIndex = 0;
             for (int index = 0; index < arraySize; index++) {
                 final Element element = expandableArray.get(index);
                 if (element == pointer) {
-                    if (newSize != index) {
+                    if (newIndex != index) {
                         Log.w(TAG, "Found duplicated element in remove: " + pointer);
                     }
                     continue; // Remove this element from the expandableArray.
                 }
-                if (newSize != index) {
+                if (newIndex != index) {
                     // Shift this element toward the beginning of the expandableArray.
-                    expandableArray.set(newSize, element);
+                    expandableArray.set(newIndex, element);
                 }
-                newSize++;
+                newIndex++;
             }
-            mArraySize = newSize;
+            mArraySize = newIndex;
         }
     }
 
@@ -95,8 +101,8 @@
             }
             final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
             final int arraySize = mArraySize;
-            int newSize, index;
-            for (newSize = index = 0; index < arraySize; index++) {
+            int newIndex, index;
+            for (newIndex = index = 0; index < arraySize; index++) {
                 final Element element = expandableArray.get(index);
                 if (element == pointer) {
                     break; // Stop releasing elements.
@@ -105,29 +111,30 @@
                     element.onPhantomUpEvent(eventTime);
                     continue; // Remove this element from the expandableArray.
                 }
-                if (newSize != index) {
+                if (newIndex != index) {
                     // Shift this element toward the beginning of the expandableArray.
-                    expandableArray.set(newSize, element);
+                    expandableArray.set(newIndex, element);
                 }
-                newSize++;
+                newIndex++;
             }
             // Shift rest of the expandableArray.
             int count = 0;
             for (; index < arraySize; index++) {
                 final Element element = expandableArray.get(index);
                 if (element == pointer) {
-                    if (count > 0) {
+                    count++;
+                    if (count > 1) {
                         Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
                                 + pointer);
                     }
-                    count++;
                 }
-                if (newSize != index) {
-                    expandableArray.set(newSize, expandableArray.get(index));
-                    newSize++;
+                if (newIndex != index) {
+                    // Shift this element toward the beginning of the expandableArray.
+                    expandableArray.set(newIndex, expandableArray.get(index));
                 }
+                newIndex++;
             }
-            mArraySize = newSize;
+            mArraySize = newIndex;
         }
     }
 
@@ -146,26 +153,26 @@
             }
             final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
             final int arraySize = mArraySize;
-            int newSize = 0, count = 0;
+            int newIndex = 0, count = 0;
             for (int index = 0; index < arraySize; index++) {
                 final Element element = expandableArray.get(index);
                 if (element == pointer) {
-                    if (count > 0) {
+                    count++;
+                    if (count > 1) {
                         Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: "
                                 + pointer);
                     }
-                    count++;
                 } else {
                     element.onPhantomUpEvent(eventTime);
                     continue; // Remove this element from the expandableArray.
                 }
-                if (newSize != index) {
+                if (newIndex != index) {
                     // Shift this element toward the beginning of the expandableArray.
-                    expandableArray.set(newSize, element);
+                    expandableArray.set(newIndex, element);
                 }
-                newSize++;
+                newIndex++;
             }
-            mArraySize = newSize;
+            mArraySize = newIndex;
         }
     }
 
@@ -202,6 +209,9 @@
 
     public void cancelAllPointerTracker() {
         synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                Log.d(TAG, "cancelAllPointerTracker: " + this);
+            }
             final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
             final int arraySize = mArraySize;
             for (int index = 0; index < arraySize; index++) {
diff --git a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
index 75c2cf2..b9db9a0 100644
--- a/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/ContactsBinaryDictionary.java
@@ -16,21 +16,26 @@
 
 package com.android.inputmethod.latin;
 
+import com.android.inputmethod.latin.personalization.AccountUtils;
+
 import android.content.ContentResolver;
 import android.content.Context;
 import android.database.ContentObserver;
 import android.database.Cursor;
+import android.net.Uri;
 import android.os.SystemClock;
 import android.provider.BaseColumns;
+import android.provider.ContactsContract;
 import android.provider.ContactsContract.Contacts;
 import android.text.TextUtils;
 import android.util.Log;
 
+import java.util.List;
 import java.util.Locale;
 
 public class ContactsBinaryDictionary extends ExpandableBinaryDictionary {
 
-    private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME,};
+    private static final String[] PROJECTION = {BaseColumns._ID, Contacts.DISPLAY_NAME};
     private static final String[] PROJECTION_ID_ONLY = {BaseColumns._ID};
 
     private static final String TAG = ContactsBinaryDictionary.class.getSimpleName();
@@ -102,9 +107,32 @@
 
     @Override
     public void loadDictionaryAsync() {
+        clearFusionDictionary();
+        loadDeviceAccountsEmailAddresses();
+        loadDictionaryAsyncForUri(ContactsContract.Profile.CONTENT_URI);
+        // TODO: Switch this URL to the newer ContactsContract too
+        loadDictionaryAsyncForUri(Contacts.CONTENT_URI);
+    }
+
+    private void loadDeviceAccountsEmailAddresses() {
+        final List<String> accountVocabulary =
+                AccountUtils.getDeviceAccountsEmailAddresses(mContext);
+        if (accountVocabulary == null || accountVocabulary.isEmpty()) {
+            return;
+        }
+        for (String word : accountVocabulary) {
+            if (DEBUG) {
+                Log.d(TAG, "loadAccountVocabulary: " + word);
+            }
+            super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
+                    false /* isNotAWord */);
+        }
+    }
+
+    private void loadDictionaryAsyncForUri(final Uri uri) {
         try {
             Cursor cursor = mContext.getContentResolver()
-                    .query(Contacts.CONTENT_URI, PROJECTION, null, null, null);
+                    .query(uri, PROJECTION, null, null, null);
             if (cursor != null) {
                 try {
                     if (cursor.moveToFirst()) {
@@ -129,7 +157,6 @@
     }
 
     private void addWords(final Cursor cursor) {
-        clearFusionDictionary();
         int count = 0;
         while (!cursor.isAfterLast() && count < MAX_CONTACT_COUNT) {
             String name = cursor.getString(INDEX_NAME);
@@ -173,6 +200,9 @@
                 // capitalization of i.
                 final int wordLen = StringUtils.codePointCount(word);
                 if (wordLen < MAX_WORD_LENGTH && wordLen > 1) {
+                    if (DEBUG) {
+                        Log.d(TAG, "addName " + name + ", " + word + ", " + prevWord);
+                    }
                     super.addWord(word, null /* shortcut */, FREQUENCY_FOR_CONTACTS,
                             false /* isNotAWord */);
                     if (!TextUtils.isEmpty(prevWord)) {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 84c7529..9caec55 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -250,6 +250,7 @@
         }
 
         public void postResumeSuggestions() {
+            removeMessages(MSG_RESUME_SUGGESTIONS);
             sendMessageDelayed(obtainMessage(MSG_RESUME_SUGGESTIONS), mDelayUpdateSuggestions);
         }
 
@@ -759,7 +760,8 @@
         }
         mSuggestedWords = SuggestedWords.EMPTY;
 
-        mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart);
+        mConnection.resetCachesUponCursorMove(editorInfo.initialSelStart,
+                false /* shouldFinishComposition */);
 
         if (isDifferentTextField) {
             mainKeyboardView.closing();
@@ -1148,13 +1150,14 @@
     // This will reset the whole input state to the starting state. It will clear
     // the composing word, reset the last composed word, tell the inputconnection about it.
     private void resetEntireInputState(final int newCursorPosition) {
+        final boolean shouldFinishComposition = mWordComposer.isComposingWord();
         resetComposingState(true /* alsoResetLastComposedWord */);
         if (mSettings.getCurrent().mBigramPredictionEnabled) {
             clearSuggestionStrip();
         } else {
             setSuggestedWords(mSettings.getCurrent().mSuggestPuncList, false);
         }
-        mConnection.resetCachesUponCursorMove(newCursorPosition);
+        mConnection.resetCachesUponCursorMove(newCursorPosition, shouldFinishComposition);
     }
 
     private void resetComposingState(final boolean alsoResetLastComposedWord) {
@@ -1749,9 +1752,16 @@
 
     // Called from PointerTracker through the KeyboardActionListener interface
     @Override
+    public void onFinishSlidingInput() {
+        // User finished sliding input.
+        mKeyboardSwitcher.onFinishSlidingInput();
+    }
+
+    // Called from PointerTracker through the KeyboardActionListener interface
+    @Override
     public void onCancelInput() {
         // User released a finger outside any key
-        mKeyboardSwitcher.onCancelInput();
+        // Nothing to do so far.
     }
 
     @Override
@@ -2436,10 +2446,15 @@
     private void restartSuggestionsOnWordTouchedByCursor() {
         // If the cursor is not touching a word, or if there is a selection, return right away.
         if (mLastSelectionStart != mLastSelectionEnd) return;
+        // If we don't know the cursor location, return.
+        if (mLastSelectionStart < 0) return;
         if (!mConnection.isCursorTouchingWord(mSettings.getCurrent())) return;
         final Range range = mConnection.getWordRangeAtCursor(mSettings.getWordSeparators(),
                 0 /* additionalPrecedingWordsCount */);
         if (null == range) return; // Happens if we don't have an input connection at all
+        // If for some strange reason (editor bug or so) we measure the text before the cursor as
+        // longer than what the entire text is supposed to be, the safe thing to do is bail out.
+        if (range.mCharsBefore > mLastSelectionStart) return;
         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         final String typedWord = range.mWord.toString();
         if (range.mWord instanceof SpannableString) {
@@ -2604,8 +2619,8 @@
     // Callback called by PointerTracker through the KeyboardActionListener. This is called when a
     // key is depressed; release matching call is onReleaseKey below.
     @Override
-    public void onPressKey(final int primaryCode) {
-        mKeyboardSwitcher.onPressKey(primaryCode);
+    public void onPressKey(final int primaryCode, final boolean isSinglePointer) {
+        mKeyboardSwitcher.onPressKey(primaryCode, isSinglePointer);
     }
 
     // Callback by PointerTracker through the KeyboardActionListener. This is called when a key
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
index b74b979..a9fba53 100644
--- a/java/src/com/android/inputmethod/latin/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -19,14 +19,17 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.os.Build;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.TypedValue;
 
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import java.util.ArrayList;
 import java.util.HashMap;
 
 public final class ResourceUtils {
     private static final String TAG = ResourceUtils.class.getSimpleName();
-    private static final boolean DEBUG = false;
 
     public static final float UNDEFINED_RATIO = -1.0f;
     public static final int UNDEFINED_DIMENSION = -1;
@@ -35,11 +38,32 @@
         // This utility class is not publicly instantiable.
     }
 
-    private static final String DEFAULT_PREFIX = "DEFAULT,";
-    private static final String HARDWARE_PREFIX = Build.HARDWARE + ",";
     private static final HashMap<String, String> sDeviceOverrideValueMap =
             CollectionUtils.newHashMap();
 
+    private static final String[] BUILD_KEYS_AND_VALUES = {
+        "HARDWARE", Build.HARDWARE,
+        "MODEL", Build.MODEL,
+        "BRAND", Build.BRAND,
+        "MANUFACTURER", Build.MANUFACTURER
+    };
+    private static final HashMap<String, String> sBuildKeyValues;
+    private static final String sBuildKeyValuesDebugString;
+
+    static {
+        sBuildKeyValues = CollectionUtils.newHashMap();
+        final ArrayList<String> keyValuePairs = CollectionUtils.newArrayList();
+        final int keyCount = BUILD_KEYS_AND_VALUES.length / 2;
+        for (int i = 0; i < keyCount; i++) {
+            final int index = i * 2;
+            final String key = BUILD_KEYS_AND_VALUES[index];
+            final String value = BUILD_KEYS_AND_VALUES[index + 1];
+            sBuildKeyValues.put(key, value);
+            keyValuePairs.add(key + '=' + value);
+        }
+        sBuildKeyValuesDebugString = "[" + TextUtils.join(" ", keyValuePairs) + "]";
+    }
+
     public static String getDeviceOverrideValue(final Resources res, final int overrideResId) {
         final int orientation = res.getConfiguration().orientation;
         final String key = overrideResId + "-" + orientation;
@@ -48,33 +72,114 @@
         }
 
         final String[] overrideArray = res.getStringArray(overrideResId);
-        final String overrideValue = StringUtils.findPrefixedString(HARDWARE_PREFIX, overrideArray);
+        final String overrideValue = findConstantForKeyValuePairs(sBuildKeyValues, overrideArray);
         // The overrideValue might be an empty string.
         if (overrideValue != null) {
-            if (DEBUG) {
-                Log.d(TAG, "Find override value:"
-                        + " resource="+ res.getResourceEntryName(overrideResId)
-                        + " Build.HARDWARE=" + Build.HARDWARE + " override=" + overrideValue);
-            }
+            Log.i(TAG, "Find override value:"
+                    + " resource="+ res.getResourceEntryName(overrideResId)
+                    + " build=" + sBuildKeyValuesDebugString
+                    + " override=" + overrideValue);
             sDeviceOverrideValueMap.put(key, overrideValue);
             return overrideValue;
         }
 
-        final String defaultValue = StringUtils.findPrefixedString(DEFAULT_PREFIX, overrideArray);
+        final String defaultValue = findDefaultConstant(overrideArray);
         // The defaultValue might be an empty string.
         if (defaultValue == null) {
             Log.w(TAG, "Couldn't find override value nor default value:"
                     + " resource="+ res.getResourceEntryName(overrideResId)
-                    + " Build.HARDWARE=" + Build.HARDWARE);
-        } else if (DEBUG) {
-            Log.d(TAG, "Found default value:"
-                + " resource="+ res.getResourceEntryName(overrideResId)
-                + " Build.HARDWARE=" + Build.HARDWARE + " default=" + defaultValue);
+                    + " build=" + sBuildKeyValuesDebugString);
+        } else {
+            Log.i(TAG, "Found default value:"
+                    + " resource="+ res.getResourceEntryName(overrideResId)
+                    + " build=" + sBuildKeyValuesDebugString
+                    + " default=" + defaultValue);
         }
         sDeviceOverrideValueMap.put(key, defaultValue);
         return defaultValue;
     }
 
+    /**
+     * Find the condition that fulfills specified key value pairs from an array of
+     * "condition,constant", and return the corresponding string constant. A condition is
+     * "pattern1[:pattern2...] (or an empty string for the default). A pattern is
+     * "key=regexp_value" string. The condition matches only if all patterns of the condition
+     * are true for the specified key value pairs.
+     *
+     * For example, "condition,constant" has the following format.
+     * (See {@link ResourceUtilsTests#testFindConstantForKeyValuePairsRegexp()})
+     *  - HARDWARE=mako,constantForNexus4
+     *  - MODEL=Nexus 4:MANUFACTURER=LGE,constantForNexus4
+     *  - ,defaultConstant
+     *
+     * @param keyValuePairs attributes to be used to look for a matched condition.
+     * @param conditionConstantArray an array of "condition,constant" elements to be searched.
+     * @return the constant part of the matched "condition,constant" element. Returns null if no
+     * condition matches.
+     */
+    @UsedForTesting
+    static String findConstantForKeyValuePairs(final HashMap<String, String> keyValuePairs,
+            final String[] conditionConstantArray) {
+        if (conditionConstantArray == null || keyValuePairs == null) {
+            return null;
+        }
+        for (final String conditionConstant : conditionConstantArray) {
+            final int posComma = conditionConstant.indexOf(',');
+            if (posComma < 0) {
+                throw new RuntimeException("Array element has no comma: " + conditionConstant);
+            }
+            final String condition = conditionConstant.substring(0, posComma);
+            if (condition.isEmpty()) {
+                // Default condition. The default condition should be searched by
+                // {@link #findConstantForDefault(String[])}.
+                continue;
+            }
+            if (fulfillsCondition(keyValuePairs, condition)) {
+                return conditionConstant.substring(posComma + 1);
+            }
+        }
+        return null;
+    }
+
+    private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
+            final String condition) {
+        final String[] patterns = condition.split(":");
+        // Check all patterns in a condition are true
+        for (final String pattern : patterns) {
+            final int posEqual = pattern.indexOf('=');
+            if (posEqual < 0) {
+                throw new RuntimeException("Pattern has no '=': " + condition);
+            }
+            final String key = pattern.substring(0, posEqual);
+            final String value = keyValuePairs.get(key);
+            if (value == null) {
+                throw new RuntimeException("Found unknown key: " + condition);
+            }
+            final String patternRegexpValue = pattern.substring(posEqual + 1);
+            if (!value.matches(patternRegexpValue)) {
+                return false;
+            }
+        }
+        return true;
+    }
+
+    @UsedForTesting
+    static String findDefaultConstant(final String[] conditionConstantArray) {
+        if (conditionConstantArray == null) {
+            return null;
+        }
+        for (final String condition : conditionConstantArray) {
+            final int posComma = condition.indexOf(',');
+            if (posComma < 0) {
+                throw new RuntimeException("Array element has no comma: " + condition);
+            }
+            if (posComma == 0) { // condition is empty.
+                return condition.substring(posComma + 1);
+            }
+        }
+        return null;
+    }
+
     public static boolean isValidFraction(final float fraction) {
         return fraction >= 0.0f;
     }
diff --git a/java/src/com/android/inputmethod/latin/RichInputConnection.java b/java/src/com/android/inputmethod/latin/RichInputConnection.java
index 8ed7ab2..980215d 100644
--- a/java/src/com/android/inputmethod/latin/RichInputConnection.java
+++ b/java/src/com/android/inputmethod/latin/RichInputConnection.java
@@ -135,13 +135,14 @@
         if (DEBUG_PREVIOUS_TEXT) checkConsistencyForDebug();
     }
 
-    public void resetCachesUponCursorMove(final int newCursorPosition) {
+    public void resetCachesUponCursorMove(final int newCursorPosition,
+            final boolean shouldFinishComposition) {
         mCurrentCursorPosition = newCursorPosition;
         mComposingText.setLength(0);
         mCommittedTextBeforeComposingText.setLength(0);
         final CharSequence textBeforeCursor = getTextBeforeCursor(DEFAULT_TEXT_CACHE_SIZE, 0);
         if (null != textBeforeCursor) mCommittedTextBeforeComposingText.append(textBeforeCursor);
-        if (null != mIC) {
+        if (null != mIC && shouldFinishComposition) {
             mIC.finishComposingText();
             if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
                 ResearchLogger.richInputConnection_finishComposingText();
diff --git a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
index 3f7be99..94513e6 100644
--- a/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
+++ b/java/src/com/android/inputmethod/latin/RichInputMethodManager.java
@@ -100,6 +100,12 @@
         throw new RuntimeException("Input method id for " + packageName + " not found.");
     }
 
+    public List<InputMethodSubtype> getMyEnabledInputMethodSubtypeList(
+            boolean allowsImplicitlySelectedSubtypes) {
+        return mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
+                mInputMethodInfoOfThisIme, allowsImplicitlySelectedSubtypes);
+    }
+
     public boolean switchToNextInputMethod(final IBinder token, final boolean onlyCurrentIme) {
         if (mImmWrapper.switchToNextInputMethod(token, onlyCurrentIme)) {
             return true;
@@ -116,8 +122,8 @@
             final boolean onlyCurrentIme) {
         final InputMethodManager imm = mImmWrapper.mImm;
         final InputMethodSubtype currentSubtype = imm.getCurrentInputMethodSubtype();
-        final List<InputMethodSubtype> enabledSubtypes = imm.getEnabledInputMethodSubtypeList(
-                mInputMethodInfoOfThisIme, true /* allowsImplicitlySelectedSubtypes */);
+        final List<InputMethodSubtype> enabledSubtypes = getMyEnabledInputMethodSubtypeList(
+                true /* allowsImplicitlySelectedSubtypes */);
         final int currentIndex = getSubtypeIndexInList(currentSubtype, enabledSubtypes);
         if (currentIndex == INDEX_NOT_FOUND) {
             Log.w(TAG, "Can't find current subtype in enabled subtypes: subtype="
@@ -214,8 +220,8 @@
             final InputMethodSubtype subtype) {
         final boolean subtypeEnabled = checkIfSubtypeBelongsToThisImeAndEnabled(subtype);
         final boolean subtypeExplicitlyEnabled = checkIfSubtypeBelongsToList(
-                subtype, mImmWrapper.mImm.getEnabledInputMethodSubtypeList(
-                        mInputMethodInfoOfThisIme, false /* allowsImplicitlySelectedSubtypes */));
+                subtype, getMyEnabledInputMethodSubtypeList(
+                        false /* allowsImplicitlySelectedSubtypes */));
         return subtypeEnabled && !subtypeExplicitlyEnabled;
     }
 
@@ -312,8 +318,7 @@
         if (filteredImisCount > 1) {
             return true;
         }
-        final List<InputMethodSubtype> subtypes =
-                mImmWrapper.mImm.getEnabledInputMethodSubtypeList(null, true);
+        final List<InputMethodSubtype> subtypes = getMyEnabledInputMethodSubtypeList(true);
         int keyboardCount = 0;
         // imm.getEnabledInputMethodSubtypeList(null, true) will return the current IME's
         // both explicitly and implicitly enabled input method subtype.
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
index 9819a02..7c4156c 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
+++ b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
@@ -59,7 +59,7 @@
 
     public void setInterface(final ValueProxy proxy) {
         mValueProxy = proxy;
-        setSummary(getValueText(proxy.readValue(getKey())));
+        setSummary(getValueText(clipValue(proxy.readValue(getKey()))));
     }
 
     private String getValueText(final int value) {
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index d5ee58a..ab050d7 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -65,23 +65,6 @@
     }
 
     /**
-     * Find a string that start with specified prefix from an array.
-     *
-     * @param prefix a prefix string to find.
-     * @param array an string array to be searched.
-     * @return the rest part of the string that starts with the prefix.
-     * Returns null if it couldn't be found.
-     */
-    public static String findPrefixedString(final String prefix, final String[] array) {
-        for (final String element : array) {
-            if (element.startsWith(prefix)) {
-                return element.substring(prefix.length());
-            }
-        }
-        return null;
-    }
-
-    /**
      * Remove duplicates from an array of strings.
      *
      * This method will always keep the first occurrence of all strings at their position
diff --git a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index bef8a3c..282b579 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -115,7 +115,7 @@
      */
     public void updateParametersOnStartInputView() {
         final List<InputMethodSubtype> enabledSubtypesOfThisIme =
-                mRichImm.getInputMethodManager().getEnabledInputMethodSubtypeList(null, true);
+                mRichImm.getMyEnabledInputMethodSubtypeList(true);
         mNeedsToDisplayLanguage.updateEnabledSubtypeCount(enabledSubtypesOfThisIme.size());
         updateShortcutIME();
     }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 51bd901..e078f03 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -16,7 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 
@@ -211,9 +210,8 @@
     }
 
     /**
-     * Internal method to retrieve reasonable proximity info for a character.
+     * Add a dummy key by retrieving reasonable coordinates
      */
-    @UsedForTesting
     public void addKeyInfo(final int codePoint, final Keyboard keyboard) {
         final int x, y;
         final Key key;
diff --git a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
new file mode 100644
index 0000000..93687e1
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 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.personalization;
+
+import android.accounts.Account;
+import android.accounts.AccountManager;
+import android.content.Context;
+import android.util.Patterns;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountUtils {
+    private AccountUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    private static Account[] getAccounts(final Context context) {
+        return AccountManager.get(context).getAccounts();
+    }
+
+    public static List<String> getDeviceAccountsEmailAddresses(final Context context) {
+        final ArrayList<String> retval = new ArrayList<String>();
+        for (final Account account : getAccounts(context)) {
+            final String name = account.name;
+            if (Patterns.EMAIL_ADDRESS.matcher(name).matches()) {
+                retval.add(name);
+                retval.add(name.split("@")[0]);
+            }
+        }
+        return retval;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
index affe3a3..8a2de88 100644
--- a/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
+++ b/java/src/com/android/inputmethod/latin/setup/SetupActivity.java
@@ -17,270 +17,27 @@
 package com.android.inputmethod.latin.setup;
 
 import android.app.Activity;
-import android.content.ContentResolver;
 import android.content.Context;
 import android.content.Intent;
-import android.content.res.Resources;
-import android.media.MediaPlayer;
-import android.net.Uri;
 import android.os.Bundle;
-import android.os.Message;
 import android.provider.Settings;
-import android.util.Log;
-import android.view.View;
 import android.view.inputmethod.InputMethodInfo;
 import android.view.inputmethod.InputMethodManager;
-import android.widget.ImageView;
-import android.widget.TextView;
-import android.widget.VideoView;
 
-import com.android.inputmethod.compat.TextViewCompatUtils;
-import com.android.inputmethod.compat.ViewCompatUtils;
-import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.RichInputMethodManager;
-import com.android.inputmethod.latin.SettingsActivity;
-import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 
-import java.util.ArrayList;
-
-// TODO: Use Fragment to implement welcome screen and setup steps.
-public final class SetupActivity extends Activity implements View.OnClickListener {
-    private static final String TAG = SetupActivity.class.getSimpleName();
-
-    private View mWelcomeScreen;
-    private View mSetupScreen;
-    private Uri mWelcomeVideoUri;
-    private VideoView mWelcomeVideoView;
-    private View mActionStart;
-    private View mActionNext;
-    private TextView mStep1Bullet;
-    private TextView mActionFinish;
-    private SetupStepGroup mSetupStepGroup;
-    private static final String STATE_STEP = "step";
-    private int mStepNumber;
-    private static final int STEP_WELCOME = 0;
-    private static final int STEP_1 = 1;
-    private static final int STEP_2 = 2;
-    private static final int STEP_3 = 3;
-    private boolean mWasLanguageAndInputSettingsInvoked;
-
-    private final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
-
-    static final class SettingsPoolingHandler extends StaticInnerHandlerWrapper<SetupActivity> {
-        private static final int MSG_POLLING_IME_SETTINGS = 0;
-        private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
-
-        public SettingsPoolingHandler(final SetupActivity outerInstance) {
-            super(outerInstance);
-        }
-
-        @Override
-        public void handleMessage(final Message msg) {
-            final SetupActivity setupActivity = getOuterInstance();
-            if (setupActivity == null) {
-                return;
-            }
-            switch (msg.what) {
-            case MSG_POLLING_IME_SETTINGS:
-                if (SetupActivity.isThisImeEnabled(setupActivity)) {
-                    setupActivity.invokeSetupWizardOfThisIme();
-                    return;
-                }
-                startPollingImeSettings();
-                break;
-            }
-        }
-
-        public void startPollingImeSettings() {
-            sendMessageDelayed(obtainMessage(MSG_POLLING_IME_SETTINGS),
-                    IME_SETTINGS_POLLING_INTERVAL);
-        }
-
-        public void cancelPollingImeSettings() {
-            removeMessages(MSG_POLLING_IME_SETTINGS);
-        }
-    }
-
+public final class SetupActivity extends Activity {
     @Override
     protected void onCreate(final Bundle savedInstanceState) {
-        setTheme(android.R.style.Theme_DeviceDefault_Light_NoActionBar);
         super.onCreate(savedInstanceState);
-
-        setContentView(R.layout.setup_wizard);
-
-        RichInputMethodManager.init(this);
-
-        if (savedInstanceState == null) {
-            mStepNumber = determineSetupStepNumber();
-            if (mStepNumber == STEP_1 && !mWasLanguageAndInputSettingsInvoked) {
-                mStepNumber = STEP_WELCOME;
-            }
-            if (mStepNumber == STEP_3) {
-                // This IME already has been enabled and set as current IME.
-                // TODO: Implement tutorial.
-                invokeSettingsOfThisIme();
-                finish();
-                return;
-            }
-        } else {
-            mStepNumber = savedInstanceState.getInt(STATE_STEP);
-        }
-
-        final String applicationName = getResources().getString(getApplicationInfo().labelRes);
-        mWelcomeScreen = findViewById(R.id.setup_welcome_screen);
-        final TextView welcomeTitle = (TextView)findViewById(R.id.setup_welcome_title);
-        welcomeTitle.setText(getString(R.string.setup_welcome_title, applicationName));
-
-        mSetupScreen = findViewById(R.id.setup_steps_screen);
-        final TextView stepsTitle = (TextView)findViewById(R.id.setup_title);
-        stepsTitle.setText(getString(R.string.setup_steps_title, applicationName));
-
-        final SetupStepIndicatorView indicatorView =
-                (SetupStepIndicatorView)findViewById(R.id.setup_step_indicator);
-        mSetupStepGroup = new SetupStepGroup(indicatorView);
-
-        mStep1Bullet = (TextView)findViewById(R.id.setup_step1_bullet);
-        mStep1Bullet.setOnClickListener(this);
-        final SetupStep step1 = new SetupStep(STEP_1, applicationName,
-                mStep1Bullet, findViewById(R.id.setup_step1),
-                R.string.setup_step1_title, R.string.setup_step1_instruction,
-                R.string.setup_step1_finished_instruction, R.drawable.ic_setup_step1,
-                R.string.setup_step1_action);
-        step1.setAction(new Runnable() {
-            @Override
-            public void run() {
-                invokeLanguageAndInputSettings();
-                mHandler.startPollingImeSettings();
-            }
-        });
-        mSetupStepGroup.addStep(step1);
-
-        final SetupStep step2 = new SetupStep(STEP_2, applicationName,
-                (TextView)findViewById(R.id.setup_step2_bullet), findViewById(R.id.setup_step2),
-                R.string.setup_step2_title, R.string.setup_step2_instruction,
-                0 /* finishedInstruction */, R.drawable.ic_setup_step2,
-                R.string.setup_step2_action);
-        step2.setAction(new Runnable() {
-            @Override
-            public void run() {
-                // Invoke input method picker.
-                RichInputMethodManager.getInstance().getInputMethodManager()
-                        .showInputMethodPicker();
-            }
-        });
-        mSetupStepGroup.addStep(step2);
-
-        final SetupStep step3 = new SetupStep(STEP_3, applicationName,
-                (TextView)findViewById(R.id.setup_step3_bullet), findViewById(R.id.setup_step3),
-                R.string.setup_step3_title, R.string.setup_step3_instruction,
-                0 /* finishedInstruction */, R.drawable.ic_setup_step3,
-                R.string.setup_step3_action);
-        step3.setAction(new Runnable() {
-            @Override
-            public void run() {
-                invokeSubtypeEnablerOfThisIme();
-            }
-        });
-        mSetupStepGroup.addStep(step3);
-
-        mWelcomeVideoUri = new Uri.Builder()
-                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
-                .authority(getPackageName())
-                .path(Integer.toString(R.raw.setup_welcome_video))
-                .build();
-        mWelcomeVideoView = (VideoView)findViewById(R.id.setup_welcome_video);
-        mWelcomeVideoView.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
-            @Override
-            public void onCompletion(final MediaPlayer mp) {
-                mp.start();
-            }
-        });
-        mWelcomeVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
-            @Override
-            public void onPrepared(final MediaPlayer mp) {
-                // Now VideoView has been laid-out and ready to play, remove background of it to
-                // reveal the video.
-                mWelcomeVideoView.setBackgroundResource(0);
-            }
-        });
-        final ImageView welcomeImageView = (ImageView)findViewById(R.id.setup_welcome_image);
-        mWelcomeVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
-            @Override
-            public boolean onError(final MediaPlayer mp, final int what, final int extra) {
-                Log.e(TAG, "Playing welcome video causes error: what=" + what + " extra=" + extra);
-                mWelcomeVideoView.setVisibility(View.GONE);
-                welcomeImageView.setImageResource(R.raw.setup_welcome_image);
-                welcomeImageView.setVisibility(View.VISIBLE);
-                return true;
-            }
-        });
-
-        mActionStart = findViewById(R.id.setup_start_label);
-        mActionStart.setOnClickListener(this);
-        mActionNext = findViewById(R.id.setup_next);
-        mActionNext.setOnClickListener(this);
-        mActionFinish = (TextView)findViewById(R.id.setup_finish);
-        TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(mActionFinish,
-                getResources().getDrawable(R.drawable.ic_setup_finish), null, null, null);
-        mActionFinish.setOnClickListener(this);
-    }
-
-    @Override
-    public void onClick(final View v) {
-        if (v == mActionFinish) {
+        final Intent intent = new Intent();
+        intent.setClass(this, SetupWizardActivity.class);
+        intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP
+                | Intent.FLAG_ACTIVITY_NEW_TASK);
+        startActivity(intent);
+        if (!isFinishing()) {
             finish();
-            return;
         }
-        final int currentStep = determineSetupStepNumber();
-        final int nextStep;
-        if (v == mActionStart) {
-            nextStep = STEP_1;
-        } else if (v == mActionNext) {
-            nextStep = mStepNumber + 1;
-        } else if (v == mStep1Bullet && currentStep == STEP_2) {
-            nextStep = STEP_1;
-        } else {
-            nextStep = mStepNumber;
-        }
-        if (mStepNumber != nextStep) {
-            mStepNumber = nextStep;
-            updateSetupStepView();
-        }
-    }
-
-    private void invokeSetupWizardOfThisIme() {
-        final Intent intent = new Intent();
-        intent.setClass(this, SetupActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
-                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        startActivity(intent);
-    }
-
-    private void invokeSettingsOfThisIme() {
-        final Intent intent = new Intent();
-        intent.setClass(this, SettingsActivity.class);
-        intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
-                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
-        startActivity(intent);
-    }
-
-    private void invokeLanguageAndInputSettings() {
-        final Intent intent = new Intent();
-        intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
-        intent.addCategory(Intent.CATEGORY_DEFAULT);
-        startActivity(intent);
-        mWasLanguageAndInputSettingsInvoked = true;
-    }
-
-    private void invokeSubtypeEnablerOfThisIme() {
-        final InputMethodInfo imi =
-                RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
-        final Intent intent = new Intent();
-        intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
-        intent.addCategory(Intent.CATEGORY_DEFAULT);
-        intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId());
-        startActivity(intent);
     }
 
     /**
@@ -317,164 +74,4 @@
                 context.getContentResolver(), Settings.Secure.DEFAULT_INPUT_METHOD);
         return myImi.getId().equals(currentImeId);
     }
-
-    private int determineSetupStepNumber() {
-        mHandler.cancelPollingImeSettings();
-        if (!isThisImeEnabled(this)) {
-            return STEP_1;
-        }
-        if (!isThisImeCurrent(this)) {
-            return STEP_2;
-        }
-        return STEP_3;
-    }
-
-    @Override
-    protected void onSaveInstanceState(final Bundle outState) {
-        super.onSaveInstanceState(outState);
-        outState.putInt(STATE_STEP, mStepNumber);
-    }
-
-    @Override
-    protected void onRestoreInstanceState(final Bundle savedInstanceState) {
-        super.onRestoreInstanceState(savedInstanceState);
-        mStepNumber = savedInstanceState.getInt(STATE_STEP);
-    }
-
-    @Override
-    protected void onRestart() {
-        super.onRestart();
-        if (mStepNumber != STEP_WELCOME) {
-            mStepNumber = determineSetupStepNumber();
-        }
-    }
-
-    @Override
-    protected void onResume() {
-        super.onResume();
-        updateSetupStepView();
-    }
-
-    @Override
-    public void onBackPressed() {
-        if (mStepNumber == STEP_1) {
-            mStepNumber = STEP_WELCOME;
-            updateSetupStepView();
-            return;
-        }
-        super.onBackPressed();
-    }
-
-    @Override
-    protected void onPause() {
-        mWelcomeVideoView.stopPlayback();
-        super.onPause();
-    }
-
-    @Override
-    public void onWindowFocusChanged(final boolean hasFocus) {
-        super.onWindowFocusChanged(hasFocus);
-        if (hasFocus && mStepNumber != STEP_WELCOME) {
-            mStepNumber = determineSetupStepNumber();
-            updateSetupStepView();
-        }
-    }
-
-    private void updateSetupStepView() {
-        final boolean welcomeScreen = (mStepNumber == STEP_WELCOME);
-        mWelcomeScreen.setVisibility(welcomeScreen ? View.VISIBLE : View.GONE);
-        mSetupScreen.setVisibility(welcomeScreen ? View.GONE: View.VISIBLE);
-        if (welcomeScreen) {
-            mWelcomeVideoView.setVideoURI(mWelcomeVideoUri);
-            mWelcomeVideoView.start();
-            return;
-        }
-        mWelcomeVideoView.stopPlayback();
-        final boolean isStepActionAlreadyDone = mStepNumber < determineSetupStepNumber();
-        mSetupStepGroup.enableStep(mStepNumber, isStepActionAlreadyDone);
-        mActionNext.setVisibility(isStepActionAlreadyDone ? View.VISIBLE : View.GONE);
-        mActionFinish.setVisibility((mStepNumber == STEP_3) ? View.VISIBLE : View.GONE);
-    }
-
-    static final class SetupStep implements View.OnClickListener {
-        public final int mStepNo;
-        private final View mStepView;
-        private final TextView mBulletView;
-        private final int mActivatedColor;
-        private final int mDeactivatedColor;
-        private final String mInstruction;
-        private final String mFinishedInstruction;
-        private final TextView mActionLabel;
-        private Runnable mAction;
-
-        public SetupStep(final int stepNo, final String applicationName, final TextView bulletView,
-                final View stepView, final int title, final int instruction,
-                final int finishedInstruction,final int actionIcon, final int actionLabel) {
-            mStepNo = stepNo;
-            mStepView = stepView;
-            mBulletView = bulletView;
-            final Resources res = stepView.getResources();
-            mActivatedColor = res.getColor(R.color.setup_text_action);
-            mDeactivatedColor = res.getColor(R.color.setup_text_dark);
-
-            final TextView titleView = (TextView)mStepView.findViewById(R.id.setup_step_title);
-            titleView.setText(res.getString(title, applicationName));
-            mInstruction = (instruction == 0) ? null
-                    : res.getString(instruction, applicationName);
-            mFinishedInstruction = (finishedInstruction == 0) ? null
-                    : res.getString(finishedInstruction, applicationName);
-
-            mActionLabel = (TextView)mStepView.findViewById(R.id.setup_step_action_label);
-            mActionLabel.setText(res.getString(actionLabel));
-            if (actionIcon == 0) {
-                final int paddingEnd = ViewCompatUtils.getPaddingEnd(mActionLabel);
-                ViewCompatUtils.setPaddingRelative(mActionLabel, paddingEnd, 0, paddingEnd, 0);
-            } else {
-                TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
-                        mActionLabel, res.getDrawable(actionIcon), null, null, null);
-            }
-        }
-
-        public void setEnabled(final boolean enabled, final boolean isStepActionAlreadyDone) {
-            mStepView.setVisibility(enabled ? View.VISIBLE : View.GONE);
-            mBulletView.setTextColor(enabled ? mActivatedColor : mDeactivatedColor);
-            final TextView instructionView = (TextView)mStepView.findViewById(
-                    R.id.setup_step_instruction);
-            instructionView.setText(isStepActionAlreadyDone ? mFinishedInstruction : mInstruction);
-            mActionLabel.setVisibility(isStepActionAlreadyDone ? View.GONE : View.VISIBLE);
-        }
-
-        public void setAction(final Runnable action) {
-            mActionLabel.setOnClickListener(this);
-            mAction = action;
-        }
-
-        @Override
-        public void onClick(final View v) {
-            if (v == mActionLabel && mAction != null) {
-                mAction.run();
-                return;
-            }
-        }
-    }
-
-    static final class SetupStepGroup {
-        private final SetupStepIndicatorView mIndicatorView;
-        private final ArrayList<SetupStep> mGroup = CollectionUtils.newArrayList();
-
-        public SetupStepGroup(final SetupStepIndicatorView indicatorView) {
-            mIndicatorView = indicatorView;
-        }
-
-        public void addStep(final SetupStep step) {
-            mGroup.add(step);
-        }
-
-        public void enableStep(final int enableStepNo, final boolean isStepActionAlreadyDone) {
-            for (final SetupStep step : mGroup) {
-                step.setEnabled(step.mStepNo == enableStepNo, isStepActionAlreadyDone);
-            }
-            mIndicatorView.setIndicatorPosition(enableStepNo - STEP_1, mGroup.size());
-        }
-    }
 }
diff --git a/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
new file mode 100644
index 0000000..0d25bc3
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/setup/SetupWizardActivity.java
@@ -0,0 +1,472 @@
+/*
+ * Copyright (C) 2013 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.setup;
+
+import android.app.Activity;
+import android.content.ContentResolver;
+import android.content.Intent;
+import android.content.res.Resources;
+import android.media.MediaPlayer;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.Message;
+import android.provider.Settings;
+import android.util.Log;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.inputmethod.InputMethodInfo;
+import android.widget.ImageView;
+import android.widget.TextView;
+import android.widget.VideoView;
+
+import com.android.inputmethod.compat.TextViewCompatUtils;
+import com.android.inputmethod.compat.ViewCompatUtils;
+import com.android.inputmethod.latin.CollectionUtils;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.RichInputMethodManager;
+import com.android.inputmethod.latin.SettingsActivity;
+import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+
+import java.util.ArrayList;
+
+// TODO: Use Fragment to implement welcome screen and setup steps.
+public final class SetupWizardActivity extends Activity implements View.OnClickListener {
+    static final String TAG = SetupWizardActivity.class.getSimpleName();
+
+    private View mSetupWizard;
+    private View mWelcomeScreen;
+    private View mSetupScreen;
+    private Uri mWelcomeVideoUri;
+    private VideoView mWelcomeVideoView;
+    private View mActionStart;
+    private View mActionNext;
+    private TextView mStep1Bullet;
+    private TextView mActionFinish;
+    private SetupStepGroup mSetupStepGroup;
+    private static final String STATE_STEP = "step";
+    private int mStepNumber;
+    private static final int STEP_WELCOME = 0;
+    private static final int STEP_1 = 1;
+    private static final int STEP_2 = 2;
+    private static final int STEP_3 = 3;
+    private static final int STEP_LAUNCHING_IME_SETTINGS = 4;
+    private static final int STEP_BACK_FROM_IME_SETTINGS = 5;
+
+    final SettingsPoolingHandler mHandler = new SettingsPoolingHandler(this);
+
+    static final class SettingsPoolingHandler
+            extends StaticInnerHandlerWrapper<SetupWizardActivity> {
+        private static final int MSG_POLLING_IME_SETTINGS = 0;
+        private static final long IME_SETTINGS_POLLING_INTERVAL = 200;
+
+        public SettingsPoolingHandler(final SetupWizardActivity outerInstance) {
+            super(outerInstance);
+        }
+
+        @Override
+        public void handleMessage(final Message msg) {
+            final SetupWizardActivity setupWizardActivity = getOuterInstance();
+            if (setupWizardActivity == null) {
+                return;
+            }
+            switch (msg.what) {
+            case MSG_POLLING_IME_SETTINGS:
+                if (SetupActivity.isThisImeEnabled(setupWizardActivity)) {
+                    setupWizardActivity.invokeSetupWizardOfThisIme();
+                    return;
+                }
+                startPollingImeSettings();
+                break;
+            }
+        }
+
+        public void startPollingImeSettings() {
+            sendMessageDelayed(obtainMessage(MSG_POLLING_IME_SETTINGS),
+                    IME_SETTINGS_POLLING_INTERVAL);
+        }
+
+        public void cancelPollingImeSettings() {
+            removeMessages(MSG_POLLING_IME_SETTINGS);
+        }
+    }
+
+    @Override
+    protected void onCreate(final Bundle savedInstanceState) {
+        setTheme(android.R.style.Theme_Translucent_NoTitleBar);
+        super.onCreate(savedInstanceState);
+
+        setContentView(R.layout.setup_wizard);
+        mSetupWizard = findViewById(R.id.setup_wizard);
+
+        RichInputMethodManager.init(this);
+
+        if (savedInstanceState == null) {
+            mStepNumber = determineSetupStepNumberFromLauncher();
+        } else {
+            mStepNumber = savedInstanceState.getInt(STATE_STEP);
+        }
+
+        final String applicationName = getResources().getString(getApplicationInfo().labelRes);
+        mWelcomeScreen = findViewById(R.id.setup_welcome_screen);
+        final TextView welcomeTitle = (TextView)findViewById(R.id.setup_welcome_title);
+        welcomeTitle.setText(getString(R.string.setup_welcome_title, applicationName));
+
+        mSetupScreen = findViewById(R.id.setup_steps_screen);
+        final TextView stepsTitle = (TextView)findViewById(R.id.setup_title);
+        stepsTitle.setText(getString(R.string.setup_steps_title, applicationName));
+
+        final SetupStepIndicatorView indicatorView =
+                (SetupStepIndicatorView)findViewById(R.id.setup_step_indicator);
+        mSetupStepGroup = new SetupStepGroup(indicatorView);
+
+        mStep1Bullet = (TextView)findViewById(R.id.setup_step1_bullet);
+        mStep1Bullet.setOnClickListener(this);
+        final SetupStep step1 = new SetupStep(STEP_1, applicationName,
+                mStep1Bullet, findViewById(R.id.setup_step1),
+                R.string.setup_step1_title, R.string.setup_step1_instruction,
+                R.string.setup_step1_finished_instruction, R.drawable.ic_setup_step1,
+                R.string.setup_step1_action);
+        step1.setAction(new Runnable() {
+            @Override
+            public void run() {
+                invokeLanguageAndInputSettings();
+                mHandler.startPollingImeSettings();
+            }
+        });
+        mSetupStepGroup.addStep(step1);
+
+        final SetupStep step2 = new SetupStep(STEP_2, applicationName,
+                (TextView)findViewById(R.id.setup_step2_bullet), findViewById(R.id.setup_step2),
+                R.string.setup_step2_title, R.string.setup_step2_instruction,
+                0 /* finishedInstruction */, R.drawable.ic_setup_step2,
+                R.string.setup_step2_action);
+        step2.setAction(new Runnable() {
+            @Override
+            public void run() {
+                // Invoke input method picker.
+                RichInputMethodManager.getInstance().getInputMethodManager()
+                        .showInputMethodPicker();
+            }
+        });
+        mSetupStepGroup.addStep(step2);
+
+        final SetupStep step3 = new SetupStep(STEP_3, applicationName,
+                (TextView)findViewById(R.id.setup_step3_bullet), findViewById(R.id.setup_step3),
+                R.string.setup_step3_title, R.string.setup_step3_instruction,
+                0 /* finishedInstruction */, R.drawable.ic_setup_step3,
+                R.string.setup_step3_action);
+        step3.setAction(new Runnable() {
+            @Override
+            public void run() {
+                invokeSubtypeEnablerOfThisIme();
+            }
+        });
+        mSetupStepGroup.addStep(step3);
+
+        mWelcomeVideoUri = new Uri.Builder()
+                .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE)
+                .authority(getPackageName())
+                .path(Integer.toString(R.raw.setup_welcome_video))
+                .build();
+        final VideoView welcomeVideoView = (VideoView)findViewById(R.id.setup_welcome_video);
+        welcomeVideoView.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
+            @Override
+            public void onPrepared(final MediaPlayer mp) {
+                // Now VideoView has been laid-out and ready to play, remove background of it to
+                // reveal the video.
+                welcomeVideoView.setBackgroundResource(0);
+                mp.setLooping(true);
+            }
+        });
+        final ImageView welcomeImageView = (ImageView)findViewById(R.id.setup_welcome_image);
+        welcomeVideoView.setOnErrorListener(new MediaPlayer.OnErrorListener() {
+            @Override
+            public boolean onError(final MediaPlayer mp, final int what, final int extra) {
+                Log.e(TAG, "Playing welcome video causes error: what=" + what + " extra=" + extra);
+                welcomeVideoView.setVisibility(View.GONE);
+                welcomeImageView.setImageResource(R.raw.setup_welcome_image);
+                welcomeImageView.setVisibility(View.VISIBLE);
+                // Remove unnecessary light gray background around still image.
+                final ViewGroup videoFrame = (ViewGroup)findViewById(
+                        R.id.setup_welcome_video_frame);
+                videoFrame.setBackgroundColor(getResources().getColor(R.color.setup_background));
+                videoFrame.requestLayout();
+                return true;
+            }
+        });
+        mWelcomeVideoView = welcomeVideoView;
+
+        mActionStart = findViewById(R.id.setup_start_label);
+        mActionStart.setOnClickListener(this);
+        mActionNext = findViewById(R.id.setup_next);
+        mActionNext.setOnClickListener(this);
+        mActionFinish = (TextView)findViewById(R.id.setup_finish);
+        TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(mActionFinish,
+                getResources().getDrawable(R.drawable.ic_setup_finish), null, null, null);
+        mActionFinish.setOnClickListener(this);
+    }
+
+    @Override
+    public void onClick(final View v) {
+        if (v == mActionFinish) {
+            finish();
+            return;
+        }
+        final int currentStep = determineSetupStepNumber();
+        final int nextStep;
+        if (v == mActionStart) {
+            nextStep = STEP_1;
+        } else if (v == mActionNext) {
+            nextStep = mStepNumber + 1;
+        } else if (v == mStep1Bullet && currentStep == STEP_2) {
+            nextStep = STEP_1;
+        } else {
+            nextStep = mStepNumber;
+        }
+        if (mStepNumber != nextStep) {
+            mStepNumber = nextStep;
+            updateSetupStepView();
+        }
+    }
+
+    void invokeSetupWizardOfThisIme() {
+        final Intent intent = new Intent();
+        intent.setClass(this, SetupWizardActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                | Intent.FLAG_ACTIVITY_SINGLE_TOP
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(intent);
+    }
+
+    private void invokeSettingsOfThisIme() {
+        final Intent intent = new Intent();
+        intent.setClass(this, SettingsActivity.class);
+        intent.setFlags(Intent.FLAG_ACTIVITY_RESET_TASK_IF_NEEDED
+                | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+        startActivity(intent);
+    }
+
+    void invokeLanguageAndInputSettings() {
+        final Intent intent = new Intent();
+        intent.setAction(Settings.ACTION_INPUT_METHOD_SETTINGS);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        startActivity(intent);
+    }
+
+    void invokeSubtypeEnablerOfThisIme() {
+        final InputMethodInfo imi =
+                RichInputMethodManager.getInstance().getInputMethodInfoOfThisIme();
+        final Intent intent = new Intent();
+        intent.setAction(Settings.ACTION_INPUT_METHOD_SUBTYPE_SETTINGS);
+        intent.addCategory(Intent.CATEGORY_DEFAULT);
+        intent.putExtra(Settings.EXTRA_INPUT_METHOD_ID, imi.getId());
+        startActivity(intent);
+    }
+
+    private int determineSetupStepNumberFromLauncher() {
+        final int stepNumber = determineSetupStepNumber();
+        if (stepNumber == STEP_1) {
+            return STEP_WELCOME;
+        }
+        if (stepNumber == STEP_3) {
+            return STEP_LAUNCHING_IME_SETTINGS;
+        }
+        return stepNumber;
+    }
+
+    private int determineSetupStepNumber() {
+        mHandler.cancelPollingImeSettings();
+        if (!SetupActivity.isThisImeEnabled(this)) {
+            return STEP_1;
+        }
+        if (!SetupActivity.isThisImeCurrent(this)) {
+            return STEP_2;
+        }
+        return STEP_3;
+    }
+
+    @Override
+    protected void onSaveInstanceState(final Bundle outState) {
+        super.onSaveInstanceState(outState);
+        outState.putInt(STATE_STEP, mStepNumber);
+    }
+
+    @Override
+    protected void onRestoreInstanceState(final Bundle savedInstanceState) {
+        super.onRestoreInstanceState(savedInstanceState);
+        mStepNumber = savedInstanceState.getInt(STATE_STEP);
+    }
+
+    private static boolean isInSetupSteps(final int stepNumber) {
+        return stepNumber >= STEP_1 && stepNumber <= STEP_3;
+    }
+
+    @Override
+    protected void onRestart() {
+        super.onRestart();
+        if (isInSetupSteps(mStepNumber)) {
+            mStepNumber = determineSetupStepNumber();
+        }
+    }
+
+    @Override
+    protected void onResume() {
+        super.onResume();
+        if (mStepNumber == STEP_LAUNCHING_IME_SETTINGS) {
+            // Prevent white screen flashing while launching settings activity.
+            mSetupWizard.setVisibility(View.INVISIBLE);
+            invokeSettingsOfThisIme();
+            mStepNumber = STEP_BACK_FROM_IME_SETTINGS;
+            return;
+        }
+        if (mStepNumber == STEP_BACK_FROM_IME_SETTINGS) {
+            finish();
+            return;
+        }
+        updateSetupStepView();
+    }
+
+    @Override
+    public void onBackPressed() {
+        if (mStepNumber == STEP_1) {
+            mStepNumber = STEP_WELCOME;
+            updateSetupStepView();
+            return;
+        }
+        super.onBackPressed();
+    }
+
+    private static void hideAndStopVideo(final VideoView videoView) {
+        videoView.stopPlayback();
+        videoView.setVisibility(View.INVISIBLE);
+    }
+
+    @Override
+    protected void onPause() {
+        hideAndStopVideo(mWelcomeVideoView);
+        super.onPause();
+    }
+
+    @Override
+    public void onWindowFocusChanged(final boolean hasFocus) {
+        super.onWindowFocusChanged(hasFocus);
+        if (hasFocus && isInSetupSteps(mStepNumber)) {
+            mStepNumber = determineSetupStepNumber();
+            updateSetupStepView();
+        }
+    }
+
+    private void updateSetupStepView() {
+        mSetupWizard.setVisibility(View.VISIBLE);
+        final boolean welcomeScreen = (mStepNumber == STEP_WELCOME);
+        mWelcomeScreen.setVisibility(welcomeScreen ? View.VISIBLE : View.GONE);
+        mSetupScreen.setVisibility(welcomeScreen ? View.GONE : View.VISIBLE);
+        if (welcomeScreen) {
+            mWelcomeVideoView.setVisibility(View.VISIBLE);
+            mWelcomeVideoView.setVideoURI(mWelcomeVideoUri);
+            mWelcomeVideoView.start();
+            return;
+        }
+        hideAndStopVideo(mWelcomeVideoView);
+        final boolean isStepActionAlreadyDone = mStepNumber < determineSetupStepNumber();
+        mSetupStepGroup.enableStep(mStepNumber, isStepActionAlreadyDone);
+        mActionNext.setVisibility(isStepActionAlreadyDone ? View.VISIBLE : View.GONE);
+        mActionFinish.setVisibility((mStepNumber == STEP_3) ? View.VISIBLE : View.GONE);
+    }
+
+    static final class SetupStep implements View.OnClickListener {
+        public final int mStepNo;
+        private final View mStepView;
+        private final TextView mBulletView;
+        private final int mActivatedColor;
+        private final int mDeactivatedColor;
+        private final String mInstruction;
+        private final String mFinishedInstruction;
+        private final TextView mActionLabel;
+        private Runnable mAction;
+
+        public SetupStep(final int stepNo, final String applicationName, final TextView bulletView,
+                final View stepView, final int title, final int instruction,
+                final int finishedInstruction, final int actionIcon, final int actionLabel) {
+            mStepNo = stepNo;
+            mStepView = stepView;
+            mBulletView = bulletView;
+            final Resources res = stepView.getResources();
+            mActivatedColor = res.getColor(R.color.setup_text_action);
+            mDeactivatedColor = res.getColor(R.color.setup_text_dark);
+
+            final TextView titleView = (TextView)mStepView.findViewById(R.id.setup_step_title);
+            titleView.setText(res.getString(title, applicationName));
+            mInstruction = (instruction == 0) ? null
+                    : res.getString(instruction, applicationName);
+            mFinishedInstruction = (finishedInstruction == 0) ? null
+                    : res.getString(finishedInstruction, applicationName);
+
+            mActionLabel = (TextView)mStepView.findViewById(R.id.setup_step_action_label);
+            mActionLabel.setText(res.getString(actionLabel));
+            if (actionIcon == 0) {
+                final int paddingEnd = ViewCompatUtils.getPaddingEnd(mActionLabel);
+                ViewCompatUtils.setPaddingRelative(mActionLabel, paddingEnd, 0, paddingEnd, 0);
+            } else {
+                TextViewCompatUtils.setCompoundDrawablesRelativeWithIntrinsicBounds(
+                        mActionLabel, res.getDrawable(actionIcon), null, null, null);
+            }
+        }
+
+        public void setEnabled(final boolean enabled, final boolean isStepActionAlreadyDone) {
+            mStepView.setVisibility(enabled ? View.VISIBLE : View.GONE);
+            mBulletView.setTextColor(enabled ? mActivatedColor : mDeactivatedColor);
+            final TextView instructionView = (TextView)mStepView.findViewById(
+                    R.id.setup_step_instruction);
+            instructionView.setText(isStepActionAlreadyDone ? mFinishedInstruction : mInstruction);
+            mActionLabel.setVisibility(isStepActionAlreadyDone ? View.GONE : View.VISIBLE);
+        }
+
+        public void setAction(final Runnable action) {
+            mActionLabel.setOnClickListener(this);
+            mAction = action;
+        }
+
+        @Override
+        public void onClick(final View v) {
+            if (v == mActionLabel && mAction != null) {
+                mAction.run();
+                return;
+            }
+        }
+    }
+
+    static final class SetupStepGroup {
+        private final SetupStepIndicatorView mIndicatorView;
+        private final ArrayList<SetupStep> mGroup = CollectionUtils.newArrayList();
+
+        public SetupStepGroup(final SetupStepIndicatorView indicatorView) {
+            mIndicatorView = indicatorView;
+        }
+
+        public void addStep(final SetupStep step) {
+            mGroup.add(step);
+        }
+
+        public void enableStep(final int enableStepNo, final boolean isStepActionAlreadyDone) {
+            for (final SetupStep step : mGroup) {
+                step.setEnabled(step.mStepNo == enableStepNo, isStepActionAlreadyDone);
+            }
+            mIndicatorView.setIndicatorPosition(enableStepNo - STEP_1, mGroup.size());
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index aa60496..13fcaf4 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -23,7 +23,7 @@
 import android.util.Log;
 import android.view.textservice.SuggestionsInfo;
 
-import com.android.inputmethod.keyboard.ProximityInfo;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
 import com.android.inputmethod.latin.BinaryDictionary;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.ContactsBinaryDictionary;
@@ -126,6 +126,19 @@
         return script;
     }
 
+    private static String getKeyboardLayoutNameForScript(final int script) {
+        switch (script) {
+        case AndroidSpellCheckerService.SCRIPT_LATIN:
+            return "qwerty";
+        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
+            return "east_slavic";
+        case AndroidSpellCheckerService.SCRIPT_GREEK:
+            return "greek";
+        default:
+            throw new RuntimeException("Wrong script supplied: " + script);
+        }
+    }
+
     @Override
     public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
         if (!PREF_USE_CONTACTS_KEY.equals(key)) return;
@@ -385,9 +398,13 @@
         return pool;
     }
 
-    public DictAndProximity createDictAndProximity(final Locale locale) {
+    public DictAndKeyboard createDictAndKeyboard(final Locale locale) {
         final int script = getScriptFromLocale(locale);
-        final ProximityInfo proximityInfo = new SpellCheckerProximityInfo(script);
+        final String keyboardLayoutName = getKeyboardLayoutNameForScript(script);
+        final KeyboardLayoutSet keyboardLayoutSet =
+                KeyboardLayoutSet.createKeyboardSetForSpellChecker(this, locale.toString(),
+                        keyboardLayoutName);
+
         final DictionaryCollection dictionaryCollection =
                 DictionaryFactory.createMainDictionaryFromManager(this, locale,
                         true /* useFullEditDistance */);
@@ -412,6 +429,6 @@
             mDictionaryCollectionsList.add(
                     new WeakReference<DictionaryCollection>(dictionaryCollection));
         }
-        return new DictAndProximity(dictionaryCollection, proximityInfo);
+        return new DictAndKeyboard(dictionaryCollection, keyboardLayoutSet);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index 61850e4..16e9fb7 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -257,7 +257,7 @@
             }
 
             if (shouldFilterOut(inText, mScript)) {
-                DictAndProximity dictInfo = null;
+                DictAndKeyboard dictInfo = null;
                 try {
                     dictInfo = mDictionaryPool.pollWithDefaultTimeout();
                     if (!DictionaryPool.isAValidDictionary(dictInfo)) {
@@ -286,7 +286,7 @@
 
             final int capitalizeType = StringUtils.getCapitalizationType(text);
             boolean isInDict = true;
-            DictAndProximity dictInfo = null;
+            DictAndKeyboard dictInfo = null;
             try {
                 dictInfo = mDictionaryPool.pollWithDefaultTimeout();
                 if (!DictionaryPool.isAValidDictionary(dictInfo)) {
@@ -296,20 +296,13 @@
                 final int length = text.length();
                 for (int i = 0; i < length; i = text.offsetByCodePoints(i, 1)) {
                     final int codePoint = text.codePointAt(i);
-                    // The getXYForCodePointAndScript method returns (Y << 16) + X
-                    final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
-                            codePoint, mScript);
-                    if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
-                        composer.add(codePoint,
-                                Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE);
-                    } else {
-                        composer.add(codePoint, xy & 0xFFFF, xy >> 16);
-                    }
+                    composer.addKeyInfo(codePoint, dictInfo.getKeyboard(codePoint));
                 }
                 // TODO: make a spell checker option to block offensive words or not
                 final ArrayList<SuggestedWordInfo> suggestions =
                         dictInfo.mDictionary.getSuggestions(composer, prevWord,
-                                dictInfo.mProximityInfo, true /* blockOffensiveWords */);
+                                dictInfo.getProximityInfo(),
+                                true /* blockOffensiveWords */);
                 for (final SuggestedWordInfo suggestion : suggestions) {
                     final String suggestionStr = suggestion.mWord;
                     suggestionsGatherer.addWord(suggestionStr.toCharArray(), null, 0,
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java
new file mode 100644
index 0000000..b77f3e2
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictAndKeyboard.java
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2011 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin.spellcheck;
+
+import com.android.inputmethod.latin.Dictionary;
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardLayoutSet;
+import com.android.inputmethod.keyboard.ProximityInfo;
+
+/**
+ * A container for a Dictionary and a Keyboard.
+ */
+public final class DictAndKeyboard {
+    public final Dictionary mDictionary;
+    private final Keyboard mKeyboard;
+    private final Keyboard mManualShiftedKeyboard;
+
+    public DictAndKeyboard(
+            final Dictionary dictionary, final KeyboardLayoutSet keyboardLayoutSet) {
+        mDictionary = dictionary;
+        if (keyboardLayoutSet == null) {
+            mKeyboard = null;
+            mManualShiftedKeyboard = null;
+            return;
+        }
+        mKeyboard = keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET);
+        mManualShiftedKeyboard =
+                keyboardLayoutSet.getKeyboard(KeyboardId.ELEMENT_ALPHABET_MANUAL_SHIFTED);
+    }
+
+    public Keyboard getKeyboard(final int codePoint) {
+        if (mKeyboard == null) {
+            return null;
+        }
+        return mKeyboard.getKey(codePoint) != null ? mKeyboard : mManualShiftedKeyboard;
+    }
+
+    public ProximityInfo getProximityInfo() {
+        return mKeyboard == null ? null : mKeyboard.getProximityInfo();
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java b/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
deleted file mode 100644
index 017a4f5..0000000
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictAndProximity.java
+++ /dev/null
@@ -1,32 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin.spellcheck;
-
-import com.android.inputmethod.latin.Dictionary;
-import com.android.inputmethod.keyboard.ProximityInfo;
-
-/**
- * A simple container for both a Dictionary and a ProximityInfo.
- */
-public final class DictAndProximity {
-    public final Dictionary mDictionary;
-    public final ProximityInfo mProximityInfo;
-    public DictAndProximity(final Dictionary dictionary, final ProximityInfo proximityInfo) {
-        mDictionary = dictionary;
-        mProximityInfo = proximityInfo;
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
index 27964b3..a20e09e 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/DictionaryPool.java
@@ -36,7 +36,7 @@
  * the client code, but may help with sloppy clients.
  */
 @SuppressWarnings("serial")
-public final class DictionaryPool extends LinkedBlockingQueue<DictAndProximity> {
+public final class DictionaryPool extends LinkedBlockingQueue<DictAndKeyboard> {
     private final static String TAG = DictionaryPool.class.getSimpleName();
     // How many seconds we wait for a dictionary to become available. Past this delay, we give up in
     // fear some bug caused a deadlock, and reset the whole pool.
@@ -47,7 +47,7 @@
     private int mSize;
     private volatile boolean mClosed;
     final static ArrayList<SuggestedWordInfo> noSuggestions = CollectionUtils.newArrayList();
-    private final static DictAndProximity dummyDict = new DictAndProximity(
+    private final static DictAndKeyboard dummyDict = new DictAndKeyboard(
             new Dictionary(Dictionary.TYPE_MAIN) {
                 @Override
                 public ArrayList<SuggestedWordInfo> getSuggestions(final WordComposer composer,
@@ -64,7 +64,7 @@
                 }
             }, null);
 
-    static public boolean isAValidDictionary(final DictAndProximity dictInfo) {
+    static public boolean isAValidDictionary(final DictAndKeyboard dictInfo) {
         return null != dictInfo && dummyDict != dictInfo;
     }
 
@@ -79,32 +79,32 @@
     }
 
     @Override
-    public DictAndProximity poll(final long timeout, final TimeUnit unit)
+    public DictAndKeyboard poll(final long timeout, final TimeUnit unit)
             throws InterruptedException {
-        final DictAndProximity dict = poll();
+        final DictAndKeyboard dict = poll();
         if (null != dict) return dict;
         synchronized(this) {
             if (mSize >= mMaxSize) {
                 // Our pool is already full. Wait until some dictionary is ready, or TIMEOUT
                 // expires to avoid a deadlock.
-                final DictAndProximity result = super.poll(timeout, unit);
+                final DictAndKeyboard result = super.poll(timeout, unit);
                 if (null == result) {
                     Log.e(TAG, "Deadlock detected ! Resetting dictionary pool");
                     clear();
                     mSize = 1;
-                    return mService.createDictAndProximity(mLocale);
+                    return mService.createDictAndKeyboard(mLocale);
                 } else {
                     return result;
                 }
             } else {
                 ++mSize;
-                return mService.createDictAndProximity(mLocale);
+                return mService.createDictAndKeyboard(mLocale);
             }
         }
     }
 
     // Convenience method
-    public DictAndProximity pollWithDefaultTimeout() {
+    public DictAndKeyboard pollWithDefaultTimeout() {
         try {
             return poll(TIMEOUT, TimeUnit.SECONDS);
         } catch (InterruptedException e) {
@@ -115,7 +115,7 @@
     public void close() {
         synchronized(this) {
             mClosed = true;
-            for (DictAndProximity dict : this) {
+            for (DictAndKeyboard dict : this) {
                 dict.mDictionary.close();
             }
             clear();
@@ -123,7 +123,7 @@
     }
 
     @Override
-    public boolean offer(final DictAndProximity dict) {
+    public boolean offer(final DictAndKeyboard dict) {
         if (mClosed) {
             dict.mDictionary.close();
             return super.offer(dummyDict);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
deleted file mode 100644
index 0c480ea..0000000
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ /dev/null
@@ -1,462 +0,0 @@
-/*
- * Copyright (C) 2011 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.inputmethod.latin.spellcheck;
-
-import android.util.SparseIntArray;
-
-import com.android.inputmethod.keyboard.ProximityInfo;
-import com.android.inputmethod.latin.Constants;
-
-public final class SpellCheckerProximityInfo extends ProximityInfo {
-    public SpellCheckerProximityInfo(final int script) {
-        super(getProximityForScript(script), PROXIMITY_GRID_WIDTH, PROXIMITY_GRID_HEIGHT);
-    }
-
-    private static final int NUL = Constants.NOT_A_CODE;
-
-    // This must be the same as MAX_PROXIMITY_CHARS_SIZE else it will not work inside
-    // native code - this value is passed at creation of the binary object and reused
-    // as the size of the passed array afterwards so they can't be different.
-    private static final int ROW_SIZE = ProximityInfo.MAX_PROXIMITY_CHARS_SIZE;
-
-    // The number of keys in a row of the grid used by the spell checker.
-    private static final int PROXIMITY_GRID_WIDTH = 11;
-    // The number of rows in the grid used by the spell checker.
-    private static final int PROXIMITY_GRID_HEIGHT = 3;
-
-    private static final int NOT_AN_INDEX = -1;
-    public static final int NOT_A_COORDINATE_PAIR = -1;
-
-    // Helper methods
-    static void buildProximityIndices(final int[] proximity, final int rowSize,
-            final SparseIntArray indices) {
-        for (int i = 0; i < proximity.length; i += rowSize) {
-            if (NUL != proximity[i]) indices.put(proximity[i], i / rowSize);
-        }
-    }
-
-    private static final class Latin {
-        // The proximity here is the union of
-        // - the proximity for a QWERTY keyboard.
-        // - the proximity for an AZERTY keyboard.
-        // - the proximity for a QWERTZ keyboard.
-        // ...plus, add all characters in the ('a', 'e', 'i', 'o', 'u') set to each other.
-        //
-        // The reasoning behind this construction is, almost any alphabetic text we may want
-        // to spell check has been entered with one of the keyboards above. Also, specifically
-        // to English, many spelling errors consist of the last vowel of the word being wrong
-        // because in English vowels tend to merge with each other in pronunciation.
-        /*
-        The Qwerty layout this represents looks like the following:
-            q w e r t y u i o p
-             a s d f g h j k l
-               z x c v b n m
-        */
-        static final int[] PROXIMITY = {
-            // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
-            // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
-            // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
-            'q', 'w', 's', 'a', 'z', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'w', 'q', 'a', 's', 'd', 'e', 'x', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'e', 'w', 's', 'd', 'f', 'r', 'a', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
-            'r', 'e', 'd', 'f', 'g', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            't', 'r', 'f', 'g', 'h', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'y', 't', 'g', 'h', 'j', 'u', 'a', 's', 'd', 'x', NUL, NUL, NUL, NUL, NUL, NUL,
-            'u', 'y', 'h', 'j', 'k', 'i', 'a', 'e', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'i', 'u', 'j', 'k', 'l', 'o', 'a', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'o', 'i', 'k', 'l', 'p', 'a', 'e', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'p', 'o', 'l', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            // Proximity for row 2. See comment above about size.
-            'a', 'z', 'x', 's', 'w', 'q', 'e', 'i', 'o', 'u', NUL, NUL, NUL, NUL, NUL, NUL,
-            's', 'q', 'a', 'z', 'x', 'c', 'd', 'e', 'w', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'd', 'w', 's', 'x', 'c', 'v', 'f', 'r', 'e', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'f', 'e', 'd', 'c', 'v', 'b', 'g', 't', 'r', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'g', 'r', 'f', 'v', 'b', 'n', 'h', 'y', 't', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'h', 't', 'g', 'b', 'n', 'm', 'j', 'u', 'y', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'j', 'y', 'h', 'n', 'm', 'k', 'i', 'u', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'k', 'u', 'j', 'm', 'l', 'o', 'i', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'l', 'i', 'k', 'p', 'o', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            // Proximity for row 3. See comment above about size.
-            'z', 'a', 's', 'd', 'x', 't', 'g', 'h', 'j', 'u', 'q', 'e', NUL, NUL, NUL, NUL,
-            'x', 'z', 'a', 's', 'd', 'c', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'c', 'x', 's', 'd', 'f', 'v', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'v', 'c', 'd', 'f', 'g', 'b', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'b', 'v', 'f', 'g', 'h', 'n', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'n', 'b', 'g', 'h', 'j', 'm', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'm', 'n', 'h', 'j', 'k', 'l', 'o', 'p', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-        };
-
-        // This is a mapping array from the code point to the index in the PROXIMITY array.
-        // When we check the spelling of a word, we need to pass (x,y) coordinates to the native
-        // code for each letter of the word. These are most easily computed from the index in the
-        // PROXIMITY array. Since we'll need to do that very often, the index lookup from the code
-        // point needs to be as fast as possible, and a map is probably the best way to do this.
-        // To avoid unnecessary boxing conversion to Integer, here we use SparseIntArray.
-        static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
-        static {
-            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
-        }
-    }
-
-    private static final class Cyrillic {
-        // TODO: The following table is solely based on the keyboard layout. Consult with Russian
-        // speakers on commonly misspelled words/letters.
-        /*
-        The Russian layout this represents looks like the following:
-            й ц у к е н г ш щ з х
-            ф ы в а п р о л д ж э
-              я ч с м и т ь б ю
-
-        This gives us the following table:
-            'й', 'ц', 'ф', 'ы', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ц', 'й', 'ф', 'ы', 'в', 'у', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'у', 'ц', 'ы', 'в', 'а', 'к', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'к', 'у', 'в', 'а', 'п', 'е', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'е', 'к', 'а', 'п', 'р', 'н', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'н', 'е', 'п', 'р', 'о', 'г', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'г', 'н', 'р', 'о', 'л', 'ш', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ш', 'г', 'о', 'л', 'д', 'щ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'щ', 'ш', 'л', 'д', 'ж', 'з', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'з', 'щ', 'д', 'ж', 'э', 'х', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'х', 'з', 'ж', 'э', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            'ф', 'й', 'ц', 'ы', 'я', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ы', 'й', 'ц', 'у', 'ф', 'в', 'я', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'в', 'ц', 'у', 'к', 'ы', 'а', 'я', 'ч', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'а', 'у', 'к', 'е', 'в', 'п', 'ч', 'с', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'п', 'к', 'е', 'н', 'а', 'р', 'с', 'м', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'р', 'е', 'н', 'г', 'п', 'о', 'м', 'и', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'о', 'н', 'г', 'ш', 'р', 'л', 'и', 'т', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'л', 'г', 'ш', 'щ', 'о', 'д', 'т', 'ь', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'д', 'ш', 'щ', 'з', 'л', 'ж', 'ь', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ж', 'щ', 'з', 'х', 'д', 'э', 'б', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'э', 'з', 'х', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            'я', 'ф', 'ы', 'в', 'ч', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ч', 'ы', 'в', 'а', 'я', 'с', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'с', 'в', 'а', 'п', 'ч', 'м', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'м', 'а', 'п', 'р', 'с', 'и', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'и', 'п', 'р', 'о', 'м', 'т', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'т', 'р', 'о', 'л', 'и', 'ь', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ь', 'о', 'л', 'д', 'т', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'б', 'л', 'д', 'ж', 'ь', 'ю', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ю', 'д', 'ж', 'э', 'б', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-        Using the following characters:
-        */
-        private static final int CY_SHORT_I = '\u0439'; // й
-        private static final int CY_TSE = '\u0446'; // ц
-        private static final int CY_U = '\u0443'; // у
-        private static final int CY_KA = '\u043A'; // к
-        private static final int CY_IE = '\u0435'; // е
-        private static final int CY_EN = '\u043D'; // н
-        private static final int CY_GHE = '\u0433'; // г
-        private static final int CY_SHA = '\u0448'; // ш
-        private static final int CY_SHCHA = '\u0449'; // щ
-        private static final int CY_ZE = '\u0437'; // з
-        private static final int CY_HA = '\u0445'; // х
-        private static final int CY_EF = '\u0444'; // ф
-        private static final int CY_YERU = '\u044B'; // ы
-        private static final int CY_VE = '\u0432'; // в
-        private static final int CY_A = '\u0430'; // а
-        private static final int CY_PE = '\u043F'; // п
-        private static final int CY_ER = '\u0440'; // р
-        private static final int CY_O = '\u043E'; // о
-        private static final int CY_EL = '\u043B'; // л
-        private static final int CY_DE = '\u0434'; // д
-        private static final int CY_ZHE = '\u0436'; // ж
-        private static final int CY_E = '\u044D'; // э
-        private static final int CY_YA = '\u044F'; // я
-        private static final int CY_CHE = '\u0447'; // ч
-        private static final int CY_ES = '\u0441'; // с
-        private static final int CY_EM = '\u043C'; // м
-        private static final int CY_I = '\u0438'; // и
-        private static final int CY_TE = '\u0442'; // т
-        private static final int CY_SOFT_SIGN = '\u044C'; // ь
-        private static final int CY_BE = '\u0431'; // б
-        private static final int CY_YU = '\u044E'; // ю
-        static final int[] PROXIMITY = {
-            // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
-            // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
-            // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
-            CY_SHORT_I, CY_TSE, CY_EF, CY_YERU, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_TSE, CY_SHORT_I, CY_EF, CY_YERU, CY_VE, CY_U, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_U, CY_TSE, CY_YERU, CY_VE, CY_A, CY_KA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_KA, CY_U, CY_VE, CY_A, CY_PE, CY_IE, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_IE, CY_KA, CY_A, CY_PE, CY_ER, CY_EN, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_EN, CY_IE, CY_PE, CY_ER, CY_O, CY_GHE, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_GHE, CY_EN, CY_ER, CY_O, CY_EL, CY_SHA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_SHA, CY_GHE, CY_O, CY_EL, CY_DE, CY_SHCHA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_SHCHA, CY_SHA, CY_EL, CY_DE, CY_ZHE, CY_ZE, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_ZE, CY_SHCHA, CY_DE, CY_ZHE, CY_E, CY_HA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_HA, CY_ZE, CY_ZHE, CY_E, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            // Proximity for row 2. See comment above about size.
-            CY_EF, CY_SHORT_I, CY_TSE, CY_YERU, CY_YA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_YERU, CY_SHORT_I, CY_TSE, CY_U, CY_EF, CY_VE, CY_YA, CY_CHE,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_VE, CY_TSE, CY_U, CY_KA, CY_YERU, CY_A, CY_YA, CY_CHE,
-                    CY_ES, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_A, CY_U, CY_KA, CY_IE, CY_VE, CY_PE, CY_CHE, CY_ES,
-                    CY_EM, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_PE, CY_KA, CY_IE, CY_EN, CY_A, CY_ER, CY_ES, CY_EM,
-                    CY_I, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_ER, CY_IE, CY_EN, CY_GHE, CY_PE, CY_O, CY_EM, CY_I,
-                    CY_TE, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_O, CY_EN, CY_GHE, CY_SHA, CY_ER, CY_EL, CY_I, CY_TE,
-                    CY_SOFT_SIGN, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_EL, CY_GHE, CY_SHA, CY_SHCHA, CY_O, CY_DE, CY_TE, CY_SOFT_SIGN,
-                    CY_BE, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_DE, CY_SHA, CY_SHCHA, CY_ZE, CY_EL, CY_ZHE, CY_SOFT_SIGN, CY_BE,
-                    CY_YU, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_ZHE, CY_SHCHA, CY_ZE, CY_HA, CY_DE, CY_E, CY_BE, CY_YU,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_E, CY_ZE, CY_HA, CY_YU, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            // Proximity for row 3. See comment above about size.
-            CY_YA, CY_EF, CY_YERU, CY_VE, CY_CHE, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_CHE, CY_YERU, CY_VE, CY_A, CY_YA, CY_ES, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_ES, CY_VE, CY_A, CY_PE, CY_CHE, CY_EM, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_EM, CY_A, CY_PE, CY_ER, CY_ES, CY_I, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_I, CY_PE, CY_ER, CY_O, CY_EM, CY_TE, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_TE, CY_ER, CY_O, CY_EL, CY_I, CY_SOFT_SIGN, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_SOFT_SIGN, CY_O, CY_EL, CY_DE, CY_TE, CY_BE, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_BE, CY_EL, CY_DE, CY_ZHE, CY_SOFT_SIGN, CY_YU, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            CY_YU, CY_DE, CY_ZHE, CY_E, CY_BE, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-        };
-
-        static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
-        static {
-            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
-        }
-    }
-
-    private static final class Greek {
-        // TODO: The following table is solely based on the keyboard layout. Consult with Greek
-        // speakers on commonly misspelled words/letters.
-        /*
-        The Greek layout this represents looks like the following:
-            ; ς ε ρ τ υ θ ι ο π
-             α σ δ φ γ η ξ κ λ
-               ζ χ ψ ω β ν μ
-
-        This gives us the following table:
-            'ς', 'ε', 'α', 'σ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ε', 'ς', 'ρ', 'σ', 'δ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ρ', 'ε', 'τ', 'δ', 'φ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'τ', 'ρ', 'υ', 'φ', 'γ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'υ', 'τ', 'θ', 'γ', 'η', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'θ', 'υ', 'ι', 'η', 'ξ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ι', 'θ', 'ο', 'ξ', 'κ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ο', 'ι', 'π', 'κ', 'λ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'π', 'ο', 'λ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            'α', 'ς', 'σ', 'ζ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'σ', 'ς', 'ε', 'α', 'δ', 'ζ', 'χ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'δ', 'ε', 'ρ', 'σ', 'φ', 'ζ', 'χ', 'ψ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'φ', 'ρ', 'τ', 'δ', 'γ', 'χ', 'ψ', 'ω', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'γ', 'τ', 'υ', 'φ', 'η', 'ψ', 'ω', 'β', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'η', 'υ', 'θ', 'γ', 'ξ', 'ω', 'β', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ξ', 'θ', 'ι', 'η', 'κ', 'β', 'ν', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'κ', 'ι', 'ο', 'ξ', 'λ', 'ν', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'λ', 'ο', 'π', 'κ', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            'ζ', 'α', 'σ', 'δ', 'χ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'χ', 'σ', 'δ', 'φ', 'ζ', 'ψ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ψ', 'δ', 'φ', 'γ', 'χ', 'ω', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ω', 'φ', 'γ', 'η', 'ψ', 'β', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'β', 'γ', 'η', 'ξ', 'ω', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'ν', 'η', 'ξ', 'κ', 'β', 'μ', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            'μ', 'ξ', 'κ', 'λ', 'ν', NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-        Using the following characters:
-        */
-        private static final int GR_FINAL_SIGMA = '\u03C2'; // ς
-        private static final int GR_EPSILON = '\u03B5'; // ε
-        private static final int GR_RHO = '\u03C1'; // ρ
-        private static final int GR_TAU = '\u03C4'; // τ
-        private static final int GR_UPSILON = '\u03C5'; // υ
-        private static final int GR_THETA = '\u03B8'; // θ
-        private static final int GR_IOTA = '\u03B9'; // ι
-        private static final int GR_OMICRON = '\u03BF'; // ο
-        private static final int GR_PI = '\u03C0'; // π
-        private static final int GR_ALPHA = '\u03B1'; // α
-        private static final int GR_SIGMA = '\u03C3'; // σ
-        private static final int GR_DELTA = '\u03B4'; // δ
-        private static final int GR_PHI = '\u03C6'; // φ
-        private static final int GR_GAMMA = '\u03B3'; // γ
-        private static final int GR_ETA = '\u03B7'; // η
-        private static final int GR_XI = '\u03BE'; // ξ
-        private static final int GR_KAPPA = '\u03BA'; // κ
-        private static final int GR_LAMDA = '\u03BB'; // λ
-        private static final int GR_ZETA = '\u03B6'; // ζ
-        private static final int GR_CHI = '\u03C7'; // χ
-        private static final int GR_PSI = '\u03C8'; // ψ
-        private static final int GR_OMEGA = '\u03C9'; // ω
-        private static final int GR_BETA = '\u03B2'; // β
-        private static final int GR_NU = '\u03BD'; // ν
-        private static final int GR_MU = '\u03BC'; // μ
-        static final int[] PROXIMITY = {
-            // Proximity for row 1. This must have exactly ROW_SIZE entries for each letter,
-            // and exactly PROXIMITY_GRID_WIDTH letters for a row. Pad with NUL's.
-            // The number of rows must be exactly PROXIMITY_GRID_HEIGHT.
-            GR_FINAL_SIGMA, GR_EPSILON, GR_ALPHA, GR_SIGMA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_EPSILON, GR_FINAL_SIGMA, GR_RHO, GR_SIGMA, GR_DELTA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_RHO, GR_EPSILON, GR_TAU, GR_DELTA, GR_PHI, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_TAU, GR_RHO, GR_UPSILON, GR_PHI, GR_GAMMA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_UPSILON, GR_TAU, GR_THETA, GR_GAMMA, GR_ETA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_THETA, GR_UPSILON, GR_IOTA, GR_ETA, GR_XI, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_IOTA, GR_THETA, GR_OMICRON, GR_XI, GR_KAPPA, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_OMICRON, GR_IOTA, GR_PI, GR_KAPPA, GR_LAMDA, NUL, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_PI, GR_OMICRON, GR_LAMDA, NUL, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            GR_ALPHA, GR_FINAL_SIGMA, GR_SIGMA, GR_ZETA, NUL, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_SIGMA, GR_FINAL_SIGMA, GR_EPSILON, GR_ALPHA, GR_DELTA, GR_ZETA, GR_CHI, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_DELTA, GR_EPSILON, GR_RHO, GR_SIGMA, GR_PHI, GR_ZETA, GR_CHI, GR_PSI,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_PHI, GR_RHO, GR_TAU, GR_DELTA, GR_GAMMA, GR_CHI, GR_PSI, GR_OMEGA,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_GAMMA, GR_TAU, GR_UPSILON, GR_PHI, GR_ETA, GR_PSI, GR_OMEGA, GR_BETA,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_ETA, GR_UPSILON, GR_THETA, GR_GAMMA, GR_XI, GR_OMEGA, GR_BETA, GR_NU,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_XI, GR_THETA, GR_IOTA, GR_ETA, GR_KAPPA, GR_BETA, GR_NU, GR_MU,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_KAPPA, GR_IOTA, GR_OMICRON, GR_XI, GR_LAMDA, GR_NU, GR_MU, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_LAMDA, GR_OMICRON, GR_PI, GR_KAPPA, GR_MU, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-
-            GR_ZETA, GR_ALPHA, GR_SIGMA, GR_DELTA, GR_CHI, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_CHI, GR_SIGMA, GR_DELTA, GR_PHI, GR_ZETA, GR_PSI, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_PSI, GR_DELTA, GR_PHI, GR_GAMMA, GR_CHI, GR_OMEGA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_OMEGA, GR_PHI, GR_GAMMA, GR_ETA, GR_PSI, GR_BETA, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_BETA, GR_GAMMA, GR_ETA, GR_XI, GR_OMEGA, GR_NU, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_NU, GR_ETA, GR_XI, GR_KAPPA, GR_BETA, GR_MU, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            GR_MU, GR_XI, GR_KAPPA, GR_LAMDA, GR_NU, NUL, NUL, NUL,
-                    NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-            NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL, NUL,
-        };
-
-        static final SparseIntArray INDICES = new SparseIntArray(PROXIMITY.length / ROW_SIZE);
-
-        static {
-            buildProximityIndices(PROXIMITY, ROW_SIZE, INDICES);
-        }
-    }
-
-    private static int[] getProximityForScript(final int script) {
-        switch (script) {
-        case AndroidSpellCheckerService.SCRIPT_LATIN:
-            return Latin.PROXIMITY;
-        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
-            return Cyrillic.PROXIMITY;
-        case AndroidSpellCheckerService.SCRIPT_GREEK:
-            return Greek.PROXIMITY;
-        default:
-            throw new RuntimeException("Wrong script supplied: " + script);
-        }
-    }
-
-    private static int getIndexOfCodeForScript(final int codePoint, final int script) {
-        switch (script) {
-        case AndroidSpellCheckerService.SCRIPT_LATIN:
-            return Latin.INDICES.get(codePoint, NOT_AN_INDEX);
-        case AndroidSpellCheckerService.SCRIPT_CYRILLIC:
-            return Cyrillic.INDICES.get(codePoint, NOT_AN_INDEX);
-        case AndroidSpellCheckerService.SCRIPT_GREEK:
-            return Greek.INDICES.get(codePoint, NOT_AN_INDEX);
-        default:
-            throw new RuntimeException("Wrong script supplied: " + script);
-        }
-    }
-
-    // Returns (Y << 16) + X to avoid creating a temporary object. This is okay because
-    // X and Y are limited to PROXIMITY_GRID_WIDTH resp. PROXIMITY_GRID_HEIGHT which is very
-    // inferior to 1 << 16
-    // As an exception, this returns NOT_A_COORDINATE_PAIR if the key is not on the grid
-    public static int getXYForCodePointAndScript(final int codePoint, final int script) {
-        final int index = getIndexOfCodeForScript(codePoint, script);
-        if (NOT_AN_INDEX == index) return NOT_A_COORDINATE_PAIR;
-        final int y = index / PROXIMITY_GRID_WIDTH;
-        final int x = index % PROXIMITY_GRID_WIDTH;
-        if (y > PROXIMITY_GRID_HEIGHT) {
-            // Safety check, should be entirely useless
-            throw new RuntimeException("Wrong y coordinate in spell checker proximity");
-        }
-        return (y << 16) + x;
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
index 5f4c446..58c8f26 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionaryAddWordFragment.java
@@ -74,14 +74,14 @@
 
     @Override
     public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
+        final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0,
+                R.string.user_dict_settings_add_menu_title).setIcon(R.drawable.ic_menu_add);
+        actionItemAdd.setShowAsAction(
+                MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
         final MenuItem actionItemDelete = menu.add(0, OPTIONS_MENU_DELETE, 0,
                 R.string.user_dict_settings_delete).setIcon(android.R.drawable.ic_menu_delete);
         actionItemDelete.setShowAsAction(
                 MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
-        final MenuItem actionItemAdd = menu.add(0, OPTIONS_MENU_ADD, 0,
-                R.string.user_dict_settings_delete).setIcon(R.drawable.ic_menu_add);
-        actionItemAdd.setShowAsAction(
-                MenuItem.SHOW_AS_ACTION_IF_ROOM | MenuItem.SHOW_AS_ACTION_WITH_TEXT);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
index 36bc5ba..50dda96 100644
--- a/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
+++ b/java/src/com/android/inputmethod/latin/userdictionary/UserDictionarySettings.java
@@ -108,6 +108,7 @@
     @Override
     public void onCreate(Bundle savedInstanceState) {
         super.onCreate(savedInstanceState);
+        getActivity().getActionBar().setTitle(R.string.edit_personal_dictionary);
     }
 
     @Override
diff --git a/java/src/com/android/inputmethod/research/FixedLogBuffer.java b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
index 4249af5..8b64de8 100644
--- a/java/src/com/android/inputmethod/research/FixedLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/FixedLogBuffer.java
@@ -65,6 +65,7 @@
         final int numWordsIncoming = newLogUnit.getNumWords();
         if (mNumActualWords >= mWordCapacity) {
             // Give subclass a chance to handle the buffer full condition by shifting out logUnits.
+            // TODO: Tell onBufferFull() how much space it needs to make to avoid forced eviction.
             onBufferFull();
             // If still full, evict.
             if (mNumActualWords >= mWordCapacity) {
@@ -119,21 +120,19 @@
     /**
      * Remove LogUnits from the front of the LogBuffer until {@code numWords} have been removed.
      *
-     * If there are less than {@code numWords} word-containing {@link LogUnit}s, shifts out
-     * all {@code LogUnit}s in the buffer.
+     * If there are less than {@code numWords} in the buffer, shifts out all {@code LogUnit}s.
      *
-     * @param numWords the minimum number of word-containing {@link LogUnit}s to shift out
-     * @return the number of actual {@code LogUnit}s shifted out
+     * @param numWords the minimum number of words in {@link LogUnit}s to shift out
+     * @return the number of actual words LogUnit}s shifted out
      */
     protected int shiftOutWords(final int numWords) {
-        int numWordContainingLogUnitsShiftedOut = 0;
-        for (LogUnit logUnit = shiftOut(); logUnit != null
-                && numWordContainingLogUnitsShiftedOut < numWords; logUnit = shiftOut()) {
-            if (logUnit.hasOneOrMoreWords()) {
-                numWordContainingLogUnitsShiftedOut += logUnit.getNumWords();
-            }
-        }
-        return numWordContainingLogUnitsShiftedOut;
+        int numWordsShiftedOut = 0;
+        do {
+            final LogUnit logUnit = shiftOut();
+            if (logUnit == null) break;
+            numWordsShiftedOut += logUnit.getNumWords();
+        } while (numWordsShiftedOut < numWords);
+        return numWordsShiftedOut;
     }
 
     public void shiftOutAll() {
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 4d60bda..cf1388f 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -25,6 +25,7 @@
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
@@ -135,9 +136,11 @@
      * @param researchLog where to publish the contents of this {@code LogUnit}
      * @param canIncludePrivateData whether the private data in this {@code LogUnit} should be
      * included
+     *
+     * @throws IOException if publication to the log file is not possible
      */
     public synchronized void publishTo(final ResearchLog researchLog,
-            final boolean canIncludePrivateData) {
+            final boolean canIncludePrivateData) throws IOException {
         // Write out any logStatement that passes the privacy filter.
         final int size = mLogStatementList.size();
         if (size != 0) {
diff --git a/java/src/com/android/inputmethod/research/MainLogBuffer.java b/java/src/com/android/inputmethod/research/MainLogBuffer.java
index 42ef5d3..9aa3499 100644
--- a/java/src/com/android/inputmethod/research/MainLogBuffer.java
+++ b/java/src/com/android/inputmethod/research/MainLogBuffer.java
@@ -23,6 +23,7 @@
 import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.LinkedList;
 
@@ -177,7 +178,7 @@
         return numWordsInLogUnitList == minNGramSize;
     }
 
-    public void shiftAndPublishAll() {
+    public void shiftAndPublishAll() throws IOException {
         final LinkedList<LogUnit> logUnits = getLogUnits();
         while (!logUnits.isEmpty()) {
             publishLogUnitsAtFrontOfBuffer();
@@ -186,26 +187,40 @@
 
     @Override
     protected final void onBufferFull() {
-        publishLogUnitsAtFrontOfBuffer();
+        try {
+            publishLogUnitsAtFrontOfBuffer();
+        } catch (final IOException e) {
+            if (DEBUG) {
+                Log.w(TAG, "IOException when publishing front of LogBuffer", e);
+            }
+        }
     }
 
-    protected final void publishLogUnitsAtFrontOfBuffer() {
+    protected final void publishLogUnitsAtFrontOfBuffer() throws IOException {
+        // TODO: Refactor this method to require fewer passes through the LogUnits.  Should really
+        // require only one pass.
         ArrayList<LogUnit> logUnits = peekAtFirstNWords(N_GRAM_SIZE);
         if (isSafeNGram(logUnits, N_GRAM_SIZE)) {
             // Good n-gram at the front of the buffer.  Publish it, disclosing details.
             publish(logUnits, true /* canIncludePrivateData */);
             shiftOutWords(N_GRAM_SIZE);
             mNumWordsUntilSafeToSample = mNumWordsBetweenNGrams;
-        } else {
-            // No good n-gram at front, and buffer is full.  Shift out up through the first logUnit
-            // with associated words (or if there is none, all the existing logUnits).
-            logUnits.clear();
-            for (LogUnit logUnit = shiftOut(); logUnit != null && !logUnit.hasOneOrMoreWords();
-                    logUnit = shiftOut()) {
-                logUnits.add(logUnit);
-            }
-            publish(logUnits, false /* canIncludePrivateData */);
+            return;
         }
+        // No good n-gram at front, and buffer is full.  Shift out up through the first logUnit
+        // with associated words (or if there is none, all the existing logUnits).
+        logUnits.clear();
+        LogUnit logUnit = shiftOut();
+        while (logUnit != null) {
+            logUnits.add(logUnit);
+            final int numWords = logUnit.getNumWords();
+            if (numWords > 0) {
+                mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWords);
+                break;
+            }
+            logUnit = shiftOut();
+        }
+        publish(logUnits, false /* canIncludePrivateData */);
     }
 
     /**
@@ -216,18 +231,19 @@
      * @param logUnits The list of logUnits to be published.
      * @param canIncludePrivateData Whether the private data in the logUnits can be included in
      * publication.
+     *
+     * @throws IOException if publication to the log file is not possible
      */
     protected abstract void publish(final ArrayList<LogUnit> logUnits,
-            final boolean canIncludePrivateData);
+            final boolean canIncludePrivateData) throws IOException;
 
     @Override
     protected int shiftOutWords(final int numWords) {
-        final int numWordContainingLogUnitsShiftedOut = super.shiftOutWords(numWords);
-        mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample
-                - numWordContainingLogUnitsShiftedOut);
+        final int numWordsShiftedOut = super.shiftOutWords(numWords);
+        mNumWordsUntilSafeToSample = Math.max(0, mNumWordsUntilSafeToSample - numWordsShiftedOut);
         if (DEBUG) {
             Log.d(TAG, "wordsUntilSafeToSample now at " + mNumWordsUntilSafeToSample);
         }
-        return numWordContainingLogUnitsShiftedOut;
+        return numWordsShiftedOut;
     }
 }
diff --git a/java/src/com/android/inputmethod/research/ResearchLog.java b/java/src/com/android/inputmethod/research/ResearchLog.java
index 18bf7ba..3e82139 100644
--- a/java/src/com/android/inputmethod/research/ResearchLog.java
+++ b/java/src/com/android/inputmethod/research/ResearchLog.java
@@ -25,6 +25,7 @@
 
 import java.io.BufferedWriter;
 import java.io.File;
+import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
@@ -61,7 +62,11 @@
     /* package */ final File mFile;
     private final Context mContext;
 
-    private JsonWriter mJsonWriter = NULL_JSON_WRITER;
+    // Earlier implementations used a dummy JsonWriter that just swallowed what it was given, but
+    // this was tricky to do well, because JsonWriter throws an exception if it is passed more than
+    // one top-level object.
+    private JsonWriter mJsonWriter = null;
+
     // true if at least one byte of data has been written out to the log file.  This must be
     // remembered because JsonWriter requires that calls matching calls to beginObject and
     // endObject, as well as beginArray and endArray, and the file is opened lazily, only when
@@ -69,26 +74,6 @@
     // could be caught, but this might suppress other errors.
     private boolean mHasWrittenData = false;
 
-    private static final JsonWriter NULL_JSON_WRITER = new JsonWriter(
-            new OutputStreamWriter(new NullOutputStream()));
-    private static class NullOutputStream extends OutputStream {
-        /** {@inheritDoc} */
-        @Override
-        public void write(byte[] buffer, int offset, int count) {
-            // nop
-        }
-
-        /** {@inheritDoc} */
-        @Override
-        public void write(byte[] buffer) {
-            // nop
-        }
-
-        @Override
-        public void write(int oneByte) {
-        }
-    }
-
     public ResearchLog(final File outputFile, final Context context) {
         mExecutor = Executors.newSingleThreadScheduledExecutor();
         mFile = outputFile;
@@ -108,6 +93,7 @@
             @Override
             public Object call() throws Exception {
                 try {
+                    if (mJsonWriter == null) return null;
                     // TODO: This is necessary to avoid an exception.  Better would be to not even
                     // open the JsonWriter if the file is not even opened unless there is valid data
                     // to write.
@@ -119,9 +105,9 @@
                     mJsonWriter.flush();
                     mJsonWriter.close();
                     if (DEBUG) {
-                        Log.d(TAG, "wrote log to " + mFile);
+                        Log.d(TAG, "closed " + mFile);
                     }
-                } catch (Exception e) {
+                } catch (final Exception e) {
                     Log.d(TAG, "error when closing ResearchLog:", e);
                 } finally {
                     // Marking the file as read-only signals that this log file is ready to be
@@ -162,6 +148,7 @@
             @Override
             public Object call() throws Exception {
                 try {
+                    if (mJsonWriter == null) return null;
                     if (mHasWrittenData) {
                         // TODO: This is necessary to avoid an exception.  Better would be to not
                         // even open the JsonWriter if the file is not even opened unless there is
@@ -217,7 +204,7 @@
     private final Callable<Object> mFlushCallable = new Callable<Object>() {
         @Override
         public Object call() throws Exception {
-            mJsonWriter.flush();
+            if (mJsonWriter != null) mJsonWriter.flush();
             return null;
         }
     };
@@ -263,30 +250,29 @@
     /**
      * Return a JsonWriter for this ResearchLog.  It is initialized the first time this method is
      * called.  The cached value is returned in future calls.
+     *
+     * @throws IOException if opening the JsonWriter is not possible
      */
-    public JsonWriter getInitializedJsonWriterLocked() {
-        if (mJsonWriter != NULL_JSON_WRITER || mFile == null) return mJsonWriter;
+    public JsonWriter getInitializedJsonWriterLocked() throws IOException {
+        if (mJsonWriter != null) return mJsonWriter;
+        if (mFile == null) throw new FileNotFoundException();
         try {
             final JsonWriter jsonWriter = createJsonWriter(mContext, mFile);
-            if (jsonWriter != null) {
-                jsonWriter.beginArray();
-                mJsonWriter = jsonWriter;
-                mHasWrittenData = true;
-            }
+            if (jsonWriter == null) throw new IOException("Could not create JsonWriter");
+
+            jsonWriter.beginArray();
+            mJsonWriter = jsonWriter;
+            mHasWrittenData = true;
+            return mJsonWriter;
         } catch (final IOException e) {
-            Log.w(TAG, "Error in JsonWriter; disabling logging", e);
-            try {
-                mJsonWriter.close();
-            } catch (final IllegalStateException e1) {
-                // Assume that this is just the json not being terminated properly.
-                // Ignore
-            } catch (final IOException e1) {
-                Log.w(TAG, "Error in closing JsonWriter; disabling logging", e1);
-            } finally {
-                mJsonWriter = NULL_JSON_WRITER;
+            if (DEBUG) {
+                Log.w(TAG, "Exception when creating JsonWriter", e);
+                Log.w(TAG, "Closing JsonWriter");
             }
+            if (mJsonWriter != null) mJsonWriter.close();
+            mJsonWriter = null;
+            throw e;
         }
-        return mJsonWriter;
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index 1f6845c..8b8ea21 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -118,7 +118,6 @@
     private static final boolean FEEDBACK_DIALOG_SHOULD_PRESERVE_TEXT_FIELD = false;
     /* package */ static boolean sIsLogging = false;
     private static final int OUTPUT_FORMAT_VERSION = 5;
-    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
     // Whether all words should be recorded, leaving unsampled word between bigrams.  Useful for
     // testing.
     /* package for test */ static final boolean IS_LOGGING_EVERYTHING = false
@@ -150,24 +149,18 @@
     private static final ResearchLogger sInstance = new ResearchLogger();
     private static String sAccountType = null;
     private static String sAllowedAccountDomain = null;
-    /* package */ ResearchLog mMainResearchLog;
+    private ResearchLog mMainResearchLog; // always non-null after init() is called
     // mFeedbackLog records all events for the session, private or not (excepting
     // passwords).  It is written to permanent storage only if the user explicitly commands
     // the system to do so.
     // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
     // complete.
-    /* package */ MainLogBuffer mMainLogBuffer;
-    // TODO: Remove the feedback log.  The feedback log continuously captured user data in case the
-    // user wanted to submit it.  We now use the mUserRecordingLogBuffer to allow the user to
-    // explicitly reproduce a problem.
-    /* package */ ResearchLog mFeedbackLog;
-    /* package */ LogBuffer mFeedbackLogBuffer;
+    /* package for test */ MainLogBuffer mMainLogBuffer; // always non-null after init() is called
     /* package */ ResearchLog mUserRecordingLog;
     /* package */ LogBuffer mUserRecordingLogBuffer;
     private File mUserRecordingFile = null;
 
     private boolean mIsPasswordView = false;
-    private boolean mIsLoggingSuspended = false;
     private SharedPreferences mPrefs;
 
     // digits entered by the user are replaced with this codepoint.
@@ -202,15 +195,6 @@
     private long mSavedDownEventTime;
     private Bundle mFeedbackDialogBundle = null;
     private boolean mInFeedbackDialog = false;
-    // The feedback dialog causes stop() to be called for the keyboard connected to the original
-    // window.  This is because the feedback dialog must present its own EditText box that displays
-    // a keyboard.  stop() normally causes mFeedbackLogBuffer, which contains the user's data, to be
-    // cleared, and causes mFeedbackLog, which is ready to collect information in case the user
-    // wants to upload, to be closed.  This is good because we don't need to log information about
-    // what the user is typing in the feedback dialog, but bad because this data must be uploaded.
-    // Here we save the LogBuffer and Log so the feedback dialog can later access their data.
-    private LogBuffer mSavedFeedbackLogBuffer;
-    private ResearchLog mSavedFeedbackLog;
     private Handler mUserRecordingTimeoutHandler;
     private static final long USER_RECORDING_TIMEOUT_MS = 30L * DateUtils.SECOND_IN_MILLIS;
 
@@ -241,6 +225,9 @@
         mResearchLogDirectory = new ResearchLogDirectory(mLatinIME);
         cleanLogDirectoryIfNeeded(mResearchLogDirectory, System.currentTimeMillis());
 
+        // Initialize log buffers
+        resetLogBuffers();
+
         // Initialize external services
         mUploadIntent = new Intent(mLatinIME, UploaderService.class);
         mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
@@ -252,6 +239,35 @@
         mReplayer.setKeyboardSwitcher(keyboardSwitcher);
     }
 
+    private void resetLogBuffers() {
+        mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+                System.currentTimeMillis(), System.nanoTime()), mLatinIME);
+        final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
+        mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
+                mSuggest) {
+            @Override
+            protected void publish(final ArrayList<LogUnit> logUnits,
+                    boolean canIncludePrivateData) {
+                canIncludePrivateData |= IS_LOGGING_EVERYTHING;
+                for (final LogUnit logUnit : logUnits) {
+                    if (DEBUG) {
+                        final String wordsString = logUnit.getWordsAsString();
+                        Log.d(TAG, "onPublish: '" + wordsString
+                                + "', hc: " + logUnit.containsCorrection()
+                                + ", cipd: " + canIncludePrivateData);
+                    }
+                    for (final String word : logUnit.getWordsAsStringArray()) {
+                        final Dictionary dictionary = getDictionary();
+                        mStatistics.recordWordEntered(
+                                dictionary != null && dictionary.isValidWord(word),
+                                logUnit.containsCorrection());
+                    }
+                }
+                publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
+            }
+        };
+    }
+
     private void cleanLogDirectoryIfNeeded(final ResearchLogDirectory researchLogDirectory,
             final long now) {
         final long lastCleanupTime = ResearchSettings.readResearchLastDirCleanupTime(mPrefs);
@@ -376,53 +392,9 @@
             Log.d(TAG, "start called");
         }
         maybeShowSplashScreen();
-        updateSuspendedState();
         requestIndicatorRedraw();
         mStatistics.reset();
         checkForEmptyEditor();
-        if (mFeedbackLogBuffer == null) {
-            resetFeedbackLogging();
-        }
-        if (!isAllowedToLog()) {
-            // Log.w(TAG, "not in usability mode; not logging");
-            return;
-        }
-        if (mMainLogBuffer == null) {
-            mMainResearchLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
-                    System.currentTimeMillis(), System.nanoTime()), mLatinIME);
-            final int numWordsToIgnore = new Random().nextInt(NUMBER_OF_WORDS_BETWEEN_SAMPLES + 1);
-            mMainLogBuffer = new MainLogBuffer(NUMBER_OF_WORDS_BETWEEN_SAMPLES, numWordsToIgnore,
-                    mSuggest) {
-                @Override
-                protected void publish(final ArrayList<LogUnit> logUnits,
-                        boolean canIncludePrivateData) {
-                    canIncludePrivateData |= IS_LOGGING_EVERYTHING;
-                    for (final LogUnit logUnit : logUnits) {
-                        if (DEBUG) {
-                            final String wordsString = logUnit.getWordsAsString();
-                            Log.d(TAG, "onPublish: '" + wordsString
-                                    + "', hc: " + logUnit.containsCorrection()
-                                    + ", cipd: " + canIncludePrivateData);
-                        }
-                        for (final String word : logUnit.getWordsAsStringArray()) {
-                            final Dictionary dictionary = getDictionary();
-                            mStatistics.recordWordEntered(
-                                    dictionary != null && dictionary.isValidWord(word),
-                                    logUnit.containsCorrection());
-                        }
-                    }
-                    if (mMainResearchLog != null) {
-                        publishLogUnits(logUnits, mMainResearchLog, canIncludePrivateData);
-                    }
-                }
-            };
-        }
-    }
-
-    private void resetFeedbackLogging() {
-        mFeedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
-                System.currentTimeMillis(), System.nanoTime()), mLatinIME);
-        mFeedbackLogBuffer = new FixedLogBuffer(FEEDBACK_WORD_BUFFER_SIZE);
     }
 
     /* package */ void stop() {
@@ -432,35 +404,32 @@
         // Commit mCurrentLogUnit before closing.
         commitCurrentLogUnit();
 
-        if (mMainLogBuffer != null) {
+        try {
             mMainLogBuffer.shiftAndPublishAll();
-            logStatistics();
-            commitCurrentLogUnit();
-            mMainLogBuffer.setIsStopping();
+        } catch (final IOException e) {
+            Log.w(TAG, "IOException when publishing LogBuffer", e);
+        }
+        logStatistics();
+        commitCurrentLogUnit();
+        mMainLogBuffer.setIsStopping();
+        try {
             mMainLogBuffer.shiftAndPublishAll();
-            mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
-            mMainLogBuffer = null;
+        } catch (final IOException e) {
+            Log.w(TAG, "IOException when publishing LogBuffer", e);
         }
-        if (mFeedbackLogBuffer != null) {
-            mFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
-            mFeedbackLogBuffer = null;
-        }
+        mMainResearchLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
+
+        resetLogBuffers();
     }
 
     public void abort() {
         if (DEBUG) {
             Log.d(TAG, "abort called");
         }
-        if (mMainLogBuffer != null) {
-            mMainLogBuffer.clear();
-            mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
-            mMainLogBuffer = null;
-        }
-        if (mFeedbackLogBuffer != null) {
-            mFeedbackLogBuffer.clear();
-            mFeedbackLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
-            mFeedbackLogBuffer = null;
-        }
+        mMainLogBuffer.clear();
+        mMainResearchLog.blockingAbort(RESEARCHLOG_ABORT_TIMEOUT_IN_MS);
+
+        resetLogBuffers();
     }
 
     private void restart() {
@@ -468,23 +437,11 @@
         start();
     }
 
-    private long mResumeTime = 0L;
-    private void updateSuspendedState() {
-        final long time = System.currentTimeMillis();
-        if (time > mResumeTime) {
-            mIsLoggingSuspended = false;
-        }
-    }
-
     @Override
     public void onSharedPreferenceChanged(final SharedPreferences prefs, final String key) {
         if (key == null || prefs == null) {
             return;
         }
-        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
-        if (sIsLogging == false) {
-            abort();
-        }
         requestIndicatorRedraw();
         mPrefs = prefs;
         prefsChanged(prefs);
@@ -504,12 +461,6 @@
             saveRecording();
         }
         mInFeedbackDialog = true;
-        mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
-        mSavedFeedbackLog = mFeedbackLog;
-        // Set the non-saved versions to null so that the stop() caused by switching to the
-        // Feedback dialog will not close them.
-        mFeedbackLogBuffer = null;
-        mFeedbackLog = null;
 
         final Intent intent = new Intent();
         intent.setClass(mLatinIME, FeedbackActivity.class);
@@ -667,12 +618,6 @@
             new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
     public void sendFeedback(final String feedbackContents, final boolean includeHistory,
             final boolean isIncludingAccountName, final boolean isIncludingRecording) {
-        if (mSavedFeedbackLogBuffer == null) {
-            return;
-        }
-        if (!includeHistory) {
-            mSavedFeedbackLogBuffer.clear();
-        }
         String recording = "";
         if (isIncludingRecording) {
             // Try to read recording from recently written json file
@@ -704,9 +649,13 @@
         final String accountName = isIncludingAccountName ? getAccountName() : "";
         feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
                 feedbackContents, accountName, recording);
-        mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
-        publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
-        mSavedFeedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
+
+        final ResearchLog feedbackLog = new ResearchLog(mResearchLogDirectory.getLogFilePath(
+                System.currentTimeMillis(), System.nanoTime()), mLatinIME);
+        final LogBuffer feedbackLogBuffer = new LogBuffer();
+        feedbackLogBuffer.shiftIn(feedbackLogUnit);
+        publishLogBuffer(feedbackLogBuffer, feedbackLog, true /* isIncludingPrivateData */);
+        feedbackLog.blockingClose(RESEARCHLOG_CLOSE_TIMEOUT_IN_MS);
         uploadNow();
 
         if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
@@ -745,8 +694,8 @@
 
     public void initSuggest(final Suggest suggest) {
         mSuggest = suggest;
-        // MainLogBuffer has out-of-date Suggest object.  Need to close it down and create a new
-        // one.
+        // MainLogBuffer now has an out-of-date Suggest object.  Close down MainLogBuffer and create
+        // a new one.
         if (mMainLogBuffer != null) {
             stop();
             start();
@@ -765,7 +714,7 @@
     }
 
     private boolean isAllowedToLog() {
-        return !mIsPasswordView && !mIsLoggingSuspended && sIsLogging && !mInFeedbackDialog;
+        return !mIsPasswordView && sIsLogging && !mInFeedbackDialog;
     }
 
     public void requestIndicatorRedraw() {
@@ -857,12 +806,7 @@
                     ": " + mCurrentLogUnit.getWordsAsString() : ""));
         }
         if (!mCurrentLogUnit.isEmpty()) {
-            if (mMainLogBuffer != null) {
-                mMainLogBuffer.shiftIn(mCurrentLogUnit);
-            }
-            if (mFeedbackLogBuffer != null) {
-                mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
-            }
+            mMainLogBuffer.shiftIn(mCurrentLogUnit);
             if (mUserRecordingLogBuffer != null) {
                 mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
             }
@@ -887,9 +831,6 @@
         //
         // Note that we don't use mLastLogUnit here, because it only goes one word back and is only
         // needed for reverts, which only happen one back.
-        if (mMainLogBuffer == null) {
-            return;
-        }
         final LogUnit oldLogUnit = mMainLogBuffer.peekLastLogUnit();
 
         // Check that expected word matches.
@@ -911,9 +852,6 @@
         } else {
             mCurrentLogUnit = oldLogUnit;
         }
-        if (mFeedbackLogBuffer != null) {
-            mFeedbackLogBuffer.unshiftIn();
-        }
         enqueueEvent(LOGSTATEMENT_UNCOMMIT_CURRENT_LOGUNIT);
         if (DEBUG) {
             Log.d(TAG, "uncommitCurrentLogUnit (dump=" + dumpCurrentLogUnit + ") back to "
@@ -943,6 +881,7 @@
             final ResearchLog researchLog, final boolean canIncludePrivateData) {
         final LogUnit openingLogUnit = new LogUnit();
         if (logUnits.isEmpty()) return;
+        if (!isAllowedToLog()) return;
         // LogUnits not containing private data, such as contextual data for the log, do not require
         // logSegment boundary statements.
         if (canIncludePrivateData) {
@@ -1376,11 +1315,7 @@
     public static void latinIME_promotePhantomSpace() {
         final ResearchLogger researchLogger = getInstance();
         final LogUnit logUnit;
-        if (researchLogger.mMainLogBuffer == null) {
-            logUnit = researchLogger.mCurrentLogUnit;
-        } else {
-            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
-        }
+        logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
         researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_PROMOTEPHANTOMSPACE);
     }
 
@@ -1397,11 +1332,7 @@
             final String charactersAfterSwap) {
         final ResearchLogger researchLogger = getInstance();
         final LogUnit logUnit;
-        if (researchLogger.mMainLogBuffer == null) {
-            logUnit = null;
-        } else {
-            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
-        }
+        logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
         if (logUnit != null) {
             researchLogger.enqueueEvent(logUnit, LOGSTATEMENT_LATINIME_SWAPSWAPPERANDSPACE,
                     originalCharacters, charactersAfterSwap);
@@ -1474,11 +1405,7 @@
         final ResearchLogger researchLogger = getInstance();
         // TODO: Verify that mCurrentLogUnit has been restored and contains the reverted word.
         final LogUnit logUnit;
-        if (researchLogger.mMainLogBuffer == null) {
-            logUnit = null;
-        } else {
-            logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
-        }
+        logUnit = researchLogger.mMainLogBuffer.peekLastLogUnit();
         if (originallyTypedWord.length() > 0 && hasLetters(originallyTypedWord)) {
             if (logUnit != null) {
                 logUnit.setWords(originallyTypedWord);
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 11fa3da..1dd68ea 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -109,7 +109,8 @@
     }
     Dictionary *dictionary = 0;
     if (BinaryFormat::UNKNOWN_FORMAT
-            == BinaryFormat::detectFormat(static_cast<uint8_t *>(dictBuf))) {
+            == BinaryFormat::detectFormat(static_cast<uint8_t *>(dictBuf),
+                    static_cast<int>(dictSize))) {
         AKLOGE("DICT: dictionary format is unknown, bad magic number");
 #ifdef USE_MMAP_FOR_DICTIONARY
         releaseDictBuf(static_cast<const char *>(dictBuf) - adjust, adjDictSize, fd);
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 06f50dc..9824153 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -64,13 +64,14 @@
     static const int UNKNOWN_FORMAT = -1;
     static const int SHORTCUT_LIST_SIZE_SIZE = 2;
 
-    static int detectFormat(const uint8_t *const dict);
-    static int getHeaderSize(const uint8_t *const dict);
-    static int getFlags(const uint8_t *const dict);
+    static int detectFormat(const uint8_t *const dict, const int dictSize);
+    static int getHeaderSize(const uint8_t *const dict, const int dictSize);
+    static int getFlags(const uint8_t *const dict, const int dictSize);
     static bool hasBlacklistedOrNotAWordFlag(const int flags);
-    static void readHeaderValue(const uint8_t *const dict, const char *const key, int *outValue,
-            const int outValueSize);
-    static int readHeaderValueInt(const uint8_t *const dict, const char *const key);
+    static void readHeaderValue(const uint8_t *const dict, const int dictSize,
+            const char *const key, int *outValue, const int outValueSize);
+    static int readHeaderValueInt(const uint8_t *const dict, const int dictSize,
+            const char *const key);
     static int getGroupCountAndForwardPointer(const uint8_t *const dict, int *pos);
     static uint8_t getFlagsAndForwardPointer(const uint8_t *const dict, int *pos);
     static int getCodePointAndForwardPointer(const uint8_t *const dict, int *pos);
@@ -96,7 +97,7 @@
             const uint8_t *bigramFilter, const int unigramProbability);
     static int getBigramProbabilityFromHashMap(const int position,
             const hash_map_compat<int, int> *bigramMap, const int unigramProbability);
-    static float getMultiWordCostMultiplier(const uint8_t *const dict);
+    static float getMultiWordCostMultiplier(const uint8_t *const dict, const int dictSize);
     static void fillBigramProbabilityToHashMap(const uint8_t *const root, int position,
             hash_map_compat<int, int> *bigramMap);
     static int getBigramProbability(const uint8_t *const root, int position,
@@ -122,6 +123,8 @@
     static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_TWOBYTES = 0x20;
     static const int FLAG_ATTRIBUTE_ADDRESS_TYPE_THREEBYTES = 0x30;
 
+    // Any file smaller than this is not a dictionary.
+    static const int DICTIONARY_MINIMUM_SIZE = 4;
     // Originally, format version 1 had a 16-bit magic number, then the version number `01'
     // then options that must be 0. Hence the first 32-bits of the format are always as follow
     // and it's okay to consider them a magic number as a whole.
@@ -131,6 +134,8 @@
     // number, so we had to change it so that version 2 files would be rejected by older
     // implementations. On this occasion, we made the magic number 32 bits long.
     static const int FORMAT_VERSION_2_MAGIC_NUMBER = -1681835266; // 0x9BC13AFE
+    // Magic number (4 bytes), version (2 bytes), options (2 bytes), header size (4 bytes) = 12
+    static const int FORMAT_VERSION_2_MINIMUM_SIZE = 12;
 
     static const int CHARACTER_ARRAY_TERMINATOR_SIZE = 1;
     static const int MINIMAL_ONE_BYTE_CHARACTER_VALUE = 0x20;
@@ -141,8 +146,11 @@
     static int skipBigrams(const uint8_t *const dict, const uint8_t flags, const int pos);
 };
 
-AK_FORCE_INLINE int BinaryFormat::detectFormat(const uint8_t *const dict) {
+AK_FORCE_INLINE int BinaryFormat::detectFormat(const uint8_t *const dict, const int dictSize) {
     // The magic number is stored big-endian.
+    // If the dictionary is less than 4 bytes, we can't even read the magic number, so we don't
+    // understand this format.
+    if (dictSize < DICTIONARY_MINIMUM_SIZE) return UNKNOWN_FORMAT;
     const int magicNumber = (dict[0] << 24) + (dict[1] << 16) + (dict[2] << 8) + dict[3];
     switch (magicNumber) {
     case FORMAT_VERSION_1_MAGIC_NUMBER:
@@ -152,6 +160,10 @@
         // Options (2 bytes) must be 0x00 0x00
         return 1;
     case FORMAT_VERSION_2_MAGIC_NUMBER:
+        // Version 2 dictionaries are at least 12 bytes long (see below details for the header).
+        // If this dictionary has the version 2 magic number but is less than 12 bytes long, then
+        // it's an unknown format and we need to avoid confidently reading the next bytes.
+        if (dictSize < FORMAT_VERSION_2_MINIMUM_SIZE) return UNKNOWN_FORMAT;
         // Format 2 header is as follows:
         // Magic number (4 bytes) 0x9B 0xC1 0x3A 0xFE
         // Version number (2 bytes) 0x00 0x02
@@ -163,8 +175,8 @@
     }
 }
 
-inline int BinaryFormat::getFlags(const uint8_t *const dict) {
-    switch (detectFormat(dict)) {
+inline int BinaryFormat::getFlags(const uint8_t *const dict, const int dictSize) {
+    switch (detectFormat(dict, dictSize)) {
     case 1:
         return NO_FLAGS; // TODO: NO_FLAGS is unused anywhere else?
     default:
@@ -176,8 +188,8 @@
     return (flags & (FLAG_IS_BLACKLISTED | FLAG_IS_NOT_A_WORD)) != 0;
 }
 
-inline int BinaryFormat::getHeaderSize(const uint8_t *const dict) {
-    switch (detectFormat(dict)) {
+inline int BinaryFormat::getHeaderSize(const uint8_t *const dict, const int dictSize) {
+    switch (detectFormat(dict, dictSize)) {
     case 1:
         return FORMAT_VERSION_1_HEADER_SIZE;
     case 2:
@@ -188,12 +200,12 @@
     }
 }
 
-inline void BinaryFormat::readHeaderValue(const uint8_t *const dict, const char *const key,
-        int *outValue, const int outValueSize) {
+inline void BinaryFormat::readHeaderValue(const uint8_t *const dict, const int dictSize,
+        const char *const key, int *outValue, const int outValueSize) {
     int outValueIndex = 0;
     // Only format 2 and above have header attributes as {key,value} string pairs. For prior
     // formats, we just return an empty string, as if the key wasn't found.
-    if (2 <= detectFormat(dict)) {
+    if (2 <= detectFormat(dict, dictSize)) {
         const int headerOptionsOffset = 4 /* magic number */
                 + 2 /* dictionary version */ + 2 /* flags */;
         const int headerSize =
@@ -236,11 +248,12 @@
     if (outValueIndex >= 0) outValue[outValueIndex] = 0;
 }
 
-inline int BinaryFormat::readHeaderValueInt(const uint8_t *const dict, const char *const key) {
+inline int BinaryFormat::readHeaderValueInt(const uint8_t *const dict, const int dictSize,
+        const char *const key) {
     const int bufferSize = LARGEST_INT_DIGIT_COUNT;
     int intBuffer[bufferSize];
     char charBuffer[bufferSize];
-    BinaryFormat::readHeaderValue(dict, key, intBuffer, bufferSize);
+    BinaryFormat::readHeaderValue(dict, dictSize, key, intBuffer, bufferSize);
     for (int i = 0; i < bufferSize; ++i) {
         charBuffer[i] = intBuffer[i];
     }
@@ -256,8 +269,10 @@
     return ((msb & 0x7F) << 8) | dict[(*pos)++];
 }
 
-inline float BinaryFormat::getMultiWordCostMultiplier(const uint8_t *const dict) {
-    const int headerValue = readHeaderValueInt(dict, "MULTIPLE_WORDS_DEMOTION_RATE");
+inline float BinaryFormat::getMultiWordCostMultiplier(const uint8_t *const dict,
+        const int dictSize) {
+    const int headerValue = readHeaderValueInt(dict, dictSize,
+            "MULTIPLE_WORDS_DEMOTION_RATE");
     if (headerValue == S_INT_MIN) {
         return 1.0f;
     }
diff --git a/native/jni/src/dictionary.cpp b/native/jni/src/dictionary.cpp
index c998c06..dadb2ba 100644
--- a/native/jni/src/dictionary.cpp
+++ b/native/jni/src/dictionary.cpp
@@ -34,9 +34,11 @@
 
 Dictionary::Dictionary(void *dict, int dictSize, int mmapFd, int dictBufAdjust)
         : mDict(static_cast<unsigned char *>(dict)),
-          mOffsetDict((static_cast<unsigned char *>(dict)) + BinaryFormat::getHeaderSize(mDict)),
+          mOffsetDict((static_cast<unsigned char *>(dict))
+                  + BinaryFormat::getHeaderSize(mDict, dictSize)),
           mDictSize(dictSize), mMmapFd(mmapFd), mDictBufAdjust(dictBufAdjust),
-          mUnigramDictionary(new UnigramDictionary(mOffsetDict, BinaryFormat::getFlags(mDict))),
+          mUnigramDictionary(new UnigramDictionary(mOffsetDict,
+                  BinaryFormat::getFlags(mDict, dictSize))),
           mBigramDictionary(new BigramDictionary(mOffsetDict)),
           mGestureSuggest(new Suggest(GestureSuggestPolicyFactory::getGestureSuggestPolicy())),
           mTypingSuggest(new Suggest(TypingSuggestPolicyFactory::getTypingSuggestPolicy())) {
diff --git a/native/jni/src/suggest/core/session/dic_traverse_session.cpp b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
index 5116585..6408f01 100644
--- a/native/jni/src/suggest/core/session/dic_traverse_session.cpp
+++ b/native/jni/src/suggest/core/session/dic_traverse_session.cpp
@@ -64,7 +64,8 @@
 void DicTraverseSession::init(const Dictionary *const dictionary, const int *prevWord,
         int prevWordLength) {
     mDictionary = dictionary;
-    mMultiWordCostMultiplier = BinaryFormat::getMultiWordCostMultiplier(mDictionary->getDict());
+    mMultiWordCostMultiplier = BinaryFormat::getMultiWordCostMultiplier(mDictionary->getDict(),
+            mDictionary->getDictSize());
     if (!prevWord) {
         mPrevWordPos = NOT_VALID_WORD;
         return;
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
index a3f9dbd..d5b9d1d 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateSingleTouchTests.java
@@ -352,30 +352,34 @@
         // Alphabet -> shift key + letter -> alphabet.
         // Press and slide from shift key, enter alphabet shifted.
         pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Enter/release letter key, switch back to alphabet.
-        pressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter keys, switch back to alphabet.
+        pressAndSlideFromKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        stopSlidingOnKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
 
         // Alphabet -> "?123" key + letter -> alphabet.
         // Press and slide from "123?" key, enter symbols.
         pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
-        // Enter/release into symbol letter key, switch back to alphabet.
-        pressAndReleaseKey('!', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release into symbol letter keys, switch back to alphabet.
+        pressAndSlideFromKey('@', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        stopSlidingOnKey('!', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
 
         // Alphabet shifted -> shift key + letter -> alphabet.
         // Press/release shift key, enter alphabet shifted.
         pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
         // Press and slide from shift key, remain alphabet shifted.
         pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
-        // Enter/release letter key, switch back to alphabet (not alphabet shifted).
-        pressAndReleaseKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter keys, switch back to alphabet (not alphabet shifted).
+        pressAndSlideFromKey('A', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        stopSlidingOnKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_UNSHIFTED);
 
         // Alphabet shifted -> "?123" key + letter -> alphabet.
         // Press/release shift key, enter alphabet shifted.
         pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
         // Press and slide from "123?" key, enter symbols.
         pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
-        // Enter/release into symbol letter key, switch back to alphabet (not alphabet shifted).
-        pressAndReleaseKey('!', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release into symbol letter keys, switch back to alphabet (not alphabet shifted).
+        pressAndSlideFromKey('@', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        stopSlidingOnKey('!', SYMBOLS_UNSHIFTED, ALPHABET_UNSHIFTED);
 
         // Alphabet shift locked -> shift key + letter -> alphabet shift locked.
         // Long press shift key, enter alphabet shift locked.
@@ -383,14 +387,76 @@
                 ALPHABET_SHIFT_LOCKED);
         // Press and slide from "123?" key, enter symbols.
         pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
-        // Enter/release into symbol letter key, switch back to alphabet shift locked.
-        pressAndReleaseKey('!', SYMBOLS_UNSHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Enter/release into symbol letter keys, switch back to alphabet shift locked.
+        pressAndSlideFromKey('!', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        stopSlidingOnKey('!', SYMBOLS_UNSHIFTED, ALPHABET_SHIFT_LOCKED);
 
         // Alphabet shift locked -> "?123" key + letter -> alphabet shift locked.
         // Press and slide from shift key, enter alphabet shifted.
         pressAndSlideFromKey(CODE_SHIFT, ALPHABET_SHIFT_LOCK_SHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Enter/release letter keys, switch back to shift locked.
+        pressAndSlideFromKey('A', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        stopSlidingOnKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Cancel sliding input in alphabet.
+    public void testSlidingAlphabetCancel() {
+        // Alphabet -> shift key + letter -> cancel -> alphabet.
+        // Press and slide from shift key, enter alphabet shifted.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press and slide from shift key, enter alphabet shifted.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Enter/release letter key, remains in alphabet shifted.
+        pressAndSlideFromKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Cancel sliding, switch back to alphabet.
+        stopSlidingAndCancel(ALPHABET_UNSHIFTED);
+
+        // Alphabet -> "?123" key + letter -> cancel -> alphabet.
+        // Press and slide from "123?" key, enter symbols.
+        pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release into symbol letter key, remains in symbols.
+        pressAndSlideFromKey('!', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Cancel sliding, switch back to alphabet.
+        stopSlidingAndCancel(ALPHABET_UNSHIFTED);
+
+        // Alphabet shifted -> shift key + letter -> cancel -> alphabet.
+        // Press/release shift key, enter alphabet shifted.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press and slide from shift key, remain alphabet shifted.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Enter/release letter key, remains in alphabet shifted.
+        pressAndSlideFromKey('Z', ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Cancel sliding, switch back to alphabet (not alphabet shifted).
+        stopSlidingAndCancel(ALPHABET_UNSHIFTED);
+
+        // Alphabet shifted -> "?123" key + letter -> cancel -> alphabet.
+        // Press/release shift key, enter alphabet shifted.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press and slide from "123?" key, enter symbols.
+        pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release into symbol letter key, remains in symbols.
+        pressAndSlideFromKey('!', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Cancel sliding, switch back to alphabet (not alphabet shifted).
+        stopSlidingAndCancel(ALPHABET_UNSHIFTED);
+
+        // Alphabet shift locked -> shift key + letter -> cancel -> alphabet shift locked.
+        // Long press shift key, enter alphabet shift locked.
+        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+                ALPHABET_SHIFT_LOCKED);
+        // Press and slide from "123?" key, enter symbols.
+        pressAndSlideFromKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release into symbol letter key, remains in symbols.
+        pressAndSlideFromKey('!', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Cancel sliding, switch back to alphabet shift locked.
+        stopSlidingAndCancel( ALPHABET_SHIFT_LOCKED);
+
+        // Alphabet shift locked -> "?123" key + letter -> cancel -> alphabet shift locked.
+        // Press and slide from shift key, enter alphabet shifted.
+        pressAndSlideFromKey(CODE_SHIFT, ALPHABET_SHIFT_LOCK_SHIFTED, ALPHABET_SHIFT_LOCKED);
+        // Enter/release letter key, remains in alphabet shift locked.
+        pressAndSlideFromKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
         // Enter/release letter key, switch back to shift locked.
-        pressAndReleaseKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        stopSlidingAndCancel(ALPHABET_SHIFT_LOCKED);
     }
 
     // Sliding input in symbols.
@@ -398,16 +464,18 @@
         // Symbols -> "=\<" key + letter -> symbols.
         // Press/release "?123" key, enter into symbols.
         pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
-        // Press and slide from shift key, enter symols shifted.
+        // Press and slide from shift key, enter symbols shifted.
         pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
-        // Enter/release symbol shifted letter key, switch back to symbols.
-        pressAndReleaseKey('~', SYMBOLS_SHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release symbol shifted letter keys, switch back to symbols.
+        pressAndSlideFromKey('|', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        stopSlidingOnKey('~', SYMBOLS_SHIFTED, SYMBOLS_UNSHIFTED);
 
         // Symbols -> "ABC" key + letter -> Symbols.
         // Press and slide from "ABC" key, enter alphabet.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
-        // Enter/release letter key, switch back to symbols.
-        pressAndReleaseKey('a', ALPHABET_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release letter keys, switch back to symbols.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        stopSlidingOnKey('a', ALPHABET_UNSHIFTED, SYMBOLS_UNSHIFTED);
         // Press/release "ABC" key, switch to alphabet.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
 
@@ -421,8 +489,9 @@
         pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
         // Press and slide from "ABC" key.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
-        // Enter/release letter key, switch back to symbols.
-        pressAndReleaseKey('a', ALPHABET_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release letter keys, switch back to symbols.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        stopSlidingOnKey('a', ALPHABET_UNSHIFTED, SYMBOLS_UNSHIFTED);
         // Press/release "ABC" key, switch to alphabet (not alphabet shifted).
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
 
@@ -437,8 +506,9 @@
         pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
         // Press and slide from "ABC" key, enter alphabet shift locked.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
-        // Enter/release letter key, switch back to symbols.
-        pressAndReleaseKey('A', ALPHABET_SHIFT_LOCKED, SYMBOLS_UNSHIFTED);
+        // Enter/release letter keys, switch back to symbols.
+        pressAndSlideFromKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        stopSlidingOnKey('A', ALPHABET_SHIFT_LOCKED, SYMBOLS_UNSHIFTED);
         // Press/release "ABC" key, switch to alphabet shift locked.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
 
@@ -453,8 +523,85 @@
         pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
         // Press and slide from "=\<" key, enter symbols shifted.
         pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
-        // Enter/release symbols shift letter key, switch back to symbols.
-        pressAndReleaseKey('~', SYMBOLS_SHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release symbols shift letter keys, switch back to symbols.
+        pressAndSlideFromKey('|', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        stopSlidingOnKey('~', SYMBOLS_SHIFTED, SYMBOLS_UNSHIFTED);
+        // Press/release "ABC" key, switch to alphabet shift locked.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Cancel sliding input in symbols.
+    public void testSlidingSymbolsCancel() {
+        // Symbols -> "=\<" key + letter -> cancel -> symbols.
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press and slide from shift key, enter symbols shifted.
+        pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Enter/release symbol shifted letter key, remains in symbols shifted.
+        pressAndSlideFromKey('|', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Cancel sliding, switch back to symbols.
+        stopSlidingAndCancel(SYMBOLS_UNSHIFTED);
+
+        // Symbols -> "ABC" key + letter -> Symbols.
+        // Press and slide from "ABC" key, enter alphabet.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter keys, remains in alphabet.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Cancel sliding, switch back to symbols.
+        stopSlidingAndCancel(SYMBOLS_UNSHIFTED);
+        // Press/release "ABC" key, switch to alphabet.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Alphabet shifted -> symbols -> "ABC" key + letter -> symbols ->
+        // alphabet.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Press/release shift key, enter alphabet shifted.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press and slide from "ABC" key.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter key, remains in alphabet.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Cancel sliding, switch back to symbols.
+        stopSlidingAndCancel(SYMBOLS_UNSHIFTED);
+        // Press/release "ABC" key, switch to alphabet (not alphabet shifted).
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Alphabet shift locked -> symbols -> "ABC" key + letter -> symbols ->
+        // alphabet shift locked.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Long press shift key, enter alphabet shift locked.
+        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+                ALPHABET_SHIFT_LOCKED);
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press and slide from "ABC" key, enter alphabet shift locked.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        // Enter/release letter key, remains in alphabet shifted.
+        pressAndSlideFromKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        // Cancel sliding, switch back to symbols.
+        stopSlidingAndCancel(SYMBOLS_UNSHIFTED);
+        // Press/release "ABC" key, switch to alphabet shift locked.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+
+        // Alphabet shift locked -> symbols -> "=\<" key + letter -> symbols ->
+        // alphabet shift locked.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Long press shift key, enter alphabet shift locked.
+        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+                ALPHABET_SHIFT_LOCKED);
+        // Press/release "?123" key, enter into symbols.
+        pressAndReleaseKey(CODE_SYMBOL, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Press and slide from "=\<" key, enter symbols shifted.
+        pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Enter/release symbols shift letter key, remains in symbols shifted.
+        pressAndSlideFromKey('|', SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
+        // Cancel sliding, switch back to symbols.
+        stopSlidingAndCancel(SYMBOLS_UNSHIFTED);
         // Press/release "ABC" key, switch to alphabet shift locked.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
     }
@@ -468,14 +615,16 @@
         pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
         // Press and slide from shift key, enter symbols.
         pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
-        // Enter/release symbol letter key, switch back to symbols shifted.
-        pressAndReleaseKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_SHIFTED);
+        // Enter/release symbol letter keys, switch back to symbols shifted.
+        pressAndSlideFromKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        stopSlidingOnKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_SHIFTED);
 
         // Symbols shifted -> "ABC" key + letter -> symbols shifted.
         // Press and slide from "ABC" key, enter alphabet.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
-        // Enter/release letter key, switch back to symbols shifted.
-        pressAndReleaseKey('a', ALPHABET_UNSHIFTED, SYMBOLS_SHIFTED);
+        // Enter/release letter keys, switch back to symbols shifted.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        stopSlidingOnKey('a', ALPHABET_UNSHIFTED, SYMBOLS_SHIFTED);
         // Press/release "ABC" key, switch to alphabet.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
 
@@ -491,8 +640,9 @@
         pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
         // Press and slide from "ABC" key.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
-        // Enter/release letter key, switch back to symbols shifted.
-        pressAndReleaseKey('a', ALPHABET_UNSHIFTED, SYMBOLS_SHIFTED);
+        // Enter/release letter keys, switch back to symbols shifted.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        stopSlidingOnKey('a', ALPHABET_UNSHIFTED, SYMBOLS_SHIFTED);
         // Press/release "ABC" key, switch to alphabet (not alphabet shifted).
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
 
@@ -509,8 +659,9 @@
         pressAndReleaseKey(CODE_SHIFT, SYMBOLS_SHIFTED, SYMBOLS_SHIFTED);
         // Press and slide from "ABC" key.
         pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
-        // Enter/release letter key, switch back to symbols shifted.
-        pressAndReleaseKey('A', ALPHABET_SHIFT_LOCKED, SYMBOLS_SHIFTED);
+        // Enter/release letter keys, switch back to symbols shifted.
+        pressAndSlideFromKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        stopSlidingOnKey('A', ALPHABET_SHIFT_LOCKED, SYMBOLS_SHIFTED);
         // Press/release "ABC" key, switch to alphabet shift locked.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
 
@@ -527,8 +678,93 @@
         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);
+        // Enter/release symbol letter keys, switch back to symbols shifted.
+        pressAndSlideFromKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        stopSlidingOnKey('1', SYMBOLS_UNSHIFTED, SYMBOLS_SHIFTED);
+        // Press/release "ABC" key, switch to alphabet shift locked.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+    }
+
+    // Cancel sliding input in symbols shifted.
+    public void testSlidingSymbolsShiftedCancel() {
+        // Symbols shifted -> "?123" + letter -> symbols shifted.
+        // 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 shift key, enter symbols.
+        pressAndSlideFromKey(CODE_SHIFT, SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Enter/release symbol letter key, remains in symbols.
+        pressAndSlideFromKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Cancel sliding, switch back to symbols shifted.
+        stopSlidingAndCancel(SYMBOLS_SHIFTED);
+
+        // Symbols shifted -> "ABC" key + letter -> symbols shifted.
+        // Press and slide from "ABC" key, enter alphabet.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter key, remains in alphabet.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Cancel sliding, switch back to symbols shifted.
+        stopSlidingAndCancel(SYMBOLS_SHIFTED);
+        // Press/release "ABC" key, switch to alphabet.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Alphabet shifted -> symbols shifted -> "ABC" + letter -> symbols shifted ->
+        // alphabet.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Press/release shift key, enter alphabet shifted.
+        pressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED);
+        // 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 "ABC" key.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Enter/release letter key, remains in alphabet.
+        pressAndSlideFromKey('z', ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+        // Cancel sliding, switch back to symbols shifted.
+        stopSlidingAndCancel(SYMBOLS_SHIFTED);
+        // Press/release "ABC" key, switch to alphabet (not alphabet shifted).
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_UNSHIFTED, ALPHABET_UNSHIFTED);
+
+        // Alphabet shift locked -> symbols shifted -> "ABC" + letter -> symbols shifted ->
+        // alphabet shift locked.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Long press shift key, enter alphabet shift locked.
+        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+                ALPHABET_SHIFT_LOCKED);
+        // 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 "ABC" key.
+        pressAndSlideFromKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        // Enter/release letter key, remains in alphabet shift locked.
+        pressAndSlideFromKey('Z', ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+        // Cancel sliding, switch back to symbols shifted.
+        stopSlidingAndCancel(SYMBOLS_SHIFTED);
+        // Press/release "ABC" key, switch to alphabet shift locked.
+        pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
+
+        // Alphabet shift locked -> symbols shifted -> "?123" + letter -> symbols shifted ->
+        // alphabet shift locked.
+        // Load keyboard
+        loadKeyboard(ALPHABET_UNSHIFTED);
+        // Long press shift key, enter alphabet shift locked.
+        longPressAndReleaseKey(CODE_SHIFT, ALPHABET_MANUAL_SHIFTED, ALPHABET_MANUAL_SHIFTED,
+                ALPHABET_SHIFT_LOCKED);
+        // 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, remains in symbols.
+        pressAndSlideFromKey('2', SYMBOLS_UNSHIFTED, SYMBOLS_UNSHIFTED);
+        // Cancel sliding, switch back to symbols shifted.
+        stopSlidingAndCancel(SYMBOLS_SHIFTED);
         // Press/release "ABC" key, switch to alphabet shift locked.
         pressAndReleaseKey(CODE_SYMBOL, ALPHABET_SHIFT_LOCKED, ALPHABET_SHIFT_LOCKED);
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java
index 5e94aeb..e06ca06 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeyboardStateTestsBase.java
@@ -71,7 +71,7 @@
     }
 
     public void releaseKey(final int code, final int afterRelease) {
-        mSwitcher.onCodeInput(code, SINGLE);
+        mSwitcher.onCodeInput(code);
         mSwitcher.onReleaseKey(code, NOT_SLIDING);
         assertLayout("afterRelease", afterRelease, mSwitcher.getLayoutId());
     }
@@ -87,7 +87,7 @@
     }
 
     public void chordingReleaseKey(final int code, final int afterRelease) {
-        mSwitcher.onCodeInput(code, MULTI);
+        mSwitcher.onCodeInput(code);
         mSwitcher.onReleaseKey(code, NOT_SLIDING);
         assertLayout("afterRelease", afterRelease, mSwitcher.getLayoutId());
     }
@@ -104,6 +104,19 @@
         assertLayout("afterSlide", afterSlide, mSwitcher.getLayoutId());
     }
 
+    public void stopSlidingOnKey(final int code, final int afterPress, final int afterSlide) {
+        pressKey(code, afterPress);
+        mSwitcher.onCodeInput(code);
+        mSwitcher.onReleaseKey(code, NOT_SLIDING);
+        mSwitcher.onFinishSlidingInput();
+        assertLayout("afterSlide", afterSlide, mSwitcher.getLayoutId());
+    }
+
+    public void stopSlidingAndCancel(final int afterCancelSliding) {
+        mSwitcher.onFinishSlidingInput();
+        assertLayout("afterCancelSliding", afterCancelSliding, mSwitcher.getLayoutId());
+    }
+
     public void longPressKey(final int code, final int afterPress, final int afterLongPress) {
         pressKey(code, afterPress);
         mSwitcher.onLongPressTimeout(code);
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
index 74506d2..2544b6c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
@@ -185,7 +185,7 @@
         }
     }
 
-    public void onCodeInput(final int code, final boolean isSinglePointer) {
+    public void onCodeInput(final int code) {
         if (mAutoCapsMode == MockConstants.CAP_MODE_WORDS) {
             if (Constants.isLetterCode(code)) {
                 mAutoCapsState = (code == MockConstants.CODE_AUTO_CAPS_TRIGGER)
@@ -194,10 +194,10 @@
         } else {
             mAutoCapsState = mAutoCapsMode;
         }
-        mState.onCodeInput(code, isSinglePointer, mAutoCapsState);
+        mState.onCodeInput(code, mAutoCapsState);
     }
 
-    public void onCancelInput(final boolean isSinglePointer) {
-        mState.onCancelInput(isSinglePointer);
+    public void onFinishSlidingInput() {
+        mState.onFinishSlidingInput();
     }
-}
\ No newline at end of file
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
index a572daa..279559c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueueTests.java
@@ -66,23 +66,23 @@
     private final PointerTrackerQueue mQueue = new PointerTrackerQueue();
 
     public void testEmpty() {
-        assertEquals("empty queue", 0, mQueue.size());
-        assertEquals("empty queue", "[]", mQueue.toString());
+        assertEquals(0, mQueue.size());
+        assertEquals("[]", mQueue.toString());
     }
 
     public void testAdd() {
         mQueue.add(mElement1);
-        assertEquals("add element1", 1, mQueue.size());
-        assertEquals("after adding element1", "[1]", mQueue.toString());
+        assertEquals(1, mQueue.size());
+        assertEquals("[1]", mQueue.toString());
         mQueue.add(mElement2);
-        assertEquals("add element2", 2, mQueue.size());
-        assertEquals("after adding element2", "[1 2]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[1 2]", mQueue.toString());
         mQueue.add(mElement3);
-        assertEquals("add element3", 3, mQueue.size());
-        assertEquals("after adding element3", "[1 2 3]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 2 3]", mQueue.toString());
         mQueue.add(mElement4);
-        assertEquals("add element4", 4, mQueue.size());
-        assertEquals("after adding element4", "[1 2 3 4]", mQueue.toString());
+        assertEquals(4, mQueue.size());
+        assertEquals("[1 2 3 4]", mQueue.toString());
     }
 
     public void testRemove() {
@@ -94,33 +94,29 @@
         mQueue.add(mElement4);
 
         mQueue.remove(mElement2);
-        assertEquals("remove element2", 3, mQueue.size());
-        assertEquals("after removing element2", "[1 3 4]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 3 4]", mQueue.toString());
         mQueue.remove(mElement4);
-        assertEquals("remove element4", 2, mQueue.size());
-        assertEquals("after removing element4", "[1 3]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[1 3]", mQueue.toString());
         mQueue.remove(mElement4);
-        assertEquals("remove element4 again", 2, mQueue.size());
-        assertEquals("after removing element4 again", "[1 3]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[1 3]", mQueue.toString());
         mQueue.remove(mElement1);
-        assertEquals("remove element1", 1, mQueue.size());
-        assertEquals("after removing element4", "[3]", mQueue.toString());
+        assertEquals(1, mQueue.size());
+        assertEquals("[3]", mQueue.toString());
         mQueue.remove(mElement3);
-        assertEquals("remove element3", 0, mQueue.size());
-        assertEquals("after removing element3", "[]", mQueue.toString());
+        assertEquals(0, mQueue.size());
+        assertEquals("[]", mQueue.toString());
         mQueue.remove(mElement1);
-        assertEquals("remove element1 again", 0, mQueue.size());
-        assertEquals("after removing element1 again", "[]", mQueue.toString());
+        assertEquals(0, mQueue.size());
+        assertEquals("[]", mQueue.toString());
 
-        assertEquals("after remove elements", 0, Element.sPhantomUpCount);
-        assertEquals("after remove element1",
-                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
-        assertEquals("after remove element2",
-                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
-        assertEquals("after remove element3",
-                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
-        assertEquals("after remove element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(0, Element.sPhantomUpCount);
+        assertEquals(Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 
     public void testAddAndRemove() {
@@ -132,38 +128,34 @@
         mQueue.add(mElement4);
 
         mQueue.remove(mElement2);
-        assertEquals("remove element2", 3, mQueue.size());
-        assertEquals("after removing element2", "[1 3 4]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 3 4]", mQueue.toString());
         mQueue.remove(mElement4);
-        assertEquals("remove element4", 2, mQueue.size());
-        assertEquals("after removing element4", "[1 3]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[1 3]", mQueue.toString());
         mQueue.add(mElement2);
-        assertEquals("add element2", 3, mQueue.size());
-        assertEquals("after adding element2", "[1 3 2]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 3 2]", mQueue.toString());
         mQueue.remove(mElement4);
-        assertEquals("remove element4 again", 3, mQueue.size());
-        assertEquals("after removing element4 again", "[1 3 2]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 3 2]", mQueue.toString());
         mQueue.remove(mElement1);
-        assertEquals("remove element1", 2, mQueue.size());
-        assertEquals("after removing element4", "[3 2]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[3 2]", mQueue.toString());
         mQueue.add(mElement1);
-        assertEquals("add element1", 3, mQueue.size());
-        assertEquals("after adding element1", "[3 2 1]", mQueue.toString());
+        assertEquals(3, mQueue.size());
+        assertEquals("[3 2 1]", mQueue.toString());
         mQueue.remove(mElement3);
-        assertEquals("remove element3", 2, mQueue.size());
-        assertEquals("after removing element3", "[2 1]", mQueue.toString());
+        assertEquals(2, mQueue.size());
+        assertEquals("[2 1]", mQueue.toString());
         mQueue.remove(mElement1);
-        assertEquals("remove element1 again", 1, mQueue.size());
-        assertEquals("after removing element1 again", "[2]", mQueue.toString());
+        assertEquals(1, mQueue.size());
+        assertEquals("[2]", mQueue.toString());
 
-        assertEquals("after remove element1",
-                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
-        assertEquals("after remove element2",
-                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
-        assertEquals("after remove element3",
-                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
-        assertEquals("after remove element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 
     public void testReleaseAllPointers() {
@@ -176,20 +168,33 @@
         final long eventTime = 123;
         Element.sPhantomUpCount = 0;
         mQueue.releaseAllPointers(eventTime);
-        assertEquals("after releaseAllPointers", 4, Element.sPhantomUpCount);
-        assertEquals("after releaseAllPointers", 0, mQueue.size());
-        assertEquals("after releaseAllPointers", "[]", mQueue.toString());
-        assertEquals("after releaseAllPointers element1",
-                eventTime + 1, mElement1.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointers element2",
-                eventTime + 2, mElement2.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointers element3",
-                eventTime + 3, mElement3.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointers element4",
-                eventTime + 4, mElement4.mPhantomUpEventTime);
+        assertEquals(4, Element.sPhantomUpCount);
+        assertEquals(0, mQueue.size());
+        assertEquals("[]", mQueue.toString());
+        assertEquals(eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals(eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals(eventTime + 3, mElement3.mPhantomUpEventTime);
+        assertEquals(eventTime + 4, mElement4.mPhantomUpEventTime);
     }
 
-    public void testReleaseAllPointersOlderThan() {
+    public void testReleaseAllPointersOlderThanFirst() {
+        mElement2.mIsModifier = true;
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointersOlderThan(mElement1, eventTime);
+        assertEquals(0, Element.sPhantomUpCount);
+        assertEquals(3, mQueue.size());
+        assertEquals("[1 2 3]", mQueue.toString());
+        assertEquals(Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointersOlderThanLast() {
         mElement2.mIsModifier = true;
         mQueue.add(mElement1);
         mQueue.add(mElement2);
@@ -199,20 +204,34 @@
         final long eventTime = 123;
         Element.sPhantomUpCount = 0;
         mQueue.releaseAllPointersOlderThan(mElement4, eventTime);
-        assertEquals("after releaseAllPointersOlderThan", 2, Element.sPhantomUpCount);
-        assertEquals("after releaseAllPointersOlderThan", 2, mQueue.size());
-        assertEquals("after releaseAllPointersOlderThan", "[2 4]", mQueue.toString());
-        assertEquals("after releaseAllPointersOlderThan element1",
-                eventTime + 1, mElement1.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan element2",
-                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan element3",
-                eventTime + 2, mElement3.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(2, Element.sPhantomUpCount);
+        assertEquals(2, mQueue.size());
+        assertEquals("[2 4]", mQueue.toString());
+        assertEquals(eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(eventTime + 2, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 
-    public void testReleaseAllPointersOlderThanWithoutModifier() {
+    public void testReleaseAllPointersOlderThanWithoutModifierMiddle() {
+        mQueue.add(mElement1);
+        mQueue.add(mElement2);
+        mQueue.add(mElement3);
+        mQueue.add(mElement4);
+
+        final long eventTime = 123;
+        Element.sPhantomUpCount = 0;
+        mQueue.releaseAllPointersOlderThan(mElement3, eventTime);
+        assertEquals(2, Element.sPhantomUpCount);
+        assertEquals(2, mQueue.size());
+        assertEquals("[3 4]", mQueue.toString());
+        assertEquals(eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals(eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+    }
+
+    public void testReleaseAllPointersOlderThanWithoutModifierLast() {
         mQueue.add(mElement1);
         mQueue.add(mElement2);
         mQueue.add(mElement3);
@@ -221,19 +240,13 @@
         final long eventTime = 123;
         Element.sPhantomUpCount = 0;
         mQueue.releaseAllPointersOlderThan(mElement4, eventTime);
-        assertEquals("after releaseAllPointersOlderThan without modifier",
-                3, Element.sPhantomUpCount);
-        assertEquals("after releaseAllPointersOlderThan without modifier", 1, mQueue.size());
-        assertEquals("after releaseAllPointersOlderThan without modifier",
-                "[4]", mQueue.toString());
-        assertEquals("after releaseAllPointersOlderThan without modifier element1",
-                eventTime + 1, mElement1.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan without modifier element2",
-                eventTime + 2, mElement2.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan without modifier element3",
-                eventTime + 3, mElement3.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersOlderThan without modifier element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(3, Element.sPhantomUpCount);
+        assertEquals(1, mQueue.size());
+        assertEquals("[4]", mQueue.toString());
+        assertEquals(eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals(eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals(eventTime + 3, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 
     public void testReleaseAllPointersExcept() {
@@ -246,17 +259,13 @@
         final long eventTime = 123;
         Element.sPhantomUpCount = 0;
         mQueue.releaseAllPointersExcept(mElement3, eventTime);
-        assertEquals("after releaseAllPointersExcept", 3, Element.sPhantomUpCount);
-        assertEquals("after releaseAllPointersExcept", 1, mQueue.size());
-        assertEquals("after releaseAllPointersExcept", "[3]", mQueue.toString());
-        assertEquals("after releaseAllPointersExcept element1",
-                eventTime + 1, mElement1.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersExcept element2",
-                eventTime + 2, mElement2.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersExcept element3",
-                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
-        assertEquals("after releaseAllPointersExcept element4",
-                eventTime + 3, mElement4.mPhantomUpEventTime);
+        assertEquals(3, Element.sPhantomUpCount);
+        assertEquals(1, mQueue.size());
+        assertEquals("[3]", mQueue.toString());
+        assertEquals(eventTime + 1, mElement1.mPhantomUpEventTime);
+        assertEquals(eventTime + 2, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(eventTime + 3, mElement4.mPhantomUpEventTime);
     }
 
     public void testHasModifierKeyOlderThan() {
@@ -268,54 +277,46 @@
         mQueue.add(mElement3);
         mQueue.add(mElement4);
 
-        assertFalse("hasModifierKeyOlderThan element1", mQueue.hasModifierKeyOlderThan(mElement1));
-        assertFalse("hasModifierKeyOlderThan element2", mQueue.hasModifierKeyOlderThan(mElement2));
-        assertFalse("hasModifierKeyOlderThan element3", mQueue.hasModifierKeyOlderThan(mElement3));
-        assertFalse("hasModifierKeyOlderThan element4", mQueue.hasModifierKeyOlderThan(mElement4));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement1));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement2));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement3));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement4));
 
         mElement2.mIsModifier = true;
-        assertFalse("hasModifierKeyOlderThan element1", mQueue.hasModifierKeyOlderThan(mElement1));
-        assertFalse("hasModifierKeyOlderThan element2", mQueue.hasModifierKeyOlderThan(mElement2));
-        assertTrue("hasModifierKeyOlderThan element3", mQueue.hasModifierKeyOlderThan(mElement3));
-        assertTrue("hasModifierKeyOlderThan element4", mQueue.hasModifierKeyOlderThan(mElement4));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement1));
+        assertFalse(mQueue.hasModifierKeyOlderThan(mElement2));
+        assertTrue(mQueue.hasModifierKeyOlderThan(mElement3));
+        assertTrue(mQueue.hasModifierKeyOlderThan(mElement4));
 
-        assertEquals("after hasModifierKeyOlderThan", 0, Element.sPhantomUpCount);
-        assertEquals("after hasModifierKeyOlderThan", 4, mQueue.size());
-        assertEquals("after hasModifierKeyOlderThan", "[1 2 3 4]", mQueue.toString());
-        assertEquals("after hasModifierKeyOlderThan element1",
-                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
-        assertEquals("after hasModifierKeyOlderThan element2",
-                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
-        assertEquals("after hasModifierKeyOlderThan element3",
-                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
-        assertEquals("after hasModifierKeyOlderThan element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(0, Element.sPhantomUpCount);
+        assertEquals(4, mQueue.size());
+        assertEquals("[1 2 3 4]", mQueue.toString());
+        assertEquals(Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 
     public void testIsAnyInSlidingKeyInput() {
         Element.sPhantomUpCount = 0;
-        assertFalse("isAnyInSlidingKeyInput empty", mQueue.isAnyInSlidingKeyInput());
+        assertFalse(mQueue.isAnyInSlidingKeyInput());
 
         mQueue.add(mElement1);
         mQueue.add(mElement2);
         mQueue.add(mElement3);
         mQueue.add(mElement4);
 
-        assertFalse("isAnyInSlidingKeyInput element1", mQueue.isAnyInSlidingKeyInput());
+        assertFalse(mQueue.isAnyInSlidingKeyInput());
 
         mElement3.mIsInSlidingKeyInput = true;
-        assertTrue("isAnyInSlidingKeyInput element1", mQueue.isAnyInSlidingKeyInput());
+        assertTrue(mQueue.isAnyInSlidingKeyInput());
 
-        assertEquals("after isAnyInSlidingKeyInput", 0, Element.sPhantomUpCount);
-        assertEquals("after isAnyInSlidingKeyInput", 4, mQueue.size());
-        assertEquals("after isAnyInSlidingKeyInput", "[1 2 3 4]", mQueue.toString());
-        assertEquals("after isAnyInSlidingKeyInput element1",
-                Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
-        assertEquals("after isAnyInSlidingKeyInput element2",
-                Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
-        assertEquals("after isAnyInSlidingKeyInput element3",
-                Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
-        assertEquals("after isAnyInSlidingKeyInput element4",
-                Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
+        assertEquals(0, Element.sPhantomUpCount);
+        assertEquals(4, mQueue.size());
+        assertEquals("[1 2 3 4]", mQueue.toString());
+        assertEquals(Element.NOT_HAPPENED, mElement1.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement2.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement3.mPhantomUpEventTime);
+        assertEquals(Element.NOT_HAPPENED, mElement4.mPhantomUpEventTime);
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java b/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java
new file mode 100644
index 0000000..ed16846
--- /dev/null
+++ b/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java
@@ -0,0 +1,176 @@
+/*
+ * Copyright (C) 2013 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.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+import java.util.HashMap;
+
+@SmallTest
+public class ResourceUtilsTests extends AndroidTestCase {
+    public void testFindDefaultConstant() {
+        final String[] nullArray = null;
+        assertNull(ResourceUtils.findDefaultConstant(nullArray));
+
+        final String[] emptyArray = {};
+        assertNull(ResourceUtils.findDefaultConstant(emptyArray));
+
+        final String[] array = {
+            "HARDWARE=grouper,0.3",
+            "HARDWARE=mako,0.4",
+            ",defaultValue1",
+            "HARDWARE=manta,0.2",
+            ",defaultValue2",
+        };
+        assertEquals(ResourceUtils.findDefaultConstant(array), "defaultValue1");
+    }
+
+    public void testFindConstantForKeyValuePairsSimple() {
+        final HashMap<String,String> anyKeyValue = CollectionUtils.newHashMap();
+        anyKeyValue.put("anyKey", "anyValue");
+        final HashMap<String,String> nullKeyValue = null;
+        final HashMap<String,String> emptyKeyValue = CollectionUtils.newHashMap();
+
+        final String[] nullArray = null;
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(anyKeyValue, nullArray));
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, nullArray));
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(nullKeyValue, nullArray));
+
+        final String[] emptyArray = {};
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(anyKeyValue, emptyArray));
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, emptyArray));
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(nullKeyValue, emptyArray));
+
+        final String HARDWARE_KEY = "HARDWARE";
+        final String[] array = {
+            ",defaultValue",
+            "HARDWARE=grouper,0.3",
+            "HARDWARE=mako,0.4",
+            "HARDWARE=manta,0.2",
+            "HARDWARE=mako,0.5",
+        };
+
+        final HashMap<String,String> keyValues = CollectionUtils.newHashMap();
+        keyValues.put(HARDWARE_KEY, "grouper");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        keyValues.put(HARDWARE_KEY, "mako");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        keyValues.put(HARDWARE_KEY, "manta");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+
+        try {
+            keyValues.clear();
+            keyValues.put("hardware", "grouper");
+            final String constant = ResourceUtils.findConstantForKeyValuePairs(keyValues, array);
+            fail("condition without HARDWARE must fail: constant=" + constant);
+        } catch (final RuntimeException e) {
+            assertEquals(e.getMessage(), "Found unknown key: HARDWARE=grouper");
+        }
+        keyValues.clear();
+        keyValues.put(HARDWARE_KEY, "MAKO");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
+        keyValues.put(HARDWARE_KEY, "mantaray");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
+
+        try {
+            final String constant = ResourceUtils.findConstantForKeyValuePairs(
+                    emptyKeyValue, array);
+            fail("emptyCondition shouldn't match: constant=" + constant);
+        } catch (final RuntimeException e) {
+            assertEquals(e.getMessage(), "Found unknown key: HARDWARE=grouper");
+        }
+    }
+
+    public void testFindConstantForKeyValuePairsCombined() {
+        final String HARDWARE_KEY = "HARDWARE";
+        final String MODEL_KEY = "MODEL";
+        final String MANUFACTURER_KEY = "MANUFACTURER";
+        final String[] array = {
+            ",defaultValue",
+            "HARDWARE=grouper:MANUFACTURER=asus,0.3",
+            "HARDWARE=mako:MODEL=Nexus 4,0.4",
+            "HARDWARE=manta:MODEL=Nexus 10:MANUFACTURER=samsung,0.2"
+        };
+        final String[] failArray = {
+            ",defaultValue",
+            "HARDWARE=grouper:MANUFACTURER=ASUS,0.3",
+            "HARDWARE=mako:MODEL=Nexus_4,0.4",
+            "HARDWARE=mantaray:MODEL=Nexus 10:MANUFACTURER=samsung,0.2"
+        };
+
+        final HashMap<String,String> keyValues = CollectionUtils.newHashMap();
+        keyValues.put(HARDWARE_KEY, "grouper");
+        keyValues.put(MODEL_KEY, "Nexus 7");
+        keyValues.put(MANUFACTURER_KEY, "asus");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
+
+        keyValues.clear();
+        keyValues.put(HARDWARE_KEY, "mako");
+        keyValues.put(MODEL_KEY, "Nexus 4");
+        keyValues.put(MANUFACTURER_KEY, "LGE");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
+
+        keyValues.clear();
+        keyValues.put(HARDWARE_KEY, "manta");
+        keyValues.put(MODEL_KEY, "Nexus 10");
+        keyValues.put(MANUFACTURER_KEY, "samsung");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
+        keyValues.put(HARDWARE_KEY, "mantaray");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray), "0.2");
+    }
+
+    public void testFindConstantForKeyValuePairsRegexp() {
+        final String HARDWARE_KEY = "HARDWARE";
+        final String MODEL_KEY = "MODEL";
+        final String MANUFACTURER_KEY = "MANUFACTURER";
+        final String[] array = {
+            ",defaultValue",
+            "HARDWARE=grouper|tilapia:MANUFACTURER=asus,0.3",
+            "HARDWARE=[mM][aA][kK][oO]:MODEL=Nexus 4,0.4",
+            "HARDWARE=manta.*:MODEL=Nexus 10:MANUFACTURER=samsung,0.2"
+        };
+
+        final HashMap<String,String> keyValues = CollectionUtils.newHashMap();
+        keyValues.put(HARDWARE_KEY, "grouper");
+        keyValues.put(MODEL_KEY, "Nexus 7");
+        keyValues.put(MANUFACTURER_KEY, "asus");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        keyValues.put(HARDWARE_KEY, "tilapia");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+
+        keyValues.clear();
+        keyValues.put(HARDWARE_KEY, "mako");
+        keyValues.put(MODEL_KEY, "Nexus 4");
+        keyValues.put(MANUFACTURER_KEY, "LGE");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        keyValues.put(HARDWARE_KEY, "MAKO");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+
+        keyValues.clear();
+        keyValues.put(HARDWARE_KEY, "manta");
+        keyValues.put(MODEL_KEY, "Nexus 10");
+        keyValues.put(MANUFACTURER_KEY, "samsung");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        keyValues.put(HARDWARE_KEY, "mantaray");
+        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 1e3cc8a..29e790a 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -178,7 +178,7 @@
         assertTrue(StringUtils.isIdenticalAfterDowncase(""));
     }
 
-    private void checkCapitalize(final String src, final String dst, final String separators,
+    private static void checkCapitalize(final String src, final String dst, final String separators,
             final Locale locale) {
         assertEquals(dst, StringUtils.capitalizeEachWord(src, separators, locale));
         assert(src.equals(dst)