Merge "Add ZWNJ and ZWJ icons"
diff --git a/java/res/values-land/dimens.xml b/java/res/values-land/dimens.xml
index 550d29f..1157b27 100644
--- a/java/res/values-land/dimens.xml
+++ b/java/res/values-land/dimens.xml
@@ -24,8 +24,7 @@
     <dimen name="keyboardHeight">176.0dp</dimen>
     <fraction name="minKeyboardHeight">45%p</fraction>
     <!-- key_height + key_bottom_gap = popup_key_height -->
-<!--    <dimen name="key_height">0.260in</dimen>-->
-    <dimen name="popup_key_height">0.280in</dimen>
+    <dimen name="popup_key_height">44.8dp</dimen>
 
     <fraction name="keyboard_top_padding">1.818%p</fraction>
     <fraction name="keyboard_bottom_padding">0.0%p</fraction>
@@ -54,11 +53,11 @@
     <fraction name="key_uppercase_letter_ratio">40%</fraction>
     <fraction name="key_preview_text_ratio">90%</fraction>
     <fraction name="spacebar_text_ratio">40.000%</fraction>
-    <dimen name="key_preview_offset">0.08in</dimen>
+    <dimen name="key_preview_offset">12.8dp</dimen>
 
-    <dimen name="key_preview_offset_ics">0.01in</dimen>
+    <dimen name="key_preview_offset_ics">1.6dp</dimen>
     <!-- popup_key_height x -0.5 -->
-    <dimen name="more_keys_keyboard_vertical_correction_ics">-0.140in</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction_ics">-22.4dp</dimen>
 
     <dimen name="suggestions_strip_height">36dp</dimen>
     <dimen name="more_suggestions_row_height">36dp</dimen>
@@ -66,7 +65,7 @@
     <fraction name="min_more_suggestions_width">60%</fraction>
     <!-- Amount of allowance for selecting keys in a mini popup keyboard by sliding finger. -->
     <!-- popup_key_height x 1.2 -->
-    <dimen name="more_keys_keyboard_slide_allowance">0.336in</dimen>
+    <dimen name="more_keys_keyboard_slide_allowance">53.76dp</dimen>
     <!-- popup_key_height x -1.0 -->
-    <dimen name="more_keys_keyboard_vertical_correction">-0.280in</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction">-44.8dp</dimen>
 </resources>
diff --git a/java/res/values-sw600dp-land/dimens.xml b/java/res/values-sw600dp-land/dimens.xml
index c6c6f2b..8a59c9b 100644
--- a/java/res/values-sw600dp-land/dimens.xml
+++ b/java/res/values-sw600dp-land/dimens.xml
@@ -38,7 +38,7 @@
     <fraction name="key_bottom_gap_ics">4.0%p</fraction>
     <fraction name="keyboard_bottom_padding_ics">0.0%p</fraction>
 
-    <dimen name="popup_key_height">13.0mm</dimen>
+    <dimen name="popup_key_height">81.9dp</dimen>
 
     <!-- left or right padding of label alignment -->
     <dimen name="key_label_horizontal_padding">18dp</dimen>
@@ -51,7 +51,7 @@
     <fraction name="key_uppercase_letter_ratio">29%</fraction>
     <fraction name="spacebar_text_ratio">33.33%</fraction>
 
-    <dimen name="suggestions_strip_padding">40.0mm</dimen>
+    <dimen name="suggestions_strip_padding">252.0dp</dimen>
     <integer name="max_more_suggestions_row">5</integer>
     <fraction name="min_more_suggestions_width">50%</fraction>
 </resources>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index ebe3882..f03ce29 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -25,7 +25,7 @@
     <fraction name="maxKeyboardHeight">50%p</fraction>
     <fraction name="minKeyboardHeight">-35.0%p</fraction>
 
-    <dimen name="popup_key_height">10.0mm</dimen>
+    <dimen name="popup_key_height">63.0dp</dimen>
 
     <fraction name="keyboard_top_padding">2.291%p</fraction>
     <fraction name="keyboard_bottom_padding">0.0%p</fraction>
@@ -44,9 +44,9 @@
     <dimen name="more_keys_keyboard_key_horizontal_padding">6dp</dimen>
     <!-- Amount of allowance for selecting keys in a mini popup keyboard by sliding finger. -->
     <!-- popup_key_height x 1.2 -->
-    <dimen name="more_keys_keyboard_slide_allowance">15.6mm</dimen>
+    <dimen name="more_keys_keyboard_slide_allowance">98.3dp</dimen>
     <!-- popup_key_height x -1.0 -->
-    <dimen name="more_keys_keyboard_vertical_correction">-13.0mm</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction">-81.9dp</dimen>
 
     <!-- left or right padding of label alignment -->
     <dimen name="key_label_horizontal_padding">6dp</dimen>
@@ -61,19 +61,19 @@
     <fraction name="key_uppercase_letter_ratio">26%</fraction>
     <fraction name="key_preview_text_ratio">50%</fraction>
     <fraction name="spacebar_text_ratio">32.14%</fraction>
-    <dimen name="key_preview_height">15.0mm</dimen>
-    <dimen name="key_preview_offset">0.1in</dimen>
+    <dimen name="key_preview_height">94.5dp</dimen>
+    <dimen name="key_preview_offset">16.0dp</dimen>
 
