Merge "Add unit tests in prevision of magic space removal"
diff --git a/java/res/values-en/additional-proximitychars.xml b/java/res/values-en/additional-proximitychars.xml
new file mode 100644
index 0000000..0e12767
--- /dev/null
+++ b/java/res/values-en/additional-proximitychars.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+
+    <string-array name="additional_proximitychars">
+        <!-- Empty entry terminates the proximity chars array. -->
+
+        <!-- Additional proximity chars for a -->
+        <item>a</item>
+        <item>e</item>
+        <item>i</item>
+        <item>o</item>
+        <item>u</item>
+        <item></item>
+        <!-- Additional proximity chars for e -->
+        <item>e</item>
+        <item>a</item>
+        <item>i</item>
+        <item>o</item>
+        <item>u</item>
+        <item></item>
+        <!-- Additional proximity chars for i -->
+        <item>i</item>
+        <item>a</item>
+        <item>e</item>
+        <item>o</item>
+        <item>u</item>
+        <item></item>
+        <!-- Additional proximity chars for o -->
+        <item>o</item>
+        <item>a</item>
+        <item>e</item>
+        <item>i</item>
+        <item>u</item>
+        <item></item>
+        <!-- Additional proximity chars for u -->
+        <item>u</item>
+        <item>a</item>
+        <item>e</item>
+        <item>i</item>
+        <item>o</item>
+        <item></item>
+    </string-array>
+
+</resources>
\ No newline at end of file
diff --git a/java/res/values/additional-proximitychars.xml b/java/res/values/additional-proximitychars.xml
new file mode 100644
index 0000000..03d10d5
--- /dev/null
+++ b/java/res/values/additional-proximitychars.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+/*
+**
+** Copyright 2012, The Android Open Source Project
+**
+** Licensed under the Apache License, Version 2.0 (the "License");
+** you may not use this file except in compliance with the License.
+** You may obtain a copy of the License at
+**
+**     http://www.apache.org/licenses/LICENSE-2.0
+**
+** Unless required by applicable law or agreed to in writing, software
+** distributed under the License is distributed on an "AS IS" BASIS,
+** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+** See the License for the specific language governing permissions and
+** limitations under the License.
+*/
+-->
+<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
+    <string-array name="additional_proximitychars">
+    </string-array>
+</resources>
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 5e58821..8f2efab 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -28,8 +28,9 @@
 import com.android.inputmethod.keyboard.internal.KeyStyles;
 import com.android.inputmethod.keyboard.internal.KeyStyles.KeyStyle;
 import com.android.inputmethod.keyboard.internal.KeyboardIconsSet;
