Merge "Build native subdirectories first"
diff --git a/java/res/layout/research_feedback_fragment_layout.xml b/java/res/layout/research_feedback_fragment_layout.xml
index 2a90ba2..2725e7f 100644
--- a/java/res/layout/research_feedback_fragment_layout.xml
+++ b/java/res/layout/research_feedback_fragment_layout.xml
@@ -14,107 +14,111 @@
      limitations under the License.
 -->
 
-<LinearLayout
-     xmlns:android="http://schemas.android.com/apk/res/android"
-     android:layout_width="fill_parent"
-     android:layout_height="fill_parent"
-     android:orientation="vertical"
->
-
-    <!-- Mimic a dialog title.  Necessary since the dialog is actually an activity, so the normal
-        dialog title construction code is not available. -->
+<!-- Adapted from frameworks/base/core/res/res/layout/alert_dialog_holo.xml.  We
+   want a dialog, but it must be its own activity so we can launch the soft
+   keyboard on it.  A regular dialog will not work since it would be launched from
+   the IME. -->
+<ScrollView>
     <LinearLayout
+         xmlns:android="http://schemas.android.com/apk/res/android"
          android:layout_width="match_parent"
-         android:layout_height="wrap_content"
-         android:orientation="vertical"
-    >
-        <com.android.internal.widget.DialogTitle
-            style="?android:attr/windowTitleStyle"
-            android:singleLine="true"
-            android:ellipsize="end"
+         android:layout_height="match_parent"
+         android:layout_marginStart="8dip"
+         android:layout_marginEnd="8dip"
+         android:orientation="vertical">
+        <LinearLayout
+             android:layout_width="match_parent"
+             android:layout_height="wrap_content"
+             android:orientation="vertical">
+            <View android:layout_width="match_parent"
+                android:layout_height="2dip"
+                android:visibility="gone"
+                android:background="@android:color/holo_blue_light" />
+            <TextView
+                style="?android:attr/windowTitleStyle"
+                android:singleLine="true"
+                android:ellipsize="end"
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:minHeight="64dip"
+                android:layout_marginLeft="16dip"
+                android:layout_marginRight="16dip"
+                android:gravity="center_vertical|left"
+                android:text="@string/research_feedback_dialog_title" />
+            <View
+                android:layout_width="match_parent"
+                android:layout_height="2dip"
+                android:background="@android:color/holo_blue_light" />
+        </LinearLayout>
+
+        <EditText
+            android:id="@+id/research_feedback_contents"
+            android:layout_height="wrap_content"
             android:layout_width="match_parent"
-            android:layout_height="64dip"
+            android:layout_gravity="fill_horizontal|center_vertical"
+            android:layout_marginLeft="8dip"
+            android:layout_marginRight="8dip"
+            android:layout_marginBottom="8dip"
+            android:layout_marginTop="8dip"
+            android:minLines="2"
+            android:scrollbars="vertical"
+            android:hint="@string/research_feedback_hint"
+            android:inputType="textMultiLine">
+            <requestFocus />
+        </EditText>
+        <CheckBox
+            android:id="@+id/research_feedback_include_account_name"
+            android:layout_height="wrap_content"
+            android:layout_width="match_parent"
             android:layout_marginLeft="16dip"
             android:layout_marginRight="16dip"
-            android:gravity="center_vertical|left"
-            android:text="@string/research_feedback_dialog_title" />
-        <View
+            android:layout_marginBottom="8dip"
+            android:checked="false"
+            android:text="@string/research_feedback_include_account_name_label" />
+        <CheckBox
+            android:id="@+id/research_feedback_include_recording_checkbox"
+            android:layout_height="wrap_content"
             android:layout_width="match_parent"
-            android:layout_height="2dip"
-            android:background="@android:color/holo_blue_light" />
-    </LinearLayout>
-
-    <EditText
-        android:id="@+id/research_feedback_contents"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:layout_gravity="fill_horizontal|center_vertical"
-        android:layout_marginLeft="8dip"
-        android:layout_marginRight="8dip"
-        android:layout_marginBottom="8dip"
-        android:layout_marginTop="8dip"
-        android:lines="2"
-        android:hint="@string/research_feedback_hint"
-        android:inputType="textMultiLine"
-        android:imeOptions="flagNoFullscreen"
-    >
-        <requestFocus />
-    </EditText>
-
-    <CheckBox
-        android:id="@+id/research_feedback_include_history"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:layout_marginBottom="8dip"
-        android:checked="true"
-        android:text="@string/research_feedback_include_history_label"
-    />
-
-    <CheckBox
-        android:id="@+id/research_feedback_include_account_name"
-        android:layout_height="wrap_content"
-        android:layout_width="match_parent"
-        android:layout_marginBottom="8dip"
-        android:checked="false"
-        android:text="@string/research_feedback_include_account_name_label"
-    />
-    <LinearLayout
-        android:layout_width="match_parent"
-        android:layout_height="wrap_content"
-        android:orientation="vertical"
-        android:divider="?android:attr/dividerHorizontal"
-        android:showDividers="beginning"
-        android:dividerPadding="0dip"
-    >
+            android:layout_marginLeft="16dip"
+            android:layout_marginRight="16dip"
+            android:layout_marginBottom="8dip"
+            android:checked="false"
+            android:text="@string/research_feedback_include_recording_label" />
         <LinearLayout
-            style="?android:attr/buttonBarStyle"
             android:layout_width="match_parent"
             android:layout_height="wrap_content"
-            android:orientation="horizontal"
-            android:measureWithLargestChild="true"
-        >
-            <Button
-                android:id="@+id/research_feedback_cancel_button"
-                android:layout_width="0dip"
-                android:layout_gravity="left"
-                android:layout_weight="1"
-                android:maxLines="2"
-                style="?android:attr/buttonBarButtonStyle"
-                android:textSize="14sp"
-                android:text="@string/research_feedback_cancel"
+            android:orientation="vertical"
+            android:divider="?android:attr/dividerHorizontal"
+            android:showDividers="beginning"
+            android:dividerPadding="0dip">
+            <LinearLayout
+                style="?android:attr/buttonBarStyle"
+                android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-            />
-            <Button
-                android:id="@+id/research_feedback_send_button"
-                android:layout_width="0dip"
-                android:layout_gravity="right"
-                android:layout_weight="1"
-                android:maxLines="2"
-                style="?android:attr/buttonBarButtonStyle"
-                android:textSize="14sp"
-                android:text="@string/research_feedback_send"
-                android:layout_height="wrap_content"
-            />
+                android:orientation="horizontal"
+                android:layoutDirection="locale"
+                android:measureWithLargestChild="true">
+                <Button
+                    android:id="@+id/research_feedback_cancel_button"
+                    android:layout_width="wrap_content"
+                    android:layout_gravity="left"
+                    android:layout_weight="1"
+                    android:maxLines="2"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:textSize="14sp"
+                    android:text="@string/research_feedback_cancel"
+                    android:layout_height="wrap_content" />
+                <Button
+                    android:id="@+id/research_feedback_send_button"
+                    android:layout_width="wrap_content"
+                    android:layout_gravity="right"
+                    android:layout_weight="1"
+                    android:maxLines="2"
+                    style="?android:attr/buttonBarButtonStyle"
+                    android:textSize="14sp"
+                    android:text="@string/research_feedback_send"
+                    android:layout_height="wrap_content" />
+            </LinearLayout>
         </LinearLayout>
     </LinearLayout>
-</LinearLayout>
+</ScrollView>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index 51a0758..3a7b39e 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -173,25 +173,18 @@
         <attr name="themeId" format="integer" />
         <!-- Touch position correction -->
         <attr name="touchPositionCorrectionData" format="reference" />
-        <!-- Default keyboard height -->
-        <attr name="keyboardHeight" format="dimension|fraction" />
-        <!-- Maximum keyboard height, in pixels or percentage of display height -->
-        <attr name="maxKeyboardHeight" format="dimension|fraction" />
-        <!-- Minimum keyboard height represented in pixels, percentage of display height if fraction
-             is positive, or percentage of display width if fraction is negative. -->
-        <attr name="minKeyboardHeight" format="dimension|fraction" />
-        <!-- Keyboard top, bottom, both horizontal edges paddings. -->
-        <attr name="keyboardTopPadding" format="dimension|fraction" />
-        <attr name="keyboardBottomPadding" format="dimension|fraction" />
-        <attr name="keyboardHorizontalEdgesPadding" format="dimension|fraction" />
-        <!-- Default height of a row (key height + vertical gap), in pixels or percentage of
+        <!-- Keyboard top, bottom, left, right edges paddings, in propotion of keyboard height. -->
+        <attr name="keyboardTopPadding" format="fraction" />
+        <attr name="keyboardBottomPadding" format="fraction" />
+        <attr name="keyboardLeftPadding" format="fraction" />
+        <attr name="keyboardRightPadding" format="fraction" />
+        <!-- Default height of a row (key height + vertical gap), in pixels or in the proportion of
              keyboard height. -->
         <attr name="rowHeight" format="dimension|fraction" />
-        <!-- Default horizontal gap between keys, in pixels or percentage of keyboard width. -->
-        <attr name="horizontalGap" format="dimension|fraction" />
-        <!-- Default vertical gap between rows of keys, in pixels or percentage of keyboard
-             height. -->
-        <attr name="verticalGap" format="dimension|fraction" />
+        <!-- Default horizontal gap between keys, in the proportion of keyboard width. -->
+        <attr name="horizontalGap" format="fraction" />
+        <!-- Default vertical gap between rows of keys, in the proportion of keyboard height. -->
+        <attr name="verticalGap" format="fraction" />
         <!-- More keys keyboard layout template -->
         <attr name="moreKeysTemplate" format="reference" />
         <!-- Icon set for key top and key preview.
@@ -295,19 +288,20 @@
         <attr name="keyIconPreview" format="string" />
         <!-- The key style to specify a set of key attributes defined by <key_style/> -->
         <attr name="keyStyle" format="string" />
-        <!-- Visual insets -->
-        <attr name="visualInsetsLeft" format="dimension|fraction" />
-        <attr name="visualInsetsRight" format="dimension|fraction" />
-        <!-- Width of the key, in pixels or percentage of display width.
-             If the value is fillRight, the actual key width will be determined to fill out the area
-             up to the right edge of the keyboard. -->
+        <!-- Visual insets, in the proportion of keyboard width. -->
+        <attr name="visualInsetsLeft" format="fraction" />
+        <attr name="visualInsetsRight" format="fraction" />
+        <!-- Width of the key, in the proportion of keyboard width.
+             If the value is fillRight, the actual key width will be determined to fill out the
+             area up to the right edge of the keyboard. -->
         <!-- This should be aligned with KeyboardBuilder.Row.KEYWIDTH_* -->
-        <attr name="keyWidth" format="dimension|fraction|enum">
+        <attr name="keyWidth" format="fraction|enum">
             <enum name="fillRight" value="-1" />
         </attr>
-        <!-- The X-coordinate of upper right corner of this key including horizontal gap.
+        <!-- The X-coordinate of upper right corner of this key including horizontal gap, in the
+             proportion of keyboard width.
              If the value is negative, the origin is the right edge of the keyboard. -->
-        <attr name="keyXPos" format="dimension|fraction" />
+        <attr name="keyXPos" format="fraction" />
 
         <!-- Key top visual attributes -->
         <attr name="keyTypeface" format="enum">
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index eb0934c..dd42acf 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -31,7 +31,8 @@
 
     <fraction name="keyboard_top_padding">1.556%p</fraction>
     <fraction name="keyboard_bottom_padding">4.669%p</fraction>
-    <fraction name="keyboard_horizontal_edges_padding">0%p</fraction>
+    <fraction name="keyboard_left_padding">0%p</fraction>
+    <fraction name="keyboard_right_padding">0%p</fraction>
     <fraction name="key_bottom_gap">6.250%p</fraction>
     <fraction name="key_horizontal_gap">1.352%p</fraction>
 
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index 1b518a1..8822e8d 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -69,8 +69,10 @@
     <!-- Option summary for showing language switch key [CHAR LIMIT=65] -->
     <string name="show_language_switch_key_summary">Show when multiple input languages are enabled</string>
 
-    <!-- Option to enable sliding key input preview. The user can see a rubber band during sliding key input. [CHAR LIMIT=30]-->
-    <string name="sliding_key_input_preview">Sliding key input preview</string>
+    <!-- Option to enable sliding key input indicator. The user can see a rubber band-like effect during sliding key input. [CHAR LIMIT=30]-->
+    <string name="sliding_key_input_preview">Show slide indicator</string>
+    <!-- Option summary to enable sliding key input indicator. The user can see a rubber band-like effect during sliding key input. [CHAR LIMIT=65]-->
+    <string name="sliding_key_input_preview_summary">Display visual cue while sliding from Shift or Symbol keys</string>
 
     <!-- Option for the dismiss delay of the key popup [CHAR LIMIT=25] -->
     <string name="key_preview_popup_dismiss_delay">Key popup dismiss delay</string>
@@ -276,6 +278,9 @@
     <!-- Text for checkbox option to include user account name in feedback for research purposes [CHAR LIMIT=50] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_include_account_name_label" translatable="false">Include account name</string>
+    <!-- Text for checkbox option to include a recording in feedback for research purposes [CHAR LIMIT=50] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_include_recording_label" translatable="false">Include recorded demonstration</string>
     <!-- Hint to user about the text entry field where they should enter research feedback [CHAR LIMIT=40] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_hint" translatable="false">Enter your feedback here.</string>
@@ -285,6 +290,13 @@
     <!-- Dialog button choice to cancel sending research feedback [CHAR LIMIT=35] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_feedback_cancel" translatable="false">Cancel</string>
+    <!-- Temporary notification to provide user with instructions about stopping a recording
+      - operation[CHAR LIMIT=100] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_demonstration_instructions" translatable="false">Please demonstrate the issue you are writing about.\n\nWhen finished, select the \"Bug?\" button again."</string>
+    <!-- Temporary notification of recording failure [CHAR LIMIT=100] -->
+    <!-- TODO: remove translatable=false attribute once text is stable -->
+    <string name="research_feedback_recording_failure" translatable="false">Recording cancelled due to timeout</string>
     <!-- Toast notification to ask user to quit the research feedback dialog to perform this operation [CHAR LIMIT=100] -->
     <!-- TODO: remove translatable=false attribute once text is stable -->
     <string name="research_please_exit_feedback_form" translatable="false">Please exit the feedback dialog to access the research log menu</string>
diff --git a/java/res/values/styles.xml b/java/res/values/styles.xml
index f07f2d3..fb59c74 100644
--- a/java/res/values/styles.xml
+++ b/java/res/values/styles.xml
@@ -21,13 +21,11 @@
         <item name="themeId">0</item>
         <item name="touchPositionCorrectionData">@array/touch_position_correction_data_default</item>
         <item name="rowHeight">25%p</item>
-        <item name="keyboardHeight">@dimen/keyboardHeight</item>
-        <item name="maxKeyboardHeight">@fraction/maxKeyboardHeight</item>
-        <item name="minKeyboardHeight">@fraction/minKeyboardHeight</item>
         <item name="moreKeysTemplate">@xml/kbd_more_keys_keyboard_template</item>
         <item name="keyboardTopPadding">@fraction/keyboard_top_padding</item>
         <item name="keyboardBottomPadding">@fraction/keyboard_bottom_padding</item>
-        <item name="keyboardHorizontalEdgesPadding">@fraction/keyboard_horizontal_edges_padding</item>
+        <item name="keyboardLeftPadding">@fraction/keyboard_left_padding</item>
+        <item name="keyboardRightPadding">@fraction/keyboard_right_padding</item>
         <item name="horizontalGap">@fraction/key_horizontal_gap</item>
         <item name="verticalGap">@fraction/key_bottom_gap</item>
         <item name="maxMoreKeysColumn">@integer/config_max_more_keys_column</item>
@@ -122,9 +120,9 @@
         name="MoreKeysKeyboard"
         parent="Keyboard"
     >
-        <item name="keyboardTopPadding">0dp</item>
-        <item name="keyboardBottomPadding">0dp</item>
-        <item name="horizontalGap">0dp</item>
+        <item name="keyboardTopPadding">0%p</item>
+        <item name="keyboardBottomPadding">0%p</item>
+        <item name="horizontalGap">0%p</item>
         <item name="touchPositionCorrectionData">@null</item>
     </style>
     <style
@@ -227,9 +225,9 @@
         name="MoreKeysKeyboard.Stone"
         parent="Keyboard.Stone"
     >
-        <item name="keyboardTopPadding">0dp</item>
-        <item name="keyboardBottomPadding">0dp</item>
-        <item name="horizontalGap">0dp</item>
+        <item name="keyboardTopPadding">0%p</item>
+        <item name="keyboardBottomPadding">0%p</item>
+        <item name="horizontalGap">0%p</item>
         <item name="touchPositionCorrectionData">@null</item>
     </style>
     <style
@@ -297,9 +295,9 @@
         name="MoreKeysKeyboard.Gingerbread"
         parent="Keyboard.Gingerbread"
     >
-        <item name="keyboardTopPadding">0dp</item>
-        <item name="keyboardBottomPadding">0dp</item>
-        <item name="horizontalGap">0dp</item>
+        <item name="keyboardTopPadding">0%p</item>
+        <item name="keyboardBottomPadding">0%p</item>
+        <item name="horizontalGap">0%p</item>
         <item name="touchPositionCorrectionData">@null</item>
     </style>
     <style