-    <dimen name="key_preview_offset_ics">0.05in</dimen>
+    <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
-    <dimen name="more_keys_keyboard_vertical_correction_ics">-5mm</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction_ics">-31.5dp</dimen>
 
     <dimen name="suggestions_strip_height">44dp</dimen>
     <dimen name="more_suggestions_row_height">44dp</dimen>
     <integer name="max_more_suggestions_row">6</integer>
     <fraction name="min_more_suggestions_width">90%</fraction>
-    <dimen name="suggestions_strip_padding">15.0mm</dimen>
-    <dimen name="suggestion_min_width">0.3in</dimen>
+    <dimen name="suggestions_strip_padding">94.5dp</dimen>
+    <dimen name="suggestion_min_width">48.0dp</dimen>
     <dimen name="suggestion_padding">12dp</dimen>
     <dimen name="suggestion_text_size">22dp</dimen>
     <dimen name="more_suggestions_hint_text_size">33dp</dimen>
diff --git a/java/res/values-sw768dp-land/dimens.xml b/java/res/values-sw768dp-land/dimens.xml
index 597ed51..b95c858 100644
--- a/java/res/values-sw768dp-land/dimens.xml
+++ b/java/res/values-sw768dp-land/dimens.xml
@@ -41,7 +41,7 @@
     <fraction name="key_bottom_gap_ics">3.690%p</fraction>
     <fraction name="key_horizontal_gap_ics">1.030%p</fraction>
 
-    <dimen name="popup_key_height">13.0mm</dimen>
+    <dimen name="popup_key_height">81.9dp</dimen>
 
     <!-- left or right padding of label alignment -->
     <dimen name="key_label_horizontal_padding">18dp</dimen>
@@ -53,10 +53,10 @@
     <fraction name="key_hint_label_ratio">28%</fraction>
     <fraction name="key_uppercase_letter_ratio">24%</fraction>
     <fraction name="spacebar_text_ratio">24.00%</fraction>
-    <dimen name="key_preview_height">17.0mm</dimen>
+    <dimen name="key_preview_height">107.1dp</dimen>
 
-    <dimen name="key_preview_offset_ics">0.05in</dimen>
+    <dimen name="key_preview_offset_ics">8.0dp</dimen>
 
-    <dimen name="suggestions_strip_padding">40.0mm</dimen>
+    <dimen name="suggestions_strip_padding">252.0dp</dimen>
     <fraction name="min_more_suggestions_width">50%</fraction>
 </resources>
diff --git a/java/res/values-sw768dp/dimens.xml b/java/res/values-sw768dp/dimens.xml
index a9f0c00..0a362fd 100644
--- a/java/res/values-sw768dp/dimens.xml
+++ b/java/res/values-sw768dp/dimens.xml
@@ -41,14 +41,14 @@
     <fraction name="key_bottom_gap_ics">3.312%p</fraction>
     <fraction name="key_horizontal_gap_ics">1.066%p</fraction>
 
-    <dimen name="popup_key_height">10.0mm</dimen>
+    <dimen name="popup_key_height">63.0dp</dimen>
 
     <dimen name="more_keys_keyboard_key_horizontal_padding">12dp</dimen>
     <!-- Amount of allowance for selecting keys in a mini popup keyboard by sliding finger. -->
     <!-- popup_key_height x 1.2 -->
-    <dimen name="more_keys_keyboard_slide_allowance">15.6mm</dimen>
+    <dimen name="more_keys_keyboard_slide_allowance">98.3dp</dimen>
     <!-- popup_key_height x -1.0 -->
-    <dimen name="more_keys_keyboard_vertical_correction">-13.0mm</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction">-81.9dp</dimen>
 
     <!-- left or right padding of label alignment -->
     <dimen name="key_label_horizontal_padding">6dp</dimen>
@@ -63,18 +63,18 @@
     <fraction name="key_uppercase_letter_ratio">26%</fraction>
     <fraction name="key_preview_text_ratio">50%</fraction>
     <fraction name="spacebar_text_ratio">29.03%</fraction>
-    <dimen name="key_preview_height">15.0mm</dimen>
-    <dimen name="key_preview_offset">0.1in</dimen>
+    <dimen name="key_preview_height">94.5dp</dimen>
+    <dimen name="key_preview_offset">16.0dp</dimen>
 
-    <dimen name="key_preview_offset_ics">0.05in</dimen>
+    <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
-    <dimen name="more_keys_keyboard_vertical_correction_ics">-5mm</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction_ics">-31.5dp</dimen>
 
     <dimen name="suggestions_strip_height">44dp</dimen>
     <dimen name="more_suggestions_row_height">44dp</dimen>
     <integer name="max_more_suggestions_row">6</integer>
     <fraction name="min_more_suggestions_width">90%</fraction>
-    <dimen name="suggestions_strip_padding">15.0mm</dimen>
+    <dimen name="suggestions_strip_padding">94.5dp</dimen>
     <dimen name="suggestion_min_width">46dp</dimen>
     <dimen name="suggestion_padding">8dp</dimen>
     <dimen name="suggestion_text_size">22dp</dimen>
