Merge "Rename containsCorrection to containsUserDeletions"
diff --git a/java/res/layout/setup_welcome_video.xml b/java/res/layout/setup_welcome_video.xml
index 7517732..01c25ea 100644
--- a/java/res/layout/setup_welcome_video.xml
+++ b/java/res/layout/setup_welcome_video.xml
@@ -36,13 +36,15 @@
                 android:id="@+id/setup_welcome_video"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
-                android:background="@color/setup_background" />
+                android:background="@color/setup_background"
+                android:contentDescription="@string/setup_welcome_additional_description"/>
             <ImageView
                 android:id="@+id/setup_welcome_image"
                 android:layout_width="match_parent"
                 android:layout_height="wrap_content"
                 android:adjustViewBounds="true"
-                android:visibility="gone" />
+                android:visibility="gone"
+                android:contentDescription="@string/setup_welcome_additional_description"/>
         </LinearLayout>
         <View
             android:layout_weight="@integer/setup_welcome_video_end_padding_weight_in_screen"
diff --git a/java/res/values/keypress-vibration-durations.xml b/java/res/values/keypress-vibration-durations.xml
index 9ce5051..ad6bead 100644
--- a/java/res/values/keypress-vibration-durations.xml
+++ b/java/res/values/keypress-vibration-durations.xml
@@ -37,7 +37,7 @@
         <item>MODEL=(SAMSUNG-)?GT-I(930[05][NT]?|9308):MANUFACTURER=samsung,8</item>
         <item>MODEL=(SAMSUNG-)?SGH-(T999[V]?|I747[M]?|N064|N035):MANUFACTURER=samsung,8</item>
         <item>MODEL=(SAMSUNG-)?SCH-(J021|R530|I535|I939):MANUFACTURER=samsung,8</item>
-        <item>MODEL=(SAMSUNG-)?(SCL21|SC-06D|SC-03E]):MANUFACTURER=samsung,8</item>
+        <item>MODEL=(SAMSUNG-)?(SCL21|SC-06D|SC-03E):MANUFACTURER=samsung,8</item>
         <item>MODEL=(SAMSUNG-)?(SHV-210[KLS]?|SPH-L710):MANUFACTURER=samsung,8</item>
         <!-- LG Optimus G -->
         <item>MODEL=LG-E97[013]|LS970|L-01E:MANUFACTURER=LGE,15</item>
diff --git a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
index d0e8446..77f67b8 100644
--- a/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/dictionarypack/LocaleUtils.java
@@ -144,7 +144,7 @@
     public static String getMatchLevelSortedString(final int matchLevel) {
         // This works because the match levels are 0~99 (actually 0~30)
         // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
-        return String.format("%02d", MATCH_LEVEL_MAX - matchLevel);
+        return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 1550e77..ae72b4a 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -453,7 +453,7 @@
         } else {
             label = "/" + mLabel;
         }
-        return String.format("%s%s %d,%d %dx%d %s/%s/%s",
+        return String.format(Locale.ROOT, "%s%s %d,%d %dx%d %s/%s/%s",
                 Constants.printableCode(mCode), label, mX, mY, mWidth, mHeight, mHintLabel,
                 KeyboardIconsSet.getIconName(mIconId), backgroundName(mBackgroundType));
     }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardId.java b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
index aa27067..4c5dd25 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardId.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardId.java
@@ -187,7 +187,7 @@
     public String toString() {
         final String orientation = (mOrientation == Configuration.ORIENTATION_PORTRAIT)
                 ? "port" : "land";
-        return String.format("[%s %s:%s %s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
+        return String.format(Locale.ROOT, "[%s %s:%s %s:%dx%d %s %s %s%s%s%s%s%s%s%s%s]",
                 elementIdToName(mElementId),
                 mLocale,
                 mSubtype.getExtraValueOf(KEYBOARD_LAYOUT_SET),
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
index ad08d64..83f1090 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardSwitcher.java
@@ -32,6 +32,7 @@
 import com.android.inputmethod.keyboard.PointerTracker.TimerProxy;
 import com.android.inputmethod.keyboard.internal.KeyboardState;
 import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.InputView;
 import com.android.inputmethod.latin.LatinIME;
 import com.android.inputmethod.latin.LatinImeLogger;
@@ -68,8 +69,6 @@
         new KeyboardTheme(5, R.style.KeyboardTheme_IceCreamSandwich),
     };
 
-    private final AudioAndHapticFeedbackManager mFeedbackManager =
-            AudioAndHapticFeedbackManager.getInstance();
     private SubtypeSwitcher mSubtypeSwitcher;
     private SharedPreferences mPrefs;
 
@@ -151,7 +150,6 @@
         mKeyboardLayoutSet = builder.build();
         try {
             mState.onLoadKeyboard();
-            mFeedbackManager.onSettingsChanged(settingsValues);
         } catch (KeyboardLayoutSetException e) {
             Log.w(TAG, "loading keyboard failed: " + e.mKeyboardId, e.getCause());
             LatinImeLogger.logOnException(e.mKeyboardId.toString(), e.getCause());
@@ -159,10 +157,6 @@
         }
     }
 
-    public void onRingerModeChanged() {
-        mFeedbackManager.onRingerModeChanged();
-    }
-
     public void saveKeyboardState() {
         if (getKeyboard() != null) {
             mState.onSaveKeyboardState();
@@ -217,9 +211,7 @@
     }
 
     public void onPressKey(final int code, final boolean isSinglePointer) {
-        if (isVibrateAndSoundFeedbackRequired()) {
-            mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
-        }
+        hapticAndAudioFeedback(code);
         mState.onPressKey(code, isSinglePointer, mLatinIME.getCurrentAutoCapsState());
     }
 
@@ -328,22 +320,19 @@
         }
     }
 
-    // Implements {@link KeyboardState.SwitchActions}.
-    @Override
-    public void hapticAndAudioFeedback(final int code) {
-        mFeedbackManager.hapticAndAudioFeedback(code, mKeyboardView);
+    private void hapticAndAudioFeedback(final int code) {
+        if (mKeyboardView == null || mKeyboardView.isInSlidingKeyInput()) {
+            return;
+        }
+        AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, mKeyboardView);
     }
 
     public void onLongPressTimeout(final int code) {
         mState.onLongPressTimeout(code);
-    }
-
-    public boolean isInMomentarySwitchState() {
-        return mState.isInMomentarySwitchState();
-    }
-
-    private boolean isVibrateAndSoundFeedbackRequired() {
-        return mKeyboardView != null && !mKeyboardView.isInSlidingKeyInput();
+        final Keyboard keyboard = getKeyboard();
+        if (keyboard != null && keyboard.mId.isAlphabetKeyboard() && code == Constants.CODE_SHIFT) {
+            hapticAndAudioFeedback(code);
+        }
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
index 6c6fc61..7493df8 100644
--- a/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/MainKeyboardView.java
@@ -57,6 +57,7 @@
 import com.android.inputmethod.keyboard.internal.PreviewPlacerView;
 import com.android.inputmethod.keyboard.internal.SlidingKeyInputPreview;
 import com.android.inputmethod.keyboard.internal.TouchScreenRegulator;
+import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.CoordinateUtils;
@@ -240,7 +241,9 @@
             case MSG_REPEAT_KEY:
                 final Key currentKey = tracker.getKey();
                 if (currentKey != null && currentKey.mCode == msg.arg1) {
-                    tracker.onRegisterKey(currentKey);
+                    tracker.onRepeatKey(currentKey);
+                    AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+                            currentKey.mCode, keyboardView);
                     startKeyRepeatTimer(tracker, mKeyRepeatInterval);
                 }
                 break;
@@ -312,9 +315,8 @@
             default:
                 final int longpressTimeout =
                         Settings.getInstance().getCurrent().mKeyLongpressTimeout;
-                if (KeyboardSwitcher.getInstance().isInMomentarySwitchState()) {
-                    // We use longer timeout for sliding finger input started from the symbols
-                    // mode key.
+                if (tracker.isInSlidingKeyInputFromModifier()) {
+                    // We use longer timeout for sliding finger input started from the modifier key.
                     delay = longpressTimeout * 3;
                 } else {
                     delay = longpressTimeout;
@@ -987,16 +989,14 @@
     /**
      * Called when a key is long pressed.
      * @param tracker the pointer tracker which pressed the parent key
-     * @return true if the long press is handled, false otherwise. Subclasses should call the
-     * method on the base class if the subclass doesn't wish to handle the call.
      */
-    private boolean onLongPress(final PointerTracker tracker) {
+    private void onLongPress(final PointerTracker tracker) {
         if (isShowingMoreKeysPanel()) {
-            return false;
+            return;
         }
         final Key key = tracker.getKey();
         if (key == null) {
-            return false;
+            return;
         }
         if (ProductionFlag.USES_DEVELOPMENT_ONLY_DIAGNOSTICS) {
             ResearchLogger.mainKeyboardView_onLongPress();
@@ -1007,18 +1007,18 @@
             tracker.onLongPressed();
             invokeCodeInput(embeddedCode);
             invokeReleaseKey(code);
-            KeyboardSwitcher.getInstance().hapticAndAudioFeedback(code);
-            return true;
+            AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(code, this);
+            return;
         }
         if (code == Constants.CODE_SPACE || code == Constants.CODE_LANGUAGE_SWITCH) {
             // Long pressing the space key invokes IME switcher dialog.
             if (invokeCustomRequest(LatinIME.CODE_SHOW_INPUT_METHOD_PICKER)) {
                 tracker.onLongPressed();
                 invokeReleaseKey(code);
-                return true;
+                return;
             }
         }
-        return openMoreKeysPanel(key, tracker);
+        openMoreKeysPanel(key, tracker);
     }
 
     private boolean invokeCustomRequest(final int requestCode) {
@@ -1034,10 +1034,10 @@
         mKeyboardActionListener.onReleaseKey(code, false);
     }
 
-    private boolean openMoreKeysPanel(final Key key, final PointerTracker tracker) {
+    private void openMoreKeysPanel(final Key key, final PointerTracker tracker) {
         final MoreKeysPanel moreKeysPanel = onCreateMoreKeysPanel(key, getContext());
         if (moreKeysPanel == null) {
-            return false;
+            return;
         }
 
         final int[] lastCoords = CoordinateUtils.newInstance();
@@ -1059,7 +1059,6 @@
         final int translatedX = moreKeysPanel.translateX(CoordinateUtils.x(lastCoords));
         final int translatedY = moreKeysPanel.translateY(CoordinateUtils.y(lastCoords));
         tracker.onShowMoreKeysPanel(translatedX, translatedY, moreKeysPanel);
-        return true;
     }
 
     public boolean isInSlidingKeyInput() {
diff --git a/java/src/com/android/inputmethod/keyboard/PointerTracker.java b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
index 1742393..5df7011 100644
--- a/java/src/com/android/inputmethod/keyboard/PointerTracker.java
+++ b/java/src/com/android/inputmethod/keyboard/PointerTracker.java
@@ -1266,15 +1266,13 @@
         if (!key.isRepeatable()) return;
         // Don't start key repeat when we are in sliding input mode.
         if (mIsInSlidingKeyInput) return;
-        onRegisterKey(key);
+        onRepeatKey(key);
         mTimerProxy.startKeyRepeatTimer(this);
     }
 
-    public void onRegisterKey(final Key key) {
-        if (key != null) {
-            detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
-            mTimerProxy.startTypingStateTimer(key);
-        }
+    public void onRepeatKey(final Key key) {
+        detectAndSendKey(key, key.mX, key.mY, SystemClock.uptimeMillis());
+        mTimerProxy.startTypingStateTimer(key);
     }
 
     private boolean isMajorEnoughMoveToBeOnNewKey(final int x, final int y, final long eventTime,
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index b1813a1..ba449ee 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -53,7 +53,9 @@
     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
 
     // Constants for parsing.
-    private static final char LABEL_END = '|';
+    private static final char COMMA = ',';
+    private static final char BACKSLASH = '\\';
+    private static final char VERTICAL_BAR = '|';
     private static final String PREFIX_TEXT = "!text/";
     static final String PREFIX_ICON = "!icon/";
     private static final String PREFIX_CODE = "!code/";
@@ -64,6 +66,59 @@
         // Intentional empty constructor for utility class.
     }
 
+    /**
+     * Split the text containing multiple key specifications separated by commas into an array of
+     * key specifications.
+     * A key specification can contain a character escaped by the backslash character, including a
+     * comma character.
+     * Note that an empty key specification will be eliminated from the result array.
+     *
+     * @param text the text containing multiple key specifications.
+     * @return an array of key specification text. Null if the specified <code>text</code> is empty
+     * or has no key specifications.
+     */
+    public static String[] splitKeySpecs(final String text) {
+        final int size = text.length();
+        if (size == 0) {
+            return null;
+        }
+        // Optimization for one-letter key specification.
+        if (size == 1) {
+            return text.charAt(0) == COMMA ? null : new String[] { text };
+        }
+
+        ArrayList<String> list = null;
+        int start = 0;
+        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
+        // match any high or low surrogate character. So it is OK to iterate through with char
+        // index.
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == COMMA) {
+                // Skip empty entry.
+                if (pos - start > 0) {
+                    if (list == null) {
+                        list = CollectionUtils.newArrayList();
+                    }
+                    list.add(text.substring(start, pos));
+                }
+                // Skip comma
+                start = pos + 1;
+            } else if (c == BACKSLASH) {
+                // Skip escape character and escaped character.
+                pos++;
+            }
+        }
+        final String remain = (size - start > 0) ? text.substring(start) : null;
+        if (list == null) {
+            return remain != null ? new String[] { remain } : null;
+        }
+        if (remain != null) {
+            list.add(remain);
+        }
+        return list.toArray(new String[list.size()]);
+    }
+
     private static boolean hasIcon(final String moreKeySpec) {
         return moreKeySpec.startsWith(PREFIX_ICON);
     }
@@ -78,14 +133,14 @@
     }
 
     private static String parseEscape(final String text) {
-        if (text.indexOf(Constants.CSV_ESCAPE) < 0) {
+        if (text.indexOf(BACKSLASH) < 0) {
             return text;
         }
         final int length = text.length();
         final StringBuilder sb = new StringBuilder();
         for (int pos = 0; pos < length; pos++) {
             final char c = text.charAt(pos);
-            if (c == Constants.CSV_ESCAPE && pos + 1 < length) {
+            if (c == BACKSLASH && pos + 1 < length) {
                 // Skip escape char
                 pos++;
                 sb.append(text.charAt(pos));
@@ -97,20 +152,20 @@
     }
 
     private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
-        if (moreKeySpec.indexOf(Constants.CSV_ESCAPE, start) < 0) {
-            final int end = moreKeySpec.indexOf(LABEL_END, start);
+        if (moreKeySpec.indexOf(BACKSLASH, start) < 0) {
+            final int end = moreKeySpec.indexOf(VERTICAL_BAR, start);
             if (end == 0) {
-                throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec);
+                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec);
             }
             return end;
         }
         final int length = moreKeySpec.length();
         for (int pos = start; pos < length; pos++) {
             final char c = moreKeySpec.charAt(pos);
-            if (c == Constants.CSV_ESCAPE && pos + 1 < length) {
+            if (c == BACKSLASH && pos + 1 < length) {
                 // Skip escape char
                 pos++;
-            } else if (c == LABEL_END) {
+            } else if (c == VERTICAL_BAR) {
                 return pos;
             }
         }
@@ -136,9 +191,9 @@
             return null;
         }
         if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-            throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
         }
-        return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1));
+        return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1));
     }
 
     static String getOutputText(final String moreKeySpec) {
@@ -169,7 +224,7 @@
         if (hasCode(moreKeySpec)) {
             final int end = indexOfLabelEnd(moreKeySpec, 0);
             if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
             }
             return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
         }
