Move some methods to MoreKeySpec class

Change-Id: I025ed15e36ac3113db6a5fccefa0c87f016703fa
diff --git a/java/src/com/android/inputmethod/keyboard/Key.java b/java/src/com/android/inputmethod/keyboard/Key.java
index 5fd70ea..f7eec6b 100644
--- a/java/src/com/android/inputmethod/keyboard/Key.java
+++ b/java/src/com/android/inputmethod/keyboard/Key.java
@@ -284,19 +284,19 @@
         int moreKeysColumn = style.getInt(keyAttr,
                 R.styleable.Keyboard_Key_maxMoreKeysColumn, params.mMaxMoreKeysKeyboardColumn);
         int value;
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_AUTO_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = value & MORE_KEYS_COLUMN_MASK;
         }
-        if ((value = KeySpecParser.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
+        if ((value = MoreKeySpec.getIntValue(moreKeys, MORE_KEYS_FIXED_COLUMN_ORDER, -1)) > 0) {
             moreKeysColumn = MORE_KEYS_FLAGS_FIXED_COLUMN_ORDER | (value & MORE_KEYS_COLUMN_MASK);
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_HAS_LABELS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_HAS_LABELS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NEEDS_DIVIDERS)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NEEDS_DIVIDERS;
         }
-        if (KeySpecParser.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
+        if (MoreKeySpec.getBooleanValue(moreKeys, MORE_KEYS_NO_PANEL_AUTO_MORE_KEY)) {
             moreKeysColumn |= MORE_KEYS_FLAGS_NO_PANEL_AUTO_MORE_KEY;
         }
         mMoreKeysColumnAndFlags = moreKeysColumn;
@@ -308,7 +308,7 @@
             additionalMoreKeys = style.getStringArray(keyAttr,
                     R.styleable.Keyboard_Key_additionalMoreKeys);
         }
-        moreKeys = KeySpecParser.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
+        moreKeys = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         if (moreKeys != null) {
             actionFlags |= ACTION_FLAGS_ENABLE_LONG_PRESS;
             mMoreKeys = new MoreKeySpec[moreKeys.length];
diff --git a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
index 4ccecb2..dce7fc5 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.keyboard.internal;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.utils.StringUtils;
 
 import android.text.TextUtils;
 
@@ -29,15 +30,16 @@
  * marker. An output text may consist of multiple code points separated by comma.
  * The format of the codesArray element should be:
  * <pre>
- *   codePointInHex[,codePoint2InHex]*(|outputTextCodePointInHex[,outputTextCodePoint2InHex]*)?
+ *   label1[,label2]*(|outputText1[,outputText2]*(|minSupportSdkVersion)?)?
  * </pre>
  */
 // TODO: Write unit tests for this class.
 public final class CodesArrayParser {
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final String VERTICAL_BAR_STRING = "\\|";
-    private static final String COMMA_STRING = ",";
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final String COMMA_REGEX = StringUtils.newSingleCodePointString(COMMA);
+    private static final String VERTICAL_BAR_REGEX = // "\\|"
+            new String(new char[] { Constants.CODE_BACKSLASH, Constants.CODE_VERTICAL_BAR });
     private static final int BASE_HEX = 16;
 
     private CodesArrayParser() {
@@ -45,7 +47,7 @@
     }
 
     private static String getLabelSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
@@ -55,7 +57,7 @@
     public static String parseLabel(final String codesArraySpec) {
         final String labelSpec = getLabelSpec(codesArraySpec);
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : labelSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : labelSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
@@ -63,17 +65,15 @@
     }
 
     private static String getCodeSpec(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 1) {
             return codesArraySpec;
         }
         return TextUtils.isEmpty(strs[1]) ? strs[0] : strs[1];
     }
 
-    // codesArraySpec consists of:
-    // <label>|<code0>,<code1>,...|<minSupportSdkVersion>
     public static int getMinSupportSdkVersion(final String codesArraySpec) {
-        final String[] strs = codesArraySpec.split(VERTICAL_BAR_STRING, -1);
+        final String[] strs = codesArraySpec.split(VERTICAL_BAR_REGEX, -1);
         if (strs.length <= 2) {
             return 0;
         }
@@ -98,7 +98,7 @@
             return null;
         }
         final StringBuilder sb = new StringBuilder();
-        for (final String codeInHex : codeSpec.split(COMMA_STRING)) {
+        for (final String codeInHex : codeSpec.split(COMMA_REGEX)) {
             final int codePoint = Integer.parseInt(codeInHex, BASE_HEX);
             sb.appendCodePoint(codePoint);
         }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
index 07c5cae..c51941d 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java
@@ -19,106 +19,46 @@
 import static com.android.inputmethod.latin.Constants.CODE_OUTPUT_TEXT;
 import static com.android.inputmethod.latin.Constants.CODE_UNSPECIFIED;
 
-import android.text.TextUtils;
-
-import com.android.inputmethod.latin.LatinImeLogger;
-import com.android.inputmethod.latin.utils.CollectionUtils;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.StringUtils;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-
 /**
- * The string parser of more keys specification.
- * The specification is comma separated texts each of which represents one "more key".
- * The specification might have label or string resource reference in it. These references are
- * expanded before parsing comma.
- * - Label reference should be a string representation of label (!text/label_name)
- * - String resource reference should be a string representation of resource (!text/resource_name)
- * Each "more key" specification is one of the following:
- * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText).
- * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name)
- *   - Icon should be a string representation of icon (!icon/icon_name).
- *   - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string
- *     representation of code (!code/code_name).
+ * The string parser of the key specification.
+ *
+ * Each key specification is one of the following:
+ * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
+ * - Label optionally followed by code point (keyLabel|!code/code_name).
+ * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
+ * - Icon followed by code point (!icon/icon_name|!code/code_name).
+ * Label and keyOutputText are one of the following:
+ * - Literal string.
+ * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
+ * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
+ * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
+ * Code is one of the following:
+ * - Code point presented by hexadecimal string prefixed with "0x"
+ * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
  * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
- * Note that the '\' is also parsed by XML parser and CSV parser as well.
- * See {@link KeyboardIconsSet} about icon_name.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
  */
 public final class KeySpecParser {
-    private static final boolean DEBUG = LatinImeLogger.sDBG;
-
     // Constants for parsing.
-    private static final char COMMA = ',';
-    private static final char BACKSLASH = '\\';
-    private static final char VERTICAL_BAR = '|';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
     private static final String PREFIX_HEX = "0x";
-    private static final String ADDITIONAL_MORE_KEY_MARKER = "%";
 
     private KeySpecParser() {
         // 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) {
-        if (TextUtils.isEmpty(text)) {
-            return null;
-        }
-        final int size = text.length();
-        // 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 keySpec) {
+        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
     }
 
-    private static boolean hasIcon(final String moreKeySpec) {
-        return moreKeySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
-    }
-
-    private static boolean hasCode(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
-        if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith(
+    private static boolean hasCode(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
+        if (end > 0 && end + 1 < keySpec.length() && keySpec.startsWith(
                 KeyboardCodesSet.PREFIX_CODE, end + 1)) {
             return true;
         }
@@ -144,17 +84,17 @@
         return sb.toString();
     }
 
-    private static int indexOfLabelEnd(final String moreKeySpec, final int start) {
-        if (moreKeySpec.indexOf(BACKSLASH, start) < 0) {
-            final int end = moreKeySpec.indexOf(VERTICAL_BAR, start);
+    private static int indexOfLabelEnd(final String keySpec, final int start) {
+        if (keySpec.indexOf(BACKSLASH, start) < 0) {
+            final int end = keySpec.indexOf(VERTICAL_BAR, start);
             if (end == 0) {
-                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + moreKeySpec);
+                throw new KeySpecParserError(VERTICAL_BAR + " at " + start + ": " + keySpec);
             }
             return end;
         }
-        final int length = moreKeySpec.length();
+        final int length = keySpec.length();
         for (int pos = start; pos < length; pos++) {
-            final char c = moreKeySpec.charAt(pos);
+            final char c = keySpec.charAt(pos);
             if (c == BACKSLASH && pos + 1 < length) {
                 // Skip escape char
                 pos++;
@@ -165,63 +105,63 @@
         return -1;
     }
 
-    public static String getLabel(final String moreKeySpec) {
-        if (hasIcon(moreKeySpec)) {
+    public static String getLabel(final String keySpec) {
+        if (hasIcon(keySpec)) {
             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);
+        final int end = indexOfLabelEnd(keySpec, 0);
+        final String label = (end > 0) ? parseEscape(keySpec.substring(0, end))
+                : parseEscape(keySpec);
+        if (label.isEmpty()) {
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         return label;
     }
 
-    private static String getOutputTextInternal(final String moreKeySpec) {
-        final int end = indexOfLabelEnd(moreKeySpec, 0);
+    private static String getOutputTextInternal(final String keySpec) {
+        final int end = indexOfLabelEnd(keySpec, 0);
         if (end <= 0) {
             return null;
         }
-        if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+        if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+            throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
         }
-        return parseEscape(moreKeySpec.substring(end + /* VERTICAL_BAR */1));
+        return parseEscape(keySpec.substring(end + /* VERTICAL_BAR */1));
     }
 
-    static String getOutputText(final String moreKeySpec) {
-        if (hasCode(moreKeySpec)) {
+    public static String getOutputText(final String keySpec) {
+        if (hasCode(keySpec)) {
             return null;
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             if (StringUtils.codePointCount(outputText) == 1) {
                 // If output text is one code point, it should be treated as a code.
                 // See {@link #getCode(Resources, String)}.
                 return null;
             }
-            if (!TextUtils.isEmpty(outputText)) {
-                return outputText;
+            if (outputText.isEmpty()) {
+                throw new KeySpecParserError("Empty outputText: " + keySpec);
             }
-            throw new KeySpecParserError("Empty outputText: " + moreKeySpec);
+            return outputText;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         if (label == null) {
-            throw new KeySpecParserError("Empty label: " + moreKeySpec);
+            throw new KeySpecParserError("Empty label: " + keySpec);
         }
         // Code is automatically generated for one letter label. See {@link getCode()}.
         return (StringUtils.codePointCount(label) == 1) ? null : label;
     }
 
-    static int getCode(final String moreKeySpec, final KeyboardCodesSet codesSet) {
-        if (hasCode(moreKeySpec)) {
-            final int end = indexOfLabelEnd(moreKeySpec, 0);
-            if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) {
-                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + moreKeySpec);
+    public static int getCode(final String keySpec, final KeyboardCodesSet codesSet) {
+        if (hasCode(keySpec)) {
+            final int end = indexOfLabelEnd(keySpec, 0);
+            if (indexOfLabelEnd(keySpec, end + 1) >= 0) {
+                throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
             }
-            return parseCode(moreKeySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
+            return parseCode(keySpec.substring(end + 1), codesSet, CODE_UNSPECIFIED);
         }
-        final String outputText = getOutputTextInternal(moreKeySpec);
+        final String outputText = getOutputTextInternal(keySpec);
         if (outputText != null) {
             // If output text is one code point, it should be treated as a code.
             // See {@link #getOutputText(String)}.
@@ -230,7 +170,7 @@
             }
             return CODE_OUTPUT_TEXT;
         }
-        final String label = getLabel(moreKeySpec);
+        final String label = getLabel(keySpec);
         // Code is automatically generated for one letter label.
         if (StringUtils.codePointCount(label) == 1) {
             return label.codePointAt(0);
@@ -250,154 +190,22 @@
         }
     }
 
-    public static int getIconId(final String moreKeySpec) {
-        if (moreKeySpec != null && hasIcon(moreKeySpec)) {
-            final int end = moreKeySpec.indexOf(
+    public static int getIconId(final String keySpec) {
+        if (keySpec != null && hasIcon(keySpec)) {
+            final int end = keySpec.indexOf(
                     VERTICAL_BAR, KeyboardIconsSet.PREFIX_ICON.length());
             final String name = (end < 0)
-                    ? moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
-                    : moreKeySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
+                    ? keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length())
+                    : keySpec.substring(KeyboardIconsSet.PREFIX_ICON.length(), end);
             return KeyboardIconsSet.getIconId(name);
         }
         return KeyboardIconsSet.ICON_UNDEFINED;
     }
 
-    private static final String[] EMPTY_STRING_ARRAY = new String[0];
-
-    private static String[] filterOutEmptyString(final String[] array) {
-        if (array == null) {
-            return EMPTY_STRING_ARRAY;
-        }
-        ArrayList<String> out = null;
-        for (int i = 0; i < array.length; i++) {
-            final String entry = array[i];
-            if (TextUtils.isEmpty(entry)) {
-                if (out == null) {
-                    out = CollectionUtils.arrayAsList(array, 0, i);
-                }
-            } else if (out != null) {
-                out.add(entry);
-            }
-        }
-        if (out == null) {
-            return array;
-        }
-        return out.toArray(new String[out.size()]);
-    }
-
-    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
-            final String[] additionalMoreKeySpecs) {
-        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
-        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
-        final int moreKeysCount = moreKeys.length;
-        final int additionalCount = additionalMoreKeys.length;
-        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 = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
-                    }
-                }
-            } 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 = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
-            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 = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
-            for (int i = additionalIndex; i < additionalCount; i++) {
-                out.add(additionalMoreKeys[additionalIndex]);
-            }
-        }
-        if (out == null && moreKeysCount > 0) {
-            return moreKeys;
-        } else if (out != null && out.size() > 0) {
-            return out.toArray(new String[out.size()]);
-        } else {
-            return null;
-        }
-    }
-
     @SuppressWarnings("serial")
     public static final class KeySpecParserError extends RuntimeException {
         public KeySpecParserError(final String message) {
             super(message);
         }
     }
-
-    public static int getIntValue(final String[] moreKeys, final String key,
-            final int defaultValue) {
-        if (moreKeys == null) {
-            return defaultValue;
-        }
-        final int keyLen = key.length();
-        boolean foundValue = false;
-        int value = defaultValue;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            try {
-                if (!foundValue) {
-                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
-                    foundValue = true;
-                }
-            } catch (NumberFormatException e) {
-                throw new RuntimeException(
-                        "integer should follow after " + key + ": " + moreKeySpec);
-            }
-        }
-        return value;
-    }
-
-    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
-        if (moreKeys == null) {
-            return false;
-        }
-        boolean value = false;
-        for (int i = 0; i < moreKeys.length; i++) {
-            final String moreKeySpec = moreKeys[i];
-            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
-                continue;
-            }
-            moreKeys[i] = null;
-            value = true;
-        }
-        return value;
-    }
 }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
index a2242b8..7941ddd 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java
@@ -40,7 +40,7 @@
     protected String[] parseStringArray(final TypedArray a, final int index) {
         if (a.hasValue(index)) {
             final String text = mTextsSet.resolveTextReference(a.getString(index));
-            return KeySpecParser.splitKeySpecs(text);
+            return MoreKeySpec.splitKeySpecs(text);
         }
         return null;
     }
diff --git a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
index 96a5b08..f8c0988 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -47,7 +48,7 @@
  */
 public final class KeyboardTextsSet {
     public static final String PREFIX_TEXT = "!text/";
-    private static final char BACKSLASH = '\\';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
 
     // Language to texts map.
diff --git a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
index dd58e1f..d3bc0c2 100644
--- a/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
+++ b/java/src/com/android/inputmethod/keyboard/internal/MoreKeySpec.java
@@ -19,10 +19,25 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.latin.Constants;
+import com.android.inputmethod.latin.LatinImeLogger;
+import com.android.inputmethod.latin.utils.CollectionUtils;
 import com.android.inputmethod.latin.utils.StringUtils;
 
+import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Locale;
 
+/**
+ * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
+ *
+ * The more keys specification is comma separated "key specification" each of which represents one
+ * "more key".
+ * The key specification might have label or string resource reference in it. These references are
+ * expanded before parsing comma.
+ * Special character, comma ',' backslash '\' can be escaped by '\' character.
+ * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
+ * as well.
+ */
 public final class MoreKeySpec {
     public final int mCode;
     public final String mLabel;
@@ -83,4 +98,196 @@
             return label + "|" + output;
         }
     }
+
+    private static final boolean DEBUG = LatinImeLogger.sDBG;
+    // Constants for parsing.
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
+    private static final String ADDITIONAL_MORE_KEY_MARKER =
+            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
+
+    /**
+     * 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) {
+        if (TextUtils.isEmpty(text)) {
+            return null;
+        }
+        final int size = text.length();
+        // 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 final String[] EMPTY_STRING_ARRAY = new String[0];
+
+    private static String[] filterOutEmptyString(final String[] array) {
+        if (array == null) {
+            return EMPTY_STRING_ARRAY;
+        }
+        ArrayList<String> out = null;
+        for (int i = 0; i < array.length; i++) {
+            final String entry = array[i];
+            if (TextUtils.isEmpty(entry)) {
+                if (out == null) {
+                    out = CollectionUtils.arrayAsList(array, 0, i);
+                }
+            } else if (out != null) {
+                out.add(entry);
+            }
+        }
+        if (out == null) {
+            return array;
+        }
+        return out.toArray(new String[out.size()]);
+    }
+
+    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
+            final String[] additionalMoreKeySpecs) {
+        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
+        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
+        final int moreKeysCount = moreKeys.length;
+        final int additionalCount = additionalMoreKeys.length;
+        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 = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
+                    }
+                }
+            } 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 = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
+            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 = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
+            for (int i = additionalIndex; i < additionalCount; i++) {
+                out.add(additionalMoreKeys[additionalIndex]);
+            }
+        }
+        if (out == null && moreKeysCount > 0) {
+            return moreKeys;
+        } else if (out != null && out.size() > 0) {
+            return out.toArray(new String[out.size()]);
+        } else {
+            return null;
+        }
+    }
+
+    public static int getIntValue(final String[] moreKeys, final String key,
+            final int defaultValue) {
+        if (moreKeys == null) {
+            return defaultValue;
+        }
+        final int keyLen = key.length();
+        boolean foundValue = false;
+        int value = defaultValue;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            try {
+                if (!foundValue) {
+                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
+                    foundValue = true;
+                }
+            } catch (NumberFormatException e) {
+                throw new RuntimeException(
+                        "integer should follow after " + key + ": " + moreKeySpec);
+            }
+        }
+        return value;
+    }
+
+    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
+        if (moreKeys == null) {
+            return false;
+        }
+        boolean value = false;
+        for (int i = 0; i < moreKeys.length; i++) {
+            final String moreKeySpec = moreKeys[i];
+            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
+                continue;
+            }
+            moreKeys[i] = null;
+            value = true;
+        }
+        return value;
+    }
 }
diff --git a/java/src/com/android/inputmethod/latin/Constants.java b/java/src/com/android/inputmethod/latin/Constants.java
index 0b396b1..d6ac71f 100644
--- a/java/src/com/android/inputmethod/latin/Constants.java
+++ b/java/src/com/android/inputmethod/latin/Constants.java
@@ -191,6 +191,8 @@
     public static final int CODE_QUESTION_MARK = '?';
     public static final int CODE_EXCLAMATION_MARK = '!';
     public static final int CODE_SLASH = '/';
+    public static final int CODE_BACKSLASH = '\\';
+    public static final int CODE_VERTICAL_BAR = '|';
     public static final int CODE_COMMERCIAL_AT = '@';
     public static final int CODE_PLUS = '+';
     public static final int CODE_PERCENT = '%';
diff --git a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
index 8ba32ff..70cb2b2 100644
--- a/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
+++ b/java/src/com/android/inputmethod/latin/settings/SpacingAndPunctuations.java
@@ -19,6 +19,7 @@
 import android.content.res.Resources;
 
 import com.android.inputmethod.keyboard.internal.KeySpecParser;
+import com.android.inputmethod.keyboard.internal.MoreKeySpec;
 import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.Dictionary;
 import com.android.inputmethod.latin.R;
@@ -55,7 +56,7 @@
                 res.getString(R.string.symbols_word_connectors));
         mSortedWordSeparators = StringUtils.toSortedCodePointArray(
                 res.getString(R.string.symbols_word_separators));
-        final String[] suggestPuncsSpec = KeySpecParser.splitKeySpecs(res.getString(
+        final String[] suggestPuncsSpec = MoreKeySpec.splitKeySpecs(res.getString(
                 R.string.suggested_punctuations));
         mSuggestPuncList = createSuggestPuncList(suggestPuncsSpec);
         mSentenceSeparator = res.getInteger(R.integer.sentence_separator);
diff --git a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
index 36b927e..b18a1d8 100644
--- a/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
+++ b/java/src/com/android/inputmethod/latin/utils/CsvUtils.java
@@ -17,6 +17,7 @@
 package com.android.inputmethod.latin.utils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 
 import java.util.ArrayList;
 
@@ -57,9 +58,9 @@
 
     // Note that none of these characters match high or low surrogate characters, so we need not
     // take care of matching by code point.
-    private static final char COMMA = ',';
-    private static final char SPACE = ' ';
-    private static final char QUOTE = '"';
+    private static final char COMMA = Constants.CODE_COMMA;
+    private static final char SPACE = Constants.CODE_SPACE;
+    private static final char QUOTE = Constants.CODE_DOUBLE_QUOTE;
 
     @SuppressWarnings("serial")
     public static class CsvParseException extends RuntimeException {
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
similarity index 99%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
index 7b7f175..5f301a8 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserSplitTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecSplitTests.java
@@ -31,7 +31,7 @@
 import java.util.Locale;
 
 @MediumTest
-public class KeySpecParserSplitTests extends InstrumentationTestCase {
+public class MoreKeySpecSplitTests extends InstrumentationTestCase {
     private static final Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
 
@@ -93,7 +93,7 @@
     private void assertTextArray(final String message, final String value,
             final String ... expectedArray) {
         final String resolvedActual = mTextsSet.resolveTextReference(value);
-        final String[] actual = KeySpecParser.splitKeySpecs(resolvedActual);
+        final String[] actual = MoreKeySpec.splitKeySpecs(resolvedActual);
         final String[] expected = (expectedArray.length == 0) ? null : expectedArray;
         assertArrayEquals(message, expected, actual);
     }
diff --git a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
similarity index 95%
rename from tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
rename to tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
index 0388435..538ba2c 100644
--- a/tests/src/com/android/inputmethod/keyboard/internal/KeySpecParserTests.java
+++ b/tests/src/com/android/inputmethod/keyboard/internal/MoreKeySpecTests.java
@@ -32,7 +32,7 @@
 import java.util.Locale;
 
 @SmallTest
-public class KeySpecParserTests extends AndroidTestCase {
+public class MoreKeySpecTests extends AndroidTestCase {
     private final static Locale TEST_LOCALE = Locale.ENGLISH;
     final KeyboardCodesSet mCodesSet = new KeyboardCodesSet();
     final KeyboardTextsSet mTextsSet = new KeyboardTextsSet();
@@ -71,8 +71,9 @@
         mSettingsIconId = KeySpecParser.getIconId(ICON_SETTINGS);
     }
 
-    private void assertParser(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
+    private void assertParser(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
         final String labelResolved = mTextsSet.resolveTextReference(moreKeySpec);
         final MoreKeySpec spec = new MoreKeySpec(labelResolved, false /* needsToUpperCase */,
                 Locale.US, mCodesSet);
@@ -86,8 +87,9 @@
                 Constants.printableCode(spec.mCode));
     }
 
-    private void assertParserError(String message, String moreKeySpec, String expectedLabel,
-            String expectedOutputText, int expectedIcon, int expectedCode) {
+    private void assertParserError(final String message, final String moreKeySpec,
+            final String expectedLabel, final String expectedOutputText, final int expectedIcon,
+            final int expectedCode) {
         try {
             assertParser(message, moreKeySpec, expectedLabel, expectedOutputText, expectedIcon,
                     expectedCode);
@@ -339,7 +341,8 @@
                 null, null, mSettingsIconId, mCodeSettings);
     }
 
-    private static void assertArrayEquals(String message, Object[] expected, Object[] actual) {
+    private static void assertArrayEquals(final String message, final Object[] expected,
+            final Object[] actual) {
         if (expected == actual) {
             return;
         }
@@ -357,10 +360,9 @@
         }
     }
 
-    private static void assertInsertAdditionalMoreKeys(String message, String[] moreKeys,
-            String[] additionalMoreKeys, String[] expected) {
-        final String[] actual =
-                KeySpecParser.insertAdditionalMoreKeys( moreKeys, additionalMoreKeys);
+    private static void assertInsertAdditionalMoreKeys(final String message,
+            final String[] moreKeys, final String[] additionalMoreKeys, final String[] expected) {
+        final String[] actual = MoreKeySpec.insertAdditionalMoreKeys(moreKeys, additionalMoreKeys);
         assertArrayEquals(message, expected, actual);
     }
 
@@ -584,10 +586,10 @@
     private static final String AUTO_COLUMN_ORDER = "!autoColumnOrder!";
     private static final String FIXED_COLUMN_ORDER = "!fixedColumnOrder!";
 
-    private static void assertGetBooleanValue(String message, String key, String[] moreKeys,
-            String[] expected, boolean expectedValue) {
+    private static void assertGetBooleanValue(final String message, final String key,
+            final String[] moreKeys, final String[] expected, final boolean expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final boolean actualValue = KeySpecParser.getBooleanValue(actual, key);
+        final boolean actualValue = MoreKeySpec.getBooleanValue(actual, key);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
@@ -622,10 +624,11 @@
                     "a", null, "b", NEEDS_DIVIDER, "!HASLABEL!", "d" }, true);
     }
 
-    private static void assertGetIntValue(String message, String key, int defaultValue,
-            String[] moreKeys, String[] expected, int expectedValue) {
+    private static void assertGetIntValue(final String message, final String key,
+            final int defaultValue, final String[] moreKeys, final String[] expected,
+            final int expectedValue) {
         final String[] actual = Arrays.copyOf(moreKeys, moreKeys.length);
-        final int actualValue = KeySpecParser.getIntValue(actual, key, defaultValue);
+        final int actualValue = MoreKeySpec.getIntValue(actual, key, defaultValue);
         assertEquals(message + " [value]", expectedValue, actualValue);
         assertArrayEquals(message, expected, actual);
     }
diff --git a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
index 9252992..db1dde3 100644
--- a/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
+++ b/tools/make-keyboard-text/res/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.tmpl
@@ -21,6 +21,7 @@
 import android.text.TextUtils;
 
 import com.android.inputmethod.annotations.UsedForTesting;
+import com.android.inputmethod.latin.Constants;
 import com.android.inputmethod.latin.utils.CollectionUtils;
 
 import java.util.HashMap;
@@ -47,7 +48,7 @@
  */
 public final class KeyboardTextsSet {
     public static final String PREFIX_TEXT = "!text/";
-    private static final char BACKSLASH = '\\';
+    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
     private static final int MAX_STRING_REFERENCE_INDIRECTION = 10;
 
     // Language to texts map.