diff --git a/java/res/values/config.xml b/java/res/values/config.xml
index 1aa0dff..f0b12e9 100644
--- a/java/res/values/config.xml
+++ b/java/res/values/config.xml
@@ -54,9 +54,9 @@
     <!--
          Configuration for LatinKeyboardView
     -->
-    <dimen name="config_key_hysteresis_distance">0.05in</dimen>
+    <dimen name="config_key_hysteresis_distance">8.0dp</dimen>
     <integer name="config_touch_noise_threshold_time">40</integer>
-    <dimen name="config_touch_noise_threshold_distance">2.0mm</dimen>
+    <dimen name="config_touch_noise_threshold_distance">12.6dp</dimen>
     <bool name="config_sliding_key_input_enabled">true</bool>
     <integer name="config_key_repeat_start_timeout">400</integer>
     <integer name="config_key_repeat_interval">50</integer>
diff --git a/java/res/values/dimens.xml b/java/res/values/dimens.xml
index 9d64a61..1889758 100644
--- a/java/res/values/dimens.xml
+++ b/java/res/values/dimens.xml
@@ -25,7 +25,7 @@
     <fraction name="maxKeyboardHeight">50%p</fraction>
     <fraction name="minKeyboardHeight">-61.8%p</fraction>
 
-    <dimen name="popup_key_height">0.330in</dimen>
+    <dimen name="popup_key_height">52.8dp</dimen>
 
     <dimen name="more_keys_keyboard_horizontal_edges_padding">16dp</dimen>
     <dimen name="more_keys_keyboard_key_horizontal_padding">8dp</dimen>
@@ -52,12 +52,10 @@
 
     <!-- Amount of allowance for selecting keys in a mini popup keyboard by sliding finger. -->
     <!-- popup_key_height x 1.2 -->
-    <dimen name="more_keys_keyboard_slide_allowance">0.396in</dimen>
+    <dimen name="more_keys_keyboard_slide_allowance">63.36dp</dimen>
     <!-- popup_key_height x -1.0 -->
-    <dimen name="more_keys_keyboard_vertical_correction">-0.330in</dimen>
-    <!-- We use "inch", not "dip" because this value tries dealing with physical distance related
-         to user's finger. -->
-    <dimen name="keyboard_vertical_correction">0.0in</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction">-52.8dp</dimen>
+    <dimen name="keyboard_vertical_correction">0.0dp</dimen>
 
     <fraction name="key_letter_ratio">55%</fraction>
     <fraction name="key_large_letter_ratio">65%</fraction>
@@ -68,23 +66,23 @@
     <fraction name="key_preview_text_ratio">82%</fraction>
     <fraction name="spacebar_text_ratio">33.735%</fraction>
     <dimen name="key_preview_height">80dp</dimen>
-    <dimen name="key_preview_offset">0.1in</dimen>
+    <dimen name="key_preview_offset">16.0dp</dimen>
 
     <dimen name="key_label_horizontal_padding">4dp</dimen>
     <dimen name="key_hint_letter_padding">1dp</dimen>
     <dimen name="key_popup_hint_letter_padding">2dp</dimen>
     <dimen name="key_uppercase_letter_padding">2dp</dimen>
 
-    <dimen name="key_preview_offset_ics">0.05in</dimen>
+    <dimen name="key_preview_offset_ics">8.0dp</dimen>
     <!-- popup_key_height x -0.5 -->
-    <dimen name="more_keys_keyboard_vertical_correction_ics">-0.165in</dimen>
+    <dimen name="more_keys_keyboard_vertical_correction_ics">-26.4dp</dimen>
 
     <dimen name="suggestions_strip_height">40dp</dimen>
     <dimen name="more_suggestions_key_horizontal_padding">12dp</dimen>
     <dimen name="more_suggestions_row_height">40dp</dimen>
     <dimen name="more_suggestions_bottom_gap">6dp</dimen>
-    <dimen name="more_suggestions_modal_tolerance">0.2in</dimen>
-    <dimen name="more_suggestions_slide_allowance">0.1in</dimen>
+    <dimen name="more_suggestions_modal_tolerance">32.0dp</dimen>
+    <dimen name="more_suggestions_slide_allowance">16.0dp</dimen>
     <integer name="max_more_suggestions_row">6</integer>
     <fraction name="min_more_suggestions_width">90%</fraction>
     <fraction name="more_suggestions_info_ratio">18%</fraction>
diff --git a/java/res/xml-sw600dp/rowkeys_thai3.xml b/java/res/xml-sw600dp/rowkeys_thai3.xml
index 529d7bf..abd6763 100644
--- a/java/res/xml-sw600dp/rowkeys_thai3.xml
+++ b/java/res/xml-sw600dp/rowkeys_thai3.xml
@@ -82,13 +82,13 @@
                 latin:keyLabel="&#x0E48;" />
             <!-- U+0E32: "า" THAI CHARACTER SARA AA -->
             <Key
-                latin:keyLabel="&#x0E46;" />
+                latin:keyLabel="&#x0E32;" />
             <!-- U+0E2A: "ส" THAI CHARACTER SO SUA -->
             <Key
-                latin:keyLabel="&#x0E46;" />
+                latin:keyLabel="&#x0E2A;" />
             <!-- U+0E27: "ว" THAI CHARACTER WO WAEN -->
             <Key
