Merge "Use SparseIntArray instead of TreeMap<Integer, Integer>"
diff --git a/java/res/values-af/strings.xml b/java/res/values-af/strings.xml
index f5a6245..2892940 100644
--- a/java/res/values-af/strings.xml
+++ b/java/res/values-af/strings.xml
@@ -135,8 +135,7 @@
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Raak weer om te stoor"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Woordeboek beskikbaar"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"Aktiveer gebruikerterugvoer"</string>
-    <!-- no translation found for prefs_description_log (7525225584555429211) -->
-    <skip />
+    <string name="prefs_description_log" msgid="7525225584555429211">"Help om hierdie invoermetode-redigeerder te verbeter deur gebruikstatistiek en omvalverslae outomaties te stuur"</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"Sleutelbordtema"</string>
     <string name="subtype_en_GB" msgid="88170601942311355">"Engels (VK)"</string>
     <string name="subtype_en_US" msgid="6160452336634534239">"Engels (VS)"</string>
@@ -163,12 +162,9 @@
     <string name="not_now" msgid="6172462888202790482">"Nie nou nie"</string>
     <string name="custom_input_style_already_exists" msgid="8008728952215449707">"Dieselfde invoerstyl bestaan ​​reeds: <xliff:g id="INPUT_STYLE_NAME">%s</xliff:g>"</string>
     <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Bruikbaarheidstudie-modus"</string>
-    <!-- no translation found for prefs_key_longpress_timeout_settings (6102240298932897873) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_vibration_duration_settings (7918341459947439226) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_sound_volume_settings (6027007337036891623) -->
-    <skip />
+    <string name="prefs_key_longpress_timeout_settings" msgid="6102240298932897873">"Vertraging van sleutellangdruk"</string>
+    <string name="prefs_keypress_vibration_duration_settings" msgid="7918341459947439226">"Sleuteldruk se vibrasie-tydsduur"</string>
+    <string name="prefs_keypress_sound_volume_settings" msgid="6027007337036891623">"Sleuteldruk se klankvolume"</string>
     <string name="prefs_read_external_dictionary" msgid="2588931418575013067">"Lees eksterne woordeboeklêer"</string>
     <string name="read_external_dictionary_no_files_message" msgid="4947420942224623792">"Geen woordeboeklêers in die aflaaiselsvouer nie"</string>
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"Kies \'n woordeboeklêer om te installeer"</string>
diff --git a/java/res/values-el/strings.xml b/java/res/values-el/strings.xml
index 41cf098..fad1cf5 100644
--- a/java/res/values-el/strings.xml
+++ b/java/res/values-el/strings.xml
@@ -135,8 +135,7 @@
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Αγγίξτε ξανά για αποθήκευση"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Λεξικό διαθέσιμο"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"Ενεργοποίηση σχολίων χρηστών"</string>
-    <!-- no translation found for prefs_description_log (7525225584555429211) -->
-    <skip />
+    <string name="prefs_description_log" msgid="7525225584555429211">"Βοηθήστε μας να βελτιώσουμε αυτό το πρόγραμμα επεξεργασίας μεθόδου εισόδου, στέλνοντας αυτόματα στατιστικά στοιχεία και αναφορές σφαλμάτων."</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"Θέμα πληκτρολογίου"</string>
     <string name="subtype_en_GB" msgid="88170601942311355">"Αγγλικά (Η.Β.)"</string>
     <string name="subtype_en_US" msgid="6160452336634534239">"Αγγλικά (Η.Π.Α)"</string>
@@ -163,12 +162,9 @@
     <string name="not_now" msgid="6172462888202790482">"Όχι τώρα"</string>
     <string name="custom_input_style_already_exists" msgid="8008728952215449707">"Το ίδιο στυλ εισόδου υπάρχει ήδη: <xliff:g id="INPUT_STYLE_NAME">%s</xliff:g>"</string>
     <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Λειτουργία μελέτης χρηστικότητας"</string>
-    <!-- no translation found for prefs_key_longpress_timeout_settings (6102240298932897873) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_vibration_duration_settings (7918341459947439226) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_sound_volume_settings (6027007337036891623) -->
-    <skip />
+    <string name="prefs_key_longpress_timeout_settings" msgid="6102240298932897873">"Καθυστέρηση παρατεταμένου πατήματος πλήκτρου"</string>
+    <string name="prefs_keypress_vibration_duration_settings" msgid="7918341459947439226">"Διάρκεια δόνησης πατήμ. πλήκτ."</string>
+    <string name="prefs_keypress_sound_volume_settings" msgid="6027007337036891623">"Ένταση ήχου πατήματος πλήκτρου"</string>
     <string name="prefs_read_external_dictionary" msgid="2588931418575013067">"Ανάγνωση εξωτερικού αρχείου λεξικού"</string>
     <string name="read_external_dictionary_no_files_message" msgid="4947420942224623792">"Δεν υπάρχουν αρχεία λεξικού στο φάκελο \"Λήψεις\""</string>
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"Επιλογή αρχείου λεξικού για εγκατάσταση"</string>
diff --git a/java/res/values-en-rGB/strings.xml b/java/res/values-en-rGB/strings.xml
index 3aab692..c0b9ede 100644
--- a/java/res/values-en-rGB/strings.xml
+++ b/java/res/values-en-rGB/strings.xml
@@ -135,8 +135,7 @@
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Touch again to save"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dictionary available"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"Enable user feedback"</string>
-    <!-- no translation found for prefs_description_log (7525225584555429211) -->
-    <skip />
+    <string name="prefs_description_log" msgid="7525225584555429211">"Help improve this input method editor by automatically sending usage statistics and crash reports"</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"Keyboard theme"</string>
     <string name="subtype_en_GB" msgid="88170601942311355">"English (UK)"</string>
     <string name="subtype_en_US" msgid="6160452336634534239">"English (US)"</string>
@@ -163,12 +162,9 @@
     <string name="not_now" msgid="6172462888202790482">"Not now"</string>
     <string name="custom_input_style_already_exists" msgid="8008728952215449707">"The same input style already exists: <xliff:g id="INPUT_STYLE_NAME">%s</xliff:g>"</string>
     <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Usability study mode"</string>
-    <!-- no translation found for prefs_key_longpress_timeout_settings (6102240298932897873) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_vibration_duration_settings (7918341459947439226) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_sound_volume_settings (6027007337036891623) -->
-    <skip />
+    <string name="prefs_key_longpress_timeout_settings" msgid="6102240298932897873">"Key long press delay"</string>
+    <string name="prefs_keypress_vibration_duration_settings" msgid="7918341459947439226">"Keypress vibration duration"</string>
+    <string name="prefs_keypress_sound_volume_settings" msgid="6027007337036891623">"Keypress sound volume"</string>
     <string name="prefs_read_external_dictionary" msgid="2588931418575013067">"Read external dictionary file"</string>
     <string name="read_external_dictionary_no_files_message" msgid="4947420942224623792">"No dictionary files in the Downloads folder"</string>
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"Select a dictionary file to install"</string>
diff --git a/java/res/values-it/strings.xml b/java/res/values-it/strings.xml
index 028a7a2..c64543a 100644
--- a/java/res/values-it/strings.xml
+++ b/java/res/values-it/strings.xml
@@ -135,8 +135,7 @@
     <string name="hint_add_to_dictionary" msgid="573678656946085380">"Tocca di nuovo per salvare"</string>
     <string name="has_dictionary" msgid="6071847973466625007">"Dizionario disponibile"</string>
     <string name="prefs_enable_log" msgid="6620424505072963557">"Attiva commenti degli utenti"</string>
-    <!-- no translation found for prefs_description_log (7525225584555429211) -->
-    <skip />
+    <string name="prefs_description_log" msgid="7525225584555429211">"Contribuisci a migliorare l\'editor del metodo di immissione inviando automaticamente statistiche sull\'utilizzo e rapporti sugli arresti anomali"</string>
     <string name="keyboard_layout" msgid="8451164783510487501">"Tema della tastiera"</string>
     <string name="subtype_en_GB" msgid="88170601942311355">"Inglese (UK)"</string>
     <string name="subtype_en_US" msgid="6160452336634534239">"Inglese (USA)"</string>
@@ -163,12 +162,9 @@
     <string name="not_now" msgid="6172462888202790482">"Non ora"</string>
     <string name="custom_input_style_already_exists" msgid="8008728952215449707">"Esiste già uno stile di inuput uguale: <xliff:g id="INPUT_STYLE_NAME">%s</xliff:g>"</string>
     <string name="prefs_usability_study_mode" msgid="1261130555134595254">"Modalità Studio sull\'usabilità"</string>