-import com.android.inputmethod.keyboard.internal.MoreKeySpecParser;
+import com.android.inputmethod.keyboard.internal.KeySpecParser;
 import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.XmlParseUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -128,7 +129,7 @@
     private boolean mEnabled = true;
 
     private static Drawable getIcon(Keyboard.Params params, String moreKeySpec) {
-        final int iconAttrId = MoreKeySpecParser.getIconAttrId(moreKeySpec);
+        final int iconAttrId = KeySpecParser.getIconAttrId(moreKeySpec);
         if (iconAttrId == KeyboardIconsSet.ICON_UNDEFINED) {
             return null;
         } else {
@@ -141,9 +142,9 @@
      */
     public Key(Resources res, Keyboard.Params params, String moreKeySpec,
             int x, int y, int width, int height) {
-        this(params, MoreKeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec),
-                MoreKeySpecParser.getCode(res, moreKeySpec),
-                MoreKeySpecParser.getOutputText(moreKeySpec),
+        this(params, KeySpecParser.getLabel(moreKeySpec), null, getIcon(params, moreKeySpec),
+                KeySpecParser.getCode(res, moreKeySpec),
+                KeySpecParser.getOutputText(moreKeySpec),
                 x, y, width, height);
     }
 
@@ -245,7 +246,7 @@
         int actionFlags = style.getFlag(keyAttr, R.styleable.Keyboard_Key_keyActionFlags, 0);
         final String[] additionalMoreKeys = style.getStringArray(
                 keyAttr, R.styleable.Keyboard_Key_additionalMoreKeys);
-        final String[] moreKeys = MoreKeySpecParser.insertAddtionalMoreKeys(style.getStringArray(
+        final String[] moreKeys = KeySpecParser.insertAddtionalMoreKeys(style.getStringArray(
                 keyAttr, R.styleable.Keyboard_Key_moreKeys), additionalMoreKeys);
         if (moreKeys != null) {
             actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
@@ -270,7 +271,7 @@
         // Choose the first letter of the label as primary code if not specified.
         if (code == Keyboard.CODE_UNSPECIFIED && TextUtils.isEmpty(outputText)
                 && !TextUtils.isEmpty(mLabel)) {
-            if (mLabel.codePointCount(0, mLabel.length()) == 1) {
+            if (Utils.codePointCount(mLabel) == 1) {
                 // Use the first letter of the hint label if shiftedLetterActivated flag is
                 // specified.
                 if (hasShiftedLetterHint() && isShiftedLetterActivated()
@@ -308,7 +309,7 @@
         if (!Keyboard.isLetterCode(code) || preserveCase) return code;
         final String text = new String(new int[] { code } , 0, 1);
         final String casedText = adjustCaseOfStringForKeyboardId(text, preserveCase, id);
-        return casedText.codePointCount(0, casedText.length()) == 1
+        return Utils.codePointCount(casedText) == 1
                 ? casedText.codePointAt(0) : Keyboard.CODE_UNSPECIFIED;
     }
 
@@ -380,7 +381,7 @@
     @Override
     public String toString() {
         String top = Keyboard.printableCode(mCode);
-        if (mLabel != null && mLabel.codePointCount(0, mLabel.length()) != 1) {
+        if (Utils.codePointCount(mLabel) != 1) {
             top += "/\"" + mLabel + '"';
         }
         return String.format("%s %d,%d", top, mX, mY);
diff --git a/java/src/com/android/inputmethod/keyboard/KeyDetector.java b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
index 0d27162..bff491f 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyDetector.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyDetector.java
@@ -19,12 +19,14 @@
 import android.util.Log;
 
 import java.util.Arrays;
+import java.util.List;
 
 public class KeyDetector {
     private static final String TAG = KeyDetector.class.getSimpleName();
     private static final boolean DEBUG = false;
 
     public static final int NOT_A_CODE = -1;
+    private static final int ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE = 2;
 
     private final int mKeyHysteresisDistanceSquared;
 
@@ -154,8 +156,9 @@
         return distances.length;
     }
 
-    private void getNearbyKeyCodes(final int[] allCodes) {
+    private void getNearbyKeyCodes(final int primaryCode, final int[] allCodes) {
         final Key[] neighborKeys = mNeighborKeys;
+        final int maxCodesSize = allCodes.length;
 
         // allCodes[0] should always have the key code even if it is a non-letter key.
         if (neighborKeys[0] == null) {
@@ -164,7 +167,7 @@
         }
 
         int numCodes = 0;
-        for (int j = 0; j < neighborKeys.length && numCodes < allCodes.length; j++) {
+        for (int j = 0; j < neighborKeys.length && numCodes < maxCodesSize; j++) {
             final Key key = neighborKeys[j];
             if (key == null)
                 break;
@@ -174,6 +177,38 @@
                 continue;
             allCodes[numCodes++] = code;
         }
+        if (maxCodesSize <= numCodes) {
+            return;
+        }
+        if (primaryCode != NOT_A_CODE) {
+            final List<Integer> additionalChars =
+                    mKeyboard.getAdditionalProximityChars().get(primaryCode);
+            if (additionalChars == null || additionalChars.size() == 0) {
+                return;
+            }
+            int currentCodesSize = numCodes;
+            allCodes[numCodes++] = ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE;
+            if (maxCodesSize <= numCodes) {
+                return;
+            }
+            // TODO: This is O(N^2). Assuming additionalChars.size() is up to 4 or 5.
+            for (int i = 0; i < additionalChars.size(); ++i) {
+                final int additionalChar = additionalChars.get(i);
+                boolean contains = false;
+                for (int j = 0; j < currentCodesSize; ++j) {
+                    if (additionalChar == allCodes[j]) {
+                        contains = true;
+                        break;
+                    }
+                }
+                if (!contains) {
+                    allCodes[numCodes++] = additionalChar;
+                    if (maxCodesSize <= numCodes) {
+                        return;
+                    }
+                }
+            }
+        }
     }
 
     /**
@@ -205,7 +240,7 @@
         }
 
         if (allCodes != null && allCodes.length > 0) {
-            getNearbyKeyCodes(allCodes);
+            getNearbyKeyCodes(primaryKey != null ? primaryKey.mCode : NOT_A_CODE, allCodes);
             if (DEBUG) {
                 Log.d(TAG, "x=" + x + " y=" + y
                         + " primary=" + printableCode(primaryKey)
diff --git a/java/src/com/android/inputmethod/keyboard/Keyboard.java b/java/src/com/android/inputmethod/keyboard/Keyboard.java
index 6653dec..10e0a5b 100644
--- a/java/src/com/android/inputmethod/keyboard/Keyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/Keyboard.java
@@ -20,6 +20,7 @@
 import android.content.res.Resources;
 import android.content.res.TypedArray;
 import android.content.res.XmlResourceParser;
+import android.text.TextUtils;
 import android.util.AttributeSet;
 import android.util.DisplayMetrics;
 import android.util.Log;
@@ -38,10 +39,12 @@
 import org.xmlpull.v1.XmlPullParserException;
 
 import java.io.IOException;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 
@@ -130,6 +133,8 @@
 
     private final ProximityInfo mProximityInfo;
 
+    public final Map<Integer, List<Integer>> mAdditionalProximityChars;
+
     public Keyboard(Params params) {
         mId = params.mId;
         mThemeId = params.mThemeId;
@@ -146,10 +151,12 @@
         mKeys = Collections.unmodifiableSet(params.mKeys);
         mShiftKeys = Collections.unmodifiableSet(params.mShiftKeys);
         mIconsSet = params.mIconsSet;
+        mAdditionalProximityChars = params.mAdditionalProximityChars;
 
         mProximityInfo = new ProximityInfo(
                 params.GRID_WIDTH, params.GRID_HEIGHT, mOccupiedWidth, mOccupiedHeight,
-                mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection);
+                mMostCommonKeyWidth, mMostCommonKeyHeight, mKeys, params.mTouchPositionCorrection,
+                params.mAdditionalProximityChars);
     }
 
     public ProximityInfo getProximityInfo() {
@@ -227,6 +234,9 @@
         public final Set<Key> mKeys = new HashSet<Key>();
         public final Set<Key> mShiftKeys = new HashSet<Key>();
         public final KeyboardIconsSet mIconsSet = new KeyboardIconsSet();
+        // TODO: Should be in Key instead of Keyboard.Params?
+        public final Map<Integer, List<Integer>> mAdditionalProximityChars =
+                new HashMap<Integer, List<Integer>>();
 
         public KeyboardSet.KeysCache mKeysCache;
 
@@ -358,6 +368,10 @@
         return mProximityInfo.getNearestKeys(adjustedX, adjustedY);
     }
 
+    public Map<Integer, List<Integer>> getAdditionalProximityChars() {
+        return mAdditionalProximityChars;
+    }
+
     public static String printableCode(int code) {
         switch (code) {
         case CODE_SHIFT: return "shift";
@@ -614,6 +628,7 @@
             mParams = params;
 
             setTouchPositionCorrectionData(context, params);
+            setAdditionalProximityChars(context, params);
 
             params.GRID_WIDTH = res.getInteger(R.integer.config_keyboard_grid_width);
             params.GRID_HEIGHT = res.getInteger(R.integer.config_keyboard_grid_height);
@@ -636,6 +651,25 @@
             params.mTouchPositionCorrection.load(data);
         }
 
+        private static void setAdditionalProximityChars(Context context, Params params) {
+            final String[] additionalChars =
+                    context.getResources().getStringArray(R.array.additional_proximitychars);
+            int currentPrimaryIndex = 0;
+            for (int i = 0; i < additionalChars.length; ++i) {
+                final String additionalChar = additionalChars[i];
+                if (TextUtils.isEmpty(additionalChar)) {
+                    currentPrimaryIndex = 0;
+                } else if (currentPrimaryIndex == 0) {
+                    currentPrimaryIndex = additionalChar.charAt(0);
+                    params.mAdditionalProximityChars.put(
+                            currentPrimaryIndex, new ArrayList<Integer>());
+                } else if (currentPrimaryIndex != 0) {
+                    final int c = additionalChar.charAt(0);
+                    params.mAdditionalProximityChars.get(currentPrimaryIndex).add(c);
+                }
+            }
+        }
+
         public void setAutoGenerate(KeyboardSet.KeysCache keysCache) {
             mParams.mKeysCache = keysCache;
         }
diff --git a/java/src/com/android/inputmethod/keyboard/KeyboardView.java b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
index 2cbd132..c6fb754 100644
--- a/java/src/com/android/inputmethod/keyboard/KeyboardView.java
+++ b/java/src/com/android/inputmethod/keyboard/KeyboardView.java
@@ -41,6 +41,7 @@
 import com.android.inputmethod.latin.LatinImeLogger;
 import com.android.inputmethod.latin.R;
 import com.android.inputmethod.latin.StaticInnerHandlerWrapper;
+import com.android.inputmethod.latin.Utils;
 
 import java.util.HashMap;
 
@@ -851,7 +852,7 @@
         if (key.mLabel != null) {
             // TODO Should take care of temporaryShiftLabel here.
             previewText.setCompoundDrawables(null, null, null, null);
-            if (key.mLabel.length() > 1) {
+            if (Utils.codePointCount(key.mLabel) > 1) {
                 previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, params.mKeyLetterSize);
                 previewText.setTypeface(Typeface.DEFAULT_BOLD);
             } else {
diff --git a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
index 433bd0d..4648da1 100644
--- a/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
+++ b/java/src/com/android/inputmethod/keyboard/MiniKeyboard.java
@@ -18,7 +18,7 @@
 
 import android.graphics.Paint;
 
-import com.android.inputmethod.keyboard.internal.MoreKeySpecParser;
+import com.android.inputmethod.keyboard.internal.KeySpecParser;
 import com.android.inputmethod.latin.R;
 
 public class MiniKeyboard extends Keyboard {
@@ -235,7 +235,7 @@
             Paint paint = null;
             int maxWidth = minKeyWidth;
             for (String moreKeySpec : moreKeys) {
-                final String label = MoreKeySpecParser.getLabel(moreKeySpec);
+                final String label = KeySpecParser.getLabel(moreKeySpec);
                 // If the label is single letter, minKeyWidth is enough to hold the label.
                 if (label != null && label.length() > 1) {
                     if (paint == null) {
diff --git a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
index c1dae06..2d1a008 100644
--- a/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
+++ b/java/src/com/android/inputmethod/keyboard/ProximityInfo.java
@@ -24,6 +24,9 @@
 
 import java.util.Arrays;
 import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class ProximityInfo {
@@ -44,7 +47,8 @@
     private final Key[][] mGridNeighbors;
 
     ProximityInfo(int gridWidth, int gridHeight, int minWidth, int height, int keyWidth,
-            int keyHeight, Set<Key> keys, TouchPositionCorrection touchPositionCorrection) {
+            int keyHeight, Set<Key> keys, TouchPositionCorrection touchPositionCorrection,
+            Map<Integer, List<Integer>> additionalProximityChars) {
         mGridWidth = gridWidth;
         mGridHeight = gridHeight;
         mGridSize = mGridWidth * mGridHeight;
@@ -58,20 +62,20 @@
             // No proximity required. Keyboard might be mini keyboard.
             return;
         }
-        computeNearestNeighbors(keyWidth, keys, touchPositionCorrection);
+        computeNearestNeighbors(keyWidth, keys, touchPositionCorrection, additionalProximityChars);
     }
 
     public static ProximityInfo createDummyProximityInfo() {
-        return new ProximityInfo(1, 1, 1, 1, 1, 1, Collections.<Key>emptySet(), null);
+        return new ProximityInfo(1, 1, 1, 1, 1, 1, Collections.<Key> emptySet(),
+                null, Collections.<Integer, List<Integer>> emptyMap());
     }
 
     public static ProximityInfo createSpellCheckerProximityInfo(final int[] proximity) {
         final ProximityInfo spellCheckerProximityInfo = createDummyProximityInfo();
         spellCheckerProximityInfo.mNativeProximityInfo =
                 spellCheckerProximityInfo.setProximityInfoNative(
-                        SpellCheckerProximityInfo.ROW_SIZE,
-                        480, 300, 11, 3, proximity,
-                        0, null, null, null, null, null, null, null, null);
+                        SpellCheckerProximityInfo.ROW_SIZE, 480, 300, 11, 3, proximity, 0,
+                        null, null, null, null, null, null, null, null);
         return spellCheckerProximityInfo;
     }
 
@@ -79,11 +83,13 @@
     static {
         Utils.loadNativeLibrary();
     }
+
     private native long setProximityInfoNative(int maxProximityCharsSize, int displayWidth,
             int displayHeight, int gridWidth, int gridHeight, int[] proximityCharsArray,
             int keyCount, int[] keyXCoordinates, int[] keyYCoordinates,
             int[] keyWidths, int[] keyHeights, int[] keyCharCodes,
             float[] sweetSpotCenterX, float[] sweetSpotCenterY, float[] sweetSpotRadii);
+
     private native void releaseProximityInfoNative(long nativeProximityInfo);
 
     private final void setProximityInfo(Key[][] gridNeighborKeys, int keyboardWidth,
@@ -138,7 +144,7 @@
                     final float radius = touchPositionCorrection.mRadii[row];
                     sweetSpotCenterXs[i] = hitBoxCenterX + x * hitBoxWidth;
                     sweetSpotCenterYs[i] = hitBoxCenterY + y * hitBoxHeight;
-                    sweetSpotRadii[i] = radius * (float)Math.sqrt(
+                    sweetSpotRadii[i] = radius * (float) Math.sqrt(
                             hitBoxWidth * hitBoxWidth + hitBoxHeight * hitBoxHeight);
                 }
             }
@@ -168,7 +174,12 @@
     }
 
     private void computeNearestNeighbors(int defaultWidth, Set<Key> keys,
-            TouchPositionCorrection touchPositionCorrection) {
+            TouchPositionCorrection touchPositionCorrection,
+            Map<Integer, List<Integer>> additionalProximityChars) {
+        final Map<Integer, Key> keyCodeMap = new HashMap<Integer, Key>();
+        for (final Key key : keys) {
+            keyCodeMap.put(key.mCode, key);
+        }
         final int thresholdBase = (int) (defaultWidth * SEARCH_DISTANCE);
         final int threshold = thresholdBase * thresholdBase;
         // Round-up so we don't have any pixels outside the grid
@@ -186,6 +197,27 @@
                         neighborKeys[count++] = key;
                     }
                 }
+                int currentCodesSize = count;
+                for (int i = 0; i < currentCodesSize; ++i) {
+                    final int c = neighborKeys[i].mCode;
+                    final List<Integer> additionalChars = additionalProximityChars.get(c);
+                    if (additionalChars == null || additionalChars.size() == 0) {
+                        continue;
+                    }
+                    for (int j = 0; j < additionalChars.size(); ++j) {
+                        final int additionalChar = additionalChars.get(j);
+                        boolean contains = false;
+                        for (int k = 0; k < count; ++k) {
+                            if(additionalChar == neighborKeys[k].mCode) {
+                                contains = true;
+                                break;
+                            }
+                        }
+                        if (!contains) {
+                            neighborKeys[count++] = keyCodeMap.get(additionalChar);
+                        }
+                    }
+                }
                 mGridNeighbors[(y / mCellHeight) * mGridWidth + (x / mCellWidth)] =
                         Arrays.copyOfRange(neighborKeys, 0, count);
             }
@@ -199,7 +231,7 @@
             return EMPTY_KEY_ARRAY;
         }
         if (x >= 0 && x < mKeyboardMinWidth && y >= 0 && y < mKeyboardHeight) {
-            int index = (y /  mCellHeight) * mGridWidth + (x / mCellWidth);
+            int index = (y / mCellHeight) * mGridWidth + (x / mCellWidth);
             if (index < mGridSize) {
                 return mGridNeighbors[index];
             }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
new file mode 100644
index 0000000..ba12676
--- /dev/null
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -0,0 +1,386 @@
+/*
+ * Copyright (C) 2010 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 android.content.res.Resources;
+import android.text.TextUtils;
+
+import com.android.inputmethod.keyboard.Keyboard;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.R;
+import com.android.inputmethod.latin.Utils;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+
+/**
+ * String parser of moreKeys attribute of Key.
+ * The string is comma separated texts each of which represents one "more key".
+ * - String resource can be embedded into specification @string/name. This is done before parsing
+ *   comma.
+ * Each "more key" specification is one of the following:
+ * - A single letter (Letter)
+ * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
+ * - Icon followed by keyOutputText or code (@icon/icon_name|@integer/key_code)
+ * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
+ * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
+ * See {@link KeyboardIconsSet} about icon_name.
+ */
+public class KeySpecParser {
+    private static final boolean DEBUG = LatinImeLogger.sDBG;
+
+    // Constants for parsing.
+    private static int COMMA = ',';
+    private static final char ESCAPE_CHAR = '\\';
+    private static final char PREFIX_AT = '@';
+    private static final char SUFFIX_SLASH = '/';
+    private static final String PREFIX_STRING = PREFIX_AT + "string";
+    private static final char LABEL_END = '|';
+    private static final String PREFIX_ICON = PREFIX_AT + "icon" + SUFFIX_SLASH;
+    private static final String PREFIX_CODE = PREFIX_AT + "integer" + SUFFIX_SLASH;
+    private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
+
+    private KeySpecParser() {
+        // Intentional empty constructor for utility class.
+    }
+
+    private static boolean hasIcon(String moreKeySpec) {
+        if (moreKeySpec.startsWith(PREFIX_ICON)) {
+            final int end = indexOfLabelEnd(moreKeySpec, 0);
+            if (end > 0) {
+                return true;
+            }
+            throw new KeySpecParserError("outputText or code not specified: " + moreKeySpec);
+        }
+        return false;
+    }
+
+    private static boolean hasCode(String moreKeySpec) {
+        final int end = indexOfLabelEnd(moreKeySpec, 0);
+        if (end > 0 && end + 1 < moreKeySpec.length()
+                && moreKeySpec.substring(end + 1).startsWith(PREFIX_CODE)) {
+            return true;
+        }
+        return false;
+    }
+
+    private static String parseEscape(String text) {
+        if (text.indexOf(ESCAPE_CHAR) < 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 == ESCAPE_CHAR && pos + 1 < length) {
+                // Skip escape char
+                pos++;
+                sb.append(text.charAt(pos));
+            } else {
+                sb.append(c);
+            }
+        }
+        return sb.toString();
+    }
+
+    private static int indexOfLabelEnd(String moreKeySpec, int start) {
+        if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) {
+            final int end = moreKeySpec.indexOf(LABEL_END, start);
+            if (end == 0) {
+                throw new KeySpecParserError(LABEL_END + " 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 == ESCAPE_CHAR && pos + 1 < length) {
+                // Skip escape char
+                pos++;
+            } else if (c == LABEL_END) {
+                return pos;
+            }
+        }
+        return -1;
+    }
+
+    public static String getLabel(String moreKeySpec) {
+        if (hasIcon(moreKeySpec)) {
+            return null;
+        }
+        final int end = indexOfLabelEnd(moreKeySpec, 0);
+        final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
+                : parseEscape(moreKeySpec);
+        if (TextUtils.isEmpty(label)) {
+            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+        }
+        return label;
+    }
+
+    public static String getOutputText(String moreKeySpec) {
+        if (hasCode(moreKeySpec)) {
+            return null;
+        }
+        final int end = indexOfLabelEnd(moreKeySpec, 0);
+        if (end > 0) {
+            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
+                    throw new KeySpecParserError("Multiple " + LABEL_END + ": "
+                            + moreKeySpec);
+            }
+            final String outputText = parseEscape(
+                    moreKeySpec.substring(end + /* LABEL_END */1));
+            if (!TextUtils.isEmpty(outputText)) {
+                return outputText;
+            }
+            throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
+        }
+        final String label = getLabel(moreKeySpec);
+        if (label == null) {
+            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+        }
+        // Code is automatically generated for one letter label. See {@link getCode()}.
+        return (Utils.codePointCount(label) == 1) ? null : label;
+    }
+
+    public static int getCode(Resources res, String moreKeySpec) {
+        if (hasCode(moreKeySpec)) {
+            final int end = indexOfLabelEnd(moreKeySpec, 0);
+            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
+                throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
+            }
+            final int resId = getResourceId(res,
+                    moreKeySpec.substring(end + /* LABEL_END */1 + /* PREFIX_AT */1),
+                    R.string.english_ime_name);
+            final int code = res.getInteger(resId);
+            return code;
+        }
+        if (indexOfLabelEnd(moreKeySpec, 0) > 0) {
+            return Keyboard.CODE_OUTPUT_TEXT;
+        }
+        final String label = getLabel(moreKeySpec);
+        // Code is automatically generated for one letter label.
+        if (Utils.codePointCount(label) == 1) {
+            return label.codePointAt(0);
+        }
+        return Keyboard.CODE_OUTPUT_TEXT;
+    }
+
+    public static int getIconAttrId(String moreKeySpec) {
+        if (hasIcon(moreKeySpec)) {
+            final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
+            final String name = moreKeySpec.substring(PREFIX_ICON.length(), end);
+            return KeyboardIconsSet.getIconAttrId(name);
+        }
+        return KeyboardIconsSet.ICON_UNDEFINED;
+    }
+
+    public static String[] insertAddtionalMoreKeys(String[] moreKeys, String[] additionalMoreKeys) {
+        final int moreKeysCount = (moreKeys != null) ? moreKeys.length : 0;
+        final int additionalCount = (additionalMoreKeys != null) ? additionalMoreKeys.length : 0;
+        ArrayList<String> out = null;
+        int additionalIndex = 0;
+        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
+            final String moreKeySpec = moreKeys[moreKeyIndex];
+            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
+                if (additionalIndex < additionalCount) {
+                    // Replace '%' marker with additional more key specification.
+                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
+                    if (out != null) {
+                        out.add(additionalMoreKey);
+                    } else {
+                        moreKeys[moreKeyIndex] = additionalMoreKey;
+                    }
+                    additionalIndex++;
+                } else {
+                    // Filter out excessive '%' marker.
+                    if (out == null) {
+                        out = new ArrayList<String>(moreKeyIndex);
+                        for (int i = 0; i < moreKeyIndex; i++) {
+                            out.add(moreKeys[i]);
+                        }
+                    }
+                }
+            } else {
+                if (out != null) {
+                    out.add(moreKeySpec);
+                }
+            }
+        }
+        if (additionalCount > 0 && additionalIndex == 0) {
+            // No '%' marker is found in more keys.
+            // Insert all additional more keys to the head of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = new ArrayList<String>(additionalCount + moreKeysCount);
+            for (int i = additionalIndex; i < additionalCount; i++) {
+                out.add(additionalMoreKeys[i]);
+            }
+            for (int i = 0; i < moreKeysCount; i++) {
+                out.add(moreKeys[i]);
+            }
+        } else if (additionalIndex < additionalCount) {
+            // The number of '%' markers are less than additional more keys.
+            // Append remained additional more keys to the tail of more keys.
+            if (DEBUG && out != null) {
+                throw new RuntimeException("Internal logic error:"
+                        + " moreKeys=" + Arrays.toString(moreKeys)
+                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
+            }
+            out = new ArrayList<String>(moreKeysCount);
+            for (int i = 0; i < moreKeysCount; i++) {
+                out.add(moreKeys[i]);
+            }
+            for (int i = additionalIndex; i < additionalCount; i++) {
+                out.add(additionalMoreKeys[additionalIndex]);
+            }
+        }
+        if (out != null) {
+            return out.size() > 0 ? out.toArray(new String[out.size()]) : null;
+        } else {
+            return moreKeys;
+        }
+    }
+
+    @SuppressWarnings("serial")
+    public static class KeySpecParserError extends RuntimeException {
+        public KeySpecParserError(String message) {
+            super(message);
+        }
+    }
+
+    private static int getResourceId(Resources res, String name, int packageNameResId) {
+        String packageName = res.getResourcePackageName(packageNameResId);
+        int resId = res.getIdentifier(name, null, packageName);
+        if (resId == 0) {
+            throw new RuntimeException("Unknown resource: " + name);
+        }
+        return resId;
+    }
+
+    private static String resolveStringResource(String text, Resources res, int packageNameResId) {
+        final int size = text.length();
+        if (size < PREFIX_STRING.length()) {
+            return text;
+        }
+
+        StringBuilder sb = null;
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == PREFIX_AT && text.startsWith(PREFIX_STRING, pos)) {
+                if (sb == null) {
+                    sb = new StringBuilder(text.substring(0, pos));
+                }
+                final int end = searchResourceNameEnd(text, pos + PREFIX_STRING.length());
+                final String resName = text.substring(pos + 1, end);
+                final int resId = getResourceId(res, resName, packageNameResId);
+                sb.append(res.getString(resId));
+                pos = end - 1;
+            } else if (c == ESCAPE_CHAR) {
+                pos++;
+                if (sb != null) {
+                    sb.append(c);
+                    if (pos < size) {
+                        sb.append(text.charAt(pos));
+                    }
+                }
+            } else if (sb != null) {
+                sb.append(c);
+            }
+        }
+        return (sb == null) ? text : sb.toString();
+    }
+
+    private static int searchResourceNameEnd(String text, int start) {
+        final int size = text.length();
+        if (start >= size || text.charAt(start) != SUFFIX_SLASH) {
+            throw new RuntimeException("Resource name not specified");
+        }
+        for (int pos = start + 1; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            // String resource name should be consisted of [a-z_0-9].
+            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
+                continue;
+            }
+            return pos;
+        }
+        return size;
+    }
+
+    public static String[] parseCsvString(String rawText, Resources res, int packageNameResId) {
+        final String text = resolveStringResource(rawText, res, packageNameResId);
+        final int size = text.length();
+        if (size == 0) {
+            return null;
+        }
+        if (Utils.codePointCount(text) == 1) {
+            return new String[] { text };
+        }
+
+        final StringBuilder sb = new StringBuilder();
+        ArrayList<String> list = null;
+        int start = 0;
+        for (int pos = 0; pos < size; pos++) {
+            final char c = text.charAt(pos);
+            if (c == COMMA) {
+                if (list == null) {
+                    list = new ArrayList<String>();
+                }
+                if (sb.length() == 0) {
+                    list.add(text.substring(start, pos));
+                } else {
+                    list.add(sb.toString());
+                    sb.setLength(0);
+                }
+                // Skip comma
+                start = pos + 1;
+                continue;
+            }
+            // TODO: Only parse escaped comma. Other escaped character should be passed through
+            // with escaped character prefixed.
+            // Skip escaped sequence.
+            if (c == ESCAPE_CHAR) {
+                if (start == pos) {
+                    // Skip escaping comma at the beginning of the text.
+                    start++;
+                    pos++;
+                } else {
+                    if (start < pos && sb.length() == 0) {
+                        sb.append(text.substring(start, pos));
+                    }
+                    // Skip comma
+                    pos++;
+                    if (pos < size) {
+                        sb.append(text.charAt(pos));
+                    }
+                }
+            } else if (sb.length() > 0) {
+                sb.append(c);
+            }
+        }
+        if (list == null) {
+            return new String[] {
+                    sb.length() > 0 ? sb.toString() : text.substring(start)
+            };
+        } else {
+            list.add(sb.length() > 0 ? sb.toString() : text.substring(start));
+            return list.toArray(new String[list.size()]);
+        }
+    }
+}
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
index 25a2c23..6ec56ca 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyles.java
@@ -21,7 +21,6 @@
 
 import com.android.inputmethod.keyboard.Keyboard;
 import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.XmlParseUtils;
 
 import org.xmlpull.v1.XmlPullParser;
@@ -72,7 +71,7 @@
         protected static String[] parseStringArray(TypedArray a, int index) {
             if (!a.hasValue(index))
                 return null;
-            return Utils.parseCsvString(
+            return KeySpecParser.parseCsvString(
                     a.getString(index), a.getResources(), R.string.english_ime_name);
         }
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
deleted file mode 100644
index abebfec..0000000
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParser.java
+++ /dev/null
@@ -1,256 +0,0 @@
-/*
- * Copyright (C) 2010 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 android.content.res.Resources;
-import android.text.TextUtils;
-
-import com.android.inputmethod.keyboard.Keyboard;
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.R;
-import com.android.inputmethod.latin.Utils;
-
-import java.util.ArrayList;
-import java.util.Arrays;
-
-/**
- * String parser of moreKeys attribute of Key.
- * The string is comma separated texts each of which represents one "more key".
- * Each "more key" specification is one of the following:
- * - A single letter (Letter)
- * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (@icon/icon_name|@integer/key_code)
- * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\'
- * character.
- * Note that the character '@' and '\' are also parsed by XML parser and CSV parser as well.
- * See {@link KeyboardIconsSet} about icon_number.
- */
-public class MoreKeySpecParser {
-    private static final boolean DEBUG = LatinImeLogger.sDBG;
-    private static final char LABEL_END = '|';
-    private static final String PREFIX_ICON = Utils.PREFIX_AT + "icon" + Utils.SUFFIX_SLASH;
-    private static final String PREFIX_CODE = Utils.PREFIX_AT + "integer" + Utils.SUFFIX_SLASH;
-    private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
-
-    private MoreKeySpecParser() {
-        // Intentional empty constructor for utility class.
-    }
-
-    private static boolean hasIcon(String moreKeySpec) {
-        if (moreKeySpec.startsWith(PREFIX_ICON)) {
-            final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (end > 0) {
-                return true;
-            }
-            throw new MoreKeySpecParserError("outputText or code not specified: " + moreKeySpec);
-        }
-        return false;
-    }
-
-    private static boolean hasCode(String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0 && end + 1 < moreKeySpec.length()
-                && moreKeySpec.substring(end + 1).startsWith(PREFIX_CODE)) {
-            return true;
-        }
-        return false;
-    }
-
-    private static String parseEscape(String text) {
-        if (text.indexOf(Utils.ESCAPE_CHAR) < 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 == Utils.ESCAPE_CHAR && pos + 1 < length) {
-                sb.append(text.charAt(++pos));
-            } else {
-                sb.append(c);
-            }
-        }
-        return sb.toString();
-    }
-
-    private static int indexOfLabelEnd(String moreKeySpec, int start) {
-        if (moreKeySpec.indexOf(Utils.ESCAPE_CHAR, start) < 0) {
-            final int end = moreKeySpec.indexOf(LABEL_END, start);
-            if (end == 0) {
-                throw new MoreKeySpecParserError(LABEL_END + " 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 == Utils.ESCAPE_CHAR && pos + 1 < length) {
-                pos++;
-            } else if (c == LABEL_END) {
-                return pos;
-            }
-        }
-        return -1;
-    }
-
-    public static String getLabel(String moreKeySpec) {
-        if (hasIcon(moreKeySpec)) {
-            return null;
-        }
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end))
-                : parseEscape(moreKeySpec);
-        if (TextUtils.isEmpty(label)) {
-            throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
-        }
-        return label;
-    }
-
-    public static String getOutputText(String moreKeySpec) {
-        if (hasCode(moreKeySpec)) {
-            return null;
-        }
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0) {
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                    throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": "
-                            + moreKeySpec);
-            }
-            final String outputText = parseEscape(
-                    moreKeySpec.substring(end + /* LABEL_END */1));
-            if (!TextUtils.isEmpty(outputText)) {
-                return outputText;
-            }
-            throw new MoreKeySpecParserError("Empty outputText: " + moreKeySpec);
-        }
-        final String label = getLabel(moreKeySpec);
-        if (label == null) {
-            throw new MoreKeySpecParserError("Empty label: " + moreKeySpec);
-        }
-        // Code is automatically generated for one letter label. See {@link getCode()}.
-        return (label.length() == 1) ? null : label;
-    }
-
-    public static int getCode(Resources res, String moreKeySpec) {
-        if (hasCode(moreKeySpec)) {
-            final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                throw new MoreKeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec);
-            }
-            final int resId = Utils.getResourceId(res,
-                    moreKeySpec.substring(end + /* LABEL_END */1 + /* PREFIX_AT */1),
-                    R.string.english_ime_name);
-            final int code = res.getInteger(resId);
-            return code;
-        }
-        if (indexOfLabelEnd(moreKeySpec, 0) > 0) {
-            return Keyboard.CODE_OUTPUT_TEXT;
-        }
-        final String label = getLabel(moreKeySpec);
-        // Code is automatically generated for one letter label.
-        if (label != null && label.length() == 1) {
-            return label.charAt(0);
-        }
-        return Keyboard.CODE_OUTPUT_TEXT;
-    }
-
-    public static int getIconAttrId(String moreKeySpec) {
-        if (hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length());
-            final String name = moreKeySpec.substring(PREFIX_ICON.length(), end);
-            return KeyboardIconsSet.getIconAttrId(name);
-        }
-        return KeyboardIconsSet.ICON_UNDEFINED;
-    }
-
-    public static String[] insertAddtionalMoreKeys(String[] moreKeys, String[] additionalMoreKeys) {
-        final int moreKeysCount = (moreKeys != null) ? moreKeys.length : 0;
-        final int additionalCount = (additionalMoreKeys != null) ? additionalMoreKeys.length : 0;
-        ArrayList<String> out = null;
-        int additionalIndex = 0;
-        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
-            final String moreKeySpec = moreKeys[moreKeyIndex];
-            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
-                if (additionalIndex < additionalCount) {
-                    // Replace '%' marker with additional more key specification.
-                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
-                    if (out != null) {
-                        out.add(additionalMoreKey);
-                    } else {
-                        moreKeys[moreKeyIndex] = additionalMoreKey;
-                    }
-                    additionalIndex++;
-                } else {
-                    // Filter out excessive '%' marker.
-                    if (out == null) {
-                        out = new ArrayList<String>(moreKeyIndex);
-                        for (int i = 0; i < moreKeyIndex; i++) {
-                            out.add(moreKeys[i]);
-                        }
-                    }
-                }
-            } else {
-                if (out != null) {
-                    out.add(moreKeySpec);
-                }
-            }
-        }
-        if (additionalCount > 0 && additionalIndex == 0) {
-            // No '%' marker is found in more keys.
-            // Insert all additional more keys to the head of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = new ArrayList<String>(additionalCount + moreKeysCount);
-            for (int i = additionalIndex; i < additionalCount; i++) {
-                out.add(additionalMoreKeys[i]);
-            }
-            for (int i = 0; i < moreKeysCount; i++) {
-                out.add(moreKeys[i]);
-            }
-        } else if (additionalIndex < additionalCount) {
-            // The number of '%' markers are less than additional more keys.
-            // Append remained additional more keys to the tail of more keys.
-            if (DEBUG && out != null) {
-                throw new RuntimeException("Internal logic error:"
-                        + " moreKeys=" + Arrays.toString(moreKeys)
-                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
-            }
-            out = new ArrayList<String>(moreKeysCount);
-            for (int i = 0; i < moreKeysCount; i++) {
-                out.add(moreKeys[i]);
-            }
-            for (int i = additionalIndex; i < additionalCount; i++) {
-                out.add(additionalMoreKeys[additionalIndex]);
-            }
-        }
-        if (out != null) {
-            return out.size() > 0 ? out.toArray(new String[out.size()]) : null;
-        } else {
-            return moreKeys;
-        }
-    }
-
-    @SuppressWarnings("serial")
-    public static class MoreKeySpecParserError extends RuntimeException {
-        public MoreKeySpecParserError(String message) {
-            super(message);
-        }
-    }
-}
diff --git a/java/src/com/android/inputmethod/latin/SettingsValues.java b/java/src/com/android/inputmethod/latin/SettingsValues.java
index 5f9cb8d..8e2f605 100644
--- a/java/src/com/android/inputmethod/latin/SettingsValues.java
+++ b/java/src/com/android/inputmethod/latin/SettingsValues.java
@@ -25,7 +25,7 @@
 
 import com.android.inputmethod.compat.InputTypeCompatUtils;
 import com.android.inputmethod.compat.VibratorCompatWrapper;