-                latin:keyLabel="&#x0E2F;" />
+                latin:keyLabel="&#x0E27;" />
             <!-- U+0E07: "ง" THAI CHARACTER NGO NGU -->
             <Key
                 latin:keyLabel="&#x0E07;" />
diff --git a/java/res/xml/rowkeys_thai2.xml b/java/res/xml/rowkeys_thai2.xml
index a5db665..02ea6c5 100644
--- a/java/res/xml/rowkeys_thai2.xml
+++ b/java/res/xml/rowkeys_thai2.xml
@@ -46,9 +46,11 @@
             <!-- U+0E0B: "ซ" THAI CHARACTER SO SO -->
             <Key
                 latin:keyLabel="&#x0E0B;" />
-            <!-- U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT -->
+            <!-- U+0E3F: "฿" THAI CURRENCY SYMBOL BAHT
+                 U+0E45: "ๅ" THAI CHARACTER LAKKHANGYAO -->
             <Key
-                latin:keyLabel="&#x0E3F;" />
+                latin:keyLabel="&#x0E3F;"
+                latin:moreKeys="&#x0E45;" />
             <!-- U+0E46: "ๆ" THAI CHARACTER MAIYAMOK
                  U+0E2F: "ฯ" THAI CHARACTER PAIYANNOI -->
             <Key
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 07b9c1e..9623790 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -394,7 +394,7 @@
      *     &gt;Row row_attributes*&lt;
      *       &gt;!-- Row Content --&lt;
      *       &gt;Key key_attributes* /&lt;
-     *       &gt;Spacer horizontalGap="0.2in" /&lt;
+     *       &gt;Spacer horizontalGap="32.0dp" /&lt;
      *       &gt;include keyboardLayout="@xml/other_keys"&lt;
      *       ...
      *     &gt;/Row&lt;
diff --git a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
index b66d166..3f6c374 100644
--- a/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/LatinKeyboardView.java
@@ -46,6 +46,7 @@
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.ResearchLogger;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
 import com.android.inputmethod.latin.StringUtils;
 import com.android.inputmethod.latin.SubtypeUtils;
@@ -66,6 +67,9 @@
         SuddenJumpingTouchEventHandler.ProcessMotionEvent {
     private static final String TAG = LatinKeyboardView.class.getSimpleName();
 
+    // TODO: Kill process when the usability study mode was changed.
+    private static final boolean ENABLE_USABILITY_STUDY_LOG = LatinImeLogger.sUsabilityStudy;
+
     /** Listener for {@link KeyboardActionListener}. */
     private KeyboardActionListener mKeyboardActionListener;
 
@@ -653,8 +657,6 @@
         final int index = me.getActionIndex();
         final int id = me.getPointerId(index);
         final int x, y;
-        final float size = me.getSize(index);
-        final float pressure = me.getPressure(index);
         if (mMoreKeysPanel != null && id == mMoreKeysPanelPointerTrackerId) {
             x = mMoreKeysPanel.translateX((int)me.getX(index));
             y = mMoreKeysPanel.translateY((int)me.getY(index));
@@ -662,10 +664,44 @@
             x = (int)me.getX(index);
             y = (int)me.getY(index);
         }
-        if (LatinImeLogger.sUsabilityStudy) {
+        if (ENABLE_USABILITY_STUDY_LOG) {
+            final String eventTag;
+            switch (action) {
+                case MotionEvent.ACTION_UP:
+                    eventTag = "[Up]";
+                    break;
+                case MotionEvent.ACTION_DOWN:
+                    eventTag = "[Down]";
+                    break;
+                case MotionEvent.ACTION_POINTER_UP:
+                    eventTag = "[PointerUp]";
+                    break;
+                case MotionEvent.ACTION_POINTER_DOWN:
+                    eventTag = "[PointerDown]";
+                    break;
+                case MotionEvent.ACTION_MOVE: // Skip this as being logged below
+                    eventTag = "";
+                    break;
+                default:
+                    eventTag = "[Action" + action + "]";
+                    break;
+            }
+            if (!TextUtils.isEmpty(eventTag)) {
+                final float size = me.getSize(index);
+                final float pressure = me.getPressure(index);
+                UsabilityStudyLogUtils.getInstance().write(
+                        eventTag + eventTime + "," + id + "," + x + "," + y + ","
+                        + size + "," + pressure);
+            }
+        }
+        if (ResearchLogger.sIsLogging) {
+            // TODO: remove redundant calculations of size and pressure by
+            // removing UsabilityStudyLog code once the ResearchLogger is mature enough
+            final float size = me.getSize(index);
+            final float pressure = me.getPressure(index);
             if (action != MotionEvent.ACTION_MOVE) {
                 // Skip ACTION_MOVE events as they are logged below
-                UsabilityStudyLogUtils.getInstance().writeMotionEvent(action, eventTime, id, x,
+                ResearchLogger.getInstance().logMotionEvent(action, eventTime, id, x,
                         y, size, pressure);
             }
         }
@@ -714,8 +750,9 @@
 
         if (action == MotionEvent.ACTION_MOVE) {
             for (int i = 0; i < pointerCount; i++) {
+                final int pointerId = me.getPointerId(i);
                 final PointerTracker tracker = PointerTracker.getPointerTracker(
-                        me.getPointerId(i), this);
+                        pointerId, this);
                 final int px, py;
                 if (mMoreKeysPanel != null
                         && tracker.mPointerId == mMoreKeysPanelPointerTrackerId) {
@@ -726,9 +763,19 @@
                     py = (int)me.getY(i);
                 }
                 tracker.onMoveEvent(px, py, eventTime);
-                if (LatinImeLogger.sUsabilityStudy) {
-                    UsabilityStudyLogUtils.getInstance().writeMotionEvent(action, eventTime, id,
-                            px, py, size, pressure);
+                if (ENABLE_USABILITY_STUDY_LOG) {
+                    final float pointerSize = me.getSize(i);
+                    final float pointerPressure = me.getPressure(i);
+                    UsabilityStudyLogUtils.getInstance().write("[Move]"  + eventTime + ","
+                            + pointerId + "," + px + "," + py + ","
+                            + pointerSize + "," + pointerPressure);
+                }
+                if (ResearchLogger.sIsLogging) {
+                    // TODO: earlier comment about redundant calculations applies here too
+                    final float pointerSize = me.getSize(i);
+                    final float pointerPressure = me.getPressure(i);
+                    ResearchLogger.getInstance().logMotionEvent(action, eventTime, pointerId,
+                            px, py, pointerSize, pointerPressure);
                 }
             }
         } else {
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 175d953..7272006 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -439,6 +439,7 @@
         final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this);
         mPrefs = prefs;
         LatinImeLogger.init(this, prefs);
+        ResearchLogger.init(this, prefs);
         LanguageSwitcherProxy.init(this, prefs);
         InputMethodManagerCompatWrapper.init(this);
         SubtypeSwitcher.init(this);
@@ -528,7 +529,7 @@
         resetContactsDictionary(oldContactsDictionary);
 
         mUserHistoryDictionary
-                = new UserHistoryDictionary(this, this, localeStr, Suggest.DIC_USER_HISTORY);
+                = new UserHistoryDictionary(this, localeStr, Suggest.DIC_USER_HISTORY);
         mSuggest.setUserHistoryDictionary(mUserHistoryDictionary);
 
         LocaleUtils.setSystemLocale(res, savedLocale);
@@ -1263,8 +1264,8 @@
         }
         mLastKeyTime = when;
 
-        if (LatinImeLogger.sUsabilityStudy) {
-            UsabilityStudyLogUtils.getInstance().writeKeyEvent(primaryCode, x, y);
+        if (ResearchLogger.sIsLogging) {
+            ResearchLogger.getInstance().logKeyEvent(primaryCode, x, y);
         }
 
         final KeyboardSwitcher switcher = mKeyboardSwitcher;
@@ -2008,8 +2009,14 @@
             } else {
                 prevWord = null;
             }
+            final String secondWord;
+            if (mWordComposer.isAutoCapitalized() && !mWordComposer.isMostlyCaps()) {
+                secondWord = suggestion.toString().toLowerCase(mSubtypeSwitcher.getInputLocale());
+            } else {
+                secondWord = suggestion.toString();
+            }
             mUserHistoryDictionary.addToUserHistory(null == prevWord ? null : prevWord.toString(),
-                    suggestion.toString());
+                    secondWord);
         }
     }
 
