1 /* 2 * Copyright (C) 2010 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you may not 5 * use this file except in compliance with the License. You may obtain a copy of 6 * 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, WITHOUT 12 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 13 * License for the specific language governing permissions and limitations under 14 * the License. 15 */ 16 17 package com.android.inputmethod.keyboard.internal; 18 19 import static com.android.inputmethod.keyboard.Keyboard.CODE_UNSPECIFIED; 20 21 import android.text.TextUtils; 22 23 import com.android.inputmethod.keyboard.Keyboard; 24 import com.android.inputmethod.latin.LatinImeLogger; 25 import com.android.inputmethod.latin.StringUtils; 26 27 import java.util.ArrayList; 28 import java.util.Arrays; 29 import java.util.Locale; 30 31 /** 32 * The string parser of more keys specification. 33 * The specification is comma separated texts each of which represents one "more key". 34 * The specification might have label or string resource reference in it. These references are 35 * expanded before parsing comma. 36 * - Label reference should be a string representation of label (!text/label_name) 37 * - String resource reference should be a string representation of resource (!text/resource_name) 38 * Each "more key" specification is one of the following: 39 * - Label optionally followed by keyOutputText or code (keyLabel|keyOutputText). 40 * - Icon followed by keyOutputText or code (!icon/icon_name|!code/code_name) 41 * - Icon should be a string representation of icon (!icon/icon_name). 42 * - Code should be a code point presented by hexadecimal string prefixed with "0x", or a string 43 * representation of code (!code/code_name). 44 * Special character, comma ',' backslash '\', and bar '|' can be escaped by '\' character. 45 * Note that the '\' is also parsed by XML parser and CSV parser as well. 46 * See {@link KeyboardIconsSet} about icon_name. 47 */ 48 public class KeySpecParser { 49 private static final boolean DEBUG = LatinImeLogger.sDBG; 50 51 private static final int MAX_STRING_REFERENCE_INDIRECTION = 10; 52 53 // Constants for parsing. 54 private static int COMMA = ','; 55 private static final char ESCAPE_CHAR = '\\'; 56 private static final char LABEL_END = '|'; 57 private static final String PREFIX_TEXT = "!text/"; 58 private static final String PREFIX_ICON = "!icon/"; 59 private static final String PREFIX_CODE = "!code/"; 60 private static final String PREFIX_HEX = "0x"; 61 private static final String ADDITIONAL_MORE_KEY_MARKER = "%"; 62 63 public static class MoreKeySpec { 64 public final int mCode; 65 public final String mLabel; 66 public final String mOutputText; 67 public final int mIconId; 68 MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale, final KeyboardCodesSet codesSet)69 public MoreKeySpec(final String moreKeySpec, boolean needsToUpperCase, Locale locale, 70 final KeyboardCodesSet codesSet) { 71 mCode = toUpperCaseOfCodeForLocale(getCode(moreKeySpec, codesSet), 72 needsToUpperCase, locale); 73 mLabel = toUpperCaseOfStringForLocale(getLabel(moreKeySpec), 74 needsToUpperCase, locale); 75 mOutputText = toUpperCaseOfStringForLocale(getOutputText(moreKeySpec), 76 needsToUpperCase, locale); 77 mIconId = getIconId(moreKeySpec); 78 } 79 } 80 KeySpecParser()81 private KeySpecParser() { 82 // Intentional empty constructor for utility class. 83 } 84 hasIcon(String moreKeySpec)85 private static boolean hasIcon(String moreKeySpec) { 86 return moreKeySpec.startsWith(PREFIX_ICON); 87 } 88 hasCode(String moreKeySpec)89 private static boolean hasCode(String moreKeySpec) { 90 final int end = indexOfLabelEnd(moreKeySpec, 0); 91 if (end > 0 && end + 1 < moreKeySpec.length() && moreKeySpec.startsWith( 92 PREFIX_CODE, end + 1)) { 93 return true; 94 } 95 return false; 96 } 97 parseEscape(String text)98 private static String parseEscape(String text) { 99 if (text.indexOf(ESCAPE_CHAR) < 0) { 100 return text; 101 } 102 final int length = text.length(); 103 final StringBuilder sb = new StringBuilder(); 104 for (int pos = 0; pos < length; pos++) { 105 final char c = text.charAt(pos); 106 if (c == ESCAPE_CHAR && pos + 1 < length) { 107 // Skip escape char 108 pos++; 109 sb.append(text.charAt(pos)); 110 } else { 111 sb.append(c); 112 } 113 } 114 return sb.toString(); 115 } 116 indexOfLabelEnd(String moreKeySpec, int start)117 private static int indexOfLabelEnd(String moreKeySpec, int start) { 118 if (moreKeySpec.indexOf(ESCAPE_CHAR, start) < 0) { 119 final int end = moreKeySpec.indexOf(LABEL_END, start); 120 if (end == 0) { 121 throw new KeySpecParserError(LABEL_END + " at " + start + ": " + moreKeySpec); 122 } 123 return end; 124 } 125 final int length = moreKeySpec.length(); 126 for (int pos = start; pos < length; pos++) { 127 final char c = moreKeySpec.charAt(pos); 128 if (c == ESCAPE_CHAR && pos + 1 < length) { 129 // Skip escape char 130 pos++; 131 } else if (c == LABEL_END) { 132 return pos; 133 } 134 } 135 return -1; 136 } 137 getLabel(String moreKeySpec)138 public static String getLabel(String moreKeySpec) { 139 if (hasIcon(moreKeySpec)) { 140 return null; 141 } 142 final int end = indexOfLabelEnd(moreKeySpec, 0); 143 final String label = (end > 0) ? parseEscape(moreKeySpec.substring(0, end)) 144 : parseEscape(moreKeySpec); 145 if (TextUtils.isEmpty(label)) { 146 throw new KeySpecParserError("Empty label: " + moreKeySpec); 147 } 148 return label; 149 } 150 getOutputTextInternal(String moreKeySpec)151 private static String getOutputTextInternal(String moreKeySpec) { 152 final int end = indexOfLabelEnd(moreKeySpec, 0); 153 if (end <= 0) { 154 return null; 155 } 156 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 157 throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); 158 } 159 return parseEscape(moreKeySpec.substring(end + /* LABEL_END */1)); 160 } 161 getOutputText(String moreKeySpec)162 static String getOutputText(String moreKeySpec) { 163 if (hasCode(moreKeySpec)) { 164 return null; 165 } 166 final String outputText = getOutputTextInternal(moreKeySpec); 167 if (outputText != null) { 168 if (StringUtils.codePointCount(outputText) == 1) { 169 // If output text is one code point, it should be treated as a code. 170 // See {@link #getCode(Resources, String)}. 171 return null; 172 } 173 if (!TextUtils.isEmpty(outputText)) { 174 return outputText; 175 } 176 throw new KeySpecParserError("Empty outputText: " + moreKeySpec); 177 } 178 final String label = getLabel(moreKeySpec); 179 if (label == null) { 180 throw new KeySpecParserError("Empty label: " + moreKeySpec); 181 } 182 // Code is automatically generated for one letter label. See {@link getCode()}. 183 return (StringUtils.codePointCount(label) == 1) ? null : label; 184 } 185 getCode(String moreKeySpec, KeyboardCodesSet codesSet)186 static int getCode(String moreKeySpec, KeyboardCodesSet codesSet) { 187 if (hasCode(moreKeySpec)) { 188 final int end = indexOfLabelEnd(moreKeySpec, 0); 189 if (indexOfLabelEnd(moreKeySpec, end + 1) >= 0) { 190 throw new KeySpecParserError("Multiple " + LABEL_END + ": " + moreKeySpec); 191 } 192 return parseCode(moreKeySpec.substring(end + 1), codesSet, Keyboard.CODE_UNSPECIFIED); 193 } 194 final String outputText = getOutputTextInternal(moreKeySpec); 195 if (outputText != null) { 196 // If output text is one code point, it should be treated as a code. 197 // See {@link #getOutputText(String)}. 198 if (StringUtils.codePointCount(outputText) == 1) { 199 return outputText.codePointAt(0); 200 } 201 return Keyboard.CODE_OUTPUT_TEXT; 202 } 203 final String label = getLabel(moreKeySpec); 204 // Code is automatically generated for one letter label. 205 if (StringUtils.codePointCount(label) == 1) { 206 return label.codePointAt(0); 207 } 208 return Keyboard.CODE_OUTPUT_TEXT; 209 } 210 parseCode(String text, KeyboardCodesSet codesSet, int defCode)211 public static int parseCode(String text, KeyboardCodesSet codesSet, int defCode) { 212 if (text == null) return defCode; 213 if (text.startsWith(PREFIX_CODE)) { 214 return codesSet.getCode(text.substring(PREFIX_CODE.length())); 215 } else if (text.startsWith(PREFIX_HEX)) { 216 return Integer.parseInt(text.substring(PREFIX_HEX.length()), 16); 217 } else { 218 return Integer.parseInt(text); 219 } 220 } 221 getIconId(String moreKeySpec)222 public static int getIconId(String moreKeySpec) { 223 if (moreKeySpec != null && hasIcon(moreKeySpec)) { 224 final int end = moreKeySpec.indexOf(LABEL_END, PREFIX_ICON.length()); 225 final String name = (end < 0) ? moreKeySpec.substring(PREFIX_ICON.length()) 226 : moreKeySpec.substring(PREFIX_ICON.length(), end); 227 return KeyboardIconsSet.getIconId(name); 228 } 229 return KeyboardIconsSet.ICON_UNDEFINED; 230 } 231 arrayAsList(T[] array, int start, int end)232 private static <T> ArrayList<T> arrayAsList(T[] array, int start, int end) { 233 if (array == null) { 234 throw new NullPointerException(); 235 } 236 if (start < 0 || start > end || end > array.length) { 237 throw new IllegalArgumentException(); 238 } 239 240 final ArrayList<T> list = new ArrayList<T>(end - start); 241 for (int i = start; i < end; i++) { 242 list.add(array[i]); 243 } 244 return list; 245 } 246 247 private static final String[] EMPTY_STRING_ARRAY = new String[0]; 248 filterOutEmptyString(String[] array)249 private static String[] filterOutEmptyString(String[] array) { 250 if (array == null) { 251 return EMPTY_STRING_ARRAY; 252 } 253 ArrayList<String> out = null; 254 for (int i = 0; i < array.length; i++) { 255 final String entry = array[i]; 256 if (TextUtils.isEmpty(entry)) { 257 if (out == null) { 258 out = arrayAsList(array, 0, i); 259 } 260 } else if (out != null) { 261 out.add(entry); 262 } 263 } 264 if (out == null) { 265 return array; 266 } 267 return out.toArray(new String[out.size()]); 268 } 269 insertAdditionalMoreKeys(String[] moreKeySpecs, String[] additionalMoreKeySpecs)270 public static String[] insertAdditionalMoreKeys(String[] moreKeySpecs, 271 String[] additionalMoreKeySpecs) { 272 final String[] moreKeys = filterOutEmptyString(moreKeySpecs); 273 final String[] additionalMoreKeys = filterOutEmptyString(additionalMoreKeySpecs); 274 final int moreKeysCount = moreKeys.length; 275 final int additionalCount = additionalMoreKeys.length; 276 ArrayList<String> out = null; 277 int additionalIndex = 0; 278 for (int moreKeyIndex = 0; moreKeyIndex < moreKeysCount; moreKeyIndex++) { 279 final String moreKeySpec = moreKeys[moreKeyIndex]; 280 if (moreKeySpec.equals(ADDITIONAL_MORE_KEY_MARKER)) { 281 if (additionalIndex < additionalCount) { 282 // Replace '%' marker with additional more key specification. 283 final String additionalMoreKey = additionalMoreKeys[additionalIndex]; 284 if (out != null) { 285 out.add(additionalMoreKey); 286 } else { 287 moreKeys[moreKeyIndex] = additionalMoreKey; 288 } 289 additionalIndex++; 290 } else { 291 // Filter out excessive '%' marker. 292 if (out == null) { 293 out = arrayAsList(moreKeys, 0, moreKeyIndex); 294 } 295 } 296 } else { 297 if (out != null) { 298 out.add(moreKeySpec); 299 } 300 } 301 } 302 if (additionalCount > 0 && additionalIndex == 0) { 303 // No '%' marker is found in more keys. 304 // Insert all additional more keys to the head of more keys. 305 if (DEBUG && out != null) { 306 throw new RuntimeException("Internal logic error:" 307 + " moreKeys=" + Arrays.toString(moreKeys) 308 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 309 } 310 out = arrayAsList(additionalMoreKeys, additionalIndex, additionalCount); 311 for (int i = 0; i < moreKeysCount; i++) { 312 out.add(moreKeys[i]); 313 } 314 } else if (additionalIndex < additionalCount) { 315 // The number of '%' markers are less than additional more keys. 316 // Append remained additional more keys to the tail of more keys. 317 if (DEBUG && out != null) { 318 throw new RuntimeException("Internal logic error:" 319 + " moreKeys=" + Arrays.toString(moreKeys) 320 + " additionalMoreKeys=" + Arrays.toString(additionalMoreKeys)); 321 } 322 out = arrayAsList(moreKeys, 0, moreKeysCount); 323 for (int i = additionalIndex; i < additionalCount; i++) { 324 out.add(additionalMoreKeys[additionalIndex]); 325 } 326 } 327 if (out == null && moreKeysCount > 0) { 328 return moreKeys; 329 } else if (out != null && out.size() > 0) { 330 return out.toArray(new String[out.size()]); 331 } else { 332 return null; 333 } 334 } 335 336 @SuppressWarnings("serial") 337 public static class KeySpecParserError extends RuntimeException { KeySpecParserError(String message)338 public KeySpecParserError(String message) { 339 super(message); 340 } 341 } 342 resolveTextReference(String rawText, KeyboardTextsSet textsSet)343 public static String resolveTextReference(String rawText, KeyboardTextsSet textsSet) { 344 int level = 0; 345 String text = rawText; 346 StringBuilder sb; 347 do { 348 level++; 349 if (level >= MAX_STRING_REFERENCE_INDIRECTION) { 350 throw new RuntimeException("too many @string/resource indirection: " + text); 351 } 352 353 final int prefixLen = PREFIX_TEXT.length(); 354 final int size = text.length(); 355 if (size < prefixLen) { 356 return text; 357 } 358 359 sb = null; 360 for (int pos = 0; pos < size; pos++) { 361 final char c = text.charAt(pos); 362 if (text.startsWith(PREFIX_TEXT, pos) && textsSet != null) { 363 if (sb == null) { 364 sb = new StringBuilder(text.substring(0, pos)); 365 } 366 final int end = searchTextNameEnd(text, pos + prefixLen); 367 final String name = text.substring(pos + prefixLen, end); 368 sb.append(textsSet.getText(name)); 369 pos = end - 1; 370 } else if (c == ESCAPE_CHAR) { 371 if (sb != null) { 372 // Append both escape character and escaped character. 373 sb.append(text.substring(pos, Math.min(pos + 2, size))); 374 } 375 pos++; 376 } else if (sb != null) { 377 sb.append(c); 378 } 379 } 380 381 if (sb != null) { 382 text = sb.toString(); 383 } 384 } while (sb != null); 385 386 return text; 387 } 388 searchTextNameEnd(String text, int start)389 private static int searchTextNameEnd(String text, int start) { 390 final int size = text.length(); 391 for (int pos = start; pos < size; pos++) { 392 final char c = text.charAt(pos); 393 // Label name should be consisted of [a-zA-Z_0-9]. 394 if ((c >= 'a' && c <= 'z') || c == '_' || (c >= '0' && c <= '9')) { 395 continue; 396 } 397 return pos; 398 } 399 return size; 400 } 401 parseCsvString(String rawText, KeyboardTextsSet textsSet)402 public static String[] parseCsvString(String rawText, KeyboardTextsSet textsSet) { 403 final String text = resolveTextReference(rawText, textsSet); 404 final int size = text.length(); 405 if (size == 0) { 406 return null; 407 } 408 if (StringUtils.codePointCount(text) == 1) { 409 return text.codePointAt(0) == COMMA ? null : new String[] { text }; 410 } 411 412 ArrayList<String> list = null; 413 int start = 0; 414 for (int pos = 0; pos < size; pos++) { 415 final char c = text.charAt(pos); 416 if (c == COMMA) { 417 // Skip empty entry. 418 if (pos - start > 0) { 419 if (list == null) { 420 list = new ArrayList<String>(); 421 } 422 list.add(text.substring(start, pos)); 423 } 424 // Skip comma 425 start = pos + 1; 426 } else if (c == ESCAPE_CHAR) { 427 // Skip escape character and escaped character. 428 pos++; 429 } 430 } 431 final String remain = (size - start > 0) ? text.substring(start) : null; 432 if (list == null) { 433 return remain != null ? new String[] { remain } : null; 434 } 435 if (remain != null) { 436 list.add(remain); 437 } 438 return list.toArray(new String[list.size()]); 439 } 440 getIntValue(String[] moreKeys, String key, int defaultValue)441 public static int getIntValue(String[] moreKeys, String key, int defaultValue) { 442 if (moreKeys == null) { 443 return defaultValue; 444 } 445 final int keyLen = key.length(); 446 boolean foundValue = false; 447 int value = defaultValue; 448 for (int i = 0; i < moreKeys.length; i++) { 449 final String moreKeySpec = moreKeys[i]; 450 if (moreKeySpec == null || !moreKeySpec.startsWith(key)) { 451 continue; 452 } 453 moreKeys[i] = null; 454 try { 455 if (!foundValue) { 456 value = Integer.parseInt(moreKeySpec.substring(keyLen)); 457 foundValue = true; 458 } 459 } catch (NumberFormatException e) { 460 throw new RuntimeException( 461 "integer should follow after " + key + ": " + moreKeySpec); 462 } 463 } 464 return value; 465 } 466 getBooleanValue(String[] moreKeys, String key)467 public static boolean getBooleanValue(String[] moreKeys, String key) { 468 if (moreKeys == null) { 469 return false; 470 } 471 boolean value = false; 472 for (int i = 0; i < moreKeys.length; i++) { 473 final String moreKeySpec = moreKeys[i]; 474 if (moreKeySpec == null || !moreKeySpec.equals(key)) { 475 continue; 476 } 477 moreKeys[i] = null; 478 value = true; 479 } 480 return value; 481 } 482 toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase, Locale locale)483 public static int toUpperCaseOfCodeForLocale(int code, boolean needsToUpperCase, 484 Locale locale) { 485 if (!Keyboard.isLetterCode(code) || !needsToUpperCase) return code; 486 final String text = new String(new int[] { code } , 0, 1); 487 final String casedText = KeySpecParser.toUpperCaseOfStringForLocale( 488 text, needsToUpperCase, locale); 489 return StringUtils.codePointCount(casedText) == 1 490 ? casedText.codePointAt(0) : CODE_UNSPECIFIED; 491 } 492 toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase, Locale locale)493 public static String toUpperCaseOfStringForLocale(String text, boolean needsToUpperCase, 494 Locale locale) { 495 if (text == null || !needsToUpperCase) return text; 496 return text.toUpperCase(locale); 497 } 498 } 499