-    <!-- no translation found for prefs_key_longpress_timeout_settings (6102240298932897873) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_vibration_duration_settings (7918341459947439226) -->
-    <skip />
-    <!-- no translation found for prefs_keypress_sound_volume_settings (6027007337036891623) -->
-    <skip />
+    <string name="prefs_key_longpress_timeout_settings" msgid="6102240298932897873">"Ritardo pressione lunga tasti"</string>
+    <string name="prefs_keypress_vibration_duration_settings" msgid="7918341459947439226">"Durata vibraz. pressione tasto"</string>
+    <string name="prefs_keypress_sound_volume_settings" msgid="6027007337036891623">"Volume audio a pressione tasto"</string>
     <string name="prefs_read_external_dictionary" msgid="2588931418575013067">"Leggi file dizionario esterno"</string>
     <string name="read_external_dictionary_no_files_message" msgid="4947420942224623792">"Nessun file di dizionario nella cartella Download"</string>
     <string name="read_external_dictionary_multiple_files_title" msgid="7637749044265808628">"Seleziona un file di dizionario da installare"</string>
diff --git a/java/res/values/setup-wizard.xml b/java/res/values/setup-wizard.xml
new file mode 100644
index 0000000..8464709
--- /dev/null
+++ b/java/res/values/setup-wizard.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2013, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources>
+    <bool name="config_setup_wizard_available">false</bool>
+</resources>
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 745e7df..7dfbea3 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -1460,7 +1460,7 @@
             return "";
         }
         final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
-        return StringUtils.toTitleCase(locale.getLanguage(), locale);
+        return StringUtils.capitalizeFirstCodePoint(locale.getLanguage(), locale);
     }
 
     // Get InputMethodSubtype's middle display name in its locale.
diff --git a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
index 66c3014..f81ab8d 100644
--- a/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MoreKeysKeyboard.java
@@ -73,10 +73,11 @@
                 final int rowHeight, final int coordXInParent, final int parentKeyboardWidth,
                 final boolean isFixedColumnOrder, final int dividerWidth) {
             mIsFixedOrder = isFixedColumnOrder;
-            if (parentKeyboardWidth / keyWidth < maxColumns) {
+            if (parentKeyboardWidth / keyWidth < Math.min(numKeys, maxColumns)) {
                 throw new IllegalArgumentException(
                         "Keyboard is too small to hold more keys keyboard: "
-                                + parentKeyboardWidth + " " + keyWidth + " " + maxColumns);
+                                + parentKeyboardWidth + " " + keyWidth + " "
+                                + numKeys + " " + maxColumns);
             }
             mDefaultKeyWidth = keyWidth;
             mDefaultRowHeight = rowHeight;
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index 809ed68..b77e378 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -88,8 +88,9 @@
         this("", 1, 1, 1, 1, 1, 1, EMPTY_KEY_ARRAY, null);
         mNativeProximityInfo = setProximityInfoNative("" /* locale */,
                 gridWidth /* displayWidth */, gridHeight /* displayHeight */,
-                gridWidth, gridHeight, 1 /* mostCommonKeyWidth */, proximityCharsArray,
-                0 /* keyCount */, null /*keyXCoordinates */, null /* keyYCoordinates */,
+                gridWidth, gridHeight, 1 /* mostCommonKeyWidth */,
+                1 /* mostCommonKeyHeight */, proximityCharsArray, 0 /* keyCount */,
+                null /*keyXCoordinates */, null /* keyYCoordinates */,
                 null /* keyWidths */, null /* keyHeights */, null /* keyCharCodes */,
                 null /* sweetSpotCenterXs */, null /* sweetSpotCenterYs */,
                 null /* sweetSpotRadii */);
@@ -103,9 +104,10 @@
     // TODO: Stop passing proximityCharsArray
     private static native long setProximityInfoNative(String locale,
             int displayWidth, int displayHeight, int gridWidth, int gridHeight,
-            int mostCommonKeyWidth, int[] proximityCharsArray, int keyCount, int[] keyXCoordinates,
-            int[] keyYCoordinates, int[] keyWidths, int[] keyHeights, int[] keyCharCodes,
-            float[] sweetSpotCenterXs, float[] sweetSpotCenterYs, float[] sweetSpotRadii);
+            int mostCommonKeyWidth, int mostCommonKeyHeight, int[] proximityCharsArray,
+            int keyCount, int[] keyXCoordinates, int[] keyYCoordinates, int[] keyWidths,
+            int[] keyHeights, int[] keyCharCodes, float[] sweetSpotCenterXs,
+            float[] sweetSpotCenterYs, float[] sweetSpotRadii);
 
     private static native void releaseProximityInfoNative(long nativeProximityInfo);
 
@@ -232,9 +234,9 @@
 
         // TODO: Stop passing proximityCharsArray
         return setProximityInfoNative(mLocaleStr, mKeyboardMinWidth, mKeyboardHeight,
-                mGridWidth, mGridHeight, mMostCommonKeyWidth, proximityCharsArray, keyCount,
-                keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
-                sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
+                mGridWidth, mGridHeight, mMostCommonKeyWidth, mMostCommonKeyHeight,
+                proximityCharsArray, keyCount, keyXCoordinates, keyYCoordinates, keyWidths,
+                keyHeights, keyCharCodes, sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
     }
 
     public long getNativeProximityInfo() {
diff --git a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
index e3e6d39..f682b51 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/GesturePreviewTrail.java
@@ -37,6 +37,7 @@
 final class GesturePreviewTrail {
     private static final int DEFAULT_CAPACITY = GestureStrokeWithPreviewPoints.PREVIEW_CAPACITY;
 
+    // These three {@link ResizableIntArray}s should be synchronized by {@link #mEventTimes}.
     private final ResizableIntArray mXCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mYCoordinates = new ResizableIntArray(DEFAULT_CAPACITY);
     private final ResizableIntArray mEventTimes = new ResizableIntArray(DEFAULT_CAPACITY);
@@ -90,7 +91,13 @@
     }
 
     public void addStroke(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
-        final int trailSize = mEventTimes.getLength();
+        synchronized (mEventTimes) {
+            addStrokeLocked(stroke, downTime);
+        }
+    }
+
+    private void addStrokeLocked(final GestureStrokeWithPreviewPoints stroke, final long downTime) {
+            final int trailSize = mEventTimes.getLength();
         stroke.appendPreviewStroke(mEventTimes, mXCoordinates, mYCoordinates);
         if (mEventTimes.getLength() == trailSize) {
             return;
@@ -169,6 +176,13 @@
      */
     public boolean drawGestureTrail(final Canvas canvas, final Paint paint,
             final Rect outBoundsRect, final Params params) {
+        synchronized (mEventTimes) {
+            return drawGestureTrailLocked(canvas, paint, outBoundsRect, params);
+        }
+    }
+
+    private boolean drawGestureTrailLocked(final Canvas canvas, final Paint paint,
+            final Rect outBoundsRect, final Params params) {
         // Initialize bounds rectangle.
         outBoundsRect.setEmpty();
         final int trailSize = mEventTimes.getLength();
diff --git a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
index 2df7e5c..6bc6acc 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/PointerTrackerQueue.java
@@ -34,175 +34,197 @@
     }
 
     private static final int INITIAL_CAPACITY = 10;
+    // Note: {@link #mExpandableArrayOfActivePointers} and {@link #mArraySize} are synchronized by
+    // {@link #mExpandableArrayOfActivePointers}
     private final ArrayList<Element> mExpandableArrayOfActivePointers =
             CollectionUtils.newArrayList(INITIAL_CAPACITY);
     private int mArraySize = 0;
 
-    public synchronized int size() {
-        return mArraySize;
-    }
-
-    public synchronized void add(final Element pointer) {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        if (arraySize < expandableArray.size()) {
-            expandableArray.set(arraySize, pointer);
-        } else {
-            expandableArray.add(pointer);
+    public int size() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            return mArraySize;
         }
-        mArraySize = arraySize + 1;
     }
 
-    public synchronized void remove(final Element pointer) {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        int newSize = 0;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element == pointer) {
+    public void add(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            if (arraySize < expandableArray.size()) {
+                expandableArray.set(arraySize, pointer);
+            } else {
+                expandableArray.add(pointer);
+            }
+            mArraySize = arraySize + 1;
+        }
+    }
+
+    public void remove(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            int newSize = 0;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element == pointer) {
+                    if (newSize != index) {
+                        Log.w(TAG, "Found duplicated element in remove: " + pointer);
+                    }
+                    continue; // Remove this element from the expandableArray.
+                }
                 if (newSize != index) {
-                    Log.w(TAG, "Found duplicated element in remove: " + pointer);
+                    // Shift this element toward the beginning of the expandableArray.
+                    expandableArray.set(newSize, element);
                 }
-                continue; // Remove this element from the expandableArray.
-            }
-            if (newSize != index) {
-                // Shift this element toward the beginning of the expandableArray.
-                expandableArray.set(newSize, element);
-            }
-            newSize++;
-        }
-        mArraySize = newSize;
-    }
-
-    public synchronized Element getOldestElement() {
-        return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0);
-    }
-
-    public synchronized void releaseAllPointersOlderThan(final Element pointer,
-            final long eventTime) {
-        if (DEBUG) {
-            Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
-        }
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        int newSize, index;
-        for (newSize = index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element == pointer) {
-                break; // Stop releasing elements.
-            }
-            if (!element.isModifier()) {
-                element.onPhantomUpEvent(eventTime);
-                continue; // Remove this element from the expandableArray.
-            }
-            if (newSize != index) {
-                // Shift this element toward the beginning of the expandableArray.
-                expandableArray.set(newSize, element);
-            }
-            newSize++;
-        }
-        // Shift rest of the expandableArray.
-        int count = 0;
-        for (; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element == pointer) {
-                if (count > 0) {
-                    Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
-                            + pointer);
-                }
-                count++;
-            }
-            if (newSize != index) {
-                expandableArray.set(newSize, expandableArray.get(index));
                 newSize++;
             }