@@ -2250,10 +2257,6 @@
         mFeedbackManager.vibrate(mKeyboardSwitcher.getKeyboardView());
     }
 
-    public boolean isAutoCapitalized() {
-        return mWordComposer.isAutoCapitalized();
-    }
-
     private void updateCorrectionMode() {
         // TODO: cleanup messy flags
         final boolean shouldAutoCorrect = mSettingsValues.mAutoCorrectEnabled
diff --git a/java/src/com/android/inputmethod/latin/ResearchLogger.java b/java/src/com/android/inputmethod/latin/ResearchLogger.java
new file mode 100644
index 0000000..6ba9118
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/ResearchLogger.java
@@ -0,0 +1,304 @@
+/*
+ * Copyright (C) 2012 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License"); you may not
+ * use this file except in compliance with the License. You may obtain a copy of
+ * the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+ * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+ * License for the specific language governing permissions and limitations under
+ * the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import android.content.SharedPreferences;
+import android.inputmethodservice.InputMethodService;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+import android.os.SystemClock;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MotionEvent;
+
+import com.android.inputmethod.keyboard.Keyboard;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.PrintWriter;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+/**
+ * Logs the use of the LatinIME keyboard.
+ *
+ * This class logs operations on the IME keyboard, including what the user has typed.
+ * Data is stored locally in a file in app-specific storage.
+ *
+ * This functionality is off by default.
+ */
+public class ResearchLogger implements SharedPreferences.OnSharedPreferenceChangeListener {
+    private static final String TAG = ResearchLogger.class.getSimpleName();
+    private static final String PREF_USABILITY_STUDY_MODE = "usability_study_mode";
+
+    private static final ResearchLogger sInstance = new ResearchLogger(new LogFileManager());
+    public static boolean sIsLogging = false;
+    private final Handler mLoggingHandler;
+    private InputMethodService mIms;
+    private final Date mDate;
+    private final SimpleDateFormat mDateFormat;
+
+    /**
+     * Isolates management of files. This variable should never be null, but can be changed
+     * to support testing.
+     */
+    private LogFileManager mLogFileManager;
+
+    /**
+     * Manages the file(s) that stores the logs.
+     *
+     * Handles creation, deletion, and provides Readers, Writers, and InputStreams to access
+     * the logs.
+     */
+    public static class LogFileManager {
+        private static final String DEFAULT_FILENAME = "log.txt";
+        private static final String DEFAULT_LOG_DIRECTORY = "researchLogger";
+
+        private static final long LOGFILE_PURGE_INTERVAL = 1000 * 60 * 60 * 24;
+
+        private InputMethodService mIms;
+        private File mFile;
+        private PrintWriter mPrintWriter;
+
+        /* package */ LogFileManager() {
+        }
+
+        public void init(InputMethodService ims) {
+            mIms = ims;
+        }
+
+        public synchronized void createLogFile() {
+            try {
+                createLogFile(DEFAULT_LOG_DIRECTORY, DEFAULT_FILENAME);
+            } catch (FileNotFoundException e) {
+                Log.w(TAG, e);
+            }
+        }
+
+        public synchronized void createLogFile(String dir, String filename)
+                throws FileNotFoundException {
+            if (mIms == null) {
+                Log.w(TAG, "InputMethodService is not configured.  Logging is off.");
+                return;
+            }
+            File filesDir = mIms.getFilesDir();
+            if (filesDir == null || !filesDir.exists()) {
+                Log.w(TAG, "Storage directory does not exist.  Logging is off.");
+                return;
+            }
+            File directory = new File(filesDir, dir);
+            if (!directory.exists()) {
+                boolean wasCreated = directory.mkdirs();
+                if (!wasCreated) {
+                    Log.w(TAG, "Log directory cannot be created.  Logging is off.");
+                    return;
+                }
+            }
+
+            close();
+            mFile = new File(directory, filename);
+            boolean append = true;
+            if (mFile.exists() && mFile.lastModified() + LOGFILE_PURGE_INTERVAL <
+                    System.currentTimeMillis()) {
+                append = false;
+            }
+            mPrintWriter = new PrintWriter(new FileOutputStream(mFile, append), true);
+        }
+
+        public synchronized boolean append(String s) {
+            if (mPrintWriter == null) {
+                Log.w(TAG, "PrintWriter is null");
+                return false;
+            } else {
+                mPrintWriter.print(s);
+                return !mPrintWriter.checkError();
+            }
+        }
+
+        public synchronized void reset() {
+            if (mPrintWriter != null) {
+                mPrintWriter.close();
+                mPrintWriter = null;
+            }
+            if (mFile != null && mFile.exists()) {
+                mFile.delete();
+                mFile = null;
+            }
+        }
+
+        public synchronized void close() {
+            if (mPrintWriter != null) {
+                mPrintWriter.close();
+                mPrintWriter = null;
+                mFile = null;
+            }
+        }
+    }
+
+    private ResearchLogger(LogFileManager logFileManager) {
+        mDate = new Date();
+        mDateFormat = new SimpleDateFormat("yyyyMMdd-HHmmss.SSSZ");
+
+        HandlerThread handlerThread = new HandlerThread("ResearchLogger logging task",
+                Process.THREAD_PRIORITY_BACKGROUND);
+        handlerThread.start();
+        mLoggingHandler = new Handler(handlerThread.getLooper());
+        mLogFileManager = logFileManager;
+    }
+
+    public static ResearchLogger getInstance() {
+        return sInstance;
+    }
+
+    public static void init(InputMethodService ims, SharedPreferences prefs) {
+        sInstance.initInternal(ims, prefs);
+    }
+
+    public void initInternal(InputMethodService ims, SharedPreferences prefs) {
+        mIms = ims;
+        if (mLogFileManager != null) {
+            mLogFileManager.init(ims);
+            mLogFileManager.createLogFile();
+        }
+        if (prefs != null) {
+            sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+        }
+        prefs.registerOnSharedPreferenceChangeListener(this);
+    }
+
+    /**
+     * Change to a different logFileManager.  Will not allow it to be set to null.
+     */
+    /* package */ void setLogFileManager(ResearchLogger.LogFileManager manager) {
+        if (manager == null) {
+            Log.w(TAG, "warning: trying to set null logFileManager.  ignoring.");
+        } else {
+            mLogFileManager = manager;
+        }
+    }
+
+    /**
+     * Represents a category of logging events that share the same subfield structure.
+     */
+    private static enum LogGroup {
+        MOTION_EVENT("m"),
+        KEY("k"),
+        CORRECTION("c"),
+        STATE_CHANGE("s");
+
+        private final String mLogString;
+
+        private LogGroup(String logString) {
+            mLogString = logString;
+        }
+    }
+
+    public void logMotionEvent(final int action, final long eventTime, final int id,
+            final int x, final int y, final float size, final float pressure) {
+        final String eventTag;
+        switch (action) {
+            case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
+            case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
+            case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
+            case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
+            case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
+            case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
+            case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
+            default: eventTag = "[Action" + action + "]"; break;
+        }
+        if (!TextUtils.isEmpty(eventTag)) {
+            StringBuilder sb = new StringBuilder();
+            sb.append(eventTag);
+            sb.append('\t'); sb.append(eventTime);
+            sb.append('\t'); sb.append(id);
+            sb.append('\t'); sb.append(x);
+            sb.append('\t'); sb.append(y);
+            sb.append('\t'); sb.append(size);
+            sb.append('\t'); sb.append(pressure);
+            write(LogGroup.MOTION_EVENT, sb.toString());
+        }
+    }
+
+    public void logKeyEvent(int code, int x, int y) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(Keyboard.printableCode(code));
+        sb.append('\t'); sb.append(x);
+        sb.append('\t'); sb.append(y);
+        write(LogGroup.KEY, sb.toString());
+
+        LatinImeLogger.onPrintAllUsabilityStudyLogs();
+    }
+
+    public void logCorrection(String subgroup, String before, String after, int position) {
+        final StringBuilder sb = new StringBuilder();
+        sb.append(subgroup);
+        sb.append('\t'); sb.append(before);
+        sb.append('\t'); sb.append(after);
+        sb.append('\t'); sb.append(position);
+        write(LogGroup.CORRECTION, sb.toString());
+    }
+
+    public void logStateChange(String subgroup, String details) {
+        write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
+    }
+
+    private void write(final LogGroup logGroup, final String log) {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                final long currentTime = System.currentTimeMillis();
+                mDate.setTime(currentTime);
+                final long upTime = SystemClock.uptimeMillis();
+
+                final String printString = String.format("%s\t%d\t%s\t%s\n",
+                        mDateFormat.format(mDate), upTime, logGroup.mLogString, log);
+                if (LatinImeLogger.sDBG) {
+                    Log.d(TAG, "Write: " + '[' + logGroup.mLogString + ']' + log);
+                }
+                if (mLogFileManager.append(printString)) {
+                    // success
+                } else {
+                    if (LatinImeLogger.sDBG) {
+                        Log.w(TAG, "Unable to write to log.");
+                    }
+                }
+            }
+        });
+    }
+
+    public void clearAll() {
+        mLoggingHandler.post(new Runnable() {
+            @Override
+            public void run() {
+                if (LatinImeLogger.sDBG) {
+                    Log.d(TAG, "Delete log file.");
+                }
+                mLogFileManager.reset();
+            }
+        });
+    }
+
+    @Override
+    public void onSharedPreferenceChanged(SharedPreferences prefs, String key) {
+        if (key == null || prefs == null) {
+            return;
+        }
+        sIsLogging = prefs.getBoolean(PREF_USABILITY_STUDY_MODE, false);
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
index 4e79846..db2cdf9 100644
--- a/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/UserHistoryDictionary.java
@@ -75,8 +75,6 @@
     private static final String FREQ_COLUMN_PAIR_ID = "pair_id";
     private static final String FREQ_COLUMN_FREQUENCY = "freq";
 
-    private final LatinIME mIme;
-
     /** Locale for which this auto dictionary is storing words */
     private String mLocale;
 
@@ -139,9 +137,8 @@
         sDeleteHistoryBigrams = deleteHistoryBigram;
     }
 
