Merge "Convert dimension unit "in" and "mm" to "dp""
diff --git a/java/res/values-sw600dp-land/dimens.xml b/java/res/values-sw600dp-land/dimens.xml
index b4a12e3..8a59c9b 100644
--- a/java/res/values-sw600dp-land/dimens.xml
+++ b/java/res/values-sw600dp-land/dimens.xml
@@ -35,6 +35,7 @@
     <fraction name="key_bottom_gap_gb">5.200%p</fraction>
     <fraction name="key_horizontal_gap_gb">1.447%p</fraction>
 
+    <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">81.9dp</dimen>
diff --git a/java/res/values-sw600dp/dimens.xml b/java/res/values-sw600dp/dimens.xml
index 61729ce..f03ce29 100644
--- a/java/res/values-sw600dp/dimens.xml
+++ b/java/res/values-sw600dp/dimens.xml
@@ -38,6 +38,7 @@
     <fraction name="key_bottom_gap_gb">4.625%p</fraction>
     <fraction name="key_horizontal_gap_gb">2.113%p</fraction>
 
+    <fraction name="key_bottom_gap_ics">4.0%p</fraction>
     <fraction name="keyboard_bottom_padding_ics">0.0%p</fraction>
 
     <dimen name="more_keys_keyboard_key_horizontal_padding">6dp</dimen>
diff --git a/java/res/values/attrs.xml b/java/res/values/attrs.xml
index cf55fbe..e33f0ba 100644
--- a/java/res/values/attrs.xml
+++ b/java/res/values/attrs.xml
@@ -202,9 +202,10 @@
         <!-- Default height of a row (key height + vertical gap), in pixels or percentage of
              keyboard height. -->
         <attr name="rowHeight" format="dimension|fraction" />
-        <!-- Default horizontal gap between keys. -->
+        <!-- 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. -->
+        <!-- Default vertical gap between rows of keys, in pixels or percentage of keyboard
+             height. -->
         <attr name="verticalGap" format="dimension|fraction" />
         <!-- More keys keyboard layout template -->
         <attr name="moreKeysTemplate" format="reference" />
diff --git a/java/res/xml-sw600dp-land/kbd_thai.xml b/java/res/xml-sw600dp-land/kbd_thai.xml
new file mode 100644
index 0000000..ac36ea5
--- /dev/null
+++ b/java/res/xml-sw600dp-land/kbd_thai.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="3.20%p"
+>
+    <include
+        latin:keyboardLayout="@xml/rows_thai" />
+</Keyboard>
diff --git a/java/res/xml-sw600dp/kbd_thai.xml b/java/res/xml-sw600dp/kbd_thai.xml
index 06d98e1..ac36ea5 100644
--- a/java/res/xml-sw600dp/kbd_thai.xml
+++ b/java/res/xml-sw600dp/kbd_thai.xml
@@ -21,6 +21,7 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
+    latin:verticalGap="3.20%p"
 >
     <include
         latin:keyboardLayout="@xml/rows_thai" />
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-sw768dp-land/kbd_thai.xml b/java/res/xml-sw768dp-land/kbd_thai.xml
new file mode 100644
index 0000000..4bfc9cb
--- /dev/null
+++ b/java/res/xml-sw768dp-land/kbd_thai.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="2.65%p"
+>
+    <include
+        latin:keyboardLayout="@xml/rows_thai" />
+</Keyboard>
diff --git a/java/res/xml-sw768dp-land/kbd_thai_symbols.xml b/java/res/xml-sw768dp-land/kbd_thai_symbols.xml
new file mode 100644
index 0000000..a3feeaa
--- /dev/null
+++ b/java/res/xml-sw768dp-land/kbd_thai_symbols.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="2.65%p"
+>
+    <include
+        latin:keyboardLayout="@xml/rows_thai_symbols" />
+</Keyboard>
diff --git a/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml b/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml
new file mode 100644
index 0000000..8b4a8ea
--- /dev/null
+++ b/java/res/xml-sw768dp-land/kbd_thai_symbols_shift.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="2.65%p"
+>
+    <include
+        latin:keyboardLayout="@xml/rows_thai_symbols_shift" />
+</Keyboard>
diff --git a/java/res/xml-sw768dp/kbd_thai.xml b/java/res/xml-sw768dp/kbd_thai.xml
new file mode 100644
index 0000000..dd0ac36
--- /dev/null
+++ b/java/res/xml-sw768dp/kbd_thai.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+
+<Keyboard
+    xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
+    latin:rowHeight="20%p"
+    latin:verticalGap="2.95%p"
+>
+    <include
+        latin:keyboardLayout="@xml/rows_thai" />
+</Keyboard>
diff --git a/java/res/xml-sw768dp/kbd_thai_symbols.xml b/java/res/xml-sw768dp/kbd_thai_symbols.xml
index f020334..91cf808 100644
--- a/java/res/xml-sw768dp/kbd_thai_symbols.xml
+++ b/java/res/xml-sw768dp/kbd_thai_symbols.xml
@@ -21,6 +21,7 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
+    latin:verticalGap="2.95%p"
 >
     <include
         latin:keyboardLayout="@xml/rows_thai_symbols" />