+            mArraySize = newSize;
         }
-        mArraySize = newSize;
+    }
+
+    public Element getOldestElement() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            return (mArraySize == 0) ? null : mExpandableArrayOfActivePointers.get(0);
+        }
+    }
+
+    public void releaseAllPointersOlderThan(final Element pointer, final long eventTime) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                Log.d(TAG, "releaseAllPoniterOlderThan: " + pointer + " " + this);
+            }
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            int newSize, index;
+            for (newSize = index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element == pointer) {
+                    break; // Stop releasing elements.
+                }
+                if (!element.isModifier()) {
+                    element.onPhantomUpEvent(eventTime);
+                    continue; // Remove this element from the expandableArray.
+                }
+                if (newSize != index) {
+                    // Shift this element toward the beginning of the expandableArray.
+                    expandableArray.set(newSize, element);
+                }
+                newSize++;
+            }
+            // Shift rest of the expandableArray.
+            int count = 0;
+            for (; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element == pointer) {
+                    if (count > 0) {
+                        Log.w(TAG, "Found duplicated element in releaseAllPointersOlderThan: "
+                                + pointer);
+                    }
+                    count++;
+                }
+                if (newSize != index) {
+                    expandableArray.set(newSize, expandableArray.get(index));
+                    newSize++;
+                }
+            }
+            mArraySize = newSize;
+        }
     }
 
     public void releaseAllPointers(final long eventTime) {
         releaseAllPointersExcept(null, eventTime);
     }
 
-    public synchronized void releaseAllPointersExcept(final Element pointer,
-            final long eventTime) {
-        if (DEBUG) {
-            if (pointer == null) {
-                Log.d(TAG, "releaseAllPoniters: " + this);
-            } else {
-                Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
-            }
-        }
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        int newSize = 0, count = 0;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element == pointer) {
-                if (count > 0) {
-                    Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: " + pointer);
+    public void releaseAllPointersExcept(final Element pointer, final long eventTime) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            if (DEBUG) {
+                if (pointer == null) {
+                    Log.d(TAG, "releaseAllPoniters: " + this);
+                } else {
+                    Log.d(TAG, "releaseAllPoniterExcept: " + pointer + " " + this);
                 }
-                count++;
-            } else {
-                element.onPhantomUpEvent(eventTime);
-                continue; // Remove this element from the expandableArray.
             }
-            if (newSize != index) {
-                // Shift this element toward the beginning of the expandableArray.
-                expandableArray.set(newSize, element);
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            int newSize = 0, count = 0;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element == pointer) {
+                    if (count > 0) {
+                        Log.w(TAG, "Found duplicated element in releaseAllPointersExcept: "
+                                + pointer);
+                    }
+                    count++;
+                } else {
+                    element.onPhantomUpEvent(eventTime);
+                    continue; // Remove this element from the expandableArray.
+                }
+                if (newSize != index) {
+                    // Shift this element toward the beginning of the expandableArray.
+                    expandableArray.set(newSize, element);
+                }
+                newSize++;
             }
-            newSize++;
+            mArraySize = newSize;
         }
-        mArraySize = newSize;
     }
 
-    public synchronized boolean hasModifierKeyOlderThan(final Element pointer) {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element == pointer) {
-                return false; // Stop searching modifier key.
+    public boolean hasModifierKeyOlderThan(final Element pointer) {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element == pointer) {
+                    return false; // Stop searching modifier key.
+                }
+                if (element.isModifier()) {
+                    return true;
+                }
             }
-            if (element.isModifier()) {
-                return true;
-            }
+            return false;
         }
-        return false;
     }
 
-    public synchronized boolean isAnyInSlidingKeyInput() {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (element.isInSlidingKeyInput()) {
-                return true;
+    public boolean isAnyInSlidingKeyInput() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (element.isInSlidingKeyInput()) {
+                    return true;
+                }
             }
+            return false;
         }
-        return false;
     }
 
-    public synchronized void cancelAllPointerTracker() {
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            element.cancelTracking();
+    public void cancelAllPointerTracker() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                element.cancelTracking();
+            }
         }
     }
 
     @Override
