Loading java/src/com/android/inputmethod/keyboard/Key.java +6 −6 Original line number Diff line number Diff line Loading @@ -284,19 +284,19 @@ public class Key implements Comparable<Key> { 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; Loading @@ -308,7 +308,7 @@ public class Key implements Comparable<Key> { 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]; Loading java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +11 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -29,15 +30,16 @@ import android.text.TextUtils; * 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() { Loading @@ -45,7 +47,7 @@ public final class CodesArrayParser { } 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; } Loading @@ -55,7 +57,7 @@ public final class CodesArrayParser { 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); } Loading @@ -63,17 +65,15 @@ public final class CodesArrayParser { } 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; } Loading @@ -98,7 +98,7 @@ public final class CodesArrayParser { 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); } Loading java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +64 −256 Original line number Diff line number Diff line Loading @@ -19,106 +19,46 @@ package com.android.inputmethod.keyboard.internal; 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 }; private static boolean hasIcon(final String keySpec) { return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); } ArrayList<String> list = null; int start = 0; // The characters in question in this loop are COMMA and BACKSLASH. These characters never // match any high or low surrogate character. So it is OK to iterate through with char // index. for (int pos = 0; pos < size; pos++) { final char c = text.charAt(pos); if (c == COMMA) { // Skip empty entry. if (pos - start > 0) { if (list == null) { list = CollectionUtils.newArrayList(); } list.add(text.substring(start, pos)); } // Skip comma start = pos + 1; } else if (c == BACKSLASH) { // Skip escape character and escaped character. pos++; } } final String remain = (size - start > 0) ? text.substring(start) : null; if (list == null) { return remain != null ? new String[] { remain } : null; } if (remain != null) { list.add(remain); } return list.toArray(new String[list.size()]); } private static boolean hasIcon(final String moreKeySpec) { return moreKeySpec.startsWith(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; } Loading @@ -144,17 +84,17 @@ public final class KeySpecParser { 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++; Loading @@ -165,63 +105,63 @@ public final class KeySpecParser { 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)}. Loading @@ -230,7 +170,7 @@ public final class KeySpecParser { } 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); Loading @@ -250,154 +190,22 @@ public final class KeySpecParser { } } 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; } } java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +1 −1 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ public abstract class KeyStyle { 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; } Loading java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +2 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.res.Resources; 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; Loading @@ -47,7 +48,7 @@ import java.util.HashMap; */ 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. Loading Loading
java/src/com/android/inputmethod/keyboard/Key.java +6 −6 Original line number Diff line number Diff line Loading @@ -284,19 +284,19 @@ public class Key implements Comparable<Key> { 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; Loading @@ -308,7 +308,7 @@ public class Key implements Comparable<Key> { 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]; Loading
java/src/com/android/inputmethod/keyboard/internal/CodesArrayParser.java +11 −11 Original line number Diff line number Diff line Loading @@ -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; Loading @@ -29,15 +30,16 @@ import android.text.TextUtils; * 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() { Loading @@ -45,7 +47,7 @@ public final class CodesArrayParser { } 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; } Loading @@ -55,7 +57,7 @@ public final class CodesArrayParser { 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); } Loading @@ -63,17 +65,15 @@ public final class CodesArrayParser { } 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; } Loading @@ -98,7 +98,7 @@ public final class CodesArrayParser { 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); } Loading
java/src/com/android/inputmethod/keyboard/internal/KeySpecParser.java +64 −256 Original line number Diff line number Diff line Loading @@ -19,106 +19,46 @@ package com.android.inputmethod.keyboard.internal; 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 }; private static boolean hasIcon(final String keySpec) { return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON); } ArrayList<String> list = null; int start = 0; // The characters in question in this loop are COMMA and BACKSLASH. These characters never // match any high or low surrogate character. So it is OK to iterate through with char // index. for (int pos = 0; pos < size; pos++) { final char c = text.charAt(pos); if (c == COMMA) { // Skip empty entry. if (pos - start > 0) { if (list == null) { list = CollectionUtils.newArrayList(); } list.add(text.substring(start, pos)); } // Skip comma start = pos + 1; } else if (c == BACKSLASH) { // Skip escape character and escaped character. pos++; } } final String remain = (size - start > 0) ? text.substring(start) : null; if (list == null) { return remain != null ? new String[] { remain } : null; } if (remain != null) { list.add(remain); } return list.toArray(new String[list.size()]); } private static boolean hasIcon(final String moreKeySpec) { return moreKeySpec.startsWith(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; } Loading @@ -144,17 +84,17 @@ public final class KeySpecParser { 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++; Loading @@ -165,63 +105,63 @@ public final class KeySpecParser { 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)}. Loading @@ -230,7 +170,7 @@ public final class KeySpecParser { } 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); Loading @@ -250,154 +190,22 @@ public final class KeySpecParser { } } 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; } }
java/src/com/android/inputmethod/keyboard/internal/KeyStyle.java +1 −1 Original line number Diff line number Diff line Loading @@ -40,7 +40,7 @@ public abstract class KeyStyle { 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; } Loading
java/src/com/android/inputmethod/keyboard/internal/KeyboardTextsSet.java +2 −1 Original line number Diff line number Diff line Loading @@ -21,6 +21,7 @@ import android.content.res.Resources; 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; Loading @@ -47,7 +48,7 @@ import java.util.HashMap; */ 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. Loading