@@ -356,9 +354,9 @@
         name="MoreKeysKeyboard.IceCreamSandwich"
         parent="Keyboard.IceCreamSandwich"
     >
-        <item name="keyboardTopPadding">0dp</item>
-        <item name="keyboardBottomPadding">0dp</item>
-        <item name="horizontalGap">0dp</item>
+        <item name="keyboardTopPadding">0%p</item>
+        <item name="keyboardBottomPadding">0%p</item>
+        <item name="horizontalGap">0%p</item>
         <item name="touchPositionCorrectionData">@null</item>
     </style>
     <style
diff --git a/java/res/xml-land/kbd_number.xml b/java/res/xml-land/kbd_number.xml
index 8d31df1..0dced28 100644
--- a/java/res/xml-land/kbd_number.xml
+++ b/java/res/xml-land/kbd_number.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="26.67%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-land/kbd_phone.xml b/java/res/xml-land/kbd_phone.xml
index 2f8fc35..e5d7b44 100644
--- a/java/res/xml-land/kbd_phone.xml
+++ b/java/res/xml-land/kbd_phone.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="26.67%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-land/kbd_phone_symbols.xml b/java/res/xml-land/kbd_phone_symbols.xml
index 0e6bcdd..b881e62 100644
--- a/java/res/xml-land/kbd_phone_symbols.xml
+++ b/java/res/xml-land/kbd_phone_symbols.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="26.67%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw600dp-land/kbd_number.xml b/java/res/xml-sw600dp-land/kbd_number.xml
index 63dfc90..a9e8121 100644
--- a/java/res/xml-sw600dp-land/kbd_number.xml
+++ b/java/res/xml-sw600dp-land/kbd_number.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="18%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw600dp-land/kbd_phone.xml b/java/res/xml-sw600dp-land/kbd_phone.xml
index b616111..4cabdeb 100644
--- a/java/res/xml-sw600dp-land/kbd_phone.xml
+++ b/java/res/xml-sw600dp-land/kbd_phone.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="18%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw600dp-land/kbd_phone_symbols.xml b/java/res/xml-sw600dp-land/kbd_phone_symbols.xml
index 9b0bee0..9c3e825 100644
--- a/java/res/xml-sw600dp-land/kbd_phone_symbols.xml
+++ b/java/res/xml-sw600dp-land/kbd_phone_symbols.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="18%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw768dp-land/kbd_number.xml b/java/res/xml-sw768dp-land/kbd_number.xml
index de8d559..1cb775e 100644
--- a/java/res/xml-sw768dp-land/kbd_number.xml
+++ b/java/res/xml-sw768dp-land/kbd_number.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="13.250%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw768dp-land/kbd_phone.xml b/java/res/xml-sw768dp-land/kbd_phone.xml
index f88a076..8905182 100644
--- a/java/res/xml-sw768dp-land/kbd_phone.xml
+++ b/java/res/xml-sw768dp-land/kbd_phone.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="13.250%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml-sw768dp-land/kbd_phone_symbols.xml b/java/res/xml-sw768dp-land/kbd_phone_symbols.xml
index eaa413e..6038b1f 100644
--- a/java/res/xml-sw768dp-land/kbd_phone_symbols.xml
+++ b/java/res/xml-sw768dp-land/kbd_phone_symbols.xml
@@ -20,7 +20,8 @@
 
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
-    latin:keyboardHorizontalEdgesPadding="10%p"
+    latin:keyboardLeftPadding="10%p"
+    latin:keyboardRightPadding="10%p"
     latin:keyWidth="13.250%p"
     latin:touchPositionCorrectionData="@array/touch_position_correction_data_default"
 >
diff --git a/java/res/xml/prefs.xml b/java/res/xml/prefs.xml
index da6e601..5ed0bb0 100644
--- a/java/res/xml/prefs.xml
+++ b/java/res/xml/prefs.xml
@@ -145,6 +145,7 @@
             <CheckBoxPreference
                 android:key="pref_sliding_key_input_preview"
                 android:title="@string/sliding_key_input_preview"
+                android:summary="@string/sliding_key_input_preview_summary"
                 android:persistent="true"
                 android:defaultValue="true" />
             <ListPreference
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 9b97175..1e5af51 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -41,7 +41,6 @@
 import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.StringUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -225,8 +224,8 @@
     public Key(final Resources res, final KeyboardParams params, final KeyboardRow row,
             final XmlPullParser parser) throws XmlPullParserException {
         final float horizontalGap = isSpacer() ? 0 : params.mHorizontalGap;
-        final int keyHeight = row.mRowHeight;
-        mHeight = keyHeight - params.mVerticalGap;
+        final int rowHeight = row.mRowHeight;
+        mHeight = rowHeight - params.mVerticalGap;
 
         final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard_Key);
@@ -241,17 +240,18 @@
         mY = keyYPos;
         mWidth = Math.round(keyWidth - horizontalGap);
         mHitBox.set(Math.round(keyXPos), keyYPos, Math.round(keyXPos + keyWidth) + 1,
-                keyYPos + keyHeight);
+                keyYPos + rowHeight);
         // Update row to have current x coordinate.
         row.setXPos(keyXPos + keyWidth);
 
         mBackgroundType = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_backgroundType, row.getDefaultBackgroundType());
 
-        final int visualInsetsLeft = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
-                R.styleable.Keyboard_Key_visualInsetsLeft, params.mBaseWidth, 0));
-        final int visualInsetsRight = Math.round(ResourceUtils.getDimensionOrFraction(keyAttr,
-                R.styleable.Keyboard_Key_visualInsetsRight, params.mBaseWidth, 0));
+        final int baseWidth = params.mBaseWidth;
+        final int visualInsetsLeft = Math.round(keyAttr.getFraction(
+                R.styleable.Keyboard_Key_visualInsetsLeft, baseWidth, baseWidth, 0));
+        final int visualInsetsRight = Math.round(keyAttr.getFraction(
+                R.styleable.Keyboard_Key_visualInsetsRight, baseWidth, baseWidth, 0));
         mIconId = KeySpecParser.getIconId(style.getString(keyAttr,
                 R.styleable.Keyboard_Key_keyIcon));
         final int disabledIconId = KeySpecParser.getIconId(style.getString(keyAttr,
@@ -470,11 +470,11 @@
     }
 
     public void markAsLeftEdge(final KeyboardParams params) {
-        mHitBox.left = params.mHorizontalEdgesPadding;
+        mHitBox.left = params.mLeftPadding;
     }
 
     public void markAsRightEdge(final KeyboardParams params) {
-        mHitBox.right = params.mOccupiedWidth - params.mHorizontalEdgesPadding;
+        mHitBox.right = params.mOccupiedWidth - params.mRightPadding;
     }
 
     public void markAsTopEdge(final KeyboardParams params) {
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index c2036fc..e87ecbc 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -30,11 +30,11 @@
  * <p>The layout file for a keyboard contains XML that looks like the following snippet:</p>
  * <pre>
  * &lt;Keyboard
- *         latin:keyWidth="%10p"
- *         latin:keyHeight="50px"
- *         latin:horizontalGap="2px"
- *         latin:verticalGap="2px" &gt;
- *     &lt;Row latin:keyWidth="32px" &gt;
+ *         latin:keyWidth="10%p"
+ *         latin:rowHeight="50px"
+ *         latin:horizontalGap="2%p"
+ *         latin:verticalGap="2%p" &gt;
+ *     &lt;Row latin:keyWidth="10%p" &gt;
  *         &lt;Key latin:keyLabel="A" /&gt;
  *         ...
  *     &lt;/Row&gt;
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index 4b43bcc..ee8ee9a 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -18,6 +18,7 @@
 
 import static com.android.inputmethod.latin.Constants.Subtype.ExtraValue.KEYBOARD_LAYOUT_SET;
 
+import android.content.res.Configuration;
 import android.text.InputType;
 import android.text.TextUtils;
 import android.view.inputmethod.EditorInfo;
@@ -62,8 +63,10 @@
     public final InputMethodSubtype mSubtype;
     public final Locale mLocale;
     public final int mDeviceFormFactor;
+    // TODO: Remove this member. It is used only for logging purpose.
     public final int mOrientation;
     public final int mWidth;
+    public final int mHeight;
     public final int mMode;
     public final int mElementId;
     private final EditorInfo mEditorInfo;
@@ -81,7 +84,8 @@
         mLocale = SubtypeLocale.getSubtypeLocale(mSubtype);
         mDeviceFormFactor = params.mDeviceFormFactor;
         mOrientation = params.mOrientation;
-        mWidth = params.mWidth;
+        mWidth = params.mKeyboardWidth;
+        mHeight = params.mKeyboardHeight;
         mMode = params.mMode;
         mElementId = elementId;
         mEditorInfo = params.mEditorInfo;
@@ -108,6 +112,7 @@
                 id.mElementId,
                 id.mMode,
                 id.mWidth,
+                id.mHeight,
                 id.passwordInput(),
                 id.mClobberSettingsKey,
                 id.mShortcutKeyEnabled,
@@ -130,6 +135,7 @@
                 && other.mElementId == mElementId
                 && other.mMode == mMode
                 && other.mWidth == mWidth
+                && other.mHeight == mHeight
                 && other.passwordInput() == passwordInput()
                 && other.mClobberSettingsKey == mClobberSettingsKey
                 && other.mShortcutKeyEnabled == mShortcutKeyEnabled
@@ -187,11 +193,13 @@
 
     @Override
     public String toString() {
-        return String.format("[%s %s:%s %s-%s:%d %s %s %s%s%s%s%s%s%s%s%s]",
+        final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
+                ? "port" : "land";
+        return String.format("[%s %s:%s %s-%s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
                 elementIdToName(mElementId),
                 mLocale,
                 mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
-                deviceFormFactor(mDeviceFormFactor), (mOrientation == 1 ? "port" : "land"), mWidth,
+                deviceFormFactor(mDeviceFormFactor), orientation, mWidth, mHeight,
                 modeName(mMode),
                 imeAction(),
                 (navigateNext() ? "navigateNext" : ""),
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
index f060ad0..fd9edec 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardLayoutSet.java
@@ -28,6 +28,7 @@
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
 import android.text.InputType;
+import android.text.TextUtils;
 import android.util.Log;
 import android.util.SparseArray;
 import android.util.Xml;
@@ -44,6 +45,7 @@
 import com.android.inputmethod.latin.InputTypeUtils;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResourceUtils;
 import com.android.inputmethod.latin.SubtypeLocale;
 import com.android.inputmethod.latin.SubtypeSwitcher;
 import com.android.inputmethod.latin.XmlParseUtils;
@@ -106,7 +108,8 @@
         InputMethodSubtype mSubtype;
         int mDeviceFormFactor;
         int mOrientation;
-        int mWidth;
+        int mKeyboardWidth;
+        int mKeyboardHeight;
         // Sparse array of KeyboardLayoutSet element parameters indexed by element's id.
         final SparseArray<ElementParams> mKeyboardLayoutSetElementIdToParamsMap =
                 CollectionUtils.newSparseArray();
@@ -214,15 +217,43 @@
                     mPackageName, NO_SETTINGS_KEY, mEditorInfo);
         }
 
-        public Builder setScreenGeometry(final int deviceFormFactor, final int orientation,
-                final int widthPixels) {
+        public Builder setScreenGeometry(final int deviceFormFactor, final int widthPixels,
+                final int heightPixels) {
             final Params params = mParams;
             params.mDeviceFormFactor = deviceFormFactor;
-            params.mOrientation = orientation;
-            params.mWidth = widthPixels;
+            params.mOrientation = (heightPixels > widthPixels)
+                    ? Configuration.ORIENTATION_PORTRAIT : Configuration.ORIENTATION_LANDSCAPE;
+            setDefaultKeyboardSize(widthPixels, heightPixels);
             return this;
         }
 
+        private void setDefaultKeyboardSize(final int widthPixels, final int heightPixels) {
+            final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
+                    mResources, R.array.keyboard_heights);
+            final float keyboardHeight;
+            if (TextUtils.isEmpty(keyboardHeightString)) {
+                keyboardHeight = mResources.getDimension(R.dimen.keyboardHeight);
+            } else {
+                keyboardHeight = Float.parseFloat(keyboardHeightString)
+                        * mResources.getDisplayMetrics().density;
+            }
+            final float maxKeyboardHeight = mResources.getFraction(
+                    R.fraction.maxKeyboardHeight, heightPixels, heightPixels);
+            float minKeyboardHeight = mResources.getFraction(
+                    R.fraction.minKeyboardHeight, heightPixels, heightPixels);
+            if (minKeyboardHeight < 0.0f) {
+                // Specified fraction was negative, so it should be calculated against display
+                // width.
+                minKeyboardHeight = -mResources.getFraction(
+                        R.fraction.minKeyboardHeight, widthPixels, widthPixels);
+            }
+            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
+            // minKeyboardHeight.
+            mParams.mKeyboardHeight = (int)Math.max(
+                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
+            mParams.mKeyboardWidth = widthPixels;
+        }
+
         public Builder setSubtype(final InputMethodSubtype subtype) {
             final boolean asciiCapable = subtype.containsExtraValueKey(ASCII_CAPABLE);
             @SuppressWarnings("deprecation")
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index 30949ae..d15f14f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -20,6 +20,7 @@
 import android.content.SharedPreferences;
 import android.content.res.Resources;
 import android.preference.PreferenceManager;
+import android.util.DisplayMetrics;
 import android.util.Log;
 import android.view.ContextThemeWrapper;
 import android.view.LayoutInflater;
@@ -140,8 +141,9 @@
         final KeyboardLayoutSet.Builder builder = new KeyboardLayoutSet.Builder(
                 mThemeContext, editorInfo);
         final Resources res = mThemeContext.getResources();
+        final DisplayMetrics dm = res.getDisplayMetrics();
         builder.setScreenGeometry(res.getInteger(R.integer.config_device_form_factor),
-                res.getConfiguration().orientation, res.getDisplayMetrics().widthPixels);
+                dm.widthPixels, dm.heightPixels);
         builder.setSubtype(mSubtypeSwitcher.getCurrentSubtype());
         builder.setOptions(
                 settingsValues.isVoiceKeyEnabled(editorInfo),
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
index 04b8ecb..e087a45 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardBuilder.java
@@ -20,9 +20,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
-import android.text.TextUtils;
 import android.util.AttributeSet;
-import android.util.DisplayMetrics;
 import android.util.Log;
 import android.util.TypedValue;
 import android.util.Xml;
@@ -138,7 +136,6 @@
     protected final KP mParams;
     protected final Context mContext;
     protected final Resources mResources;
-    private final DisplayMetrics mDisplayMetrics;
 
     private int mCurrentY = 0;
     private KeyboardRow mCurrentRow = null;
@@ -150,7 +147,6 @@
         mContext = context;
         final Resources res = context.getResources();
         mResources = res;
-        mDisplayMetrics = res.getDisplayMetrics();
 
         mParams = params;
 
@@ -232,63 +228,43 @@
     }
 
     private void parseKeyboardAttributes(final XmlPullParser parser) {
-        final int displayWidth = mDisplayMetrics.widthPixels;
         final TypedArray keyboardAttr = mContext.obtainStyledAttributes(
                 Xml.asAttributeSet(parser), R.styleable.Keyboard, R.attr.keyboardStyle,
                 R.style.Keyboard);
         final TypedArray keyAttr = mResources.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard_Key);
         try {
-            final int displayHeight = mDisplayMetrics.heightPixels;
-            final String keyboardHeightString = ResourceUtils.getDeviceOverrideValue(
-                    mResources, R.array.keyboard_heights);
-            final float keyboardHeight;
-            if (TextUtils.isEmpty(keyboardHeightString)) {
-                keyboardHeight = keyboardAttr.getDimension(
-                        R.styleable.Keyboard_keyboardHeight, displayHeight / 2);
-            } else {
-                keyboardHeight = Float.parseFloat(keyboardHeightString)
-                        * mDisplayMetrics.density;
-            }
-            final float maxKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_maxKeyboardHeight, displayHeight, displayHeight / 2);
-            float minKeyboardHeight = ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_minKeyboardHeight, displayHeight, displayHeight / 2);
-            if (minKeyboardHeight < 0) {
-                // Specified fraction was negative, so it should be calculated against display
-                // width.
-                minKeyboardHeight = -ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                        R.styleable.Keyboard_minKeyboardHeight, displayWidth, displayWidth / 2);
-            }
             final KeyboardParams params = mParams;
-            // Keyboard height will not exceed maxKeyboardHeight and will not be less than
-            // minKeyboardHeight.
-            params.mOccupiedHeight = (int)Math.max(
-                    Math.min(keyboardHeight, maxKeyboardHeight), minKeyboardHeight);
-            params.mOccupiedWidth = params.mId.mWidth;
-            params.mTopPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_keyboardTopPadding, params.mOccupiedHeight, 0);
-            params.mBottomPadding = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_keyboardBottomPadding, params.mOccupiedHeight, 0);
-            params.mHorizontalEdgesPadding = (int)ResourceUtils.getDimensionOrFraction(
-                    keyboardAttr,
-                    R.styleable.Keyboard_keyboardHorizontalEdgesPadding,
-                    mParams.mOccupiedWidth, 0);
+            final int height = params.mId.mHeight;
+            final int width = params.mId.mWidth;
+            params.mOccupiedHeight = height;
+            params.mOccupiedWidth = width;
+            params.mTopPadding = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_keyboardTopPadding, height, height, 0);
+            params.mBottomPadding = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_keyboardBottomPadding, height, height, 0);
+            params.mLeftPadding = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_keyboardLeftPadding, width, width, 0);
+            params.mRightPadding = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_keyboardRightPadding, width, width, 0);
 