-    public synchronized String toString() {
-        final StringBuilder sb = new StringBuilder();
-        final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
-        final int arraySize = mArraySize;
-        for (int index = 0; index < arraySize; index++) {
-            final Element element = expandableArray.get(index);
-            if (sb.length() > 0)
-                sb.append(" ");
-            sb.append(element.toString());
+    public String toString() {
+        synchronized (mExpandableArrayOfActivePointers) {
+            final StringBuilder sb = new StringBuilder();
+            final ArrayList<Element> expandableArray = mExpandableArrayOfActivePointers;
+            final int arraySize = mArraySize;
+            for (int index = 0; index < arraySize; index++) {
+                final Element element = expandableArray.get(index);
+                if (sb.length() > 0) {
+                    sb.append(" ");
+                }
+                sb.append(element.toString());
+            }
+            return "[" + sb.toString() + "]";
         }
-        return "[" + sb.toString() + "]";
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/CapsModeUtils.java b/java/src/com/android/inputmethod/latin/CapsModeUtils.java
index 1012cd5..4b8d1ac 100644
--- a/java/src/com/android/inputmethod/latin/CapsModeUtils.java
+++ b/java/src/com/android/inputmethod/latin/CapsModeUtils.java
@@ -41,7 +41,7 @@
         if (WordComposer.CAPS_MODE_AUTO_SHIFT_LOCKED == capitalizeMode) {
             return s.toUpperCase(locale);
         } else if (WordComposer.CAPS_MODE_AUTO_SHIFTED == capitalizeMode) {
-            return StringUtils.toTitleCase(s, locale);
+            return StringUtils.capitalizeFirstCodePoint(s, locale);
         } else {
             return s;
         }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index 56b1c78..0f1f149 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -1540,7 +1540,8 @@
             }
         } else {
             final int codePointBeforeCursor = mConnection.getCodePointBeforeCursor();
-            if (mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
+            if (Character.isLetter(codePointBeforeCursor)
+                    || mSettings.getCurrent().isUsuallyFollowedBySpace(codePointBeforeCursor)) {
                 mSpaceState = SPACE_STATE_PHANTOM;
             }
         }
@@ -1551,7 +1552,8 @@
     private static final class BatchInputUpdater implements Handler.Callback {
         private final Handler mHandler;
         private LatinIME mLatinIme;
-        private boolean mInBatchInput; // synchronized using "this".
+        private final Object mLock = new Object();
+        private boolean mInBatchInput; // synchronized using {@link #mLock}.
 
         private BatchInputUpdater() {
             final HandlerThread handlerThread = new HandlerThread(
@@ -1582,21 +1584,25 @@
         }
 
         // Run in the UI thread.
-        public synchronized void onStartBatchInput(final LatinIME latinIme) {
-            mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
-            mLatinIme = latinIme;
-            mInBatchInput = true;
+        public void onStartBatchInput(final LatinIME latinIme) {
+            synchronized (mLock) {
+                mHandler.removeMessages(MSG_UPDATE_GESTURE_PREVIEW_AND_SUGGESTION_STRIP);
+                mLatinIme = latinIme;
+                mInBatchInput = true;
+            }
         }
 
         // Run in the Handler thread.
-        private synchronized void updateBatchInput(final InputPointers batchPointers) {
-            if (!mInBatchInput) {
-                // Batch input has ended or canceled while the message was being delivered.
-                return;
+        private void updateBatchInput(final InputPointers batchPointers) {
+            synchronized (mLock) {
+                if (!mInBatchInput) {
+                    // Batch input has ended or canceled while the message was being delivered.
+                    return;
+                }
+                final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        suggestedWords, false /* dismissGestureFloatingPreviewText */);
             }
-            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    suggestedWords, false /* dismissGestureFloatingPreviewText */);
         }
 
         // Run in the UI thread.
@@ -1609,19 +1615,23 @@
                     .sendToTarget();
         }
 
-        public synchronized void onCancelBatchInput() {
-            mInBatchInput = false;
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+        public void onCancelBatchInput() {
+            synchronized (mLock) {
+                mInBatchInput = false;
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        SuggestedWords.EMPTY, true /* dismissGestureFloatingPreviewText */);
+            }
         }
 
         // Run in the UI thread.
-        public synchronized SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
-            mInBatchInput = false;
-            final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
-            mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
-                    suggestedWords, true /* dismissGestureFloatingPreviewText */);
-            return suggestedWords;
+        public SuggestedWords onEndBatchInput(final InputPointers batchPointers) {
+            synchronized (mLock) {
+                mInBatchInput = false;
+                final SuggestedWords suggestedWords = getSuggestedWordsGestureLocked(batchPointers);
+                mLatinIme.mHandler.showGesturePreviewAndSuggestionStrip(
+                        suggestedWords, true /* dismissGestureFloatingPreviewText */);
+                return suggestedWords;
+            }
         }
 
         // {@link LatinIME#getSuggestedWords(int)} method calls with same session id have to
diff --git a/java/src/com/android/inputmethod/latin/Settings.java b/java/src/com/android/inputmethod/latin/Settings.java
index ce659bf..8fbe843 100644
--- a/java/src/com/android/inputmethod/latin/Settings.java
+++ b/java/src/com/android/inputmethod/latin/Settings.java
@@ -272,6 +272,11 @@
 
     public static boolean readShowSetupWizardIcon(final SharedPreferences prefs,
             final Context context) {
+        final boolean enableSetupWizardByConfig = context.getResources().getBoolean(
+                R.bool.config_setup_wizard_available);
+        if (!enableSetupWizardByConfig) {
+            return false;
+        }
         if (!prefs.contains(Settings.PREF_SHOW_SETUP_WIZARD_ICON)) {
             final ApplicationInfo appInfo = context.getApplicationInfo();
             final boolean isApplicationInSystemImage =
diff --git a/java/src/com/android/inputmethod/latin/SettingsActivity.java b/java/src/com/android/inputmethod/latin/SettingsActivity.java
index 99b572e..37ac2e3 100644
--- a/java/src/com/android/inputmethod/latin/SettingsActivity.java
+++ b/java/src/com/android/inputmethod/latin/SettingsActivity.java
@@ -25,7 +25,10 @@
     @Override
     public Intent getIntent() {
         final Intent intent = super.getIntent();
-        intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+        final String fragment = intent.getStringExtra(EXTRA_SHOW_FRAGMENT);
+        if (fragment == null) {
+            intent.putExtra(EXTRA_SHOW_FRAGMENT, DEFAULT_FRAGMENT);
+        }
         intent.putExtra(EXTRA_NO_HEADERS, true);
         return intent;
     }
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 928141c..5405a5e 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -165,6 +165,10 @@
                     Settings.readKeyPreviewPopupEnabled(prefs, res));
         }
 
+        if (!res.getBoolean(R.bool.config_setup_wizard_available)) {
+            removePreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON, advancedSettings);
+        }
+
         setPreferenceEnabled(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST,
                 Settings.readShowsLanguageSwitchKey(prefs));
 
@@ -203,7 +207,9 @@
         final SharedPreferences prefs = getPreferenceManager().getSharedPreferences();
         final CheckBoxPreference showSetupWizardIcon =
                 (CheckBoxPreference)findPreference(Settings.PREF_SHOW_SETUP_WIZARD_ICON);
-        showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity()));
+        if (showSetupWizardIcon != null) {
+            showSetupWizardIcon.setChecked(Settings.readShowSetupWizardIcon(prefs, getActivity()));
+        }
         updateShowCorrectionSuggestionsSummary();
         updateKeyPreviewPopupDelaySummary();
         updateCustomInputStylesSummary();
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index 59ad28f..11ef60d 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -106,10 +106,19 @@
         }
     }
 
-    public static String toTitleCase(final String s, final Locale locale) {
+    public static String capitalizeFirstCodePoint(final String s, final Locale locale) {
         if (s.length() <= 1) {
-            // TODO: is this really correct? Shouldn't this be s.toUpperCase()?
-            return s;
+            return s.toUpperCase(locale);
+        }
+        // Please refer to the comment below in
+        // {@link #capitalizeFirstAndDowncaseRest(String,Locale)} as this has the same shortcomings
+        final int cutoff = s.offsetByCodePoints(0, 1);
+        return s.substring(0, cutoff).toUpperCase(locale) + s.substring(cutoff);
+    }
+
+    public static String capitalizeFirstAndDowncaseRest(final String s, final Locale locale) {
+        if (s.length() <= 1) {
+            return s.toUpperCase(locale);
         }
         // TODO: fix the bugs below
         // - This does not work for Greek, because it returns upper case instead of title case.
diff --git a/java/src/com/android/inputmethod/latin/SubtypeLocale.java b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
index 5e28cc2..4d88ecc 100644
--- a/java/src/com/android/inputmethod/latin/SubtypeLocale.java
+++ b/java/src/com/android/inputmethod/latin/SubtypeLocale.java
@@ -183,7 +183,7 @@
             final Locale locale = LocaleUtils.constructLocaleFromString(localeString);
             displayName = locale.getDisplayName(displayLocale);
         }
-        return StringUtils.toTitleCase(displayName, displayLocale);
+        return StringUtils.capitalizeFirstCodePoint(displayName, displayLocale);
     }
 
     // InputMethodSubtype's display name in its locale.
@@ -243,7 +243,7 @@
                 }
             }
         };
-        return StringUtils.toTitleCase(
+        return StringUtils.capitalizeFirstCodePoint(
                 getSubtypeName.runInLocale(sResources, displayLocale), displayLocale);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index 975664d..6464bd0 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -394,7 +394,7 @@
         if (isAllUpperCase) {
             sb.append(wordInfo.mWord.toUpperCase(locale));
         } else if (isFirstCharCapitalized) {
-            sb.append(StringUtils.toTitleCase(wordInfo.mWord, locale));
+            sb.append(StringUtils.capitalizeFirstCodePoint(wordInfo.mWord, locale));
         } else {
             sb.append(wordInfo.mWord);
         }
diff --git a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
index e7c7e2b..17d2815 100644
--- a/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
+++ b/java/src/com/android/inputmethod/latin/makedict/FusionDictionary.java
@@ -647,7 +647,7 @@
 
         if (index < codePoints.length) return null;
         if (!currentGroup.isTerminal()) return null;
-        if (DBG && !codePoints.equals(checker.toString())) return null;
+        if (DBG && !string.equals(checker.toString())) return null;
         return currentGroup;
     }
 