-import com.android.inputmethod.keyboard.internal.MoreKeySpecParser;
+import com.android.inputmethod.keyboard.internal.KeySpecParser;
 
 import java.util.Arrays;
 import java.util.Locale;
@@ -100,7 +100,7 @@
                 }
             }
         }
-        final String[] suggestPuncsSpec = Utils.parseCsvString(
+        final String[] suggestPuncsSpec = KeySpecParser.parseCsvString(
                 res.getString(R.string.suggested_punctuations), res, R.string.english_ime_name);
         mSuggestPuncs = createSuggestPuncs(suggestPuncsSpec);
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
@@ -158,7 +158,7 @@
         final StringBuilder sb = new StringBuilder();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
-                sb.append(MoreKeySpecParser.getLabel(puncSpec));
+                sb.append(KeySpecParser.getLabel(puncSpec));
             }
         }
         return sb.toString();
@@ -168,7 +168,7 @@
         final SuggestedWords.Builder builder = new SuggestedWords.Builder();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
-                builder.addWord(MoreKeySpecParser.getLabel(puncSpec));
+                builder.addWord(KeySpecParser.getLabel(puncSpec));
             }
         }
         return builder.setIsPunctuationSuggestions().build();
@@ -178,11 +178,11 @@
         final SuggestedWords.Builder builder = new SuggestedWords.Builder();
         if (puncs != null) {
             for (final String puncSpec : puncs) {
-                final String outputText = MoreKeySpecParser.getOutputText(puncSpec);
+                final String outputText = KeySpecParser.getOutputText(puncSpec);
                 if (outputText != null) {
                     builder.addWord(outputText);
                 } else {
-                    builder.addWord(MoreKeySpecParser.getLabel(puncSpec));
+                    builder.addWord(KeySpecParser.getLabel(puncSpec));
                 }
             }
         }