diff --git a/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml b/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
index af24da0..85745ac 100644
--- a/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
+++ b/java/res/xml-sw768dp/kbd_thai_symbols_shift.xml
@@ -21,6 +21,7 @@
 <Keyboard
     xmlns:latin="http://schemas.android.com/apk/res/com.android.inputmethod.latin"
     latin:rowHeight="20%p"
+    latin:verticalGap="2.95%p"
 >
     <include
         latin:keyboardLayout="@xml/rows_thai_symbols_shift" />
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/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 7903820..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);
@@ -503,7 +504,7 @@
 
     private void initSuggest() {
         final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
-        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
+        final Locale keyboardLocale = mSubtypeSwitcher.getInputLocale();
 
         final Resources res = mResources;
         final Locale savedLocale = LocaleUtils.setSystemLocale(res, keyboardLocale);
@@ -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);
@@ -567,8 +568,7 @@
     }
 
     /* package private */ void resetSuggestMainDict() {
-        final String localeStr = mSubtypeSwitcher.getInputLocaleStr();
-        final Locale keyboardLocale = LocaleUtils.constructLocaleFromString(localeStr);
+        final Locale keyboardLocale = mSubtypeSwitcher.getInputLocale();
         int mainDicResId = DictionaryFactory.getMainDictionaryResourceId(mResources);
         mSuggest.resetMainDict(this, mainDicResId, keyboardLocale);
     }
@@ -1007,10 +1007,6 @@
             final int touchHeight = inputView.getHeight() + extraHeight
                     // Extend touchable region below the keyboard.
                     + EXTENDED_TOUCHABLE_REGION_HEIGHT;
-            if (DEBUG) {
-                Log.d(TAG, "Touchable region: y=" + touchY + " width=" + touchWidth
-                        + " height=" + touchHeight);
-            }
             setTouchableRegionCompat(outInsets, 0, touchY, touchWidth, touchHeight);
         }
         outInsets.contentTopInsets = touchY;
@@ -1268,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;
@@ -1995,7 +1991,7 @@
     }
 
     private void addToUserHistoryDictionary(final CharSequence suggestion) {
-        if (suggestion == null || suggestion.length() < 1) return;
+        if (TextUtils.isEmpty(suggestion)) return;
 
         // Only auto-add to dictionary if auto-correct is ON. Otherwise we'll be
         // adding words in situations where the user or application really didn't
@@ -2013,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);
         }
     }
 
@@ -2255,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/SubtypeSwitcher.java b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
index ffbbf9b..3524c72 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeSwitcher.java
@@ -47,13 +47,13 @@
     private static final String TAG = SubtypeSwitcher.class.getSimpleName();
 
     public static final String KEYBOARD_MODE = "keyboard";
-    private static final char LOCALE_SEPARATER = '_';
+    private static final char LOCALE_SEPARATOR = '_';
     private static final String VOICE_MODE = "voice";
     private static final String SUBTYPE_EXTRAVALUE_REQUIRE_NETWORK_CONNECTIVITY =
             "requireNetworkConnectivity";
 
     private final TextUtils.SimpleStringSplitter mLocaleSplitter =