@@ -853,16 +853,19 @@
                 if (currentPos.pos.hasNext()) {
                     final CharGroup currentGroup = currentPos.pos.next();
                     currentPos.length = mCurrentString.length();
-                    for (int i : currentGroup.mChars)
+                    for (int i : currentGroup.mChars) {
                         mCurrentString.append(Character.toChars(i));
+                    }
                     if (null != currentGroup.mChildren) {
                         currentPos = new Position(currentGroup.mChildren.mData);
+                        currentPos.length = mCurrentString.length();
                         mPositions.addLast(currentPos);
                     }
-                    if (currentGroup.mFrequency >= 0)
+                    if (currentGroup.mFrequency >= 0) {
                         return new Word(mCurrentString.toString(), currentGroup.mFrequency,
                                 currentGroup.mShortcutTargets, currentGroup.mBigrams,
                                 currentGroup.mIsNotAWord, currentGroup.mIsBlacklistEntry);
+                    }
                 } else {
                     mPositions.removeLast();
                     currentPos = mPositions.getLast();
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
index fbed139..2d0a89b 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidSpellCheckerService.java
@@ -330,7 +330,7 @@
                 } else if (StringUtils.CAPITALIZE_FIRST == capitalizeType) {
                     for (int i = 0; i < mSuggestions.size(); ++i) {
                         // Likewise
-                        mSuggestions.set(i, StringUtils.toTitleCase(
+                        mSuggestions.set(i, StringUtils.capitalizeFirstCodePoint(
                                 mSuggestions.get(i).toString(), locale));
                     }
                 }
diff --git a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
index b150632..96b2c81 100644
--- a/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
+++ b/java/src/com/android/inputmethod/latin/spellcheck/AndroidWordLevelSpellCheckerSession.java
@@ -226,7 +226,7 @@
         // If the lower case version is not in the dictionary, it's still possible
         // that we have an all-caps version of a word that needs to be capitalized
         // according to the dictionary. E.g. "GERMANS" only exists in the dictionary as "Germans".
-        return dict.isValidWord(StringUtils.toTitleCase(lowerCaseText, mLocale));
+        return dict.isValidWord(StringUtils.capitalizeFirstAndDowncaseRest(lowerCaseText, mLocale));
     }
 
     // Note : this must be reentrant
diff --git a/java/src/com/android/inputmethod/research/MotionEventReader.java b/java/src/com/android/inputmethod/research/MotionEventReader.java
index e1cc2da..fbfd9b5 100644
--- a/java/src/com/android/inputmethod/research/MotionEventReader.java
+++ b/java/src/com/android/inputmethod/research/MotionEventReader.java
@@ -22,6 +22,7 @@
 import android.view.MotionEvent.PointerCoords;
 import android.view.MotionEvent.PointerProperties;
 
+import com.android.inputmethod.annotations.UsedForTesting;
 import com.android.inputmethod.latin.define.ProductionFlag;
 
 import java.io.BufferedReader;
@@ -64,6 +65,7 @@
         return replayData;
     }
 
+    @UsedForTesting
     static class ReplayData {
         final ArrayList<Integer> mActions = new ArrayList<Integer>();
         final ArrayList<PointerProperties[]> mPointerPropertiesArrays
@@ -134,6 +136,7 @@
      * },
      * </pre>
      */
+    @UsedForTesting
     /* package for test */ void readLogStatement(final JsonReader jsonReader,
             final ReplayData replayData) throws IOException {
         String logStatementType = null;
diff --git a/java/src/com/android/inputmethod/research/ResearchLogger.java b/java/src/com/android/inputmethod/research/ResearchLogger.java
index e0bd37c..5aaced0 100644
--- a/java/src/com/android/inputmethod/research/ResearchLogger.java
+++ b/java/src/com/android/inputmethod/research/ResearchLogger.java
@@ -81,6 +81,7 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Random;
+import java.util.regex.Pattern;
 
 /**
  * Logs the use of the LatinIME keyboard.
@@ -1065,7 +1066,7 @@
             new LogStatement("LatinImeOnStartInputViewInternal", false, false, "uuid",
                     "packageName", "inputType", "imeOptions", "fieldId", "display", "model",
                     "prefs", "versionCode", "versionName", "outputFormatVersion", "logEverything",
-                    "isUsingDevelopmentOnlyDiagnosticsDebug");
+                    "isDevTeamBuild");
     public static void latinIME_onStartInputViewInternal(final EditorInfo editorInfo,
             final SharedPreferences prefs) {
         final ResearchLogger researchLogger = getInstance();
@@ -1087,13 +1088,29 @@
                         Integer.toHexString(editorInfo.imeOptions), editorInfo.fieldId,
                         Build.DISPLAY, Build.MODEL, prefs, versionCode, versionName,
                         OUTPUT_FORMAT_VERSION, IS_LOGGING_EVERYTHING,
-                        ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS_DEBUG);
-            } catch (NameNotFoundException e) {
-                e.printStackTrace();
+                        researchLogger.isDevTeamBuild());
+            } catch (final NameNotFoundException e) {
+                Log.e(TAG, "NameNotFound", e);
             }
         }
     }
 
+    // TODO: Update this heuristic pattern to something more reliable.  Developer builds tend to
+    // have the developer name and year embedded.
+    private static final Pattern developerBuildRegex = Pattern.compile("[A-Za-z]\\.20[1-9]");
+    private boolean isDevTeamBuild() {
+        try {
+            final PackageInfo packageInfo;
+            packageInfo = mLatinIME.getPackageManager().getPackageInfo(mLatinIME.getPackageName(),
+                    0);
+            final String versionName = packageInfo.versionName;
+            return !(developerBuildRegex.matcher(versionName).find());
+        } catch (final NameNotFoundException e) {
+            Log.e(TAG, "Could not determine package name", e);
+            return false;
+        }
+    }
+
     /**
      * Log a change in preferences.
      *
diff --git a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
index 3c482ca..dedb02a 100644
--- a/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
+++ b/native/jni/com_android_inputmethod_keyboard_ProximityInfo.cpp
@@ -26,13 +26,13 @@
 
 static jlong latinime_Keyboard_setProximityInfo(JNIEnv *env, jclass clazz, jstring localeJStr,
         jint displayWidth, jint displayHeight, jint gridWidth, jint gridHeight,
-        jint mostCommonkeyWidth, jintArray proximityChars, jint keyCount,
+        jint mostCommonkeyWidth, jint mostCommonkeyHeight, jintArray proximityChars, jint keyCount,
         jintArray keyXCoordinates, jintArray keyYCoordinates, jintArray keyWidths,
         jintArray keyHeights, jintArray keyCharCodes, jfloatArray sweetSpotCenterXs,
         jfloatArray sweetSpotCenterYs, jfloatArray sweetSpotRadii) {
     ProximityInfo *proximityInfo = new ProximityInfo(env, localeJStr, displayWidth, displayHeight,
-            gridWidth, gridHeight, mostCommonkeyWidth, proximityChars, keyCount,
-            keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
+            gridWidth, gridHeight, mostCommonkeyWidth, mostCommonkeyHeight, proximityChars,
+            keyCount, keyXCoordinates, keyYCoordinates, keyWidths, keyHeights, keyCharCodes,
             sweetSpotCenterXs, sweetSpotCenterYs, sweetSpotRadii);
     return reinterpret_cast<jlong>(proximityInfo);
 }
@@ -44,7 +44,7 @@
 
 static JNINativeMethod sMethods[] = {
     {const_cast<char *>("setProximityInfoNative"),
-     const_cast<char *>("(Ljava/lang/String;IIIII[II[I[I[I[I[I[F[F[F)J"),
+     const_cast<char *>("(Ljava/lang/String;IIIIII[II[I[I[I[I[I[F[F[F)J"),
      reinterpret_cast<void *>(latinime_Keyboard_setProximityInfo)},
     {const_cast<char *>("releaseProximityInfoNative"),
      const_cast<char *>("(J)V"),
diff --git a/native/jni/src/digraph_utils.cpp b/native/jni/src/digraph_utils.cpp
index 8781c50..6a1ab02 100644
--- a/native/jni/src/digraph_utils.cpp
+++ b/native/jni/src/digraph_utils.cpp
@@ -27,39 +27,47 @@
 const DigraphUtils::digraph_t DigraphUtils::FRENCH_LIGATURES_DIGRAPHS[] =
         { { 'a', 'e', 0x00E6 }, // U+00E6 : LATIN SMALL LETTER AE
         { 'o', 'e', 0x0153 } }; // U+0153 : LATIN SMALL LIGATURE OE
+const DigraphUtils::DigraphType DigraphUtils::USED_DIGRAPH_TYPES[] =
+        { DIGRAPH_TYPE_GERMAN_UMLAUT, DIGRAPH_TYPE_FRENCH_LIGATURES };
 
 /* static */ bool DigraphUtils::hasDigraphForCodePoint(
         const int dictFlags, const int compositeGlyphCodePoint) {
-    if (DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint)) {
+    const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags);
+    if (DigraphUtils::getDigraphForDigraphTypeAndCodePoint(digraphType, compositeGlyphCodePoint)) {
         return true;
     }
     return false;
 }
 