diff --git a/java/src/com/android/inputmethod/latin/Utils.java b/java/src/com/android/inputmethod/latin/Utils.java
index d1b808f..3975ddd 100644
--- a/java/src/com/android/inputmethod/latin/Utils.java
+++ b/java/src/com/android/inputmethod/latin/Utils.java
@@ -62,12 +62,6 @@
     private static boolean DBG = LatinImeLogger.sDBG;
     private static boolean DBG_EDIT_DISTANCE = false;
 
-    // Constants for resource name parsing.
-    public static final char ESCAPE_CHAR = '\\';
-    public static final char PREFIX_AT = '@';
-    public static final char SUFFIX_SLASH = '/';
-    private static final String PREFIX_STRING = PREFIX_AT + "string";
-
     private Utils() {
         // Intentional empty constructor for utility class.
     }
@@ -800,116 +794,8 @@
         }
     }
 
-    public static int getResourceId(Resources res, String name, int packageNameResId) {
-        String packageName = res.getResourcePackageName(packageNameResId);
-        int resId = res.getIdentifier(name, null, packageName);
-        if (resId == 0) {
-            throw new RuntimeException("Unknown resource: " + name);
-        }
-        return resId;
-    }
-
-    public static String resolveStringResource(String text, Resources res, int packageNameResId) {
-        final int size = text.length();
-        if (size < PREFIX_STRING.length()) {
-            return text;
-        }
-
-        StringBuilder sb = null;
-        for (int pos = 0; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            if (c == PREFIX_AT && text.startsWith(PREFIX_STRING, pos)) {
-                if (sb == null) {
-                    sb = new StringBuilder(text.substring(0, pos));
-                }
-                final int end = Utils.searchResourceNameEnd(text, pos + PREFIX_STRING.length());
-                final String resName = text.substring(pos + 1, end);
-                final int resId = getResourceId(res, resName, packageNameResId);
-                sb.append(res.getString(resId));
-                pos = end - 1;
-            } else if (c == ESCAPE_CHAR) {
-                pos++;
-                if (sb != null) {
-                    sb.append(c);
-                    if (pos < size) {
-                        sb.append(text.charAt(pos));
-                    }
-                }
-            } else if (sb != null) {
-                sb.append(c);
-            }
-        }
-        return (sb == null) ? text : sb.toString();
-    }
-
-    private static int searchResourceNameEnd(String text, int start) {
-        final int size = text.length();
-        if (start >= size || text.charAt(start) != SUFFIX_SLASH) {
-            throw new RuntimeException("Resource name not specified");
-        }
-        for (int pos = start + 1; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            // String resource name should be consisted of [a-z_0-9].
-            if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) {
-                continue;
-            }
-            return pos;
-        }
-        return size;
-    }
-
-    public static String[] parseCsvString(String rawText, Resources res, int packageNameResId) {
-        final String text = resolveStringResource(rawText, res, packageNameResId);
-        final int size = text.length();
-        if (size == 0) {
-            return null;
-        }
-        if (size == 1) {
-            return new String[] { text };
-        }
-
-        final StringBuilder sb = new StringBuilder();
-        ArrayList<String> list = null;
-        int start = 0;
-        for (int pos = 0; pos < size; pos++) {
-            final char c = text.charAt(pos);
-            if (c == ',') {
-                if (list == null) {
-                    list = new ArrayList<String>();
-                }
-                if (sb.length() == 0) {
-                    list.add(text.substring(start, pos));
-                } else {
-                    list.add(sb.toString());
-                    sb.setLength(0);
-                }
-                start = pos + 1;
-                continue;
-            } else if (c == ESCAPE_CHAR) {
-                if (start == pos) {
-                    // Skip escape character at the beginning of the value.
-                    start++;
-                    pos++;
-                } else {
-                    if (start < pos && sb.length() == 0) {
-                        sb.append(text.subSequence(start, pos));
-                    }
-                    pos++;
-                    if (pos < size) {
-                        sb.append(text.charAt(pos));
-                    }
-                }
-            } else if (sb.length() > 0) {
-                sb.append(c);
-            }
-        }
-        if (list == null) {
-            return new String[] {
-                    sb.length() > 0 ? sb.toString() : text.substring(start)
-            };
-        } else {
-            list.add(sb.length() > 0 ? sb.toString() : text.substring(start));
-            return list.toArray(new String[list.size()]);
-        }
+    public static int codePointCount(String text) {
+        if (TextUtils.isEmpty(text)) return 0;
+        return text.codePointCount(0, text.length());
     }
 }
