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 import android.util.SparseIntArray;
21 
22 import com.android.inputmethod.compat.CharacterCompat;
23 import com.android.inputmethod.keyboard.Key;
24 import com.android.inputmethod.latin.common.CollectionUtils;
25 import com.android.inputmethod.latin.common.Constants;
26 import com.android.inputmethod.latin.common.StringUtils;
27 
28 import java.util.ArrayList;
29 import java.util.HashSet;
30 import java.util.Locale;
31 
32 import javax.annotation.Nonnull;
33 import 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.
47 public 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 
MoreKeySpec(@onnull final String moreKeySpec, boolean needsToUpperCase, @Nonnull final Locale locale)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
buildKey(final int x, final int y, final int labelFlags, @Nonnull final KeyboardParams params)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
hashCode()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
equals(final Object o)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
toString()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 
addLetter(@onnull final Key key)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 
contains(@onnull final MoreKeySpec moreKey)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
removeRedundantMoreKeys(@ullable final MoreKeySpec[] moreKeys, @Nonnull final LettersOnBaseLayout lettersOnBaseLayout)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
splitKeySpecs(@ullable final String text)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
filterOutEmptyString(@ullable final String[] array)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 
insertAdditionalMoreKeys(@ullable final String[] moreKeySpecs, @Nullable final String[] additionalMoreKeySpecs)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 
getIntValue(@ullable final String[] moreKeys, final String key, final int defaultValue)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 
getBooleanValue(@ullable final String[] moreKeys, final String key)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