MoreKeySpec.java revision 5b91b551e5ffaf2c2e691dfbd434f21c82293986
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        mLabel = StringUtils.toUpperCaseOfStringForLocale(
59                KeySpecParser.getLabel(moreKeySpec), needsToUpperCase, locale);
60        final int code = StringUtils.toUpperCaseOfCodeForLocale(
61                KeySpecParser.getCode(moreKeySpec), needsToUpperCase, locale);
62        if (code == Constants.CODE_UNSPECIFIED) {
63            // Some letter, for example German Eszett (U+00DF: "ß"), has multiple characters
64            // upper case representation ("SS").
65            mCode = Constants.CODE_OUTPUT_TEXT;
66            mOutputText = mLabel;
67        } else {
68            mCode = code;
69            mOutputText = StringUtils.toUpperCaseOfStringForLocale(
70                    KeySpecParser.getOutputText(moreKeySpec), needsToUpperCase, locale);
71        }
72        mIconId = KeySpecParser.getIconId(moreKeySpec);
73    }
74
75    @Nonnull
76    public Key buildKey(final int x, final int y, final int labelFlags,
77            final KeyboardParams params) {
78        return new Key(mLabel, mIconId, mCode, mOutputText, null /* hintLabel */, labelFlags,
79                Key.BACKGROUND_TYPE_NORMAL, x, y, params.mDefaultKeyWidth, params.mDefaultRowHeight,
80                params.mHorizontalGap, params.mVerticalGap);
81    }
82
83    @Override
84    public int hashCode() {
85        int hashCode = 1;
86        hashCode = 31 + mCode;
87        hashCode = hashCode * 31 + mIconId;
88        hashCode = hashCode * 31 + (mLabel == null ? 0 : mLabel.hashCode());
89        hashCode = hashCode * 31 + (mOutputText == null ? 0 : mOutputText.hashCode());
90        return hashCode;
91    }
92
93    @Override
94    public boolean equals(final Object o) {
95        if (this == o) return true;
96        if (o instanceof MoreKeySpec) {
97            final MoreKeySpec other = (MoreKeySpec)o;
98            return mCode == other.mCode
99                    && mIconId == other.mIconId
100                    && TextUtils.equals(mLabel, other.mLabel)
101                    && TextUtils.equals(mOutputText, other.mOutputText);
102        }
103        return false;
104    }
105
106    @Override
107    public String toString() {
108        final String label = (mIconId == KeyboardIconsSet.ICON_UNDEFINED ? mLabel
109                : KeyboardIconsSet.PREFIX_ICON + KeyboardIconsSet.getIconName(mIconId));
110        final String output = (mCode == Constants.CODE_OUTPUT_TEXT ? mOutputText
111                : Constants.printableCode(mCode));
112        if (StringUtils.codePointCount(label) == 1 && label.codePointAt(0) == mCode) {
113            return output;
114        }
115        return label + "|" + output;
116    }
117
118    public static class LettersOnBaseLayout {
119        private final SparseIntArray mCodes = new SparseIntArray();
120        private final HashSet<String> mTexts = new HashSet<>();
121
122        public void addLetter(final Key key) {
123            final int code = key.getCode();
124            if (CharacterCompat.isAlphabetic(code)) {
125                mCodes.put(code, 0);
126            } else if (code == Constants.CODE_OUTPUT_TEXT) {
127                mTexts.add(key.getOutputText());
128            }
129        }
130
131        public boolean contains(final MoreKeySpec moreKey) {
132            final int code = moreKey.mCode;
133            if (CharacterCompat.isAlphabetic(code) && mCodes.indexOfKey(code) >= 0) {
134                return true;
135            } else if (code == Constants.CODE_OUTPUT_TEXT && mTexts.contains(moreKey.mOutputText)) {
136                return true;
137            }
138            return false;
139        }
140    }
141
142    public static MoreKeySpec[] removeRedundantMoreKeys(final MoreKeySpec[] moreKeys,
143            final LettersOnBaseLayout lettersOnBaseLayout) {
144        if (moreKeys == null) {
145            return null;
146        }
147        final ArrayList<MoreKeySpec> filteredMoreKeys = new ArrayList<>();
148        for (final MoreKeySpec moreKey : moreKeys) {
149            if (!lettersOnBaseLayout.contains(moreKey)) {
150                filteredMoreKeys.add(moreKey);
151            }
152        }
153        final int size = filteredMoreKeys.size();
154        if (size == moreKeys.length) {
155            return moreKeys;
156        }
157        if (size == 0) {
158            return null;
159        }
160        return filteredMoreKeys.toArray(new MoreKeySpec[size]);
161    }
162
163    private static final boolean DEBUG = DebugFlags.DEBUG_ENABLED;
164    // Constants for parsing.
165    private static final char COMMA = Constants.CODE_COMMA;
166    private static final char BACKSLASH = Constants.CODE_BACKSLASH;
167    private static final String ADDITIONAL_MORE_KEY_MARKER =
168            StringUtils.newSingleCodePointString(Constants.CODE_PERCENT);
169
170    /**
171     * Split the text containing multiple key specifications separated by commas into an array of
172     * key specifications.
173     * A key specification can contain a character escaped by the backslash character, including a
174     * comma character.
175     * Note that an empty key specification will be eliminated from the result array.
176     *
177     * @param text the text containing multiple key specifications.
178     * @return an array of key specification text. Null if the specified <code>text</code> is empty
179     * or has no key specifications.
180     */
181    public static String[] splitKeySpecs(final String text) {
182        if (TextUtils.isEmpty(text)) {
183            return null;
184        }
185        final int size = text.length();
186        // Optimization for one-letter key specification.
187        if (size == 1) {
188            return text.charAt(0) == COMMA ? null : new String[] { text };
189        }
190
191        ArrayList<String> list = null;
192        int start = 0;
193        // The characters in question in this loop are COMMA and BACKSLASH. These characters never
194        // match any high or low surrogate character. So it is OK to iterate through with char
195        // index.
196        for (int pos = 0; pos < size; pos++) {
197            final char c = text.charAt(pos);
198            if (c == COMMA) {
199                // Skip empty entry.
200                if (pos - start > 0) {
201                    if (list == null) {
202                        list = new ArrayList<>();
203                    }
204                    list.add(text.substring(start, pos));
205                }
206                // Skip comma
207                start = pos + 1;
208            } else if (c == BACKSLASH) {
209                // Skip escape character and escaped character.
210                pos++;
211            }
212        }
213        final String remain = (size - start > 0) ? text.substring(start) : null;
214        if (list == null) {
215            return remain != null ? new String[] { remain } : null;
216        }
217        if (remain != null) {
218            list.add(remain);
219        }
220        return list.toArray(new String[list.size()]);
221    }
222
223    private static final String[] EMPTY_STRING_ARRAY = new String[0];
224
225    private static String[] filterOutEmptyString(final String[] array) {
226        if (array == null) {
227            return EMPTY_STRING_ARRAY;
228        }
229        ArrayList<String> out = null;
230        for (int i = 0; i < array.length; i++) {
231            final String entry = array[i];
232            if (TextUtils.isEmpty(entry)) {
233                if (out == null) {
234                    out = CollectionUtils.arrayAsList(array, 0, i);
235                }
236            } else if (out != null) {
237                out.add(entry);
238            }
239        }
240        if (out == null) {
241            return array;
242        }
243        return out.toArray(new String[out.size()]);
244    }
245
246    public static String[] insertAdditionalMoreKeys(final String[] moreKeySpecs,
247            final String[] additionalMoreKeySpecs) {
248        final String[] moreKeys = filterOutEmptyString(moreKeySpecs);
249        final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs);
250        final int moreKeysCount = moreKeys.length;
251        final int additionalCount = additionalMoreKeys.length;
252        ArrayList<String> out = null;
253        int additionalIndex = 0;
254        for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) {
255            final String moreKeySpec = moreKeys[moreKeyIndex];
256            if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) {
257                if (additionalIndex < additionalCount) {
258                    // Replace '%' marker with additional more key specification.
259                    final String additionalMoreKey = additionalMoreKeys[additionalIndex];
260                    if (out != null) {
261                        out.add(additionalMoreKey);
262                    } else {
263                        moreKeys[moreKeyIndex] = additionalMoreKey;
264                    }
265                    additionalIndex++;
266                } else {
267                    // Filter out excessive '%' marker.
268                    if (out == null) {
269                        out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeyIndex);
270                    }
271                }
272            } else {
273                if (out != null) {
274                    out.add(moreKeySpec);
275                }
276            }
277        }
278        if (additionalCount > 0 && additionalIndex == 0) {
279            // No '%' marker is found in more keys.
280            // Insert all additional more keys to the head of more keys.
281            if (DEBUG && out != null) {
282                throw new RuntimeException("Internal logic error:"
283                        + " moreKeys=" + Arrays.toString(moreKeys)
284                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
285            }
286            out = CollectionUtils.arrayAsList(additionalMoreKeys, additionalIndex, additionalCount);
287            for (int i = 0; i < moreKeysCount; i++) {
288                out.add(moreKeys[i]);
289            }
290        } else if (additionalIndex < additionalCount) {
291            // The number of '%' markers are less than additional more keys.
292            // Append remained additional more keys to the tail of more keys.
293            if (DEBUG && out != null) {
294                throw new RuntimeException("Internal logic error:"
295                        + " moreKeys=" + Arrays.toString(moreKeys)
296                        + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys));
297            }
298            out = CollectionUtils.arrayAsList(moreKeys, 0, moreKeysCount);
299            for (int i = additionalIndex; i < additionalCount; i++) {
300                out.add(additionalMoreKeys[additionalIndex]);
301            }
302        }
303        if (out == null && moreKeysCount > 0) {
304            return moreKeys;
305        } else if (out != null && out.size() > 0) {
306            return out.toArray(new String[out.size()]);
307        } else {
308            return null;
309        }
310    }
311
312    public static int getIntValue(final String[] moreKeys, final String key,
313            final int defaultValue) {
314        if (moreKeys == null) {
315            return defaultValue;
316        }
317        final int keyLen = key.length();
318        boolean foundValue = false;
319        int value = defaultValue;
320        for (int i = 0; i < moreKeys.length; i++) {
321            final String moreKeySpec = moreKeys[i];
322            if (moreKeySpec == null || !moreKeySpec.startsWith(key)) {
323                continue;
324            }
325            moreKeys[i] = null;
326            try {
327                if (!foundValue) {
328                    value = Integer.parseInt(moreKeySpec.substring(keyLen));
329                    foundValue = true;
330                }
331            } catch (NumberFormatException e) {
332                throw new RuntimeException(
333                        "integer should follow after " + key + ": " + moreKeySpec);
334            }
335        }
336        return value;
337    }
338
339    public static boolean getBooleanValue(final String[] moreKeys, final String key) {
340        if (moreKeys == null) {
341            return false;
342        }
343        boolean value = false;
344        for (int i = 0; i < moreKeys.length; i++) {
345            final String moreKeySpec = moreKeys[i];
346            if (moreKeySpec == null || !moreKeySpec.equals(key)) {
347                continue;
348            }
349            moreKeys[i] = null;
350            value = true;
351        }
352        return value;
353    }
354}
355