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