-            params.mBaseWidth = params.mOccupiedWidth - params.mHorizontalEdgesPadding * 2
-                    - params.mHorizontalCenterPadding;
-            params.mDefaultKeyWidth = (int)ResourceUtils.getDimensionOrFraction(keyAttr,
-                    R.styleable.Keyboard_Key_keyWidth, params.mBaseWidth,
-                    params.mBaseWidth / DEFAULT_KEYBOARD_COLUMNS);
-            params.mHorizontalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_horizontalGap, params.mBaseWidth, 0);
-            params.mVerticalGap = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_verticalGap, params.mOccupiedHeight, 0);
-            params.mBaseHeight = params.mOccupiedHeight - params.mTopPadding
+            final int baseWidth =
+                    params.mOccupiedWidth - params.mLeftPadding - params.mRightPadding;
+            params.mBaseWidth = baseWidth;
+            params.mDefaultKeyWidth = (int)keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+                    baseWidth, baseWidth, baseWidth / DEFAULT_KEYBOARD_COLUMNS);
+            params.mHorizontalGap = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_horizontalGap, baseWidth, baseWidth, 0);
+            // TODO: Fix keyboard geometry calculation clearer. Historically vertical gap between
+            // rows are determined based on the entire keyboard height including top and bottom
+            // paddings.
+            params.mVerticalGap = (int)keyboardAttr.getFraction(
+                    R.styleable.Keyboard_verticalGap, height, height, 0);
+            final int baseHeight = params.mOccupiedHeight - params.mTopPadding
                     - params.mBottomPadding + params.mVerticalGap;
+            params.mBaseHeight = baseHeight;
             params.mDefaultRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
-                    R.styleable.Keyboard_rowHeight, params.mBaseHeight,
-                    params.mBaseHeight / DEFAULT_KEYBOARD_ROWS);
+                    R.styleable.Keyboard_rowHeight, baseHeight, baseHeight / DEFAULT_KEYBOARD_ROWS);
 
             params.mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr);
 
@@ -766,7 +742,7 @@
     }
 
     private void startRow(final KeyboardRow row) {
-        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+        addEdgeSpace(mParams.mLeftPadding, row);
         mCurrentRow = row;
         mLeftEdge = true;
         mRightEdgeKey = null;
@@ -780,7 +756,7 @@
             mRightEdgeKey.markAsRightEdge(mParams);
             mRightEdgeKey = null;
         }
-        addEdgeSpace(mParams.mHorizontalEdgesPadding, row);
+        addEdgeSpace(mParams.mRightPadding, row);
         mCurrentY += row.mRowHeight;
         mCurrentRow = null;
         mTopEdge = false;
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
index e13dbe5..15eb690 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardParams.java
@@ -42,8 +42,8 @@
 
     public int mTopPadding;
     public int mBottomPadding;
-    public int mHorizontalEdgesPadding;
-    public int mHorizontalCenterPadding;
+    public int mLeftPadding;
+    public int mRightPadding;
 
     public KeyVisualAttributes mKeyVisualAttributes;
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
index 2278020..855f655 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardRow.java
@@ -54,17 +54,16 @@
     public KeyboardRow(final Resources res, final KeyboardParams params, final XmlPullParser parser,
             final int y) {
         mParams = params;
-        TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+        final TypedArray keyboardAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard);
         mRowHeight = (int)ResourceUtils.getDimensionOrFraction(keyboardAttr,
                 R.styleable.Keyboard_rowHeight,
                 params.mBaseHeight, params.mDefaultRowHeight);
         keyboardAttr.recycle();
-        TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
+        final TypedArray keyAttr = res.obtainAttributes(Xml.asAttributeSet(parser),
                 R.styleable.Keyboard_Key);
-        mDefaultKeyWidth = ResourceUtils.getDimensionOrFraction(keyAttr,
-                R.styleable.Keyboard_Key_keyWidth,
-                params.mBaseWidth, params.mDefaultKeyWidth);
+        mDefaultKeyWidth = keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+                params.mBaseWidth, params.mBaseWidth, params.mDefaultKeyWidth);
         mDefaultBackgroundType = keyAttr.getInt(R.styleable.Keyboard_Key_backgroundType,
                 Key.BACKGROUND_TYPE_NORMAL);
         keyAttr.recycle();
@@ -112,20 +111,19 @@
     }
 
     public float getKeyX(final TypedArray keyAttr) {
-        final int keyboardRightEdge = mParams.mOccupiedWidth
-                - mParams.mHorizontalEdgesPadding;
         if (keyAttr.hasValue(R.styleable.Keyboard_Key_keyXPos)) {
-            final float keyXPos = ResourceUtils.getDimensionOrFraction(keyAttr,
-                    R.styleable.Keyboard_Key_keyXPos, mParams.mBaseWidth, 0);
+            final float keyXPos = keyAttr.getFraction(R.styleable.Keyboard_Key_keyXPos,
+                    mParams.mBaseWidth, mParams.mBaseWidth, 0);
             if (keyXPos < 0) {
                 // If keyXPos is negative, the actual x-coordinate will be
                 // keyboardWidth + keyXPos.
                 // keyXPos shouldn't be less than mCurrentX because drawable area for this
                 // key starts at mCurrentX. Or, this key will overlaps the adjacent key on
                 // its left hand side.
+                final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
                 return Math.max(keyXPos + keyboardRightEdge, mCurrentX);
             } else {
-                return keyXPos + mParams.mHorizontalEdgesPadding;
+                return keyXPos + mParams.mLeftPadding;
             }
         }
         return mCurrentX;
@@ -140,15 +138,13 @@
                 R.styleable.Keyboard_Key_keyWidth, KEYWIDTH_NOT_ENUM);
         switch (widthType) {
         case KEYWIDTH_FILL_RIGHT:
-            final int keyboardRightEdge =
-                    mParams.mOccupiedWidth - mParams.mHorizontalEdgesPadding;
             // If keyWidth is fillRight, the actual key width will be determined to fill
             // out the area up to the right edge of the keyboard.
+            final int keyboardRightEdge = mParams.mOccupiedWidth - mParams.mRightPadding;
             return keyboardRightEdge - keyXPos;
         default: // KEYWIDTH_NOT_ENUM
-            return ResourceUtils.getDimensionOrFraction(keyAttr,
-                    R.styleable.Keyboard_Key_keyWidth,
-                    mParams.mBaseWidth, mDefaultKeyWidth);
+            return keyAttr.getFraction(R.styleable.Keyboard_Key_keyWidth,
+                    mParams.mBaseWidth, mParams.mBaseWidth, mDefaultKeyWidth);
         }
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/AssetFileAddress.java b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
index 16296f0..47c750f 100644
--- a/java/src/com/android/inputmethod/latin/AssetFileAddress.java
+++ b/java/src/com/android/inputmethod/latin/AssetFileAddress.java
@@ -35,11 +35,14 @@
         mLength = length;
     }
 