-// Retrieves the set of all digraphs associated with the given dictionary.
-// Returns the size of the digraph array, or 0 if none exist.
-/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(
-        const int dictFlags, const DigraphUtils::digraph_t **digraphs) {
+// Returns the digraph type associated with the given dictionary.
+/* static */ DigraphUtils::DigraphType DigraphUtils::getDigraphTypeForDictionary(
+        const int dictFlags) {
     if (BinaryFormat::REQUIRES_GERMAN_UMLAUT_PROCESSING & dictFlags) {
-        *digraphs = DigraphUtils::GERMAN_UMLAUT_DIGRAPHS;
-        return NELEMS(DigraphUtils::GERMAN_UMLAUT_DIGRAPHS);
+        return DIGRAPH_TYPE_GERMAN_UMLAUT;
     }
     if (BinaryFormat::REQUIRES_FRENCH_LIGATURES_PROCESSING & dictFlags) {
-        *digraphs = DigraphUtils::FRENCH_LIGATURES_DIGRAPHS;
-        return NELEMS(DigraphUtils::FRENCH_LIGATURES_DIGRAPHS);
+        return DIGRAPH_TYPE_FRENCH_LIGATURES;
     }
-    return 0;
+    return DIGRAPH_TYPE_NONE;
+}
+
+// Retrieves the set of all digraphs associated with the given dictionary flags.
+// Returns the size of the digraph array, or 0 if none exist.
+/* static */ int DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(
+        const int dictFlags, const DigraphUtils::digraph_t **const digraphs) {
+    const DigraphUtils::DigraphType digraphType = getDigraphTypeForDictionary(dictFlags);
+    return getAllDigraphsForDigraphTypeAndReturnSize(digraphType, digraphs);
 }
 
 // Returns the digraph codepoint for the given composite glyph codepoint and digraph codepoint index
 // (which specifies the first or second codepoint in the digraph).
-/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int dictFlags,
-        const int compositeGlyphCodePoint, const DigraphCodePointIndex digraphCodePointIndex) {
+/* static */ int DigraphUtils::getDigraphCodePointForIndex(const int compositeGlyphCodePoint,
+        const DigraphCodePointIndex digraphCodePointIndex) {
     if (digraphCodePointIndex == NOT_A_DIGRAPH_INDEX) {
         return NOT_A_CODE_POINT;
     }
-    const DigraphUtils::digraph_t *digraph =
-            DigraphUtils::getDigraphForCodePoint(dictFlags, compositeGlyphCodePoint);
+    const DigraphUtils::digraph_t *const digraph =
+            DigraphUtils::getDigraphForCodePoint(compositeGlyphCodePoint);
     if (!digraph) {
         return NOT_A_CODE_POINT;
     }
@@ -72,16 +80,48 @@
     return NOT_A_CODE_POINT;
 }
 
+// Retrieves the set of all digraphs associated with the given digraph type.
+// Returns the size of the digraph array, or 0 if none exist.
+/* static */ int DigraphUtils::getAllDigraphsForDigraphTypeAndReturnSize(
+        const DigraphUtils::DigraphType digraphType,
+        const DigraphUtils::digraph_t **const digraphs) {
+    if (digraphType == DigraphUtils::DIGRAPH_TYPE_GERMAN_UMLAUT) {
+        *digraphs = GERMAN_UMLAUT_DIGRAPHS;
+        return NELEMS(GERMAN_UMLAUT_DIGRAPHS);
+    }
+    if (digraphType == DIGRAPH_TYPE_FRENCH_LIGATURES) {
+        *digraphs = FRENCH_LIGATURES_DIGRAPHS;
+        return NELEMS(FRENCH_LIGATURES_DIGRAPHS);
+    }
+    return 0;
+}
+
 /**
  * Returns the digraph for the input composite glyph codepoint, or 0 if none exists.
- * dictFlags: the dictionary flags needed to determine which digraphs are supported.
  * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint.
  */
 /* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForCodePoint(
-        const int dictFlags, const int compositeGlyphCodePoint) {
+        const int compositeGlyphCodePoint) {
+    for (size_t i = 0; i < NELEMS(USED_DIGRAPH_TYPES); i++) {
+        const DigraphUtils::digraph_t *const digraph = getDigraphForDigraphTypeAndCodePoint(
+                USED_DIGRAPH_TYPES[i], compositeGlyphCodePoint);
+        if (digraph) {
+            return digraph;
+        }
+    }
+    return 0;
+}
+
+/**
+ * Returns the digraph for the input composite glyph codepoint, or 0 if none exists.
+ * digraphType: the type of digraphs supported.
+ * compositeGlyphCodePoint: the method returns the digraph corresponding to this codepoint.
+ */
+/* static */ const DigraphUtils::digraph_t *DigraphUtils::getDigraphForDigraphTypeAndCodePoint(
+        const DigraphUtils::DigraphType digraphType, const int compositeGlyphCodePoint) {
     const DigraphUtils::digraph_t *digraphs = 0;
     const int digraphsSize =
-            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(dictFlags, &digraphs);
+            DigraphUtils::getAllDigraphsForDictionaryAndReturnSize(digraphType, &digraphs);
     for (int i = 0; i < digraphsSize; i++) {
         if (digraphs[i].compositeGlyph == compositeGlyphCodePoint) {
             return &digraphs[i];
diff --git a/native/jni/src/digraph_utils.h b/native/jni/src/digraph_utils.h
index 6e364b6..9443522 100644
--- a/native/jni/src/digraph_utils.h
+++ b/native/jni/src/digraph_utils.h
@@ -27,21 +27,34 @@
         SECOND_DIGRAPH_CODEPOINT
     } DigraphCodePointIndex;
 
+    typedef enum {
+        DIGRAPH_TYPE_NONE,
+        DIGRAPH_TYPE_GERMAN_UMLAUT,
+        DIGRAPH_TYPE_FRENCH_LIGATURES
+    } DigraphType;
+
     typedef struct { int first; int second; int compositeGlyph; } digraph_t;
 
     static bool hasDigraphForCodePoint(const int dictFlags, const int compositeGlyphCodePoint);
     static int getAllDigraphsForDictionaryAndReturnSize(
-            const int dictFlags, const digraph_t **digraphs);
+            const int dictFlags, const digraph_t **const digraphs);
     static int getDigraphCodePointForIndex(const int dictFlags, const int compositeGlyphCodePoint,
             const DigraphCodePointIndex digraphCodePointIndex);
+    static int getDigraphCodePointForIndex(const int compositeGlyphCodePoint,
+            const DigraphCodePointIndex digraphCodePointIndex);
 
  private:
     DISALLOW_IMPLICIT_CONSTRUCTORS(DigraphUtils);
-    static const digraph_t *getDigraphForCodePoint(
-            const int dictFlags, const int compositeGlyphCodePoint);
+    static DigraphType getDigraphTypeForDictionary(const int dictFlags);
+    static int getAllDigraphsForDigraphTypeAndReturnSize(
+            const DigraphType digraphType, const digraph_t **const digraphs);
+    static const digraph_t *getDigraphForCodePoint(const int compositeGlyphCodePoint);
+    static const digraph_t *getDigraphForDigraphTypeAndCodePoint(
+            const DigraphType digraphType, const int compositeGlyphCodePoint);
 
     static const digraph_t GERMAN_UMLAUT_DIGRAPHS[];
     static const digraph_t FRENCH_LIGATURES_DIGRAPHS[];
+    static const DigraphType USED_DIGRAPH_TYPES[];
 };
 } // namespace latinime
 #endif // DIGRAPH_UTILS_H
diff --git a/native/jni/src/proximity_info.cpp b/native/jni/src/proximity_info.cpp
index 50f38e8..88d670d 100644
--- a/native/jni/src/proximity_info.cpp
+++ b/native/jni/src/proximity_info.cpp
@@ -49,13 +49,17 @@
 
 ProximityInfo::ProximityInfo(JNIEnv *env, const jstring localeJStr,
         const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-        const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars,
-        const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates,
-        const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes,
-        const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs,
-        const jfloatArray sweetSpotRadii)
+        const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight,
+        const jintArray proximityChars, const int keyCount, const jintArray keyXCoordinates,
+        const jintArray keyYCoordinates, const jintArray keyWidths, const jintArray keyHeights,
+        const jintArray keyCharCodes, const jfloatArray sweetSpotCenterXs,
+        const jfloatArray sweetSpotCenterYs, const jfloatArray sweetSpotRadii)
         : GRID_WIDTH(gridWidth), GRID_HEIGHT(gridHeight), MOST_COMMON_KEY_WIDTH(mostCommonKeyWidth),
           MOST_COMMON_KEY_WIDTH_SQUARE(mostCommonKeyWidth * mostCommonKeyWidth),