diff --git a/java/src/com/android/inputmethod/latin/WordComposer.java b/java/src/com/android/inputmethod/latin/WordComposer.java
index 230c291..bd244b9 100644
--- a/java/src/com/android/inputmethod/latin/WordComposer.java
+++ b/java/src/com/android/inputmethod/latin/WordComposer.java
@@ -16,8 +16,6 @@
 
 package com.android.inputmethod.latin;
 
-import android.text.TextUtils;
-
 import com.android.inputmethod.keyboard.Key;
 import com.android.inputmethod.keyboard.KeyDetector;
 import com.android.inputmethod.keyboard.Keyboard;
@@ -33,7 +31,7 @@
     public static final int NOT_A_CODE = KeyDetector.NOT_A_CODE;
     public static final int NOT_A_COORDINATE = -1;
 
-    final int N = BinaryDictionary.MAX_WORD_LENGTH;
+    final static int N = BinaryDictionary.MAX_WORD_LENGTH;
 
     private ArrayList<int[]> mCodes;
     private int[] mXCoordinates;
diff --git a/native/src/correction.cpp b/native/src/correction.cpp
index 7323747..dafc0fd 100644
--- a/native/src/correction.cpp
+++ b/native/src/correction.cpp
@@ -383,7 +383,10 @@
             incrementInputIndex();
         } else {
             --mTransposedCount;
-            if (DEBUG_CORRECTION) {
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 DUMP_WORD(mWord, mOutputIndex);
                 AKLOGI("UNRELATED(0): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
                         mTransposedCount, mExcessiveCount, c);
@@ -404,13 +407,17 @@
             : mProximityInfo->getMatchedProximityId(
                     mInputIndex, c, checkProximityChars, &proximityIndex);
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId) {
+    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
+            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
         if (canTryCorrection && mOutputIndex > 0
                 && mCorrectionStates[mOutputIndex].mProximityMatching
                 && mCorrectionStates[mOutputIndex].mExceeding
                 && isEquivalentChar(mProximityInfo->getMatchedProximityId(
                         mInputIndex, mWord[mOutputIndex - 1], false))) {
-            if (DEBUG_CORRECTION) {
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 AKLOGI("CONVERSION p->e %c", mWord[mOutputIndex - 1]);
             }
             // Conversion p->e
@@ -429,7 +436,8 @@
         }
     }
 
-    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId) {
+    if (ProximityInfo::UNRELATED_CHAR == matchedProximityCharId
+            || ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
         // TODO: Optimize
         // As the current char turned out to be an unrelated char,
         // we will try other correction-types. Please note that mCorrectionStates[mOutputIndex]
@@ -481,12 +489,47 @@
                 ++mExcessiveCount;
                 incrementInputIndex();
             }
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
+                DUMP_WORD(mWord, mOutputIndex);
+                if (mTransposing) {
+                    AKLOGI("TRANSPOSE: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
+                            mTransposedCount, mExcessiveCount, c);
+                } else {
+                    AKLOGI("EXCEED: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
+                            mTransposedCount, mExcessiveCount, c);
+                }
+            }
         } else if (mSkipping) {
             // 3. Skip correction
             ++mSkippedCount;
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
+                AKLOGI("SKIP: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
+                        mTransposedCount, mExcessiveCount, c);
+            }
             return processSkipChar(c, isTerminal, false);