@@ -204,7 +259,7 @@
 
     public static int getIconId(final String moreKeySpec) {
         if (moreKeySpec != null && hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
+            final int end = moreKeySpec.indexOf(VERTICAL_BAR, PREFIX_ICON.length());
             final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length())
                     : moreKeySpec.substring(PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
@@ -351,7 +406,7 @@
                     final String name = text.substring(pos + prefixLen, end);
                     sb.append(textsSet.getText(name));
                     pos = end - 1;
-                } else if (c == Constants.CSV_ESCAPE) {
+                } else if (c == BACKSLASH) {
                     if (sb != null) {
                         // Append both escape character and escaped character.
                         sb.append(text.substring(pos, Math.min(pos + 2, size)));
@@ -366,7 +421,6 @@
                 text = sb.toString();
             }
         } while (sb != null);
-
         return text;
     }
 
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index 5db3ebb..f650569 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -18,8 +18,6 @@
 
 import android.content.res.TypedArray;
 
-import com.android.inputmethod.latin.StringUtils;
-
 public abstract class KeyStyle {
     private final KeyboardTextsSet mTextsSet;
 
@@ -42,7 +40,7 @@
     protected String[] parseStringArray(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
             final String text = KeySpecParser.resolveTextReference(a.getString(index), mTextsSet);
-            return StringUtils.parseCsvString(text);
+            return KeySpecParser.splitKeySpecs(text);
         }
         return null;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
index 6af1bd7..9f6374b 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardState.java
@@ -58,7 +58,6 @@
         public void cancelDoubleTapTimer();
         public void startLongPressTimer(int code);
         public void cancelLongPressTimer();
-        public void hapticAndAudioFeedback(int code);
     }
 
     private final SwitchActions mSwitchActions;
@@ -387,7 +386,6 @@
         }
         if (mIsAlphabetMode && code == Constants.CODE_SHIFT) {
             mLongPressShiftLockFired = true;
-            mSwitchActions.hapticAndAudioFeedback(code);
         }
     }
 
@@ -576,11 +574,6 @@
         }
     }
 
