19b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka/*
28632bff2d5a8e1160989008dea6eff4b94b065ddTadashi G. Takaoka * Copyright (C) 2010 The Android Open Source Project
39b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka *
48aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * Licensed under the Apache License, Version 2.0 (the "License");
58aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * you may not use this file except in compliance with the License.
68aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * You may obtain a copy of the License at
79b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka *
88aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka *      http://www.apache.org/licenses/LICENSE-2.0
99b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka *
109b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka * Unless required by applicable law or agreed to in writing, software
118aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * distributed under the License is distributed on an "AS IS" BASIS,
128aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
138aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * See the License for the specific language governing permissions and
148aa9963a895f9dd5bb1bc92ab2e4f461e058f87aTadashi G. Takaoka * limitations under the License.
159b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka */
169b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
1772934bd5967d0127f71fd4d66158b18b4e6ceefeTadashi G. Takaokapackage com.android.inputmethod.keyboard.internal;
189b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
199342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport static com.android.inputmethod.latin.common.Constants.CODE_OUTPUT_TEXT;
209342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport static com.android.inputmethod.latin.common.Constants.CODE_UNSPECIFIED;
217ab7f66c2d0f4a0b2e29be718b310ccaf368a4f4Tadashi G. Takaoka
229342484e8d573a40f470b6a593df31c602fa4076Ken Wakasaimport com.android.inputmethod.latin.common.Constants;
234beeb9253a06482299e0c67467531d30436a02fcJean Chalardimport com.android.inputmethod.latin.common.StringUtils;
249b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
259d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaokaimport javax.annotation.Nonnull;
269d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaokaimport javax.annotation.Nullable;
279d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka
289b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka/**
29e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * The string parser of the key specification.
30e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka *
31e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Each key specification is one of the following:
32e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label optionally followed by keyOutputText (keyLabel|keyOutputText).
33e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label optionally followed by code point (keyLabel|!code/code_name).
34e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Icon followed by keyOutputText (!icon/icon_name|keyOutputText).
35e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Icon followed by code point (!icon/icon_name|!code/code_name).
36e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Label and keyOutputText are one of the following:
37e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Literal string.
38e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Label reference represented by (!text/label_name), see {@link KeyboardTextsSet}.
39e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - String resource reference represented by (!text/resource_name), see {@link KeyboardTextsSet}.
40e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Icon is represented by (!icon/icon_name), see {@link KeyboardIconsSet}.
41e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Code is one of the following:
42e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Code point presented by hexadecimal string prefixed with "0x"
43e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * - Code reference represented by (!code/code_name), see {@link KeyboardCodesSet}.
440086861d5a2d16f86e2130ee9a7ec46ca533fadbTadashi G. Takaoka * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character.
45e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
46e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka * as well.
479b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka */
489684b33b69a62a058c767786ae6a23b809d27385Tadashi G. Takaoka// TODO: Rename to KeySpec and make this class to the key specification object.
49a28a05e971cc242b338331a3b78276fa95188d19Tadashi G. Takaokapublic final class KeySpecParser {
500086861d5a2d16f86e2130ee9a7ec46ca533fadbTadashi G. Takaoka    // Constants for parsing.
51e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
52e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka    private static final char VERTICAL_BAR = Constants.CODE_VERTICAL_BAR;
53ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka    private static final String PREFIX_HEX = "0x";
549b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
55e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka    private KeySpecParser() {
569b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        // Intentional empty constructor for utility class.
579b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
589b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
599d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static boolean hasIcon(@Nonnull final String keySpec) {
60e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka        return keySpec.startsWith(KeyboardIconsSet.PREFIX_ICON);
619b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
629b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
639d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static boolean hasCode(@Nonnull final String keySpec, final int labelEnd) {
644a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        if (labelEnd <= 0 || labelEnd + 1 >= keySpec.length()) {
654a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka            return false;
664a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        }
674a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        if (keySpec.startsWith(KeyboardCodesSet.PREFIX_CODE, labelEnd + 1)) {
684a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka            return true;
694a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        }
704a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        // This is a workaround to have a key that has a supplementary code point. We can't put a
714a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        // string in resource as a XML entity of a supplementary code point or a surrogate pair.
724a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        if (keySpec.startsWith(PREFIX_HEX, labelEnd + 1)) {
739b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            return true;
749b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
759b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        return false;
769b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
779b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
789d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nonnull
799d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static String parseEscape(@Nonnull final String text) {
80dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka        if (text.indexOf(BACKSLASH) < 0) {
819b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            return text;
8223bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
839b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        final int length = text.length();
849b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        final StringBuilder sb = new StringBuilder();
859b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        for (int pos = 0; pos < length; pos++) {
869b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            final char c = text.charAt(pos);
87dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka            if (c == BACKSLASH && pos + 1 < length) {
88e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka                // Skip escape char
89e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka                pos++;
90e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka                sb.append(text.charAt(pos));
919b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            } else {
929b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka                sb.append(c);
939b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            }
949b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
959b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        return sb.toString();
969b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
979b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
989d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static int indexOfLabelEnd(@Nonnull final String keySpec) {
99f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka        final int length = keySpec.length();
100bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (keySpec.indexOf(BACKSLASH) < 0) {
101bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            final int labelEnd = keySpec.indexOf(VERTICAL_BAR);
102bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            if (labelEnd == 0) {
103f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka                if (length == 1) {
104f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka                    // Treat a sole vertical bar as a special case of key label.
105f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka                    return -1;
106f7d8b8fcbcfe137c1c25ca7ea5bd83f856f06b9eTadashi G. Takaoka                }
107bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka                throw new KeySpecParserError("Empty label");
10823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka            }
109bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            return labelEnd;
1109b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
111bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        for (int pos = 0; pos < length; pos++) {
112e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka            final char c = keySpec.charAt(pos);
113dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka            if (c == BACKSLASH && pos + 1 < length) {
114e01d272603f3643ce613e61dd3204379f4f4fb73Tadashi G. Takaoka                // Skip escape char
1159b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka                pos++;
116dbed20ad8d48e9b42b52c522c54bfd9e62553f7aTadashi G. Takaoka            } else if (c == VERTICAL_BAR) {
1179b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka                return pos;
1189b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            }
1199b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
1209b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        return -1;
1219b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
1229b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
1239d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nonnull
1249d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static String getBeforeLabelEnd(@Nonnull final String keySpec, final int labelEnd) {
125bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        return (labelEnd < 0) ? keySpec : keySpec.substring(0, labelEnd);
126bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka    }
127bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka
1289d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nonnull
1299d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static String getAfterLabelEnd(@Nonnull final String keySpec, final int labelEnd) {
130bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        return keySpec.substring(labelEnd + /* VERTICAL_BAR */1);
131bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka    }
132bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka
1339d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static void checkDoubleLabelEnd(@Nonnull final String keySpec, final int labelEnd) {
134bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (indexOfLabelEnd(getAfterLabelEnd(keySpec, labelEnd)) < 0) {
135bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            return;
136bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        }
137bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        throw new KeySpecParserError("Multiple " + VERTICAL_BAR + ": " + keySpec);
138bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka    }
139bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka
1409d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nullable
1419d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    public static String getLabel(@Nullable final String keySpec) {
142d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        if (keySpec == null) {
143d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
144d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            return null;
145d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        }
146e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka        if (hasIcon(keySpec)) {
1479b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            return null;
14823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
149bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final int labelEnd = indexOfLabelEnd(keySpec);
150bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final String label = parseEscape(getBeforeLabelEnd(keySpec, labelEnd));
151e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka        if (label.isEmpty()) {
152e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka            throw new KeySpecParserError("Empty label: " + keySpec);
15323bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
1549b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        return label;
1559b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
1569b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
1579d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nullable
1589d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    private static String getOutputTextInternal(@Nonnull final String keySpec, final int labelEnd) {
159bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (labelEnd <= 0) {
160c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka            return null;
161c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka        }
162bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        checkDoubleLabelEnd(keySpec, labelEnd);
163bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        return parseEscape(getAfterLabelEnd(keySpec, labelEnd));
164c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka    }
165c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka
1669d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    @Nullable
1679d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    public static String getOutputText(@Nullable final String keySpec) {
168d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        if (keySpec == null) {
169d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
170d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            return null;
171d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        }
172bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final int labelEnd = indexOfLabelEnd(keySpec);
173bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (hasCode(keySpec, labelEnd)) {
1749b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka            return null;
17523bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
176bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final String outputText = getOutputTextInternal(keySpec, labelEnd);
177c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka        if (outputText != null) {
178cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka            if (StringUtils.codePointCount(outputText) == 1) {
179c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka                // If output text is one code point, it should be treated as a code.
180c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka                // See {@link #getCode(Resources, String)}.
181c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka                return null;
18223bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka            }
183e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka            if (outputText.isEmpty()) {
184e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka                throw new KeySpecParserError("Empty outputText: " + keySpec);
18523bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka            }
186e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka            return outputText;
1879b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
188e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka        final String label = getLabel(keySpec);
18923bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        if (label == null) {
190e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka            throw new KeySpecParserError("Empty label: " + keySpec);
19123bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
192c4f71668d7b8203dc66f0f04c089a363189eb4ceTadashi G. Takaoka        // Code is automatically generated for one letter label. See {@link getCode()}.
193cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka        return (StringUtils.codePointCount(label) == 1) ? null : label;
1949b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
1959b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
1969d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    public static int getCode(@Nullable final String keySpec) {
197d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        if (keySpec == null) {
198d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
199d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            return CODE_UNSPECIFIED;
200d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        }
201bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final int labelEnd = indexOfLabelEnd(keySpec);
202bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (hasCode(keySpec, labelEnd)) {
203bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            checkDoubleLabelEnd(keySpec, labelEnd);
2047ae6721ffad1e79ee446de87d13f18a27619830bTadashi G. Takaoka            return parseCode(getAfterLabelEnd(keySpec, labelEnd), CODE_UNSPECIFIED);
2059b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka        }
206bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final String outputText = getOutputTextInternal(keySpec, labelEnd);
207c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka        if (outputText != null) {
208c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka            // If output text is one code point, it should be treated as a code.
209c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka            // See {@link #getOutputText(String)}.
210cc8c8b99bd0463f5977dea82f5e2379ea1dd4e73Tadashi G. Takaoka            if (StringUtils.codePointCount(outputText) == 1) {
211c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka                return outputText.codePointAt(0);
212c217dc9237e5d1e1e721b9007139d771dcb41145Tadashi G. Takaoka            }
213240871ecafde7834ebb4270cd7758fc904a5f3a7Tadashi G. Takaoka            return CODE_OUTPUT_TEXT;
21423bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
215e855093f5513e46f7f2da6d99e74873ac4f1eeefTadashi G. Takaoka        final String label = getLabel(keySpec);
216bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (label == null) {
217bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            throw new KeySpecParserError("Empty label: " + keySpec);
21823bacdb6a58cf22535aea8d22d3b6e14ea23667eTadashi G. Takaoka        }
219bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        // Code is automatically generated for one letter label.
220bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        return (StringUtils.codePointCount(label) == 1) ? label.codePointAt(0) : CODE_OUTPUT_TEXT;
2219b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
2229b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
2239d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    public static int parseCode(@Nullable final String text, final int defaultCode) {
224bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (text == null) {
2254a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka            return defaultCode;
226bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        }
2273b4eb03fa171ab42ad4f38abcfa5184c5362e5aeTadashi G. Takaoka        if (text.startsWith(KeyboardCodesSet.PREFIX_CODE)) {
2287ae6721ffad1e79ee446de87d13f18a27619830bTadashi G. Takaoka            return KeyboardCodesSet.getCode(text.substring(KeyboardCodesSet.PREFIX_CODE.length()));
229bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        }
2304a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        // This is a workaround to have a key that has a supplementary code point. We can't put a
2314a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        // string in resource as a XML entity of a supplementary code point or a surrogate pair.
232bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (text.startsWith(PREFIX_HEX)) {
233ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka            return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16);
234ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka        }
2354a64ad9538ed3cfb1c60e2c6ede7368c6d691973Tadashi G. Takaoka        return defaultCode;
236ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka    }
237ed3bac91f242850c6d1833a5f8981b9cc208c5ddTadashi G. Takaoka
2389d5d01a54319797db17fa8ac4b612f8836c83b9aTadashi G. Takaoka    public static int getIconId(@Nullable final String keySpec) {
239d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        if (keySpec == null) {
240d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            // TODO: Throw {@link KeySpecParserError} once Key.keyLabel attribute becomes mandatory.
241d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka            return KeyboardIconsSet.ICON_UNDEFINED;
242d9c6b332090c90e4d4840e62fe3eb45c834b2e14Tadashi G. Takaoka        }
243bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        if (!hasIcon(keySpec)) {
244bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka            return KeyboardIconsSet.ICON_UNDEFINED;
245bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        }
246bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final int labelEnd = indexOfLabelEnd(keySpec);
247bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        final String iconName = getBeforeLabelEnd(keySpec, labelEnd)
248bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka                .substring(KeyboardIconsSet.PREFIX_ICON.length());
249bc9514032abe85ff6c18d2af5802ad0ee11a2241Tadashi G. Takaoka        return KeyboardIconsSet.getIconId(iconName);
2509b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka    }
2519b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka
25215c99e9f1a7776b95325d36cf9e38b6d674e483bTadashi G. Takaoka    @SuppressWarnings("serial")
253a28a05e971cc242b338331a3b78276fa95188d19Tadashi G. Takaoka    public static final class KeySpecParserError extends RuntimeException {
25435ff94547c16c84c5b6fafdae0b4a683be782b97Tadashi G. Takaoka        public KeySpecParserError(final String message) {
25515c99e9f1a7776b95325d36cf9e38b6d674e483bTadashi G. Takaoka            super(message);
2562fe68b9616ebdeb24daf043fbc590ea6a11f10a0Tadashi G. Takaoka        }
2572fe68b9616ebdeb24daf043fbc590ea6a11f10a0Tadashi G. Takaoka    }
2589b6d1d52d91f8f18952ae3841f4bb0d7309bfc0eTadashi G. Takaoka}
259