-            new TextUtils.SimpleStringSplitter(LOCALE_SEPARATER);
+            new TextUtils.SimpleStringSplitter(LOCALE_SEPARATOR);
 
     private static final SubtypeSwitcher sInstance = new SubtypeSwitcher();
     private /* final */ LatinIME mService;
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/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index 70530c3..64fcd7f 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -59,7 +59,7 @@
      */
     public static class WeightedString {
         final String mWord;
-        final int mFrequency;
+        int mFrequency;
         public WeightedString(String word, int frequency) {
             mWord = word;
             mFrequency = frequency;
@@ -94,10 +94,10 @@
     public static class CharGroup {
         public static final int NOT_A_TERMINAL = -1;
         final int mChars[];
-        final ArrayList<WeightedString> mShortcutTargets;
-        final ArrayList<WeightedString> mBigrams;
-        final int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
-        final boolean mIsShortcutOnly; // Only valid if this is a terminal.
+        ArrayList<WeightedString> mShortcutTargets;
+        ArrayList<WeightedString> mBigrams;
+        int mFrequency; // NOT_A_TERMINAL == mFrequency indicates this is not a terminal.
+        boolean mIsShortcutOnly; // Only valid if this is a terminal.
         Node mChildren;
         // The two following members to help with binary generation
         int mCachedSize;
@@ -146,6 +146,102 @@
             assert(mChars.length > 0);
             return 1 < mChars.length;
         }
+
+        /**
+         * Adds a word to the bigram list. Updates the frequency if the word already
+         * exists.
+         */
+        public void addBigram(final String word, final int frequency) {
+            if (mBigrams == null) {
+                mBigrams = new ArrayList<WeightedString>();
+            }
+            WeightedString bigram = getBigram(word);
+            if (bigram != null) {
+                bigram.mFrequency = frequency;
+            } else {
+                bigram = new WeightedString(word, frequency);
+                mBigrams.add(bigram);
+            }
+        }
+
+        /**
+         * Gets the shortcut target for the given word. Returns null if the word is not in the
+         * shortcut list.
+         */
+        public WeightedString getShortcut(final String word) {
+            if (mShortcutTargets != null) {
+                final int size = mShortcutTargets.size();
+                for (int i = 0; i < size; ++i) {
+                    WeightedString shortcut = mShortcutTargets.get(i);
+                    if (shortcut.mWord.equals(word)) {
+                        return shortcut;
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Gets the bigram for the given word.
+         * Returns null if the word is not in the bigrams list.
+         */
+        public WeightedString getBigram(final String word) {
+            if (mBigrams != null) {
+                final int size = mBigrams.size();
+                for (int i = 0; i < size; ++i) {
+                    WeightedString bigram = mBigrams.get(i);
+                    if (bigram.mWord.equals(word)) {
+                        return bigram;
+                    }
+                }
+            }
+            return null;
+        }
+
+        /**
+         * Updates the CharGroup with the given properties. Adds the shortcut and bigram lists to
+         * the existing ones if any. Note: unigram, bigram, and shortcut frequencies are only
+         * updated if they are higher than the existing ones.
+         */
+        public void update(int frequency, ArrayList<WeightedString> shortcutTargets,
+                ArrayList<WeightedString> bigrams, boolean isShortcutOnly) {
+            if (frequency > mFrequency) {
+                mFrequency = frequency;
+            }
+            if (shortcutTargets != null) {
+                if (mShortcutTargets == null) {
+                    mShortcutTargets = shortcutTargets;
+                } else {
+                    final int size = shortcutTargets.size();
+                    for (int i = 0; i < size; ++i) {
+                        final WeightedString shortcut = shortcutTargets.get(i);
+                        final WeightedString existingShortcut = getShortcut(shortcut.mWord);
+                        if (existingShortcut == null) {
+                            mShortcutTargets.add(shortcut);
+                        } else if (existingShortcut.mFrequency < shortcut.mFrequency) {
+                            existingShortcut.mFrequency = shortcut.mFrequency;
+                        }
+                    }
+                }
+            }
+            if (bigrams != null) {
+                if (mBigrams == null) {
+                    mBigrams = bigrams;
+                } else {
+                    final int size = bigrams.size();
+                    for (int i = 0; i < size; ++i) {
+                        final WeightedString bigram = bigrams.get(i);
+                        final WeightedString existingBigram = getBigram(bigram.mWord);
+                        if (existingBigram == null) {
+                            mBigrams.add(bigram);
+                        } else if (existingBigram.mFrequency < bigram.mFrequency) {
+                            existingBigram.mFrequency = bigram.mFrequency;
+                        }
+                    }
+                }
+            }
+            mIsShortcutOnly = isShortcutOnly;
+        }
     }
 
     /**
@@ -259,6 +355,27 @@
     }
 
     /**
+     * Helper method to add a new bigram to the dictionary.
+     *
+     * @param word1 the previous word of the context
+     * @param word2 the next word of the context
+     * @param frequency the bigram frequency
+     */
+    public void setBigram(final String word1, final String word2, final int frequency) {
+        CharGroup charGroup = findWordInTree(mRoot, word1);
+        if (charGroup != null) {
+            final CharGroup charGroup2 = findWordInTree(mRoot, word2);
+            if (charGroup2 == null) {
+                // TODO: refactor with the identical code in addNeutralWords
+                add(getCodePoints(word2), 0, null, null, false /* isShortcutOnly */);
+            }
+            charGroup.addBigram(word2, frequency);
+        } else {
+            throw new RuntimeException("First word of bigram not found");
+        }
+    }
+
+    /**
      * Add a word to this dictionary.
      *
      * The shortcuts and bigrams, if any, have to be in the dictionary already. If they aren't,
@@ -306,17 +423,9 @@
             if (differentCharIndex == currentGroup.mChars.length) {
                 if (charIndex + differentCharIndex >= word.length) {
                     // The new word is a prefix of an existing word, but the node on which it
-                    // should end already exists as is.
-                    if (currentGroup.mFrequency > 0) {
-                        throw new RuntimeException("Such a word already exists in the dictionary : "
-                                + new String(word, 0, word.length));
-                    } else {
-                        final CharGroup newNode = new CharGroup(currentGroup.mChars,
-                                shortcutTargets, bigrams, frequency, currentGroup.mChildren,
-                                isShortcutOnly);
-                        currentNode.mData.set(nodeIndex, newNode);
-                        checkStack(currentNode);
-                    }
+                    // should end already exists as is. Since the old CharNode was not a terminal, 
+                    // make it one by filling in its frequency and other attributes
+                    currentGroup.update(frequency, shortcutTargets, bigrams, isShortcutOnly);
                 } else {
                     // The new word matches the full old word and extends past it.
                     // We only have to create a new node and add it to the end of this.
@@ -328,19 +437,9 @@
                 }
             } else {
                 if (0 == differentCharIndex) {
-                    // Exact same word. Check the frequency is 0 or NOT_A_TERMINAL, and update.
-                    if (0 != frequency) {
-                        if (0 < currentGroup.mFrequency) {
-                            throw new RuntimeException("This word already exists with frequency "
-                                    + currentGroup.mFrequency + " : "
-                                    + new String(word, 0, word.length));
-                        }
-                        final CharGroup newGroup = new CharGroup(word,
-                                currentGroup.mShortcutTargets, currentGroup.mBigrams,
-                                frequency, currentGroup.mChildren,
-                                currentGroup.mIsShortcutOnly && isShortcutOnly);
-                        currentNode.mData.set(nodeIndex, newGroup);
-                    }
+                    // Exact same word. Update the frequency if higher. This will also add the
+                    // new bigrams to the existing bigram list if it already exists.
+                    currentGroup.update(frequency, shortcutTargets, bigrams, isShortcutOnly);
                 } else {
                     // Partial prefix match only. We have to replace the current node with a node
                     // containing the current prefix and create two new ones for the tails.
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) {