+          MOST_COMMON_KEY_HEIGHT(mostCommonKeyHeight),
+          NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE(1.0f +
+                  SQUARE_FLOAT(static_cast<float>(mostCommonKeyHeight) /
+                          static_cast<float>(mostCommonKeyWidth))),
           CELL_WIDTH((keyboardWidth + gridWidth - 1) / gridWidth),
           CELL_HEIGHT((keyboardHeight + gridHeight - 1) / gridHeight),
           KEY_COUNT(min(keyCount, MAX_KEY_COUNT_IN_A_KEYBOARD)),
diff --git a/native/jni/src/proximity_info.h b/native/jni/src/proximity_info.h
index e21262f..deb9ae0 100644
--- a/native/jni/src/proximity_info.h
+++ b/native/jni/src/proximity_info.h
@@ -30,11 +30,11 @@
  public:
     ProximityInfo(JNIEnv *env, const jstring localeJStr,
             const int keyboardWidth, const int keyboardHeight, const int gridWidth,
-            const int gridHeight, const int mostCommonKeyWidth, const jintArray proximityChars,
-            const int keyCount, const jintArray keyXCoordinates, const jintArray keyYCoordinates,
-            const jintArray keyWidths, const jintArray keyHeights, const jintArray keyCharCodes,
-            const jfloatArray sweetSpotCenterXs, const jfloatArray sweetSpotCenterYs,
-            const jfloatArray sweetSpotRadii);
+            const int gridHeight, const int mostCommonKeyWidth, const int mostCommonKeyHeight,
+            const jintArray proximityChars, const int keyCount, const jintArray keyXCoordinates,
+            const jintArray keyYCoordinates, const jintArray keyWidths, const jintArray keyHeights,
+            const jintArray keyCharCodes, const jfloatArray sweetSpotCenterXs,
+            const jfloatArray sweetSpotCenterYs, const jfloatArray sweetSpotRadii);
     ~ProximityInfo();
     bool hasSpaceProximity(const int x, const int y) const;
     int getNormalizedSquaredDistance(const int inputIndex, const int proximityIndex) const;
@@ -56,6 +56,9 @@
     bool hasTouchPositionCorrectionData() const { return HAS_TOUCH_POSITION_CORRECTION_DATA; }
     int getMostCommonKeyWidth() const { return MOST_COMMON_KEY_WIDTH; }
     int getMostCommonKeyWidthSquare() const { return MOST_COMMON_KEY_WIDTH_SQUARE; }
+    float getNormalizedSquaredMostCommonKeyHypotenuse() const {
+        return NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE;
+    }
     int getKeyCount() const { return KEY_COUNT; }
     int getCellHeight() const { return CELL_HEIGHT; }
     int getCellWidth() const { return CELL_WIDTH; }
@@ -99,6 +102,8 @@
     const int GRID_HEIGHT;
     const int MOST_COMMON_KEY_WIDTH;
     const int MOST_COMMON_KEY_WIDTH_SQUARE;
+    const int MOST_COMMON_KEY_HEIGHT;
+    const float NORMALIZED_SQUARED_MOST_COMMON_KEY_HYPOTENUSE;
     const int CELL_WIDTH;
     const int CELL_HEIGHT;
     const int KEY_COUNT;
diff --git a/native/jni/src/suggest/core/dicnode/dic_node.h b/native/jni/src/suggest/core/dicnode/dic_node.h
index cde7b99..32faae5 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node.h
@@ -23,6 +23,7 @@
 #include "dic_node_profiler.h"
 #include "dic_node_properties.h"
 #include "dic_node_release_listener.h"
+#include "digraph_utils.h"
 
 #if DEBUG_DICT
 #define LOGI_SHOW_ADD_COST_PROP \
@@ -399,8 +400,15 @@
     // TODO: Remove     //
     //////////////////////
     // TODO: Remove once touch path is merged into ProximityInfoState
+    // Note: Returned codepoint may be a digraph codepoint if the node is in a composite glyph.
     int getNodeCodePoint() const {
-        return mDicNodeProperties.getNodeCodePoint();
+        const int codePoint = mDicNodeProperties.getNodeCodePoint();
+        const DigraphUtils::DigraphCodePointIndex digraphIndex =
+                mDicNodeState.mDicNodeStateScoring.getDigraphIndex();
+        if (digraphIndex == DigraphUtils::NOT_A_DIGRAPH_INDEX) {
+            return codePoint;
+        }
+        return DigraphUtils::getDigraphCodePointForIndex(codePoint, digraphIndex);
     }
 
     ////////////////////////////////
@@ -452,6 +460,15 @@
         mDicNodeState.mDicNodeStateScoring.setDoubleLetterLevel(doubleLetterLevel);
     }
 
+    bool isInDigraph() const {
+        return mDicNodeState.mDicNodeStateScoring.getDigraphIndex()
+                != DigraphUtils::NOT_A_DIGRAPH_INDEX;
+    }
+
+    void advanceDigraphIndex() {
+        mDicNodeState.mDicNodeStateScoring.advanceDigraphIndex();
+    }
+
     uint8_t getFlags() const {
         return mDicNodeProperties.getFlags();
     }
diff --git a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
index 8e81632..8902d31 100644
--- a/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
+++ b/native/jni/src/suggest/core/dicnode/dic_node_state_scoring.h
@@ -20,6 +20,7 @@
 #include <stdint.h>
 
 #include "defines.h"
+#include "digraph_utils.h"
 
 namespace latinime {
 
@@ -27,6 +28,7 @@
  public:
     AK_FORCE_INLINE DicNodeStateScoring()
             : mDoubleLetterLevel(NOT_A_DOUBLE_LETTER),
+              mDigraphIndex(DigraphUtils::NOT_A_DIGRAPH_INDEX),
               mEditCorrectionCount(0), mProximityCorrectionCount(0),
               mNormalizedCompoundDistance(0.0f), mSpatialDistance(0.0f), mLanguageDistance(0.0f),
               mTotalPrevWordsLanguageCost(0.0f), mRawLength(0.0f) {
@@ -43,6 +45,7 @@
         mTotalPrevWordsLanguageCost = 0.0f;
         mRawLength = 0.0f;
         mDoubleLetterLevel = NOT_A_DOUBLE_LETTER;
+        mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
     }
 
     AK_FORCE_INLINE void init(const DicNodeStateScoring *const scoring) {
@@ -54,6 +57,7 @@
         mTotalPrevWordsLanguageCost = scoring->mTotalPrevWordsLanguageCost;
         mRawLength = scoring->mRawLength;
         mDoubleLetterLevel = scoring->mDoubleLetterLevel;
+        mDigraphIndex = scoring->mDigraphIndex;
     }
 
     void addCost(const float spatialCost, const float languageCost, const bool doNormalization,
@@ -126,6 +130,24 @@
         }
     }
 
+    DigraphUtils::DigraphCodePointIndex getDigraphIndex() const {
+        return mDigraphIndex;
+    }
+
+    void advanceDigraphIndex() {
+        switch(mDigraphIndex) {
+            case DigraphUtils::NOT_A_DIGRAPH_INDEX:
+                mDigraphIndex = DigraphUtils::FIRST_DIGRAPH_CODEPOINT;
+                break;
+            case DigraphUtils::FIRST_DIGRAPH_CODEPOINT:
+                mDigraphIndex = DigraphUtils::SECOND_DIGRAPH_CODEPOINT;
+                break;
+            case DigraphUtils::SECOND_DIGRAPH_CODEPOINT:
+                mDigraphIndex = DigraphUtils::NOT_A_DIGRAPH_INDEX;
+                break;
+        }
+    }
+
     float getTotalPrevWordsLanguageCost() const {
         return mTotalPrevWordsLanguageCost;
     }
@@ -135,6 +157,7 @@
     // Use a default copy constructor and an assign operator because shallow copies are ok
     // for this class
     DoubleLetterLevel mDoubleLetterLevel;