+        } else if (ProximityInfo::ADDITIONAL_PROXIMITY_CHAR == matchedProximityCharId) {
+            // As a last resort, use additional proximity characters
+            mProximityMatching = true;
+            ++mProximityCount;
+            mDistances[mOutputIndex] = ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO;
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
+                AKLOGI("ADDITIONALPROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
+                        mTransposedCount, mExcessiveCount, c);
+            }
         } else {
-            if (DEBUG_CORRECTION) {
+            if (DEBUG_CORRECTION
+                    && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                    && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                            || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
                 DUMP_WORD(mWord, mOutputIndex);
                 AKLOGI("UNRELATED(1): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
                         mTransposedCount, mExcessiveCount, c);
@@ -506,6 +549,13 @@
         ++mProximityCount;
         mDistances[mOutputIndex] =
                 mProximityInfo->getNormalizedSquaredDistance(mInputIndex, proximityIndex);
+        if (DEBUG_CORRECTION
+                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0
+                        || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
+            AKLOGI("PROX: %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
+                    mTransposedCount, mExcessiveCount, c);
+        }
     }
 
     addCharToCurrentWord(c);
@@ -539,7 +589,9 @@
             || isSameAsUserTypedLength) && isTerminal) {
         mTerminalInputIndex = mInputIndex - 1;
         mTerminalOutputIndex = mOutputIndex - 1;
-        if (DEBUG_CORRECTION) {
+        if (DEBUG_CORRECTION
+                && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == mInputLength)
+                && (MIN_OUTPUT_INDEX_FOR_DEBUG <= 0 || MIN_OUTPUT_INDEX_FOR_DEBUG < mOutputIndex)) {
             DUMP_WORD(mWord, mOutputIndex);
             AKLOGI("ONTERMINAL(1): %d, %d, %d, %d, %c", mProximityCount, mSkippedCount,
                     mTransposedCount, mExcessiveCount, c);
@@ -678,7 +730,7 @@
     if (excessiveCount > 0) {
         multiplyRate(WORDS_WITH_EXCESSIVE_CHARACTER_DEMOTION_RATE, &finalFreq);
         if (!lastCharExceeded && !proximityInfo->existsAdjacentProximityChars(excessivePos)) {
-            if (DEBUG_CORRECTION_FREQ) {
+            if (DEBUG_DICT_FULL) {
                 AKLOGI("Double excessive demotion");
             }
             // If an excessive character is not adjacent to the left char or the right char,
@@ -687,51 +739,46 @@
         }
     }
 
+    const bool performTouchPositionCorrection =
+            CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled()
+                        && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0;
     // Score calibration by touch coordinates is being done only for pure-fat finger typing error
     // cases.
     // TODO: Remove this constraint.
-    if (CALIBRATE_SCORE_BY_TOUCH_COORDINATES && proximityInfo->touchPositionCorrectionEnabled()
-            && skippedCount == 0 && excessiveCount == 0 && transposedCount == 0) {
-        for (int i = 0; i < outputLength; ++i) {
-            const int squaredDistance = correction->mDistances[i];
-            if (i < adjustedProximityMatchedCount) {
-                multiplyIntCapped(typedLetterMultiplier, &finalFreq);
-            }
-            if (squaredDistance >= 0) {
-                // Promote or demote the score according to the distance from the sweet spot
-                static const float A = ZERO_DISTANCE_PROMOTION_RATE / 100.0f;
-                static const float B = 1.0f;
-                static const float C = 0.5f;
-                static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS;
-                static const float R2 = HALF_SCORE_SQUARED_RADIUS;
-                const float x = (float)squaredDistance
-                        / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
-                const float factor = (x < R1)
-                    ? (A * (R1 - x) + B * x) / R1
-                    : (B * (R2 - x) + C * (x - R1)) / (R2 - R1);
-                // factor is piecewise linear function like:
-                // A -_                  .
-                //     ^-_               .
-                // B      \              .
-                //         \             .
-                // C        \            .
-                //   0   R1 R2
-                if (factor <= 0) {
-                    return -1;
-                }
-                multiplyRate((int)(factor * 100), &finalFreq);
-            } else if (squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) {
-                multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
-            }
-        }
-    } else {
-        // Promotion for a word with proximity characters
-        for (int i = 0; i < adjustedProximityMatchedCount; ++i) {
-            // A word with proximity corrections
-            if (DEBUG_DICT_FULL) {
-                AKLOGI("Found a proximity correction.");
-            }
+    for (int i = 0; i < outputLength; ++i) {
+        const int squaredDistance = correction->mDistances[i];
+        if (i < adjustedProximityMatchedCount) {
             multiplyIntCapped(typedLetterMultiplier, &finalFreq);
+        }
+
+        if (performTouchPositionCorrection && squaredDistance >= 0) {
+            // Promote or demote the score according to the distance from the sweet spot
+            static const float A = ZERO_DISTANCE_PROMOTION_RATE / 100.0f;
+            static const float B = 1.0f;
+            static const float C = 0.5f;
+            static const float MIN = 0.3f;
+            static const float R1 = NEUTRAL_SCORE_SQUARED_RADIUS;
+            static const float R2 = HALF_SCORE_SQUARED_RADIUS;
+            const float x = (float)squaredDistance
+                    / ProximityInfo::NORMALIZED_SQUARED_DISTANCE_SCALING_FACTOR;
+            const float factor = max((x < R1)
+                ? (A * (R1 - x) + B * x) / R1
+                : (B * (R2 - x) + C * (x - R1)) / (R2 - R1), MIN);
+            // factor is piecewise linear function like:
+            // A -_                  .
+            //     ^-_               .
+            // B      \              .
+            //         \_            .
+            // C         ------------.
+            //                       .
+            // 0   R1 R2             .
+            multiplyRate((int)(factor * 100), &finalFreq);
+        } else if (performTouchPositionCorrection
+                && squaredDistance == PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO) {
+            multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
+        } else if (squaredDistance == ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO) {
+            multiplyRate(WORDS_WITH_ADDITIONAL_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
+        } else if (i < adjustedProximityMatchedCount) {
             multiplyRate(WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE, &finalFreq);
         }
     }
@@ -794,7 +841,8 @@
         AKLOGI("calc: %d, %d", outputLength, sameLength);
     }
 
-    if (DEBUG_CORRECTION_FREQ) {
+    if (DEBUG_CORRECTION_FREQ
+            && (INPUTLENGTH_FOR_DEBUG <= 0 || INPUTLENGTH_FOR_DEBUG == inputLength)) {
         DUMP_WORD(correction->mWord, outputLength);
         AKLOGI("FinalFreq: [P%d, S%d, T%d, E%d] %d, %d, %d, %d, %d, %d", proximityMatchedCount,
                 skippedCount, transposedCount, excessiveCount, outputLength, lastCharExceeded,
diff --git a/native/src/correction.h b/native/src/correction.h
index b246070..a711c99 100644
--- a/native/src/correction.h
+++ b/native/src/correction.h
@@ -85,7 +85,7 @@
         }
     }
 
-    Correction(const int typedLetterMultiplier, const int fullWordMultiplier);
+            Correction(const int typedLetterMultiplier, const int fullWordMultiplier);
     void initCorrection(
             const ProximityInfo *pi, const int inputLength, const int maxWordLength);
     void initCorrectionState(const int rootPos, const int childCount, const bool traverseAll);
diff --git a/native/src/defines.h b/native/src/defines.h
index 3f3f5ba..02c1fe0 100644
--- a/native/src/defines.h
+++ b/native/src/defines.h
@@ -172,6 +172,7 @@
 #define NOT_A_COORDINATE -1
 #define EQUIVALENT_CHAR_WITHOUT_DISTANCE_INFO -2
 #define PROXIMITY_CHAR_WITHOUT_DISTANCE_INFO -3
+#define ADDITIONAL_PROXIMITY_CHAR_DISTANCE_INFO -4
 #define NOT_A_INDEX -1
 #define NOT_A_FREQUENCY -1
 
@@ -194,6 +195,7 @@
 #define WORDS_WITH_TRANSPOSED_CHARACTERS_DEMOTION_RATE 70
 #define FULL_MATCHED_WORDS_PROMOTION_RATE 120
 #define WORDS_WITH_PROXIMITY_CHARACTER_DEMOTION_RATE 90
+#define WORDS_WITH_ADDITIONAL_PROXIMITY_CHARACTER_DEMOTION_RATE 30
 #define WORDS_WITH_MATCH_SKIP_PROMOTION_RATE 105
 #define WORDS_WITH_JUST_ONE_CORRECTION_PROMOTION_RATE 160
 #define CORRECTION_COUNT_RATE_DEMOTION_RATE_BASE 45
@@ -210,6 +212,9 @@
 // This is only used for the size of array. Not to be used in c functions.
 #define MAX_WORD_LENGTH_INTERNAL 48
 
+// This must be equal to ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE in KeyDetector.java
+#define ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE 2
+
 // Word limit for sub queues used in WordsPriorityQueuePool.  Sub queues are temporary queues used
 // for better performance.
 // Holds up to 1 candidate for each word
@@ -241,4 +246,8 @@
 // The ratio of neutral area radius to sweet spot radius.
 #define NEUTRAL_AREA_RADIUS_RATIO 1.3f
 
+// DEBUG
+#define INPUTLENGTH_FOR_DEBUG -1
+#define MIN_OUTPUT_INDEX_FOR_DEBUG -1
+
 #endif // LATINIME_DEFINES_H
diff --git a/native/src/proximity_info.cpp b/native/src/proximity_info.cpp
index e0e9380..b6bab22 100644
--- a/native/src/proximity_info.cpp
+++ b/native/src/proximity_info.cpp
@@ -261,7 +261,8 @@
 
     // Not an exact nor an accent-alike match: search the list of close keys
     int j = 1;
-    while (j < MAX_PROXIMITY_CHARS_SIZE && currentChars[j] > 0) {
+    while (j < MAX_PROXIMITY_CHARS_SIZE
+            && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
         const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
         if (matched) {
             if (proximityIndex) {
@@ -271,6 +272,21 @@
         }
         ++j;
     }
+    if (j < MAX_PROXIMITY_CHARS_SIZE
+            && currentChars[j] == ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+        ++j;
+        while (j < MAX_PROXIMITY_CHARS_SIZE
+                && currentChars[j] > ADDITIONAL_PROXIMITY_CHAR_DELIMITER_CODE) {
+            const bool matched = (currentChars[j] == baseLowerC || currentChars[j] == c);
+            if (matched) {
+                if (proximityIndex) {
+                    *proximityIndex = j;
+                }
+                return ADDITIONAL_PROXIMITY_CHAR;
+            }
+            ++j;
+        }
+    }
 
     // Was not included, signal this as an unrelated character.
     return UNRELATED_CHAR;
diff --git a/native/src/proximity_info.h b/native/src/proximity_info.h
index 9ca5505..b77c1bb 100644
--- a/native/src/proximity_info.h
+++ b/native/src/proximity_info.h
@@ -38,7 +38,9 @@
         // It is a char located nearby on the keyboard
         NEAR_PROXIMITY_CHAR,
         // It is an unrelated char
-        UNRELATED_CHAR
+        UNRELATED_CHAR,
+        // Additional proximity char which can differ by language.
+        ADDITIONAL_PROXIMITY_CHAR
     } ProximityType;
 
     ProximityInfo(const int maxProximityCharsSize, const int keyboardWidth,
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java b/tests/src/com/android/inputmethod/keyboard/internal/CsvParserTests.java
similarity index 81%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/CsvParserTests.java
index 54a8e62..ef80d4f 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeyStylesTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/CsvParserTests.java
@@ -20,12 +20,11 @@
 import android.test.AndroidTestCase;
 import android.text.TextUtils;
 
-import com.android.inputmethod.latin.Utils;
 import com.android.inputmethod.latin.tests.R;
 
 import java.util.Arrays;
 
-public class KeyStylesTests extends AndroidTestCase {
+public class CsvParserTests extends AndroidTestCase {
     private Resources mTestResources;
 
     @Override
@@ -40,7 +39,7 @@
     }
 
     private void assertTextArray(String message, String value, String ... expected) {
-        final String actual[] = Utils.parseCsvString(value, mTestResources,
+        final String actual[] = KeySpecParser.parseCsvString(value, mTestResources,
                 R.string.empty_string);
         if (expected.length == 0) {
             assertNull(message, actual);
@@ -64,42 +63,77 @@
         }
     }
 
+    // \U001d11e: MUSICAL SYMBOL G CLEF
+    private static final String PAIR1 = "\ud834\udd1e";
+    // \U001d122: MUSICAL SYMBOL F CLEF
+    private static final String PAIR2 = "\ud834\udd22";
+    // \U002f8a6: CJK COMPATIBILITY IDEOGRAPH-2F8A6; variant character of \u6148.
+    private static final String PAIR3 = "\ud87e\udca6";
+    private static final String SURROGATE1 = PAIR1 + PAIR2;
+    private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
+
     public void testParseCsvTextZero() {
         assertTextArray("Empty string", "");
     }
 
     public void testParseCsvTextSingle() {
         assertTextArray("Single char", "a", "a");
+        assertTextArray("Surrogate pair", PAIR1, PAIR1);
         assertTextArray("Space", " ", " ");
         assertTextArray("Single label", "abc", "abc");
+        assertTextArray("Single srrogate pairs label", SURROGATE2, SURROGATE2);
         assertTextArray("Spaces", "   ", "   ");
         assertTextArray("Spaces in label", "a b c", "a b c");
         assertTextArray("Spaces at beginning of label", " abc", " abc");
         assertTextArray("Spaces at end of label", "abc ", "abc ");
         assertTextArray("Label surrounded by spaces", " abc ", " abc ");
+        assertTextArray("Surrogate pair surrounded by space",
+                " " + PAIR1 + " ",
+                " " + PAIR1 + " ");
+        assertTextArray("Surrogate pair within characters",
+                "ab" + PAIR2 + "cd",
+                "ab" + PAIR2 + "cd");
+        assertTextArray("Surrogate pairs within characters",
+                "ab" + SURROGATE1 + "cd",
+                "ab" + SURROGATE1 + "cd");
 
         assertTextArray("Incomplete resource reference 1", "string", "string");
         assertTextArray("Incomplete resource reference 2", "@strin", "@strin");
+        assertTextArray("Incomplete resource reference 3", "@" + SURROGATE2, "@" + SURROGATE2);
     }
 
     public void testParseCsvTextSingleEscaped() {
         assertTextArray("Escaped char", "\\a", "a");
+        assertTextArray("Escaped surrogate pair", "\\" + PAIR1, PAIR1);
         assertTextArray("Escaped comma", "\\,", ",");
         assertTextArray("Escaped escape", "\\\\", "\\");
         assertTextArray("Escaped label", "a\\bc", "abc");
+        assertTextArray("Escaped surrogate", "a\\" + PAIR1 + "c", "a" + PAIR1 + "c");
         assertTextArray("Escaped label at beginning", "\\abc", "abc");
+        assertTextArray("Escaped surrogate at beginning", "\\" + SURROGATE2, SURROGATE2);
         assertTextArray("Escaped label with comma", "a\\,c", "a,c");
+        assertTextArray("Escaped surrogate with comma", PAIR1 + "\\," + PAIR2, PAIR1 + "," + PAIR2);
         assertTextArray("Escaped label with comma at beginning", "\\,bc", ",bc");
+        assertTextArray("Escaped surrogate with comma at beginning",
+                "\\," + SURROGATE1, "," + SURROGATE1);
         assertTextArray("Escaped label with successive", "\\,\\\\bc", ",\\bc");
+        assertTextArray("Escaped surrogate with successive",
+                "\\,\\\\" + SURROGATE1, ",\\" + SURROGATE1);
         assertTextArray("Escaped label with escape", "a\\\\c", "a\\c");
+        assertTextArray("Escaped surrogate with escape",
+                PAIR1 + "\\\\" + PAIR2, PAIR1 + "\\" + PAIR2);
 
         assertTextArray("Escaped @string", "\\@string/empty_string", "@string/empty_string");
     }
 
     public void testParseCsvTextMulti() {
         assertTextArray("Multiple chars", "a,b,c", "a", "b", "c");
+        assertTextArray("Multiple surrogates", PAIR1 + "," + PAIR2 + "," + PAIR3,
+                PAIR1, PAIR2, PAIR3);
         assertTextArray("Multiple chars surrounded by spaces", " a , b , c ", " a ", " b ", " c ");
         assertTextArray("Multiple labels", "abc,def,ghi", "abc", "def", "ghi");
+        assertTextArray("Multiple surrogated", SURROGATE1 + "," + SURROGATE2,
+                SURROGATE1, SURROGATE2);
         assertTextArray("Multiple labels surrounded by spaces", " abc , def , ghi ",
                 " abc ", " def ", " ghi ");
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
similarity index 89%
rename from tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
index bc38cc1..d27c55c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
@@ -24,7 +24,7 @@
 
 import java.util.Arrays;
 
-public class MoreKeySpecParserTests extends AndroidTestCase {
+public class KeySpecParserTests extends AndroidTestCase {
     private Resources mRes;
 
     private static final int ICON_SETTINGS_KEY = R.styleable.Keyboard_iconSettingsKey;
@@ -49,16 +49,16 @@
 
     private void assertParser(String message, String moreKeySpec, String expectedLabel,
             String expectedOutputText, int expectedIcon, int expectedCode) {
-        String actualLabel = MoreKeySpecParser.getLabel(moreKeySpec);
+        String actualLabel = KeySpecParser.getLabel(moreKeySpec);
         assertEquals(message + ": label:", expectedLabel, actualLabel);
 
-        String actualOutputText = MoreKeySpecParser.getOutputText(moreKeySpec);
+        String actualOutputText = KeySpecParser.getOutputText(moreKeySpec);
         assertEquals(message + ": ouptputText:", expectedOutputText, actualOutputText);
 
-        int actualIcon = MoreKeySpecParser.getIconAttrId(moreKeySpec);
+        int actualIcon = KeySpecParser.getIconAttrId(moreKeySpec);
         assertEquals(message + ": icon:", expectedIcon, actualIcon);
 
-        int actualCode = MoreKeySpecParser.getCode(mRes, moreKeySpec);
+        int actualCode = KeySpecParser.getCode(mRes, moreKeySpec);
         assertEquals(message + ": codes value:", expectedCode, actualCode);
     }
 
@@ -73,9 +73,22 @@
         }
     }
 
+    // \U001d11e: MUSICAL SYMBOL G CLEF
+    private static final String PAIR1 = "\ud834\udd1e";
+    private static final int CODE1 = PAIR1.codePointAt(0);
+    // \U001d122: MUSICAL SYMBOL F CLEF
+    private static final String PAIR2 = "\ud834\udd22";
+    private static final int CODE2 = PAIR2.codePointAt(0);
+    // \U002f8a6: CJK COMPATIBILITY IDEOGRAPH-2F8A6; variant character of \u6148.
+    private static final String PAIR3 = "\ud87e\udca6";
+    private static final String SURROGATE1 = PAIR1 + PAIR2;
+    private static final String SURROGATE2 = PAIR1 + PAIR2 + PAIR3;
+
     public void testSingleLetter() {
         assertParser("Single letter", "a",
                 "a", null, ICON_UNDEFINED, 'a');
+        assertParser("Single surrogate", PAIR1,
+                PAIR1, null, ICON_UNDEFINED, CODE1);
         assertParser("Single escaped bar", "\\|",
                 "|", null, ICON_UNDEFINED, '|');
         assertParser("Single escaped escape", "\\\\",
@@ -86,20 +99,31 @@
                 ",", null, ICON_UNDEFINED, ',');
         assertParser("Single escaped letter", "\\a",
                 "a", null, ICON_UNDEFINED, 'a');
+        assertParser("Single escaped surrogate", "\\" + PAIR2,
+                PAIR2, null, ICON_UNDEFINED, CODE2);
         assertParser("Single at", "@",
                 "@", null, ICON_UNDEFINED, '@');
         assertParser("Single escaped at", "\\@",
                 "@", null, ICON_UNDEFINED, '@');
         assertParser("Single letter with outputText", "a|abc",
                 "a", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Single letter with surrogate outputText", "a|" + SURROGATE1,
+                "a", SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Single surrogate with outputText", PAIR3 + "|abc",
+                PAIR3, "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped outputText", "a|a\\|c",
                 "a", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Single letter with escaped surrogate outputText",
+                "a|" + PAIR1 + "\\|" + PAIR2,
+                "a", PAIR1 + "|" + PAIR2, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with comma outputText", "a|a,b",
                 "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped comma outputText", "a|a\\,b",
                 "a", "a,b", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with outputText starts with at", "a|@bc",
                 "a", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Single letter with surrogate outputText starts with at", "a|@" + SURROGATE2,
+                "a", "@" + SURROGATE2, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with outputText contains at", "a|a@c",
                 "a", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Single letter with escaped at outputText", "a|\\@bc",
@@ -115,8 +139,13 @@
     public void testLabel() {
         assertParser("Simple label", "abc",
                 "abc", "abc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Simple surrogate label", SURROGATE1,
+                SURROGATE1, SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped bar", "a\\|c",
                 "a|c", "a|c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Surrogate label with escaped bar", PAIR1 + "\\|" + PAIR2,
+                PAIR1 + "|" + PAIR2, PAIR1 + "|" + PAIR2,
+                ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped escape", "a\\\\c",
                 "a\\c", "a\\c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with comma", "a,c",
@@ -125,6 +154,8 @@
                 "a,c", "a,c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label starts with at", "@bc",
                 "@bc", "@bc", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
+        assertParser("Surrogate label starts with at", "@" + SURROGATE1,
+                "@" + SURROGATE1, "@" + SURROGATE1, ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label contains at", "a@c",
                 "a@c", "a@c", ICON_UNDEFINED, Keyboard.CODE_OUTPUT_TEXT);
         assertParser("Label with escaped at", "\\@bc",
@@ -220,9 +251,9 @@
                 null, null, ICON_SETTINGS_KEY, mCodeSettings);
     }
 
-    private void assertMoreKeys(String message, String[] moreKeys, String[] additionalMoreKeys,
-            String[] expected) {
-        final String[] actual = MoreKeySpecParser.insertAddtionalMoreKeys(
+    private static void assertMoreKeys(String message, String[] moreKeys,
+            String[] additionalMoreKeys, String[] expected) {
+        final String[] actual = KeySpecParser.insertAddtionalMoreKeys(
                 moreKeys, additionalMoreKeys);
         if (expected == null && actual == null) {
             return;