-    public UserHistoryDictionary(Context context, LatinIME ime, String locale, int dicTypeId) {
+    public UserHistoryDictionary(final Context context, final String locale, final int dicTypeId) {
         super(context, dicTypeId);
-        mIme = ime;
         mLocale = locale;
         if (sOpenHelper == null) {
             sOpenHelper = new DatabaseHelper(getContext());
@@ -179,10 +176,6 @@
      * The second word may not be null (a NullPointerException would be thrown).
      */
     public int addToUserHistory(final String word1, String word2) {
-        // remove caps if second word is autocapitalized
-        if (mIme != null && mIme.isAutoCapitalized()) {
-            word2 = Character.toLowerCase(word2.charAt(0)) + word2.substring(1);
-        }
         super.addWord(word2, FREQUENCY_FOR_TYPED);
         // Do not insert a word as a bigram of itself
         if (word2.equals(word1)) {
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index a3589da..be64c2f 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -220,6 +220,7 @@
     }
 
     public static class UsabilityStudyLogUtils {
+        // TODO: remove code duplication with ResearchLog class
         private static final String USABILITY_TAG = UsabilityStudyLogUtils.class.getSimpleName();
         private static final String FILENAME = "log.txt";
         private static final UsabilityStudyLogUtils sInstance =
@@ -262,73 +263,28 @@
             }
         }
 
-        /**
-         * Represents a category of logging events that share the same subfield structure.
-         */
-        public static enum LogGroup {
-            MOTION_EVENT("m"),
-            KEY("k"),
-            CORRECTION("c"),
-            STATE_CHANGE("s");
-
-            private final String mLogString;
-
-            private LogGroup(String logString) {
-                mLogString = logString;
-            }
+        public static void writeBackSpace(int x, int y) {
+            UsabilityStudyLogUtils.getInstance().write("<backspace>\t" + x + "\t" + y);
         }
 
-        public void writeMotionEvent(final int action, final long eventTime, final int id,
-                final int x, final int y, final float size, final float pressure) {
-            final String eventTag;
-            switch (action) {
-                case MotionEvent.ACTION_CANCEL: eventTag = "[Cancel]"; break;
-                case MotionEvent.ACTION_UP: eventTag = "[Up]"; break;
-                case MotionEvent.ACTION_DOWN: eventTag = "[Down]"; break;
-                case MotionEvent.ACTION_POINTER_UP: eventTag = "[PointerUp]"; break;
-                case MotionEvent.ACTION_POINTER_DOWN: eventTag = "[PointerDown]"; break;
-                case MotionEvent.ACTION_MOVE: eventTag = "[Move]"; break;
-                case MotionEvent.ACTION_OUTSIDE: eventTag = "[Outside]"; break;
-                default: eventTag = "[Action" + action + "]"; break;
+        public void writeChar(char c, int x, int y) {
+            String inputChar = String.valueOf(c);
+            switch (c) {
+                case '\n':
+                    inputChar = "<enter>";
+                    break;
+                case '\t':
+                    inputChar = "<tab>";
+                    break;
+                case ' ':
+                    inputChar = "<space>";
+                    break;
             }
-            if (!TextUtils.isEmpty(eventTag)) {
-                StringBuilder sb = new StringBuilder();
-                sb.append(eventTag);
-                sb.append('\t'); sb.append(eventTime);
-                sb.append('\t'); sb.append(id);
-                sb.append('\t'); sb.append(x);
-                sb.append('\t'); sb.append(y);
-                sb.append('\t'); sb.append(size);
-                sb.append('\t'); sb.append(pressure);
-                write(LogGroup.MOTION_EVENT, sb.toString());
-            }
-        }
-
-        public void writeKeyEvent(int code, int x, int y) {
-            final StringBuilder sb = new StringBuilder();
-            sb.append(Keyboard.printableCode(code));
-            sb.append('\t'); sb.append(x);
-            sb.append('\t'); sb.append(y);
-            write(LogGroup.KEY, sb.toString());
-
-            // TODO: replace with a cleaner flush+retrieve mechanism
+            UsabilityStudyLogUtils.getInstance().write(inputChar + "\t" + x + "\t" + y);
             LatinImeLogger.onPrintAllUsabilityStudyLogs();
         }
 
-        public void writeCorrection(String subgroup, String before, String after, int position) {
-            final StringBuilder sb = new StringBuilder();
-            sb.append(subgroup);
-            sb.append('\t'); sb.append(before);
-            sb.append('\t'); sb.append(after);
-            sb.append('\t'); sb.append(position);
-            write(LogGroup.CORRECTION, sb.toString());
-        }
-
-        public void writeStateChange(String subgroup, String details) {
-            write(LogGroup.STATE_CHANGE, subgroup + "\t" + details);
-        }
-
-        private void write(final LogGroup logGroup, final String log) {
+        public void write(final String log) {
             mLoggingHandler.post(new Runnable() {
                 @Override
                 public void run() {
@@ -336,8 +292,8 @@
                     final long currentTime = System.currentTimeMillis();
                     mDate.setTime(currentTime);
 
-                    final String printString = String.format("%s\t%d\t%s\t%s\n",
-                            mDateFormat.format(mDate), currentTime, logGroup.mLogString, log);
+                    final String printString = String.format("%s\t%d\t%s\n",
+                            mDateFormat.format(mDate), currentTime, log);
                     if (LatinImeLogger.sDBG) {
                         Log.d(USABILITY_TAG, "Write: " + log);
                     }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index 973a448..cd34ba8 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -574,7 +574,12 @@
                     // The getXYForCodePointAndScript method returns (Y << 16) + X
                     final int xy = SpellCheckerProximityInfo.getXYForCodePointAndScript(
                             codePoint, mScript);
-                    composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
+                    if (SpellCheckerProximityInfo.NOT_A_COORDINATE_PAIR == xy) {
+                        composer.add(codePoint, WordComposer.NOT_A_COORDINATE,
+                                WordComposer.NOT_A_COORDINATE, null);
+                    } else {
+                        composer.add(codePoint, xy & 0xFFFF, xy >> 16, null);
+                    }
                 }
 
                 final int capitalizeType = getCapitalizationType(text);
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
index 7627700..0103e84 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/SpellCheckerProximityInfo.java
@@ -35,6 +35,9 @@
     // The number of rows in the grid used by the spell checker.
     final public static int PROXIMITY_GRID_HEIGHT = 3;
 
+    final private static int NOT_AN_INDEX = -1;
+    final public static int NOT_A_COORDINATE_PAIR = -1;
+
     // Helper methods
     final protected static void buildProximityIndices(final int[] proximity,
             final TreeMap<Integer, Integer> indices) {
@@ -45,7 +48,7 @@
     final protected static int computeIndex(final int characterCode,
             final TreeMap<Integer, Integer> indices) {
         final Integer result = indices.get(characterCode);
-        if (null == result) return -1;
+        if (null == result) return NOT_AN_INDEX;
         return result;
     }
 
@@ -196,8 +199,10 @@
     // Returns (Y << 16) + X to avoid creating a temporary object. This is okay because
     // X and Y are limited to PROXIMITY_GRID_WIDTH resp. PROXIMITY_GRID_HEIGHT which is very
     // inferior to 1 << 16
+    // As an exception, this returns NOT_A_COORDINATE_PAIR if the key is not on the grid
     public static int getXYForCodePointAndScript(final int codePoint, final int script) {
         final int index = getIndexOfCodeForScript(codePoint, script);
+        if (NOT_AN_INDEX == index) return NOT_A_COORDINATE_PAIR;
         final int y = index / PROXIMITY_GRID_WIDTH;
         final int x = index % PROXIMITY_GRID_WIDTH;
         if (y > PROXIMITY_GRID_HEIGHT) {