+    DigraphUtils::DigraphCodePointIndex mDigraphIndex;
 
     int16_t mEditCorrectionCount;
     int16_t mProximityCorrectionCount;
diff --git a/native/jni/src/suggest/core/suggest.cpp b/native/jni/src/suggest/core/suggest.cpp
index 764c372..63bb200 100644
--- a/native/jni/src/suggest/core/suggest.cpp
+++ b/native/jni/src/suggest/core/suggest.cpp
@@ -18,6 +18,7 @@
 
 #include "char_utils.h"
 #include "dictionary.h"
+#include "digraph_utils.h"
 #include "proximity_info.h"
 #include "suggest/core/dicnode/dic_node.h"
 #include "suggest/core/dicnode/dic_node_priority_queue.h"
@@ -221,7 +222,7 @@
 void Suggest::expandCurrentDicNodes(DicTraverseSession *traverseSession) const {
     const int inputSize = traverseSession->getInputSize();
     DicNodeVector childDicNodes(TRAVERSAL->getDefaultExpandDicNodeSize());
-    DicNode omissionDicNode;
+    DicNode correctionDicNode;
 
     // TODO: Find more efficient caching
     const bool shouldDepthLevelCache = TRAVERSAL->shouldDepthLevelCache(traverseSession);
@@ -257,7 +258,10 @@
             dicNode.setCached();
         }
 
-        if (isLookAheadCorrection) {
+        if (dicNode.isInDigraph()) {
+            // Finish digraph handling if the node is in the middle of a digraph expansion.
+            processDicNodeAsDigraph(traverseSession, &dicNode);
+        } else if (isLookAheadCorrection) {
             // The algorithm maintains a small set of "deferred" nodes that have not consumed the
             // latest touch point yet. These are needed to apply look-ahead correction operations
             // that require special handling of the latest touch point. For example, with insertions
@@ -291,12 +295,18 @@
                     processDicNodeAsMatch(traverseSession, childDicNode);
                     continue;
                 }
+                if (DigraphUtils::hasDigraphForCodePoint(traverseSession->getDictFlags(),
+                        childDicNode->getNodeCodePoint())) {
+                    correctionDicNode.initByCopy(childDicNode);
+                    correctionDicNode.advanceDigraphIndex();
+                    processDicNodeAsDigraph(traverseSession, &correctionDicNode);
+                }
                 if (allowsErrorCorrections
                         && TRAVERSAL->isOmission(traverseSession, &dicNode, childDicNode)) {
                     // TODO: (Gesture) Change weight between omission and substitution errors
                     // TODO: (Gesture) Terminal node should not be handled as omission
-                    omissionDicNode.initByCopy(childDicNode);
-                    processDicNodeAsOmission(traverseSession, &omissionDicNode);
+                    correctionDicNode.initByCopy(childDicNode);
+                    processDicNodeAsOmission(traverseSession, &correctionDicNode);
                 }
                 const ProximityType proximityType = TRAVERSAL->getProximityType(
                         traverseSession, &dicNode, childDicNode);
@@ -400,6 +410,16 @@
     processExpandedDicNode(traverseSession, childDicNode);
 }
 
+// Process the node codepoint as a digraph. This means that composite glyphs like the German
+// u-umlaut is expanded to the transliteration "ue". Note that this happens in parallel with
+// the normal non-digraph traversal, so both "uber" and "ueber" can be corrected to "[u-umlaut]ber".
+void Suggest::processDicNodeAsDigraph(DicTraverseSession *traverseSession,
+        DicNode *childDicNode) const {
+    weightChildNode(traverseSession, childDicNode);
+    childDicNode->advanceDigraphIndex();
+    processExpandedDicNode(traverseSession, childDicNode);
+}
+
 /**
  * Handle the dicNode as an omission error (e.g., ths => this). Skip the current letter and consider
  * matches for all possible next letters. Note that just skipping the current letter without any
diff --git a/native/jni/src/suggest/core/suggest.h b/native/jni/src/suggest/core/suggest.h
index 6c09b94..136c4e5 100644
--- a/native/jni/src/suggest/core/suggest.h
+++ b/native/jni/src/suggest/core/suggest.h
@@ -64,6 +64,7 @@
     void generateFeatures(
             DicTraverseSession *traverseSession, DicNode *dicNode, float *features) const;
     void processDicNodeAsOmission(DicTraverseSession *traverseSession, DicNode *dicNode) const;
+    void processDicNodeAsDigraph(DicTraverseSession *traverseSession, DicNode *dicNode) const;
     void processDicNodeAsTransposition(DicTraverseSession *traverseSession,
             DicNode *dicNode) const;
     void processDicNodeAsInsertion(DicTraverseSession *traverseSession, DicNode *dicNode) const;
diff --git a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
index 1398db9..850af94 100644
--- a/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/SpacebarTextTests.java
@@ -113,7 +113,8 @@
             final String subtypeName = SubtypeLocale.getSubtypeDisplayName(subtype);
             final Locale locale = SubtypeLocale.getSubtypeLocale(subtype);
             final String spacebarText = MainKeyboardView.getShortDisplayName(subtype);
-            final String languageCode = StringUtils.toTitleCase(locale.getLanguage(), locale);
+            final String languageCode = StringUtils.capitalizeFirstCodePoint(
+                    locale.getLanguage(), locale);
             if (SubtypeLocale.isNoLanguage(subtype)) {
                 assertEquals(subtypeName, "", spacebarText);
             } else {
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 966919e..b6a05e9 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -93,25 +93,43 @@
                 StringUtils.removeFromCsvIfExists("key", "key1,key,key3,key,key5"));
     }
 
-    public void testToTitleCase() {
+
+    public void testCapitalizeFirstCodePoint() {
         assertEquals("SSaa",
-                StringUtils.toTitleCase("ßaa", Locale.GERMAN));
+                StringUtils.capitalizeFirstCodePoint("ßaa", Locale.GERMAN));
         assertEquals("Aßa",
-                StringUtils.toTitleCase("aßa", Locale.GERMAN));
+                StringUtils.capitalizeFirstCodePoint("aßa", Locale.GERMAN));
         assertEquals("Iab",
-                StringUtils.toTitleCase("iab", Locale.ENGLISH));
-        assertEquals("Camelcase",
-                StringUtils.toTitleCase("cAmElCaSe", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("iab", Locale.ENGLISH));
+        assertEquals("CAmElCaSe",
+                StringUtils.capitalizeFirstCodePoint("cAmElCaSe", Locale.ENGLISH));
         assertEquals("İab",
-                StringUtils.toTitleCase("iab", new Locale("tr")));
-        assertEquals("Aib",
-                StringUtils.toTitleCase("AİB", new Locale("tr")));
-        // For one character, toTitleCase returns the string as is. Not sure what the motivation
-        // is, but that's how it works now.
-        assertEquals("a",
-                StringUtils.toTitleCase("a", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("iab", new Locale("tr")));
+        assertEquals("AİB",
+                StringUtils.capitalizeFirstCodePoint("AİB", new Locale("tr")));
         assertEquals("A",
-                StringUtils.toTitleCase("A", Locale.ENGLISH));
+                StringUtils.capitalizeFirstCodePoint("a", Locale.ENGLISH));
+        assertEquals("A",
+                StringUtils.capitalizeFirstCodePoint("A", Locale.ENGLISH));
+    }
+
+    public void testCapitalizeFirstAndDowncaseRest() {
+        assertEquals("SSaa",
+                StringUtils.capitalizeFirstAndDowncaseRest("ßaa", Locale.GERMAN));
+        assertEquals("Aßa",
+                StringUtils.capitalizeFirstAndDowncaseRest("aßa", Locale.GERMAN));
+        assertEquals("Iab",
+                StringUtils.capitalizeFirstAndDowncaseRest("iab", Locale.ENGLISH));
+        assertEquals("Camelcase",
+                StringUtils.capitalizeFirstAndDowncaseRest("cAmElCaSe", Locale.ENGLISH));
+        assertEquals("İab",
+                StringUtils.capitalizeFirstAndDowncaseRest("iab", new Locale("tr")));
+        assertEquals("Aib",
+                StringUtils.capitalizeFirstAndDowncaseRest("AİB", new Locale("tr")));
+        assertEquals("A",
+                StringUtils.capitalizeFirstAndDowncaseRest("a", Locale.ENGLISH));
+        assertEquals("A",
+                StringUtils.capitalizeFirstAndDowncaseRest("A", Locale.ENGLISH));
     }
 
     public void testGetCapitalizationType() {