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