+    public static AssetFileAddress makeFromFile(final File file) {
+        if (!file.isFile()) return null;
+        return new AssetFileAddress(file.getAbsolutePath(), 0L, file.length());
+    }
+
     public static AssetFileAddress makeFromFileName(final String filename) {
         if (null == filename) return null;
-        final File f = new File(filename);
-        if (!f.isFile()) return null;
-        return new AssetFileAddress(filename, 0l, f.length());
+        return makeFromFile(new File(filename));
     }
 
     public static AssetFileAddress makeFromFileNameAndOffset(final String filename,
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
index d0bd01f..443ffa2 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryFileDumper.java
@@ -149,7 +149,7 @@
         final int MODE_MAX = NONE;
 
         final Uri.Builder wordListUriBuilder = getProviderUriBuilder(id);
-        final String finalFileName = BinaryDictionaryGetter.getCacheFileName(id, locale, context);
+        final String finalFileName = DictionaryInfoUtils.getCacheFileName(id, locale, context);
         String tempFileName;
         try {
             tempFileName = BinaryDictionaryGetter.getTempFileName(id, context);
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
index 22b5cd5..5da0f1b 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionaryGetter.java
@@ -66,109 +66,11 @@
     private BinaryDictionaryGetter() {}
 
     /**
-     * Returns whether we may want to use this character as part of a file name.
-     *
-     * This basically only accepts ascii letters and numbers, and rejects everything else.
-     */
-    private static boolean isFileNameCharacter(int codePoint) {
-        if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
-        if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
-        if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
-        return codePoint == '_'; // Underscore
-    }
-
-    /**
-     * Escapes a string for any characters that may be suspicious for a file or directory name.
-     *
-     * Concretely this does a sort of URL-encoding except it will encode everything that's not
-     * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
-     * we cannot allow here)
-     */
-    // TODO: create a unit test for this method
-    private static String replaceFileNameDangerousCharacters(final String name) {
-        // This assumes '%' is fully available as a non-separator, normal
-        // character in a file name. This is probably true for all file systems.
-        final StringBuilder sb = new StringBuilder();
-        final int nameLength = name.length();
-        for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
-            final int codePoint = name.codePointAt(i);
-            if (isFileNameCharacter(codePoint)) {
-                sb.appendCodePoint(codePoint);
-            } else {
-                // 6 digits - unicode is limited to 21 bits
-                sb.append(String.format((Locale)null, "%%%1$06x", codePoint));
-            }
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Reverse escaping done by replaceFileNameDangerousCharacters.
-     */
-    private static String getWordListIdFromFileName(final String fname) {
-        final StringBuilder sb = new StringBuilder();
-        final int fnameLength = fname.length();
-        for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
-            final int codePoint = fname.codePointAt(i);
-            if ('%' != codePoint) {
-                sb.appendCodePoint(codePoint);
-            } else {
-                final int encodedCodePoint = Integer.parseInt(fname.substring(i + 1, i + 7), 16);
-                i += 6;
-                sb.appendCodePoint(encodedCodePoint);
-            }
-        }
-        return sb.toString();
-    }
-
-    /**
-     * Helper method to get the top level cache directory.
-     */
-    private static String getWordListCacheDirectory(final Context context) {
-        return context.getFilesDir() + File.separator + "dicts";
-    }
-
-    /**
-     * Find out the cache directory associated with a specific locale.
-     */
-    private static String getCacheDirectoryForLocale(final String locale, final Context context) {
-        final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
-        final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
-                + relativeDirectoryName;
-        final File directory = new File(absoluteDirectoryName);
-        if (!directory.exists()) {
-            if (!directory.mkdirs()) {
-                Log.e(TAG, "Could not create the directory for locale" + locale);
-            }
-        }
-        return absoluteDirectoryName;
-    }
-
-    /**
-     * Generates a file name for the id and locale passed as an argument.
-     *
-     * In the current implementation the file name returned will always be unique for
-     * any id/locale pair, but please do not expect that the id can be the same for
-     * different dictionaries with different locales. An id should be unique for any
-     * dictionary.
-     * The file name is pretty much an URL-encoded version of the id inside a directory
-     * named like the locale, except it will also escape characters that look dangerous
-     * to some file systems.
-     * @param id the id of the dictionary for which to get a file name
-     * @param locale the locale for which to get the file name as a string
-     * @param context the context to use for getting the directory
-     * @return the name of the file to be created
-     */
-    public static String getCacheFileName(String id, String locale, Context context) {
-        final String fileName = replaceFileNameDangerousCharacters(id);
-        return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
-    }
-
-    /**
      * Generates a unique temporary file name in the app cache directory.
      */
     public static String getTempFileName(String id, Context context) throws IOException {
-        return File.createTempFile(replaceFileNameDangerousCharacters(id), null).getAbsolutePath();
+        return File.createTempFile(DictionaryInfoUtils.replaceFileNameDangerousCharacters(id),
+                null).getAbsolutePath();
     }
 
     /**
@@ -222,27 +124,6 @@
     }
 
     /**
-     * Helper method to the list of cache directories, one for each distinct locale.
-     */
-    private static File[] getCachedDirectoryList(final Context context) {
-        return new File(getWordListCacheDirectory(context)).listFiles();
-    }
-
-    /**
-     * Returns the category for a given file name.
-     *
-     * This parses the file name, extracts the category, and returns it. See
-     * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
-     * @return The category as a string or null if it can't be found in the file name.
-     */
-    private static String getCategoryFromFileName(final String fileName) {
-        final String id = getWordListIdFromFileName(fileName);
-        final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
-        if (2 != idArray.length) return null;
-        return idArray[0];
-    }
-
-    /**
      * Utility class for the {@link #getCachedWordLists} method
      */
     private static final class FileAndMatchLevel {
@@ -268,20 +149,21 @@
      * @param context the context on which to open the files upon.
      * @return an array of binary dictionary files, which may be empty but may not be null.
      */
-    private static File[] getCachedWordLists(final String locale,
-            final Context context) {
-        final File[] directoryList = getCachedDirectoryList(context);
+    private static File[] getCachedWordLists(final String locale, final Context context) {
+        final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
         if (null == directoryList) return EMPTY_FILE_ARRAY;
         final HashMap<String, FileAndMatchLevel> cacheFiles = CollectionUtils.newHashMap();
         for (File directory : directoryList) {
             if (!directory.isDirectory()) continue;
-            final String dirLocale = getWordListIdFromFileName(directory.getName());
+            final String dirLocale =
+                    DictionaryInfoUtils.getWordListIdFromFileName(directory.getName());
             final int matchLevel = LocaleUtils.getMatchLevel(dirLocale, locale);
             if (LocaleUtils.isMatch(matchLevel)) {
                 final File[] wordLists = directory.listFiles();
                 if (null != wordLists) {
                     for (File wordList : wordLists) {
-                        final String category = getCategoryFromFileName(wordList.getName());
+                        final String category =
+                                DictionaryInfoUtils.getCategoryFromFileName(wordList.getName());
                         final FileAndMatchLevel currentBestMatch = cacheFiles.get(category);
                         if (null == currentBestMatch || currentBestMatch.mMatchLevel < matchLevel) {
                             cacheFiles.put(category, new FileAndMatchLevel(wordList, matchLevel));
@@ -310,7 +192,7 @@
             final File fileToKeep) {
         try {
             final File canonicalFileToKeep = fileToKeep.getCanonicalFile();
-            final File[] directoryList = getCachedDirectoryList(context);
+            final File[] directoryList = DictionaryInfoUtils.getCachedDirectoryList(context);
             if (null == directoryList) return;
             for (File directory : directoryList) {
                 // There is one directory per locale. See #getCachedDirectoryList
@@ -318,7 +200,8 @@
                 final File[] wordLists = directory.listFiles();
                 if (null == wordLists) continue;
                 for (File wordList : wordLists) {
-                    final String fileId = getWordListIdFromFileName(wordList.getName());
+                    final String fileId =
+                            DictionaryInfoUtils.getWordListIdFromFileName(wordList.getName());
                     if (fileId.equals(id)) {
                         if (!canonicalFileToKeep.equals(wordList.getCanonicalFile())) {
                             wordList.delete();
@@ -331,28 +214,6 @@
         }
     }
 
-
-    /**
-     * Returns the id associated with the main word list for a specified locale.
-     *
-     * Word lists stored in Android Keyboard's resources are referred to as the "main"
-     * word lists. Since they can be updated like any other list, we need to assign a
-     * unique ID to them. This ID is just the name of the language (locale-wise) they
-     * are for, and this method returns this ID.
-     */
-    private static String getMainDictId(final Locale locale) {
-        // This works because we don't include by default different dictionaries for
-        // different countries. This actually needs to return the id that we would
-        // like to use for word lists included in resources, and the following is okay.
-        return MAIN_DICTIONARY_CATEGORY + ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
-    }
-
-    private static boolean isMainWordListId(final String id) {
-        final String[] idArray = id.split(ID_CATEGORY_SEPARATOR);
-        if (2 != idArray.length) return false;
-        return MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
-    }
-
     // ## HACK ## we prevent usage of a dictionary before version 18 for English only. The reason
     // for this is, since those do not include whitelist entries, the new code with an old version
     // of the dictionary would lose whitelist functionality.
@@ -429,16 +290,16 @@
                     hasDefaultWordList);
         }
         final File[] cachedWordLists = getCachedWordLists(locale.toString(), context);
-        final String mainDictId = getMainDictId(locale);
+        final String mainDictId = DictionaryInfoUtils.getMainDictId(locale);
         final DictPackSettings dictPackSettings = new DictPackSettings(context);
 
         boolean foundMainDict = false;
         final ArrayList<AssetFileAddress> fileList = CollectionUtils.newArrayList();
         // cachedWordLists may not be null, see doc for getCachedDictionaryList
         for (final File f : cachedWordLists) {
-            final String wordListId = getWordListIdFromFileName(f.getName());
+            final String wordListId = DictionaryInfoUtils.getWordListIdFromFileName(f.getName());
             final boolean canUse = f.canRead() && hackCanUseDictionaryFile(locale, f);
-            if (canUse && isMainWordListId(wordListId)) {
+            if (canUse && DictionaryInfoUtils.isMainWordListId(wordListId)) {
                 foundMainDict = true;
             }
             if (!dictPackSettings.isWordListActive(wordListId)) continue;
@@ -451,7 +312,7 @@
 
         if (!foundMainDict && dictPackSettings.isWordListActive(mainDictId)) {
             final int fallbackResId =
-                    DictionaryFactory.getMainDictionaryResourceId(context.getResources(), locale);
+                    DictionaryInfoUtils.getMainDictionaryResourceId(context.getResources(), locale);
             final AssetFileAddress fallbackAsset = loadFallbackResource(context, fallbackResId);
             if (null != fallbackAsset) {
                 fileList.add(fallbackAsset);
diff --git a/java/src/com/android/inputmethod/latin/DictionaryFactory.java b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
index 388ad6c..40e5167 100644
--- a/java/src/com/android/inputmethod/latin/DictionaryFactory.java
+++ b/java/src/com/android/inputmethod/latin/DictionaryFactory.java
@@ -31,9 +31,6 @@
  */
 public final class DictionaryFactory {
     private static final String TAG = DictionaryFactory.class.getSimpleName();
-    // This class must be located in the same package as LatinIME.java.
-    private static final String RESOURCE_PACKAGE_NAME =
-            DictionaryFactory.class.getPackage().getName();
 
     /**
      * Initializes a main dictionary collection from a dictionary pack, with explicit flags.
@@ -96,8 +93,8 @@
             final Locale locale) {
         AssetFileDescriptor afd = null;
         try {
-            final int resId =
-                    getMainDictionaryResourceIdIfAvailableForLocale(context.getResources(), locale);
+            final int resId = DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
+                    context.getResources(), locale);
             if (0 == resId) return null;
             afd = context.getResources().openRawResourceFd(resId);
             if (afd == null) {
@@ -154,47 +151,7 @@
      */
     public static boolean isDictionaryAvailable(Context context, Locale locale) {
         final Resources res = context.getResources();
-        return 0 != getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
-    }
-
-    private static final String DEFAULT_MAIN_DICT = "main";
-    private static final String MAIN_DICT_PREFIX = "main_";
-
-    /**
-     * Helper method to return a dictionary res id for a locale, or 0 if none.
-     * @param locale dictionary locale
-     * @return main dictionary resource id
-     */
-    private static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
-            final Locale locale) {
-        int resId;
-        // Try to find main_language_country dictionary.
-        if (!locale.getCountry().isEmpty()) {
-            final String dictLanguageCountry = MAIN_DICT_PREFIX + locale.toString().toLowerCase();
-            if ((resId = res.getIdentifier(
-                    dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
-                return resId;
-            }
-        }
-
-        // Try to find main_language dictionary.
-        final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
-        if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
-            return resId;
-        }
-
-        // Not found, return 0
-        return 0;
-    }
-
-    /**
-     * Returns a main dictionary resource id
-     * @param locale dictionary locale
-     * @return main dictionary resource id
-     */
-    public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
-        int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
-        if (0 != resourceId) return resourceId;
-        return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
+        return 0 != DictionaryInfoUtils.getMainDictionaryResourceIdIfAvailableForLocale(
+                res, locale);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
new file mode 100644
index 0000000..c676bf1
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/DictionaryInfoUtils.java
@@ -0,0 +1,245 @@
+/*
+ * 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.content.Context;
+import android.content.res.Resources;
+import android.util.Log;
+
+import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
+import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
+import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Locale;
+
+/**
+ * This class encapsulates the logic for the Latin-IME side of dictionary information management.
+ */
+public class DictionaryInfoUtils {
+    private static final String TAG = DictionaryInfoUtils.class.getSimpleName();
+    // This class must be located in the same package as LatinIME.java.
+    private static final String RESOURCE_PACKAGE_NAME =
+            DictionaryInfoUtils.class.getPackage().getName();
+    private static final String DEFAULT_MAIN_DICT = "main";
+    private static final String MAIN_DICT_PREFIX = "main_";
+    // 6 digits - unicode is limited to 21 bits
+    private static final int MAX_HEX_DIGITS_FOR_CODEPOINT = 6;
+
+    private DictionaryInfoUtils() {
+        // Private constructor to forbid instantation of this helper class.
+    }
+
+    /**
+     * Returns whether we may want to use this character as part of a file name.
+     *
+     * This basically only accepts ascii letters and numbers, and rejects everything else.
+     */
+    private static boolean isFileNameCharacter(int codePoint) {
+        if (codePoint >= 0x30 && codePoint <= 0x39) return true; // Digit
+        if (codePoint >= 0x41 && codePoint <= 0x5A) return true; // Uppercase
+        if (codePoint >= 0x61 && codePoint <= 0x7A) return true; // Lowercase
+        return codePoint == '_'; // Underscore
+    }
+
+    /**
+     * Escapes a string for any characters that may be suspicious for a file or directory name.
+     *
+     * Concretely this does a sort of URL-encoding except it will encode everything that's not
+     * alphanumeric or underscore. (true URL-encoding leaves alone characters like '*', which
+     * we cannot allow here)
+     */
+    // TODO: create a unit test for this method
+    public static String replaceFileNameDangerousCharacters(final String name) {
+        // This assumes '%' is fully available as a non-separator, normal
+        // character in a file name. This is probably true for all file systems.
+        final StringBuilder sb = new StringBuilder();
+        final int nameLength = name.length();
+        for (int i = 0; i < nameLength; i = name.offsetByCodePoints(i, 1)) {
+            final int codePoint = name.codePointAt(i);
+            if (DictionaryInfoUtils.isFileNameCharacter(codePoint)) {
+                sb.appendCodePoint(codePoint);
+            } else {
+                sb.append(String.format((Locale)null, "%%%1$0" + MAX_HEX_DIGITS_FOR_CODEPOINT + "x",
+                        codePoint));
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Helper method to get the top level cache directory.
+     */
+    private static String getWordListCacheDirectory(final Context context) {
+        return context.getFilesDir() + File.separator + "dicts";
+    }
+
+    /**
+     * Reverse escaping done by replaceFileNameDangerousCharacters.
+     */
+    public static String getWordListIdFromFileName(final String fname) {
+        final StringBuilder sb = new StringBuilder();
+        final int fnameLength = fname.length();
+        for (int i = 0; i < fnameLength; i = fname.offsetByCodePoints(i, 1)) {
+            final int codePoint = fname.codePointAt(i);
+            if ('%' != codePoint) {
+                sb.appendCodePoint(codePoint);
+            } else {
+                // + 1 to pass the % sign
+                final int encodedCodePoint = Integer.parseInt(
+                        fname.substring(i + 1, i + 1 + MAX_HEX_DIGITS_FOR_CODEPOINT), 16);
+                i += MAX_HEX_DIGITS_FOR_CODEPOINT;
+                sb.appendCodePoint(encodedCodePoint);
+            }
+        }
+        return sb.toString();
+    }
+
+    /**
+     * Helper method to the list of cache directories, one for each distinct locale.
+     */
+    public static File[] getCachedDirectoryList(final Context context) {
+        return new File(DictionaryInfoUtils.getWordListCacheDirectory(context)).listFiles();
+    }
+
+    /**
+     * Returns the category for a given file name.
+     *
+     * This parses the file name, extracts the category, and returns it. See
+     * {@link #getMainDictId(Locale)} and {@link #isMainWordListId(String)}.
+     * @return The category as a string or null if it can't be found in the file name.
+     */
+    public static String getCategoryFromFileName(final String fileName) {
+        final String id = getWordListIdFromFileName(fileName);
+        final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+        // An id is supposed to be in format category:locale, so splitting on the separator
+        // should yield a 2-elements array
+        if (2 != idArray.length) return null;
+        return idArray[0];
+    }
+
+    /**
+     * Find out the cache directory associated with a specific locale.
+     */
+    private static String getCacheDirectoryForLocale(final String locale, final Context context) {
+        final String relativeDirectoryName = replaceFileNameDangerousCharacters(locale);
+        final String absoluteDirectoryName = getWordListCacheDirectory(context) + File.separator
+                + relativeDirectoryName;
+        final File directory = new File(absoluteDirectoryName);
+        if (!directory.exists()) {
+            if (!directory.mkdirs()) {
+                Log.e(TAG, "Could not create the directory for locale" + locale);
+            }
+        }
+        return absoluteDirectoryName;
+    }
+
+    /**
+     * Generates a file name for the id and locale passed as an argument.
+     *
+     * In the current implementation the file name returned will always be unique for
+     * any id/locale pair, but please do not expect that the id can be the same for
+     * different dictionaries with different locales. An id should be unique for any
+     * dictionary.
+     * The file name is pretty much an URL-encoded version of the id inside a directory
+     * named like the locale, except it will also escape characters that look dangerous
+     * to some file systems.
+     * @param id the id of the dictionary for which to get a file name
+     * @param locale the locale for which to get the file name as a string
+     * @param context the context to use for getting the directory
+     * @return the name of the file to be created
+     */
+    public static String getCacheFileName(String id, String locale, Context context) {
+        final String fileName = replaceFileNameDangerousCharacters(id);
+        return getCacheDirectoryForLocale(locale, context) + File.separator + fileName;
+    }
+
+    public static boolean isMainWordListId(final String id) {
+        final String[] idArray = id.split(BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR);
+        // An id is supposed to be in format category:locale, so splitting on the separator
+        // should yield a 2-elements array
+        if (2 != idArray.length) return false;
+        return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY.equals(idArray[0]);
+    }
+
+    /**
+     * Helper method to return a dictionary res id for a locale, or 0 if none.
+     * @param locale dictionary locale
+     * @return main dictionary resource id
+     */
+    public static int getMainDictionaryResourceIdIfAvailableForLocale(final Resources res,
+            final Locale locale) {
+        int resId;
+        // Try to find main_language_country dictionary.
+        if (!locale.getCountry().isEmpty()) {
+            final String dictLanguageCountry =
+                    MAIN_DICT_PREFIX + locale.toString().toLowerCase(Locale.ROOT);
+            if ((resId = res.getIdentifier(
+                    dictLanguageCountry, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+                return resId;
+            }
+        }
+
+        // Try to find main_language dictionary.
+        final String dictLanguage = MAIN_DICT_PREFIX + locale.getLanguage();
+        if ((resId = res.getIdentifier(dictLanguage, "raw", RESOURCE_PACKAGE_NAME)) != 0) {
+            return resId;
+        }
+
+        // Not found, return 0
+        return 0;
+    }
+
+    /**
+     * Returns a main dictionary resource id
+     * @param locale dictionary locale
+     * @return main dictionary resource id
+     */
+    public static int getMainDictionaryResourceId(final Resources res, final Locale locale) {
+        int resourceId = getMainDictionaryResourceIdIfAvailableForLocale(res, locale);
+        if (0 != resourceId) return resourceId;
+        return res.getIdentifier(DEFAULT_MAIN_DICT, "raw", RESOURCE_PACKAGE_NAME);
+    }
+
+    /**
+     * Returns the id associated with the main word list for a specified locale.
+     *
+     * Word lists stored in Android Keyboard's resources are referred to as the "main"
+     * word lists. Since they can be updated like any other list, we need to assign a
+     * unique ID to them. This ID is just the name of the language (locale-wise) they
+     * are for, and this method returns this ID.
+     */
+    public static String getMainDictId(final Locale locale) {
+        // This works because we don't include by default different dictionaries for
+        // different countries. This actually needs to return the id that we would
+        // like to use for word lists included in resources, and the following is okay.
+        return BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY +
+                BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale.getLanguage().toString();
+    }
+
+    public static FileHeader getDictionaryFileHeaderOrNull(final File file) {
+        try {
+            final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeader(file);
+            return header;
+        } catch (UnsupportedFormatException e) {
+            return null;
+        } catch (IOException e) {
+            return null;
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java b/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
index 6e5a37c..d9e4bb6 100644
--- a/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
+++ b/java/src/com/android/inputmethod/latin/ExternalDictionaryGetterForDebug.java
@@ -21,11 +21,8 @@
 import android.content.DialogInterface;
 import android.content.DialogInterface.OnClickListener;
 import android.os.Environment;
-import android.util.Log;
 
-import com.android.inputmethod.latin.makedict.BinaryDictIOUtils;
 import com.android.inputmethod.latin.makedict.FormatSpec.FileHeader;
-import com.android.inputmethod.latin.makedict.UnsupportedFormatException;
 
 import java.io.BufferedInputStream;
 import java.io.BufferedOutputStream;
@@ -44,22 +41,11 @@
             + "/Download";
     private static final String DICTIONARY_LOCALE_ATTRIBUTE = "locale";
 
-    private static FileHeader getDictionaryFileHeaderOrNull(final File file) {
-        try {
-            final FileHeader header = BinaryDictIOUtils.getDictionaryFileHeader(file);
-            return header;
-        } catch (UnsupportedFormatException e) {
-            return null;
-        } catch (IOException e) {
-            return null;
-        }
-    }
-
     private static String[] findDictionariesInTheDownloadedFolder() {
         final File[] files = new File(SOURCE_FOLDER).listFiles();
         final ArrayList<String> eligibleList = CollectionUtils.newArrayList();
         for (File f : files) {
-            final FileHeader header = getDictionaryFileHeaderOrNull(f);
+            final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(f);
             if (null == header) continue;
             eligibleList.add(f.getName());
         }
@@ -102,7 +88,7 @@
 
     private static void askInstallFile(final Context context, final String fileName) {
         final File file = new File(SOURCE_FOLDER, fileName.toString());
-        final FileHeader header = getDictionaryFileHeaderOrNull(file);
+        final FileHeader header = DictionaryInfoUtils.getDictionaryFileHeaderOrNull(file);
         final StringBuilder message = new StringBuilder();
         final String locale =
                 header.mDictionaryOptions.mAttributes.get(DICTIONARY_LOCALE_ATTRIBUTE);
@@ -143,7 +129,7 @@
             final String id = BinaryDictionaryGetter.MAIN_DICTIONARY_CATEGORY
                     + BinaryDictionaryGetter.ID_CATEGORY_SEPARATOR + locale;
             final String finalFileName =
-                    BinaryDictionaryGetter.getCacheFileName(id, locale, context);
+                    DictionaryInfoUtils.getCacheFileName(id, locale, context);
             final String tempFileName = BinaryDictionaryGetter.getTempFileName(id, context);
             tempFile = new File(tempFileName);
             tempFile.delete();
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index d6487cb..0821732 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -428,7 +428,7 @@
         initSuggest();
 
         if (ProductionFlag.IS_EXPERIMENTAL) {
-            ResearchLogger.getInstance().init(this);
+            ResearchLogger.getInstance().init(this, mKeyboardSwitcher);
         }
         mDisplayOrientation = getResources().getConfiguration().orientation;
 
diff --git a/java/src/com/android/inputmethod/research/FeedbackActivity.java b/java/src/com/android/inputmethod/research/FeedbackActivity.java
index f66d55b..b985fda 100644
--- a/java/src/com/android/inputmethod/research/FeedbackActivity.java
+++ b/java/src/com/android/inputmethod/research/FeedbackActivity.java
@@ -28,25 +28,10 @@
         super.onCreate(savedInstanceState);
         setContentView(R.layout.research_feedback_activity);
         final FeedbackLayout layout = (FeedbackLayout) findViewById(R.id.research_feedback_layout);
-        final CheckBox checkbox = (CheckBox) findViewById(R.id.research_feedback_include_history);
-        final CharSequence cs = checkbox.getText();
-        final String actualString = String.format(cs.toString(),
-                ResearchLogger.FEEDBACK_WORD_BUFFER_SIZE);
-        checkbox.setText(actualString);
         layout.setActivity(this);
     }
 
     @Override
-    protected void onResume() {
-        super.onResume();
-    }
-
-    @Override
-    protected void onPause() {
-        super.onPause();
-    }
-
-    @Override
     public void onBackPressed() {
         ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
         super.onBackPressed();
diff --git a/java/src/com/android/inputmethod/research/FeedbackFragment.java b/java/src/com/android/inputmethod/research/FeedbackFragment.java
index fee61a9..11a833a 100644
--- a/java/src/com/android/inputmethod/research/FeedbackFragment.java
+++ b/java/src/com/android/inputmethod/research/FeedbackFragment.java
@@ -20,6 +20,7 @@
 import android.app.Fragment;
 import android.os.Bundle;
 import android.text.Editable;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.View.OnClickListener;
@@ -30,10 +31,18 @@
 
 import com.android.inputmethod.latin.R;
 
-public class FeedbackFragment extends Fragment {
+public class FeedbackFragment extends Fragment implements OnClickListener {
+    private static final String TAG = FeedbackFragment.class.getSimpleName();
+
+    private static final String KEY_FEEDBACK_STRING = "FeedbackString";
+    private static final String KEY_INCLUDE_ACCOUNT_NAME = "IncludeAccountName";
+    public static final String KEY_HAS_USER_RECORDING = "HasRecording";
+
     private EditText mEditText;
-    private CheckBox mIncludingHistoryCheckBox;
     private CheckBox mIncludingAccountNameCheckBox;
+    private CheckBox mIncludingUserRecordingCheckBox;
+    private Button mSendButton;
+    private Button mCancelButton;
 
     @Override
     public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -41,39 +50,96 @@
         final View view = inflater.inflate(R.layout.research_feedback_fragment_layout, container,
                 false);
         mEditText = (EditText) view.findViewById(R.id.research_feedback_contents);
-        mIncludingHistoryCheckBox = (CheckBox) view.findViewById(
-                R.id.research_feedback_include_history);
+        mEditText.requestFocus();
         mIncludingAccountNameCheckBox = (CheckBox) view.findViewById(
                 R.id.research_feedback_include_account_name);
+        mIncludingUserRecordingCheckBox = (CheckBox) view.findViewById(
+                R.id.research_feedback_include_recording_checkbox);
+        mIncludingUserRecordingCheckBox.setOnClickListener(this);
 
-        final Button sendButton = (Button) view.findViewById(
-                R.id.research_feedback_send_button);
-        sendButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final Editable editable = mEditText.getText();
-                final String feedbackContents = editable.toString();
-                final boolean isIncludingHistory = mIncludingHistoryCheckBox.isChecked();
-                final boolean isIncludingAccountName = mIncludingAccountNameCheckBox.isChecked();
-                ResearchLogger.getInstance().sendFeedback(feedbackContents, isIncludingHistory,
-                        isIncludingAccountName);
-                final Activity activity = FeedbackFragment.this.getActivity();
-                activity.finish();
-                ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
+        mSendButton = (Button) view.findViewById(R.id.research_feedback_send_button);
+        mSendButton.setOnClickListener(this);
+        mCancelButton = (Button) view.findViewById(R.id.research_feedback_cancel_button);
+        mCancelButton.setOnClickListener(this);
+
+        if (savedInstanceState != null) {
+            Log.d(TAG, "restoring from savedInstanceState");
+            restoreState(savedInstanceState);
+        } else {
+            final Bundle bundle = getActivity().getIntent().getExtras();
+            if (bundle != null) {
+                Log.d(TAG, "restoring from getArguments()");
+                restoreState(bundle);
             }
-        });
-
-        final Button cancelButton = (Button) view.findViewById(
-                R.id.research_feedback_cancel_button);
-        cancelButton.setOnClickListener(new OnClickListener() {
-            @Override
-            public void onClick(View v) {
-                final Activity activity = FeedbackFragment.this.getActivity();
-                activity.finish();
-                ResearchLogger.getInstance().onLeavingSendFeedbackDialog();
-            }
-        });
-
+        }
         return view;
     }
+
+    @Override
+    public void onClick(final View view) {
+        final ResearchLogger researchLogger = ResearchLogger.getInstance();
+        if (view == mIncludingUserRecordingCheckBox) {
+            if (hasUserRecording()) {
+                // Remove the recording
+                setHasUserRecording(false);
+            } else {
+                final Bundle bundle = new Bundle();
+                onSaveInstanceState(bundle);
+
+                // Let the user make a recording
+                getActivity().finish();
+
+                researchLogger.setFeedbackDialogBundle(bundle);
+                researchLogger.onLeavingSendFeedbackDialog();
+                researchLogger.startRecording();
+            }
+        } else if (view == mSendButton) {
+            final Editable editable = mEditText.getText();
+            final String feedbackContents = editable.toString();
+            final boolean isIncludingAccountName = isIncludingAccountName();
+            researchLogger.sendFeedback(feedbackContents,
+                    false /* isIncludingHistory */, isIncludingAccountName, hasUserRecording());
+            getActivity().finish();
+            researchLogger.setFeedbackDialogBundle(null);
+            researchLogger.onLeavingSendFeedbackDialog();
+        } else if (view == mCancelButton) {
+            Log.d(TAG, "Finishing");
+            getActivity().finish();
+            researchLogger.setFeedbackDialogBundle(null);
+            researchLogger.onLeavingSendFeedbackDialog();
+        } else {
+            Log.e(TAG, "Unknown view passed to FeedbackFragment.onClick()");
+        }
+    }
+
+    @Override
+    public void onSaveInstanceState(final Bundle bundle) {
+        final String savedFeedbackString = mEditText.getText().toString();
+
+        bundle.putString(KEY_FEEDBACK_STRING, savedFeedbackString);
+        bundle.putBoolean(KEY_INCLUDE_ACCOUNT_NAME, isIncludingAccountName());
+        bundle.putBoolean(KEY_HAS_USER_RECORDING, hasUserRecording());
+    }
+
+    public void restoreState(final Bundle bundle) {
+        mEditText.setText(bundle.getString(KEY_FEEDBACK_STRING));
+        setIsIncludingAccountName(bundle.getBoolean(KEY_INCLUDE_ACCOUNT_NAME));
+        setHasUserRecording(bundle.getBoolean(KEY_HAS_USER_RECORDING));
+    }
+
+    private boolean hasUserRecording() {
+        return mIncludingUserRecordingCheckBox.isChecked();
+    }
+
+    private void setHasUserRecording(final boolean hasRecording) {
+        mIncludingUserRecordingCheckBox.setChecked(hasRecording);
+    }
+
+    private boolean isIncludingAccountName() {
+        return mIncludingAccountNameCheckBox.isChecked();
+    }
+
+    private void setIsIncludingAccountName(final boolean isIncludingAccountName) {
+        mIncludingAccountNameCheckBox.setChecked(isIncludingAccountName);
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/LogStatement.java b/java/src/com/android/inputmethod/research/LogStatement.java
new file mode 100644
index 0000000..090c58e
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/LogStatement.java
@@ -0,0 +1,146 @@
+/*
+ * 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.research;
+
+/**
+ * A template for typed information stored in the logs.
+ *
+ * A LogStatement contains a name, keys, and flags about whether the {@code Object[] values}
+ * associated with the {@code String[] keys} are likely to reveal information about the user.  The
+ * actual values are stored separately.
+ */
+class LogStatement {
+    // Constants for particular statements
+    public static final String TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT =
+            "PointerTrackerCallListenerOnCodeInput";
+    public static final String KEY_CODE = "code";
+    public static final String VALUE_RESEARCH = "research";
+    public static final String TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS =
+            "LatinKeyboardViewOnLongPress";
+    public static final String ACTION = "action";
+    public static final String VALUE_DOWN = "DOWN";
+    public static final String TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS =
+            "LatinKeyboardViewProcessMotionEvents";
+    public static final String KEY_LOGGING_RELATED = "loggingRelated";
+
+    // Name specifying the LogStatement type.
+    private final String mType;
+
+    // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
+    // the word that this event is a part of is determined to be privacy-sensitive, then this
+    // event should not be included in the output log.  The system waits to output until the
+    // containing word is known.
+    private final boolean mIsPotentiallyPrivate;
+
+    // mIsPotentiallyRevealing indicates that this statement may disclose details about other
+    // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
+    // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
+    // say the user is typing "for the win", and the system wants to record the bigram "the
+    // win".  If the user types "forthe", omitting the space, the system will give "for the" as
+    // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
+    // included in the log for the word "the", disclosing that the previous word had been "for".
+    // For now, we simply do not include this data when logging part of a "megaword".
+    private final boolean mIsPotentiallyRevealing;
+
+    // mKeys stores the names that are the attributes in the output json objects
+    private final String[] mKeys;
+    private static final String[] NULL_KEYS = new String[0];
+
+    LogStatement(final String name, final boolean isPotentiallyPrivate,
+            final boolean isPotentiallyRevealing, final String... keys) {
+        mType = name;
+        mIsPotentiallyPrivate = isPotentiallyPrivate;
+        mIsPotentiallyRevealing = isPotentiallyRevealing;
+        mKeys = (keys == null) ? NULL_KEYS : keys;
+    }
+
+    public String getType() {
+        return mType;
+    }
+
+    public boolean isPotentiallyPrivate() {
+        return mIsPotentiallyPrivate;
+    }
+
+    public boolean isPotentiallyRevealing() {
+        return mIsPotentiallyRevealing;
+    }
+
+    public String[] getKeys() {
+        return mKeys;
+    }
+
+    /**
+     * Utility function to test whether a key-value pair exists in a LogStatement.
+     *
+     * A LogStatement is really just a template -- it does not contain the values, only the
+     * keys.  So the values must be passed in as an argument.
+     *
+     * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
+     * LogStatement
+     * @param queryValue an Object that must be {@code Object.equals()} to the key's corresponding
+     * value in the {@code values} array
+     * @param values the values corresponding to mKeys
+     *
+     * @returns {@true} if {@code queryKey} exists in the keys for this LogStatement, and {@code
+     * queryValue} matches the corresponding value in {@code values}
+     *
+     * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
+     */
+    public boolean containsKeyValuePair(final String queryKey, final Object queryValue,
+            final Object[] values) {
+        if (mKeys.length != values.length) {
+            throw new IllegalArgumentException("Mismatched number of keys and values.");
+        }
+        final int length = mKeys.length;
+        for (int i = 0; i < length; i++) {
+            if (mKeys[i].equals(queryKey) && values[i].equals(queryValue)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * Utility function to set a value in a LogStatement.
+     *
+     * A LogStatement is really just a template -- it does not contain the values, only the
+     * keys.  So the values must be passed in as an argument.
+     *
+     * @param queryKey the String that is tested by {@code String.equals()} to the keys in the
+     * LogStatement
+     * @param values the array of values corresponding to mKeys
+     * @param newValue the replacement value to go into the {@code values} array
+     *
+     * @returns {@true} if the key exists and the value was successfully set, {@false} otherwise
+     *
+     * @throws IllegalArgumentException if {@code values.length} is not equal to keys().length()
+     */
+    public boolean setValue(final String queryKey, final Object[] values, final Object newValue) {
+        if (mKeys.length != values.length) {
+            throw new IllegalArgumentException("Mismatched number of keys and values.");
+        }
+        final int length = mKeys.length;
+        for (int i = 0; i < length; i++) {
+            if (mKeys[i].equals(queryKey)) {
+                values[i] = newValue;
+                return true;
+            }
+        }
+        return false;
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/LogUnit.java b/java/src/com/android/inputmethod/research/LogUnit.java
index 638b7d9..608fab3 100644
--- a/java/src/com/android/inputmethod/research/LogUnit.java
+++ b/java/src/com/android/inputmethod/research/LogUnit.java
@@ -26,15 +26,12 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
-import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.define.ProductionFlag;
-import com.android.inputmethod.research.ResearchLogger.LogStatement;
 
 import java.io.IOException;
 import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
-import java.util.Map;
 
 /**
  * A group of log statements related to each other.
@@ -53,6 +50,7 @@
 /* package */ class LogUnit {
     private static final String TAG = LogUnit.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
     private final ArrayList<LogStatement> mLogStatementList;
     private final ArrayList<Object[]> mValuesList;
     // Assume that mTimeList is sorted in increasing order.  Do not insert null values into
@@ -142,10 +140,10 @@
             JsonWriter jsonWriter = null;
             for (int i = 0; i < size; i++) {
                 final LogStatement logStatement = mLogStatementList.get(i);
-                if (!canIncludePrivateData && logStatement.mIsPotentiallyPrivate) {
+                if (!canIncludePrivateData && logStatement.isPotentiallyPrivate()) {
                     continue;
                 }
-                if (mIsPartOfMegaword && logStatement.mIsPotentiallyRevealing) {
+                if (mIsPartOfMegaword && logStatement.isPotentiallyRevealing()) {
                     continue;
                 }
                 // Only retrieve the jsonWriter if we need to.  If we don't get this far, then
@@ -228,16 +226,16 @@
     private boolean outputLogStatementToLocked(final JsonWriter jsonWriter,
             final LogStatement logStatement, final Object[] values, final Long time) {
         if (DEBUG) {
-            if (logStatement.mKeys.length != values.length) {
-                Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.mName);
+            if (logStatement.getKeys().length != values.length) {
+                Log.d(TAG, "Key and Value list sizes do not match. " + logStatement.getType());
             }
         }
         try {
             jsonWriter.beginObject();
             jsonWriter.name(CURRENT_TIME_KEY).value(System.currentTimeMillis());
             jsonWriter.name(UPTIME_KEY).value(time);
-            jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.mName);
-            final String[] keys = logStatement.mKeys;
+            jsonWriter.name(EVENT_TYPE_KEY).value(logStatement.getType());
+            final String[] keys = logStatement.getKeys();
             final int length = values.length;
             for (int i = 0; i < length; i++) {
                 jsonWriter.name(keys[i]);
@@ -261,8 +259,8 @@
                 } else if (value == null) {
                     jsonWriter.nullValue();
                 } else {
-                    Log.w(TAG, "Unrecognized type to be logged: " +
-                            (value == null ? "<null>" : value.getClass().getName()));
+                    Log.w(TAG, "Unrecognized type to be logged: "
+                            + (value == null ? "<null>" : value.getClass().getName()));
                     jsonWriter.nullValue();
                 }
             }
@@ -422,4 +420,123 @@
         }
         return false;
     }
+
+    /**
+     * Remove data associated with selecting the Research button.
+     *
+     * A LogUnit will capture all user interactions with the IME, including the "meta-interactions"
+     * of using the Research button to control the logging (e.g. by starting and stopping recording
+     * of a test case).  Because meta-interactions should not be part of the normal log, calling
+     * this method will set a field in the LogStatements of the motion events to indiciate that
+     * they should be disregarded.
+     *
+     * This implementation assumes that the data recorded by the meta-interaction takes the
+     * form of all events following the first MotionEvent.ACTION_DOWN before the first long-press
+     * before the last onCodeEvent containing a code matching {@code LogStatement.VALUE_RESEARCH}.
+     *
+     * @returns true if data was removed
+     */
+    public boolean removeResearchButtonInvocation() {
+        // This method is designed to be idempotent.
+
+        // First, find last invocation of "research" key
+        final int indexOfLastResearchKey = findLastIndexContainingKeyValue(
+                LogStatement.TYPE_POINTER_TRACKER_CALL_LISTENER_ON_CODE_INPUT,
+                LogStatement.KEY_CODE, LogStatement.VALUE_RESEARCH);
+        if (indexOfLastResearchKey < 0) {
+            // Could not find invocation of "research" key.  Leave log as is.
+            if (DEBUG) {
+                Log.d(TAG, "Could not find research key");
+            }
+            return false;
+        }
+
+        // Look for the long press that started the invocation of the research key code input.
+        final int indexOfLastLongPressBeforeResearchKey =
+                findLastIndexBefore(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_ON_LONG_PRESS,
+                        indexOfLastResearchKey);
+
+        // Look for DOWN event preceding the long press
+        final int indexOfLastDownEventBeforeLongPress =
+                findLastIndexContainingKeyValueBefore(
+                        LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS,
+                        LogStatement.ACTION, LogStatement.VALUE_DOWN,
+                        indexOfLastLongPressBeforeResearchKey);
+
+        // Flag all LatinKeyboardViewProcessMotionEvents from the DOWN event to the research key as
+        // logging-related
+        final int startingIndex = indexOfLastDownEventBeforeLongPress == -1 ? 0
+                : indexOfLastDownEventBeforeLongPress;
+        for (int index = startingIndex; index < indexOfLastResearchKey; index++) {
+            final LogStatement logStatement = mLogStatementList.get(index);
+            final String type = logStatement.getType();
+            final Object[] values = mValuesList.get(index);
+            if (type.equals(LogStatement.TYPE_LATIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENTS)) {
+                logStatement.setValue(LogStatement.KEY_LOGGING_RELATED, values, true);
+            }
+        }
+        return true;
+    }
+
+    /**
+     * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}.
+     *
+     * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+     * @param startingIndex the index to start the backward search from.  Must be less than the
+     * length of mLogStatementList, or an IndexOutOfBoundsException is thrown.  Can be negative,
+     * in which case -1 is returned.
+     *
+     * @return The index of the last LogStatement, -1 if none exists.
+     */
+    private int findLastIndexBefore(final String queryType, final int startingIndex) {
+        return findLastIndexContainingKeyValueBefore(queryType, null, null, startingIndex);
+    }
+
+    /**
+     * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
+     * containing the given key-value pair.
+     *
+     * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+     * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
+     * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
+     * value
+     *
+     * @return The index of the last LogStatement, -1 if none exists.
+     */
+    private int findLastIndexContainingKeyValue(final String queryType, final String queryKey,
+            final Object queryValue) {
+        return findLastIndexContainingKeyValueBefore(queryType, queryKey, queryValue,
+                mLogStatementList.size() - 1);
+    }
+
+    /**
+     * Find the index of the last LogStatement before {@code startingIndex} of type {@code type}
+     * containing the given key-value pair.
+     *
+     * @param queryType a String that must be {@code String.equals()} to the LogStatement type
+     * @param queryKey a String that must be {@code String.equals()} to a key in the LogStatement
+     * @param queryValue an Object that must be {@code String.equals()} to the key's corresponding
+     * value
+     * @param startingIndex the index to start the backward search from.  Must be less than the
+     * length of mLogStatementList, or an IndexOutOfBoundsException is thrown.  Can be negative,
+     * in which case -1 is returned.
+     *
+     * @return The index of the last LogStatement, -1 if none exists.
+     */
+    private int findLastIndexContainingKeyValueBefore(final String queryType, final String queryKey,
+            final Object queryValue, final int startingIndex) {
+        if (startingIndex < 0) {
+            return -1;
+        }
+        for (int index = startingIndex; index >= 0; index--) {
+            final LogStatement logStatement = mLogStatementList.get(index);
+            final String type = logStatement.getType();
+            if (type.equals(queryType) && (queryKey == null
+                    || logStatement.containsKeyValuePair(queryKey, queryValue,
+                            mValuesList.get(index)))) {
+                return index;
+            }
+        }
+        return -1;
+    }
 }
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
new file mode 100644
index 0000000..36e75be
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -0,0 +1,113 @@
+/*
+ * 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.research;
+
+import android.util.JsonReader;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.latin.define.ProductionFlag;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.util.ArrayList;
+
+public class MotionEventReader {
+    private static final String TAG = MotionEventReader.class.getSimpleName();
+    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+
+    public ReplayData readMotionEventData(final File file) {
+        final ReplayData replayData = new ReplayData();
+        try {
+            // Read file
+            final JsonReader jsonReader = new JsonReader(new BufferedReader(new InputStreamReader(
+                    new FileInputStream(file))));
+            jsonReader.beginArray();
+            while (jsonReader.hasNext()) {
+                readLogStatement(jsonReader, replayData);
+            }
+            jsonReader.endArray();
+        } catch (FileNotFoundException e) {
+            e.printStackTrace();
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+        return replayData;
+    }
+
+    static class ReplayData {
+        final ArrayList<Integer> mActions = new ArrayList<Integer>();
+        final ArrayList<Integer> mXCoords = new ArrayList<Integer>();
+        final ArrayList<Integer> mYCoords = new ArrayList<Integer>();
+        final ArrayList<Long> mTimes = new ArrayList<Long>();
+    }
+
+    private void readLogStatement(final JsonReader jsonReader, final ReplayData replayData)
+            throws IOException {
+        String logStatementType = null;
+        Integer actionType = null;
+        Integer x = null;
+        Integer y = null;
+        Long time = null;
+        boolean loggingRelated = false;
+
+        jsonReader.beginObject();
+        while (jsonReader.hasNext()) {
+            final String key = jsonReader.nextName();
+            if (key.equals("_ty")) {
+                logStatementType = jsonReader.nextString();
+            } else if (key.equals("_ut")) {
+                time = jsonReader.nextLong();
+            } else if (key.equals("x")) {
+                x = jsonReader.nextInt();
+            } else if (key.equals("y")) {
+                y = jsonReader.nextInt();
+            } else if (key.equals("action")) {
+                final String s = jsonReader.nextString();
+                if (s.equals("UP")) {
+                    actionType = MotionEvent.ACTION_UP;
+                } else if (s.equals("DOWN")) {
+                    actionType = MotionEvent.ACTION_DOWN;
+                } else if (s.equals("MOVE")) {
+                    actionType = MotionEvent.ACTION_MOVE;
+                }
+            } else if (key.equals("loggingRelated")) {
+                loggingRelated = jsonReader.nextBoolean();
+            } else {
+                if (DEBUG) {
+                    Log.w(TAG, "Unknown JSON key in LogStatement: " + key);
+                }
+                jsonReader.skipValue();
+            }
+        }
+        jsonReader.endObject();
+
+        if (logStatementType != null && time != null && x != null && y != null && actionType != null
+                && logStatementType.equals("MainKeyboardViewProcessMotionEvent")
+                && !loggingRelated) {
+            replayData.mActions.add(actionType);
+            replayData.mXCoords.add(x);
+            replayData.mYCoords.add(y);
+            replayData.mTimes.add(time);
+        }
+    }
+
+}
diff --git a/java/src/com/android/inputmethod/research/Replayer.java b/java/src/com/android/inputmethod/research/Replayer.java
new file mode 100644
index 0000000..4cc2a58
--- /dev/null
+++ b/java/src/com/android/inputmethod/research/Replayer.java
@@ -0,0 +1,120 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.research;
+
+import android.os.Handler;
+import android.os.Message;
+import android.os.SystemClock;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
+import com.android.inputmethod.keyboard.MainKeyboardView;
+import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
+
+/**
+ * Replays a sequence of motion events in realtime on the screen.
+ *
+ * Useful for user inspection of logged data.
+ */
+public class Replayer {
+    private static final String TAG = Replayer.class.getSimpleName();
+    private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+    private static final long START_TIME_DELAY_MS = 500;
+
+    private boolean mIsReplaying = false;
+    private KeyboardSwitcher mKeyboardSwitcher;
+
+    public void setKeyboardSwitcher(final KeyboardSwitcher keyboardSwitcher) {
+        mKeyboardSwitcher = keyboardSwitcher;
+    }
+
+    private static final int MSG_MOTION_EVENT = 0;
+    private static final int MSG_DONE = 1;
+    private static final int COMPLETION_TIME_MS = 500;
+
+    // TODO: Support historical events and multi-touch.
+    public void replay(final ReplayData replayData) {
+        if (mIsReplaying) {
+            return;
+        }
+
+        mIsReplaying = true;
+        final int numActions = replayData.mActions.size();
+        if (DEBUG) {
+            Log.d(TAG, "replaying " + numActions + " actions");
+        }
+        if (numActions == 0) {
+            mIsReplaying = false;
+            return;
+        }
+        final MainKeyboardView mainKeyboardView = mKeyboardSwitcher.getMainKeyboardView();
+
+        // The reference time relative to the times stored in events.
+        final long origStartTime = replayData.mTimes.get(0);
+        // The reference time relative to which events are replayed in the present.
+        final long currentStartTime = SystemClock.uptimeMillis() + START_TIME_DELAY_MS;
+        // The adjustment needed to translate times from the original recorded time to the current
+        // time.
+        final long timeAdjustment = currentStartTime - origStartTime;
+        final Handler handler = new Handler() {
+            // Track the time of the most recent DOWN event, to be passed as a parameter when
+            // constructing a MotionEvent.  It's initialized here to the origStartTime, but this is
+            // only a precaution.  The value should be overwritten by the first ACTION_DOWN event
+            // before the first use of the variable.  Note that this may cause the first few events
+            // to have incorrect {@code downTime}s.
+            private long mOrigDownTime = origStartTime;
+
+            @Override
+            public void handleMessage(final Message msg) {
+                switch (msg.what) {
+                case MSG_MOTION_EVENT:
+                    final int index = msg.arg1;
+                    final int action = replayData.mActions.get(index);
+                    final int x = replayData.mXCoords.get(index);
+                    final int y = replayData.mYCoords.get(index);
+                    final long origTime = replayData.mTimes.get(index);
+                    if (action == MotionEvent.ACTION_DOWN) {
+                        mOrigDownTime = origTime;
+                    }
+
+                    final MotionEvent me = MotionEvent.obtain(mOrigDownTime + timeAdjustment,
+                            origTime + timeAdjustment, action, x, y, 0);
+                    mainKeyboardView.processMotionEvent(me);
+                    me.recycle();
+                    break;
+                case MSG_DONE:
+                    mIsReplaying = false;
+                    break;
+                }
+            }
+        };
+
+        for (int i = 0; i < numActions; i++) {
+            final Message msg = Message.obtain(handler, MSG_MOTION_EVENT, i, 0);
+            final long msgTime = replayData.mTimes.get(i) + timeAdjustment;
+            handler.sendMessageAtTime(msg, msgTime);
+            if (DEBUG) {
+                Log.d(TAG, "queuing event at " + msgTime);
+            }
+        }
+        final long presentDoneTime = replayData.mTimes.get(numActions - 1) + timeAdjustment
+                + COMPLETION_TIME_MS;
+        handler.sendMessageAtTime(Message.obtain(handler, MSG_DONE), presentDoneTime);
+    }
+}
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index dbf2d29..c4d53e1 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -39,6 +39,8 @@
 import android.graphics.Paint.Style;
 import android.net.Uri;
 import android.os.Build;
+import android.os.Bundle;
+import android.os.Handler;
 import android.os.IBinder;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
@@ -57,6 +59,7 @@
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.keyboard.KeyboardId;
+import com.android.inputmethod.keyboard.KeyboardSwitcher;
 import com.android.inputmethod.keyboard.KeyboardView;
 import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.latin.Constants;
@@ -69,8 +72,17 @@
 import com.android.inputmethod.latin.Suggest;
 import com.android.inputmethod.latin.SuggestedWords;
 import com.android.inputmethod.latin.define.ProductionFlag;
+import com.android.inputmethod.research.MotionEventReader.ReplayData;
 
+import java.io.BufferedReader;
 import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.charset.Charset;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Date;
@@ -87,8 +99,18 @@
  * This functionality is off by default. See {@link ProductionFlag#IS_EXPERIMENTAL}.
  */
 public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+    // TODO: This class has grown quite large and combines several concerns that should be
+    // separated.  The following refactorings will be applied as soon as possible after adding
+    // support for replaying historical events, fixing some replay bugs, adding some ui constraints
+    // on the feedback dialog, and adding the survey dialog.
+    // TODO: Refactor.  Move splash screen code into separate class.
+    // TODO: Refactor.  Move feedback screen code into separate class.
+    // TODO: Refactor.  Move logging invocations into their own class.
+    // TODO: Refactor.  Move currentLogUnit management into separate class.
     private static final String TAG = ResearchLogger.class.getSimpleName();
     private static final boolean DEBUG = false && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
+    private static final boolean DEBUG_REPLAY_AFTER_FEEDBACK = false
+            && ProductionFlag.IS_EXPERIMENTAL_DEBUG;
     // Whether the TextView contents are logged at the end of the session.  true will disclose
     // private info.
     private static final boolean LOG_FULL_TEXTVIEW_CONTENTS = false
@@ -98,8 +120,10 @@
     private static final int OUTPUT_FORMAT_VERSION = 5;
     private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
     private static final String PREF_RESEARCH_HAS_SEEN_SPLASH = "pref_research_has_seen_splash";
-    /* package */ static final String FILENAME_PREFIX = "researchLog";
-    private static final String FILENAME_SUFFIX = ".txt";
+    /* package */ static final String LOG_FILENAME_PREFIX = "researchLog";
+    private static final String LOG_FILENAME_SUFFIX = ".txt";
+    /* package */ static final String USER_RECORDING_FILENAME_PREFIX = "recording";
+    private static final String USER_RECORDING_FILENAME_SUFFIX = ".txt";
     private static final SimpleDateFormat TIMESTAMP_DATEFORMAT =
             new SimpleDateFormat("yyyyMMddHHmmssS", Locale.US);
     // Whether to show an indicator on the screen that logging is on.  Currently a very small red
@@ -129,9 +153,15 @@
     // the system to do so.
     // LogUnits are queued in the LogBuffers and published to the ResearchLogs when words are
     // complete.
-    /* package */ ResearchLog mFeedbackLog;
     /* 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 */ ResearchLog mUserRecordingLog;
+    /* package */ LogBuffer mUserRecordingLogBuffer;
+    private File mUserRecordingFile = null;
 
     private boolean mIsPasswordView = false;
     private boolean mIsLoggingSuspended = false;
@@ -144,7 +174,7 @@
     /* package for test */ static final String WORD_REPLACEMENT_STRING = "\uE001";
     private static final String PREF_LAST_CLEANUP_TIME = "pref_last_cleanup_time";
     private static final long DURATION_BETWEEN_DIR_CLEANUP_IN_MS = DateUtils.DAY_IN_MILLIS;
-    private static final long MAX_LOGFILE_AGE_IN_MS = DateUtils.DAY_IN_MILLIS;
+    private static final long MAX_LOGFILE_AGE_IN_MS = 4 * DateUtils.DAY_IN_MILLIS;
     protected static final int SUSPEND_DURATION_IN_MINUTES = 1;
     // set when LatinIME should ignore an onUpdateSelection() callback that
     // arises from operations in this class
@@ -153,10 +183,14 @@
     // used to check whether words are not unique
     private Suggest mSuggest;
     private MainKeyboardView mMainKeyboardView;
+    // TODO: Check whether a superclass can be used instead of LatinIME.
     private LatinIME mLatinIME;
     private final Statistics mStatistics;
+    private final MotionEventReader mMotionEventReader = new MotionEventReader();
+    private final Replayer mReplayer = new Replayer();
 
     private Intent mUploadIntent;
+    private Intent mUploadNowIntent;
 
     private LogUnit mCurrentLogUnit = new LogUnit();
 
@@ -165,6 +199,20 @@
     // thereby leaking private data, we store the time of the down event that started the second
     // gesture, and when committing the earlier word, split the LogUnit.
     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;
+
     private ResearchLogger() {
         mStatistics = Statistics.getInstance();
     }
@@ -173,7 +221,7 @@
         return sInstance;
     }
 
-    public void init(final LatinIME latinIME) {
+    public void init(final LatinIME latinIME, final KeyboardSwitcher keyboardSwitcher) {
         assert latinIME != null;
         if (latinIME == null) {
             Log.w(TAG, "IMS is null; logging is off");
@@ -210,6 +258,9 @@
         mLatinIME = latinIME;
         mPrefs = prefs;
         mUploadIntent = new Intent(mLatinIME, UploaderService.class);
+        mUploadNowIntent = new Intent(mLatinIME, UploaderService.class);
+        mUploadNowIntent.putExtra(UploaderService.EXTRA_UPLOAD_UNCONDITIONALLY, true);
+        mReplayer.setKeyboardSwitcher(keyboardSwitcher);
 
         if (ProductionFlag.IS_EXPERIMENTAL) {
             scheduleUploadingService(mLatinIME);
@@ -237,8 +288,10 @@
 
     private void cleanupLoggingDir(final File dir, final long time) {
         for (File file : dir.listFiles()) {
-            if (file.getName().startsWith(ResearchLogger.FILENAME_PREFIX) &&
-                    file.lastModified() < time) {
+            final String filename = file.getName();
+            if ((filename.startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
+                    || filename.startsWith(ResearchLogger.USER_RECORDING_FILENAME_PREFIX))
+                    && file.lastModified() < time) {
                 file.delete();
             }
         }
@@ -335,9 +388,9 @@
 
     private static int sLogFileCounter = 0;
 
-    private File createLogFile(File filesDir) {
+    private File createLogFile(final File filesDir) {
         final StringBuilder sb = new StringBuilder();
-        sb.append(FILENAME_PREFIX).append('-');
+        sb.append(LOG_FILENAME_PREFIX).append('-');
         sb.append(mUUIDString).append('-');
         sb.append(TIMESTAMP_DATEFORMAT.format(new Date())).append('-');
         // Sometimes logFiles are created within milliseconds of each other.  Append a counter to
@@ -349,7 +402,16 @@
             sLogFileCounter = 0;
         }
         sb.append(sLogFileCounter);
-        sb.append(FILENAME_SUFFIX);
+        sb.append(LOG_FILENAME_SUFFIX);
+        return new File(filesDir, sb.toString());
+    }
+
+    private File createUserRecordingFile(final File filesDir) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(USER_RECORDING_FILENAME_PREFIX).append('-');
+        sb.append(mUUIDString).append('-');
+        sb.append(TIMESTAMP_DATEFORMAT.format(new Date()));
+        sb.append(USER_RECORDING_FILENAME_SUFFIX);
         return new File(filesDir, sb.toString());
     }
 
@@ -517,51 +579,10 @@
         presentFeedbackDialog(latinIME);
     }
 
-    // TODO: currently unreachable.  Remove after being sure no menu is needed.
-    /*
-    public void presentResearchDialog(final LatinIME latinIME) {
-        final CharSequence title = latinIME.getString(R.string.english_ime_research_log);
-        final boolean showEnable = mIsLoggingSuspended || !sIsLogging;
-        final CharSequence[] items = new CharSequence[] {
-                latinIME.getString(R.string.research_feedback_menu_option),
-                showEnable ? latinIME.getString(R.string.research_enable_session_logging) :
-                        latinIME.getString(R.string.research_do_not_log_this_session)
-        };
-        final DialogInterface.OnClickListener listener = new DialogInterface.OnClickListener() {
-            @Override
-            public void onClick(DialogInterface di, int position) {
-                di.dismiss();
-                switch (position) {
-                    case 0:
-                        presentFeedbackDialog(latinIME);
-                        break;
-                    case 1:
-                        enableOrDisable(showEnable, latinIME);
-                        break;
-                }
-            }
-
-        };
-        final AlertDialog.Builder builder = new AlertDialog.Builder(latinIME)
-                .setItems(items, listener)
-                .setTitle(title);
-        latinIME.showOptionDialog(builder.create());
-    }
-    */
-
-    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;
-
     public void presentFeedbackDialog(LatinIME latinIME) {
+        if (isMakingUserRecording()) {
+            saveRecording();
+        }
         mInFeedbackDialog = true;
         mSavedFeedbackLogBuffer = mFeedbackLogBuffer;
         mSavedFeedbackLog = mFeedbackLog;
@@ -569,7 +590,90 @@
         // Feedback dialog will not close them.
         mFeedbackLogBuffer = null;
         mFeedbackLog = null;
-        latinIME.launchKeyboardedDialogActivity(FeedbackActivity.class);
+
+        Intent intent = new Intent();
+        intent.setClass(mLatinIME, FeedbackActivity.class);
+        if (mFeedbackDialogBundle != null) {
+            Log.d(TAG, "putting extra in feedbackdialogbundle");
+            intent.putExtras(mFeedbackDialogBundle);
+        }
+        intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+        latinIME.startActivity(intent);
+    }
+
+    public void setFeedbackDialogBundle(final Bundle bundle) {
+        mFeedbackDialogBundle = bundle;
+    }
+
+    public void startRecording() {
+        final Resources res = mLatinIME.getResources();
+        Toast.makeText(mLatinIME,
+                res.getString(R.string.research_feedback_demonstration_instructions),
+                Toast.LENGTH_LONG).show();
+        startRecordingInternal();
+    }
+
+    private void startRecordingInternal() {
+        commitCurrentLogUnit();
+        if (mUserRecordingLog != null) {
+            mUserRecordingLog.abort();
+        }
+        mUserRecordingFile = createUserRecordingFile(mFilesDir);
+        mUserRecordingLog = new ResearchLog(mUserRecordingFile, mLatinIME);
+        mUserRecordingLogBuffer = new LogBuffer();
+        resetRecordingTimer();
+    }
+
+    private boolean isMakingUserRecording() {
+        return mUserRecordingLog != null;
+    }
+
+    private void resetRecordingTimer() {
+        if (mUserRecordingTimeoutHandler == null) {
+            mUserRecordingTimeoutHandler = new Handler();
+        }
+        clearRecordingTimer();
+        mUserRecordingTimeoutHandler.postDelayed(mRecordingHandlerTimeoutRunnable,
+                USER_RECORDING_TIMEOUT_MS);
+    }
+
+    private void clearRecordingTimer() {
+        mUserRecordingTimeoutHandler.removeCallbacks(mRecordingHandlerTimeoutRunnable);
+    }
+
+    private Runnable mRecordingHandlerTimeoutRunnable = new Runnable() {
+        @Override
+        public void run() {
+            cancelRecording();
+            requestIndicatorRedraw();
+            final Resources res = mLatinIME.getResources();
+            Toast.makeText(mLatinIME, res.getString(R.string.research_feedback_recording_failure),
+                    Toast.LENGTH_LONG).show();
+        }
+    };
+
+    private void cancelRecording() {
+        if (mUserRecordingLog != null) {
+            mUserRecordingLog.abort();
+        }
+        mUserRecordingLog = null;
+        mUserRecordingLogBuffer = null;
+        if (mFeedbackDialogBundle != null) {
+            mFeedbackDialogBundle.putBoolean("HasRecording", false);
+        }
+    }
+
+    private void saveRecording() {
+        commitCurrentLogUnit();
+        publishLogBuffer(mUserRecordingLogBuffer, mUserRecordingLog, true);
+        mUserRecordingLog.close(null);
+        mUserRecordingLog = null;
+        mUserRecordingLogBuffer = null;
+
+        if (mFeedbackDialogBundle != null) {
+            mFeedbackDialogBundle.putBoolean(FeedbackFragment.KEY_HAS_USER_RECORDING, true);
+        }
+        clearRecordingTimer();
     }
 
     // TODO: currently unreachable.  Remove after being sure enable/disable is
@@ -631,52 +735,39 @@
         return null;
     }
 
-    static class LogStatement {
-        final String mName;
-
-        // mIsPotentiallyPrivate indicates that event contains potentially private information.  If
-        // the word that this event is a part of is determined to be privacy-sensitive, then this
-        // event should not be included in the output log.  The system waits to output until the
-        // containing word is known.
-        final boolean mIsPotentiallyPrivate;
-
-        // mIsPotentiallyRevealing indicates that this statement may disclose details about other
-        // words typed in other LogUnits.  This can happen if the user is not inserting spaces, and
-        // data from Suggestions and/or Composing text reveals the entire "megaword".  For example,
-        // say the user is typing "for the win", and the system wants to record the bigram "the
-        // win".  If the user types "forthe", omitting the space, the system will give "for the" as
-        // a suggestion.  If the user accepts the autocorrection, the suggestion for "for the" is
-        // included in the log for the word "the", disclosing that the previous word had been "for".
-        // For now, we simply do not include this data when logging part of a "megaword".
-        final boolean mIsPotentiallyRevealing;
-
-        // mKeys stores the names that are the attributes in the output json objects
-        final String[] mKeys;
-        private static final String[] NULL_KEYS = new String[0];
-
-        LogStatement(final String name, final boolean isPotentiallyPrivate,
-                final boolean isPotentiallyRevealing, final String... keys) {
-            mName = name;
-            mIsPotentiallyPrivate = isPotentiallyPrivate;
-            mIsPotentiallyRevealing = isPotentiallyRevealing;
-            mKeys = (keys == null) ? NULL_KEYS : keys;
-        }
-    }
-
     private static final LogStatement LOGSTATEMENT_FEEDBACK =
-            new LogStatement("UserFeedback", false, false, "contents", "accountName");
+            new LogStatement("UserFeedback", false, false, "contents", "accountName", "recording");
     public void sendFeedback(final String feedbackContents, final boolean includeHistory,
-            final boolean isIncludingAccountName) {
+            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
+            if (mUserRecordingFile != null) {
+                try {
+                    final FileChannel channel =
+                            new FileInputStream(mUserRecordingFile).getChannel();
+                    final MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_ONLY, 0,
+                            channel.size());
+                    // Android's openFileOutput() creates the file, so we use Android's default
+                    // Charset (UTF-8) here to read it.
+                    recording = Charset.defaultCharset().decode(buffer).toString();
+                } catch (FileNotFoundException e) {
+                    e.printStackTrace();
+                } catch (IOException e) {
+                    e.printStackTrace();
+                }
+            }
+        }
         final LogUnit feedbackLogUnit = new LogUnit();
         final String accountName = isIncludingAccountName ? getAccountName() : "";
         feedbackLogUnit.addLogStatement(LOGSTATEMENT_FEEDBACK, SystemClock.uptimeMillis(),
-                feedbackContents, accountName);
+                feedbackContents, accountName, recording);
         mFeedbackLogBuffer.shiftIn(feedbackLogUnit);
         publishLogBuffer(mFeedbackLogBuffer, mSavedFeedbackLog, true /* isIncludingPrivateData */);
         mSavedFeedbackLog.close(new Runnable() {
@@ -685,13 +776,25 @@
                 uploadNow();
             }
         });
+
+        if (isIncludingRecording && DEBUG_REPLAY_AFTER_FEEDBACK) {
+            final Handler handler = new Handler();
+            handler.postDelayed(new Runnable() {
+                @Override
+                public void run() {
+                    final ReplayData replayData =
+                            mMotionEventReader.readMotionEventData(mUserRecordingFile);
+                    mReplayer.replay(replayData);
+                }
+            }, 1000);
+        }
     }
 
     public void uploadNow() {
         if (DEBUG) {
             Log.d(TAG, "calling uploadNow()");
         }
-        mLatinIME.startService(mUploadIntent);
+        mLatinIME.startService(mUploadNowIntent);
     }
 
     public void onLeavingSendFeedbackDialog() {
@@ -734,11 +837,11 @@
             int height) {
         // TODO: Reimplement using a keyboard background image specific to the ResearchLogger
         // and remove this method.
-        // The check for MainKeyboardView ensures that a red border is only placed around
-        // the main keyboard, not every keyboard.
+        // The check for MainKeyboardView ensures that the indicator only decorates the main
+        // keyboard, not every keyboard.
         if (IS_SHOWING_INDICATOR && isAllowedToLog() && view instanceof MainKeyboardView) {
             final int savedColor = paint.getColor();
-            paint.setColor(Color.RED);
+            paint.setColor(isMakingUserRecording() ? Color.YELLOW : Color.RED);
             final Style savedStyle = paint.getStyle();
             paint.setStyle(Style.STROKE);
             final float savedStrokeWidth = paint.getStrokeWidth();
@@ -747,10 +850,9 @@
                 canvas.drawLine(0, 0, 0, height, paint);
                 canvas.drawLine(width, 0, width, height, paint);
             } else {
-                // Put a tiny red dot on the screen so a knowledgeable user can check whether
-                // it is enabled.  The dot is actually a zero-width, zero-height rectangle,
-                // placed at the lower-right corner of the canvas, painted with a non-zero border
-                // width.
+                // Put a tiny dot on the screen so a knowledgeable user can check whether it is
+                // enabled.  The dot is actually a zero-width, zero-height rectangle, placed at the
+                // lower-right corner of the canvas, painted with a non-zero border width.
                 paint.setStrokeWidth(3);
                 canvas.drawRect(width, height, width, height, paint);
             }
@@ -770,7 +872,7 @@
 
     private synchronized void enqueueEvent(final LogUnit logUnit, final LogStatement logStatement,
             final Object... values) {
-        assert values.length == logStatement.mKeys.length;
+        assert values.length == logStatement.getKeys().length;
         if (isAllowedToLog() && logUnit != null) {
             final long time = SystemClock.uptimeMillis();
             logUnit.addLogStatement(logStatement, time, values);
@@ -801,6 +903,9 @@
             if (mFeedbackLogBuffer != null) {
                 mFeedbackLogBuffer.shiftIn(mCurrentLogUnit);
             }
+            if (mUserRecordingLogBuffer != null) {
+                mUserRecordingLogBuffer.shiftIn(mCurrentLogUnit);
+            }
             mCurrentLogUnit = new LogUnit();
         } else {
             if (DEBUG) {
@@ -1058,7 +1163,7 @@
      *
      */
     private static final LogStatement LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT =
-            new LogStatement("MotionEvent", true, false, "action", "MotionEvent");
+            new LogStatement("MotionEvent", true, false, "action", "MotionEvent", "loggingRelated");
     public static void mainKeyboardView_processMotionEvent(final MotionEvent me, final int action,
             final long eventTime, final int index, final int id, final int x, final int y) {
         if (me != null) {
@@ -1075,12 +1180,16 @@
             }
             final ResearchLogger researchLogger = getInstance();
             researchLogger.enqueueEvent(LOGSTATEMENT_MAIN_KEYBOARD_VIEW_PROCESS_MOTION_EVENT,
-                    actionString, MotionEvent.obtain(me));
+                    actionString, MotionEvent.obtain(me), false);
             if (action == MotionEvent.ACTION_DOWN) {
                 // Subtract 1 from eventTime so the down event is included in the later
                 // LogUnit, not the earlier (the test is for inequality).
                 researchLogger.setSavedDownEventTime(eventTime - 1);
             }
+            // Refresh the timer in case we are capturing user feedback.
+            if (researchLogger.isMakingUserRecording()) {
+                researchLogger.resetRecordingTimer();
+            }
         }
     }
 
@@ -1442,13 +1551,21 @@
             final int code) {
         if (key != null) {
             String outputText = key.getOutputText();
-            getInstance().enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
+            final ResearchLogger researchLogger = getInstance();
+            researchLogger.enqueueEvent(LOGSTATEMENT_POINTERTRACKER_CALLLISTENERONCODEINPUT,
                     Constants.printableCode(scrubDigitFromCodePoint(code)),
                     outputText == null ? null : scrubDigitsFromString(outputText.toString()),
                     x, y, ignoreModifierKey, altersCode, key.isEnabled());
+            if (code == Constants.CODE_RESEARCH) {
+                researchLogger.suppressResearchKeyMotionData();
+            }
         }
     }
 
+    private void suppressResearchKeyMotionData() {
+        mCurrentLogUnit.removeResearchButtonInvocation();
+    }
+
     /**
      * Log a call to PointerTracker.callListenerCallListenerOnRelease().
      *
diff --git a/java/src/com/android/inputmethod/research/UploaderService.java b/java/src/com/android/inputmethod/research/UploaderService.java
index 5e3cf55..89c67fb 100644
--- a/java/src/com/android/inputmethod/research/UploaderService.java
+++ b/java/src/com/android/inputmethod/research/UploaderService.java
@@ -51,7 +51,7 @@
     private static final boolean IS_INHIBITING_AUTO_UPLOAD = false
             && ProductionFlag.IS_EXPERIMENTAL_DEBUG;  // Force false in production
     public static final long RUN_INTERVAL = AlarmManager.INTERVAL_HOUR;
-    private static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
+    public static final String EXTRA_UPLOAD_UNCONDITIONALLY = UploaderService.class.getName()
             + ".extra.UPLOAD_UNCONDITIONALLY";
     private static final int BUF_SIZE = 1024 * 8;
     protected static final int TIMEOUT_IN_MS = 1000 * 4;
@@ -131,7 +131,7 @@
         final File[] files = mFilesDir.listFiles(new FileFilter() {
             @Override
             public boolean accept(File pathname) {
-                return pathname.getName().startsWith(ResearchLogger.FILENAME_PREFIX)
+                return pathname.getName().startsWith(ResearchLogger.LOG_FILENAME_PREFIX)
                         && !pathname.canWrite();
             }
         });
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 5c8ef7e..ca38b0d 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -87,7 +87,7 @@
         AKLOGE("DICT: Can't allocate memory region for dictionary. errno=%d", errno);
         return 0;
     }
-    int ret = fseek(file, (long)dictOffset, SEEK_SET);
+    int ret = fseek(file, static_cast<long>(dictOffset), SEEK_SET);
     if (ret != 0) {
         AKLOGE("DICT: Failure in fseek. ret=%d errno=%d", ret, errno);
         return 0;
@@ -121,7 +121,7 @@
     }
     PROF_END(66);
     PROF_CLOSE;
-    return (jlong)dictionary;
+    return reinterpret_cast<jlong>(dictionary);
 }
 
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jclass clazz, jlong dict,
@@ -216,7 +216,7 @@
 static jboolean latinime_BinaryDictionary_isValidBigram(JNIEnv *env, jclass clazz, jlong dict,
         jintArray wordArray1, jintArray wordArray2) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
-    if (!dictionary) return (jboolean) false;
+    if (!dictionary) return JNI_FALSE;
     const jsize codePointLength1 = env->GetArrayLength(wordArray1);
     const jsize codePointLength2 = env->GetArrayLength(wordArray2);
     int codePoints1[codePointLength1];
diff --git a/native/jni/src/bigram_dictionary.cpp b/native/jni/src/bigram_dictionary.cpp
index 44dc75e..ef0434c 100644
--- a/native/jni/src/bigram_dictionary.cpp
+++ b/native/jni/src/bigram_dictionary.cpp
@@ -21,6 +21,7 @@
 #include "bigram_dictionary.h"
 #include "binary_format.h"
 #include "bloom_filter.h"
+#include "char_utils.h"
 #include "defines.h"
 #include "dictionary.h"
 
@@ -50,7 +51,7 @@
     int insertAt = 0;
     while (insertAt < MAX_RESULTS) {
         if (frequency > bigramFreq[insertAt] || (bigramFreq[insertAt] == frequency
-                && length < Dictionary::wideStrLen(
+                && length < getCodePointCount(MAX_WORD_LENGTH,
                         bigramCodePoints + insertAt * MAX_WORD_LENGTH))) {
             break;
         }
diff --git a/native/jni/src/binary_format.h b/native/jni/src/binary_format.h
index 61780de..f19d5e3 100644
--- a/native/jni/src/binary_format.h
+++ b/native/jni/src/binary_format.h
@@ -314,7 +314,7 @@
 }
 
 static AK_FORCE_INLINE int shortcutByteSize(const uint8_t *const dict, const int pos) {
-    return ((int)(dict[pos] << 8)) + (dict[pos + 1]);
+    return (static_cast<int>(dict[pos] << 8)) + (dict[pos + 1]);
 }
 
 inline int BinaryFormat::skipChildrenPosition(const uint8_t flags, const int pos) {
diff --git a/native/jni/src/correction.cpp b/native/jni/src/correction.cpp
index e892c85..d4bd4aa 100644
--- a/native/jni/src/correction.cpp
+++ b/native/jni/src/correction.cpp
@@ -112,7 +112,7 @@
     mMaxErrors = maxErrors;
 }
 
-void Correction::checkState() {
+void Correction::checkState() const {
     if (DEBUG_DICT) {
         int inputCount = 0;
         if (mSkipPos >= 0) ++inputCount;
@@ -121,12 +121,12 @@
     }
 }
 
-bool Correction::sameAsTyped() {
+bool Correction::sameAsTyped() const {
     return mProximityInfoState.sameAsTyped(mWord, mOutputIndex);
 }
 
 int Correction::getFreqForSplitMultipleWords(const int *freqArray, const int *wordLengthArray,
-        const int wordCount, const bool isSpaceProximity, const int *word) {
+        const int wordCount, const bool isSpaceProximity, const int *word) const {
     return Correction::RankingAlgorithm::calcFreqForSplitMultipleWords(freqArray, wordLengthArray,
             wordCount, this, isSpaceProximity, word);
 }
@@ -677,7 +677,7 @@
             const float factor =
                     SuggestUtils::getDistanceScalingFactor(static_cast<float>(squaredDistance));
             if (factor > 0.0f) {
-                multiplyRate((int)(factor * 100.0f), &finalFreq);
+                multiplyRate(static_cast<int>(factor * 100.0f), &finalFreq);
             } else if (squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) {
                 multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
             }
diff --git a/native/jni/src/correction.h b/native/jni/src/correction.h
index 89e300d..0873dae 100644
--- a/native/jni/src/correction.h
+++ b/native/jni/src/correction.h
@@ -64,8 +64,8 @@
     void setCorrectionParams(const int skipPos, const int excessivePos, const int transposedPos,
             const int spaceProximityPos, const int missingSpacePos, const bool useFullEditDistance,
             const bool doAutoCompletion, const int maxErrors);
-    void checkState();
-    bool sameAsTyped();
+    void checkState() const;
+    bool sameAsTyped() const;
     bool initProcessState(const int index);
 
     int getInputIndex() const;
@@ -77,7 +77,7 @@
     }
 
     int getFreqForSplitMultipleWords(const int *freqArray, const int *wordLengthArray,
-            const int wordCount, const bool isSpaceProximity, const int *word);
+            const int wordCount, const bool isSpaceProximity, const int *word) const;
     int getFinalProbability(const int probability, int **word, int *wordLength);
     int getFinalProbabilityForSubQueue(const int probability, int **word, int *wordLength,
             const int inputSize);
diff --git a/native/jni/src/defines.h b/native/jni/src/defines.h
index 922a746..9883168 100644
--- a/native/jni/src/defines.h
+++ b/native/jni/src/defines.h
@@ -370,7 +370,7 @@
 
 // TODO: Remove
 #define MAX_POINTER_COUNT 1
-#define MAX_POINTER_COUNT_FOR_G 2
+#define MAX_POINTER_COUNT_G 2
 
 // Size, in bytes, of the bloom filter index for bigrams
 // 128 gives us 1024 buckets. The probability of false positive is (1 - e ** (-kn/m))**k,
diff --git a/native/jni/src/dic_traverse_wrapper.h b/native/jni/src/dic_traverse_wrapper.h
index 9a1db38..1108a45 100644
--- a/native/jni/src/dic_traverse_wrapper.h
+++ b/native/jni/src/dic_traverse_wrapper.h
@@ -31,8 +31,8 @@
         }
         return 0;
     }
-    static void initDicTraverseSession(void *traverseSession,
-            const Dictionary *const dictionary, const int *prevWord, const int prevWordLength) {
+    static void initDicTraverseSession(void *traverseSession, const Dictionary *const dictionary,
+            const int *prevWord, const int prevWordLength) {
         if (sDicTraverseSessionInitMethod) {
             sDicTraverseSessionInitMethod(traverseSession, dictionary, prevWord, prevWordLength);
         }
@@ -42,8 +42,7 @@
             sDicTraverseSessionReleaseMethod(traverseSession);
         }
     }
-    static void setTraverseSessionFactoryMethod(
-            void *(*factoryMethod)(JNIEnv *, jstring)) {
+    static void setTraverseSessionFactoryMethod(void *(*factoryMethod)(JNIEnv *, jstring)) {
         sDicTraverseSessionFactoryMethod = factoryMethod;
     }
     static void setTraverseSessionInitMethod(
@@ -53,6 +52,7 @@
     static void setTraverseSessionReleaseMethod(void (*releaseMethod)(void *)) {
         sDicTraverseSessionReleaseMethod = releaseMethod;
     }
+
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DicTraverseWrapper);
     static void *(*sDicTraverseSessionFactoryMethod)(JNIEnv *, jstring);
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 121cf05..83676b2 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -65,10 +65,6 @@
     int getDictBufAdjust() const { return mDictBufAdjust; }
     virtual ~Dictionary();
 
-    // public static utility methods
-    // static inline methods should be defined in the header file
-    static int wideStrLen(int *str);
-
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(Dictionary);
     const uint8_t *mDict;
@@ -84,17 +80,5 @@
     const BigramDictionary *mBigramDictionary;
     SuggestInterface *mGestureSuggest;
 };
-
-// public static utility methods
-// static inline methods should be defined in the header file
-inline int Dictionary::wideStrLen(int *str) {
-    if (!str) return 0;
-    int length = 0;
-    while (*str) {
-        str++;
-        length++;
-    }
-    return length;
-}
 } // namespace latinime
 #endif // LATINIME_DICTIONARY_H
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index 8157fe2..a0bad1a 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -110,8 +110,8 @@
         return false;
     }
 
-    const int startIndex = ProximityInfoUtils::getStartIndexFromCoordinates(
-            MAX_PROXIMITY_CHARS_SIZE, x, y, CELL_HEIGHT, CELL_WIDTH, GRID_WIDTH);
+    const int startIndex = ProximityInfoUtils::getStartIndexFromCoordinates(x, y,
+            CELL_HEIGHT, CELL_WIDTH, GRID_WIDTH);
     if (DEBUG_PROXIMITY_INFO) {
         AKLOGI("hasSpaceProximity: index %d, %d, %d", startIndex, x, y);
     }
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index 6d571d7..f3a68e4 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -47,57 +47,21 @@
         // the radius of the key is assigned to zero.
         return mSweetSpotRadii[keyIndex] > 0.0f;
     }
-    float getSweetSpotRadiiAt(int keyIndex) const {
-        return mSweetSpotRadii[keyIndex];
-    }
-    float getSweetSpotCenterXAt(int keyIndex) const {
-        return mSweetSpotCenterXs[keyIndex];
-    }
-    float getSweetSpotCenterYAt(int keyIndex) const {
-        return mSweetSpotCenterYs[keyIndex];
-    }
+    float getSweetSpotRadiiAt(int keyIndex) const { return mSweetSpotRadii[keyIndex]; }
+    float getSweetSpotCenterXAt(int keyIndex) const { return mSweetSpotCenterXs[keyIndex]; }
+    float getSweetSpotCenterYAt(int keyIndex) const { return mSweetSpotCenterYs[keyIndex]; }
     void calculateNearbyKeyCodes(
             const int x, const int y, const int primaryKey, int *inputCodes) const;
-
-    bool hasTouchPositionCorrectionData() const {
-        return HAS_TOUCH_POSITION_CORRECTION_DATA;
-    }
-
-    int getMostCommonKeyWidth() const {
-        return MOST_COMMON_KEY_WIDTH;
-    }
-
-    int getMostCommonKeyWidthSquare() const {
-        return MOST_COMMON_KEY_WIDTH_SQUARE;
-    }
-
-    int getKeyCount() const {
-        return KEY_COUNT;
-    }
-
-    int getCellHeight() const {
-        return CELL_HEIGHT;
-    }
-
-    int getCellWidth() const {
-        return CELL_WIDTH;
-    }
-
-    int getGridWidth() const {
-        return GRID_WIDTH;
-    }
-
-    int getGridHeight() const {
-        return GRID_HEIGHT;
-    }
-
-    int getKeyboardWidth() const {
-        return KEYBOARD_WIDTH;
-    }
-
-    int getKeyboardHeight() const {
-        return KEYBOARD_HEIGHT;
-    }
+    bool hasTouchPositionCorrectionData() const { return HAS_TOUCH_POSITION_CORRECTION_DATA; }
+    int getMostCommonKeyWidth() const { return MOST_COMMON_KEY_WIDTH; }
+    int getMostCommonKeyWidthSquare() const { return MOST_COMMON_KEY_WIDTH_SQUARE; }
+    int getKeyCount() const { return KEY_COUNT; }
+    int getCellHeight() const { return CELL_HEIGHT; }
+    int getCellWidth() const { return CELL_WIDTH; }
+    int getGridWidth() const { return GRID_WIDTH; }
+    int getGridHeight() const { return GRID_HEIGHT; }
+    int getKeyboardWidth() const { return KEYBOARD_WIDTH; }
+    int getKeyboardHeight() const { return KEYBOARD_HEIGHT; }
 
     int getKeyCenterXOfCodePointG(int charCode) const;
     int getKeyCenterYOfCodePointG(int charCode) const;
@@ -109,9 +73,8 @@
             const int *const inputYCoordinates, const int inputSize, int *allInputCodes) const {
         ProximityInfoUtils::initializeProximities(inputCodes, inputXCoordinates, inputYCoordinates,
                 inputSize, mKeyXCoordinates, mKeyYCoordinates, mKeyWidths, mKeyHeights,
-                mProximityCharsArray, MAX_PROXIMITY_CHARS_SIZE, CELL_HEIGHT, CELL_WIDTH,
-                GRID_WIDTH, MOST_COMMON_KEY_WIDTH, KEY_COUNT, mLocaleStr, &mCodeToKeyMap,
-                allInputCodes);
+                mProximityCharsArray, CELL_HEIGHT, CELL_WIDTH, GRID_WIDTH, MOST_COMMON_KEY_WIDTH,
+                KEY_COUNT, mLocaleStr, &mCodeToKeyMap, allInputCodes);
     }
 
     int getKeyIndexOf(const int c) const {
diff --git a/native/jni/src/proximity_info_state.cpp b/native/jni/src/proximity_info_state.cpp
index 387b03a..141be26 100644
--- a/native/jni/src/proximity_info_state.cpp
+++ b/native/jni/src/proximity_info_state.cpp
@@ -27,8 +27,6 @@
 
 namespace latinime {
 
-const int ProximityInfoState::NOT_A_CODE = -1;
-
 void ProximityInfoState::initInputParams(const int pointerId, const float maxPointToKeyLength,
         const ProximityInfo *proximityInfo, const int *const inputCodes, const int inputSize,
         const int *const xCoordinates, const int *const yCoordinates, const int *const times,
diff --git a/native/jni/src/proximity_info_state.h b/native/jni/src/proximity_info_state.h
index 7422cb0..ff1b350 100644
--- a/native/jni/src/proximity_info_state.h
+++ b/native/jni/src/proximity_info_state.h
@@ -32,9 +32,6 @@
 
 class ProximityInfoState {
  public:
-
-    static const int NOT_A_CODE;
-
     /////////////////////////////////////////
     // Defined in proximity_info_state.cpp //
     /////////////////////////////////////////
@@ -196,6 +193,7 @@
             const int from, const int to, const int keyId, const bool extend) const;
 
     bool isKeyInSerchKeysAfterIndex(const int index, const int keyId) const;
+
  private:
     DISALLOW_COPY_AND_ASSIGN(ProximityInfoState);
     /////////////////////////////////////////
diff --git a/native/jni/src/proximity_info_state_utils.cpp b/native/jni/src/proximity_info_state_utils.cpp
index 9f85743..ac74a4e 100644
--- a/native/jni/src/proximity_info_state_utils.cpp
+++ b/native/jni/src/proximity_info_state_utils.cpp
@@ -211,7 +211,7 @@
                             ProximityInfoParams::NOT_A_DISTANCE_FLOAT;
             if (squaredDistance >= 0.0f) {
                 normalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] =
-                        (int) (squaredDistance
+                        static_cast<int>(squaredDistance
                                 * ProximityInfoParams::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR);
             } else {
                 normalizedSquaredDistances[i * MAX_PROXIMITY_CHARS_SIZE + j] =
diff --git a/native/jni/src/proximity_info_utils.h b/native/jni/src/proximity_info_utils.h
index 24917d8..c50df57 100644
--- a/native/jni/src/proximity_info_utils.h
+++ b/native/jni/src/proximity_info_utils.h
@@ -49,9 +49,9 @@
             const int *const inputXCoordinates, const int *const inputYCoordinates,
             const int inputSize, const int *const keyXCoordinates,
             const int *const keyYCoordinates, const int *const keyWidths, const int *keyHeights,
-            const int *const proximityCharsArray, const int maxProximityCharsSize,
-            const int cellHeight, const int cellWidth, const int gridWidth,
-            const int mostCommonKeyWidth, const int keyCount, const char *const localeStr,
+            const int *const proximityCharsArray, const int cellHeight, const int cellWidth,
+            const int gridWidth, const int mostCommonKeyWidth, const int keyCount,
+            const char *const localeStr,
             const hash_map_compat<int, int> *const codeToKeyMap, int *inputProximities) {
         // Initialize
         // - mInputCodes
@@ -63,9 +63,8 @@
             const int y = inputYCoordinates[i];
             int *proximities = &inputProximities[i * MAX_PROXIMITY_CHARS_SIZE];
             calculateProximities(keyXCoordinates, keyYCoordinates, keyWidths, keyHeights,
-                    proximityCharsArray, maxProximityCharsSize, cellHeight, cellWidth, gridWidth,
-                    mostCommonKeyWidth, keyCount, x, y, primaryKey, localeStr, codeToKeyMap,
-                    proximities);
+                    proximityCharsArray, cellHeight, cellWidth, gridWidth, mostCommonKeyWidth,
+                    keyCount, x, y, primaryKey, localeStr, codeToKeyMap, proximities);
         }
 
         if (DEBUG_PROXIMITY_CHARS) {
@@ -81,10 +80,9 @@
         }
     }
 
-    static AK_FORCE_INLINE int getStartIndexFromCoordinates(const int maxProximityCharsSize,
-            const int x, const int y, const int cellHeight, const int cellWidth,
-            const int gridWidth) {
-        return ((y / cellHeight) * gridWidth + (x / cellWidth)) * maxProximityCharsSize;
+    static AK_FORCE_INLINE int getStartIndexFromCoordinates(const int x, const int y,
+            const int cellHeight, const int cellWidth, const int gridWidth) {
+        return ((y / cellHeight) * gridWidth + (x / cellWidth)) * MAX_PROXIMITY_CHARS_SIZE;
     }
 
     static inline float getSquaredDistanceFloat(const float x1, const float y1, const float x2,
@@ -153,21 +151,18 @@
         return left < right && top < bottom && x >= left && x < right && y >= top && y < bottom;
     }
 
-    static void calculateProximities(
-            const int *const keyXCoordinates, const int *const keyYCoordinates,
-            const int *const keyWidths, const int *keyHeights,
-            const int *const proximityCharsArray,
-            const int maxProximityCharsSize, const int cellHeight, const int cellWidth,
+    static void calculateProximities(const int *const keyXCoordinates,
+            const int *const keyYCoordinates, const int *const keyWidths, const int *keyHeights,
+            const int *const proximityCharsArray, const int cellHeight, const int cellWidth,
             const int gridWidth, const int mostCommonKeyWidth, const int keyCount,
             const int x, const int y, const int primaryKey, const char *const localeStr,
             const hash_map_compat<int, int> *const codeToKeyMap, int *proximities) {
         const int mostCommonKeyWidthSquare = mostCommonKeyWidth * mostCommonKeyWidth;
         int insertPos = 0;
         proximities[insertPos++] = primaryKey;
-        const int startIndex = getStartIndexFromCoordinates(
-                maxProximityCharsSize, x, y, cellHeight, cellWidth, gridWidth);
+        const int startIndex = getStartIndexFromCoordinates(x, y, cellHeight, cellWidth, gridWidth);
         if (startIndex >= 0) {
-            for (int i = 0; i < maxProximityCharsSize; ++i) {
+            for (int i = 0; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
                 const int c = proximityCharsArray[startIndex + i];
                 if (c < KEYCODE_SPACE || c == primaryKey) {
                     continue;
@@ -179,7 +174,7 @@
                         keyWidths, keyHeights, keyIndex, x, y);
                 if (onKey || distance < mostCommonKeyWidthSquare) {
                     proximities[insertPos++] = c;
-                    if (insertPos >= maxProximityCharsSize) {
+                    if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
                         if (DEBUG_DICT) {
                             ASSERT(false);
                         }
@@ -191,7 +186,7 @@
                     AdditionalProximityChars::getAdditionalCharsSize(localeStr, primaryKey);
             if (additionalProximitySize > 0) {
                 proximities[insertPos++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE;
-                if (insertPos >= maxProximityCharsSize) {
+                if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
                     if (DEBUG_DICT) {
                         ASSERT(false);
                     }
@@ -212,7 +207,7 @@
                         continue;
                     }
                     proximities[insertPos++] = ac;
-                    if (insertPos >= maxProximityCharsSize) {
+                    if (insertPos >= MAX_PROXIMITY_CHARS_SIZE) {
                         if (DEBUG_DICT) {
                             ASSERT(false);
                         }
@@ -222,7 +217,7 @@
             }
         }
         // Add a delimiter for the proximity characters
-        for (int i = insertPos; i < maxProximityCharsSize; ++i) {
+        for (int i = insertPos; i < MAX_PROXIMITY_CHARS_SIZE; ++i) {
             proximities[i] = NOT_A_CODE_POINT;
         }
     }
diff --git a/native/jni/src/suggest_utils.h b/native/jni/src/suggest_utils.h
index 7d49cde..aab9f7b 100644
--- a/native/jni/src/suggest_utils.h
+++ b/native/jni/src/suggest_utils.h
@@ -19,7 +19,6 @@
 
 #include "defines.h"
 #include "proximity_info_params.h"
-#include "proximity_info_state.h"
 
 namespace latinime {
 class SuggestUtils {
diff --git a/native/jni/src/words_priority_queue.h b/native/jni/src/words_priority_queue.h
index 7aab1e0..8a22f05 100644
--- a/native/jni/src/words_priority_queue.h
+++ b/native/jni/src/words_priority_queue.h
@@ -87,7 +87,7 @@
         }
     }
 
-    SuggestedWord *top() {
+    SuggestedWord *top() const {
         if (mSuggestions.empty()) return 0;
         SuggestedWord *sw = mSuggestions.top();
         return sw;
@@ -110,7 +110,7 @@
         }
     }
 
-    AK_FORCE_INLINE void dumpTopWord() {
+    AK_FORCE_INLINE void dumpTopWord() const {
         if (size() <= 0) {
             return;
         }
@@ -118,7 +118,7 @@
     }
 
     AK_FORCE_INLINE float getHighestNormalizedScore(const int *before, const int beforeLength,
-            int **outWord, int *outScore, int *outLength) {
+            int **outWord, int *outScore, int *outLength) const {
         if (!mHighestSuggestedWord) {
             return 0.0f;
         }
@@ -137,7 +137,7 @@
         }
     };
 
-    SuggestedWord *getFreeSuggestedWord(int score, int *word, int wordLength, int type) {
+    SuggestedWord *getFreeSuggestedWord(int score, int *word, int wordLength, int type) const {
         for (int i = 0; i < MAX_WORD_LENGTH; ++i) {
             if (!mSuggestedWords[i].mUsed) {
                 mSuggestedWords[i].setParams(score, word, wordLength, type);
diff --git a/native/jni/src/words_priority_queue_pool.h b/native/jni/src/words_priority_queue_pool.h
index cfe7ede..2cd210a 100644
--- a/native/jni/src/words_priority_queue_pool.h
+++ b/native/jni/src/words_priority_queue_pool.h
@@ -44,11 +44,11 @@
         }
     }
 
-    WordsPriorityQueue *getMasterQueue() {
+    WordsPriorityQueue *getMasterQueue() const {
         return mMasterQueue;
     }
 
-    WordsPriorityQueue *getSubQueue(const int wordIndex, const int inputWordLength) {
+    WordsPriorityQueue *getSubQueue(const int wordIndex, const int inputWordLength) const {
         if (wordIndex >= MULTIPLE_WORDS_SUGGESTION_MAX_WORDS) {
             return 0;
         }
@@ -77,7 +77,7 @@
         }
     }
 
-    void dumpSubQueue1TopSuggestions() {
+    void dumpSubQueue1TopSuggestions() const {
         AKLOGI("DUMP SUBQUEUE1 TOP SUGGESTIONS");
         for (int i = 0; i < SUB_QUEUE_MAX_COUNT; ++i) {
             getSubQueue(0, i)->dumpTopWord();