MoreKeySpec.java revision 6c47403e27fd703ece844f4b1b24632721da1772
1/*
2 * Copyright (C) 2012 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.inputmethod.keyboard.internal;
18
19import android.text.TextUtils;
20import android.util.SparseIntArray;
21
22import com.android.inputmethod.compat.CharacterCompat;
23import com.android.inputmethod.keyboard.Key;
24import com.android.inputmethod.latin.common.CollectionUtils;
25import com.android.inputmethod.latin.common.Constants;
26import com.android.inputmethod.latin.common.StringUtils;
27import com.android.inputmethod.latin.define.DebugFlags;
28
29import java.util.ArrayList;
30import java.util.Arrays;
31import java.util.HashSet;
32import java.util.Locale;
33
34import javax.annotation.Nonnull;
35
36/**
37 * The more key specification object. The more keys are an array of {@link MoreKeySpec}.
38 *
39 * The more keys specification is comma separated "key specification" each of which represents one
40 * "more key".
41 * The key specification might have label or string resource reference in it. These references are
42 * expanded before parsing comma.
43 * Special character, comma ',' backslash '\' can be escaped by '\' character.
44 * Note that the '\' is also parsed by XML parser and {@link MoreKeySpec#splitKeySpecs(String)}
45 * as well.
46 */
47// TODO: Should extend the key specification object.
48public final class MoreKeySpec {
49    public final int mCode;
50    public final String mLabel;
51    public final String mOutputText;
52    public final int mIconId;
53
54    public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, final Locale locale) {
55        if (TextUtils.isEmpty(moreKeySpec)) {
56            throw new KeySpecParser.KeySpecParserError("Empty more key spec");
57        }
58        final String label = KeySpecParser.getLabel(moreKeySpec);
59        mLabel = needsToUpperCase ? StringUtils.toTitleCaseOfKeyLabel(label, locale) : label;
60        final int codeInSpec = KeySpecParser.getCode(moreKeySpec);
61        final int code = needsToUpperCase ? StringUtils.toTitleCaseOfKeyCode(codeInSpec, locale)
62                : codeInSpec;
63        if (code == Constants.CODE_UNSPECIFIED) {
64            // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
65            // upper case representation ("SS").
66            mCode = Constants.CODE_OUTPUT_TEXT;
67            mOutputText = mLabel;
68        } else {
69            mCode = code;
70            final String outputText = KeySpecParser.getOutputText(moreKeySpec);
71            mOutputText = needsToUpperCase
72                    ? StringUtils.toTitleCaseOfKeyLabel(outputText, locale) : outputText;
73        }
74        mIconId = KeySpecParser.getIconId(moreKeySpec);
75    }
76
77    @Nonnull
78    public Key buildKey(final int x, final int y, final int labelFlags,
79            final KeyboardParams params) {
80        return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
81                Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
82                params.mHorizontalGap, params.mVerticalGap);
83    }
84
85    @Override
86    public int hashCode() {
87        int hashCode = 1;
88        hashCode = 31 + mCode;
89        hashCode = hashCode * 31 + mIconId;
90        hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
91        hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
92        return hashCode;
93    }
94
95    @Override
96    public boolean equals(final Object o) {
97        if (this == o) return true;
98        if (o instanceof MoreKeySpec) {
99            final MoreKeySpec other = (MoreKeySpec)o;
100            return mCode == other.mCode
101                    && mIconId == other.mIconId
102                    && TextUtils.equals(mLabel, other.mLabel)
103                    && TextUtils.equals(mOutputText, other.mOutputText);
104        }
105        return false;
106    }
107
108    @Override
109    public String toString() {
110        final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
111                : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
112        final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
113                : Constants.printableCode(mCode));
114        if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
115            return output;
116        }
117        return label + "|" + output;
118    }
119
120    public static class LettersOnBaseLayout {
121        private final SparseIntArray mCodes = new SparseIntArray();
122        private final HashSet<String> mTexts = new HashSet<>();
123
124        public void addLetter(final Key key) {
125            final int code = key.getCode();
126            if (CharacterCompat.isAlphabetic(code)) {
127                mCodes.put(code, 0);
128            } else if (code == Constants.CODE_OUTPUT_TEXT) {
129                mTexts.add(key.getOutputText());
130            }
131        }
132
133        public boolean contains(final MoreKeySpec moreKey) {
134            final int code = moreKey.mCode;
135            if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
136                return true;
137            } else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
138                return true;
139            }
140            return false;
141        }
142    }
143
144    public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
145            final LettersOnBaseLayout lettersOnBaseLayout) {
146        if (moreKeys == null) {
147            return null;
148        }
149        final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
150        for (final MoreKeySpec moreKey : moreKeys) {
151            if (!lettersOnBaseLayout.contains(moreKey)) {
152                filteredMoreKeys.add(moreKey);
153            }
154        }
155        final int size = filteredMoreKeys.size();
156        if (size == moreKeys.length) {
157            return moreKeys;
158        }
159        if (size == 0) {
160            return null;
161        }
162        return filteredMoreKeys.toArray(new MoreKeySpec[size]);
163    }
164
165    private static final boolean DEBUG = DebugFlags.DEBUG_ENABLED;
166    // Constants for parsing.
167    private static final char COMMA = Constants.CODE_COMMA;
168    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
169    private static final String ADDITIONAL_MORE_KEY_MARKER =
170            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
171
172    /**
173     * Split the text containing multiple key specifications separated by commas into an array of
174     * key specifications.
175     * A key specification can contain a character escaped by the backslash character, including a
176     * comma character.
177     * Note that an empty key specification will be eliminated from the result array.
178     *
179     * @param text the text containing multiple key specifications.
180     * @return an array of key specification text. Null if the specified <code>text</code> is empty
181     * or has no key specifications.
182     */
183    public static String[] splitKeySpecs(final String text) {
184        if (TextUtils.isEmpty(text)) {
185            return null;
186        }
187        final int size = text.length();
188        // Optimization for one-letter key specification.
189        if (size == 1) {
190            return text.charAt(0) == COMMA ? null : new String[] { text };
191        }
192
193        ArrayList<String> list = null;
194        int start = 0;
195        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
196        // match any high or low surrogate character. So it is OK to iterate through with char
197        // index.
198        for (int pos = 0; pos < size; pos++) {
199            final char c = text.charAt(pos);
200            if (c == COMMA) {
201                // Skip empty entry.
202                if (pos - start > 0) {
203                    if (list == null) {
204                        list = new ArrayList<>();
205                    }
206                    list.add(text.substring(start, pos));
207                }
208                // Skip comma
209                start = pos + 1;
210            } else if (c == BACKSLASH) {
211                // Skip escape character and escaped character.
212                pos++;
213            }
214        }
215        final String remain = (size - start > 0) ? text.substring(start) : null;
216        if (list == null) {
217            return remain != null ? new String[] { remain } : null;
218        }
219        if (remain != null) {
220            list.add(remain);
221        }
222        return list.toArray(new String[list.size()]);
223    }
224
225    private static final String[] EMPTY_STRING_ARRAY = new String[0];
226
227    private static String[] filterOutEmptyString(final String[] array) {
228        if (array == null) {
229            return EMPTY_STRING_ARRAY;
230        }
231        ArrayList<String> out = null;
232        for (int i = 0; i < array.length; i++) {
233            final String entry = array[i];
234            if (TextUtils.isEmpty(entry)) {
235                if (out == null) {
236                    out = CollectionUtils.arrayAsList(array, 0, i);
237                }
238            } else if (out != null) {
239                out.add(entry);
240            }
241        }
242        if (out == null) {
243            return array;
244        }
245        return out.toArray(new String[out.size()]);
246    }
247
248    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
249            final String[] additionalMoreKeySpecs) {
250        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
251        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
252        final int moreKeysCount = moreKeys.length;
253        final int additionalCount = additionalMoreKeys.length;
254        ArrayList<String> out = null;
255        int additionalIndex = 0;
256        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
257            final String moreKeySpec = moreKeys[moreKeyIndex];
258            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
259                if (additionalIndex < additionalCount) {
260                    // Replace '%' marker with additional more key specification.
261                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
262                    if (out != null) {
263                        out.add(additionalMoreKey);
264                    } else {
265                        moreKeys[moreKeyIndex] = additionalMoreKey;
266                    }
267                    additionalIndex++;
268                } else {
269                    // Filter out excessive '%' marker.
270                    if (out == null) {
271                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
272                    }
273                }
274            } else {
275                if (out != null) {
276                    out.add(moreKeySpec);
277                }
278            }
279        }
280        if (additionalCount > 0 && additionalIndex == 0) {
281            // No '%' marker is found in more keys.
282            // Insert all additional more keys to the head of more keys.
283            if (DEBUG && out != null) {
284                throw new RuntimeException("Internal logic error:"
285                        + " moreKeys=" + Arrays.toString(moreKeys)
286                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
287            }
288            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
289            for (int i = 0; i < moreKeysCount; i++) {
290                out.add(moreKeys[i]);
291            }
292        } else if (additionalIndex < additionalCount) {
293            // The number of '%' markers are less than additional more keys.
294            // Append remained additional more keys to the tail of more keys.
295            if (DEBUG && out != null) {
296                throw new RuntimeException("Internal logic error:"
297                        + " moreKeys=" + Arrays.toString(moreKeys)
298                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
299            }
300            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
301            for (int i = additionalIndex; i < additionalCount; i++) {
302                out.add(additionalMoreKeys[additionalIndex]);
303            }
304        }
305        if (out == null && moreKeysCount > 0) {
306            return moreKeys;
307        } else if (out != null && out.size() > 0) {
308            return out.toArray(new String[out.size()]);
309        } else {
310            return null;
311        }
312    }
313
314    public static int getIntValue(final String[] moreKeys, final String key,
315            final int defaultValue) {
316        if (moreKeys == null) {
317            return defaultValue;
318        }
319        final int keyLen = key.length();
320        boolean foundValue = false;
321        int value = defaultValue;
322        for (int i = 0; i < moreKeys.length; i++) {
323            final String moreKeySpec = moreKeys[i];
324            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
325                continue;
326            }
327            moreKeys[i] = null;
328            try {
329                if (!foundValue) {
330                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
331                    foundValue = true;
332                }
333            } catch (NumberFormatException e) {
334                throw new RuntimeException(
335                        "integer should follow after " + key + ": " + moreKeySpec);
336            }
337        }
338        return value;
339    }
340
341    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
342        if (moreKeys == null) {
343            return false;
344        }
345        boolean value = false;
346        for (int i = 0; i < moreKeys.length; i++) {
347            final String moreKeySpec = moreKeys[i];
348            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
349                continue;
350            }
351            moreKeys[i] = null;
352            value = true;
353        }
354        return value;
355    }
356}
357