-    public boolean isInMomentarySwitchState() {
-        return mSwitchState == SWITCH_STATE_MOMENTARY_ALPHA_AND_SYMBOL
-                || mSwitchState == SWITCH_STATE_MOMENTARY_SYMBOL_AND_MORE;
-    }
-
     private static boolean isSpaceCharacter(final int c) {
         return c == Constants.CODE_SPACE || c == Constants.CODE_ENTER;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
new file mode 100644
index 0000000..4916a15
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/MatrixUtils.java
@@ -0,0 +1,166 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Utilities for matrix operations. Don't instantiate objects inside this class to prevent
+ * unexpected performance regressions.
+ */
+@UsedForTesting
+public class MatrixUtils {
+    private static final String TAG = MatrixUtils.class.getSimpleName();
+    public static class MatrixOperationFailedException extends Exception {
+        private static final String TAG = MatrixOperationFailedException.class.getSimpleName();
+        private static final long serialVersionUID = 4384485606788583829L;
+
+        public MatrixOperationFailedException(String msg) {
+            super(msg);
+            Log.d(TAG, msg);
+        }
+    }
+
+    /**
+     * A utility function to inverse matrix.
+     * Find a pivot and swap the row of squareMatrix0 and squareMatrix1
+     */
+    private static void findPivotAndSwapRow(final int row, final float[][] squareMatrix0,
+            final float[][] squareMatrix1, final int size) {
+        int ip = row;
+        float pivot = Math.abs(squareMatrix0[row][row]);
+        for (int i = row + 1; i < size; ++i) {
+            if (pivot < Math.abs(squareMatrix0[i][row])) {
+                ip = i;
+                pivot = Math.abs(squareMatrix0[i][row]);
+            }
+        }
+        if (ip != row) {
+            for (int j = 0; j < size; ++j) {
+                final float temp0 = squareMatrix0[ip][j];
+                squareMatrix0[ip][j] = squareMatrix0[row][j];
+                squareMatrix0[row][j] = temp0;
+                final float temp1 = squareMatrix1[ip][j];
+                squareMatrix1[ip][j] = squareMatrix1[row][j];
+                squareMatrix1[row][j] = temp1;
+            }
+        }
+    }
+
+    /**
+     * A utility function to inverse matrix. This function calculates answer for each row by
+     * sweeping method of Gauss Jordan elimination
+     */
+    private static void sweep(final int row, final float[][] squareMatrix0,
+            final float[][] squareMatrix1, final int size) throws MatrixOperationFailedException {
+        final float pivot = squareMatrix0[row][row];
+        if (pivot == 0) {
+            throw new MatrixOperationFailedException("Inverse failed. Invalid pivot");
+        }
+        for (int j = 0; j < size; ++j) {
+            squareMatrix0[row][j] /= pivot;
+            squareMatrix1[row][j] /= pivot;
+        }
+        for (int i = 0; i < size; i++) {
+            final float sweepTargetValue = squareMatrix0[i][row];
+            if (i != row) {
+                for (int j = row; j < size; ++j) {
+                    squareMatrix0[i][j] -= sweepTargetValue * squareMatrix0[row][j];
+                }
+                for (int j = 0; j < size; ++j) {
+                    squareMatrix1[i][j] -= sweepTargetValue * squareMatrix1[row][j];
+                }
+            }
+        }
+    }
+
+    /**
+     * A function to inverse matrix.
+     * The inverse matrix of squareMatrix will be output to inverseMatrix. Please notice that
+     * the value of squareMatrix is modified in this function and can't be resuable.
+     */
+    @UsedForTesting
+    public static void inverse(final float[][] squareMatrix,
+            final float[][] inverseMatrix) throws MatrixOperationFailedException {
+        final int size = squareMatrix.length;
+        if (squareMatrix[0].length != size || inverseMatrix.length != size
+                || inverseMatrix[0].length != size) {
+            throw new MatrixOperationFailedException(
+                    "--- invalid length. column should be 2 times larger than row.");
+        }
+        for (int i = 0; i < size; ++i) {
+            Arrays.fill(inverseMatrix[i], 0.0f);
+            inverseMatrix[i][i] = 1.0f;
+        }
+        for (int i = 0; i < size; ++i) {
+            findPivotAndSwapRow(i, squareMatrix, inverseMatrix, size);
+            sweep(i, squareMatrix, inverseMatrix, size);
+        }
+    }
+
+    /**
+     * A matrix operation to multiply m0 and m1.
+     */
+    @UsedForTesting
+    public static void multiply(final float[][] m0, final float[][] m1,
+            final float[][] retval) throws MatrixOperationFailedException {
+        if (m0[0].length != m1.length) {
+            throw new MatrixOperationFailedException(
+                    "--- invalid length for multiply " + m0[0].length + ", " + m1.length);
+        }
+        final int m0h = m0.length;
+        final int m0w = m0[0].length;
+        final int m1w = m1[0].length;
+        if (retval.length != m0h || retval[0].length != m1w) {
+            throw new MatrixOperationFailedException(
+                    "--- invalid length of retval " + retval.length + ", " + retval[0].length);
+        }
+
+        for (int i = 0; i < m0h; i++) {
+            Arrays.fill(retval[i], 0);
+            for (int j = 0; j < m1w; j++) {
+                for (int k = 0; k < m0w; k++) {
+                    retval[i][j] += m0[i][k] * m1[k][j];
+                }
+            }
+        }
+    }
+
+    /**
+     * A utility function to dump the specified matrix in a readable way
+     */
+    @UsedForTesting
+    public static void dump(final String title, final float[][] a) {
+        final int column = a[0].length;
+        final int row = a.length;
+        Log.d(TAG, "Dump matrix: " + title);
+        Log.d(TAG, "/*---------------------");
+        final StringBuilder sb = new StringBuilder();
+        for (int i = 0; i < row; ++i) {
+            sb.setLength(0);
+            for (int j = 0; j < column; ++j) {
+                sb.append(String.format("%4f", a[i][j])).append(' ');
+            }
+            Log.d(TAG, sb.toString());
+        }
+        Log.d(TAG, "---------------------*/");
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
new file mode 100644
index 0000000..e5665bc
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/SmoothingUtils.java
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.keyboard.internal.MatrixUtils.MatrixOperationFailedException;
+
+import android.util.Log;
+
+import java.util.Arrays;
+
+/**
+ * Utilities to smooth coordinates. Currently, we calculate 3d least squares formula by using
+ * Lagrangian smoothing
+ */
+@UsedForTesting
+public class SmoothingUtils {
+    private static final String TAG = SmoothingUtils.class.getSimpleName();
+    private static final boolean DEBUG = false;
+
+    private SmoothingUtils() {
+        // not allowed to instantiate publicly
+    }
+
+    /**
+     * Find a most likely 3d least squares formula for specified coordinates.
+     * "retval" should be a 1x4 size matrix.
+     */
+    @UsedForTesting
+    public static void get3DParameters(final float[] xs, final float[] ys,
+            final float[][] retval) throws MatrixOperationFailedException {
+        final int COEFF_COUNT = 4; // Coefficient count for 3d smoothing
+        if (retval.length != COEFF_COUNT || retval[0].length != 1) {
+            Log.d(TAG, "--- invalid length of 3d retval " + retval.length + ", "
+                    + retval[0].length);
+            return;
+        }
+        final int N = xs.length;
+        // TODO: Never isntantiate the matrix
+        final float[][] m0 = new float[COEFF_COUNT][COEFF_COUNT];
+        final float[][] m0Inv = new float[COEFF_COUNT][COEFF_COUNT];
+        final float[][] m1 = new float[COEFF_COUNT][N];
+        final float[][] m2 = new float[N][1];
+
+        // m0
+        for (int i = 0; i < COEFF_COUNT; ++i) {
+            Arrays.fill(m0[i], 0);
+            for (int j = 0; j < COEFF_COUNT; ++j) {
+                final int pow = i + j;
+                for (int k = 0; k < N; ++k) {
+                    m0[i][j] += (float) Math.pow((double) xs[k], pow);
+                }
+            }
+        }
+        // m0Inv
+        MatrixUtils.inverse(m0, m0Inv);
+        if (DEBUG) {
+            MatrixUtils.dump("m0-1", m0Inv);
+        }
+
+        // m1
+        for (int i = 0; i < COEFF_COUNT; ++i) {
+            for (int j = 0; j < N; ++j) {
+                m1[i][j] = (i == 0) ? 1.0f : m1[i - 1][j] * xs[j];
+            }
+        }
+
+        // m2
+        for (int i = 0; i < N; ++i) {
+            m2[i][0] = ys[i];
+        }
+
+        final float[][] m0Invxm1 = new float[COEFF_COUNT][N];
+        if (DEBUG) {
+            MatrixUtils.dump("a0", m0Inv);
+            MatrixUtils.dump("a1", m1);
+        }
+        MatrixUtils.multiply(m0Inv, m1, m0Invxm1);
+        if (DEBUG) {
+            MatrixUtils.dump("a2", m0Invxm1);
+            MatrixUtils.dump("a3", m2);
+        }
+        MatrixUtils.multiply(m0Invxm1, m2, retval);
+        if (DEBUG) {
+            MatrixUtils.dump("result", retval);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java b/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java
new file mode 100644
index 0000000..0fdaea5
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/AdditionalFeaturesSettingUtils.java
@@ -0,0 +1,47 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+import com.android.inputmethodcommon.InputMethodSettingsFragment;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+
+/**
+ * Utility class for managing additional features settings.
+ */
+public class AdditionalFeaturesSettingUtils {
+    public static final int ADDITIONAL_FEATURES_SETTINGS_SIZE = 0;
+
+    private AdditionalFeaturesSettingUtils() {
+        // This utility class is not publicly instantiable.
+    }
+
+    public static void addAdditionalFeaturesPreferences(
+            final Context context, final InputMethodSettingsFragment settingsFragment) {
+        // do nothing.
+    }
+
+    public static void readAdditionalFeaturesPreferencesIntoArray(
+            final SharedPreferences prefs, final int[] additionalFeaturesPreferences) {
+        // do nothing.
+    }
+
+    public static int[] getAdditionalNativeSuggestOptions() {
+        return Settings.getInstance().getCurrent().mAdditionalFeaturesSettingValues;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
index 99b95ea..85b14d8 100644
--- a/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
+++ b/java/src/com/android/inputmethod/latin/AdditionalSubtype.java
@@ -49,13 +49,14 @@
                 && SubtypeLocale.isExceptionalLocale(localeString)) {
             final String layoutDisplayName = SubtypeLocale.getKeyboardLayoutSetDisplayName(
                     keyboardLayoutSetName);
-            layoutDisplayNameExtraValue = StringUtils.appendToCsvIfNotExists(
+            layoutDisplayNameExtraValue = StringUtils.appendToCommaSplittableTextIfNotExists(
                     UNTRANSLATABLE_STRING_IN_SUBTYPE_NAME + "=" + layoutDisplayName, extraValue);
         } else {
             layoutDisplayNameExtraValue = extraValue;
         }
-        final String additionalSubtypeExtraValue = StringUtils.appendToCsvIfNotExists(
-                IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
+        final String additionalSubtypeExtraValue =
+                StringUtils.appendToCommaSplittableTextIfNotExists(
+                        IS_ADDITIONAL_SUBTYPE, layoutDisplayNameExtraValue);
         final int nameId = SubtypeLocale.getSubtypeNameId(localeString, keyboardLayoutSetName);
         return new InputMethodSubtype(nameId, R.drawable.ic_subtype_keyboard,
                 localeString, KEYBOARD_MODE,
@@ -66,8 +67,9 @@
         final String localeString = subtype.getLocale();
         final String keyboardLayoutSetName = SubtypeLocale.getKeyboardLayoutSetName(subtype);
         final String layoutExtraValue = KEYBOARD_LAYOUT_SET + "=" + keyboardLayoutSetName;
-        final String extraValue = StringUtils.removeFromCsvIfExists(layoutExtraValue,
-                StringUtils.removeFromCsvIfExists(IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
+        final String extraValue = StringUtils.removeFromCommaSplittableTextIfExists(
+                layoutExtraValue, StringUtils.removeFromCommaSplittableTextIfExists(
+                        IS_ADDITIONAL_SUBTYPE, subtype.getExtraValue()));
         final String basePrefSubtype = localeString + LOCALE_AND_LAYOUT_SEPARATOR
                 + keyboardLayoutSetName;
         return extraValue.isEmpty() ? basePrefSubtype
diff --git a/java/src/com/android/inputmethod/latin/AutoCorrection.java b/java/src/com/android/inputmethod/latin/AutoCorrection.java
index fa35922..86be429 100644
--- a/java/src/com/android/inputmethod/latin/AutoCorrection.java
+++ b/java/src/com/android/inputmethod/latin/AutoCorrection.java
@@ -32,12 +32,13 @@
         // Purely static class: can't instantiate.
     }
 
-    public static boolean isValidWord(final ConcurrentHashMap<String, Dictionary> dictionaries,
-            final String word, final boolean ignoreCase) {
+    public static boolean isValidWord(final Suggest suggest, final String word,
+            final boolean ignoreCase) {
         if (TextUtils.isEmpty(word)) {
             return false;
         }
-        final String lowerCasedWord = word.toLowerCase();
+        final ConcurrentHashMap<String, Dictionary> dictionaries = suggest.getUnigramDictionaries();
+        final String lowerCasedWord = word.toLowerCase(suggest.mLocale);
         for (final String key : dictionaries.keySet()) {
             final Dictionary dictionary = dictionaries.get(key);
             // It's unclear how realistically 'dictionary' can be null, but the monkey is somehow
@@ -73,13 +74,6 @@
         return maxFreq;
     }
 
-    // Returns true if this is in any of the dictionaries.
-    public static boolean isInTheDictionary(
-            final ConcurrentHashMap<String, Dictionary> dictionaries,
-            final String word, final boolean ignoreCase) {
-        return isValidWord(dictionaries, word, ignoreCase);
-    }
-
     public static boolean suggestionExceedsAutoCorrectionThreshold(
             final SuggestedWordInfo suggestion, final String consideredWord,
             final float autoCorrectionThreshold) {
diff --git a/java/src/com/android/inputmethod/latin/BinaryDictionary.java b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
index 4fc1919..aad129d 100644
--- a/java/src/com/android/inputmethod/latin/BinaryDictionary.java
+++ b/java/src/com/android/inputmethod/latin/BinaryDictionary.java
@@ -45,7 +45,7 @@
     private final int[] mOutputScores = new int[MAX_RESULTS];
     private final int[] mOutputTypes = new int[MAX_RESULTS];
 
-    private final boolean mUseFullEditDistance;
+    private final NativeSuggestOptions mNativeSuggestOptions = new NativeSuggestOptions();
 
     private final SparseArray<DicTraverseSession> mDicTraverseSessions =
             CollectionUtils.newSparseArray();
@@ -79,7 +79,7 @@
             final boolean useFullEditDistance, final Locale locale, final String dictType) {
         super(dictType);
         mLocale = locale;
-        mUseFullEditDistance = useFullEditDistance;
+        mNativeSuggestOptions.setUseFullEditDistance(useFullEditDistance);
         loadDictionary(filename, offset, length);
     }
 
@@ -94,7 +94,7 @@
     private static native int getSuggestionsNative(long dict, long proximityInfo,
             long traverseSession, int[] xCoordinates, int[] yCoordinates, int[] times,
             int[] pointerIds, int[] inputCodePoints, int inputSize, int commitPoint,
-            boolean isGesture, int[] prevWordCodePointArray, boolean useFullEditDistance,
+            int[] suggestOptions, int[] prevWordCodePointArray,
             int[] outputCodePoints, int[] outputScores, int[] outputIndices, int[] outputTypes);
     private static native float calcNormalizedScoreNative(int[] before, int[] after, int score);
     private static native int editDistanceNative(int[] before, int[] after);
@@ -135,12 +135,15 @@
 
         final InputPointers ips = composer.getInputPointers();
         final int inputSize = isGesture ? ips.getPointerSize() : composerSize;
+        mNativeSuggestOptions.setIsGesture(isGesture);
+        mNativeSuggestOptions.setAdditionalFeaturesOptions(
+                AdditionalFeaturesSettingUtils.getAdditionalNativeSuggestOptions());
         // proximityInfo and/or prevWordForBigrams may not be null.
         final int count = getSuggestionsNative(mNativeDict, proximityInfo.getNativeProximityInfo(),
                 getTraverseSession(sessionId).getSession(), ips.getXCoordinates(),
                 ips.getYCoordinates(), ips.getTimes(), ips.getPointerIds(), mInputCodePoints,
-                inputSize, 0 /* commitPoint */, isGesture, prevWordCodePointArray,
-                mUseFullEditDistance, mOutputCodePoints, mOutputScores, mSpaceIndices,
+                inputSize, 0 /* commitPoint */, mNativeSuggestOptions.getOptions(),
+                prevWordCodePointArray, mOutputCodePoints, mOutputScores, mSpaceIndices,
                 mOutputTypes);
         final ArrayList<SuggestedWordInfo> suggestions = CollectionUtils.newArrayList();
         for (int j = 0; j < count; ++j) {
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 86bb255..64c14d3 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -215,10 +215,6 @@
         }
     }
 
-    // Constants for CSV parsing.
-    public static final char CSV_SEPARATOR = ',';
-    public static final char CSV_ESCAPE = '\\';
-
     private Constants() {
         // This utility class is not publicly instantiable.
     }
diff --git a/java/src/com/android/inputmethod/latin/InputAttributes.java b/java/src/com/android/inputmethod/latin/InputAttributes.java
index dd58db5..1f673e9 100644
--- a/java/src/com/android/inputmethod/latin/InputAttributes.java
+++ b/java/src/com/android/inputmethod/latin/InputAttributes.java
@@ -199,6 +199,6 @@
         if (editorInfo == null) return false;
         final String findingKey = (packageName != null) ? packageName + "." + key
                 : key;
-        return StringUtils.containsInCsv(findingKey, editorInfo.privateImeOptions);
+        return StringUtils.containsInCommaSplittableText(findingKey, editorInfo.privateImeOptions);
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/LatinIME.java b/java/src/com/android/inputmethod/latin/LatinIME.java
index f85c16b..cebc93c 100644
--- a/java/src/com/android/inputmethod/latin/LatinIME.java
+++ b/java/src/com/android/inputmethod/latin/LatinIME.java
@@ -480,6 +480,7 @@
         final InputAttributes inputAttributes =
                 new InputAttributes(getCurrentInputEditorInfo(), isFullscreenMode());
         mSettings.loadSettings(locale, inputAttributes);
+        AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(mSettings.getCurrent());
         // May need to reset the contacts dictionary depending on the user settings.
         resetContactsDictionary(null == mSuggest ? null : mSuggest.getContactsDictionary());
     }
@@ -2368,9 +2369,11 @@
         // Please note that if mSuggest is null, it means that everything is off: suggestion
         // and correction, so we shouldn't try to show the hint
         final boolean showingAddToDictionaryHint =
-                SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind && mSuggest != null
-                // If the suggestion is not in the dictionary, the hint should be shown.
-                && !AutoCorrection.isValidWord(mSuggest.getUnigramDictionaries(), suggestion, true);
+                (SuggestedWordInfo.KIND_TYPED == suggestionInfo.mKind
+                        || SuggestedWordInfo.KIND_OOV_CORRECTION == suggestionInfo.mKind)
+                        && mSuggest != null
+                        // If the suggestion is not in the dictionary, the hint should be shown.
+                        && !AutoCorrection.isValidWord(mSuggest, suggestion, true);
 
         if (mSettings.isInternal()) {
             Stats.onSeparator((char)Constants.CODE_SPACE,
@@ -2701,7 +2704,7 @@
             if (action.equals(ConnectivityManager.CONNECTIVITY_ACTION)) {
                 mSubtypeSwitcher.onNetworkStateChanged(intent);
             } else if (action.equals(AudioManager.RINGER_MODE_CHANGED_ACTION)) {
-                mKeyboardSwitcher.onRingerModeChanged();
+                AudioAndHapticFeedbackManager.getInstance().onRingerModeChanged();
             }
         }
     };
diff --git a/java/src/com/android/inputmethod/latin/LocaleUtils.java b/java/src/com/android/inputmethod/latin/LocaleUtils.java
index 5fde815..a1e4050 100644
--- a/java/src/com/android/inputmethod/latin/LocaleUtils.java
+++ b/java/src/com/android/inputmethod/latin/LocaleUtils.java
@@ -148,7 +148,7 @@
     public static String getMatchLevelSortedString(int matchLevel) {
         // This works because the match levels are 0~99 (actually 0~30)
         // Ideally this should use a number of digits equals to the 1og10 of the greater matchLevel
-        return String.format("%02d", MATCH_LEVEL_MAX - matchLevel);
+        return String.format(Locale.ROOT, "%02d", MATCH_LEVEL_MAX - matchLevel);
     }
 
     /**
diff --git a/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java b/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java
new file mode 100644
index 0000000..2915513
--- /dev/null
+++ b/java/src/com/android/inputmethod/latin/NativeSuggestOptions.java
@@ -0,0 +1,53 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.latin;
+
+public class NativeSuggestOptions {
+    // Need to update suggest_options.h when you add, remove or reorder options.
+    private static final int IS_GESTURE = 0;
+    private static final int USE_FULL_EDIT_DISTANCE = 1;
+    private static final int OPTIONS_SIZE = 2;
+
+    private final int[] mOptions = new int[OPTIONS_SIZE
+            + AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
+
+    public void setIsGesture(final boolean value) {
+        setBooleanOption(IS_GESTURE, value);
+    }
+
+    public void setUseFullEditDistance(final boolean value) {
+        setBooleanOption(USE_FULL_EDIT_DISTANCE, value);
+    }
+
+    public void setAdditionalFeaturesOptions(final int[] additionalOptions) {
+        for (int i = 0; i < additionalOptions.length; i++) {
+            setIntegerOption(OPTIONS_SIZE + i, additionalOptions[i]);
+        }
+    }
+
+    public int[] getOptions() {
+        return mOptions;
+    }
+
+    private void setBooleanOption(final int key, final boolean value) {
+        mOptions[key] = value ? 1 : 0;
+    }
+
+    private void setIntegerOption(final int key, final int value) {
+        mOptions[key] = value;
+    }
+}
diff --git a/java/src/com/android/inputmethod/latin/ResourceUtils.java b/java/src/com/android/inputmethod/latin/ResourceUtils.java
index a9fba53..0eb8b4f 100644
--- a/java/src/com/android/inputmethod/latin/ResourceUtils.java
+++ b/java/src/com/android/inputmethod/latin/ResourceUtils.java
@@ -27,6 +27,7 @@
 
 import java.util.ArrayList;
 import java.util.HashMap;
+import java.util.regex.PatternSyntaxException;
 
 public final class ResourceUtils {
     private static final String TAG = ResourceUtils.class.getSimpleName();
@@ -83,22 +84,39 @@
             return overrideValue;
         }
 
-        final String defaultValue = findDefaultConstant(overrideArray);
-        // The defaultValue might be an empty string.
-        if (defaultValue == null) {
-            Log.w(TAG, "Couldn't find override value nor default value:"
-                    + " resource="+ res.getResourceEntryName(overrideResId)
-                    + " build=" + sBuildKeyValuesDebugString);
-        } else {
-            Log.i(TAG, "Found default value:"
-                    + " resource="+ res.getResourceEntryName(overrideResId)
-                    + " build=" + sBuildKeyValuesDebugString
-                    + " default=" + defaultValue);
+        String defaultValue = null;
+        try {
+            defaultValue = findDefaultConstant(overrideArray);
+            // The defaultValue might be an empty string.
+            if (defaultValue == null) {
+                Log.w(TAG, "Couldn't find override value nor default value:"
+                        + " resource="+ res.getResourceEntryName(overrideResId)
+                        + " build=" + sBuildKeyValuesDebugString);
+            } else {
+                Log.i(TAG, "Found default value:"
+                        + " resource="+ res.getResourceEntryName(overrideResId)
+                        + " build=" + sBuildKeyValuesDebugString
+                        + " default=" + defaultValue);
+            }
+        } catch (final DeviceOverridePatternSyntaxError e) {
+            Log.w(TAG, "Syntax error, ignored", e);
         }
         sDeviceOverrideValueMap.put(key, defaultValue);
         return defaultValue;
     }
 
+    @SuppressWarnings("serial")
+    static class DeviceOverridePatternSyntaxError extends Exception {
+        public DeviceOverridePatternSyntaxError(final String message, final String expression) {
+            this(message, expression, null);
+        }
+
+        public DeviceOverridePatternSyntaxError(final String message, final String expression,
+                final Throwable throwable) {
+            super(message + ": " + expression, throwable);
+        }
+    }
+
     /**
      * Find the condition that fulfills specified key value pairs from an array of
      * "condition,constant", and return the corresponding string constant. A condition is
@@ -123,10 +141,12 @@
         if (conditionConstantArray == null || keyValuePairs == null) {
             return null;
         }
+        String foundValue = null;
         for (final String conditionConstant : conditionConstantArray) {
             final int posComma = conditionConstant.indexOf(',');
             if (posComma < 0) {
-                throw new RuntimeException("Array element has no comma: " + conditionConstant);
+                Log.w(TAG, "Array element has no comma: " + conditionConstant);
+                continue;
             }
             final String condition = conditionConstant.substring(0, posComma);
             if (condition.isEmpty()) {
@@ -134,44 +154,59 @@
                 // {@link #findConstantForDefault(String[])}.
                 continue;
             }
-            if (fulfillsCondition(keyValuePairs, condition)) {
-                return conditionConstant.substring(posComma + 1);
+            try {
+                if (fulfillsCondition(keyValuePairs, condition)) {
+                    // Take first match
+                    if (foundValue == null) {
+                        foundValue = conditionConstant.substring(posComma + 1);
+                    }
+                    // And continue walking through all conditions.
+                }
+            } catch (final DeviceOverridePatternSyntaxError e) {
+                Log.w(TAG, "Syntax error, ignored", e);
             }
         }
-        return null;
+        return foundValue;
     }
 
     private static boolean fulfillsCondition(final HashMap<String,String> keyValuePairs,
-            final String condition) {
+            final String condition) throws DeviceOverridePatternSyntaxError {
         final String[] patterns = condition.split(":");
         // Check all patterns in a condition are true
+        boolean matchedAll = true;
         for (final String pattern : patterns) {
             final int posEqual = pattern.indexOf('=');
             if (posEqual < 0) {
-                throw new RuntimeException("Pattern has no '=': " + condition);
+                throw new DeviceOverridePatternSyntaxError("Pattern has no '='", condition);
             }
             final String key = pattern.substring(0, posEqual);
             final String value = keyValuePairs.get(key);
             if (value == null) {
-                throw new RuntimeException("Found unknown key: " + condition);
+                throw new DeviceOverridePatternSyntaxError("Unknown key", condition);
             }
             final String patternRegexpValue = pattern.substring(posEqual + 1);
-            if (!value.matches(patternRegexpValue)) {
-                return false;
+            try {
+                if (!value.matches(patternRegexpValue)) {
+                    matchedAll = false;
+                    // And continue walking through all patterns.
+                }
+            } catch (final PatternSyntaxException e) {
+                throw new DeviceOverridePatternSyntaxError("Syntax error", condition, e);
             }
         }
-        return true;
+        return matchedAll;
     }
 
     @UsedForTesting
-    static String findDefaultConstant(final String[] conditionConstantArray) {
+    static String findDefaultConstant(final String[] conditionConstantArray)
+            throws DeviceOverridePatternSyntaxError {
         if (conditionConstantArray == null) {
             return null;
         }
         for (final String condition : conditionConstantArray) {
             final int posComma = condition.indexOf(',');
             if (posComma < 0) {
-                throw new RuntimeException("Array element has no comma: " + condition);
+                throw new DeviceOverridePatternSyntaxError("Array element has no comma", condition);
             }
             if (posComma == 0) { // condition is empty.
                 return condition.substring(posComma + 1);
diff --git a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
index 7c4156c..3ea9fed 100644
--- a/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
+++ b/java/src/com/android/inputmethod/latin/SeekBarDialogPreference.java
@@ -32,6 +32,7 @@
         public int readValue(final String key);
         public int readDefaultValue(final String key);
         public void writeValue(final int value, final String key);
+        public void writeDefaultValue(final String key);
         public void feedbackValue(final int value);
     }
 
@@ -122,12 +123,16 @@
     @Override
     public void onClick(final DialogInterface dialog, final int which) {
         super.onClick(dialog, which);
+        final String key = getKey();
         if (which == DialogInterface.BUTTON_NEUTRAL) {
-            setValue(clipValue(mValueProxy.readDefaultValue(getKey())), false /* fromUser */);
+            setValue(clipValue(mValueProxy.readDefaultValue(key)), false /* fromUser */);
+            mValueProxy.writeDefaultValue(key);
+            return;
         }
-        if (which != DialogInterface.BUTTON_NEGATIVE) {
+        if (which == DialogInterface.BUTTON_POSITIVE) {
             setSummary(mValueView.getText());
-            mValueProxy.writeValue(getClippedValueFromProgress(mSeekBar.getProgress()), getKey());
+            mValueProxy.writeValue(getClippedValueFromProgress(mSeekBar.getProgress()), key);
+            return;
         }
     }
 
diff --git a/java/src/com/android/inputmethod/latin/SettingsFragment.java b/java/src/com/android/inputmethod/latin/SettingsFragment.java
index 835ef7b..7225cd6 100644
--- a/java/src/com/android/inputmethod/latin/SettingsFragment.java
+++ b/java/src/com/android/inputmethod/latin/SettingsFragment.java
@@ -207,6 +207,8 @@
 
         if (!Settings.readFromBuildConfigIfGestureInputEnabled(res)) {
             removePreference(Settings.PREF_GESTURE_SETTINGS, getPreferenceScreen());
+        } else {
+            AdditionalFeaturesSettingUtils.addAdditionalFeaturesPreferences(context, this);
         }
 
         setupKeyLongpressTimeoutSettings(prefs, res);
@@ -327,6 +329,11 @@
             }
 
             @Override
+            public void writeDefaultValue(final String key) {
+                sp.edit().remove(key).apply();
+            }
+
+            @Override
             public int readValue(final String key) {
                 return Settings.readKeypressVibrationDuration(sp, res);
             }
@@ -357,6 +364,11 @@
             }
 
             @Override
+            public void writeDefaultValue(final String key) {
+                sp.edit().remove(key).apply();
+            }
+
+            @Override
             public int readValue(final String key) {
                 return Settings.readKeyLongpressTimeout(sp, res);
             }
@@ -395,6 +407,11 @@
             }
 
             @Override
+            public void writeDefaultValue(final String key) {
+                sp.edit().remove(key).apply();
+            }
+
+            @Override
             public int readValue(final String key) {
                 return getPercentageFromValue(Settings.readKeypressSoundVolume(sp, res));
             }
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 615b2df..0910244 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -80,6 +80,10 @@
     private final boolean mVoiceKeyEnabled;
     private final boolean mVoiceKeyOnMain;
 
+    // Setting values for additional features
+    public final int[] mAdditionalFeaturesSettingValues =
+            new int[AdditionalFeaturesSettingUtils.ADDITIONAL_FEATURES_SETTINGS_SIZE];
+
     // Debug settings
     public final boolean mIsInternal;
 
@@ -96,7 +100,7 @@
         mWordConnectors =
                 StringUtils.toCodePointArray(res.getString(R.string.symbols_word_connectors));
         Arrays.sort(mWordConnectors);
-        final String[] suggestPuncsSpec = StringUtils.parseCsvString(res.getString(
+        final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mWordSeparators = res.getString(R.string.symbols_word_separators);
@@ -149,6 +153,8 @@
                 Settings.PREF_SHOW_SUGGESTIONS_SETTING,
                 res.getString(R.string.prefs_suggestion_visibility_default_value));
         mSuggestionVisibility = createSuggestionVisibility(res, showSuggestionsSetting);
+        AdditionalFeaturesSettingUtils.readAdditionalFeaturesPreferencesIntoArray(
+                prefs, mAdditionalFeaturesSettingValues);
         mIsInternal = Settings.isInternal(prefs);
     }
 
diff --git a/java/src/com/android/inputmethod/latin/StringUtils.java b/java/src/com/android/inputmethod/latin/StringUtils.java
index ab050d7..c2fd4fb 100644
--- a/java/src/com/android/inputmethod/latin/StringUtils.java
+++ b/java/src/com/android/inputmethod/latin/StringUtils.java
@@ -35,33 +35,55 @@
         return text.codePointCount(0, text.length());
     }
 
-    public static boolean containsInArray(final String key, final String[] array) {
+    public static boolean containsInArray(final String text, final String[] array) {
         for (final String element : array) {
-            if (key.equals(element)) return true;
+            if (text.equals(element)) return true;
         }
         return false;
     }
 
-    public static boolean containsInCsv(final String key, final String csv) {
-        if (TextUtils.isEmpty(csv)) return false;
-        return containsInArray(key, csv.split(","));
+    /**
+     * Comma-Splittable Text is similar to Comma-Separated Values (CSV) but has much simpler syntax.
+     * Unlike CSV, Comma-Splittable Text has no escaping mechanism, so that the text can't contain
+     * a comma character in it.
+     */
+    private static final String SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT = ",";
+
+    public static boolean containsInCommaSplittableText(final String text,
+            final String extraValues) {
+        if (TextUtils.isEmpty(extraValues)) {
+            return false;
+        }
+        return containsInArray(text, extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT));
     }
 
-    public static String appendToCsvIfNotExists(final String key, final String csv) {
-        if (TextUtils.isEmpty(csv)) return key;
-        if (containsInCsv(key, csv)) return csv;
-        return csv + "," + key;
+    public static String appendToCommaSplittableTextIfNotExists(final String text,
+            final String extraValues) {
+        if (TextUtils.isEmpty(extraValues)) {
+            return text;
+        }
+        if (containsInCommaSplittableText(text, extraValues)) {
+            return extraValues;
+        }
+        return extraValues + SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT + text;
     }
 
-    public static String removeFromCsvIfExists(final String key, final String csv) {
-        if (TextUtils.isEmpty(csv)) return "";
-        final String[] elements = csv.split(",");
-        if (!containsInArray(key, elements)) return csv;
+    public static String removeFromCommaSplittableTextIfExists(final String text,
+            final String extraValues) {
+        if (TextUtils.isEmpty(extraValues)) {
+            return "";
+        }
+        final String[] elements = extraValues.split(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT);
+        if (!containsInArray(text, elements)) {
+            return extraValues;
+        }
         final ArrayList<String> result = CollectionUtils.newArrayList(elements.length - 1);
         for (final String element : elements) {
-            if (!key.equals(element)) result.add(element);
+            if (!text.equals(element)) {
+                result.add(element);
+            }
         }
-        return TextUtils.join(",", result);
+        return TextUtils.join(SEPARATOR_FOR_COMMA_SPLITTABLE_TEXT, result);
     }
 
     /**
@@ -131,44 +153,6 @@
         return codePoints;
     }
 
-    public static String[] parseCsvString(final String text) {
-        final int size = text.length();
-        if (size == 0) {
-            return null;
-        }
-        if (codePointCount(text) == 1) {
-            return text.codePointAt(0) == Constants.CSV_SEPARATOR ? null : new String[] { text };
-        }
-
-        ArrayList<String> list = null;
-        int start = 0;
-        for (int pos = 0; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            if (c == Constants.CSV_SEPARATOR) {
-                // Skip empty entry.
-                if (pos - start > 0) {
-                    if (list == null) {
-                        list = CollectionUtils.newArrayList();
-                    }
-                    list.add(text.substring(start, pos));
-                }
-                // Skip comma
-                start = pos + 1;
-            } else if (c == Constants.CSV_ESCAPE) {
-                // Skip escape character and escaped character.
-                pos++;
-            }
-        }
-        final String remain = (size - start > 0) ? text.substring(start) : null;
-        if (list == null) {
-            return remain != null ? new String[] { remain } : null;
-        }
-        if (remain != null) {
-            list.add(remain);
-        }
-        return list.toArray(new String[list.size()]);
-    }
-
     // This method assumes the text is not null. For the empty string, it returns CAPITALIZE_NONE.
     public static int getCapitalizationType(final String text) {
         // If the first char is not uppercase, then the word is either all lower case or
diff --git a/java/src/com/android/inputmethod/latin/Suggest.java b/java/src/com/android/inputmethod/latin/Suggest.java
index dc9bef2..5d580f2 100644
--- a/java/src/com/android/inputmethod/latin/Suggest.java
+++ b/java/src/com/android/inputmethod/latin/Suggest.java
@@ -229,7 +229,7 @@
         // or if it's a 2+ characters non-word (i.e. it's not in the dictionary).
         final boolean allowsToBeAutoCorrected = (null != whitelistedWord
                 && !whitelistedWord.equals(consideredWord))
-                || (consideredWord.length() > 1 && !AutoCorrection.isInTheDictionary(mDictionaries,
+                || (consideredWord.length() > 1 && !AutoCorrection.isValidWord(this,
                         consideredWord, wordComposer.isFirstCharCapitalized()));
 
         final boolean hasAutoCorrection;
@@ -379,7 +379,8 @@
                     typedWord, cur.toString(), cur.mScore);
             final String scoreInfoString;
             if (normalizedScore > 0) {
-                scoreInfoString = String.format("%d (%4.2f)", cur.mScore, normalizedScore);
+                scoreInfoString = String.format(
+                        Locale.ROOT, "%d (%4.2f)", cur.mScore, normalizedScore);
             } else {
                 scoreInfoString = Integer.toString(cur.mScore);
             }
diff --git a/java/src/com/android/inputmethod/latin/SuggestedWords.java b/java/src/com/android/inputmethod/latin/SuggestedWords.java
index dfddb0f..1f45327 100644
--- a/java/src/com/android/inputmethod/latin/SuggestedWords.java
+++ b/java/src/com/android/inputmethod/latin/SuggestedWords.java
@@ -132,7 +132,10 @@
         public static final int KIND_APP_DEFINED = 6; // Suggested by the application
         public static final int KIND_SHORTCUT = 7; // A shortcut
         public static final int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
-        public static final int KIND_RESUMED = 9; // A resumed suggestion (comes from a span)
+        // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only
+        // in java for re-correction)
+        public static final int KIND_RESUMED = 9;
+        public static final int KIND_OOV_CORRECTION = 10; // Most probable string correction
 
         public static final int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
         public static final int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
diff --git a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
index 93687e1..a446672 100644
--- a/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
+++ b/java/src/com/android/inputmethod/latin/personalization/AccountUtils.java
@@ -23,6 +23,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 
 public class AccountUtils {
     private AccountUtils() {
@@ -44,4 +45,22 @@
         }
         return retval;
     }
+
+    /**
+     * Get all device accounts having specified domain name.
+     * @param context application context
+     * @param domain domain name used for filtering
+     * @return List of account names that contain the specified domain name
+     */
+    public static List<String> getDeviceAccountsWithDomain(
+            final Context context, final String domain) {
+        final ArrayList<String> retval = new ArrayList<String>();
+        final String atDomain = "@" + domain.toLowerCase(Locale.ROOT);
+        for (final Account account : getAccounts(context)) {
+            if (account.name.toLowerCase(Locale.ROOT).endsWith(atDomain)) {
+                retval.add(account.name);
+            }
+        }
+        return retval;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
index ad350a0..9764610 100644
--- a/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
+++ b/java/src/com/android/inputmethod/latin/suggestions/SuggestionStripView.java
@@ -54,6 +54,7 @@
 import com.android.inputmethod.keyboard.MainKeyboardView;
 import com.android.inputmethod.keyboard.MoreKeysPanel;
 import com.android.inputmethod.keyboard.ViewLayoutUtils;
+import com.android.inputmethod.latin.AudioAndHapticFeedbackManager;
 import com.android.inputmethod.latin.AutoCorrection;
 import com.android.inputmethod.latin.CollectionUtils;
 import com.android.inputmethod.latin.Constants;
@@ -249,10 +250,12 @@
         }
 
         private CharSequence getStyledSuggestionWord(final SuggestedWords suggestedWords,
-                final int pos) {
-            final String word = suggestedWords.getWord(pos);
-            final boolean isAutoCorrect = pos == 1 && suggestedWords.willAutoCorrect();
-            final boolean isTypedWordValid = pos == 0 && suggestedWords.mTypedWordValid;
+                final int indexInSuggestedWords) {
+            final String word = suggestedWords.getWord(indexInSuggestedWords);
+            final boolean isAutoCorrect = indexInSuggestedWords == 1
+                    && suggestedWords.willAutoCorrect();
+            final boolean isTypedWordValid = indexInSuggestedWords == 0
+                    && suggestedWords.mTypedWordValid;
             if (!isAutoCorrect && !isTypedWordValid)
                 return word;
 
@@ -269,28 +272,31 @@
             return spannedWord;
         }
 
-        private int getWordPosition(final int index, final SuggestedWords suggestedWords) {
+        private int getIndexInSuggestedWords(final int indexInStrip,
+                final SuggestedWords suggestedWords) {
             // TODO: This works for 3 suggestions. Revisit this algorithm when there are 5 or more
             // suggestions.
-            final int centerPos = suggestedWords.willAutoCorrect() ? 1 : 0;
-            if (index == mCenterSuggestionIndex) {
-                return centerPos;
-            } else if (index == centerPos) {
+            final int mostImportantIndexInSuggestedWords = suggestedWords.willAutoCorrect() ? 1 : 0;
+            if (indexInStrip == mCenterSuggestionIndex) {
+                return mostImportantIndexInSuggestedWords;
+            } else if (indexInStrip == mostImportantIndexInSuggestedWords) {
                 return mCenterSuggestionIndex;
             } else {
-                return index;
+                return indexInStrip;
             }
         }
 
-        private int getSuggestionTextColor(final int index, final SuggestedWords suggestedWords,
-                final int pos) {
+        private int getSuggestionTextColor(final int indexInStrip,
+                final SuggestedWords suggestedWords) {
+            final int indexInSuggestedWords = getIndexInSuggestedWords(
+                    indexInStrip, suggestedWords);
             // TODO: Need to revisit this logic with bigram suggestions
-            final boolean isSuggested = (pos != 0);
+            final boolean isSuggested = (indexInSuggestedWords != 0);
 
             final int color;
-            if (index == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
+            if (indexInStrip == mCenterSuggestionIndex && suggestedWords.willAutoCorrect()) {
                 color = mColorAutoCorrect;
-            } else if (index == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
+            } else if (indexInStrip == mCenterSuggestionIndex && suggestedWords.mTypedWordValid) {
                 color = mColorValidTypedWord;
             } else if (isSuggested) {
                 color = mColorSuggested;
@@ -300,7 +306,7 @@
             if (LatinImeLogger.sDBG && suggestedWords.size() > 1) {
                 // If we auto-correct, then the autocorrection is in slot 0 and the typed word
                 // is in slot 1.
-                if (index == mCenterSuggestionIndex
+                if (indexInStrip == mCenterSuggestionIndex
                         && AutoCorrection.shouldBlockAutoCorrectionBySafetyNet(
                                 suggestedWords.getWord(1), suggestedWords.getWord(0))) {
                     return 0xFFFF0000;
@@ -337,67 +343,101 @@
             setupTexts(suggestedWords, countInStrip);
             mMoreSuggestionsAvailable = (suggestedWords.size() > countInStrip);
             int x = 0;
-            for (int index = 0; index < countInStrip; index++) {
-                final int pos = getWordPosition(index, suggestedWords);
-
-                if (index != 0) {
-                    final View divider = mDividers.get(pos);
+            for (int indexInStrip = 0; indexInStrip < countInStrip; indexInStrip++) {
+                if (indexInStrip != 0) {
+                    final View divider = mDividers.get(indexInStrip);
                     // Add divider if this isn't the left most suggestion in suggestions strip.
                     addDivider(stripView, divider);
                     x += divider.getMeasuredWidth();
                 }
 
-                final CharSequence styled = mTexts.get(pos);
-                final TextView word = mWords.get(pos);
-                if (index == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
-                    // TODO: This "more suggestions hint" should have nicely designed icon.
-                    word.setCompoundDrawablesWithIntrinsicBounds(
-                            null, null, null, mMoreSuggestionsHint);
-                    // HACK: To align with other TextView that has no compound drawables.
-                    word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
-                } else {
-                    word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
-                }
-
-                // Disable this suggestion if the suggestion is null or empty.
-                word.setEnabled(!TextUtils.isEmpty(styled));
-                word.setTextColor(getSuggestionTextColor(index, suggestedWords, pos));
-                final int width = getSuggestionWidth(index, stripWidth);
-                final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
-                final float scaleX = word.getTextScaleX();
-                word.setText(text); // TextView.setText() resets text scale x to 1.0.
-                word.setTextScaleX(scaleX);
+                final int width = getSuggestionWidth(indexInStrip, stripWidth);
+                final TextView word = layoutWord(suggestedWords, indexInStrip, width);
                 stripView.addView(word);
-                setLayoutWeight(
-                        word, getSuggestionWeight(index), ViewGroup.LayoutParams.MATCH_PARENT);
+                setLayoutWeight(word, getSuggestionWeight(indexInStrip),
+                        ViewGroup.LayoutParams.MATCH_PARENT);
                 x += word.getMeasuredWidth();
 
-                if (DBG && pos < suggestedWords.size()) {
-                    final String debugInfo = Utils.getDebugInfo(suggestedWords, pos);
-                    if (debugInfo != null) {
-                        final TextView info = mInfos.get(pos);
-                        info.setText(debugInfo);
-                        placer.addView(info);
-                        info.measure(ViewGroup.LayoutParams.WRAP_CONTENT,
-                                ViewGroup.LayoutParams.WRAP_CONTENT);
-                        final int infoWidth = info.getMeasuredWidth();
-                        final int y = info.getMeasuredHeight();
-                        ViewLayoutUtils.placeViewAt(
-                                info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
-                    }
+                if (DBG) {
+                    layoutDebugInfo(suggestedWords, indexInStrip, placer, x);
                 }
             }
         }
 
-        private int getSuggestionWidth(final int index, final int maxWidth) {
+        /**
+         * Format appropriately the suggested word indirectly specified by
+         * <code>indexInStrip</code> as text in a corresponding {@link TextView}. When the
+         * suggested word doesn't exist, the corresponding {@link TextView} will be disabled
+         * and never respond to user interaction. The suggested word may be shrunk or ellipsized to
+         * fit in the specified width.
+         *
+         * The <code>indexInStrip</code> argument is the index in the suggestion strip. The indices
+         * increase towards the right for LTR scripts and the left for RTL scripts, starting with 0.
+         * The index of the most important suggestion is in {@link #mCenterSuggestionIndex}. This
+         * usually doesn't match the index in <code>suggedtedWords</code> -- see
+         * {@link #getIndexInSuggestedWords(int,SuggestedWords)}.
+         *
+         * @param suggestedWords the list of suggestions.
+         * @param indexInStrip the in the suggestion strip.
+         * @param width the maximum width for layout in pixels.
+         * @return the {@link TextView} containing the suggested word appropriately formatted.
+         */
+        private TextView layoutWord(final SuggestedWords suggestedWords, final int indexInStrip,
+                final int width) {
+            final int indexInSuggestedWords = getIndexInSuggestedWords(
+                    indexInStrip, suggestedWords);
+            final CharSequence styled = mTexts.get(indexInSuggestedWords);
+            final TextView word = mWords.get(indexInSuggestedWords);
+            if (indexInStrip == mCenterSuggestionIndex && mMoreSuggestionsAvailable) {
+                // TODO: This "more suggestions hint" should have a nicely designed icon.
+                word.setCompoundDrawablesWithIntrinsicBounds(
+                        null, null, null, mMoreSuggestionsHint);
+                // HACK: Align with other TextViews that have no compound drawables.
+                word.setCompoundDrawablePadding(-mMoreSuggestionsHint.getIntrinsicHeight());
+            } else {
+                word.setCompoundDrawablesWithIntrinsicBounds(null, null, null, null);
+            }
+
+            // Disable this suggestion if the suggestion is null or empty.
+            word.setEnabled(!TextUtils.isEmpty(styled));
+            word.setTextColor(getSuggestionTextColor(indexInStrip, suggestedWords));
+            final CharSequence text = getEllipsizedText(styled, width, word.getPaint());
+            final float scaleX = word.getTextScaleX();
+            word.setText(text); // TextView.setText() resets text scale x to 1.0.
+            word.setTextScaleX(scaleX);
+            return word;
+        }
+
+        private void layoutDebugInfo(final SuggestedWords suggestedWords, final int indexInStrip,
+                final ViewGroup placer, final int x) {
+            final int indexInSuggestedWords = getIndexInSuggestedWords(
+                    indexInStrip, suggestedWords);
+            if (indexInSuggestedWords >= suggestedWords.size()) {
+                return;
+            }
+            final String debugInfo = Utils.getDebugInfo(suggestedWords, indexInSuggestedWords);
+            if (debugInfo == null) {
+                return;
+            }
+            final TextView info = mInfos.get(indexInSuggestedWords);
+            info.setText(debugInfo);
+            placer.addView(info);
+            info.measure(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT);
+            final int infoWidth = info.getMeasuredWidth();
+            final int y = info.getMeasuredHeight();
+            ViewLayoutUtils.placeViewAt(
+                    info, x - infoWidth, y, infoWidth, info.getMeasuredHeight());
+        }
+
+        private int getSuggestionWidth(final int indexInStrip, final int maxWidth) {
             final int paddings = mPadding * mSuggestionsCountInStrip;
             final int dividers = mDividerWidth * (mSuggestionsCountInStrip - 1);
             final int availableWidth = maxWidth - paddings - dividers;
-            return (int)(availableWidth * getSuggestionWeight(index));
+            return (int)(availableWidth * getSuggestionWeight(indexInStrip));
         }
 
-        private float getSuggestionWeight(final int index) {
-            if (index == mCenterSuggestionIndex) {
+        private float getSuggestionWeight(final int indexInStrip) {
+            if (indexInStrip == mCenterSuggestionIndex) {
                 return mCenterSuggestionWeight;
             } else {
                 // TODO: Revisit this for cases of 5 or more suggestions
@@ -421,16 +461,16 @@
         private void layoutPunctuationSuggestions(final SuggestedWords suggestedWords,
                 final ViewGroup stripView) {
             final int countInStrip = Math.min(suggestedWords.size(), PUNCTUATIONS_IN_STRIP);
-            for (int index = 0; index < countInStrip; index++) {
-                if (index != 0) {
+            for (int indexInStrip = 0; indexInStrip < countInStrip; indexInStrip++) {
+                if (indexInStrip != 0) {
                     // Add divider if this isn't the left most suggestion in suggestions strip.
-                    addDivider(stripView, mDividers.get(index));
+                    addDivider(stripView, mDividers.get(indexInStrip));
                 }
 
-                final TextView word = mWords.get(index);
+                final TextView word = mWords.get(indexInStrip);
                 word.setEnabled(true);
                 word.setTextColor(mColorAutoCorrect);
-                final String text = suggestedWords.getWord(index);
+                final String text = suggestedWords.getWord(indexInStrip);
                 word.setText(text);
                 word.setTextScaleX(1.0f);
                 word.setCompoundDrawables(null, null, null, null);
@@ -689,7 +729,8 @@
 
     @Override
     public boolean onLongClick(final View view) {
-        KeyboardSwitcher.getInstance().hapticAndAudioFeedback(Constants.NOT_A_CODE);
+        AudioAndHapticFeedbackManager.getInstance().hapticAndAudioFeedback(
+                Constants.NOT_A_CODE, this);
         return showMoreSuggestions();
     }
 
diff --git a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
index 1dd68ea..0e9c292 100644
--- a/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
+++ b/native/jni/com_android_inputmethod_latin_BinaryDictionary.cpp
@@ -35,6 +35,7 @@
 #include "dictionary.h"
 #include "jni.h"
 #include "jni_common.h"
+#include "suggest_options.h"
 
 namespace latinime {
 
@@ -128,10 +129,9 @@
 static int latinime_BinaryDictionary_getSuggestions(JNIEnv *env, jclass clazz, jlong dict,
         jlong proximityInfo, jlong dicTraverseSession, jintArray xCoordinatesArray,
         jintArray yCoordinatesArray, jintArray timesArray, jintArray pointerIdsArray,
-        jintArray inputCodePointsArray, jint inputSize, jint commitPoint, jboolean isGesture,
-        jintArray prevWordCodePointsForBigrams, jboolean useFullEditDistance,
-        jintArray outputCodePointsArray, jintArray scoresArray, jintArray spaceIndicesArray,
-        jintArray outputTypesArray) {
+        jintArray inputCodePointsArray, jint inputSize, jint commitPoint, jintArray suggestOptions,
+        jintArray prevWordCodePointsForBigrams, jintArray outputCodePointsArray,
+        jintArray scoresArray, jintArray spaceIndicesArray, jintArray outputTypesArray) {
     Dictionary *dictionary = reinterpret_cast<Dictionary *>(dict);
     if (!dictionary) return 0;
     ProximityInfo *pInfo = reinterpret_cast<ProximityInfo *>(proximityInfo);
@@ -159,6 +159,11 @@
         prevWordCodePoints = prevWordCodePointsInternal;
     }
 
+    const jsize numberOfOptions = env->GetArrayLength(suggestOptions);
+    int options[numberOfOptions];
+    env->GetIntArrayRegion(suggestOptions, 0, numberOfOptions, options);
+    SuggestOptions givenOptions(options, numberOfOptions);
+
     // Output values
     /* By the way, let's check the output array length here to make sure */
     const jsize outputCodePointsLength = env->GetArrayLength(outputCodePointsArray);
@@ -185,11 +190,12 @@
     memset(outputTypes, 0, sizeof(outputTypes));
 
     int count;
-    if (isGesture || inputSize > 0) {
+    if (givenOptions.isGesture() || inputSize > 0) {
         count = dictionary->getSuggestions(pInfo, traverseSession, xCoordinates, yCoordinates,
                 times, pointerIds, inputCodePoints, inputSize, prevWordCodePoints,
-                prevWordCodePointsLength, commitPoint, isGesture, useFullEditDistance,
-                outputCodePoints, scores, spaceIndices, outputTypes);
+                prevWordCodePointsLength, commitPoint, givenOptions.isGesture(),
+                givenOptions.useFullEditDistance(), outputCodePoints, scores,
+                spaceIndices, outputTypes);
     } else {
         count = dictionary->getBigrams(prevWordCodePoints, prevWordCodePointsLength,
                 inputCodePoints, inputSize, outputCodePoints, scores, outputTypes);
@@ -288,7 +294,7 @@
      const_cast<char *>("(J)V"),
      reinterpret_cast<void *>(latinime_BinaryDictionary_close)},
     {const_cast<char *>("getSuggestionsNative"),
-     const_cast<char *>("(JJJ[I[I[I[I[IIIZ[IZ[I[I[I[I)I"),
+     const_cast<char *>("(JJJ[I[I[I[I[III[I[I[I[I[I[I)I"),
      reinterpret_cast<void *>(latinime_BinaryDictionary_getSuggestions)},
     {const_cast<char *>("getProbabilityNative"),
      const_cast<char *>("(J[I)I"),
diff --git a/native/jni/src/dictionary.h b/native/jni/src/dictionary.h
index 2ad5b6c..edec83f 100644
--- a/native/jni/src/dictionary.h
+++ b/native/jni/src/dictionary.h
@@ -41,6 +41,10 @@
     static const int KIND_APP_DEFINED = 6; // Suggested by the application
     static const int KIND_SHORTCUT = 7; // A shortcut
     static const int KIND_PREDICTION = 8; // A prediction (== a suggestion with no input)
+    // KIND_RESUMED: A resumed suggestion (comes from a span, currently this type is used only
+    // in java for re-correction)
+    static const int KIND_RESUMED = 9;
+    static const int KIND_OOV_CORRECTION = 10; // Most probable string correction
 
     static const int KIND_MASK_FLAGS = 0xFFFFFF00; // Mask to get the flags
     static const int KIND_FLAG_POSSIBLY_OFFENSIVE = 0x80000000;
diff --git a/native/jni/src/suggest_options.h b/native/jni/src/suggest_options.h
new file mode 100644
index 0000000..1bed47c
--- /dev/null
+++ b/native/jni/src/suggest_options.h
@@ -0,0 +1,65 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#ifndef LATINIME_SUGGEST_OPTIONS_H
+#define LATINIME_SUGGEST_OPTIONS_H
+
+#include "defines.h"
+
+namespace latinime {
+
+class SuggestOptions{
+ public:
+    AK_FORCE_INLINE bool isGesture() const {
+        return getBoolOption(IS_GESTURE);
+    }
+
+    AK_FORCE_INLINE bool useFullEditDistance() const {
+        return getBoolOption(USE_FULL_EDIT_DISTANCE);
+    }
+
+    SuggestOptions(const int *const options, const int length)
+            : mOptions(options), mLength(length) {}
+
+ private:
+    // Need to update com.android.inputmethod.latin.NativeSuggestOptions when you add, remove or
+    // reorder options.
+    static const int IS_GESTURE = 0;
+    static const int USE_FULL_EDIT_DISTANCE = 1;
+
+    const int *const mOptions;
+    const int mLength;
+
+    AK_FORCE_INLINE bool isValidKey(const int key) const {
+        return 0 <= key && key < mLength;
+    }
+
+    AK_FORCE_INLINE bool getBoolOption(const int key) const {
+        if (isValidKey(key)) {
+            return mOptions[key] != 0;
+        }
+        return false;
+    }
+
+    AK_FORCE_INLINE int getIntOption(const int key) const {
+        if (isValidKey(key)) {
+            return mOptions[key];
+        }
+        return 0;
+    }
+};
+} // namespace latinime
+#endif // LATINIME_SUGGEST_OPTIONS_H
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
similarity index 91%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
index 9014e7c..eea1efc 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserCsvTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
@@ -17,11 +17,13 @@
 package com.android.inputmethod.keyboard.internal;
 
 import android.app.Instrumentation;
+import android.content.Context;
+import android.content.res.Resources;
 import android.test.InstrumentationTestCase;
 import android.test.suitebuilder.annotation.MediumTest;
 
 import com.android.inputmethod.latin.CollectionUtils;
-import com.android.inputmethod.latin.StringUtils;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
 
 import java.lang.reflect.Field;
 import java.util.ArrayList;
@@ -29,16 +31,24 @@
 import java.util.Locale;
 
 @MediumTest
-public class KeySpecParserCsvTests extends InstrumentationTestCase {
-    private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+public class KeySpecParserSplitTests extends InstrumentationTestCase {
+    private static final Locale TEST_LOCALE = Locale.ENGLISH;
+    final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
     @Override
     protected void setUp() throws Exception {
         super.setUp();
 
         final Instrumentation instrumentation = getInstrumentation();
-        mTextsSet.setLanguage(Locale.ENGLISH.getLanguage());
-        mTextsSet.loadStringResources(instrumentation.getTargetContext());
+        final Context targetContext = instrumentation.getTargetContext();
+        mTextsSet.setLanguage(TEST_LOCALE.getLanguage());
+        new RunInLocale<Void>() {
+            @Override
+            protected Void job(final Resources res) {
+                mTextsSet.loadStringResources(targetContext);
+                return null;
+            }
+        }.runInLocale(targetContext.getResources(), TEST_LOCALE);
         final String[] testResourceNames = getAllResourceIdNames(
                 com.android.inputmethod.latin.tests.R.string.class);
         mTextsSet.loadStringResourcesInternal(instrumentation.getContext(),
@@ -56,8 +66,8 @@
         return names.toArray(new String[names.size()]);
     }
 
-    private static void assertArrayEquals(final String message, final Object[] expected,
-            final Object[] actual) {
+    private static <T> void assertArrayEquals(final String message, final T[] expected,
+            final T[] actual) {
         if (expected == actual) {
             return;
         }
@@ -70,15 +80,19 @@
             return;
         }
         for (int i = 0; i < expected.length; i++) {
-            assertEquals(message + " [" + i + "]",
-                    Arrays.toString(expected), Arrays.toString(actual));
+            final T e = expected[i];
+            final T a = actual[i];
+            if (e == a) {
+                continue;
+            }
+            assertEquals(message + " [" + i + "]", e, a);
         }
     }
 
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
         final String resolvedActual = KeySpecParser.resolveTextReference(value, mTextsSet);
-        final String[] actual = StringUtils.parseCsvString(resolvedActual);
+        final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual);
         final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
         assertArrayEquals(message, expected, actual);
     }
@@ -101,7 +115,7 @@
     private static final String SURROGATE1 = PAIR1 + PAIR2;
     private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
 
-    public void testParseCsvTextZero() {
+    public void testSplitZero() {
         assertTextArray("Empty string", "");
         assertTextArray("Empty entry", ",");
         assertTextArray("Empty entry at beginning", ",a", "a");
@@ -110,7 +124,7 @@
         assertTextArray("Empty entries with escape", ",a,b\\,c,,d,", "a", "b\\,c", "d");
     }
 
-    public void testParseCsvTextSingle() {
+    public void testSplitSingle() {
         assertTextArray("Single char", "a", "a");
         assertTextArray("Surrogate pair", PAIR1, PAIR1);
         assertTextArray("Single escape", "\\", "\\");
@@ -139,7 +153,7 @@
         assertTextArray("Incomplete resource reference 4", "!" + SURROGATE2, "!" + SURROGATE2);
     }
 
-    public void testParseCsvTextSingleEscaped() {
+    public void testSplitSingleEscaped() {
         assertTextArray("Escaped char", "\\a", "\\a");
         assertTextArray("Escaped surrogate pair", "\\" + PAIR1, "\\" + PAIR1);
         assertTextArray("Escaped comma", "\\,", "\\,");
@@ -174,7 +188,7 @@
         assertTextArray("Escaped !TEXT/NAME", "\\!TEXT/EMPTY_STRING", "\\!TEXT/EMPTY_STRING");
     }
 
-    public void testParseCsvTextMulti() {
+    public void testSplitMulti() {
         assertTextArray("Multiple chars", "a,b,c", "a", "b", "c");
         assertTextArray("Multiple chars", "a,b,\\c", "a", "b", "\\c");
         assertTextArray("Multiple chars and escape at beginning and end",
@@ -189,7 +203,7 @@
                 " abc ", " def ", " ghi ");
     }
 
-    public void testParseCsvTextMultiEscaped() {
+    public void testSplitMultiEscaped() {
         assertTextArray("Multiple chars with comma", "a,\\,,c", "a", "\\,", "c");
         assertTextArray("Multiple chars with comma surrounded by spaces", " a , \\, , c ",
                 " a ", " \\, ", " c ");
@@ -208,17 +222,17 @@
                 "\\!", "\\!TEXT/EMPTY_STRING");
     }
 
-    public void testParseCsvResourceError() {
+    public void testSplitResourceError() {
         assertError("Incomplete resource name", "!text/", "!text/");
         assertError("Non existing resource", "!text/non_existing");
     }
 
-    public void testParseCsvResourceZero() {
+    public void testSplitResourceZero() {
         assertTextArray("Empty string",
                 "!text/empty_string");
     }
 
-    public void testParseCsvResourceSingle() {
+    public void testSplitResourceSingle() {
         assertTextArray("Single char",
                 "!text/single_char", "a");
         assertTextArray("Space",
@@ -240,7 +254,7 @@
                 "\\\\!text/single_char", "\\\\a");
     }
 
-    public void testParseCsvResourceSingleEscaped() {
+    public void testSplitResourceSingleEscaped() {
         assertTextArray("Escaped char",
                 "!text/escaped_char", "\\a");
         assertTextArray("Escaped comma",
@@ -267,7 +281,7 @@
                 "!text/escaped_label_with_escape", "a\\\\c");
     }
 
-    public void testParseCsvResourceMulti() {
+    public void testSplitResourceMulti() {
         assertTextArray("Multiple chars",
                 "!text/multiple_chars", "a", "b", "c");
         assertTextArray("Multiple chars surrounded by spaces",
@@ -279,7 +293,7 @@
                 "!text/multiple_labels_surrounded_by_spaces", " abc ", " def ", " ghi ");
     }
 
-    public void testParseCsvResourcetMultiEscaped() {
+    public void testSplitResourcetMultiEscaped() {
         assertTextArray("Multiple chars with comma",
                 "!text/multiple_chars_with_comma",
                 "a", "\\,", "c");
@@ -300,7 +314,7 @@
                 " ab\\\\ ", " d\\\\\\, ", " g\\,i ");
     }
 
-    public void testParseMultipleResources() {
+    public void testSplitMultipleResources() {
         assertTextArray("Literals and resources",
                 "1,!text/multiple_chars,z", "1", "a", "b", "c", "z");
         assertTextArray("Literals and resources and escape at end",
@@ -322,7 +336,7 @@
                 "abcabc", "def", "ghi");
     }
 
-    public void testParseIndirectReference() {
+    public void testSplitIndirectReference() {
         assertTextArray("Indirect",
                 "!text/indirect_string", "a", "b", "c");
         assertTextArray("Indirect with literal",
@@ -331,7 +345,7 @@
                 "!text/indirect2_string", "a", "b", "c");
     }
 
-    public void testParseInfiniteIndirectReference() {
+    public void testSplitInfiniteIndirectReference() {
         assertError("Infinite indirection",
                 "1,!text/infinite_indirection,2", "1", "infinite", "<infinite>", "loop", "2");
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
index b1ae6f5..b55158d 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
@@ -20,23 +20,27 @@
 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
 
+import android.content.Context;
+import android.content.res.Resources;
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LocaleUtils.RunInLocale;
 
 import java.util.Arrays;
 import java.util.Locale;
 
 @SmallTest
 public class KeySpecParserTests extends AndroidTestCase {
-    private final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
-    private final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
+    private final static Locale TEST_LOCALE = Locale.ENGLISH;
+    final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
+    final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
     private static final String CODE_SETTINGS = "!code/key_settings";
     private static final String ICON_SETTINGS = "!icon/settings_key";
-    private static final String CODE_SETTINGS_UPPERCASE = CODE_SETTINGS.toUpperCase();
-    private static final String ICON_SETTINGS_UPPERCASE = ICON_SETTINGS.toUpperCase();
+    private static final String CODE_SETTINGS_UPPERCASE = CODE_SETTINGS.toUpperCase(Locale.ROOT);
+    private static final String ICON_SETTINGS_UPPERCASE = ICON_SETTINGS.toUpperCase(Locale.ROOT);
     private static final String CODE_NON_EXISTING = "!code/non_existing";
     private static final String ICON_NON_EXISTING = "!icon/non_existing";
 
@@ -48,10 +52,17 @@
     protected void setUp() throws Exception {
         super.setUp();
 
-        final String language = Locale.ENGLISH.getLanguage();
+        final String language = TEST_LOCALE.getLanguage();
         mCodesSet.setLanguage(language);
         mTextsSet.setLanguage(language);
-        mTextsSet.loadStringResources(getContext());
+        final Context context = getContext();
+        new RunInLocale<Void>() {
+            @Override
+            protected Void job(final Resources res) {
+                mTextsSet.loadStringResources(context);
+                return null;
+            }
+        }.runInLocale(context.getResources(), TEST_LOCALE);
 
         mCodeSettings = KeySpecParser.parseCode(
                 CODE_SETTINGS, mCodesSet, CODE_UNSPECIFIED);
@@ -587,7 +598,7 @@
                 new String[] { null, "a", "b", "c" }, true);
         // Upper case specification will not work.
         assertGetBooleanValue("HAS LABEL", HAS_LABEL,
-                new String[] { HAS_LABEL.toUpperCase(), "a", "b", "c" },
+                new String[] { HAS_LABEL.toUpperCase(Locale.ROOT), "a", "b", "c" },
                 new String[] { "!HASLABEL!", "a", "b", "c" }, false);
 
         assertGetBooleanValue("No has label", HAS_LABEL,
@@ -600,13 +611,13 @@
         // Upper case specification will not work.
         assertGetBooleanValue("Multiple has label", HAS_LABEL,
                 new String[] {
-                    "a", HAS_LABEL.toUpperCase(), "b", "c", HAS_LABEL, "d" },
+                    "a", HAS_LABEL.toUpperCase(Locale.ROOT), "b", "c", HAS_LABEL, "d" },
                 new String[] {
                     "a", "!HASLABEL!", "b", "c", null, "d" }, true);
         // Upper case specification will not work.
         assertGetBooleanValue("Multiple has label with needs dividers", HAS_LABEL,
                 new String[] {
-                    "a", HAS_LABEL, "b", NEEDS_DIVIDER, HAS_LABEL.toUpperCase(), "d" },
+                    "a", HAS_LABEL, "b", NEEDS_DIVIDER, HAS_LABEL.toUpperCase(Locale.ROOT), "d" },
                 new String[] {
                     "a", null, "b", NEEDS_DIVIDER, "!HASLABEL!", "d" }, true);
     }
@@ -625,7 +636,7 @@
                 new String[] { null, "a", "b", "c" }, 3);
         // Upper case specification will not work.
         assertGetIntValue("FIXED COLUMN ORDER 3", FIXED_COLUMN_ORDER, -1,
-                new String[] { FIXED_COLUMN_ORDER.toUpperCase() + "3", "a", "b", "c" },
+                new String[] { FIXED_COLUMN_ORDER.toUpperCase(Locale.ROOT) + "3", "a", "b", "c" },
                 new String[] { "!FIXEDCOLUMNORDER!3", "a", "b", "c" }, -1);
 
         assertGetIntValue("No fixed column order", FIXED_COLUMN_ORDER, -1,
@@ -641,7 +652,7 @@
         // Upper case specification will not work.
         assertGetIntValue("Multiple fixed column order 5,3 with has label", FIXED_COLUMN_ORDER, -1,
                 new String[] {
-                    FIXED_COLUMN_ORDER.toUpperCase() + "5", HAS_LABEL, "a",
+                    FIXED_COLUMN_ORDER.toUpperCase(Locale.ROOT) + "5", HAS_LABEL, "a",
                     FIXED_COLUMN_ORDER + "3", "b" },
                 new String[] { "!FIXEDCOLUMNORDER!5", HAS_LABEL, "a", null, "b" }, 3);
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MatrixUtilsTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MatrixUtilsTests.java
new file mode 100644
index 0000000..e2a11ab
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MatrixUtilsTests.java
@@ -0,0 +1,84 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.keyboard.internal.MatrixUtils.MatrixOperationFailedException;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class MatrixUtilsTests extends AndroidTestCase {
+    // "run tests" -c com.android.inputmethod.keyboard.internal.MatrixUtilsTests
+    private static final boolean DEBUG = false;
+    private static final float EPSILON = 0.00001f;
+
+    private static void assertEqualsFloat(float f0, float f1) {
+        assertEqualsFloat(f0, f1, EPSILON);
+    }
+
+    /* package */ static void assertEqualsFloat(float f0, float f1, float error) {
+        assertTrue(Math.abs(f0 - f1) < error);
+    }
+
+    public void testMulti() {
+        final float[][] matrixA = {{1, 2}, {3, 4}};
+        final float[][] matrixB = {{5, 6}, {7, 8}};
+        final float[][] retval = new float[2][2];
+        try {
+            MatrixUtils.multiply(matrixA, matrixB, retval);
+        } catch (MatrixOperationFailedException e) {
+            assertTrue(false);
+        }
+        if (DEBUG) {
+            MatrixUtils.dump("multi", retval);
+        }
+        assertEqualsFloat(retval[0][0], 19);
+        assertEqualsFloat(retval[0][1], 22);
+        assertEqualsFloat(retval[1][0], 43);
+        assertEqualsFloat(retval[1][1], 50);
+    }
+
+    public void testInverse() {
+        final int N = 4;
+        final float[][] matrix =
+                {{1, 2, 3, 4}, {4, 0, 5, 6}, {6, 4, 2, 0}, {6, 4, 2, 1}};
+        final float[][] inverse = new float[N][N];
+        final float[][] tempMatrix = new float[N][N];
+        for (int i = 0; i < N; ++i) {
+            for (int j = 0; j < N; ++j) {
+                tempMatrix[i][j] = matrix[i][j];
+            }
+        }
+        final float[][] retval = new float[N][N];
+        try {
+            MatrixUtils.inverse(tempMatrix, inverse);
+        } catch (MatrixOperationFailedException e) {
+            assertTrue(false);
+        }
+        try {
+            MatrixUtils.multiply(matrix, inverse, retval);
+        } catch (MatrixOperationFailedException e) {
+            assertTrue(false);
+        }
+        for (int i = 0; i < N; ++i) {
+            for (int j = 0; j < N; ++j) {
+                assertEqualsFloat(((i == j) ? 1.0f : 0.0f), retval[i][j]);
+            }
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
index 2544b6c..2cb9648 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MockKeyboardSwitcher.java
@@ -149,11 +149,6 @@
         mLongPressTimeoutCode = 0;
     }
 
-    @Override
-    public void hapticAndAudioFeedback(final int code) {
-        // Nothing to do.
-    }
-
     public void onLongPressTimeout(final int code) {
         // TODO: Handle simultaneous long presses.
         if (mLongPressTimeoutCode == code) {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/SmoothingUtilsTests.java b/tests/src/com/android/inputmethod/keyboard/internal/SmoothingUtilsTests.java
new file mode 100644
index 0000000..293741a
--- /dev/null
+++ b/tests/src/com/android/inputmethod/keyboard/internal/SmoothingUtilsTests.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.inputmethod.keyboard.internal;
+
+import com.android.inputmethod.keyboard.internal.MatrixUtils.MatrixOperationFailedException;
+
+import android.test.AndroidTestCase;
+import android.test.suitebuilder.annotation.SmallTest;
+
+@SmallTest
+public class SmoothingUtilsTests extends AndroidTestCase {
+    // "run tests" -c com.android.inputmethod.keyboard.internal.SmoothingUtilsTests
+    private static final boolean DEBUG = false;
+
+    public void testGet3DParamaters() {
+        final float[] xs = new float[] {0, 1, 2, 3, 4};
+        final float[] ys = new float[] {1, 4, 15, 40, 85}; // y = x^3 + x^2 + x + 1
+        final float[][] retval = new float[4][1];
+        try {
+            SmoothingUtils.get3DParameters(xs, ys, retval);
+            if (DEBUG) {
+                MatrixUtils.dump("3d", retval);
+            }
+            for (int i = 0; i < 4; ++i) {
+                MatrixUtilsTests.assertEqualsFloat(retval[i][0], 1.0f, 0.001f);
+            }
+        } catch (MatrixOperationFailedException e) {
+            assertTrue(false);
+        }
+    }
+}
diff --git a/tests/src/com/android/inputmethod/latin/InputPointersTests.java b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
index e1149b3..3015829 100644
--- a/tests/src/com/android/inputmethod/latin/InputPointersTests.java
+++ b/tests/src/com/android/inputmethod/latin/InputPointersTests.java
@@ -108,13 +108,13 @@
         assertNotSame("pointerIds after copy", dst.getPointerIds(), src.getPointerIds());
         assertNotSame("times after copy", dst.getTimes(), src.getTimes());
         final int size = dst.getPointerSize();
-        assertArrayEquals("xCoordinates values after copy",
+        assertIntArrayEquals("xCoordinates values after copy",
                 dst.getXCoordinates(), 0, src.getXCoordinates(), 0, size);
-        assertArrayEquals("yCoordinates values after copy",
+        assertIntArrayEquals("yCoordinates values after copy",
                 dst.getYCoordinates(), 0, src.getYCoordinates(), 0, size);
-        assertArrayEquals("pointerIds values after copy",
+        assertIntArrayEquals("pointerIds values after copy",
                 dst.getPointerIds(), 0, src.getPointerIds(), 0, size);
-        assertArrayEquals("times values after copy",
+        assertIntArrayEquals("times values after copy",
                 dst.getTimes(), 0, src.getTimes(), 0, size);
     }
 
@@ -135,34 +135,34 @@
 
         dst.append(src, 0, 0);
         assertEquals("size after append zero", dstLen, dst.getPointerSize());
-        assertArrayEquals("xCoordinates after append zero",
+        assertIntArrayEquals("xCoordinates after append zero",
                 dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
-        assertArrayEquals("yCoordinates after append zero",
+        assertIntArrayEquals("yCoordinates after append zero",
                 dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
-        assertArrayEquals("pointerIds after append zero",
+        assertIntArrayEquals("pointerIds after append zero",
                 dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
-        assertArrayEquals("times after append zero",
+        assertIntArrayEquals("times after append zero",
                 dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
 
         dst.append(src, 0, srcLen);
         assertEquals("size after append", dstLen + srcLen, dst.getPointerSize());
         assertTrue("primitive length after append",
                 dst.getPointerIds().length >= dstLen + srcLen);
-        assertArrayEquals("original xCoordinates values after append",
+        assertIntArrayEquals("original xCoordinates values after append",
                 dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
-        assertArrayEquals("original yCoordinates values after append",
+        assertIntArrayEquals("original yCoordinates values after append",
                 dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
-        assertArrayEquals("original pointerIds values after append",
+        assertIntArrayEquals("original pointerIds values after append",
                 dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
-        assertArrayEquals("original times values after append",
+        assertIntArrayEquals("original times values after append",
                 dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
-        assertArrayEquals("appended xCoordinates values after append",
+        assertIntArrayEquals("appended xCoordinates values after append",
                 src.getXCoordinates(), 0, dst.getXCoordinates(), dstLen, srcLen);
-        assertArrayEquals("appended yCoordinates values after append",
+        assertIntArrayEquals("appended yCoordinates values after append",
                 src.getYCoordinates(), 0, dst.getYCoordinates(), dstLen, srcLen);
-        assertArrayEquals("appended pointerIds values after append",
+        assertIntArrayEquals("appended pointerIds values after append",
                 src.getPointerIds(), 0, dst.getPointerIds(), dstLen, srcLen);
-        assertArrayEquals("appended times values after append",
+        assertIntArrayEquals("appended times values after append",
                 src.getTimes(), 0, dst.getTimes(), dstLen, srcLen);
     }
 
@@ -190,47 +190,55 @@
 
         dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, 0);
         assertEquals("size after append zero", dstLen, dst.getPointerSize());
-        assertArrayEquals("xCoordinates after append zero",
+        assertIntArrayEquals("xCoordinates after append zero",
                 dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
-        assertArrayEquals("yCoordinates after append zero",
+        assertIntArrayEquals("yCoordinates after append zero",
                 dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
-        assertArrayEquals("pointerIds after append zero",
+        assertIntArrayEquals("pointerIds after append zero",
                 dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
-        assertArrayEquals("times after append zero",
+        assertIntArrayEquals("times after append zero",
                 dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
 
         dst.append(srcPointerId, srcTimes, srcXCoords, srcYCoords, 0, srcLen);
         assertEquals("size after append", dstLen + srcLen, dst.getPointerSize());
         assertTrue("primitive length after append",
                 dst.getPointerIds().length >= dstLen + srcLen);
-        assertArrayEquals("original xCoordinates values after append",
+        assertIntArrayEquals("original xCoordinates values after append",
                 dstCopy.getXCoordinates(), 0, dst.getXCoordinates(), 0, dstLen);
-        assertArrayEquals("original yCoordinates values after append",
+        assertIntArrayEquals("original yCoordinates values after append",
                 dstCopy.getYCoordinates(), 0, dst.getYCoordinates(), 0, dstLen);
-        assertArrayEquals("original pointerIds values after append",
+        assertIntArrayEquals("original pointerIds values after append",
                 dstCopy.getPointerIds(), 0, dst.getPointerIds(), 0, dstLen);
-        assertArrayEquals("original times values after append",
+        assertIntArrayEquals("original times values after append",
                 dstCopy.getTimes(), 0, dst.getTimes(), 0, dstLen);
-        assertArrayEquals("appended xCoordinates values after append",
+        assertIntArrayEquals("appended xCoordinates values after append",
                 srcXCoords.getPrimitiveArray(), 0, dst.getXCoordinates(), dstLen, srcLen);
-        assertArrayEquals("appended yCoordinates values after append",
+        assertIntArrayEquals("appended yCoordinates values after append",
                 srcYCoords.getPrimitiveArray(), 0, dst.getYCoordinates(), dstLen, srcLen);
-        assertArrayEquals("appended pointerIds values after append",
+        assertIntArrayEquals("appended pointerIds values after append",
                 srcPointerIds, 0, dst.getPointerIds(), dstLen, srcLen);
-        assertArrayEquals("appended times values after append",
+        assertIntArrayEquals("appended times values after append",
                 srcTimes.getPrimitiveArray(), 0, dst.getTimes(), dstLen, srcLen);
     }
 
-    private static void assertArrayEquals(String message, int[] expecteds, int expectedPos,
-            int[] actuals, int actualPos, int length) {
-        if (expecteds == null && actuals == null) {
+    // TODO: Consolidate this method with
+    // {@link ResizableIntArrayTests#assertIntArrayEquals(String,int[],int,int[],int,int)}.
+    private static void assertIntArrayEquals(final String message, final int[] expecteds,
+            final int expectedPos, final int[] actuals, final int actualPos, final int length) {
+        if (expecteds == actuals) {
             return;
         }
         if (expecteds == null || actuals == null) {
-            fail(message + ": expecteds=" + expecteds + " actuals=" + actuals);
+            assertEquals(message, Arrays.toString(expecteds), Arrays.toString(actuals));
+            return;
+        }
+        if (expecteds.length < expectedPos + length || actuals.length < actualPos + length) {
+            fail(message + ": insufficient length: expecteds=" + Arrays.toString(expecteds)
+                    + " actuals=" + Arrays.toString(actuals));
+            return;
         }
         for (int i = 0; i < length; i++) {
-            assertEquals(message + ": element at " + i,
+            assertEquals(message + " [" + i + "]",
                     expecteds[i + expectedPos], actuals[i + actualPos]);
         }
     }
diff --git a/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
index 2d1b836..b9fee95 100644
--- a/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
+++ b/tests/src/com/android/inputmethod/latin/ResizableIntArrayTests.java
@@ -19,6 +19,8 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import java.util.Arrays;
+
 @SmallTest
 public class ResizableIntArrayTests extends AndroidTestCase {
     private static final int DEFAULT_CAPACITY = 48;
@@ -186,7 +188,7 @@
         assertEquals("length after copy", dst.getLength(), src.getLength());
         assertSame("array after copy", array, dst.getPrimitiveArray());
         assertNotSame("array after copy", dst.getPrimitiveArray(), src.getPrimitiveArray());
-        assertArrayEquals("values after copy",
+        assertIntArrayEquals("values after copy",
                 dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength());
 
         final int smallerLength = DEFAULT_CAPACITY / 2;
@@ -197,7 +199,7 @@
         assertEquals("length after copy to smaller", dst.getLength(), src.getLength());
         assertNotSame("array after copy to smaller", array2, array3);
         assertNotSame("array after copy to smaller", array3, src.getPrimitiveArray());
-        assertArrayEquals("values after copy to smaller",
+        assertIntArrayEquals("values after copy to smaller",
                 dst.getPrimitiveArray(), 0, src.getPrimitiveArray(), 0, dst.getLength());
     }
 
@@ -220,7 +222,7 @@
         dst.append(src, 0, 0);
         assertEquals("length after append zero", dstLen, dst.getLength());
         assertSame("array after append zero", array, dst.getPrimitiveArray());
-        assertArrayEquals("values after append zero",
+        assertIntArrayEquals("values after append zero",
                 dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
 
         dst.append(src, 0, srcLen);
@@ -228,9 +230,9 @@
         assertSame("array after append", array, dst.getPrimitiveArray());
         assertTrue("primitive length after append",
                 dst.getPrimitiveArray().length >= dstLen + srcLen);
-        assertArrayEquals("original values after append",
+        assertIntArrayEquals("original values after append",
                 dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
-        assertArrayEquals("appended values after append",
+        assertIntArrayEquals("appended values after append",
                 src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen, srcLen);
 
         dst.append(src, 0, srcLen);
@@ -238,11 +240,11 @@
         assertNotSame("array after 2nd append", array, dst.getPrimitiveArray());
         assertTrue("primitive length after 2nd append",
                 dst.getPrimitiveArray().length >= dstLen + srcLen * 2);
-        assertArrayEquals("original values after 2nd append",
+        assertIntArrayEquals("original values after 2nd append",
                 dstCopy.getPrimitiveArray(), 0, dst.getPrimitiveArray(), 0, dstLen);
-        assertArrayEquals("appended values after 2nd append",
+        assertIntArrayEquals("appended values after 2nd append",
                 src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen, srcLen);
-        assertArrayEquals("appended values after 2nd append",
+        assertIntArrayEquals("appended values after 2nd append",
                 src.getPrimitiveArray(), 0, dst.getPrimitiveArray(), dstLen + srcLen, srcLen);
     }
 
@@ -319,16 +321,22 @@
         }
     }
 
-    private static void assertArrayEquals(String message, int[] expecteds, int expectedPos,
-            int[] actuals, int actualPos, int length) {
-        if (expecteds == null && actuals == null) {
+    private static void assertIntArrayEquals(final String message, final int[] expecteds,
+            final int expectedPos, final int[] actuals, final int actualPos, final int length) {
+        if (expecteds == actuals) {
             return;
         }
         if (expecteds == null || actuals == null) {
-            fail(message + ": expecteds=" + expecteds + " actuals=" + actuals);
+            assertEquals(message, Arrays.toString(expecteds), Arrays.toString(actuals));
+            return;
+        }
+        if (expecteds.length < expectedPos + length || actuals.length < actualPos + length) {
+            fail(message + ": insufficient length: expecteds=" + Arrays.toString(expecteds)
+                    + " actuals=" + Arrays.toString(actuals));
+            return;
         }
         for (int i = 0; i < length; i++) {
-            assertEquals(message + ": element at " + i,
+            assertEquals(message + " [" + i + "]",
                     expecteds[i + expectedPos], actuals[i + actualPos]);
         }
     }
diff --git a/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java b/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java
index ed16846..c915522 100644
--- a/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/ResourceUtilsTests.java
@@ -19,25 +19,41 @@
 import android.test.AndroidTestCase;
 import android.test.suitebuilder.annotation.SmallTest;
 
+import com.android.inputmethod.latin.ResourceUtils.DeviceOverridePatternSyntaxError;
+
 import java.util.HashMap;
 
 @SmallTest
 public class ResourceUtilsTests extends AndroidTestCase {
     public void testFindDefaultConstant() {
         final String[] nullArray = null;
-        assertNull(ResourceUtils.findDefaultConstant(nullArray));
-
         final String[] emptyArray = {};
-        assertNull(ResourceUtils.findDefaultConstant(emptyArray));
-
         final String[] array = {
-            "HARDWARE=grouper,0.3",
-            "HARDWARE=mako,0.4",
-            ",defaultValue1",
-            "HARDWARE=manta,0.2",
-            ",defaultValue2",
+                "HARDWARE=grouper,0.3",
+                "HARDWARE=mako,0.4",
+                ",defaultValue1",
+                "HARDWARE=manta,0.2",
+                ",defaultValue2",
         };
-        assertEquals(ResourceUtils.findDefaultConstant(array), "defaultValue1");
+
+        try {
+            assertNull(ResourceUtils.findDefaultConstant(nullArray));
+            assertNull(ResourceUtils.findDefaultConstant(emptyArray));
+            assertEquals(ResourceUtils.findDefaultConstant(array), "defaultValue1");
+        } catch (final DeviceOverridePatternSyntaxError e) {
+            fail(e.getMessage());
+        }
+
+        final String[] errorArray = {
+            "HARDWARE=grouper,0.3",
+            "no_comma"
+        };
+        try {
+            final String defaultValue = ResourceUtils.findDefaultConstant(errorArray);
+            fail("exception should be thrown: defaultValue=" + defaultValue);
+        } catch (final DeviceOverridePatternSyntaxError e) {
+            assertEquals("Array element has no comma: no_comma", e.getMessage());
+        }
     }
 
     public void testFindConstantForKeyValuePairsSimple() {
@@ -67,33 +83,23 @@
 
         final HashMap<String,String> keyValues = CollectionUtils.newHashMap();
         keyValues.put(HARDWARE_KEY, "grouper");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "mako");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "manta");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
 
-        try {
-            keyValues.clear();
-            keyValues.put("hardware", "grouper");
-            final String constant = ResourceUtils.findConstantForKeyValuePairs(keyValues, array);
-            fail("condition without HARDWARE must fail: constant=" + constant);
-        } catch (final RuntimeException e) {
-            assertEquals(e.getMessage(), "Found unknown key: HARDWARE=grouper");
-        }
+        keyValues.clear();
+        keyValues.put("hardware", "grouper");
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
+
         keyValues.clear();
         keyValues.put(HARDWARE_KEY, "MAKO");
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "mantaray");
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
 
-        try {
-            final String constant = ResourceUtils.findConstantForKeyValuePairs(
-                    emptyKeyValue, array);
-            fail("emptyCondition shouldn't match: constant=" + constant);
-        } catch (final RuntimeException e) {
-            assertEquals(e.getMessage(), "Found unknown key: HARDWARE=grouper");
-        }
+        assertNull(ResourceUtils.findConstantForKeyValuePairs(emptyKeyValue, array));
     }
 
     public void testFindConstantForKeyValuePairsCombined() {
@@ -102,6 +108,8 @@
         final String MANUFACTURER_KEY = "MANUFACTURER";
         final String[] array = {
             ",defaultValue",
+            "no_comma",
+            "error_pattern,0.1",
             "HARDWARE=grouper:MANUFACTURER=asus,0.3",
             "HARDWARE=mako:MODEL=Nexus 4,0.4",
             "HARDWARE=manta:MODEL=Nexus 10:MANUFACTURER=samsung,0.2"
@@ -117,25 +125,25 @@
         keyValues.put(HARDWARE_KEY, "grouper");
         keyValues.put(MODEL_KEY, "Nexus 7");
         keyValues.put(MANUFACTURER_KEY, "asus");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
 
         keyValues.clear();
         keyValues.put(HARDWARE_KEY, "mako");
         keyValues.put(MODEL_KEY, "Nexus 4");
         keyValues.put(MANUFACTURER_KEY, "LGE");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
 
         keyValues.clear();
         keyValues.put(HARDWARE_KEY, "manta");
         keyValues.put(MODEL_KEY, "Nexus 10");
         keyValues.put(MANUFACTURER_KEY, "samsung");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
         keyValues.put(HARDWARE_KEY, "mantaray");
         assertNull(ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray), "0.2");
+        assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, failArray));
     }
 
     public void testFindConstantForKeyValuePairsRegexp() {
@@ -144,6 +152,8 @@
         final String MANUFACTURER_KEY = "MANUFACTURER";
         final String[] array = {
             ",defaultValue",
+            "no_comma",
+            "HARDWARE=error_regexp:MANUFACTURER=error[regexp,0.1",
             "HARDWARE=grouper|tilapia:MANUFACTURER=asus,0.3",
             "HARDWARE=[mM][aA][kK][oO]:MODEL=Nexus 4,0.4",
             "HARDWARE=manta.*:MODEL=Nexus 10:MANUFACTURER=samsung,0.2"
@@ -153,24 +163,24 @@
         keyValues.put(HARDWARE_KEY, "grouper");
         keyValues.put(MODEL_KEY, "Nexus 7");
         keyValues.put(MANUFACTURER_KEY, "asus");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "tilapia");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.3");
+        assertEquals("0.3", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
 
         keyValues.clear();
         keyValues.put(HARDWARE_KEY, "mako");
         keyValues.put(MODEL_KEY, "Nexus 4");
         keyValues.put(MANUFACTURER_KEY, "LGE");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "MAKO");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.4");
+        assertEquals("0.4", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
 
         keyValues.clear();
         keyValues.put(HARDWARE_KEY, "manta");
         keyValues.put(MODEL_KEY, "Nexus 10");
         keyValues.put(MANUFACTURER_KEY, "samsung");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
         keyValues.put(HARDWARE_KEY, "mantaray");
-        assertEquals(ResourceUtils.findConstantForKeyValuePairs(keyValues, array), "0.2");
+        assertEquals("0.2", ResourceUtils.findConstantForKeyValuePairs(keyValues, array));
     }
 }
diff --git a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
index 29e790a..4f26098 100644
--- a/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
+++ b/tests/src/com/android/inputmethod/latin/StringUtilsTests.java
@@ -40,57 +40,62 @@
         }));
     }
 
-    public void testContainsInCsv() {
-        assertFalse("null", StringUtils.containsInCsv("key", null));
-        assertFalse("empty", StringUtils.containsInCsv("key", ""));
-        assertFalse("not in 1 element", StringUtils.containsInCsv("key", "key1"));
-        assertFalse("not in 2 elements", StringUtils.containsInCsv("key", "key1,key2"));
+    public void testContainsInExtraValues() {
+        assertFalse("null", StringUtils.containsInCommaSplittableText("key", null));
+        assertFalse("empty", StringUtils.containsInCommaSplittableText("key", ""));
+        assertFalse("not in 1 element",
+                StringUtils.containsInCommaSplittableText("key", "key1"));
+        assertFalse("not in 2 elements",
+                StringUtils.containsInCommaSplittableText("key", "key1,key2"));
 
-        assertTrue("in 1 element", StringUtils.containsInCsv("key", "key"));
-        assertTrue("in 2 elements", StringUtils.containsInCsv("key", "key1,key"));
+        assertTrue("in 1 element", StringUtils.containsInCommaSplittableText("key", "key"));
+        assertTrue("in 2 elements", StringUtils.containsInCommaSplittableText("key", "key1,key"));
     }
 
-    public void testAppendToCsvIfNotExists() {
-        assertEquals("null", "key", StringUtils.appendToCsvIfNotExists("key", null));
-        assertEquals("empty", "key", StringUtils.appendToCsvIfNotExists("key", ""));
+    public void testAppendToExtraValuesIfNotExists() {
+        assertEquals("null", "key",
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", null));
+        assertEquals("empty", "key",
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", ""));
 
         assertEquals("not in 1 element", "key1,key",
-                StringUtils.appendToCsvIfNotExists("key", "key1"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1"));
         assertEquals("not in 2 elements", "key1,key2,key",
-                StringUtils.appendToCsvIfNotExists("key", "key1,key2"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key2"));
 
         assertEquals("in 1 element", "key",
-                StringUtils.appendToCsvIfNotExists("key", "key"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key"));
         assertEquals("in 2 elements at position 1", "key,key2",
-                StringUtils.appendToCsvIfNotExists("key", "key,key2"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key,key2"));
         assertEquals("in 2 elements at position 2", "key1,key",
-                StringUtils.appendToCsvIfNotExists("key", "key1,key"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key"));
         assertEquals("in 3 elements at position 2", "key1,key,key3",
-                StringUtils.appendToCsvIfNotExists("key", "key1,key,key3"));
+                StringUtils.appendToCommaSplittableTextIfNotExists("key", "key1,key,key3"));
     }
 
-    public void testRemoveFromCsvIfExists() {
-        assertEquals("null", "", StringUtils.removeFromCsvIfExists("key", null));
-        assertEquals("empty", "", StringUtils.removeFromCsvIfExists("key", ""));
+    public void testRemoveFromExtraValuesIfExists() {
+        assertEquals("null", "", StringUtils.removeFromCommaSplittableTextIfExists("key", null));
+        assertEquals("empty", "", StringUtils.removeFromCommaSplittableTextIfExists("key", ""));
 
         assertEquals("not in 1 element", "key1",
-                StringUtils.removeFromCsvIfExists("key", "key1"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key1"));
         assertEquals("not in 2 elements", "key1,key2",
-                StringUtils.removeFromCsvIfExists("key", "key1,key2"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key2"));
 
         assertEquals("in 1 element", "",
-                StringUtils.removeFromCsvIfExists("key", "key"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key"));
         assertEquals("in 2 elements at position 1", "key2",
-                StringUtils.removeFromCsvIfExists("key", "key,key2"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key,key2"));
         assertEquals("in 2 elements at position 2", "key1",
-                StringUtils.removeFromCsvIfExists("key", "key1,key"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key"));
         assertEquals("in 3 elements at position 2", "key1,key3",
-                StringUtils.removeFromCsvIfExists("key", "key1,key,key3"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key1,key,key3"));
 
         assertEquals("in 3 elements at position 1,2,3", "",
-                StringUtils.removeFromCsvIfExists("key", "key,key,key"));
+                StringUtils.removeFromCommaSplittableTextIfExists("key", "key,key,key"));
         assertEquals("in 5 elements at position 2,4", "key1,key3,key5",
-                StringUtils.removeFromCsvIfExists("key", "key1,key,key3,key,key5"));
+                StringUtils.removeFromCommaSplittableTextIfExists(
+                        "key", "key1,key,key3,key,key5"));
     }